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
@@ -12,14 +12,24 @@
12
12
  import MediaSort from './media-sort.svelte';
13
13
  import FilesList from './files-list.svelte';
14
14
  import TagSidebar from './tag-sidebar.svelte';
15
- import MediaSearch from './media-search.svelte';
16
- import BulkActionBar from './bulk-action-bar.svelte';
17
15
  import ListCheck from '@tabler/icons-svelte/icons/list-check';
16
+ import TagIcon from '@tabler/icons-svelte/icons/tag';
18
17
  import Toggle from '../../../components/ui/toggle/toggle.svelte';
18
+ import { Badge } from '../../../components/ui/badge/index.js';
19
+ import * as Popover from '../../../components/ui/popover/index.js';
20
+ import * as Sheet from '../../../components/ui/sheet/index.js';
21
+ import Button from '../../../components/ui/button/button.svelte';
22
+ import PageHeader from '../layout/page-header.svelte';
23
+ import TableToolbar from '../../client/collection/table-toolbar.svelte';
24
+ import TablePagination from '../../client/collection/table-pagination.svelte';
25
+ import BulkActionsBar from '../../client/collection/bulk-actions-bar.svelte';
26
+ import StateDisplay from '../../client/collection/state-display.svelte';
27
+ import { IsMobile } from '../../../hooks/is-mobile.svelte.js';
19
28
 
20
29
  const lang: Record<
21
30
  InterfaceLanguage,
22
31
  {
32
+ title: string;
23
33
  fileDeletedToast: string;
24
34
  currentFilePlaceholder: string;
25
35
  currentFileDesc: string;
@@ -28,10 +38,29 @@
28
38
  selectModeActive: string;
29
39
  selectModeAnnounce: string;
30
40
  selectModeOffAnnounce: string;
31
- loadMore: string;
41
+ search: string;
42
+ searchPlaceholder: string;
43
+ tagsFilterLabel: string;
44
+ typeFilterLabel: string;
45
+ tagSidebarLabel: string;
46
+ filesGridLabel: string;
47
+ filesListLabel: string;
48
+ filesLabel: string;
49
+ detailsLabel: string;
50
+ emptyTitle: string;
51
+ emptyDescription: string;
52
+ noResults: string;
53
+ bulkTag: string;
54
+ bulkSelectAll: string;
55
+ typeImage: string;
56
+ typeVideo: string;
57
+ typeAudio: string;
58
+ typePdf: string;
59
+ typeOther: string;
32
60
  }
33
61
  > = {
34
62
  pl: {
63
+ title: 'Biblioteka mediów',
35
64
  fileDeletedToast: 'Plik został usunięty',
36
65
  currentFilePlaceholder: 'Podgląd pliku',
37
66
  currentFileDesc: 'Wybierz plik z listy, aby zobaczyć szczegóły i edytować metadane.',
@@ -40,9 +69,28 @@
40
69
  selectModeActive: 'Zaznaczanie',
41
70
  selectModeAnnounce: 'Tryb zaznaczania włączony. Klikaj pliki, aby je zaznaczyć.',
42
71
  selectModeOffAnnounce: 'Tryb zaznaczania wyłączony.',
43
- loadMore: 'Wczytaj więcej'
72
+ search: 'Szukaj',
73
+ searchPlaceholder: 'Szukaj po nazwie…',
74
+ tagsFilterLabel: 'Tag',
75
+ typeFilterLabel: 'Typ',
76
+ tagSidebarLabel: 'Filtry tagów',
77
+ filesGridLabel: 'Siatka plików',
78
+ filesListLabel: 'Lista plików',
79
+ filesLabel: 'Pliki',
80
+ detailsLabel: 'Szczegóły pliku',
81
+ emptyTitle: 'Brak plików',
82
+ emptyDescription: 'Prześlij pierwszy plik, aby zobaczyć go w bibliotece.',
83
+ noResults: 'Brak wyników.',
84
+ bulkTag: 'Otaguj',
85
+ bulkSelectAll: 'Zaznacz stronę',
86
+ typeImage: 'Obraz',
87
+ typeVideo: 'Wideo',
88
+ typeAudio: 'Audio',
89
+ typePdf: 'PDF',
90
+ typeOther: 'Inne'
44
91
  },
45
92
  en: {
93
+ title: 'Media library',
46
94
  fileDeletedToast: 'File has been deleted',
47
95
  currentFilePlaceholder: 'File preview',
48
96
  currentFileDesc: 'Select a file from the list to view details and edit metadata.',
@@ -51,7 +99,25 @@
51
99
  selectModeActive: 'Selecting',
52
100
  selectModeAnnounce: 'Selection mode enabled. Click files to select them.',
53
101
  selectModeOffAnnounce: 'Selection mode disabled.',
54
- loadMore: 'Load more'
102
+ search: 'Search',
103
+ searchPlaceholder: 'Search by name…',
104
+ tagsFilterLabel: 'Tag',
105
+ typeFilterLabel: 'Type',
106
+ tagSidebarLabel: 'Tag filters',
107
+ filesGridLabel: 'File grid',
108
+ filesListLabel: 'File list',
109
+ filesLabel: 'Files',
110
+ detailsLabel: 'File details',
111
+ emptyTitle: 'No files',
112
+ emptyDescription: 'Upload your first file to see it here.',
113
+ noResults: 'No results.',
114
+ bulkTag: 'Tag',
115
+ bulkSelectAll: 'Select page',
116
+ typeImage: 'Image',
117
+ typeVideo: 'Video',
118
+ typeAudio: 'Audio',
119
+ typePdf: 'PDF',
120
+ typeOther: 'Other'
55
121
  }
56
122
  };
57
123
 
@@ -62,32 +128,67 @@
62
128
  selected?: string[] | string;
63
129
  multiple?: boolean;
64
130
  accept?: string;
131
+ showHeader?: boolean;
132
+ data?: MediaFile[];
133
+ state?: 'loading' | 'error' | 'ok';
65
134
  };
66
135
 
67
- let { selected = $bindable([]), multiple = false, accept }: Props = $props();
136
+ let {
137
+ selected = $bindable([]),
138
+ multiple = false,
139
+ accept,
140
+ showHeader = false,
141
+ data: injectedData,
142
+ state: injectedState
143
+ }: Props = $props();
144
+
145
+ const useInjectedData = $derived(injectedData !== undefined);
68
146
 
69
147
  const PAGE_SIZE = 48;
148
+ const VIEW_MODE_STORAGE_KEY = 'includio-media-view';
149
+
150
+ function loadViewMode(): 'grid' | 'list' {
151
+ if (typeof window === 'undefined') return 'grid';
152
+ try {
153
+ const stored = localStorage.getItem(VIEW_MODE_STORAGE_KEY);
154
+ return stored === 'list' ? 'list' : 'grid';
155
+ } catch {
156
+ return 'grid';
157
+ }
158
+ }
70
159
 
71
160
  let currentFile: MediaFile | null = $state(null);
72
161
  let activeTagFilter = $state<string | null>(null);
162
+ let activeTypeFilter = $state<string | null>(null);
73
163
  let searchQuery = $state('');
74
164
  let selectedFileIds = $state<string[]>([]);
75
165
  let dropZoneRef = $state<HTMLElement | null>(null);
76
166
  let selectionMode = $state(false);
77
167
  let selectionAnnouncement = $state('');
78
- let page = $state(0);
79
- let loadedFiles = $state<MediaFile[]>([]);
168
+ let pagination = $state({ pageIndex: 0, pageSize: PAGE_SIZE });
169
+ let viewMode = $state<'grid' | 'list'>(loadViewMode());
170
+
171
+ $effect(() => {
172
+ if (typeof window === 'undefined') return;
173
+ try {
174
+ localStorage.setItem(VIEW_MODE_STORAGE_KEY, viewMode);
175
+ } catch {
176
+ // ignore
177
+ }
178
+ });
179
+
180
+ const t = $derived(lang[interfaceLanguage.current]);
80
181
 
81
182
  function exitSelectionMode() {
82
183
  selectionMode = false;
83
184
  selectedFileIds = [];
84
185
  currentFile = null;
85
- selectionAnnouncement = lang[interfaceLanguage.current].selectModeOffAnnounce;
186
+ selectionAnnouncement = t.selectModeOffAnnounce;
86
187
  }
87
188
 
88
189
  function enterSelectionMode() {
89
190
  selectionMode = true;
90
- selectionAnnouncement = lang[interfaceLanguage.current].selectModeAnnounce;
191
+ selectionAnnouncement = t.selectModeAnnounce;
91
192
  }
92
193
 
93
194
  const filterData = $derived({
@@ -97,81 +198,145 @@
97
198
  search: searchQuery || undefined
98
199
  });
99
200
 
100
- // Reset page on filter/search change — track primitive sources only
101
201
  $effect(() => {
102
202
  void activeTagFilter;
203
+ void activeTypeFilter;
103
204
  void searchQuery;
104
205
  void accept;
105
206
  untrack(() => {
106
- page = 0;
107
- loadedFiles = [];
207
+ pagination = { ...pagination, pageIndex: 0 };
108
208
  });
109
209
  });
110
210
 
111
211
  const filesQuery = $derived(
112
- remotes.getMediaFiles({
113
- data: {
114
- ...filterData,
115
- limit: PAGE_SIZE,
116
- offset: page * PAGE_SIZE
117
- }
118
- })
212
+ useInjectedData
213
+ ? null
214
+ : remotes.getMediaFiles({
215
+ data: {
216
+ ...filterData,
217
+ limit: pagination.pageSize,
218
+ offset: pagination.pageIndex * pagination.pageSize
219
+ }
220
+ })
119
221
  );
120
222
 
121
223
  const countQuery = $derived(
122
- remotes.countMediaFiles({
123
- data: {
124
- tagIds: filterData.tagIds,
125
- untagged: filterData.untagged,
126
- mimeTypes: filterData.mimeTypes,
127
- search: filterData.search
128
- }
129
- })
224
+ useInjectedData
225
+ ? null
226
+ : remotes.countMediaFiles({
227
+ data: {
228
+ tagIds: filterData.tagIds,
229
+ untagged: filterData.untagged,
230
+ mimeTypes: filterData.mimeTypes,
231
+ search: filterData.search
232
+ }
233
+ })
234
+ );
235
+
236
+ const tagCountsQuery = $derived(useInjectedData ? null : remotes.getMediaTagsWithCounts());
237
+ const totalCountQuery = $derived(
238
+ useInjectedData
239
+ ? null
240
+ : remotes.countMediaFiles({ data: { mimeTypes: accept?.split(',') } })
241
+ );
242
+ const untaggedCountQuery = $derived(
243
+ useInjectedData
244
+ ? null
245
+ : remotes.countMediaFiles({ data: { mimeTypes: accept?.split(','), untagged: true } })
130
246
  );
131
247
 
132
- const tagCountsQuery = $derived(remotes.getMediaTagsWithCounts());
133
- const totalCountQuery = $derived(remotes.countMediaFiles({ data: { mimeTypes: accept?.split(',') } }));
134
- const untaggedCountQuery = $derived(remotes.countMediaFiles({ data: { mimeTypes: accept?.split(','), untagged: true } }));
248
+ const fetchedFiles = $derived(
249
+ filesQuery?.ready && filesQuery.current ? (filesQuery.current as MediaFile[]) : []
250
+ );
135
251
 
136
- // Append new page results — only react to filesQuery changes
137
- $effect(() => {
138
- const ready = filesQuery.ready;
139
- const current = filesQuery.current;
140
- if (ready && current) {
141
- untrack(() => {
142
- if (page === 0) {
143
- loadedFiles = current;
144
- } else {
145
- const existingIds = new Set(loadedFiles.map((f) => f.id));
146
- loadedFiles = [...loadedFiles, ...current.filter((f) => !existingIds.has(f.id))];
147
- }
148
- });
252
+ const allInjected = $derived<MediaFile[]>(injectedData ?? []);
253
+ const filteredInjected = $derived.by(() => {
254
+ if (!useInjectedData) return [];
255
+ const q = searchQuery.trim().toLowerCase();
256
+ let list = allInjected;
257
+ if (q) list = list.filter((f) => f.name.toLowerCase().includes(q));
258
+ if (activeTypeFilter) list = list.filter((f) => f.type === activeTypeFilter);
259
+ if (activeTagFilter && activeTagFilter !== 'untagged') {
260
+ list = list.filter((f) => f.tags.some((tag) => tag.id === activeTagFilter));
261
+ } else if (activeTagFilter === 'untagged') {
262
+ list = list.filter((f) => f.tags.length === 0);
263
+ }
264
+ return list;
265
+ });
266
+
267
+ const visibleFiles = $derived.by<MediaFile[]>(() => {
268
+ if (useInjectedData) {
269
+ const start = pagination.pageIndex * pagination.pageSize;
270
+ return filteredInjected.slice(start, start + pagination.pageSize);
149
271
  }
272
+ if (activeTypeFilter) return fetchedFiles.filter((f) => f.type === activeTypeFilter);
273
+ return fetchedFiles;
150
274
  });
151
275
 
152
- const totalCount = $derived(countQuery.ready ? (countQuery.current as number) : 0);
153
- const hasMore = $derived(loadedFiles.length < totalCount);
276
+ const totalCount = $derived(
277
+ useInjectedData
278
+ ? filteredInjected.length
279
+ : countQuery?.ready
280
+ ? (countQuery.current as number)
281
+ : 0
282
+ );
283
+
284
+ const pageCount = $derived(Math.max(1, Math.ceil(totalCount / pagination.pageSize)));
285
+
286
+ const isLoading = $derived(
287
+ useInjectedData
288
+ ? injectedState === 'loading'
289
+ : !(filesQuery?.ready ?? false) && pagination.pageIndex === 0
290
+ );
291
+ const isError = $derived(
292
+ useInjectedData ? injectedState === 'error' : Boolean(filesQuery?.error)
293
+ );
294
+ const isPopulated = $derived(!isError && !isLoading && visibleFiles.length > 0);
295
+
296
+ const tagsQueryRaw = $derived(useInjectedData ? null : remotes.getMediaTags());
297
+ const allTags = $derived<MediaTag[]>(
298
+ useInjectedData
299
+ ? Array.from(
300
+ new Map(
301
+ allInjected.flatMap((f) => f.tags).map((tag) => [tag.id, tag] as const)
302
+ ).values()
303
+ )
304
+ : ((tagsQueryRaw?.current as MediaTag[] | undefined) ?? [])
305
+ );
154
306
 
155
- let tagsQuery = $derived(remotes.getMediaTags());
307
+ const tagFilterOptions = $derived([
308
+ { value: 'untagged', label: t.tagsFilterLabel + ': —' },
309
+ ...allTags.map((tag) => ({ value: tag.id, label: tag.name }))
310
+ ]);
311
+
312
+ const typeFilterOptions = $derived([
313
+ { value: 'image', label: t.typeImage },
314
+ { value: 'video', label: t.typeVideo },
315
+ { value: 'audio', label: t.typeAudio },
316
+ { value: 'pdf', label: t.typePdf },
317
+ { value: 'other', label: t.typeOther }
318
+ ]);
319
+
320
+ const dataFilters = $derived([
321
+ { slug: 'tag', label: t.tagsFilterLabel, options: tagFilterOptions },
322
+ { slug: 'type', label: t.typeFilterLabel, options: typeFilterOptions }
323
+ ]);
324
+ const activeDataFilters = $derived({ tag: activeTagFilter, type: activeTypeFilter });
156
325
 
157
326
  function handleFileSelect(file: MediaFile, event?: MouseEvent) {
158
327
  const isModifier = event && (event.ctrlKey || event.metaKey);
159
-
160
328
  if (isModifier) {
161
- // Cmd/Ctrl+click: toggle selection, auto-enter selection mode
162
329
  if (!selectionMode) enterSelectionMode();
163
330
  selectedFileIds = selectedFileIds.includes(file.id)
164
331
  ? selectedFileIds.filter((id) => id !== file.id)
165
332
  : [...selectedFileIds, file.id];
166
333
  currentFile = file;
167
334
  } else if (selectionMode) {
168
- // Normal click in selection mode: toggle
169
335
  selectedFileIds = selectedFileIds.includes(file.id)
170
336
  ? selectedFileIds.filter((id) => id !== file.id)
171
337
  : [...selectedFileIds, file.id];
172
338
  currentFile = file;
173
339
  } else {
174
- // Normal click without selection mode: open details
175
340
  selectedFileIds = [];
176
341
  currentFile = file;
177
342
  if (multiple && Array.isArray(selected)) {
@@ -185,43 +350,41 @@
185
350
  }
186
351
 
187
352
  function handleRangeSelect(fileIds: string[]) {
188
- // Merge range into selection (union)
189
353
  const merged = new Set([...selectedFileIds, ...fileIds]);
190
354
  selectedFileIds = [...merged];
191
355
  }
192
356
 
193
357
  function refreshAll() {
194
- page = 0;
195
- loadedFiles = [];
196
- filesQuery.refresh();
197
- countQuery.refresh();
198
- tagCountsQuery.refresh();
199
- totalCountQuery.refresh();
200
- untaggedCountQuery.refresh();
358
+ pagination = { ...pagination, pageIndex: 0 };
359
+ filesQuery?.refresh();
360
+ countQuery?.refresh();
361
+ tagCountsQuery?.refresh();
362
+ totalCountQuery?.refresh();
363
+ untaggedCountQuery?.refresh();
201
364
  }
202
365
 
203
366
  async function deleteFileCommand() {
204
- if (currentFile) {
367
+ if (currentFile && !useInjectedData) {
205
368
  await remotes.deleteMediaFile(currentFile.id);
206
- toast.success(lang[interfaceLanguage.current].fileDeletedToast);
369
+ toast.success(t.fileDeletedToast);
207
370
  refreshAll();
208
371
  currentFile = null;
209
372
  }
210
373
  }
211
374
 
212
375
  async function onTagUpdate(tagIds: string[]) {
213
- if (currentFile) {
376
+ if (currentFile && !useInjectedData) {
214
377
  await remotes.setMediaFileTags({ fileId: currentFile.id, tagIds });
215
- const tags = tagsQuery.current as MediaTag[] | undefined;
378
+ const tags = tagsQueryRaw?.current as MediaTag[] | undefined;
216
379
  if (tags) {
217
- currentFile = { ...currentFile, tags: tags.filter((t) => tagIds.includes(t.id)) };
380
+ currentFile = { ...currentFile, tags: tags.filter((tag) => tagIds.includes(tag.id)) };
218
381
  }
219
382
  refreshAll();
220
383
  }
221
384
  }
222
385
 
223
386
  async function onNameUpdate(newName: string) {
224
- if (currentFile) {
387
+ if (currentFile && !useInjectedData) {
225
388
  const result = await remotes.renameMediaFile({ fileId: currentFile.id, newName });
226
389
  if (result.success === true) {
227
390
  currentFile = { ...currentFile, name: result.name, url: result.url };
@@ -233,37 +396,58 @@
233
396
  }
234
397
 
235
398
  async function handleCreateTag(name: string, color: string) {
399
+ if (useInjectedData) return;
236
400
  await remotes.createMediaTag({ name, color });
237
- await tagsQuery.refresh();
238
- await tagCountsQuery.refresh();
401
+ await tagsQueryRaw?.refresh();
402
+ await tagCountsQuery?.refresh();
239
403
  }
240
404
 
241
405
  async function handleUpdateTag(id: string, name: string, color: string) {
406
+ if (useInjectedData) return;
242
407
  await remotes.updateMediaTag({ id, name, color });
243
- await tagsQuery.refresh();
408
+ await tagsQueryRaw?.refresh();
244
409
  refreshAll();
245
410
  }
246
411
 
247
412
  async function handleDeleteTag(id: string) {
413
+ if (useInjectedData) return;
248
414
  await remotes.deleteMediaTag(id);
249
- await tagsQuery.refresh();
415
+ await tagsQueryRaw?.refresh();
250
416
  refreshAll();
251
417
  }
252
418
 
253
419
  async function handleBulkTag(tagIds: string[]) {
420
+ if (useInjectedData) return;
254
421
  const fileIds = selectedFileIds;
255
422
  await remotes.bulkSetMediaFileTags({ fileIds, tagIds });
256
423
  refreshAll();
257
424
  }
258
425
 
259
426
  async function handleBulkDelete() {
427
+ if (useInjectedData) return;
260
428
  await remotes.bulkDeleteMediaFiles({ ids: selectedFileIds });
261
- toast.success(lang[interfaceLanguage.current].bulkDeletedToast);
429
+ toast.success(t.bulkDeletedToast);
262
430
  selectedFileIds = [];
263
431
  currentFile = null;
264
432
  refreshAll();
265
433
  }
266
434
 
435
+ let bulkTagOpen = $state(false);
436
+
437
+ const isMobile = new IsMobile();
438
+ let tagSheetOpen = $state(false);
439
+ let detailsSheetOpen = $state(false);
440
+
441
+ // Auto-open the details Sheet on mobile when a file becomes selected (or
442
+ // bulk selection starts). Desktop keeps the inline aside, so this is a no-op
443
+ // when isMobile.current is false.
444
+ $effect(() => {
445
+ if (!isMobile.current) return;
446
+ if (currentFile || selectionMode) {
447
+ detailsSheetOpen = true;
448
+ }
449
+ });
450
+
267
451
  onMount(() => {
268
452
  if (Array.isArray(selected)) {
269
453
  selected = selected.filter((id) => !id.startsWith('/uploads'));
@@ -273,141 +457,336 @@
273
457
  });
274
458
  </script>
275
459
 
276
- <div class="flex h-full overflow-hidden" bind:this={dropZoneRef}>
277
- <!-- Tag sidebar (192px) -->
278
- <aside class="w-48 min-w-48 shrink-0 border-r bg-card flex flex-col overflow-hidden" aria-label="Filtry tagów">
279
- {#if tagsQuery.ready && tagsQuery.current}
280
- <TagSidebar
281
- tags={tagsQuery.current}
282
- tagCounts={tagCountsQuery.ready ? tagCountsQuery.current : []}
283
- totalCount={totalCountQuery.ready ? totalCountQuery.current : 0}
284
- untaggedCount={untaggedCountQuery.ready ? untaggedCountQuery.current : 0}
285
- activeFilter={activeTagFilter}
286
- onFilterChange={(f) => (activeTagFilter = f)}
287
- onCreateTag={handleCreateTag}
288
- onUpdateTag={handleUpdateTag}
289
- onDeleteTag={handleDeleteTag}
290
- />
291
- {/if}
292
- </aside>
293
-
294
- <!-- Main content (flex-1) -->
295
- <section class="flex flex-1 flex-col overflow-hidden" aria-label="Pliki">
296
- <!-- Toolbar -->
297
- <div class="flex items-center gap-2.5 border-b bg-card px-5 py-3 shrink-0">
298
- <MediaSearch bind:value={searchQuery} />
299
- <MediaSort />
300
- <Toggle
301
- variant="outline"
302
- size="sm"
303
- pressed={selectionMode}
304
- onPressedChange={(pressed) => pressed ? enterSelectionMode() : exitSelectionMode()}
305
- class="gap-1.5 px-2.5 text-xs font-semibold whitespace-nowrap shrink-0 {selectionMode ? 'bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground border-primary' : ''}"
306
- aria-label={selectionMode ? lang[interfaceLanguage.current].selectModeActive : lang[interfaceLanguage.current].selectMode}
307
- >
308
- <ListCheck class="h-4 w-4" />
309
- {selectionMode ? lang[interfaceLanguage.current].selectModeActive : lang[interfaceLanguage.current].selectMode}
310
- </Toggle>
311
- <div class="flex-1"></div>
312
- <FileUpload
313
- onUpload={() => refreshAll()}
314
- {accept}
315
- bind:dropZoneRef
316
- tagIds={activeTagFilter && activeTagFilter !== 'untagged' ? [activeTagFilter] : undefined}
317
- />
318
- </div>
460
+ <div class="flex h-full flex-col overflow-hidden">
461
+ {#if showHeader}
462
+ <PageHeader title={t.title} count={!isLoading && !isError ? totalCount : undefined}>
463
+ {#snippet primaryActions()}
464
+ <FileUpload
465
+ onUpload={() => refreshAll()}
466
+ {accept}
467
+ bind:dropZoneRef
468
+ tagIds={activeTagFilter && activeTagFilter !== 'untagged' ? [activeTagFilter] : undefined}
469
+ />
470
+ {/snippet}
471
+ </PageHeader>
472
+ {/if}
473
+
474
+ <div class="flex flex-1 overflow-hidden" bind:this={dropZoneRef}>
475
+ <aside
476
+ class="bg-card hidden w-48 min-w-48 shrink-0 flex-col overflow-hidden border-r md:flex"
477
+ aria-label={t.tagSidebarLabel}
478
+ >
479
+ {#if useInjectedData}
480
+ <div class="text-muted-foreground p-3 text-xs">
481
+ {allTags.length === 0 ? '—' : `${allTags.length} tag(s)`}
482
+ </div>
483
+ {:else if tagsQueryRaw?.ready && tagsQueryRaw.current}
484
+ <TagSidebar
485
+ tags={tagsQueryRaw.current}
486
+ tagCounts={tagCountsQuery?.ready ? tagCountsQuery.current : []}
487
+ totalCount={totalCountQuery?.ready ? totalCountQuery.current : 0}
488
+ untaggedCount={untaggedCountQuery?.ready ? untaggedCountQuery.current : 0}
489
+ activeFilter={activeTagFilter}
490
+ onFilterChange={(f) => (activeTagFilter = f)}
491
+ onCreateTag={handleCreateTag}
492
+ onUpdateTag={handleUpdateTag}
493
+ onDeleteTag={handleDeleteTag}
494
+ />
495
+ {/if}
496
+ </aside>
497
+
498
+ <section class="flex flex-1 flex-col overflow-hidden" aria-label={t.filesLabel}>
499
+ <div class="bg-card shrink-0 border-b px-5 py-3">
500
+ <TableToolbar
501
+ {searchQuery}
502
+ searchPlaceholder={t.searchPlaceholder}
503
+ searchLabel={t.search}
504
+ onSearchChange={(q) => (searchQuery = q)}
505
+ hideStatusFilter
506
+ {viewMode}
507
+ onViewModeChange={(m) => (viewMode = m)}
508
+ {dataFilters}
509
+ {activeDataFilters}
510
+ onDataFilterChange={(slug, value) => {
511
+ if (slug === 'tag') activeTagFilter = value;
512
+ else if (slug === 'type') activeTypeFilter = value;
513
+ }}
514
+ >
515
+ {#snippet actions()}
516
+ <Sheet.Root bind:open={tagSheetOpen}>
517
+ <Sheet.Trigger>
518
+ {#snippet child({ props })}
519
+ <Button
520
+ {...props}
521
+ variant="outline"
522
+ size="sm"
523
+ class="gap-1.5 md:hidden"
524
+ aria-label={t.tagSidebarLabel}
525
+ >
526
+ <TagIcon class="size-3.5" />
527
+ {t.tagsFilterLabel}
528
+ </Button>
529
+ {/snippet}
530
+ </Sheet.Trigger>
531
+ <Sheet.Content side="left" class="w-72 max-w-[85vw] gap-0 p-0">
532
+ <Sheet.Header class="border-b p-3">
533
+ <Sheet.Title>{t.tagSidebarLabel}</Sheet.Title>
534
+ </Sheet.Header>
535
+ <div class="flex flex-1 flex-col overflow-hidden">
536
+ {#if useInjectedData}
537
+ <div class="text-muted-foreground p-3 text-xs">
538
+ {allTags.length === 0 ? '—' : `${allTags.length} tag(s)`}
539
+ </div>
540
+ {:else if tagsQueryRaw?.ready && tagsQueryRaw.current}
541
+ <TagSidebar
542
+ tags={tagsQueryRaw.current}
543
+ tagCounts={tagCountsQuery?.ready ? tagCountsQuery.current : []}
544
+ totalCount={totalCountQuery?.ready ? totalCountQuery.current : 0}
545
+ untaggedCount={untaggedCountQuery?.ready ? untaggedCountQuery.current : 0}
546
+ activeFilter={activeTagFilter}
547
+ onFilterChange={(f) => {
548
+ activeTagFilter = f;
549
+ tagSheetOpen = false;
550
+ }}
551
+ onCreateTag={handleCreateTag}
552
+ onUpdateTag={handleUpdateTag}
553
+ onDeleteTag={handleDeleteTag}
554
+ />
555
+ {/if}
556
+ </div>
557
+ </Sheet.Content>
558
+ </Sheet.Root>
559
+ <MediaSort />
560
+ <Toggle
561
+ variant="outline"
562
+ size="sm"
563
+ pressed={selectionMode}
564
+ onPressedChange={(pressed) =>
565
+ pressed ? enterSelectionMode() : exitSelectionMode()}
566
+ class="gap-1.5 px-2.5 text-xs font-semibold whitespace-nowrap"
567
+ aria-label={selectionMode ? t.selectModeActive : t.selectMode}
568
+ >
569
+ <ListCheck class="h-4 w-4" />
570
+ {selectionMode ? t.selectModeActive : t.selectMode}
571
+ </Toggle>
572
+ {#if !showHeader}
573
+ <FileUpload
574
+ onUpload={() => refreshAll()}
575
+ {accept}
576
+ bind:dropZoneRef
577
+ tagIds={activeTagFilter && activeTagFilter !== 'untagged'
578
+ ? [activeTagFilter]
579
+ : undefined}
580
+ />
581
+ {/if}
582
+ {/snippet}
583
+ </TableToolbar>
584
+ </div>
319
585
 
320
- <!-- Selection mode announcement (sr-only) -->
321
- <div class="sr-only" aria-live="polite" role="status">{selectionAnnouncement}</div>
586
+ <div class="sr-only" aria-live="polite" role="status">{selectionAnnouncement}</div>
322
587
 
323
- <!-- File grid -->
324
- <div class="flex-1 overflow-y-auto px-5 pt-4 pb-24 scrollbar-thin" role="listbox" aria-multiselectable="true" aria-label="Siatka plików">
325
- <div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3" class:opacity-60={filesQuery.loading && page === 0} style="transition: opacity 200ms">
326
- {#if !filesQuery.ready && page === 0}
327
- {#each Array(8) as _}
328
- <Skeleton class="block h-[168px] rounded-xl" />
329
- {/each}
588
+ <div
589
+ class="scrollbar-thin flex-1 overflow-y-auto px-5 pt-4 pb-24"
590
+ role={isPopulated ? 'listbox' : undefined}
591
+ aria-multiselectable={isPopulated ? 'true' : undefined}
592
+ aria-label={isPopulated ? (viewMode === 'list' ? t.filesListLabel : t.filesGridLabel) : undefined}
593
+ >
594
+ {#if isError}
595
+ <StateDisplay kind="error" />
596
+ {:else if isLoading}
597
+ {#if viewMode === 'list'}
598
+ <div class="flex flex-col gap-1.5">
599
+ {#each Array(8) as _}
600
+ <Skeleton class="block h-[56px] rounded-lg" />
601
+ {/each}
602
+ </div>
603
+ {:else}
604
+ <div class="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3">
605
+ {#each Array(8) as _}
606
+ <Skeleton class="block h-[168px] rounded-xl" />
607
+ {/each}
608
+ </div>
609
+ {/if}
610
+ {:else if visibleFiles.length === 0}
611
+ <StateDisplay kind="empty" title={t.emptyTitle} description={t.emptyDescription} />
330
612
  {:else}
331
- <FilesList
332
- files={loadedFiles}
333
- selected={selectedFileIds.length > 0 ? selectedFileIds : (selected ?? '')}
334
- onSelect={handleFileSelect}
335
- onRangeSelect={handleRangeSelect}
336
- {selectionMode}
337
- />
613
+ <div
614
+ class={viewMode === 'list'
615
+ ? 'flex flex-col gap-1.5'
616
+ : 'grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3'}
617
+ role="presentation"
618
+ >
619
+ <FilesList
620
+ files={visibleFiles}
621
+ selected={selectedFileIds.length > 0 ? selectedFileIds : (selected ?? '')}
622
+ onSelect={handleFileSelect}
623
+ onRangeSelect={handleRangeSelect}
624
+ {selectionMode}
625
+ {viewMode}
626
+ />
627
+ </div>
628
+
629
+ {#if totalCount > pagination.pageSize}
630
+ <TablePagination
631
+ pageIndex={pagination.pageIndex}
632
+ pageSize={pagination.pageSize}
633
+ {pageCount}
634
+ totalItems={totalCount}
635
+ onPageChange={(p) => (pagination = { ...pagination, pageIndex: p })}
636
+ onPageSizeChange={(s) => (pagination = { pageIndex: 0, pageSize: s })}
637
+ />
638
+ {/if}
639
+
640
+ {#if visibleFiles.some((f) => f.tags.length > 0)}
641
+ <div class="mt-6 flex flex-wrap items-center gap-1.5">
642
+ {#each Array.from(new Set(visibleFiles.flatMap((f) => f.tags.map((tag) => tag.id)))) as tagId (tagId)}
643
+ {@const tag = allTags.find((tagItem) => tagItem.id === tagId)}
644
+ {#if tag}
645
+ <Badge variant="secondary">{tag.name}</Badge>
646
+ {/if}
647
+ {/each}
648
+ </div>
649
+ {/if}
338
650
  {/if}
339
651
  </div>
340
- {#if hasMore}
341
- <div class="flex justify-center py-6">
342
- <button
343
- type="button"
344
- class="inline-flex items-center gap-2 rounded-lg border border-border px-5 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted disabled:opacity-50"
345
- disabled={filesQuery.loading}
346
- onclick={() => page++}
347
- >
348
- {lang[interfaceLanguage.current].loadMore} ({loadedFiles.length} / {totalCount})
349
- </button>
350
- </div>
351
- {/if}
352
- </div>
353
- </section>
354
-
355
- <!-- Detail panel (384px) -->
356
- <aside class="w-96 min-w-96 shrink-0 border-l bg-card flex flex-col overflow-hidden" aria-label="Szczegóły pliku">
357
- {#if selectionMode}
358
- {#if tagsQuery.ready && tagsQuery.current}
359
- <MultiFileSummary
360
- files={loadedFiles.filter((f) => selectedFileIds.includes(f.id))}
361
- allTags={tagsQuery.current}
362
- onBulkTag={handleBulkTag}
363
- onBulkDelete={handleBulkDelete}
364
- />
365
- {/if}
366
- {:else if currentFile}
367
- {#key currentFile}
368
- {#if tagsQuery.ready && tagsQuery.current}
369
- <FileDetails
370
- file={currentFile}
371
- allTags={tagsQuery.current}
372
- onDelete={deleteFileCommand}
373
- onReplace={(updated) => {
374
- currentFile = updated;
375
- refreshAll();
376
- }}
377
- {onTagUpdate}
378
- {onNameUpdate}
652
+ </section>
653
+
654
+ <aside
655
+ class="bg-card hidden w-96 min-w-96 shrink-0 flex-col overflow-hidden border-l md:flex"
656
+ aria-label={t.detailsLabel}
657
+ >
658
+ {#if selectionMode}
659
+ {#if !useInjectedData && tagsQueryRaw?.ready && tagsQueryRaw.current}
660
+ <MultiFileSummary
661
+ files={visibleFiles.filter((f) => selectedFileIds.includes(f.id))}
662
+ allTags={tagsQueryRaw.current}
663
+ onBulkTag={handleBulkTag}
664
+ onBulkDelete={handleBulkDelete}
379
665
  />
380
666
  {/if}
381
- {/key}
382
- {:else}
383
- <!-- Empty state -->
384
- <div class="flex flex-1 flex-col items-center justify-center px-5 py-10 text-center">
385
- <div class="mb-3.5 flex h-14 w-14 items-center justify-center rounded-xl bg-muted">
386
- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-text-light" fill="none" viewBox="0 0 24 24" stroke="currentColor">
387
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
388
- </svg>
667
+ {:else if currentFile}
668
+ {#key currentFile}
669
+ {#if !useInjectedData && tagsQueryRaw?.ready && tagsQueryRaw.current}
670
+ <FileDetails
671
+ file={currentFile}
672
+ allTags={tagsQueryRaw.current}
673
+ onDelete={deleteFileCommand}
674
+ onReplace={(updated) => {
675
+ currentFile = updated;
676
+ refreshAll();
677
+ }}
678
+ {onTagUpdate}
679
+ {onNameUpdate}
680
+ />
681
+ {/if}
682
+ {/key}
683
+ {:else}
684
+ <div class="flex flex-1 flex-col items-center justify-center px-5 py-10 text-center">
685
+ <div class="bg-muted mb-3.5 flex h-14 w-14 items-center justify-center rounded-xl">
686
+ <svg
687
+ xmlns="http://www.w3.org/2000/svg"
688
+ class="text-text-light h-6 w-6"
689
+ fill="none"
690
+ viewBox="0 0 24 24"
691
+ stroke="currentColor"
692
+ >
693
+ <path
694
+ stroke-linecap="round"
695
+ stroke-linejoin="round"
696
+ stroke-width="1.5"
697
+ d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
698
+ />
699
+ </svg>
700
+ </div>
701
+ <p class="text-foreground text-sm font-bold">{t.currentFilePlaceholder}</p>
702
+ <p class="text-muted-foreground mt-1 text-[13px] leading-relaxed">{t.currentFileDesc}</p>
389
703
  </div>
390
- <p class="text-sm font-bold text-foreground">{lang[interfaceLanguage.current].currentFilePlaceholder}</p>
391
- <p class="mt-1 text-[13px] leading-relaxed text-muted-foreground">{lang[interfaceLanguage.current].currentFileDesc}</p>
392
- </div>
393
- {/if}
394
- </aside>
704
+ {/if}
705
+ </aside>
706
+ </div>
395
707
  </div>
396
708
 
397
- <!-- Bulk action bar -->
398
- {#if selectedFileIds.length > 0 && tagsQuery.ready && tagsQuery.current}
399
- <BulkActionBar
400
- selectedCount={selectedFileIds.length}
401
- totalCount={totalCount}
402
- tags={tagsQuery.current}
403
- onBulkTag={handleBulkTag}
404
- onBulkDelete={handleBulkDelete}
405
- onClear={exitSelectionMode}
406
- onSelectAll={() => { selectedFileIds = loadedFiles.map((f) => f.id); }}
407
- onCreateTag={handleCreateTag}
408
- onUpdateTagColor={async (id, color) => {
409
- const tag = tagsQuery.current?.find((t) => t.id === id);
410
- if (tag) await handleUpdateTag(id, tag.name, color);
411
- }}
412
- />
413
- {/if}
709
+ <Sheet.Root bind:open={detailsSheetOpen}>
710
+ <Sheet.Content side="right" class="w-full max-w-md gap-0 p-0 md:hidden">
711
+ <Sheet.Header class="border-b p-3">
712
+ <Sheet.Title>{t.detailsLabel}</Sheet.Title>
713
+ </Sheet.Header>
714
+ <div class="flex flex-1 flex-col overflow-y-auto">
715
+ {#if selectionMode}
716
+ {#if !useInjectedData && tagsQueryRaw?.ready && tagsQueryRaw.current}
717
+ <MultiFileSummary
718
+ files={visibleFiles.filter((f) => selectedFileIds.includes(f.id))}
719
+ allTags={tagsQueryRaw.current}
720
+ onBulkTag={handleBulkTag}
721
+ onBulkDelete={handleBulkDelete}
722
+ />
723
+ {/if}
724
+ {:else if currentFile}
725
+ {#key currentFile}
726
+ {#if !useInjectedData && tagsQueryRaw?.ready && tagsQueryRaw.current}
727
+ <FileDetails
728
+ file={currentFile}
729
+ allTags={tagsQueryRaw.current}
730
+ onDelete={() => {
731
+ deleteFileCommand();
732
+ detailsSheetOpen = false;
733
+ }}
734
+ onReplace={(updated) => {
735
+ currentFile = updated;
736
+ refreshAll();
737
+ }}
738
+ {onTagUpdate}
739
+ {onNameUpdate}
740
+ />
741
+ {/if}
742
+ {/key}
743
+ {/if}
744
+ </div>
745
+ </Sheet.Content>
746
+ </Sheet.Root>
747
+
748
+ <BulkActionsBar
749
+ selectedCount={selectedFileIds.length}
750
+ onDelete={handleBulkDelete}
751
+ onClear={exitSelectionMode}
752
+ >
753
+ {#snippet actions()}
754
+ <Button
755
+ variant="ghost"
756
+ size="sm"
757
+ class="border border-white/20 text-white hover:bg-white/10 hover:text-white"
758
+ onclick={() => (selectedFileIds = visibleFiles.map((f) => f.id))}
759
+ >
760
+ {t.bulkSelectAll}
761
+ </Button>
762
+ <Popover.Root bind:open={bulkTagOpen}>
763
+ <Popover.Trigger>
764
+ {#snippet child({ props })}
765
+ <Button
766
+ {...props}
767
+ variant="ghost"
768
+ size="sm"
769
+ class="border border-white/20 text-white hover:bg-white/10 hover:text-white"
770
+ >
771
+ {t.bulkTag}
772
+ </Button>
773
+ {/snippet}
774
+ </Popover.Trigger>
775
+ <Popover.Content class="w-56 p-1" align="center" side="top">
776
+ {#each allTags as tag (tag.id)}
777
+ <button
778
+ type="button"
779
+ class="hover:bg-accent flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors"
780
+ onclick={() => {
781
+ handleBulkTag([tag.id]);
782
+ bulkTagOpen = false;
783
+ }}
784
+ >
785
+ <span class="size-2 rounded-full" style="background:{tag.color}"></span>
786
+ {tag.name}
787
+ </button>
788
+ {/each}
789
+ </Popover.Content>
790
+ </Popover.Root>
791
+ {/snippet}
792
+ </BulkActionsBar>