includio-cms 0.1.4 → 0.5.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 (296) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/ROADMAP.md +18 -13
  3. package/dist/admin/api/accept-invite.js +1 -5
  4. package/dist/admin/api/invite.js +7 -16
  5. package/dist/admin/client/account/account-page.svelte +20 -50
  6. package/dist/admin/client/account/lang.d.ts +15 -23
  7. package/dist/admin/client/account/lang.js +51 -67
  8. package/dist/admin/client/account/preferences-section.svelte +26 -84
  9. package/dist/admin/client/account/profile-section.svelte +60 -40
  10. package/dist/admin/client/account/schema.d.ts +11 -3
  11. package/dist/admin/client/account/schema.js +25 -16
  12. package/dist/admin/client/account/security-section.svelte +139 -105
  13. package/dist/admin/client/account/sessions-section.svelte +35 -34
  14. package/dist/admin/client/admin/admin-after-login-layout-content.svelte +3 -5
  15. package/dist/admin/client/admin/admin-layout.svelte +3 -2
  16. package/dist/admin/client/admin/admin-preloader.svelte +36 -0
  17. package/dist/admin/client/admin/admin-preloader.svelte.d.ts +18 -0
  18. package/dist/admin/client/admin/dashboard-page.svelte +55 -41
  19. package/dist/admin/client/collection/a11y-score-cell.svelte +45 -0
  20. package/dist/admin/client/collection/a11y-score-cell.svelte.d.ts +6 -0
  21. package/dist/admin/client/collection/bulk-actions-bar.svelte +83 -0
  22. package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +9 -0
  23. package/dist/admin/client/collection/collection-entries.svelte +255 -260
  24. package/dist/admin/client/collection/collection-view.svelte.d.ts +4 -3
  25. package/dist/admin/client/collection/collection-view.svelte.js +9 -5
  26. package/dist/admin/client/collection/collection.svelte +22 -12
  27. package/dist/admin/client/collection/data-table.svelte +50 -39
  28. package/dist/admin/client/collection/data-table.svelte.d.ts +1 -0
  29. package/dist/admin/client/collection/date-cell.svelte +7 -5
  30. package/dist/admin/client/collection/date-cell.svelte.d.ts +1 -1
  31. package/dist/admin/client/collection/empty-state.svelte +28 -0
  32. package/dist/admin/client/collection/empty-state.svelte.d.ts +9 -0
  33. package/dist/admin/client/collection/entry-link.svelte +10 -4
  34. package/dist/admin/client/collection/entry-link.svelte.d.ts +1 -0
  35. package/dist/admin/client/collection/grid-view.svelte +21 -23
  36. package/dist/admin/client/collection/grid-view.svelte.d.ts +1 -2
  37. package/dist/admin/client/collection/row-actions.svelte +60 -0
  38. package/dist/admin/client/collection/row-actions.svelte.d.ts +9 -0
  39. package/dist/admin/client/collection/status-badge.svelte +7 -8
  40. package/dist/admin/client/collection/table-pagination.svelte +122 -79
  41. package/dist/admin/client/collection/table-pagination.svelte.d.ts +1 -0
  42. package/dist/admin/client/collection/table-toolbar.svelte +108 -88
  43. package/dist/admin/client/collection/table-toolbar.svelte.d.ts +8 -9
  44. package/dist/admin/client/entry/entry-form.svelte +109 -1
  45. package/dist/admin/client/entry/entry-header.svelte +96 -37
  46. package/dist/admin/client/entry/entry-header.svelte.d.ts +5 -0
  47. package/dist/admin/client/entry/entry.svelte +171 -60
  48. package/dist/admin/client/entry/header/a11y-validator.d.ts +46 -0
  49. package/dist/admin/client/entry/header/a11y-validator.js +311 -0
  50. package/dist/admin/client/entry/header/publish-panel.svelte +373 -131
  51. package/dist/admin/client/entry/header/publish-panel.svelte.d.ts +4 -0
  52. package/dist/admin/client/entry/header/save-indicator.svelte +33 -23
  53. package/dist/admin/client/entry/header/schedule-popover.svelte +1 -1
  54. package/dist/admin/client/entry/header/status-badge.svelte +25 -118
  55. package/dist/admin/client/entry/header/version-history-sheet.svelte +314 -98
  56. package/dist/admin/client/form/form-submission/form-submission.svelte +271 -83
  57. package/dist/admin/client/form/form-submission/submission-field.svelte +12 -12
  58. package/dist/admin/client/form/form-submissions.svelte +421 -139
  59. package/dist/admin/client/form/submission-link.svelte +8 -2
  60. package/dist/admin/client/form/submission-link.svelte.d.ts +1 -0
  61. package/dist/admin/client/form/submission-status-badge.svelte +18 -4
  62. package/dist/admin/client/form/submission-status-badge.svelte.d.ts +1 -0
  63. package/dist/admin/client/login/lang.d.ts +32 -0
  64. package/dist/admin/client/login/lang.js +66 -2
  65. package/dist/admin/client/login/login-form.svelte +237 -95
  66. package/dist/admin/client/login/login-form.svelte.d.ts +2 -17
  67. package/dist/admin/client/login/login-page.svelte +34 -98
  68. package/dist/admin/client/login/reset-password-page.svelte +235 -0
  69. package/dist/admin/client/login/reset-password-page.svelte.d.ts +4 -0
  70. package/dist/admin/client/login/schema.d.ts +15 -0
  71. package/dist/admin/client/login/schema.js +21 -0
  72. package/dist/admin/client/users/accept-invite-page.svelte +166 -37
  73. package/dist/admin/client/users/create-user-dialog.svelte +15 -7
  74. package/dist/admin/client/users/delete-user-dialog.svelte +81 -16
  75. package/dist/admin/client/users/delete-user-dialog.svelte.d.ts +4 -1
  76. package/dist/admin/client/users/edit-user-dialog.svelte +3 -0
  77. package/dist/admin/client/users/invite-user-dialog.svelte +16 -3
  78. package/dist/admin/client/users/lang.d.ts +27 -0
  79. package/dist/admin/client/users/lang.js +64 -10
  80. package/dist/admin/client/users/pending-invitations.svelte +59 -23
  81. package/dist/admin/client/users/users-page.svelte +471 -72
  82. package/dist/admin/components/accessibility/accessibility-overview.svelte +2 -7
  83. package/dist/admin/components/dashboard/a11y-gauge.svelte +90 -0
  84. package/dist/admin/components/dashboard/a11y-gauge.svelte.d.ts +18 -0
  85. package/dist/admin/components/dashboard/accessibility-hub.svelte +13 -12
  86. package/dist/admin/components/dashboard/form-submissions-widget.svelte +71 -113
  87. package/dist/admin/components/dashboard/index.d.ts +4 -2
  88. package/dist/admin/components/dashboard/index.js +4 -2
  89. package/dist/admin/components/dashboard/recent-activity.svelte +53 -75
  90. package/dist/admin/components/dashboard/recent-entries.svelte +94 -0
  91. package/dist/admin/components/dashboard/recent-entries.svelte.d.ts +18 -0
  92. package/dist/admin/components/dashboard/stat-card.svelte +2 -2
  93. package/dist/admin/components/dashboard/tip-of-the-day.svelte +109 -0
  94. package/dist/admin/components/dashboard/tip-of-the-day.svelte.d.ts +3 -0
  95. package/dist/admin/components/dashboard/welcome-header.svelte +45 -0
  96. package/dist/admin/components/dashboard/welcome-header.svelte.d.ts +3 -0
  97. package/dist/admin/components/fields/{array-field.svelte → blocks-field.svelte} +4 -4
  98. package/dist/admin/components/fields/{array-field.svelte.d.ts → blocks-field.svelte.d.ts} +5 -5
  99. package/dist/admin/components/fields/content-field.svelte +27 -0
  100. package/dist/admin/components/fields/content-field.svelte.d.ts +31 -0
  101. package/dist/admin/components/fields/field-renderer.svelte +9 -7
  102. package/dist/admin/components/fields/image-field.svelte +2 -2
  103. package/dist/admin/components/fields/media-field.svelte +2 -2
  104. package/dist/admin/components/fields/seo-field.svelte +205 -25
  105. package/dist/admin/components/fields/simple-array-field.svelte +289 -0
  106. package/dist/admin/components/fields/simple-array-field.svelte.d.ts +30 -0
  107. package/dist/admin/components/fields/slug-field.svelte +3 -2
  108. package/dist/admin/components/fields/standalone-field-renderer.svelte +148 -0
  109. package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +9 -0
  110. package/dist/admin/components/fields/text-field-wrapper.svelte +13 -1
  111. package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
  112. package/dist/admin/components/fields/url-field.svelte +5 -4
  113. package/dist/admin/components/layout/app-sidebar.svelte +27 -24
  114. package/dist/admin/components/layout/lang.d.ts +6 -0
  115. package/dist/admin/components/layout/lang.js +13 -1
  116. package/dist/admin/components/layout/layout-renderer.svelte +352 -0
  117. package/dist/admin/components/layout/layout-renderer.svelte.d.ts +14 -0
  118. package/dist/admin/components/layout/nav-breadcrumbs.svelte +4 -4
  119. package/dist/admin/components/layout/nav-collections.svelte +65 -36
  120. package/dist/admin/components/layout/nav-footer.svelte +31 -0
  121. package/dist/admin/components/layout/nav-footer.svelte.d.ts +18 -0
  122. package/dist/admin/components/layout/nav-forms.svelte +55 -30
  123. package/dist/admin/components/layout/nav-main.svelte +14 -52
  124. package/dist/admin/components/layout/nav-search.svelte +4 -3
  125. package/dist/admin/components/layout/nav-singletons.svelte +59 -17
  126. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +17 -8
  127. package/dist/admin/components/layout/site-header.svelte +74 -13
  128. package/dist/admin/components/media/alt-input.svelte +32 -22
  129. package/dist/admin/components/media/bulk-action-bar.svelte +139 -150
  130. package/dist/admin/components/media/file/file-details.svelte +299 -217
  131. package/dist/admin/components/media/file/file-miniature.svelte +54 -41
  132. package/dist/admin/components/media/file/file-miniature.svelte.d.ts +1 -0
  133. package/dist/admin/components/media/file/file-preview.svelte +1 -1
  134. package/dist/admin/components/media/file-upload.svelte +24 -26
  135. package/dist/admin/components/media/files-list.svelte +112 -40
  136. package/dist/admin/components/media/files-list.svelte.d.ts +2 -0
  137. package/dist/admin/components/media/focal-point-input.svelte +122 -26
  138. package/dist/admin/components/media/media-library.svelte +127 -70
  139. package/dist/admin/components/media/media-search.svelte +6 -6
  140. package/dist/admin/components/media/media-sort.svelte +3 -1
  141. package/dist/admin/components/media/multi-file-summary.svelte +88 -68
  142. package/dist/admin/components/media/tag-combobox.svelte +141 -66
  143. package/dist/admin/components/media/tag-combobox.svelte.d.ts +1 -0
  144. package/dist/admin/components/media/tag-sidebar.svelte +139 -121
  145. package/dist/admin/components/tiptap/FigureNodeView.svelte +144 -15
  146. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +254 -0
  147. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte.d.ts +4 -0
  148. package/dist/admin/components/tiptap/SlashCommandPopup.svelte +212 -0
  149. package/dist/admin/components/tiptap/SlashCommandPopup.svelte.d.ts +8 -0
  150. package/dist/admin/components/tiptap/content-editor.svelte +280 -0
  151. package/dist/admin/components/tiptap/content-editor.svelte.d.ts +9 -0
  152. package/dist/admin/components/tiptap/editor-toolbar.svelte +230 -0
  153. package/dist/admin/components/tiptap/editor-toolbar.svelte.d.ts +16 -0
  154. package/dist/admin/components/tiptap/heading-a11y-plugin.d.ts +2 -0
  155. package/dist/admin/components/tiptap/heading-a11y-plugin.js +67 -0
  156. package/dist/admin/components/tiptap/image-dialog.svelte +172 -11
  157. package/dist/admin/components/tiptap/inline-block-node.d.ts +19 -0
  158. package/dist/admin/components/tiptap/inline-block-node.js +98 -0
  159. package/dist/admin/components/tiptap/link-dialog.svelte +9 -4
  160. package/dist/admin/components/tiptap/slash-command.d.ts +17 -0
  161. package/dist/admin/components/tiptap/slash-command.js +181 -0
  162. package/dist/admin/components/tiptap/structured-content-utils.d.ts +21 -0
  163. package/dist/admin/components/tiptap/structured-content-utils.js +150 -0
  164. package/dist/admin/components/tiptap/tiptap-editor.svelte +18 -190
  165. package/dist/admin/email/invite-template.d.ts +8 -0
  166. package/dist/admin/email/invite-template.js +99 -0
  167. package/dist/admin/email/reset-password-template.d.ts +7 -0
  168. package/dist/admin/email/reset-password-template.js +96 -0
  169. package/dist/admin/remote/ai.remote.d.ts +1 -0
  170. package/dist/admin/remote/ai.remote.js +4 -1
  171. package/dist/admin/remote/entry.remote.d.ts +8 -0
  172. package/dist/admin/remote/entry.remote.js +53 -4
  173. package/dist/admin/remote/preview.remote.js +2 -1
  174. package/dist/admin/shared/password-schema.d.ts +5 -0
  175. package/dist/admin/shared/password-schema.js +10 -0
  176. package/dist/admin/styles/admin.css +1530 -151
  177. package/dist/admin/utils/formatDate.d.ts +1 -0
  178. package/dist/admin/utils/formatDate.js +8 -0
  179. package/dist/admin/utils/roleLabel.d.ts +2 -0
  180. package/dist/admin/utils/roleLabel.js +13 -0
  181. package/dist/ai-claude/index.d.ts +2 -0
  182. package/dist/ai-claude/index.js +56 -0
  183. package/dist/cms/runtime/api.d.ts +6 -1
  184. package/dist/cms/runtime/api.js +3 -0
  185. package/dist/cms/runtime/schemas.d.ts +9 -1
  186. package/dist/cms/runtime/schemas.js +8 -0
  187. package/dist/cms/runtime/types.d.ts +82 -10
  188. package/dist/cms/runtime/types.js +4 -0
  189. package/dist/components/ui/accordion/accordion.stories.svelte +39 -0
  190. package/dist/components/ui/accordion/accordion.stories.svelte.d.ts +27 -0
  191. package/dist/components/ui/alert/alert.stories.svelte +53 -0
  192. package/dist/components/ui/alert/alert.stories.svelte.d.ts +27 -0
  193. package/dist/components/ui/alert/alert.svelte +5 -0
  194. package/dist/components/ui/alert/alert.svelte.d.ts +9 -0
  195. package/dist/components/ui/avatar/avatar.stories.svelte +16 -0
  196. package/dist/components/ui/avatar/avatar.stories.svelte.d.ts +27 -0
  197. package/dist/components/ui/badge/badge.stories.svelte +33 -0
  198. package/dist/components/ui/badge/badge.stories.svelte.d.ts +27 -0
  199. package/dist/components/ui/breadcrumb/breadcrumb.stories.svelte +33 -0
  200. package/dist/components/ui/breadcrumb/breadcrumb.stories.svelte.d.ts +27 -0
  201. package/dist/components/ui/button/button.stories.svelte +43 -0
  202. package/dist/components/ui/button/button.stories.svelte.d.ts +27 -0
  203. package/dist/components/ui/button/button.svelte +1 -2
  204. package/dist/components/ui/button/button.svelte.d.ts +0 -3
  205. package/dist/components/ui/button-group/button-group-separator.svelte.d.ts +1 -1
  206. package/dist/components/ui/card/card.stories.svelte +42 -0
  207. package/dist/components/ui/card/card.stories.svelte.d.ts +27 -0
  208. package/dist/components/ui/command/command.stories.svelte +51 -0
  209. package/dist/components/ui/command/command.stories.svelte.d.ts +27 -0
  210. package/dist/components/ui/dialog/dialog.stories.svelte +29 -0
  211. package/dist/components/ui/dialog/dialog.stories.svelte.d.ts +27 -0
  212. package/dist/components/ui/field/field-label.svelte.d.ts +1 -1
  213. package/dist/components/ui/field/field.stories.svelte +21 -0
  214. package/dist/components/ui/field/field.stories.svelte.d.ts +27 -0
  215. package/dist/components/ui/input/input.stories.svelte +40 -0
  216. package/dist/components/ui/input/input.stories.svelte.d.ts +27 -0
  217. package/dist/components/ui/input/input.svelte +2 -4
  218. package/dist/components/ui/item/item-separator.svelte.d.ts +1 -1
  219. package/dist/components/ui/label/label.stories.svelte +20 -0
  220. package/dist/components/ui/label/label.stories.svelte.d.ts +27 -0
  221. package/dist/components/ui/popover/popover.stories.svelte +29 -0
  222. package/dist/components/ui/popover/popover.stories.svelte.d.ts +27 -0
  223. package/dist/components/ui/select/select-group-heading.svelte.d.ts +1 -1
  224. package/dist/components/ui/select/select.stories.svelte +23 -0
  225. package/dist/components/ui/select/select.stories.svelte.d.ts +27 -0
  226. package/dist/components/ui/separator/separator.stories.svelte +24 -0
  227. package/dist/components/ui/separator/separator.stories.svelte.d.ts +27 -0
  228. package/dist/components/ui/sheet/sheet.stories.svelte +29 -0
  229. package/dist/components/ui/sheet/sheet.stories.svelte.d.ts +27 -0
  230. package/dist/components/ui/sidebar/sidebar-group.svelte +3 -3
  231. package/dist/components/ui/sidebar/sidebar-group.svelte.d.ts +2 -2
  232. package/dist/components/ui/sidebar/sidebar-menu-button.svelte +28 -30
  233. package/dist/components/ui/sidebar/sidebar-menu-button.svelte.d.ts +7 -7
  234. package/dist/components/ui/sidebar/sidebar-separator.svelte.d.ts +1 -1
  235. package/dist/components/ui/sidebar/sidebar-trigger.svelte +4 -4
  236. package/dist/components/ui/sonner/sonner.stories.svelte +22 -0
  237. package/dist/components/ui/sonner/sonner.stories.svelte.d.ts +26 -0
  238. package/dist/components/ui/sonner/sonner.svelte +8 -2
  239. package/dist/components/ui/sonner/toast-demo.svelte +29 -0
  240. package/dist/components/ui/sonner/toast-demo.svelte.d.ts +6 -0
  241. package/dist/components/ui/textarea/textarea.stories.svelte +22 -0
  242. package/dist/components/ui/textarea/textarea.stories.svelte.d.ts +27 -0
  243. package/dist/components/ui/textarea/textarea.svelte +0 -2
  244. package/dist/components/ui/toggle/toggle.stories.svelte +22 -0
  245. package/dist/components/ui/toggle/toggle.stories.svelte.d.ts +27 -0
  246. package/dist/components/ui/toggle-group/toggle-group.stories.svelte +17 -0
  247. package/dist/components/ui/toggle-group/toggle-group.stories.svelte.d.ts +27 -0
  248. package/dist/components/ui/tooltip/tooltip.stories.svelte +26 -0
  249. package/dist/components/ui/tooltip/tooltip.stories.svelte.d.ts +27 -0
  250. package/dist/core/fields/fieldSchemaToTs.d.ts +1 -0
  251. package/dist/core/fields/fieldSchemaToTs.js +133 -1
  252. package/dist/core/fields/layoutUtils.d.ts +17 -0
  253. package/dist/core/fields/layoutUtils.js +149 -0
  254. package/dist/core/fields/structuredToHtml.d.ts +9 -0
  255. package/dist/core/fields/structuredToHtml.js +161 -0
  256. package/dist/core/server/entries/operations/create.js +2 -1
  257. package/dist/core/server/entries/operations/get.js +8 -6
  258. package/dist/core/server/entries/operations/update.d.ts +3 -0
  259. package/dist/core/server/entries/operations/update.js +30 -2
  260. package/dist/core/server/fields/queryStructuredContent.d.ts +15 -0
  261. package/dist/core/server/fields/queryStructuredContent.js +65 -0
  262. package/dist/core/server/fields/resolveImageFields.js +51 -2
  263. package/dist/core/server/fields/resolveRelationFields.js +2 -2
  264. package/dist/core/server/fields/resolveRichtextLinks.js +80 -13
  265. package/dist/core/server/fields/resolveUrlFields.js +57 -6
  266. package/dist/core/server/fields/slugResolver.d.ts +10 -0
  267. package/dist/core/server/fields/slugResolver.js +34 -0
  268. package/dist/core/server/generator/fields.js +15 -4
  269. package/dist/core/server/generator/generator.js +3 -2
  270. package/dist/files-local/index.js +126 -64
  271. package/dist/server/auth.d.ts +5 -0
  272. package/dist/server/auth.js +12 -1
  273. package/dist/sveltekit/components/structured-content.svelte +204 -0
  274. package/dist/sveltekit/components/structured-content.svelte.d.ts +21 -0
  275. package/dist/sveltekit/config.d.ts +13 -3
  276. package/dist/sveltekit/index.d.ts +3 -0
  277. package/dist/sveltekit/index.js +3 -0
  278. package/dist/sveltekit/server/handle.js +1 -0
  279. package/dist/types/config.d.ts +3 -0
  280. package/dist/types/fields.d.ts +19 -2
  281. package/dist/types/index.d.ts +2 -0
  282. package/dist/types/index.js +2 -0
  283. package/dist/types/layout.d.ts +54 -0
  284. package/dist/types/layout.js +6 -0
  285. package/dist/types/structured-content.d.ts +63 -0
  286. package/dist/types/structured-content.js +1 -0
  287. package/dist/updates/0.1.5/index.d.ts +2 -0
  288. package/dist/updates/0.1.5/index.js +18 -0
  289. package/dist/updates/0.2.0/index.d.ts +2 -0
  290. package/dist/updates/0.2.0/index.js +11 -0
  291. package/dist/updates/0.2.2/index.d.ts +2 -0
  292. package/dist/updates/0.2.2/index.js +13 -0
  293. package/dist/updates/0.5.0/index.d.ts +2 -0
  294. package/dist/updates/0.5.0/index.js +14 -0
  295. package/dist/updates/index.js +5 -1
  296. package/package.json +16 -9
@@ -6,14 +6,26 @@
6
6
  import Loader2 from '@tabler/icons-svelte/icons/loader-2';
7
7
  import DeviceDesktop from '@tabler/icons-svelte/icons/device-desktop';
8
8
  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';
9
17
  import Input from '../../../components/ui/input/input.svelte';
10
18
  import Button from '../../../components/ui/button/button.svelte';
11
- import { Badge } from '../../../components/ui/badge/index.js';
19
+ import { Checkbox } from '../../../components/ui/checkbox/index.js';
20
+ import * as Popover from '../../../components/ui/popover/index.js';
12
21
  import * as Table from '../../../components/ui/table/index.js';
13
22
  import { authClient } from '../../auth-client.js';
14
23
  import { usersLang } from './lang.js';
15
24
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
16
25
  import { toLocaleCode } from '../../utils/formatDate.js';
26
+ import { getRoleLabel } from '../../utils/roleLabel.js';
27
+ import { toast } from 'svelte-sonner';
28
+ import { fly } from 'svelte/transition';
17
29
  import CreateUserDialog from './create-user-dialog.svelte';
18
30
  import EditUserDialog from './edit-user-dialog.svelte';
19
31
  import DeleteUserDialog from './delete-user-dialog.svelte';
@@ -44,13 +56,24 @@
44
56
  let loading = $state(true);
45
57
  let searchQuery = $state('');
46
58
 
59
+ // Sorting
60
+ let sortColumn = $state<'name' | 'role' | 'createdAt'>('createdAt');
61
+ let sortDirection = $state<'desc' | 'asc'>('desc');
62
+
63
+ // Filtering
64
+ let roleFilter = $state<string>('all');
65
+ let filterOpen = $state(false);
66
+
67
+ // Selection
68
+ let selectedIds = $state<Set<string>>(new Set());
69
+
47
70
  // Dialogs
48
71
  let createOpen = $state(false);
49
72
  let editOpen = $state(false);
50
73
  let deleteOpen = $state(false);
51
74
  let inviteOpen = $state(false);
52
75
  let editingUser = $state<User | null>(null);
53
- let deletingUserId = $state<string | null>(null);
76
+ let deletingUser = $state<{ id: string; name: string } | null>(null);
54
77
 
55
78
  // Sessions sheet
56
79
  let sessionsOpen = $state(false);
@@ -64,7 +87,8 @@
64
87
  let pageIndex = $state(0);
65
88
  const pageSize = 20;
66
89
 
67
- const filtered = $derived(
90
+ // Derived chain: users → searchFiltered → roleFiltered → sorted → paged
91
+ const searchFiltered = $derived(
68
92
  searchQuery
69
93
  ? users.filter(
70
94
  (u) =>
@@ -74,9 +98,114 @@
74
98
  : users
75
99
  );
76
100
 
77
- const totalItems = $derived(filtered.length);
101
+ const roleFiltered = $derived(
102
+ roleFilter === 'all' ? searchFiltered : searchFiltered.filter((u) => u.role === roleFilter)
103
+ );
104
+
105
+ const sorted = $derived(
106
+ [...roleFiltered].sort((a, b) => {
107
+ const dir = sortDirection === 'asc' ? 1 : -1;
108
+ if (sortColumn === 'name') return a.name.localeCompare(b.name) * dir;
109
+ if (sortColumn === 'role') return a.role.localeCompare(b.role) * dir;
110
+ return (a.createdAt.getTime() - b.createdAt.getTime()) * dir;
111
+ })
112
+ );
113
+
114
+ const totalItems = $derived(sorted.length);
78
115
  const pageCount = $derived(Math.ceil(totalItems / pageSize));
79
- const paged = $derived(filtered.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize));
116
+ const paged = $derived(sorted.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize));
117
+ const showingStart = $derived(totalItems === 0 ? 0 : pageIndex * pageSize + 1);
118
+ const showingEnd = $derived(Math.min((pageIndex + 1) * pageSize, totalItems));
119
+
120
+ const allOnPageSelected = $derived(
121
+ paged.length > 0 && paged.every((u) => selectedIds.has(u.id))
122
+ );
123
+ const someSelected = $derived(selectedIds.size > 0);
124
+
125
+ // Avatar helpers
126
+ const avatarGradients = [
127
+ 'linear-gradient(135deg, #5B4A9E, #B8A9E8)',
128
+ 'linear-gradient(135deg, #3A8A5C, #7EC4A0)',
129
+ 'linear-gradient(135deg, #C4893A, #E8C88A)',
130
+ 'linear-gradient(135deg, #463879, #8B7BC7)',
131
+ 'linear-gradient(135deg, #C44B4B, #E89B9B)',
132
+ 'linear-gradient(135deg, #2E7D9B, #7BC4D9)'
133
+ ];
134
+
135
+ function getInitials(name: string): string {
136
+ return name
137
+ .split(/\s+/)
138
+ .filter(Boolean)
139
+ .map((w) => w[0])
140
+ .slice(0, 2)
141
+ .join('')
142
+ .toUpperCase();
143
+ }
144
+
145
+ function getAvatarGradient(name: string): string {
146
+ let hash = 0;
147
+ for (let i = 0; i < name.length; i++) {
148
+ hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
149
+ }
150
+ return avatarGradients[Math.abs(hash) % avatarGradients.length];
151
+ }
152
+
153
+ function toggleSort(column: 'name' | 'role' | 'createdAt') {
154
+ if (sortColumn === column) {
155
+ sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
156
+ } else {
157
+ sortColumn = column;
158
+ sortDirection = column === 'createdAt' ? 'desc' : 'asc';
159
+ }
160
+ }
161
+
162
+ function toggleSelectAll() {
163
+ if (allOnPageSelected) {
164
+ for (const u of paged) selectedIds.delete(u.id);
165
+ } else {
166
+ for (const u of paged) selectedIds.add(u.id);
167
+ }
168
+ selectedIds = new Set(selectedIds);
169
+ }
170
+
171
+ function toggleSelect(id: string) {
172
+ if (selectedIds.has(id)) {
173
+ selectedIds.delete(id);
174
+ } else {
175
+ selectedIds.add(id);
176
+ }
177
+ selectedIds = new Set(selectedIds);
178
+ }
179
+
180
+ function clearSelection() {
181
+ selectedIds = new Set();
182
+ }
183
+
184
+ async function handleBulkDelete() {
185
+ const ids = [...selectedIds].filter((id) => id !== currentUserId);
186
+ if (ids.length === 0) return;
187
+
188
+ for (const userId of ids) {
189
+ await authClient.admin.removeUser({ userId });
190
+ }
191
+
192
+ toast.success(lang.userDeleted);
193
+ selectedIds = new Set();
194
+ await loadUsers();
195
+ }
196
+
197
+ async function handleBulkChangeRole(newRole: 'admin' | 'user') {
198
+ const ids = [...selectedIds].filter((id) => id !== currentUserId);
199
+ if (ids.length === 0) return;
200
+
201
+ for (const userId of ids) {
202
+ await authClient.admin.setRole({ userId, role: newRole });
203
+ }
204
+
205
+ toast.success(lang.userUpdated);
206
+ selectedIds = new Set();
207
+ await loadUsers();
208
+ }
80
209
 
81
210
  $effect(() => {
82
211
  loadUsers();
@@ -84,7 +213,7 @@
84
213
 
85
214
  async function loadUsers() {
86
215
  loading = true;
87
- const { data, error } = await authClient.admin.listUsers({
216
+ const { data } = await authClient.admin.listUsers({
88
217
  query: { limit: 500 }
89
218
  });
90
219
  if (data) {
@@ -112,8 +241,8 @@
112
241
  editOpen = true;
113
242
  }
114
243
 
115
- function openDelete(userId: string) {
116
- deletingUserId = userId;
244
+ function openDelete(user: User) {
245
+ deletingUser = { id: user.id, name: user.name };
117
246
  deleteOpen = true;
118
247
  }
119
248
 
@@ -122,74 +251,255 @@
122
251
  sessionsUserName = user.name;
123
252
  sessionsOpen = true;
124
253
  }
254
+
255
+ function getSortIcon(column: string) {
256
+ if (sortColumn !== column) return ArrowsSort;
257
+ return sortDirection === 'asc' ? SortAscending : SortDescending;
258
+ }
259
+
260
+ function getAriaSort(column: string): 'ascending' | 'descending' | 'none' {
261
+ if (sortColumn !== column) return 'none';
262
+ return sortDirection === 'asc' ? 'ascending' : 'descending';
263
+ }
264
+
265
+ function generatePageNumbers(current: number, total: number): (number | '...')[] {
266
+ if (total <= 7) return Array.from({ length: total }, (_, i) => i);
267
+ const pages: (number | '...')[] = [0];
268
+ if (current > 2) pages.push('...');
269
+ for (let i = Math.max(1, current - 1); i <= Math.min(total - 2, current + 1); i++) {
270
+ pages.push(i);
271
+ }
272
+ if (current < total - 3) pages.push('...');
273
+ pages.push(total - 1);
274
+ return pages;
275
+ }
125
276
  </script>
126
277
 
127
- <div class="container mx-auto max-w-5xl px-4 py-8">
128
- <div class="mb-6 flex items-center justify-between">
129
- <h1 class="text-2xl font-bold">{lang.title}</h1>
130
- <div class="flex items-center gap-2">
131
- {#if emailConfigured}
132
- <Button variant="outline" size="sm" onclick={() => (inviteOpen = true)}>
133
- <Mail class="size-4" />
134
- {lang.invite.inviteUser}
135
- </Button>
278
+ <div class="p-5 pb-24 md:p-7">
279
+ <!-- Header -->
280
+ <div class="mb-6 users-fade-up">
281
+ <div class="mb-1 flex items-center gap-3">
282
+ <h1 class="text-2xl font-bold">{lang.title}</h1>
283
+ {#if !loading}
284
+ <span
285
+ class="inline-flex items-center justify-center rounded-full px-2.5 py-0.5 text-xs font-bold"
286
+ style="background: var(--lavender-lighter); color: var(--primary);"
287
+ >
288
+ {users.length}
289
+ </span>
136
290
  {/if}
137
- <Button variant="gradient" size="sm" onclick={() => (createOpen = true)}>
138
- <Plus class="size-4" />
139
- {lang.createUser}
140
- </Button>
141
291
  </div>
292
+ <p class="text-sm" style="color: var(--muted-foreground);">{lang.description}</p>
142
293
  </div>
143
294
 
144
- <div class="relative mb-4">
145
- <Search class="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" />
146
- <Input
147
- type="text"
148
- placeholder={lang.search}
149
- class="pl-9"
150
- value={searchQuery}
151
- oninput={(e) => {
152
- searchQuery = e.currentTarget.value;
153
- pageIndex = 0;
154
- }}
155
- />
295
+ <!-- Toolbar -->
296
+ <div class="mb-4 flex flex-wrap items-center gap-2.5 users-fade-up">
297
+ <div class="relative min-w-[240px] flex-1 sm:max-w-xs">
298
+ <Search class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" style="color: var(--text-light);" />
299
+ <Input
300
+ type="text"
301
+ placeholder={lang.search}
302
+ class="pl-9"
303
+ value={searchQuery}
304
+ oninput={(e) => {
305
+ searchQuery = e.currentTarget.value;
306
+ pageIndex = 0;
307
+ }}
308
+ />
309
+ </div>
310
+
311
+ <Popover.Root bind:open={filterOpen}>
312
+ <Popover.Trigger>
313
+ <Button variant="outline" size="sm" class="gap-1.5">
314
+ <Filter class="size-4" />
315
+ {lang.filterByRole}
316
+ {#if roleFilter !== 'all'}
317
+ <span
318
+ class="ml-1 inline-flex size-5 items-center justify-center rounded-full text-[10px] font-bold"
319
+ style="background: var(--primary); color: white;"
320
+ >1</span>
321
+ {/if}
322
+ </Button>
323
+ </Popover.Trigger>
324
+ <Popover.Content class="w-48 p-2" align="start">
325
+ {#each [
326
+ { value: 'all', label: lang.allRoles },
327
+ { value: 'admin', label: lang.roleAdmin },
328
+ { value: 'user', label: lang.roleUser }
329
+ ] as option}
330
+ <button
331
+ class="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent"
332
+ class:font-semibold={roleFilter === option.value}
333
+ style={roleFilter === option.value ? 'color: var(--primary);' : ''}
334
+ onclick={() => {
335
+ roleFilter = option.value;
336
+ pageIndex = 0;
337
+ filterOpen = false;
338
+ }}
339
+ >
340
+ {option.label}
341
+ </button>
342
+ {/each}
343
+ </Popover.Content>
344
+ </Popover.Root>
345
+
346
+ <div class="flex-1"></div>
347
+
348
+ {#if emailConfigured}
349
+ <Button variant="secondary" size="sm" onclick={() => (inviteOpen = true)}>
350
+ <Mail class="size-4" />
351
+ {lang.invite.inviteUser}
352
+ </Button>
353
+ {/if}
354
+ <Button variant="default" size="sm" onclick={() => (createOpen = true)}>
355
+ <Plus class="size-4" />
356
+ {lang.createUser}
357
+ </Button>
156
358
  </div>
157
359
 
360
+ <!-- Content -->
158
361
  {#if loading}
159
- <div class="flex items-center justify-center py-16">
160
- <Loader2 class="text-muted-foreground size-6 animate-spin" />
362
+ <div class="flex items-center justify-center py-20">
363
+ <Loader2 class="size-6 animate-spin" style="color: var(--muted-foreground);" />
364
+ </div>
365
+ {:else if users.length === 0}
366
+ <!-- Empty state -->
367
+ <div class="flex flex-col items-center justify-center py-20 text-center users-fade-up">
368
+ <div
369
+ class="mb-5 flex size-[72px] items-center justify-center rounded-xl"
370
+ style="background: var(--muted); color: var(--text-light);"
371
+ >
372
+ <Users class="size-8" />
373
+ </div>
374
+ <h2 class="mb-1.5 text-lg font-bold">{lang.emptyTitle}</h2>
375
+ <p class="mb-5 max-w-sm text-sm" style="color: var(--muted-foreground);">
376
+ {lang.emptyDescription}
377
+ </p>
378
+ <Button onclick={() => (createOpen = true)}>
379
+ <Plus class="size-4" />
380
+ {lang.addUser}
381
+ </Button>
161
382
  </div>
162
383
  {:else if paged.length === 0}
163
- <p class="text-muted-foreground py-16 text-center">{lang.noResults}</p>
384
+ <p class="py-16 text-center text-sm" style="color: var(--muted-foreground);">
385
+ {lang.noResults}
386
+ </p>
164
387
  {:else}
165
- <div class="overflow-hidden rounded-2xl border">
388
+ <!-- Table -->
389
+ <div class="overflow-hidden rounded-xl border bg-card shadow-sm users-fade-up">
166
390
  <Table.Root>
167
391
  <Table.Header>
168
- <Table.Row>
169
- <Table.Head>{lang.name}</Table.Head>
170
- <Table.Head>{lang.email}</Table.Head>
171
- <Table.Head>{lang.role}</Table.Head>
172
- <Table.Head>{lang.createdAt}</Table.Head>
173
- <Table.Head class="text-right">{lang.actions}</Table.Head>
392
+ <Table.Row class="bg-muted/50">
393
+ <Table.Head class="w-11 text-center">
394
+ <Checkbox
395
+ checked={allOnPageSelected}
396
+ onCheckedChange={toggleSelectAll}
397
+ aria-label={lang.selectAll}
398
+ />
399
+ </Table.Head>
400
+ <Table.Head>
401
+ <button
402
+ class="inline-flex items-center gap-1 text-xs font-bold uppercase tracking-wide transition-colors hover:text-primary"
403
+ onclick={() => toggleSort('name')}
404
+ aria-sort={getAriaSort('name')}
405
+ >
406
+ {lang.name}
407
+ <svelte:component this={getSortIcon('name')} class="users-sort-icon size-3.5" />
408
+ </button>
409
+ </Table.Head>
410
+ <Table.Head>
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('role')}
414
+ aria-sort={getAriaSort('role')}
415
+ >
416
+ {lang.role}
417
+ <svelte:component this={getSortIcon('role')} class="users-sort-icon size-3.5" />
418
+ </button>
419
+ </Table.Head>
420
+ <Table.Head>
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('createdAt')}
424
+ aria-sort={getAriaSort('createdAt')}
425
+ >
426
+ {lang.createdAt}
427
+ <svelte:component this={getSortIcon('createdAt')} class="users-sort-icon size-3.5" />
428
+ </button>
429
+ </Table.Head>
430
+ <Table.Head class="w-[120px]">
431
+ <span class="sr-only">{lang.actions}</span>
432
+ </Table.Head>
174
433
  </Table.Row>
175
434
  </Table.Header>
176
435
  <Table.Body>
177
436
  {#each paged as user (user.id)}
178
- <Table.Row class="hover:bg-slate-50 dark:hover:bg-slate-800/50">
179
- <Table.Cell class="font-medium">{user.name}</Table.Cell>
180
- <Table.Cell>{user.email}</Table.Cell>
437
+ <Table.Row
438
+ class="transition-colors"
439
+ style={selectedIds.has(user.id) ? 'background: rgba(184,169,232,0.15);' : ''}
440
+ onmouseenter={(e) => {
441
+ if (!selectedIds.has(user.id)) e.currentTarget.style.background = 'var(--lavender-lighter)';
442
+ }}
443
+ onmouseleave={(e) => {
444
+ if (!selectedIds.has(user.id)) e.currentTarget.style.background = '';
445
+ }}
446
+ >
447
+ <Table.Cell class="text-center">
448
+ <Checkbox
449
+ checked={selectedIds.has(user.id)}
450
+ onCheckedChange={() => toggleSelect(user.id)}
451
+ aria-label="{lang.selectUser}: {user.name}"
452
+ />
453
+ </Table.Cell>
454
+ <Table.Cell>
455
+ <div class="flex items-center gap-3">
456
+ <div
457
+ class="users-avatar"
458
+ style="background: {getAvatarGradient(user.name)};"
459
+ aria-hidden="true"
460
+ >
461
+ {getInitials(user.name) || '?'}
462
+ </div>
463
+ <div class="min-w-0">
464
+ <div class="truncate text-sm font-semibold">{user.name}</div>
465
+ <div class="truncate text-xs" style="color: var(--text-light);">
466
+ {user.email}
467
+ </div>
468
+ </div>
469
+ </div>
470
+ </Table.Cell>
471
+ <Table.Cell>
472
+ <span
473
+ class="users-role-badge"
474
+ class:users-role-admin={user.role === 'admin'}
475
+ class:users-role-user={user.role !== 'admin'}
476
+ >
477
+ {getRoleLabel(user.role, interfaceLanguage.current)}
478
+ </span>
479
+ </Table.Cell>
181
480
  <Table.Cell>
182
- <Badge variant={user.role === 'admin' ? 'default' : 'secondary'}>
183
- {user.role === 'admin' ? lang.roleAdmin : lang.roleUser}
184
- </Badge>
481
+ <span class="text-xs" style="color: var(--muted-foreground);">
482
+ {formatDate(user.createdAt)}
483
+ </span>
185
484
  </Table.Cell>
186
- <Table.Cell>{formatDate(user.createdAt)}</Table.Cell>
187
- <Table.Cell class="text-right">
188
- <div class="flex items-center justify-end gap-1">
189
- <Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => openSessions(user)}>
485
+ <Table.Cell>
486
+ <div class="flex items-center justify-end gap-0.5">
487
+ <Button
488
+ variant="ghost"
489
+ size="icon"
490
+ class="h-8 w-8"
491
+ onclick={() => openSessions(user)}
492
+ title={lang.sessions.title}
493
+ >
190
494
  <DeviceDesktop class="size-4" />
191
495
  </Button>
192
- <Button variant="ghost" size="icon" class="h-8 w-8" onclick={() => openEdit(user)}>
496
+ <Button
497
+ variant="ghost"
498
+ size="icon"
499
+ class="h-8 w-8"
500
+ onclick={() => openEdit(user)}
501
+ title={lang.editUser}
502
+ >
193
503
  <Pencil class="size-4" />
194
504
  </Button>
195
505
  {#if user.id !== currentUserId}
@@ -197,7 +507,8 @@
197
507
  variant="ghost"
198
508
  size="icon"
199
509
  class="text-destructive h-8 w-8"
200
- onclick={() => openDelete(user.id)}
510
+ onclick={() => openDelete(user)}
511
+ title={lang.deleteUser}
201
512
  >
202
513
  <Trash class="size-4" />
203
514
  </Button>
@@ -210,31 +521,119 @@
210
521
  </Table.Root>
211
522
  </div>
212
523
 
213
- {#if pageCount > 1}
214
- <div class="mt-4 flex items-center justify-center gap-2">
215
- <Button variant="outline" size="sm" disabled={pageIndex === 0} onclick={() => (pageIndex -= 1)}>
216
- &larr;
217
- </Button>
218
- <span class="text-muted-foreground text-sm">
219
- {pageIndex + 1} / {pageCount}
220
- </span>
221
- <Button
222
- variant="outline"
223
- size="sm"
224
- disabled={pageIndex >= pageCount - 1}
225
- onclick={() => (pageIndex += 1)}
226
- >
227
- &rarr;
228
- </Button>
524
+ <!-- Pagination -->
525
+ {#if pageCount > 1 || totalItems > 0}
526
+ <div class="mt-4 flex items-center justify-between users-fade-up">
527
+ <p class="text-sm" style="color: var(--muted-foreground);">
528
+ {lang.showing(showingStart, showingEnd, totalItems)}
529
+ </p>
530
+ {#if pageCount > 1}
531
+ <div class="flex items-center gap-1">
532
+ <Button
533
+ variant="outline"
534
+ size="icon"
535
+ class="h-8 w-8"
536
+ disabled={pageIndex === 0}
537
+ onclick={() => (pageIndex -= 1)}
538
+ aria-label="Previous page"
539
+ >
540
+ <ChevronLeft class="size-4" />
541
+ </Button>
542
+ {#each generatePageNumbers(pageIndex, pageCount) as p}
543
+ {#if p === '...'}
544
+ <span class="px-1 text-sm" style="color: var(--muted-foreground);">...</span>
545
+ {:else}
546
+ <Button
547
+ variant={pageIndex === p ? 'default' : 'outline'}
548
+ size="sm"
549
+ class="h-8 min-w-8 px-2"
550
+ onclick={() => (pageIndex = p)}
551
+ aria-current={pageIndex === p ? 'page' : undefined}
552
+ >
553
+ {p + 1}
554
+ </Button>
555
+ {/if}
556
+ {/each}
557
+ <Button
558
+ variant="outline"
559
+ size="icon"
560
+ class="h-8 w-8"
561
+ disabled={pageIndex >= pageCount - 1}
562
+ onclick={() => (pageIndex += 1)}
563
+ aria-label="Next page"
564
+ >
565
+ <ChevronRight class="size-4" />
566
+ </Button>
567
+ </div>
568
+ {/if}
229
569
  </div>
230
570
  {/if}
231
571
  {/if}
232
572
 
573
+ <!-- Pending invitations -->
233
574
  {#if emailConfigured}
234
575
  <PendingInvitations refreshTrigger={inviteRefresh} />
235
576
  {/if}
236
577
  </div>
237
578
 
579
+ <!-- Bulk actions bar -->
580
+ {#if someSelected}
581
+ <div
582
+ class="users-bulk-bar"
583
+ transition:fly={{ y: 60, duration: 250 }}
584
+ >
585
+ <div
586
+ class="flex items-center gap-3 rounded-xl px-5 py-2.5 shadow-xl"
587
+ style="background: var(--plum-darker); color: white;"
588
+ >
589
+ <span class="text-sm font-bold whitespace-nowrap">{lang.selected(selectedIds.size)}</span>
590
+ <div class="h-5 w-px" style="background: rgba(255,255,255,0.2);"></div>
591
+
592
+ <Popover.Root>
593
+ <Popover.Trigger>
594
+ <button
595
+ 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"
596
+ style="border-color: rgba(255,255,255,0.2);"
597
+ >
598
+ {lang.changeRole}
599
+ </button>
600
+ </Popover.Trigger>
601
+ <Popover.Content class="w-40 p-1" align="center" side="top">
602
+ <button
603
+ class="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent"
604
+ onclick={() => handleBulkChangeRole('admin')}
605
+ >
606
+ {lang.roleAdmin}
607
+ </button>
608
+ <button
609
+ class="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors hover:bg-accent"
610
+ onclick={() => handleBulkChangeRole('user')}
611
+ >
612
+ {lang.roleUser}
613
+ </button>
614
+ </Popover.Content>
615
+ </Popover.Root>
616
+
617
+ <button
618
+ 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"
619
+ style="border-color: rgba(196,75,75,0.5);"
620
+ onclick={handleBulkDelete}
621
+ >
622
+ <Trash class="size-3.5" />
623
+ {lang.deleteUser}
624
+ </button>
625
+
626
+ <button
627
+ 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"
628
+ onclick={clearSelection}
629
+ aria-label={lang.cancel}
630
+ >
631
+ <X class="size-4" />
632
+ </button>
633
+ </div>
634
+ </div>
635
+ {/if}
636
+
238
637
  <CreateUserDialog bind:open={createOpen} onOpenChange={(v) => (createOpen = v)} onCreated={loadUsers} />
239
638
 
240
639
  <EditUserDialog
@@ -249,7 +648,7 @@
249
648
  bind:open={deleteOpen}
250
649
  onOpenChange={(v) => (deleteOpen = v)}
251
650
  onDeleted={loadUsers}
252
- userId={deletingUserId}
651
+ user={deletingUser}
253
652
  {currentUserId}
254
653
  />
255
654
 
@@ -22,9 +22,7 @@
22
22
  };
23
23
  </script>
24
24
 
25
- <Card.Root
26
- class="glass-card rounded-2xl border-slate-200/50 bg-white/80 backdrop-blur-xl transition-all duration-300 hover:-translate-y-1 hover:shadow-xl dark:border-white/10 dark:bg-slate-900/60"
27
- >
25
+ <Card.Root>
28
26
  <Card.Header>
29
27
  <Card.Title>{lang[interfaceLanguage.current].title}</Card.Title>
30
28
  <Card.Description>{lang[interfaceLanguage.current].description}</Card.Description>
@@ -39,10 +37,7 @@
39
37
  <div class="space-y-4">
40
38
  <p>
41
39
  {lang[interfaceLanguage.current].altInfo}
42
- <span
43
- class="bg-gradient-to-r from-[#2D4A77] to-[#4975AE] bg-clip-text text-2xl font-bold text-transparent"
44
- >{overview.filesWithoutAlt}</span
45
- >
40
+ <span class="text-2xl font-bold text-primary">{overview.filesWithoutAlt}</span>
46
41
  </p>
47
42
  </div>
48
43
  {/await}