includio-cms 0.25.0 → 0.26.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 (349) hide show
  1. package/API.md +57 -4
  2. package/CHANGELOG.md +53 -0
  3. package/DOCS.md +1 -1
  4. package/README.md +2 -0
  5. package/ROADMAP.md +6 -0
  6. package/dist/admin/client/account/lang.d.ts +1 -0
  7. package/dist/admin/client/account/lang.js +4 -2
  8. package/dist/admin/client/account/profile-section.svelte +2 -2
  9. package/dist/admin/client/account/security-section.svelte +27 -4
  10. package/dist/admin/client/account/sessions-section.svelte +1 -1
  11. package/dist/admin/client/admin/admin-after-login-layout-content.svelte +1 -1
  12. package/dist/admin/client/admin/dashboard-page.svelte +34 -10
  13. package/dist/admin/client/collection/bulk-actions-bar.svelte +86 -44
  14. package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +3 -1
  15. package/dist/admin/client/collection/collection-entries.svelte +52 -36
  16. package/dist/admin/client/collection/collection-entries.svelte.d.ts +3 -0
  17. package/dist/admin/client/collection/collection.svelte +28 -14
  18. package/dist/admin/client/collection/collection.svelte.d.ts +3 -0
  19. package/dist/admin/client/collection/data-table.svelte +279 -130
  20. package/dist/admin/client/collection/data-table.svelte.d.ts +11 -0
  21. package/dist/admin/client/collection/date-cell.svelte +4 -4
  22. package/dist/admin/client/collection/row-actions.svelte +2 -1
  23. package/dist/admin/client/collection/sortable-header.svelte +33 -9
  24. package/dist/admin/client/collection/state-display.svelte +102 -0
  25. package/dist/admin/client/collection/state-display.svelte.d.ts +12 -0
  26. package/dist/admin/client/collection/status-badge.svelte +99 -11
  27. package/dist/admin/client/collection/status-badge.svelte.d.ts +15 -1
  28. package/dist/admin/client/collection/table-pagination.svelte +21 -6
  29. package/dist/admin/client/collection/table-toolbar.svelte +105 -80
  30. package/dist/admin/client/collection/table-toolbar.svelte.d.ts +11 -8
  31. package/dist/admin/client/entry/entry-form.svelte +36 -11
  32. package/dist/admin/client/entry/entry-form.svelte.d.ts +1 -0
  33. package/dist/admin/client/entry/entry-header.svelte +22 -15
  34. package/dist/admin/client/entry/entry-header.svelte.d.ts +1 -0
  35. package/dist/admin/client/entry/entry.svelte +269 -165
  36. package/dist/admin/client/entry/header/a11y-header-badge.svelte +47 -0
  37. package/dist/admin/client/entry/header/a11y-header-badge.svelte.d.ts +8 -0
  38. package/dist/admin/client/entry/header/publish-panel.svelte +69 -13
  39. package/dist/admin/client/entry/header/save-indicator.svelte +57 -28
  40. package/dist/admin/client/entry/header/save-indicator.svelte.d.ts +1 -0
  41. package/dist/admin/client/entry/header/status-badge.svelte +60 -15
  42. package/dist/admin/client/entry/header/status-badge.svelte.d.ts +1 -2
  43. package/dist/admin/client/entry/header/version-history-sheet.svelte +1 -1
  44. package/dist/admin/client/entry/hybrid/hybrid-layout.svelte +74 -23
  45. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +1 -1
  46. package/dist/admin/client/entry/utils.d.ts +14 -0
  47. package/dist/admin/client/entry/utils.js +28 -0
  48. package/dist/admin/client/form/form-submission/form-submission.svelte +2 -2
  49. package/dist/admin/client/form/form-submissions.svelte +143 -194
  50. package/dist/admin/client/form/form-submissions.svelte.d.ts +2 -0
  51. package/dist/admin/client/login/lang.d.ts +3 -0
  52. package/dist/admin/client/login/lang.js +10 -4
  53. package/dist/admin/client/login/login-form.svelte +8 -1
  54. package/dist/admin/client/login/reset-password-page.svelte +24 -3
  55. package/dist/admin/client/login/schema.d.ts +14 -2
  56. package/dist/admin/client/login/schema.js +19 -8
  57. package/dist/admin/client/maintenance/maintenance-page.svelte +16 -17
  58. package/dist/admin/client/media/media-page.svelte +1 -1
  59. package/dist/admin/client/shop/coupon-edit-page.svelte +117 -13
  60. package/dist/admin/client/shop/coupon-form.svelte +282 -138
  61. package/dist/admin/client/shop/coupon-form.svelte.d.ts +1 -9
  62. package/dist/admin/client/shop/coupon-new-page.svelte +40 -10
  63. package/dist/admin/client/shop/coupon-new-page.svelte.d.ts +2 -17
  64. package/dist/admin/client/shop/coupon-schema.d.ts +28 -0
  65. package/dist/admin/client/shop/coupon-schema.js +53 -0
  66. package/dist/admin/client/shop/coupons-list-page.svelte +262 -118
  67. package/dist/admin/client/shop/coupons-list-page.svelte.d.ts +16 -1
  68. package/dist/admin/client/shop/shipping-method-edit-page.svelte +108 -59
  69. package/dist/admin/client/shop/shipping-method-form.svelte +36 -9
  70. package/dist/admin/client/shop/shipping-method-new-page.svelte +44 -13
  71. package/dist/admin/client/shop/shipping-methods-list-page.svelte +101 -59
  72. package/dist/admin/client/shop/shop-order-detail-page.svelte +113 -84
  73. package/dist/admin/client/shop/shop-orders-list-page.svelte +302 -152
  74. package/dist/admin/client/shop/shop-orders-list-page.svelte.d.ts +18 -1
  75. package/dist/admin/client/shop/shop-products-list-page.svelte +355 -118
  76. package/dist/admin/client/shop/shop-products-list-page.svelte.d.ts +19 -1
  77. package/dist/admin/client/users/accept-invite-page.svelte +24 -3
  78. package/dist/admin/client/users/create-user-dialog.svelte +3 -8
  79. package/dist/admin/client/users/lang.d.ts +2 -0
  80. package/dist/admin/client/users/lang.js +4 -0
  81. package/dist/admin/client/users/pending-invitations.svelte +2 -9
  82. package/dist/admin/client/users/user-name-cell.svelte +20 -0
  83. package/dist/admin/client/users/user-name-cell.svelte.d.ts +9 -0
  84. package/dist/admin/client/users/user-role-badge.svelte +16 -0
  85. package/dist/admin/client/users/user-role-badge.svelte.d.ts +7 -0
  86. package/dist/admin/client/users/user-row-actions.svelte +72 -0
  87. package/dist/admin/client/users/user-row-actions.svelte.d.ts +20 -0
  88. package/dist/admin/client/users/user-sessions-sheet.svelte +2 -11
  89. package/dist/admin/client/users/users-page.svelte +283 -497
  90. package/dist/admin/client/users/users-page.svelte.d.ts +12 -1
  91. package/dist/admin/components/dashboard/form-submissions-widget.svelte +59 -74
  92. package/dist/admin/components/dashboard/recent-activity.svelte +17 -5
  93. package/dist/admin/components/dashboard/recent-entries.svelte +19 -7
  94. package/dist/admin/components/dialogs/confirmation-dialog.svelte +105 -0
  95. package/dist/admin/components/dialogs/confirmation-dialog.svelte.d.ts +13 -0
  96. package/dist/admin/components/fields/block-picker-modal.svelte +6 -0
  97. package/dist/admin/components/fields/blocks-field.svelte +46 -1
  98. package/dist/admin/components/fields/boolean-field.svelte +1 -1
  99. package/dist/admin/components/fields/field-renderer.svelte +23 -21
  100. package/dist/admin/components/fields/file-field.svelte +344 -30
  101. package/dist/admin/components/fields/media-field.svelte +16 -2
  102. package/dist/admin/components/fields/radio-field.svelte +22 -0
  103. package/dist/admin/components/fields/relation-field.svelte +123 -97
  104. package/dist/admin/components/fields/relation-picker-dialog.svelte +2 -2
  105. package/dist/admin/components/fields/seo-field.svelte +60 -30
  106. package/dist/admin/components/fields/shop-field.svelte +9 -4
  107. package/dist/admin/components/fields/simple-array-field.svelte +321 -151
  108. package/dist/admin/components/fields/simple-array-field.svelte.d.ts +3 -0
  109. package/dist/admin/components/fields/slug-field.svelte +146 -21
  110. package/dist/admin/components/fields/text-field-wrapper.svelte +37 -20
  111. package/dist/admin/components/fields/text-field.svelte +7 -2
  112. package/dist/admin/components/fields/url-field-wrapper.svelte +10 -0
  113. package/dist/admin/components/fields/url-field.svelte +36 -23
  114. package/dist/admin/components/forms/form-error-summary.svelte +143 -0
  115. package/dist/admin/components/forms/form-error-summary.svelte.d.ts +27 -0
  116. package/dist/admin/components/layout/app-sidebar.svelte +7 -2
  117. package/dist/admin/components/layout/detail-page-shell.svelte +71 -0
  118. package/dist/admin/components/layout/detail-page-shell.svelte.d.ts +24 -0
  119. package/dist/admin/components/layout/lang.d.ts +5 -0
  120. package/dist/admin/components/layout/lang.js +10 -0
  121. package/dist/admin/components/layout/layout-renderer.svelte +71 -2
  122. package/dist/admin/components/layout/layout-renderer.svelte.d.ts +1 -0
  123. package/dist/admin/components/layout/layout-tabs.svelte +172 -0
  124. package/dist/admin/components/layout/layout-tabs.svelte.d.ts +24 -0
  125. package/dist/admin/components/layout/nav-breadcrumbs.svelte +25 -7
  126. package/dist/admin/components/layout/nav-collections.svelte +23 -36
  127. package/dist/admin/components/layout/nav-forms.svelte +19 -35
  128. package/dist/admin/components/layout/nav-main.svelte +3 -28
  129. package/dist/admin/components/layout/nav-search.svelte +70 -2
  130. package/dist/admin/components/layout/nav-section.svelte +77 -0
  131. package/dist/admin/components/layout/nav-section.svelte.d.ts +22 -0
  132. package/dist/admin/components/layout/nav-shop.svelte +3 -27
  133. package/dist/admin/components/layout/nav-singletons.svelte +16 -28
  134. package/dist/admin/components/layout/page-header.stories.svelte +93 -0
  135. package/dist/admin/components/layout/page-header.stories.svelte.d.ts +27 -0
  136. package/dist/admin/components/layout/page-header.svelte +68 -0
  137. package/dist/admin/components/layout/page-header.svelte.d.ts +17 -0
  138. package/dist/admin/components/layout/site-header.svelte +9 -0
  139. package/dist/admin/components/layout/site-header.svelte.d.ts +2 -17
  140. package/dist/admin/components/media/file/file-name-input.svelte +6 -2
  141. package/dist/admin/components/media/file/file-preview.svelte +130 -17
  142. package/dist/admin/components/media/file-upload.svelte +16 -7
  143. package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
  144. package/dist/admin/components/media/files-list.svelte +153 -53
  145. package/dist/admin/components/media/files-list.svelte.d.ts +1 -0
  146. package/dist/admin/components/media/media-library.svelte +577 -198
  147. package/dist/admin/components/media/media-library.svelte.d.ts +4 -0
  148. package/dist/admin/components/media/media-selector.svelte +4 -2
  149. package/dist/admin/components/media/media-selector.svelte.d.ts +1 -0
  150. package/dist/admin/components/media/tag-sidebar.svelte +4 -4
  151. package/dist/admin/components/tiptap/FigureNodeView.svelte +10 -0
  152. package/dist/admin/components/tiptap/bubble-menu.svelte +104 -0
  153. package/dist/admin/components/tiptap/bubble-menu.svelte.d.ts +19 -0
  154. package/dist/admin/components/tiptap/content-editor.svelte +28 -24
  155. package/dist/admin/components/tiptap/editor-toolbar.svelte +7 -7
  156. package/dist/admin/components/tiptap/extensions.js +5 -1
  157. package/dist/admin/components/tiptap/image-dialog.svelte +5 -1
  158. package/dist/admin/components/tiptap/link-dialog.svelte +2 -0
  159. package/dist/admin/components/tiptap/tiptap-editor.svelte +18 -20
  160. package/dist/admin/components/tiptap/video-dialog.svelte +1 -1
  161. package/dist/admin/i18n/errors.d.ts +140 -0
  162. package/dist/admin/i18n/errors.js +151 -0
  163. package/dist/admin/remote/entry.remote.d.ts +59 -4
  164. package/dist/admin/remote/entry.remote.js +239 -62
  165. package/dist/admin/remote/shop.remote.d.ts +37 -32
  166. package/dist/admin/remote/shop.remote.js +9 -2
  167. package/dist/admin/shared/password-generate.d.ts +6 -0
  168. package/dist/admin/shared/password-generate.js +40 -0
  169. package/dist/admin/shared/password-schema.d.ts +6 -0
  170. package/dist/admin/shared/password-schema.js +10 -3
  171. package/dist/admin/styles/admin.css +23 -6
  172. package/dist/admin/styles/tokens.md +244 -0
  173. package/dist/admin/utils/accordionActivation.d.ts +13 -0
  174. package/dist/admin/utils/accordionActivation.js +35 -0
  175. package/dist/admin/utils/entryLabel.d.ts +23 -0
  176. package/dist/admin/utils/entryLabel.js +51 -12
  177. package/dist/admin/utils/field-a11y.d.ts +29 -0
  178. package/dist/admin/utils/field-a11y.js +23 -0
  179. package/dist/admin/utils/fieldPathElement.d.ts +9 -0
  180. package/dist/admin/utils/fieldPathElement.js +18 -0
  181. package/dist/admin/utils/fileDisplay.d.ts +10 -0
  182. package/dist/admin/utils/fileDisplay.js +26 -0
  183. package/dist/admin/utils/flattenFormErrors.d.ts +19 -0
  184. package/dist/admin/utils/flattenFormErrors.js +102 -0
  185. package/dist/admin/utils/formatters.d.ts +12 -0
  186. package/dist/admin/utils/{formatDate.js → formatters.js} +23 -2
  187. package/dist/admin/utils/scrollWithin.d.ts +9 -0
  188. package/dist/admin/utils/scrollWithin.js +32 -0
  189. package/dist/admin/utils/tabActivation.d.ts +12 -0
  190. package/dist/admin/utils/tabActivation.js +24 -0
  191. package/dist/cms/runtime/schema.d.ts +1 -0
  192. package/dist/cms/runtime/schema.js +1 -0
  193. package/dist/cms/runtime/types.d.ts +80 -7
  194. package/dist/components/ui/accordion/accordion-content.svelte +17 -3
  195. package/dist/components/ui/accordion/accordion.stories.svelte +21 -1
  196. package/dist/components/ui/alert/alert.stories.svelte +14 -0
  197. package/dist/components/ui/alert-dialog/alert-dialog.stories.svelte +45 -0
  198. package/dist/components/ui/alert-dialog/alert-dialog.stories.svelte.d.ts +27 -0
  199. package/dist/components/ui/avatar/avatar.stories.svelte +27 -0
  200. package/dist/components/ui/badge/badge.stories.svelte +15 -0
  201. package/dist/components/ui/breadcrumb/breadcrumb.stories.svelte +47 -0
  202. package/dist/components/ui/breadcrumb/breadcrumb.svelte +1 -1
  203. package/dist/components/ui/button/button.stories.svelte +53 -6
  204. package/dist/components/ui/button/button.svelte +39 -5
  205. package/dist/components/ui/button/button.svelte.d.ts +4 -0
  206. package/dist/components/ui/button-group/button-group.stories.svelte +44 -0
  207. package/dist/components/ui/button-group/button-group.stories.svelte.d.ts +27 -0
  208. package/dist/components/ui/calendar/calendar.stories.svelte +36 -0
  209. package/dist/components/ui/calendar/calendar.stories.svelte.d.ts +27 -0
  210. package/dist/components/ui/card/card.stories.svelte +7 -0
  211. package/dist/components/ui/carousel/carousel.stories.svelte +43 -0
  212. package/dist/components/ui/carousel/carousel.stories.svelte.d.ts +27 -0
  213. package/dist/components/ui/checkbox/checkbox.stories.svelte +67 -0
  214. package/dist/components/ui/checkbox/checkbox.stories.svelte.d.ts +27 -0
  215. package/dist/components/ui/checkbox/checkbox.svelte +3 -3
  216. package/dist/components/ui/command/command.stories.svelte +18 -0
  217. package/dist/components/ui/data-table/data-table.stories.svelte +61 -0
  218. package/dist/components/ui/data-table/data-table.stories.svelte.d.ts +18 -0
  219. package/dist/components/ui/dialog/dialog-content.svelte +5 -0
  220. package/dist/components/ui/dialog/dialog-content.svelte.d.ts +2 -0
  221. package/dist/components/ui/dialog/dialog.stories.svelte +35 -0
  222. package/dist/components/ui/dropdown-menu/dropdown-menu.stories.svelte +74 -0
  223. package/dist/components/ui/dropdown-menu/dropdown-menu.stories.svelte.d.ts +27 -0
  224. package/dist/components/ui/field/field-context.svelte.d.ts +22 -0
  225. package/dist/components/ui/field/field-context.svelte.js +9 -0
  226. package/dist/components/ui/field/field-control.svelte +18 -0
  227. package/dist/components/ui/field/field-control.svelte.d.ts +8 -0
  228. package/dist/components/ui/field/field-description.svelte +12 -0
  229. package/dist/components/ui/field/field-error.svelte +14 -6
  230. package/dist/components/ui/field/field-label.svelte +10 -0
  231. package/dist/components/ui/field/field.stories.svelte +95 -9
  232. package/dist/components/ui/field/field.svelte +57 -0
  233. package/dist/components/ui/field/field.svelte.d.ts +2 -0
  234. package/dist/components/ui/field/index.d.ts +3 -1
  235. package/dist/components/ui/field/index.js +4 -2
  236. package/dist/components/ui/form/form-field-errors.svelte +1 -1
  237. package/dist/components/ui/form/form.stories.svelte +25 -0
  238. package/dist/components/ui/form/form.stories.svelte.d.ts +26 -0
  239. package/dist/components/ui/input/input.stories.svelte +26 -0
  240. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  241. package/dist/components/ui/input-group/input-group.stories.svelte +43 -0
  242. package/dist/components/ui/input-group/input-group.stories.svelte.d.ts +27 -0
  243. package/dist/components/ui/item/item.stories.svelte +61 -0
  244. package/dist/components/ui/item/item.stories.svelte.d.ts +27 -0
  245. package/dist/components/ui/label/label.stories.svelte +7 -0
  246. package/dist/components/ui/live-region/index.d.ts +1 -0
  247. package/dist/components/ui/live-region/index.js +1 -0
  248. package/dist/components/ui/live-region/live-region-demo.svelte +32 -0
  249. package/dist/components/ui/live-region/live-region-demo.svelte.d.ts +7 -0
  250. package/dist/components/ui/live-region/live-region.stories.svelte +23 -0
  251. package/dist/components/ui/live-region/live-region.stories.svelte.d.ts +26 -0
  252. package/dist/components/ui/live-region/live-region.svelte +12 -0
  253. package/dist/components/ui/live-region/live-region.svelte.d.ts +8 -0
  254. package/dist/components/ui/popover/popover.stories.svelte +34 -0
  255. package/dist/components/ui/radio-group/radio-group.stories.svelte +58 -0
  256. package/dist/components/ui/radio-group/radio-group.stories.svelte.d.ts +27 -0
  257. package/dist/components/ui/resizable/resizable.stories.svelte +56 -0
  258. package/dist/components/ui/resizable/resizable.stories.svelte.d.ts +27 -0
  259. package/dist/components/ui/select/select.stories.svelte +49 -0
  260. package/dist/components/ui/separator/separator.stories.svelte +18 -0
  261. package/dist/components/ui/sheet/sheet.stories.svelte +34 -0
  262. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  263. package/dist/components/ui/sidebar/sidebar-menu-button.svelte +1 -0
  264. package/dist/components/ui/sidebar/sidebar-trigger.svelte +1 -1
  265. package/dist/components/ui/sidebar/sidebar.stories.svelte +72 -0
  266. package/dist/components/ui/sidebar/sidebar.stories.svelte.d.ts +27 -0
  267. package/dist/components/ui/skeleton/skeleton.stories.svelte +39 -0
  268. package/dist/components/ui/skeleton/skeleton.stories.svelte.d.ts +27 -0
  269. package/dist/components/ui/skeleton/skeleton.svelte +6 -0
  270. package/dist/components/ui/sonner/index.d.ts +1 -1
  271. package/dist/components/ui/sonner/index.js +1 -1
  272. package/dist/components/ui/sonner/sonner.stories.svelte +7 -0
  273. package/dist/components/ui/sonner/sonner.svelte +17 -1
  274. package/dist/components/ui/sonner/sonner.svelte.d.ts +6 -0
  275. package/dist/components/ui/spinner/spinner.stories.svelte +30 -0
  276. package/dist/components/ui/spinner/spinner.stories.svelte.d.ts +27 -0
  277. package/dist/components/ui/switch/switch.stories.svelte +56 -0
  278. package/dist/components/ui/switch/switch.stories.svelte.d.ts +27 -0
  279. package/dist/components/ui/table/table-cell.svelte +1 -1
  280. package/dist/components/ui/table/table-head.svelte +1 -1
  281. package/dist/components/ui/table/table.stories.svelte +68 -0
  282. package/dist/components/ui/table/table.stories.svelte.d.ts +27 -0
  283. package/dist/components/ui/table/table.svelte +1 -1
  284. package/dist/components/ui/tabs/tabs.stories.svelte +48 -0
  285. package/dist/components/ui/tabs/tabs.stories.svelte.d.ts +27 -0
  286. package/dist/components/ui/textarea/textarea.stories.svelte +21 -0
  287. package/dist/components/ui/toggle/toggle.stories.svelte +23 -0
  288. package/dist/components/ui/toggle-group/toggle-group.stories.svelte +43 -0
  289. package/dist/components/ui/tooltip/tooltip.stories.svelte +46 -6
  290. package/dist/core/fields/fieldSchemaToTs.d.ts +7 -0
  291. package/dist/core/fields/fieldSchemaToTs.js +234 -90
  292. package/dist/core/fields/layoutUtils.d.ts +4 -1
  293. package/dist/core/fields/layoutUtils.js +41 -4
  294. package/dist/core/fields/resolveSeo.d.ts +70 -0
  295. package/dist/core/fields/resolveSeo.js +88 -0
  296. package/dist/core/fields/seoFieldDescriptor.d.ts +43 -0
  297. package/dist/core/fields/seoFieldDescriptor.js +74 -0
  298. package/dist/core/fields/slugPath.d.ts +13 -0
  299. package/dist/core/fields/slugPath.js +32 -0
  300. package/dist/core/fields/urlUtils.d.ts +8 -0
  301. package/dist/core/fields/urlUtils.js +27 -0
  302. package/dist/core/index.d.ts +1 -0
  303. package/dist/core/index.js +1 -0
  304. package/dist/core/server/entries/operations/create.js +13 -0
  305. package/dist/core/server/entries/operations/get.d.ts +7 -0
  306. package/dist/core/server/entries/operations/get.js +10 -6
  307. package/dist/core/server/entries/operations/slugUniqueness.d.ts +37 -0
  308. package/dist/core/server/entries/operations/slugUniqueness.js +116 -0
  309. package/dist/core/server/entries/operations/update.d.ts +6 -1
  310. package/dist/core/server/entries/operations/update.js +24 -1
  311. package/dist/core/server/fields/slugResolver.d.ts +3 -13
  312. package/dist/core/server/fields/slugResolver.js +8 -37
  313. package/dist/core/server/generator/fields.js +10 -17
  314. package/dist/core/server/generator/formFields.js +2 -1
  315. package/dist/core/server/generator/generator.js +4 -4
  316. package/dist/core/server/generator/utils.d.ts +1 -0
  317. package/dist/core/server/generator/utils.js +4 -0
  318. package/dist/paraglide/messages/_index.d.ts +3 -36
  319. package/dist/paraglide/messages/_index.js +3 -71
  320. package/dist/paraglide/messages/hello_world.d.ts +5 -0
  321. package/dist/paraglide/messages/hello_world.js +33 -0
  322. package/dist/paraglide/messages/login_hello.d.ts +16 -0
  323. package/dist/paraglide/messages/login_hello.js +34 -0
  324. package/dist/paraglide/messages/login_please_login.d.ts +16 -0
  325. package/dist/paraglide/messages/login_please_login.js +34 -0
  326. package/dist/shop/server/orders.d.ts +1 -0
  327. package/dist/shop/server/orders.js +14 -0
  328. package/dist/shop/server/shop-data.d.ts +2 -0
  329. package/dist/shop/server/shop-data.js +20 -5
  330. package/dist/sveltekit/server/handle.js +17 -0
  331. package/dist/types/cms.schema.js +4 -2
  332. package/dist/types/fields.d.ts +35 -0
  333. package/dist/types/index.d.ts +1 -1
  334. package/dist/types/layout.d.ts +35 -2
  335. package/dist/updates/0.26.0/index.d.ts +2 -0
  336. package/dist/updates/0.26.0/index.js +51 -0
  337. package/dist/updates/index.js +3 -1
  338. package/package.json +29 -7
  339. package/dist/admin/client/collection/empty-state.svelte +0 -28
  340. package/dist/admin/client/collection/empty-state.svelte.d.ts +0 -9
  341. package/dist/admin/client/form/submission-status-badge.svelte +0 -41
  342. package/dist/admin/client/form/submission-status-badge.svelte.d.ts +0 -7
  343. package/dist/admin/components/media/file-preview.svelte +0 -51
  344. package/dist/admin/components/media/file-preview.svelte.d.ts +0 -6
  345. package/dist/admin/utils/formatDate.d.ts +0 -5
  346. package/dist/paraglide/messages/en.d.ts +0 -5
  347. package/dist/paraglide/messages/en.js +0 -14
  348. package/dist/paraglide/messages/pl.d.ts +0 -5
  349. package/dist/paraglide/messages/pl.js +0 -14
@@ -1,33 +1,16 @@
1
1
  <script lang="ts">
2
- import Search from '@tabler/icons-svelte/icons/search';
3
2
  import Plus from '@tabler/icons-svelte/icons/plus';
4
- import Pencil from '@tabler/icons-svelte/icons/pencil';
5
- import Trash from '@tabler/icons-svelte/icons/trash';
6
- import Loader2 from '@tabler/icons-svelte/icons/loader-2';
7
- import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
8
3
  import Mail from '@tabler/icons-svelte/icons/mail';
9
- import Users from '@tabler/icons-svelte/icons/users';
10
- import Filter from '@tabler/icons-svelte/icons/filter';
11
- import ArrowsSort from '@tabler/icons-svelte/icons/arrows-sort';
12
- import SortAscending from '@tabler/icons-svelte/icons/sort-ascending';
13
- import SortDescending from '@tabler/icons-svelte/icons/sort-descending';
14
- import ChevronLeft from '@tabler/icons-svelte/icons/chevron-left';
15
- import ChevronRight from '@tabler/icons-svelte/icons/chevron-right';
16
- import X from '@tabler/icons-svelte/icons/x';
17
- import Input from '../../../components/ui/input/input.svelte';
18
4
  import Button from '../../../components/ui/button/button.svelte';
19
- import { Checkbox } from '../../../components/ui/checkbox/index.js';
20
5
  import * as Popover from '../../../components/ui/popover/index.js';
21
- import * as Table from '../../../components/ui/table/index.js';
22
6
  import { authClient } from '../../auth-client.js';
23
7
  import { usersLang } from './lang.js';
24
8
  import { sidebarLang } from '../../components/layout/lang.js';
25
9
  import { getBreadcrumbs } from '../../state/breadcrumbs.svelte.js';
26
10
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
27
- import { toLocaleCode } from '../../utils/formatDate.js';
11
+ import { formatDateTime } from '../../utils/formatters.js';
28
12
  import { getRoleLabel } from '../../utils/roleLabel.js';
29
13
  import { toast } from 'svelte-sonner';
30
- import { fly } from 'svelte/transition';
31
14
  import CreateUserDialog from './create-user-dialog.svelte';
32
15
  import EditUserDialog from './edit-user-dialog.svelte';
33
16
  import DeleteUserDialog from './delete-user-dialog.svelte';
@@ -35,6 +18,19 @@
35
18
  import InviteUserDialog from './invite-user-dialog.svelte';
36
19
  import PendingInvitations from './pending-invitations.svelte';
37
20
  import { getRemotes } from '../../helpers/index.js';
21
+ import PageHeader from '../../components/layout/page-header.svelte';
22
+ import DataTable from '../collection/data-table.svelte';
23
+ import TableToolbar from '../collection/table-toolbar.svelte';
24
+ import TablePagination from '../collection/table-pagination.svelte';
25
+ import StateDisplay from '../collection/state-display.svelte';
26
+ import BulkActionsBar from '../collection/bulk-actions-bar.svelte';
27
+ import SortableHeader from '../collection/sortable-header.svelte';
28
+ import SelectionCell from '../collection/selection-cell.svelte';
29
+ import UserNameCell from './user-name-cell.svelte';
30
+ import UserRoleBadge from './user-role-badge.svelte';
31
+ import UserRowActions from './user-row-actions.svelte';
32
+ import { renderComponent } from '../../../components/ui/data-table/render-helpers.js';
33
+ import type { ColumnDef, RowSelectionState, SortingState, Table } from '@tanstack/table-core';
38
34
 
39
35
  type User = {
40
36
  id: string;
@@ -44,6 +40,15 @@
44
40
  createdAt: Date;
45
41
  };
46
42
 
43
+ type Props = {
44
+ data?: User[];
45
+ state?: 'loading' | 'error' | 'ok';
46
+ };
47
+
48
+ let { data: injectedData, state: injectedState }: Props = $props();
49
+
50
+ const useInjectedData = $derived(injectedData !== undefined);
51
+
47
52
  const remotes = getRemotes();
48
53
  const emailQuery = $derived(remotes.getEmailConfigured());
49
54
  const emailConfigured = $derived(emailQuery.data === true);
@@ -53,30 +58,22 @@
53
58
  const breadcrumbs = getBreadcrumbs();
54
59
 
55
60
  $effect(() => {
56
- breadcrumbs.state = [
57
- { label: sidebarLang[interfaceLanguage.current].main.users }
58
- ];
61
+ breadcrumbs.state = [{ label: sidebarLang[interfaceLanguage.current].main.users }];
59
62
  });
60
63
 
61
64
  const session = authClient.useSession();
62
65
  const currentUserId = $derived(session.value?.data?.user?.id ?? '');
63
66
 
64
- let users = $state<User[]>([]);
65
- let loading = $state(true);
67
+ let fetchedUsers = $state<User[]>([]);
68
+ let fetchedLoading = $state(true);
66
69
  let searchQuery = $state('');
67
70
 
68
- // Sorting
69
- let sortColumn = $state<'name' | 'role' | 'createdAt'>('createdAt');
70
- let sortDirection = $state<'desc' | 'asc'>('desc');
71
-
72
- // Filtering
71
+ let sorting = $state<SortingState>([{ id: 'createdAt', desc: true }]);
73
72
  let roleFilter = $state<string>('all');
74
- let filterOpen = $state(false);
75
-
76
- // Selection
77
- let selectedIds = $state<Set<string>>(new Set());
73
+ let rowSelection = $state<RowSelectionState>({});
74
+ let pagination = $state({ pageIndex: 0, pageSize: 20 });
75
+ let tableInstance = $state<Table<User> | null>(null);
78
76
 
79
- // Dialogs
80
77
  let createOpen = $state(false);
81
78
  let editOpen = $state(false);
82
79
  let deleteOpen = $state(false);
@@ -84,54 +81,57 @@
84
81
  let editingUser = $state<User | null>(null);
85
82
  let deletingUser = $state<{ id: string; name: string } | null>(null);
86
83
 
87
- // Sessions sheet
88
84
  let sessionsOpen = $state(false);
89
85
  let sessionsUserId = $state<string | null>(null);
90
86
  let sessionsUserName = $state('');
91
87
 
92
- // Invite refresh
93
88
  let inviteRefresh = $state(0);
94
89
 
95
- // Pagination
96
- let pageIndex = $state(0);
97
- const pageSize = 20;
98
-
99
- // Derived chain: users searchFiltered → roleFiltered → sorted → paged
100
- const searchFiltered = $derived(
101
- searchQuery
102
- ? users.filter(
103
- (u) =>
104
- u.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
105
- u.email.toLowerCase().includes(searchQuery.toLowerCase())
90
+ const allUsers = $derived(useInjectedData ? (injectedData ?? []) : fetchedUsers);
91
+ const isLoading = $derived(useInjectedData ? injectedState === 'loading' : fetchedLoading);
92
+ const isError = $derived(useInjectedData && injectedState === 'error');
93
+
94
+ const filteredUsers = $derived.by(() => {
95
+ const q = searchQuery.trim().toLowerCase();
96
+ let list = q
97
+ ? allUsers.filter(
98
+ (u) => u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)
106
99
  )
107
- : users
108
- );
100
+ : allUsers;
101
+ if (roleFilter !== 'all') {
102
+ list = list.filter((u) => u.role === roleFilter);
103
+ }
104
+ return list;
105
+ });
109
106
 
110
- const roleFiltered = $derived(
111
- roleFilter === 'all' ? searchFiltered : searchFiltered.filter((u) => u.role === roleFilter)
112
- );
107
+ const totalItems = $derived(filteredUsers.length);
108
+ const pageCount = $derived(Math.max(1, Math.ceil(totalItems / pagination.pageSize)));
113
109
 
114
- const sorted = $derived(
115
- [...roleFiltered].sort((a, b) => {
116
- const dir = sortDirection === 'asc' ? 1 : -1;
117
- if (sortColumn === 'name') return a.name.localeCompare(b.name) * dir;
118
- if (sortColumn === 'role') return a.role.localeCompare(b.role) * dir;
119
- return (a.createdAt.getTime() - b.createdAt.getTime()) * dir;
120
- })
110
+ const selectedIds = $derived(
111
+ Object.keys(rowSelection)
112
+ .filter((k) => rowSelection[k])
113
+ .map((k) => filteredUsers[Number(k)]?.id)
114
+ .filter((id): id is string => Boolean(id))
121
115
  );
122
116
 
123
- const totalItems = $derived(sorted.length);
124
- const pageCount = $derived(Math.ceil(totalItems / pageSize));
125
- const paged = $derived(sorted.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize));
126
- const showingStart = $derived(totalItems === 0 ? 0 : pageIndex * pageSize + 1);
127
- const showingEnd = $derived(Math.min((pageIndex + 1) * pageSize, totalItems));
117
+ const selectedCount = $derived(selectedIds.length);
118
+
119
+ const dataFilters = $derived([
120
+ {
121
+ slug: 'role',
122
+ label: lang.filterByRole,
123
+ options: [
124
+ { value: 'all', label: lang.allRoles },
125
+ { value: 'admin', label: lang.roleAdmin },
126
+ { value: 'user', label: lang.roleUser }
127
+ ]
128
+ }
129
+ ]);
128
130
 
129
- const allOnPageSelected = $derived(
130
- paged.length > 0 && paged.every((u) => selectedIds.has(u.id))
131
- );
132
- const someSelected = $derived(selectedIds.size > 0);
131
+ const activeDataFilters = $derived({
132
+ role: roleFilter === 'all' ? null : roleFilter
133
+ });
133
134
 
134
- // Avatar helpers
135
135
  const avatarGradients = [
136
136
  'linear-gradient(135deg, #5B4A9E, #B8A9E8)',
137
137
  'linear-gradient(135deg, #3A8A5C, #7EC4A0)',
@@ -159,74 +159,97 @@
159
159
  return avatarGradients[Math.abs(hash) % avatarGradients.length];
160
160
  }
161
161
 
162
- function toggleSort(column: 'name' | 'role' | 'createdAt') {
163
- if (sortColumn === column) {
164
- sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
165
- } else {
166
- sortColumn = column;
167
- sortDirection = column === 'createdAt' ? 'desc' : 'asc';
168
- }
169
- }
170
-
171
- function toggleSelectAll() {
172
- if (allOnPageSelected) {
173
- for (const u of paged) selectedIds.delete(u.id);
174
- } else {
175
- for (const u of paged) selectedIds.add(u.id);
176
- }
177
- selectedIds = new Set(selectedIds);
178
- }
179
-
180
- function toggleSelect(id: string) {
181
- if (selectedIds.has(id)) {
182
- selectedIds.delete(id);
183
- } else {
184
- selectedIds.add(id);
185
- }
186
- selectedIds = new Set(selectedIds);
187
- }
188
-
189
- function clearSelection() {
190
- selectedIds = new Set();
191
- }
192
-
193
- async function handleBulkDelete() {
194
- const ids = [...selectedIds].filter((id) => id !== currentUserId);
195
- if (ids.length === 0) return;
196
-
197
- for (const userId of ids) {
198
- await authClient.admin.removeUser({ userId });
199
- }
200
-
201
- toast.success(lang.userDeleted);
202
- selectedIds = new Set();
203
- await loadUsers();
204
- }
205
-
206
- async function handleBulkChangeRole(newRole: 'admin' | 'user') {
207
- const ids = [...selectedIds].filter((id) => id !== currentUserId);
208
- if (ids.length === 0) return;
209
-
210
- for (const userId of ids) {
211
- await authClient.admin.setRole({ userId, role: newRole });
162
+ const columns = $derived.by<ColumnDef<User>[]>(() => [
163
+ {
164
+ id: 'select',
165
+ header: ({ table }) =>
166
+ renderComponent(SelectionCell, {
167
+ checked: table.getIsAllPageRowsSelected(),
168
+ indeterminate: table.getIsSomePageRowsSelected(),
169
+ onCheckedChange: (value: boolean) => table.toggleAllPageRowsSelected(value),
170
+ ariaLabel: lang.selectAll
171
+ }),
172
+ cell: ({ row }) =>
173
+ renderComponent(SelectionCell, {
174
+ checked: row.getIsSelected(),
175
+ onCheckedChange: (value: boolean) => row.toggleSelected(value),
176
+ ariaLabel: `${lang.selectUser}: ${row.original.name}`
177
+ }),
178
+ enableSorting: false,
179
+ size: 48
180
+ },
181
+ {
182
+ accessorKey: 'name',
183
+ header: ({ column }) =>
184
+ renderComponent(SortableHeader<User>, {
185
+ column,
186
+ label: lang.name,
187
+ sorting
188
+ }),
189
+ cell: (info) =>
190
+ renderComponent(UserNameCell, {
191
+ name: info.row.original.name,
192
+ email: info.row.original.email,
193
+ avatarGradient: getAvatarGradient(info.row.original.name),
194
+ initials: getInitials(info.row.original.name) || '?'
195
+ })
196
+ },
197
+ {
198
+ accessorKey: 'role',
199
+ header: ({ column }) =>
200
+ renderComponent(SortableHeader<User>, {
201
+ column,
202
+ label: lang.role,
203
+ sorting
204
+ }),
205
+ cell: (info) =>
206
+ renderComponent(UserRoleBadge, {
207
+ role: info.row.original.role,
208
+ label: getRoleLabel(info.row.original.role, interfaceLanguage.current)
209
+ }),
210
+ size: 160
211
+ },
212
+ {
213
+ accessorKey: 'createdAt',
214
+ header: ({ column }) =>
215
+ renderComponent(SortableHeader<User>, {
216
+ column,
217
+ label: lang.createdAt,
218
+ sorting
219
+ }),
220
+ cell: (info) => formatDateTime(info.row.original.createdAt, interfaceLanguage.current),
221
+ size: 200
222
+ },
223
+ {
224
+ id: 'actions',
225
+ header: '',
226
+ cell: (info) =>
227
+ renderComponent(UserRowActions, {
228
+ user: info.row.original,
229
+ currentUserId,
230
+ sessionsTitle: lang.sessions.title,
231
+ editLabel: lang.editUser,
232
+ deleteLabel: lang.deleteUser,
233
+ onSessions: openSessions,
234
+ onEdit: openEdit,
235
+ onDelete: openDelete
236
+ }),
237
+ enableSorting: false,
238
+ size: 140
212
239
  }
213
-
214
- toast.success(lang.userUpdated);
215
- selectedIds = new Set();
216
- await loadUsers();
217
- }
240
+ ]);
218
241
 
219
242
  $effect(() => {
220
- loadUsers();
243
+ if (!useInjectedData) loadUsers();
221
244
  });
222
245
 
223
246
  async function loadUsers() {
224
- loading = true;
247
+ fetchedLoading = true;
225
248
  const { data } = await authClient.admin.listUsers({
226
249
  query: { limit: 500 }
227
250
  });
228
251
  if (data) {
229
- users = data.users.map((u: any) => ({
252
+ fetchedUsers = data.users.map((u: { id: string; name?: string; email: string; role?: string; createdAt: string | Date }) => ({
230
253
  id: u.id,
231
254
  name: u.name ?? '',
232
255
  email: u.email,
@@ -234,15 +257,7 @@
234
257
  createdAt: new Date(u.createdAt)
235
258
  }));
236
259
  }
237
- loading = false;
238
- }
239
-
240
- function formatDate(date: Date): string {
241
- return date.toLocaleString(toLocaleCode(interfaceLanguage.current), {
242
- year: 'numeric',
243
- month: 'short',
244
- day: 'numeric'
245
- });
260
+ fetchedLoading = false;
246
261
  }
247
262
 
248
263
  function openEdit(user: User) {
@@ -261,390 +276,161 @@
261
276
  sessionsOpen = true;
262
277
  }
263
278
 
264
- function getSortIcon(column: string) {
265
- if (sortColumn !== column) return ArrowsSort;
266
- return sortDirection === 'asc' ? SortAscending : SortDescending;
267
- }
279
+ async function handleBulkDelete() {
280
+ const ids = selectedIds.filter((id) => id !== currentUserId);
281
+ if (ids.length === 0) return;
282
+
283
+ for (const userId of ids) {
284
+ await authClient.admin.removeUser({ userId });
285
+ }
268
286
 
269
- function getAriaSort(column: string): 'ascending' | 'descending' | 'none' {
270
- if (sortColumn !== column) return 'none';
271
- return sortDirection === 'asc' ? 'ascending' : 'descending';
287
+ toast.success(lang.userDeleted);
288
+ rowSelection = {};
289
+ if (!useInjectedData) await loadUsers();
272
290
  }
273
291
 
274
- function generatePageNumbers(current: number, total: number): (number | '...')[] {
275
- if (total <= 7) return Array.from({ length: total }, (_, i) => i);
276
- const pages: (number | '...')[] = [0];
277
- if (current > 2) pages.push('...');
278
- for (let i = Math.max(1, current - 1); i <= Math.min(total - 2, current + 1); i++) {
279
- pages.push(i);
292
+ async function handleBulkChangeRole(newRole: 'admin' | 'user') {
293
+ const ids = selectedIds.filter((id) => id !== currentUserId);
294
+ if (ids.length === 0) return;
295
+
296
+ for (const userId of ids) {
297
+ await authClient.admin.setRole({ userId, role: newRole });
280
298
  }
281
- if (current < total - 3) pages.push('...');
282
- pages.push(total - 1);
283
- return pages;
299
+
300
+ toast.success(lang.userUpdated);
301
+ rowSelection = {};
302
+ if (!useInjectedData) await loadUsers();
284
303
  }
285
304
  </script>
286
305
 
287
306
  <div class="p-5 pb-24 md:p-7">
288
- <!-- Header -->
289
- <div class="mb-6 users-fade-up">
290
- <div class="mb-1 flex items-center gap-3">
291
- <h1 class="text-2xl font-bold">{lang.title}</h1>
292
- {#if !loading}
293
- <span
294
- class="inline-flex items-center justify-center rounded-full px-2.5 py-0.5 text-xs font-bold"
295
- style="background: var(--lavender-lighter); color: var(--primary);"
296
- >
297
- {users.length}
298
- </span>
299
- {/if}
300
- </div>
301
- <p class="text-sm" style="color: var(--muted-foreground);">{lang.description}</p>
302
- </div>
303
-
304
- <!-- Toolbar -->
305
- <div class="mb-4 flex flex-wrap items-center gap-2.5 users-fade-up">
306
- <div class="relative min-w-[240px] flex-1 sm:max-w-xs">
307
- <Search class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" style="color: var(--text-light);" />
308
- <Input
309
- type="text"
310
- placeholder={lang.search}
311
- class="pl-9"
312
- value={searchQuery}
313
- oninput={(e) => {
314
- searchQuery = e.currentTarget.value;
315
- pageIndex = 0;
316
- }}
317
- />
318
- </div>
319
-
320
- <Popover.Root bind:open={filterOpen}>
321
- <Popover.Trigger>
322
- <Button variant="outline" size="sm" class="gap-1.5">
323
- <Filter class="size-4" />
324
- {lang.filterByRole}
325
- {#if roleFilter !== 'all'}
326
- <span
327
- class="ml-1 inline-flex size-5 items-center justify-center rounded-full text-[10px] font-bold"
328
- style="background: var(--primary); color: white;"
329
- >1</span>
330
- {/if}
331
- </Button>
332
- </Popover.Trigger>
333
- <Popover.Content class="w-48 p-2" align="start">
334
- {#each [
335
- { value: 'all', label: lang.allRoles },
336
- { value: 'admin', label: lang.roleAdmin },
337
- { value: 'user', label: lang.roleUser }
338
- ] as option}
339
- <button
340
- class="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent"
341
- class:font-semibold={roleFilter === option.value}
342
- style={roleFilter === option.value ? 'color: var(--primary);' : ''}
343
- onclick={() => {
344
- roleFilter = option.value;
345
- pageIndex = 0;
346
- filterOpen = false;
347
- }}
348
- >
349
- {option.label}
350
- </button>
351
- {/each}
352
- </Popover.Content>
353
- </Popover.Root>
354
-
355
- <div class="flex-1"></div>
356
-
357
- {#if emailConfigured}
358
- <Button variant="secondary" size="sm" onclick={() => (inviteOpen = true)}>
359
- <Mail class="size-4" />
360
- {lang.invite.inviteUser}
361
- </Button>
362
- {/if}
363
- <Button variant="default" size="sm" onclick={() => (createOpen = true)} data-testid="create-user-button">
364
- <Plus class="size-4" />
365
- {lang.createUser}
366
- </Button>
367
- </div>
368
-
369
- <!-- Content -->
370
- {#if loading}
371
- <div class="flex items-center justify-center py-20">
372
- <Loader2 class="size-6 animate-spin" style="color: var(--muted-foreground);" />
373
- </div>
374
- {:else if users.length === 0}
375
- <!-- Empty state -->
376
- <div class="flex flex-col items-center justify-center py-20 text-center users-fade-up">
377
- <div
378
- class="mb-5 flex size-[72px] items-center justify-center rounded-xl"
379
- style="background: var(--muted); color: var(--text-light);"
380
- >
381
- <Users class="size-8" />
382
- </div>
383
- <h2 class="mb-1.5 text-lg font-bold">{lang.emptyTitle}</h2>
384
- <p class="mb-5 max-w-sm text-sm" style="color: var(--muted-foreground);">
385
- {lang.emptyDescription}
386
- </p>
307
+ <PageHeader
308
+ title={lang.title}
309
+ count={!isLoading && !isError ? allUsers.length : undefined}
310
+ description={lang.description}
311
+ >
312
+ {#snippet primaryActions()}
387
313
  <Button onclick={() => (createOpen = true)} data-testid="create-user-button">
388
314
  <Plus class="size-4" />
389
- {lang.addUser}
315
+ {lang.createUser}
390
316
  </Button>
391
- </div>
392
- {:else if paged.length === 0}
393
- <p class="py-16 text-center text-sm" style="color: var(--muted-foreground);">
394
- {lang.noResults}
395
- </p>
317
+ {/snippet}
318
+ {#snippet secondaryActions()}
319
+ {#if emailConfigured}
320
+ <Button variant="secondary" size="sm" onclick={() => (inviteOpen = true)}>
321
+ <Mail class="size-4" />
322
+ {lang.invite.inviteUser}
323
+ </Button>
324
+ {/if}
325
+ {/snippet}
326
+ </PageHeader>
327
+
328
+ {#if isError}
329
+ <StateDisplay kind="error" />
330
+ {:else if isLoading}
331
+ <StateDisplay kind="loading" />
332
+ {:else if allUsers.length === 0}
333
+ <StateDisplay
334
+ kind="empty"
335
+ title={lang.emptyTitle}
336
+ description={lang.emptyDescription}
337
+ ctaLabel={lang.addUser}
338
+ onCta={() => (createOpen = true)}
339
+ />
396
340
  {:else}
397
- <!-- Table -->
398
- <div class="overflow-hidden rounded-xl border bg-card shadow-sm users-fade-up">
399
- <Table.Root>
400
- <Table.Header>
401
- <Table.Row class="bg-muted/50">
402
- <Table.Head class="w-11 text-center">
403
- <Checkbox
404
- checked={allOnPageSelected}
405
- onCheckedChange={toggleSelectAll}
406
- aria-label={lang.selectAll}
407
- />
408
- </Table.Head>
409
- <Table.Head aria-sort={getAriaSort('name')}>
410
- {@const SortIconName = getSortIcon('name')}
411
- <button
412
- class="inline-flex items-center gap-1 text-xs font-bold uppercase tracking-wide transition-colors hover:text-primary"
413
- onclick={() => toggleSort('name')}
414
- >
415
- {lang.name}
416
- <SortIconName class="users-sort-icon size-3.5" />
417
- </button>
418
- </Table.Head>
419
- <Table.Head aria-sort={getAriaSort('role')}>
420
- {@const SortIconRole = getSortIcon('role')}
421
- <button
422
- class="inline-flex items-center gap-1 text-xs font-bold uppercase tracking-wide transition-colors hover:text-primary"
423
- onclick={() => toggleSort('role')}
424
- >
425
- {lang.role}
426
- <SortIconRole class="users-sort-icon size-3.5" />
427
- </button>
428
- </Table.Head>
429
- <Table.Head aria-sort={getAriaSort('createdAt')}>
430
- {@const SortIconCreatedat = getSortIcon('createdAt')}
431
- <button
432
- class="inline-flex items-center gap-1 text-xs font-bold uppercase tracking-wide transition-colors hover:text-primary"
433
- onclick={() => toggleSort('createdAt')}
434
- >
435
- {lang.createdAt}
436
- <SortIconCreatedat class="users-sort-icon size-3.5" />
437
- </button>
438
- </Table.Head>
439
- <Table.Head class="w-[120px]">
440
- <span class="sr-only">{lang.actions}</span>
441
- </Table.Head>
442
- </Table.Row>
443
- </Table.Header>
444
- <Table.Body>
445
- {#each paged as user (user.id)}
446
- <Table.Row
447
- class="transition-colors"
448
- style={selectedIds.has(user.id) ? 'background: rgba(184,169,232,0.15);' : ''}
449
- onmouseenter={(e) => {
450
- if (!selectedIds.has(user.id)) e.currentTarget.style.background = 'var(--lavender-lighter)';
451
- }}
452
- onmouseleave={(e) => {
453
- if (!selectedIds.has(user.id)) e.currentTarget.style.background = '';
454
- }}
455
- >
456
- <Table.Cell class="text-center">
457
- <Checkbox
458
- checked={selectedIds.has(user.id)}
459
- onCheckedChange={() => toggleSelect(user.id)}
460
- aria-label="{lang.selectUser}: {user.name}"
461
- />
462
- </Table.Cell>
463
- <Table.Cell>
464
- <div class="flex items-center gap-3">
465
- <div
466
- class="users-avatar"
467
- style="background: {getAvatarGradient(user.name)};"
468
- aria-hidden="true"
469
- >
470
- {getInitials(user.name) || '?'}
471
- </div>
472
- <div class="min-w-0">
473
- <div class="truncate text-sm font-semibold">{user.name}</div>
474
- <div class="truncate text-xs" style="color: var(--text-light);">
475
- {user.email}
476
- </div>
477
- </div>
478
- </div>
479
- </Table.Cell>
480
- <Table.Cell>
481
- <span
482
- class="users-role-badge"
483
- class:users-role-admin={user.role === 'admin'}
484
- class:users-role-user={user.role !== 'admin'}
485
- >
486
- {getRoleLabel(user.role, interfaceLanguage.current)}
487
- </span>
488
- </Table.Cell>
489
- <Table.Cell>
490
- <span class="text-xs" style="color: var(--muted-foreground);">
491
- {formatDate(user.createdAt)}
492
- </span>
493
- </Table.Cell>
494
- <Table.Cell>
495
- <div class="flex items-center justify-end gap-0.5">
496
- <Button
497
- variant="ghost"
498
- size="icon"
499
- class="h-8 w-8"
500
- onclick={() => openSessions(user)}
501
- title={lang.sessions.title}
502
- >
503
- <DeviceDesktop class="size-4" />
504
- </Button>
505
- <Button
506
- variant="ghost"
507
- size="icon"
508
- class="h-8 w-8"
509
- onclick={() => openEdit(user)}
510
- title={lang.editUser}
511
- >
512
- <Pencil class="size-4" />
513
- </Button>
514
- {#if user.id !== currentUserId}
515
- <Button
516
- variant="ghost"
517
- size="icon"
518
- class="text-destructive h-8 w-8"
519
- onclick={() => openDelete(user)}
520
- title={lang.deleteUser}
521
- data-testid="delete-user-row-button"
522
- >
523
- <Trash class="size-4" />
524
- </Button>
525
- {/if}
526
- </div>
527
- </Table.Cell>
528
- </Table.Row>
529
- {/each}
530
- </Table.Body>
531
- </Table.Root>
532
- </div>
341
+ <TableToolbar
342
+ {searchQuery}
343
+ searchPlaceholder={lang.search}
344
+ searchLabel={lang.search}
345
+ onSearchChange={(q) => {
346
+ searchQuery = q;
347
+ pagination = { ...pagination, pageIndex: 0 };
348
+ }}
349
+ hideStatusFilter
350
+ hideViewToggle
351
+ {dataFilters}
352
+ {activeDataFilters}
353
+ onDataFilterChange={(_slug, value) => {
354
+ roleFilter = value ?? 'all';
355
+ pagination = { ...pagination, pageIndex: 0 };
356
+ }}
357
+ />
358
+
359
+ <div class="overflow-hidden rounded-xl border bg-card shadow-sm">
360
+ <DataTable
361
+ data={filteredUsers}
362
+ {columns}
363
+ enableSorting
364
+ enableSelection
365
+ enablePagination
366
+ {sorting}
367
+ onSortingChange={(s) => (sorting = s)}
368
+ {rowSelection}
369
+ onRowSelectionChange={(s) => (rowSelection = s)}
370
+ {pagination}
371
+ onPaginationChange={(p) => (pagination = p)}
372
+ tableRef={(t) => (tableInstance = t)}
373
+ emptyTitle={lang.noResults}
374
+ />
533
375
 
534
- <!-- Pagination -->
535
- {#if pageCount > 1 || totalItems > 0}
536
- <div class="mt-4 flex items-center justify-between users-fade-up">
537
- <p class="text-sm" style="color: var(--muted-foreground);">
538
- {lang.showing(showingStart, showingEnd, totalItems)}
539
- </p>
540
- {#if pageCount > 1}
541
- <div class="flex items-center gap-1">
542
- <Button
543
- variant="outline"
544
- size="icon"
545
- class="h-8 w-8"
546
- disabled={pageIndex === 0}
547
- onclick={() => (pageIndex -= 1)}
548
- aria-label="Previous page"
549
- >
550
- <ChevronLeft class="size-4" />
551
- </Button>
552
- {#each generatePageNumbers(pageIndex, pageCount) as p}
553
- {#if p === '...'}
554
- <span class="px-1 text-sm" style="color: var(--muted-foreground);">...</span>
555
- {:else}
556
- <Button
557
- variant={pageIndex === p ? 'default' : 'outline'}
558
- size="sm"
559
- class="h-8 min-w-8 px-2"
560
- onclick={() => (pageIndex = p)}
561
- aria-current={pageIndex === p ? 'page' : undefined}
562
- >
563
- {p + 1}
564
- </Button>
565
- {/if}
566
- {/each}
567
- <Button
568
- variant="outline"
569
- size="icon"
570
- class="h-8 w-8"
571
- disabled={pageIndex >= pageCount - 1}
572
- onclick={() => (pageIndex += 1)}
573
- aria-label="Next page"
574
- >
575
- <ChevronRight class="size-4" />
576
- </Button>
577
- </div>
578
- {/if}
579
- </div>
580
- {/if}
376
+ <TablePagination
377
+ pageIndex={pagination.pageIndex}
378
+ pageSize={pagination.pageSize}
379
+ {pageCount}
380
+ {totalItems}
381
+ onPageChange={(p) => (pagination = { ...pagination, pageIndex: p })}
382
+ onPageSizeChange={(s) => (pagination = { pageIndex: 0, pageSize: s })}
383
+ />
384
+ </div>
581
385
  {/if}
582
386
 
583
- <!-- Pending invitations -->
584
- {#if emailConfigured}
387
+ {#if emailConfigured && !useInjectedData}
585
388
  <PendingInvitations refreshTrigger={inviteRefresh} />
586
389
  {/if}
587
390
  </div>
588
391
 
589
- <!-- Bulk actions bar -->
590
- {#if someSelected}
591
- <div
592
- class="users-bulk-bar"
593
- transition:fly={{ y: 60, duration: 250 }}
594
- >
595
- <div
596
- class="flex items-center gap-3 rounded-xl px-5 py-2.5 shadow-xl"
597
- style="background: var(--plum-darker); color: white;"
598
- >
599
- <span class="text-sm font-bold whitespace-nowrap">{lang.selected(selectedIds.size)}</span>
600
- <div class="h-5 w-px" style="background: rgba(255,255,255,0.2);"></div>
601
-
602
- <Popover.Root>
603
- <Popover.Trigger>
604
- <button
605
- class="inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-semibold text-white transition-colors hover:bg-white/10"
606
- style="border-color: rgba(255,255,255,0.2);"
392
+ <BulkActionsBar
393
+ {selectedCount}
394
+ onDelete={handleBulkDelete}
395
+ onClear={() => (rowSelection = {})}
396
+ >
397
+ {#snippet actions()}
398
+ <Popover.Root>
399
+ <Popover.Trigger>
400
+ {#snippet child({ props })}
401
+ <Button
402
+ {...props}
403
+ variant="ghost"
404
+ size="sm"
405
+ class="border border-white/20 text-white hover:bg-white/10 hover:text-white"
607
406
  >
608
407
  {lang.changeRole}
609
- </button>
610
- </Popover.Trigger>
611
- <Popover.Content class="w-40 p-1" align="center" side="top">
612
- <button
613
- class="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent"
614
- onclick={() => handleBulkChangeRole('admin')}
615
- >
616
- {lang.roleAdmin}
617
- </button>
618
- <button
619
- class="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent"
620
- onclick={() => handleBulkChangeRole('user')}
621
- >
622
- {lang.roleUser}
623
- </button>
624
- </Popover.Content>
625
- </Popover.Root>
626
-
627
- <button
628
- class="inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-semibold text-white transition-colors hover:bg-red-500/20"
629
- style="border-color: rgba(196,75,75,0.5);"
630
- onclick={handleBulkDelete}
631
- >
632
- <Trash class="size-3.5" />
633
- {lang.deleteUser}
634
- </button>
635
-
636
- <button
637
- class="ml-1 inline-flex items-center justify-center rounded-md p-1 text-white/70 transition-colors hover:bg-white/10 hover:text-white"
638
- onclick={clearSelection}
639
- aria-label={lang.cancel}
640
- >
641
- <X class="size-4" />
642
- </button>
643
- </div>
644
- </div>
645
- {/if}
408
+ </Button>
409
+ {/snippet}
410
+ </Popover.Trigger>
411
+ <Popover.Content class="w-40 p-1" align="center" side="top">
412
+ <button
413
+ class="hover:bg-accent flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors"
414
+ onclick={() => handleBulkChangeRole('admin')}
415
+ >
416
+ {lang.roleAdmin}
417
+ </button>
418
+ <button
419
+ class="hover:bg-accent flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors"
420
+ onclick={() => handleBulkChangeRole('user')}
421
+ >
422
+ {lang.roleUser}
423
+ </button>
424
+ </Popover.Content>
425
+ </Popover.Root>
426
+ {/snippet}
427
+ </BulkActionsBar>
646
428
 
647
- <CreateUserDialog bind:open={createOpen} onOpenChange={(v) => (createOpen = v)} onCreated={loadUsers} />
429
+ <CreateUserDialog
430
+ bind:open={createOpen}
431
+ onOpenChange={(v) => (createOpen = v)}
432
+ onCreated={loadUsers}
433
+ />
648
434
 
649
435
  <EditUserDialog
650
436
  bind:open={editOpen}