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
@@ -5,22 +5,34 @@
5
5
  import SubmissionLink from './submission-link.svelte';
6
6
  import SubmissionStatusBadge from './submission-status-badge.svelte';
7
7
  import DataTable from '../collection/data-table.svelte';
8
- import TableToolbar from '../collection/table-toolbar.svelte';
9
8
  import TablePagination from '../collection/table-pagination.svelte';
10
9
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
11
10
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
12
11
  import type { InterfaceLanguage } from '../../../types/languages.js';
13
- import { formatRelativeDate as formatRelativeDateUtil, formatAbsoluteDate as formatAbsoluteDateUtil } from '../../utils/formatDate.js';
14
- import type { DateFormat } from '../collection/collection-view.svelte.js';
12
+ import { formatAbsoluteDate as formatAbsoluteDateUtil } from '../../utils/formatDate.js';
15
13
  import Checkbox from '../../../components/ui/checkbox/checkbox.svelte';
16
14
  import { getRemotes } from '../../context/remotes.js';
15
+ import { invalidateAll } from '$app/navigation';
16
+ import Search from '@tabler/icons-svelte/icons/search';
17
+ import FilterIcon from '@tabler/icons-svelte/icons/filter';
17
18
  import Download from '@tabler/icons-svelte/icons/download';
19
+ import ChevronDown from '@tabler/icons-svelte/icons/chevron-down';
20
+ import FileSpreadsheet from '@tabler/icons-svelte/icons/file-spreadsheet';
21
+ import FileText from '@tabler/icons-svelte/icons/file-text';
22
+ import Trash from '@tabler/icons-svelte/icons/trash';
23
+ import Eye from '@tabler/icons-svelte/icons/eye';
24
+ import EyeOff from '@tabler/icons-svelte/icons/eye-off';
25
+ import X from '@tabler/icons-svelte/icons/x';
26
+ import ClipboardList from '@tabler/icons-svelte/icons/clipboard-list';
27
+ import Input from '../../../components/ui/input/input.svelte';
18
28
  import Button from '../../../components/ui/button/button.svelte';
19
- import { invalidateAll } from '$app/navigation';
29
+ import * as Popover from '../../../components/ui/popover/index.js';
20
30
 
21
31
  const interfaceLanguage = useInterfaceLanguage();
22
32
  const remotes = getRemotes();
23
33
 
34
+ type ReadFilter = 'all' | 'unread' | 'read';
35
+
24
36
  const lang: Record<
25
37
  InterfaceLanguage,
26
38
  {
@@ -28,34 +40,85 @@
28
40
  status: string;
29
41
  new: string;
30
42
  read: string;
31
- total: string;
32
- newCount: string;
33
- lastSubmission: string;
43
+ submissions: string;
44
+ unread: string;
45
+ unreadCount: string;
46
+ searchPlaceholder: string;
47
+ all: string;
48
+ filterAll: string;
49
+ filterUnread: string;
50
+ filterRead: string;
34
51
  export: string;
52
+ exportCsv: string;
53
+ exportJson: string;
35
54
  noSubmissions: string;
55
+ noSubmissionsDesc: string;
56
+ noResults: string;
57
+ noResultsDesc: string;
58
+ bulkSelected: (n: number) => string;
59
+ bulkMarkRead: string;
60
+ bulkMarkUnread: string;
61
+ bulkExport: string;
62
+ bulkDelete: string;
63
+ confirmBulkDelete: (n: number) => string;
64
+ itemLabel: string;
36
65
  }
37
66
  > = {
38
67
  pl: {
39
- createdAt: 'Data utworzenia',
68
+ createdAt: 'Wysłano',
40
69
  status: 'Status',
41
70
  new: 'Nowe',
42
71
  read: 'Przeczytane',
43
- total: 'Ogółem',
44
- newCount: 'Nowych',
45
- lastSubmission: 'Ostatnie',
46
- export: 'Eksportuj CSV',
47
- noSubmissions: 'Brak zgłoszeń'
72
+ submissions: 'zgłoszeń',
73
+ unread: 'nieprzeczytanych',
74
+ unreadCount: 'nieprzeczytanych',
75
+ searchPlaceholder: 'Szukaj zgłoszeń…',
76
+ all: 'Wszystkie',
77
+ filterAll: 'Wszystkie',
78
+ filterUnread: 'Nieprzeczytane',
79
+ filterRead: 'Przeczytane',
80
+ export: 'Eksportuj',
81
+ exportCsv: 'Eksportuj CSV',
82
+ exportJson: 'Eksportuj JSON',
83
+ noSubmissions: 'Brak zgłoszeń',
84
+ noSubmissionsDesc: 'Nie otrzymano jeszcze żadnych zgłoszeń z tego formularza.',
85
+ noResults: 'Brak wyników',
86
+ noResultsDesc: 'Spróbuj zmienić kryteria wyszukiwania.',
87
+ bulkSelected: (n) => `${n} zaznaczonych`,
88
+ bulkMarkRead: 'Oznacz przeczytane',
89
+ bulkMarkUnread: 'Oznacz nieprzeczytane',
90
+ bulkExport: 'Eksportuj',
91
+ bulkDelete: 'Usuń',
92
+ confirmBulkDelete: (n) => `Czy na pewno chcesz usunąć ${n} zgłoszeń?`,
93
+ itemLabel: 'zgłoszeń'
48
94
  },
49
95
  en: {
50
- createdAt: 'Created At',
96
+ createdAt: 'Sent',
51
97
  status: 'Status',
52
98
  new: 'New',
53
99
  read: 'Read',
54
- total: 'Total',
55
- newCount: 'New',
56
- lastSubmission: 'Last',
57
- export: 'Export CSV',
58
- noSubmissions: 'No submissions'
100
+ submissions: 'submissions',
101
+ unread: 'unread',
102
+ unreadCount: 'unread',
103
+ searchPlaceholder: 'Search submissions…',
104
+ all: 'All',
105
+ filterAll: 'All',
106
+ filterUnread: 'Unread',
107
+ filterRead: 'Read',
108
+ export: 'Export',
109
+ exportCsv: 'Export CSV',
110
+ exportJson: 'Export JSON',
111
+ noSubmissions: 'No submissions',
112
+ noSubmissionsDesc: 'No submissions have been received from this form yet.',
113
+ noResults: 'No results',
114
+ noResultsDesc: 'Try adjusting your search criteria.',
115
+ bulkSelected: (n) => `${n} selected`,
116
+ bulkMarkRead: 'Mark read',
117
+ bulkMarkUnread: 'Mark unread',
118
+ bulkExport: 'Export',
119
+ bulkDelete: 'Delete',
120
+ confirmBulkDelete: (n) => `Are you sure you want to delete ${n} submissions?`,
121
+ itemLabel: 'submissions'
59
122
  }
60
123
  };
61
124
 
@@ -72,19 +135,27 @@
72
135
  let { submissions, formConfig }: Props = $props();
73
136
 
74
137
  let searchQuery = $state('');
75
- let dateFormat = $state<DateFormat>('relative');
138
+ let readFilter = $state<ReadFilter>('all');
76
139
  let rowSelection = $state<RowSelectionState>({});
77
140
  let sorting = $state<SortingState>([{ id: 'createdAt', desc: true }]);
78
141
  let pageSize = $state(20);
79
142
  let tableInstance = $state<Table<SubmissionRow> | null>(null);
143
+ let exportDropdownOpen = $state(false);
144
+ let filterPopoverOpen = $state(false);
145
+
146
+ const t = $derived(lang[interfaceLanguage.current]);
147
+ const formLabel = $derived(
148
+ getLocalizedLabel(formConfig.label, interfaceLanguage.current) || formConfig.slug
149
+ );
80
150
 
81
151
  const stats = $derived({
82
152
  total: submissions.length,
83
- newCount: submissions.filter((s) => !s.read).length,
84
- last: submissions.length > 0 ? new Date(submissions[0].createdAt) : null
153
+ unreadCount: submissions.filter((s) => !s.read).length
85
154
  });
86
155
 
87
- const selectedCount = $derived(Object.keys(rowSelection).filter((k) => rowSelection[k]).length);
156
+ const selectedCount = $derived(
157
+ Object.keys(rowSelection).filter((k) => rowSelection[k]).length
158
+ );
88
159
  const selectedIds = $derived(
89
160
  Object.keys(rowSelection)
90
161
  .filter((key) => rowSelection[key])
@@ -102,6 +173,12 @@
102
173
  url: `/admin/form-submissions/${submission.id}`
103
174
  }));
104
175
 
176
+ if (readFilter === 'unread') {
177
+ result = result.filter((item) => !item.read);
178
+ } else if (readFilter === 'read') {
179
+ result = result.filter((item) => item.read);
180
+ }
181
+
105
182
  if (searchQuery) {
106
183
  const query = searchQuery.toLowerCase();
107
184
  result = result.filter((item) => {
@@ -117,17 +194,21 @@
117
194
  const totalItems = $derived(filteredData.length);
118
195
  const pageCount = $derived(Math.ceil(totalItems / pageSize));
119
196
 
197
+ const activeFilterLabel = $derived(
198
+ readFilter === 'all' ? t.filterAll : readFilter === 'unread' ? t.filterUnread : t.filterRead
199
+ );
200
+
120
201
  const columns = $derived.by(() => {
121
- // Track dateFormat as dependency
122
- const currentDateFormat = dateFormat;
123
202
  const cols: ColumnDef<SubmissionRow>[] = [];
124
203
 
204
+ // Checkbox column
125
205
  cols.push({
126
206
  id: 'select',
127
207
  header: ({ table }) =>
128
208
  renderComponent(Checkbox, {
129
209
  checked: table.getIsAllPageRowsSelected(),
130
- indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
210
+ indeterminate:
211
+ table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
131
212
  onCheckedChange: (value: boolean) => table.toggleAllPageRowsSelected(!!value)
132
213
  }),
133
214
  cell: ({ row }) =>
@@ -139,152 +220,353 @@
139
220
  enableHiding: false
140
221
  });
141
222
 
142
- formConfig.fields.forEach((field, index) => {
143
- if (field.showInDataTable) {
144
- let col: ColumnDef<SubmissionRow> = {
145
- accessorKey: field.slug,
146
- header: getLocalizedLabel(field.label, interfaceLanguage.current) || field.slug
147
- };
148
-
149
- if (index === 0) {
150
- col = {
151
- ...col,
152
- cell: (info) => {
153
- return renderComponent(SubmissionLink, {
154
- name: String(info.row.original[field.slug] ?? ''),
155
- url: info.row.original.url,
156
- isRead: info.row.original.read
157
- });
158
- }
159
- };
160
- }
161
-
162
- cols.push(col);
163
- }
164
- });
165
-
223
+ // Read dot column
166
224
  cols.push({
167
- accessorKey: 'read',
168
- header: lang[interfaceLanguage.current].status,
225
+ id: 'readDot',
226
+ header: '',
169
227
  cell: (info) => {
170
228
  return renderComponent(SubmissionStatusBadge, {
171
- isRead: info.row.original.read
229
+ isRead: info.row.original.read,
230
+ variant: 'dot'
172
231
  });
232
+ },
233
+ enableSorting: false,
234
+ enableHiding: false,
235
+ size: 32
236
+ });
237
+
238
+ // Dynamic field columns
239
+ const firstShowField = formConfig.fields.find((f) => f.showInDataTable);
240
+ formConfig.fields.forEach((field) => {
241
+ if (!field.showInDataTable) return;
242
+
243
+ let col: ColumnDef<SubmissionRow> = {
244
+ accessorKey: field.slug,
245
+ header: getLocalizedLabel(field.label, interfaceLanguage.current) || field.slug
246
+ };
247
+
248
+ if (field === firstShowField) {
249
+ col = {
250
+ ...col,
251
+ cell: (info) => {
252
+ return renderComponent(SubmissionLink, {
253
+ name: String(info.row.original[field.slug] ?? ''),
254
+ url: info.row.original.url,
255
+ isRead: info.row.original.read
256
+ });
257
+ }
258
+ };
173
259
  }
260
+
261
+ if (field.type === 'email') {
262
+ col = {
263
+ ...col,
264
+ cell: (info) => {
265
+ const val = info.row.original[field.slug];
266
+ return val ? String(val) : '-';
267
+ }
268
+ };
269
+ }
270
+
271
+ cols.push(col);
174
272
  });
175
273
 
274
+ // Message preview column (find textarea field not in showInDataTable)
275
+ const messageField = formConfig.fields.find(
276
+ (f) => f.type === 'textarea' && !f.showInDataTable
277
+ );
278
+ if (messageField) {
279
+ cols.push({
280
+ id: 'messagePreview',
281
+ accessorKey: messageField.slug,
282
+ header:
283
+ getLocalizedLabel(messageField.label, interfaceLanguage.current) ||
284
+ messageField.slug,
285
+ cell: (info) => {
286
+ const val = info.row.original[messageField.slug];
287
+ if (!val) return '-';
288
+ const text = String(val);
289
+ return text.length > 60 ? text.slice(0, 60) + '…' : text;
290
+ },
291
+ size: 280
292
+ });
293
+ }
294
+
295
+ // Date column
176
296
  cols.push({
177
297
  accessorKey: 'createdAt',
178
- header: lang[interfaceLanguage.current].createdAt,
298
+ header: t.createdAt,
179
299
  cell: (info) => {
180
300
  const date = new Date(info.row.original.createdAt);
181
- if (currentDateFormat === 'relative') {
182
- return formatRelativeDate(date);
183
- }
184
- return formatAbsoluteDate(date);
301
+ return formatAbsoluteDateUtil(date, interfaceLanguage.current);
185
302
  }
186
303
  });
187
304
 
188
305
  return cols;
189
306
  });
190
307
 
191
- function formatRelativeDate(date: Date): string {
192
- return formatRelativeDateUtil(date, interfaceLanguage.current);
308
+ async function handleExportCsv() {
309
+ const csv = await remotes.exportFormSubmissions(formConfig.slug);
310
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
311
+ const url = URL.createObjectURL(blob);
312
+ const link = document.createElement('a');
313
+ link.href = url;
314
+ link.download = `${formConfig.slug}-submissions-${new Date().toISOString().split('T')[0]}.csv`;
315
+ link.click();
316
+ URL.revokeObjectURL(url);
317
+ exportDropdownOpen = false;
193
318
  }
194
319
 
195
- function formatAbsoluteDate(date: Date): string {
196
- return formatAbsoluteDateUtil(date, interfaceLanguage.current);
320
+ async function handleExportJson() {
321
+ const blob = new Blob([JSON.stringify(submissions, null, 2)], {
322
+ type: 'application/json'
323
+ });
324
+ const url = URL.createObjectURL(blob);
325
+ const link = document.createElement('a');
326
+ link.href = url;
327
+ link.download = `${formConfig.slug}-submissions-${new Date().toISOString().split('T')[0]}.json`;
328
+ link.click();
329
+ URL.revokeObjectURL(url);
330
+ exportDropdownOpen = false;
331
+ }
332
+
333
+ async function handleBulkMarkRead() {
334
+ for (const id of selectedIds) {
335
+ await remotes.updateFormSubmission({ id, read: true });
336
+ }
337
+ rowSelection = {};
338
+ invalidateAll();
339
+ }
340
+
341
+ async function handleBulkMarkUnread() {
342
+ for (const id of selectedIds) {
343
+ await remotes.updateFormSubmission({ id, read: false });
344
+ }
345
+ rowSelection = {};
346
+ invalidateAll();
197
347
  }
198
348
 
199
349
  async function handleBulkDelete() {
200
350
  if (selectedIds.length === 0) return;
201
- const confirmMsg =
202
- interfaceLanguage.current === 'pl'
203
- ? `Czy na pewno chcesz usunąć ${selectedIds.length} zgłoszeń?`
204
- : `Are you sure you want to delete ${selectedIds.length} submissions?`;
205
- if (!confirm(confirmMsg)) return;
206
-
351
+ if (!confirm(t.confirmBulkDelete(selectedIds.length))) return;
207
352
  await remotes.deleteFormSubmissions(selectedIds);
208
353
  rowSelection = {};
209
354
  invalidateAll();
210
355
  }
211
356
 
212
- async function handleExport() {
213
- const csv = await remotes.exportFormSubmissions(formConfig.slug);
214
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
215
- const url = URL.createObjectURL(blob);
216
- const link = document.createElement('a');
217
- link.href = url;
218
- link.download = `${formConfig.slug}-submissions-${new Date().toISOString().split('T')[0]}.csv`;
219
- link.click();
220
- URL.revokeObjectURL(url);
221
- }
222
357
  </script>
223
358
 
224
- <div class="space-y-4">
225
- <div
226
- class="flex flex-wrap items-center gap-4 rounded-lg border bg-slate-50 p-4 dark:bg-slate-800/50"
227
- >
228
- <div class="flex items-center gap-2">
229
- <span class="text-muted-foreground text-sm">{lang[interfaceLanguage.current].total}:</span>
230
- <span class="font-semibold">{stats.total}</span>
231
- </div>
232
- <div class="flex items-center gap-2">
233
- <span class="text-muted-foreground text-sm">{lang[interfaceLanguage.current].newCount}:</span>
234
- <span class="font-semibold text-blue-600">{stats.newCount}</span>
235
- </div>
236
- {#if stats.last}
237
- <div class="flex items-center gap-2">
238
- <span class="text-muted-foreground text-sm"
239
- >{lang[interfaceLanguage.current].lastSubmission}:</span
359
+ <div class="space-y-5">
360
+ <!-- Header -->
361
+ <div>
362
+ <div class="mb-1 flex flex-wrap items-center gap-3">
363
+ <h1 class="text-2xl font-bold">{formLabel}</h1>
364
+ <span
365
+ class="bg-lavender-lighter text-primary inline-flex h-6 items-center rounded-full px-2.5 text-xs font-bold"
366
+ aria-label="{stats.total} {t.submissions}"
367
+ >
368
+ {stats.total} {t.submissions}
369
+ </span>
370
+ {#if stats.unreadCount > 0}
371
+ <span
372
+ class="inline-flex h-6 items-center rounded-full bg-amber-50 px-2.5 text-xs font-semibold text-amber-600"
373
+ aria-label="{stats.unreadCount} {t.unreadCount}"
240
374
  >
241
- <span class="font-semibold">{formatRelativeDate(stats.last)}</span>
375
+ {stats.unreadCount} {t.unreadCount}
376
+ </span>
377
+ {/if}
378
+ </div>
379
+
380
+ <!-- Toolbar -->
381
+ <div class="mt-4 flex flex-wrap items-center gap-2.5">
382
+ <div class="relative min-w-[240px] flex-1">
383
+ <Search
384
+ class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2"
385
+ />
386
+ <Input
387
+ type="search"
388
+ placeholder={t.searchPlaceholder}
389
+ class="border-border bg-white pl-9"
390
+ value={searchQuery}
391
+ oninput={(e) => (searchQuery = e.currentTarget.value)}
392
+ aria-label={t.searchPlaceholder}
393
+ />
242
394
  </div>
243
- {/if}
244
- <div class="ml-auto">
245
- <Button variant="outline" size="sm" onclick={handleExport}>
246
- <Download class="mr-2 size-4" />
247
- {lang[interfaceLanguage.current].export}
248
- </Button>
395
+
396
+ <Popover.Root bind:open={filterPopoverOpen}>
397
+ <Popover.Trigger>
398
+ {#snippet child({ props })}
399
+ <Button
400
+ {...props}
401
+ variant="outline"
402
+ size="sm"
403
+ class="gap-1.5 {readFilter !== 'all'
404
+ ? 'border-primary/30 bg-lavender-lighter text-primary'
405
+ : ''}"
406
+ >
407
+ <FilterIcon class="size-3.5" />
408
+ {activeFilterLabel}
409
+ </Button>
410
+ {/snippet}
411
+ </Popover.Trigger>
412
+ <Popover.Content class="w-40 p-1" align="start">
413
+ {#each ['all', 'unread', 'read'] as filter}
414
+ <button
415
+ class="hover:bg-accent flex w-full items-center rounded-md px-2.5 py-1.5 text-sm transition-colors {readFilter ===
416
+ filter
417
+ ? 'bg-accent text-accent-foreground font-medium'
418
+ : 'text-foreground'}"
419
+ onclick={() => {
420
+ readFilter = filter as ReadFilter;
421
+ filterPopoverOpen = false;
422
+ }}
423
+ >
424
+ {filter === 'all'
425
+ ? t.filterAll
426
+ : filter === 'unread'
427
+ ? t.filterUnread
428
+ : t.filterRead}
429
+ </button>
430
+ {/each}
431
+ </Popover.Content>
432
+ </Popover.Root>
433
+
434
+ <div class="flex-1"></div>
435
+
436
+ <!-- Export dropdown -->
437
+ <Popover.Root bind:open={exportDropdownOpen}>
438
+ <Popover.Trigger>
439
+ {#snippet child({ props })}
440
+ <Button {...props} variant="outline" size="sm" class="gap-1.5">
441
+ <Download class="size-3.5" />
442
+ {t.export}
443
+ <ChevronDown class="size-3" />
444
+ </Button>
445
+ {/snippet}
446
+ </Popover.Trigger>
447
+ <Popover.Content class="w-44 p-1" align="end">
448
+ <button
449
+ class="hover:bg-accent flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-sm transition-colors"
450
+ onclick={handleExportCsv}
451
+ >
452
+ <FileSpreadsheet class="size-3.5" />
453
+ {t.exportCsv}
454
+ </button>
455
+ <button
456
+ class="hover:bg-accent flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-sm transition-colors"
457
+ onclick={handleExportJson}
458
+ >
459
+ <FileText class="size-3.5" />
460
+ {t.exportJson}
461
+ </button>
462
+ </Popover.Content>
463
+ </Popover.Root>
249
464
  </div>
250
465
  </div>
251
466
 
252
- <div class="rounded-2xl border bg-white dark:bg-slate-900">
253
- <TableToolbar
254
- {searchQuery}
255
- onSearchChange={(q) => (searchQuery = q)}
256
- {dateFormat}
257
- onDateFormatChange={(f) => (dateFormat = f)}
258
- {selectedCount}
259
- onBulkDelete={handleBulkDelete}
260
- showViewModeToggle={false}
261
- />
262
-
263
- <DataTable
264
- data={filteredData}
265
- {columns}
266
- enableSorting={true}
267
- enableSelection={true}
268
- enablePagination={true}
269
- globalFilter={searchQuery}
270
- {sorting}
271
- onSortingChange={(s) => (sorting = s)}
272
- {rowSelection}
273
- onRowSelectionChange={(s) => (rowSelection = s)}
274
- pagination={{ pageIndex: 0, pageSize }}
275
- tableRef={(t) => (tableInstance = t)}
276
- />
277
-
278
- <TablePagination
279
- pageIndex={tableInstance?.getState().pagination.pageIndex ?? 0}
280
- {pageSize}
281
- {pageCount}
282
- {totalItems}
283
- onPageChange={(p) => tableInstance?.setPageIndex(p)}
284
- onPageSizeChange={(s) => {
285
- pageSize = s;
286
- tableInstance?.setPageSize(s);
287
- }}
288
- />
289
- </div>
467
+ <!-- Table -->
468
+ {#if submissions.length === 0}
469
+ <!-- Empty state: no submissions at all -->
470
+ <div class="flex flex-col items-center justify-center rounded-2xl border bg-card px-10 py-20 text-center">
471
+ <div
472
+ class="bg-surface mb-5 flex size-[72px] items-center justify-center rounded-xl"
473
+ >
474
+ <ClipboardList class="text-muted-foreground size-8" />
475
+ </div>
476
+ <h2 class="mb-1.5 text-lg font-bold">{t.noSubmissions}</h2>
477
+ <p class="text-muted-foreground max-w-[360px] text-sm leading-relaxed">
478
+ {t.noSubmissionsDesc}
479
+ </p>
480
+ </div>
481
+ {:else}
482
+ <div class="overflow-hidden rounded-xl border bg-card shadow-sm">
483
+ <DataTable
484
+ data={filteredData}
485
+ {columns}
486
+ enableSorting={true}
487
+ enableSelection={true}
488
+ enablePagination={true}
489
+ globalFilter={searchQuery}
490
+ {sorting}
491
+ onSortingChange={(s) => (sorting = s)}
492
+ {rowSelection}
493
+ onRowSelectionChange={(s) => (rowSelection = s)}
494
+ pagination={{ pageIndex: 0, pageSize }}
495
+ tableRef={(t) => (tableInstance = t)}
496
+ emptyMessage={searchQuery || readFilter !== 'all'
497
+ ? t.noResults
498
+ : t.noSubmissions}
499
+ />
500
+
501
+ <TablePagination
502
+ pageIndex={tableInstance?.getState().pagination.pageIndex ?? 0}
503
+ {pageSize}
504
+ {pageCount}
505
+ {totalItems}
506
+ itemLabel={t.itemLabel}
507
+ onPageChange={(p) => tableInstance?.setPageIndex(p)}
508
+ onPageSizeChange={(s) => {
509
+ pageSize = s;
510
+ tableInstance?.setPageSize(s);
511
+ }}
512
+ />
513
+ </div>
514
+ {/if}
290
515
  </div>
516
+
517
+ <!-- Bulk actions bar -->
518
+ {#if selectedCount > 0}
519
+ <div
520
+ class="fixed bottom-6 left-1/2 z-50 flex -translate-x-1/2 items-center gap-3 rounded-xl bg-[#2E2558] px-5 py-2.5 text-white shadow-xl"
521
+ role="toolbar"
522
+ aria-label="Bulk actions"
523
+ >
524
+ <span class="text-sm font-bold whitespace-nowrap">{t.bulkSelected(selectedCount)}</span>
525
+ <div class="h-5 w-px bg-white/20"></div>
526
+ <Button
527
+ variant="ghost"
528
+ size="sm"
529
+ class="gap-1.5 border border-white/20 text-white hover:bg-white/10 hover:text-white"
530
+ onclick={handleBulkMarkRead}
531
+ >
532
+ <Eye class="size-3.5" />
533
+ {t.bulkMarkRead}
534
+ </Button>
535
+ <Button
536
+ variant="ghost"
537
+ size="sm"
538
+ class="gap-1.5 border border-white/20 text-white hover:bg-white/10 hover:text-white"
539
+ onclick={handleBulkMarkUnread}
540
+ >
541
+ <EyeOff class="size-3.5" />
542
+ {t.bulkMarkUnread}
543
+ </Button>
544
+ <Button
545
+ variant="ghost"
546
+ size="sm"
547
+ class="gap-1.5 border border-white/20 text-white hover:bg-white/10 hover:text-white"
548
+ onclick={handleExportCsv}
549
+ >
550
+ <Download class="size-3.5" />
551
+ {t.bulkExport}
552
+ </Button>
553
+ <Button
554
+ variant="ghost"
555
+ size="sm"
556
+ class="gap-1.5 border border-red-400/50 text-white hover:bg-red-500/20 hover:text-white"
557
+ onclick={handleBulkDelete}
558
+ >
559
+ <Trash class="size-3.5" />
560
+ {t.bulkDelete}
561
+ </Button>
562
+ <Button
563
+ variant="ghost"
564
+ size="icon"
565
+ class="ml-1 size-7 text-white/60 hover:bg-white/10 hover:text-white"
566
+ onclick={() => (rowSelection = {})}
567
+ >
568
+ <X class="size-3.5" />
569
+ </Button>
570
+ </div>
571
+ {/if}
572
+