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
@@ -1,63 +1,76 @@
1
1
  <script lang="ts">
2
2
  import type { MediaFile } from '../../../../types/media.js';
3
3
  import File from '@tabler/icons-svelte/icons/file';
4
+ import Music from '@tabler/icons-svelte/icons/music';
4
5
  import Pdf from '@tabler/icons-svelte/icons/pdf';
5
6
  import Photo from '@tabler/icons-svelte/icons/photo';
6
7
  import Video from '@tabler/icons-svelte/icons/video';
8
+ import PlayerPlay from '@tabler/icons-svelte/icons/player-play';
7
9
 
8
10
  type Props = {
9
11
  file: MediaFile;
12
+ mode?: 'card' | 'thumb';
10
13
  };
11
14
 
12
- let { file }: Props = $props();
15
+ let { file, mode = 'card' }: Props = $props();
13
16
  </script>
14
17
 
15
- <div class="checkered-bg relative aspect-square w-full overflow-hidden rounded-lg">
18
+ {#if mode === 'thumb'}
19
+ <!-- Thumb mode: just the media content for 120px thumb area -->
16
20
  {#if file.type === 'image'}
17
- <div class="absolute top-1 right-1 rounded-sm bg-background/80 dark:bg-muted/80 backdrop-blur-sm p-0.5">
18
- <Photo class="text-muted-foreground" />
19
- </div>
20
21
  <img class="pointer-events-none size-full object-contain" src={file.url} alt={file.name} />
21
22
  {:else if file.type === 'video'}
22
- <div class="absolute top-1 right-1 rounded-sm bg-background/80 dark:bg-muted/80 backdrop-blur-sm p-0.5">
23
- <Video class="text-muted-foreground" />
23
+ {#if file.thumbnailUrl}
24
+ <img class="pointer-events-none size-full object-contain" src={file.thumbnailUrl} alt={file.name} />
25
+ {/if}
26
+ <div class="ml-video-overlay">
27
+ <div class="ml-video-play">
28
+ <PlayerPlay class="h-4 w-4 text-foreground" />
29
+ </div>
30
+ </div>
31
+ {:else if file.type === 'audio'}
32
+ <div class="ml-file-thumb">
33
+ <Music class="h-8 w-8" />
24
34
  </div>
25
- <img
26
- class="pointer-events-none size-full object-contain"
27
- src={file.thumbnailUrl}
28
- alt={file.name}
29
- />
30
35
  {:else if file.type === 'pdf'}
31
- <div class="absolute top-1 right-1 rounded-sm bg-background/80 dark:bg-muted/80 backdrop-blur-sm p-0.5">
32
- <Pdf class="text-muted-foreground" />
36
+ <div class="ml-file-thumb">
37
+ <Pdf class="h-8 w-8" />
33
38
  </div>
34
- <Pdf class="pointer-events-none size-full object-contain" />
35
39
  {:else}
36
- <div class="absolute top-1 right-1 rounded-sm bg-background/80 dark:bg-muted/80 backdrop-blur-sm p-0.5">
37
- <File class="text-muted-foreground" />
40
+ <div class="ml-file-thumb">
41
+ <File class="h-8 w-8" />
38
42
  </div>
39
- <File class="pointer-events-none size-full object-contain" />
40
43
  {/if}
41
- </div>
42
-
43
- <style>
44
- .checkered-bg {
45
- background-color: #f5f5f5;
46
- background-image:
47
- linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
48
- linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
49
- linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
50
- linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
51
- background-size: 16px 16px;
52
- background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
53
- }
54
-
55
- :global(.dark) .checkered-bg {
56
- background-color: #1e293b;
57
- background-image:
58
- linear-gradient(45deg, #334155 25%, transparent 25%),
59
- linear-gradient(-45deg, #334155 25%, transparent 25%),
60
- linear-gradient(45deg, transparent 75%, #334155 75%),
61
- linear-gradient(-45deg, transparent 75%, #334155 75%);
62
- }
63
- </style>
44
+ {:else}
45
+ <!-- Card mode (legacy): full aspect-square preview -->
46
+ <div class="ml-checkered-bg relative aspect-square w-full overflow-hidden rounded-lg">
47
+ {#if file.type === 'image'}
48
+ <div class="absolute top-1 right-1 rounded-sm bg-background/80 backdrop-blur-sm p-0.5">
49
+ <Photo class="text-muted-foreground" />
50
+ </div>
51
+ <img class="pointer-events-none size-full object-contain" src={file.url} alt={file.name} />
52
+ {:else if file.type === 'video'}
53
+ <div class="absolute top-1 right-1 rounded-sm bg-background/80 backdrop-blur-sm p-0.5">
54
+ <Video class="text-muted-foreground" />
55
+ </div>
56
+ <img class="pointer-events-none size-full object-contain" src={file.thumbnailUrl} alt={file.name} />
57
+ {:else if file.type === 'audio'}
58
+ <div class="absolute top-1 right-1 rounded-sm bg-background/80 backdrop-blur-sm p-0.5">
59
+ <Music class="text-muted-foreground" />
60
+ </div>
61
+ <div class="flex items-center justify-center size-full bg-muted">
62
+ <Music class="h-10 w-10 text-text-light" />
63
+ </div>
64
+ {:else if file.type === 'pdf'}
65
+ <div class="absolute top-1 right-1 rounded-sm bg-background/80 backdrop-blur-sm p-0.5">
66
+ <Pdf class="text-muted-foreground" />
67
+ </div>
68
+ <Pdf class="pointer-events-none size-full object-contain" />
69
+ {:else}
70
+ <div class="absolute top-1 right-1 rounded-sm bg-background/80 backdrop-blur-sm p-0.5">
71
+ <File class="text-muted-foreground" />
72
+ </div>
73
+ <File class="pointer-events-none size-full object-contain" />
74
+ {/if}
75
+ </div>
76
+ {/if}
@@ -1,6 +1,7 @@
1
1
  import type { MediaFile } from '../../../../types/media.js';
2
2
  type Props = {
3
3
  file: MediaFile;
4
+ mode?: 'card' | 'thumb';
4
5
  };
5
6
  declare const FileMiniature: import("svelte").Component<Props, {}, "">;
6
7
  type FileMiniature = ReturnType<typeof FileMiniature>;
@@ -42,7 +42,7 @@
42
42
 
43
43
  {#await remotes.getFileById(fileId) then file}
44
44
  {#if file}
45
- <div class="group flex items-center gap-3 rounded-xl border border-white/10 bg-white/60 p-2 shadow-sm backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-md dark:bg-slate-800/60">
45
+ <div class="group flex items-center gap-3 rounded-xl border bg-card p-2 shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md">
46
46
  <!-- Miniaturka z lightbox -->
47
47
  <button
48
48
  type="button"
@@ -1,9 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { getRemotes } from '../../context/remotes.js';
3
3
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
4
- import Button from '../../../components/ui/button/button.svelte';
5
4
  import type { InterfaceLanguage } from '../../../types/languages.js';
6
- import Plus from '@tabler/icons-svelte/icons/plus';
7
5
  import Upload from '@tabler/icons-svelte/icons/upload';
8
6
  import X from '@tabler/icons-svelte/icons/x';
9
7
 
@@ -11,17 +9,19 @@
11
9
  const interfaceLanguage = useInterfaceLanguage();
12
10
  const lang: Record<
13
11
  InterfaceLanguage,
14
- { addFiles: string; dropFiles: string; uploading: string; uploadComplete: string }
12
+ { addFiles: string; dropFiles: string; dropHint: string; uploading: string; uploadComplete: string }
15
13
  > = {
16
14
  pl: {
17
- addFiles: 'Dodaj pliki',
15
+ addFiles: 'Prześlij pliki',
18
16
  dropFiles: 'Upuść pliki tutaj',
17
+ dropHint: 'Przeciągnij pliki tutaj',
19
18
  uploading: 'Wysyłanie...',
20
19
  uploadComplete: 'Ukończono'
21
20
  },
22
21
  en: {
23
- addFiles: 'Add files',
22
+ addFiles: 'Upload files',
24
23
  dropFiles: 'Drop files here',
24
+ dropHint: 'Drag files here',
25
25
  uploading: 'Uploading...',
26
26
  uploadComplete: 'Complete'
27
27
  }
@@ -78,7 +78,6 @@
78
78
 
79
79
  remotes.getMedia().refresh();
80
80
 
81
- // Wyczyść progress po 2s
82
81
  setTimeout(() => {
83
82
  uploadProgress = [];
84
83
  }, 2000);
@@ -94,7 +93,6 @@
94
93
  }
95
94
 
96
95
  function handleDragEnter(e: DragEvent) {
97
- // Tylko pliki - ignoruj internal drag (reorder)
98
96
  if (!e.dataTransfer?.types.includes('Files')) return;
99
97
  e.preventDefault();
100
98
  isDragging = true;
@@ -102,7 +100,6 @@
102
100
 
103
101
  function handleDragLeave(e: DragEvent) {
104
102
  e.preventDefault();
105
- // Sprawdź czy opuszczamy dropzone
106
103
  const rect = dropZoneRef?.getBoundingClientRect();
107
104
  if (rect && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) {
108
105
  return;
@@ -111,13 +108,11 @@
111
108
  }
112
109
 
113
110
  function handleDragOver(e: DragEvent) {
114
- // Tylko pliki - ignoruj internal drag
115
111
  if (!e.dataTransfer?.types.includes('Files')) return;
116
112
  e.preventDefault();
117
113
  }
118
114
 
119
115
  async function handleDrop(e: DragEvent) {
120
- // Tylko pliki - ignoruj internal drag
121
116
  if (!e.dataTransfer?.types.includes('Files')) {
122
117
  isDragging = false;
123
118
  return;
@@ -157,20 +152,23 @@
157
152
  });
158
153
  </script>
159
154
 
160
- <div class="flex items-center gap-2">
161
- <Button size="sm" variant="outline" onclick={() => inputElement.click()}>
162
- <Plus />
163
- {lang[interfaceLanguage.current].addFiles}
164
- </Button>
165
- <input
166
- bind:this={inputElement}
167
- class="h-0 w-0 overflow-hidden"
168
- multiple
169
- type="file"
170
- {accept}
171
- onchange={handleUpload}
172
- />
173
- </div>
155
+ <!-- Upload button (primary) -->
156
+ <button
157
+ type="button"
158
+ class="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-1.5 text-[13px] font-semibold text-primary-foreground transition-colors hover:bg-plum-dark whitespace-nowrap"
159
+ onclick={() => inputElement.click()}
160
+ >
161
+ <Upload class="h-4 w-4" />
162
+ {lang[interfaceLanguage.current].addFiles}
163
+ </button>
164
+ <input
165
+ bind:this={inputElement}
166
+ class="h-0 w-0 overflow-hidden absolute"
167
+ multiple
168
+ type="file"
169
+ {accept}
170
+ onchange={handleUpload}
171
+ />
174
172
 
175
173
  <!-- Drag overlay -->
176
174
  {#if isDragging}
@@ -188,7 +186,7 @@
188
186
 
189
187
  <!-- Upload progress panel -->
190
188
  {#if uploadProgress.length > 0}
191
- <div class="fixed right-4 bottom-4 z-50 w-72 rounded-lg border bg-background shadow-lg">
189
+ <div class="fixed right-4 bottom-4 z-50 w-72 rounded-lg border bg-card shadow-lg">
192
190
  <div class="flex items-center justify-between border-b px-3 py-2">
193
191
  <span class="text-sm font-medium">{lang[interfaceLanguage.current].uploading}</span>
194
192
  <button
@@ -217,7 +215,7 @@
217
215
  <div class="h-1.5 overflow-hidden rounded-full bg-muted">
218
216
  <div
219
217
  class="h-full transition-all duration-200 {item.complete
220
- ? 'bg-green-500'
218
+ ? 'bg-success'
221
219
  : item.progress === -1
222
220
  ? 'bg-destructive'
223
221
  : 'bg-primary'}"
@@ -4,14 +4,23 @@
4
4
  import FileMiniature from './file/file-miniature.svelte';
5
5
  import { flip } from 'svelte/animate';
6
6
  import Check from '@tabler/icons-svelte/icons/check';
7
+ import Photo from '@tabler/icons-svelte/icons/photo';
8
+ import Video from '@tabler/icons-svelte/icons/video';
9
+ import Music from '@tabler/icons-svelte/icons/music';
10
+ import Pdf from '@tabler/icons-svelte/icons/pdf';
11
+ import File from '@tabler/icons-svelte/icons/file';
7
12
 
8
13
  type Props = {
9
14
  files: MediaFile[];
10
15
  selected: string[] | string;
11
16
  onSelect: (file: MediaFile, event?: MouseEvent) => void;
17
+ onRangeSelect?: (fileIds: string[]) => void;
18
+ selectionMode?: boolean;
12
19
  };
13
20
 
14
- let { files, onSelect, selected }: Props = $props();
21
+ let { files, onSelect, selected, selectionMode = false, onRangeSelect }: Props = $props();
22
+
23
+ let lastClickedIndex = $state<number>(-1);
15
24
 
16
25
  const isSelected = (id: string) =>
17
26
  Array.isArray(selected) ? selected.includes(id) : selected === id;
@@ -36,57 +45,120 @@
36
45
  return files;
37
46
  }
38
47
  });
48
+
49
+ function formatFileSize(bytes: number): string {
50
+ if (!bytes) return '';
51
+ const units = ['B', 'KB', 'MB', 'GB'];
52
+ const k = 1024;
53
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
54
+ const size = bytes / Math.pow(k, i);
55
+ return `${size.toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
56
+ }
57
+
58
+ function getTypeIcon(type: string) {
59
+ switch (type) {
60
+ case 'image': return Photo;
61
+ case 'video': return Video;
62
+ case 'audio': return Music;
63
+ case 'pdf': return Pdf;
64
+ default: return File;
65
+ }
66
+ }
67
+
68
+ function handleClick(file: MediaFile, event: MouseEvent, index: number) {
69
+ if (selectionMode && event.shiftKey && lastClickedIndex >= 0 && onRangeSelect) {
70
+ const start = Math.min(lastClickedIndex, index);
71
+ const end = Math.max(lastClickedIndex, index);
72
+ const rangeIds = sortedFiles.slice(start, end + 1).map((f) => f.id);
73
+ onRangeSelect(rangeIds);
74
+ lastClickedIndex = index;
75
+ } else {
76
+ lastClickedIndex = index;
77
+ onSelect(file, event);
78
+ }
79
+ }
80
+
81
+ function handleKeydown(e: KeyboardEvent, file: MediaFile, index: number) {
82
+ if (e.key === 'Enter' || e.key === ' ') {
83
+ e.preventDefault();
84
+ lastClickedIndex = index;
85
+ onSelect(file);
86
+ }
87
+ }
39
88
  </script>
40
89
 
41
90
  {#if sortedFiles.length === 0}
42
91
  <div class="col-span-full flex flex-col items-center justify-center py-16 text-center">
43
- <div class="text-muted-foreground mb-2">
44
- <svg
45
- xmlns="http://www.w3.org/2000/svg"
46
- class="mx-auto h-12 w-12 opacity-50"
47
- fill="none"
48
- viewBox="0 0 24 24"
49
- stroke="currentColor"
50
- >
51
- <path
52
- stroke-linecap="round"
53
- stroke-linejoin="round"
54
- stroke-width="1.5"
55
- 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"
56
- />
92
+ <div class="mb-3 flex h-14 w-14 items-center justify-center rounded-xl bg-muted">
93
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
94
+ <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" />
57
95
  </svg>
58
96
  </div>
59
- <p class="text-muted-foreground text-sm">No files found</p>
97
+ <p class="text-sm font-medium text-foreground">Brak plików</p>
98
+ <p class="text-xs text-muted-foreground mt-1">Prześlij pliki lub zmień filtry</p>
60
99
  </div>
61
100
  {:else}
62
- {#each sortedFiles as file (file.id)}
63
- <button
101
+ {#each sortedFiles as file, i (file.id)}
102
+ {@const sel = isSelected(file.id)}
103
+ {@const TypeIcon = getTypeIcon(file.type)}
104
+ <div
64
105
  animate:flip={{ duration: 300 }}
65
- type="button"
66
- class="group relative block aspect-square overflow-hidden rounded-xl bg-white/60 p-1.5 shadow-sm backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-lg dark:bg-slate-800/60 {isSelected(file.id)
67
- ? 'ring-2 ring-[#2D4A77] ring-offset-2 dark:ring-[#4975AE]'
68
- : 'hover:ring-1 hover:ring-slate-200/50 dark:hover:ring-white/10'}"
69
- onclick={(e) => onSelect(file, e)}
106
+ class="file-card ml-fade-up cursor-pointer overflow-hidden rounded-xl border bg-card transition-all outline-none select-none
107
+ {sel ? 'border-primary ring-2 ring-primary' : 'border-border hover:border-lavender hover:shadow-md'}
108
+ "
109
+ role="option"
110
+ aria-selected={sel}
111
+ tabindex="0"
112
+ onclick={(e) => handleClick(file, e, i)}
113
+ onkeydown={(e) => handleKeydown(e, file, i)}
70
114
  >
71
- {#if isSelected(file.id)}
72
- <div
73
- class="absolute top-2 left-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-gradient-to-br from-[#2D4A77] to-[#4975AE] text-white shadow-md"
74
- >
75
- <Check class="h-3 w-3" />
115
+ <!-- Thumb area -->
116
+ <div class="relative h-[120px] overflow-hidden ml-checkered-bg">
117
+ <FileMiniature {file} mode="thumb" />
118
+
119
+ <!-- Type badge (frosted glass, top-left) -->
120
+ <div class="absolute top-1.5 left-1.5 z-10 inline-flex items-center gap-1 rounded-full bg-white/90 backdrop-blur-sm px-1.5 py-0.5 text-[10px] font-semibold text-muted-foreground shadow-sm">
121
+ <TypeIcon class="h-3 w-3" />
122
+ <span class="sr-only">{file.type}</span>
76
123
  </div>
77
- {/if}
78
- <!-- Tag dots -->
79
- {#if file.tags && file.tags.length > 0}
80
- <div class="absolute bottom-2 right-2 z-10 flex gap-0.5">
81
- {#each file.tags.slice(0, 3) as tag}
82
- <span class="h-2 w-2 rounded-full shadow-sm" style="background-color: {tag.color}"></span>
83
- {/each}
84
- {#if file.tags.length > 3}
85
- <span class="text-[8px] text-muted-foreground">+{file.tags.length - 3}</span>
124
+
125
+ <!-- Selection indicator (top-right) -->
126
+ {#if selectionMode}
127
+ <div class="absolute top-1.5 right-1.5 z-10 flex h-[22px] w-[22px] items-center justify-center rounded-md border-2 transition-colors
128
+ {sel ? 'border-primary bg-primary' : 'border-white/80 bg-white/50 backdrop-blur-sm'}
129
+ ">
130
+ {#if sel}
131
+ <Check class="h-3.5 w-3.5 text-primary-foreground" />
132
+ {/if}
133
+ </div>
134
+ {:else}
135
+ <div class="ml-file-check {sel ? 'visible' : ''}">
136
+ <Check class="h-3 w-3 text-primary-foreground" />
137
+ </div>
138
+ {/if}
139
+ </div>
140
+
141
+ <!-- File info -->
142
+ <div class="px-2.5 py-2">
143
+ <div class="truncate text-xs font-semibold leading-snug text-foreground">{file.name}</div>
144
+ <div class="flex items-center gap-1">
145
+ {#if file.size}
146
+ <span class="text-[11px] font-medium text-text-light">{formatFileSize(file.size)}</span>
147
+ {/if}
148
+ {#if file.tags && file.tags.length > 0}
149
+ <span class="flex items-center gap-0.5 ml-auto" role="list" aria-label="Tagi">
150
+ {#each file.tags as tag (tag.id)}
151
+ <span
152
+ class="h-2 w-2 rounded-full"
153
+ style="background-color: {tag.color}"
154
+ role="listitem"
155
+ aria-label={tag.name}
156
+ ></span>
157
+ {/each}
158
+ </span>
86
159
  {/if}
87
160
  </div>
88
- {/if}
89
- <FileMiniature {file} />
90
- </button>
161
+ </div>
162
+ </div>
91
163
  {/each}
92
164
  {/if}
@@ -3,6 +3,8 @@ type Props = {
3
3
  files: MediaFile[];
4
4
  selected: string[] | string;
5
5
  onSelect: (file: MediaFile, event?: MouseEvent) => void;
6
+ onRangeSelect?: (fileIds: string[]) => void;
7
+ selectionMode?: boolean;
6
8
  };
7
9
  declare const FilesList: import("svelte").Component<Props, {}, "">;
8
10
  type FilesList = ReturnType<typeof FilesList>;
@@ -5,21 +5,56 @@
5
5
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
6
6
  import type { InterfaceLanguage } from '../../../types/languages.js';
7
7
  import type { MediaFile } from '../../../types/media.js';
8
- import Focus from '@tabler/icons-svelte/icons/focus-2';
8
+ import Click from '@tabler/icons-svelte/icons/click';
9
+ import Keyboard from '@tabler/icons-svelte/icons/keyboard';
10
+ import HandFinger from '@tabler/icons-svelte/icons/hand-finger';
9
11
 
10
12
  const interfaceLanguage = useInterfaceLanguage();
11
- const lang: Record<InterfaceLanguage, { label: string; save: string; success: string; error: string }> = {
13
+
14
+ const lang: Record<
15
+ InterfaceLanguage,
16
+ {
17
+ hint: string;
18
+ save: string;
19
+ success: string;
20
+ error: string;
21
+ ariaLabel: string;
22
+ ariaValueText: (x: number, y: number) => string;
23
+ mouse: string;
24
+ mouseDesc: string;
25
+ keyboard: string;
26
+ keyboardDesc: string;
27
+ touch: string;
28
+ touchDesc: string;
29
+ }
30
+ > = {
12
31
  pl: {
13
- label: 'Punkt ogniskowy',
14
- save: 'Zapisz punkt ogniskowy',
32
+ hint: 'Wskaż najważniejsze miejsce na zdjęciu — przy przycinaniu do różnych rozmiarów ten punkt będzie zawsze widoczny.',
33
+ save: 'Zapisz',
15
34
  success: 'Punkt ogniskowy zapisany.',
16
- error: 'Nie udało się zapisać punktu ogniskowego.'
35
+ error: 'Nie udało się zapisać punktu ogniskowego.',
36
+ ariaLabel: 'Punkt ogniskowy — wybierz najważniejsze miejsce na zdjęciu',
37
+ ariaValueText: (x, y) => `Pozycja: ${x}% od lewej, ${y}% od góry`,
38
+ mouse: 'Mysz',
39
+ mouseDesc: 'Kliknij na zdjęcie',
40
+ keyboard: 'Klawiatura',
41
+ keyboardDesc: 'Strzałki 1%, Shift 5%',
42
+ touch: 'Dotyk',
43
+ touchDesc: 'Dotknij zdjęcie'
17
44
  },
18
45
  en: {
19
- label: 'Focal point',
20
- save: 'Save focal point',
46
+ hint: 'Point to the most important part of the image — when cropped to different sizes, this point will always stay visible.',
47
+ save: 'Save',
21
48
  success: 'Focal point saved.',
22
- error: 'Failed to save focal point.'
49
+ error: 'Failed to save focal point.',
50
+ ariaLabel: 'Focal point — select the most important part of the image',
51
+ ariaValueText: (x, y) => `Position: ${x}% from left, ${y}% from top`,
52
+ mouse: 'Mouse',
53
+ mouseDesc: 'Click on the image',
54
+ keyboard: 'Keyboard',
55
+ keyboardDesc: 'Arrows 1%, Shift 5%',
56
+ touch: 'Touch',
57
+ touchDesc: 'Tap the image'
23
58
  }
24
59
  };
25
60
 
@@ -36,11 +71,20 @@
36
71
  let saving = $state(false);
37
72
  let container: HTMLDivElement;
38
73
 
74
+ let t = $derived(lang[interfaceLanguage.current]);
75
+ let xPercent = $derived(Math.round(focalX * 100));
76
+ let yPercent = $derived(Math.round(focalY * 100));
77
+ let isDirty = $derived(focalX !== (file.focalX ?? 0.5) || focalY !== (file.focalY ?? 0.5));
78
+
79
+ function clamp(v: number) {
80
+ return Math.max(0, Math.min(1, v));
81
+ }
82
+
39
83
  function handlePointer(e: PointerEvent) {
40
84
  if (!container) return;
41
85
  const rect = container.getBoundingClientRect();
42
- focalX = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
43
- focalY = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
86
+ focalX = clamp((e.clientX - rect.left) / rect.width);
87
+ focalY = clamp((e.clientY - rect.top) / rect.height);
44
88
  }
45
89
 
46
90
  function onPointerDown(e: PointerEvent) {
@@ -54,15 +98,37 @@
54
98
  }
55
99
  }
56
100
 
101
+ function onKeyDown(e: KeyboardEvent) {
102
+ const step = e.shiftKey ? 0.05 : 0.01;
103
+ let handled = true;
104
+ switch (e.key) {
105
+ case 'ArrowLeft':
106
+ focalX = clamp(focalX - step);
107
+ break;
108
+ case 'ArrowRight':
109
+ focalX = clamp(focalX + step);
110
+ break;
111
+ case 'ArrowUp':
112
+ focalY = clamp(focalY - step);
113
+ break;
114
+ case 'ArrowDown':
115
+ focalY = clamp(focalY + step);
116
+ break;
117
+ default:
118
+ handled = false;
119
+ }
120
+ if (handled) e.preventDefault();
121
+ }
122
+
57
123
  async function save() {
58
124
  saving = true;
59
125
  try {
60
126
  await remotes.setFocalPoint({ fileId: file.id, focalX, focalY });
61
127
  file.focalX = focalX;
62
128
  file.focalY = focalY;
63
- toast.success(lang[interfaceLanguage.current].success);
129
+ toast.success(t.success);
64
130
  } catch {
65
- toast.error(lang[interfaceLanguage.current].error);
131
+ toast.error(t.error);
66
132
  } finally {
67
133
  saving = false;
68
134
  }
@@ -75,32 +141,62 @@
75
141
  });
76
142
  </script>
77
143
 
78
- <div class="space-y-2">
79
- <div class="flex items-center gap-1.5 text-xs font-medium">
80
- <Focus class="h-3.5 w-3.5 text-muted-foreground" />
81
- {lang[interfaceLanguage.current].label}
82
- </div>
144
+ <div class="space-y-3">
145
+ <!-- COGA hint -->
146
+ <p class="text-xs leading-relaxed text-text-secondary">
147
+ {t.hint}
148
+ </p>
83
149
 
84
- <!-- svelte-ignore a11y_no_static_element_interactions -->
150
+ <!-- Focal point area -->
85
151
  <div
86
152
  bind:this={container}
87
- class="relative cursor-crosshair overflow-hidden rounded-lg border select-none"
153
+ role="application"
154
+ tabindex="0"
155
+ aria-label={t.ariaLabel}
156
+ aria-valuetext={t.ariaValueText(xPercent, yPercent)}
157
+ class="relative cursor-crosshair overflow-hidden rounded-lg border select-none focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2"
88
158
  onpointerdown={onPointerDown}
89
159
  onpointermove={onPointerMove}
160
+ onkeydown={onKeyDown}
90
161
  >
91
- <img src={file.url} alt={file.alt || file.name} class="w-full pointer-events-none" draggable="false" />
162
+ <img src={file.url} alt={file.alt || file.name} class="pointer-events-none w-full" draggable="false" />
92
163
  <!-- Crosshair -->
93
164
  <div
94
- class="absolute w-6 h-6 -translate-x-1/2 -translate-y-1/2 pointer-events-none"
165
+ class="pointer-events-none absolute h-7 w-7 -translate-x-1/2 -translate-y-1/2 transition-[left,top] duration-100 ease-out"
95
166
  style="left: {focalX * 100}%; top: {focalY * 100}%;"
96
167
  >
97
168
  <div class="absolute inset-0 rounded-full border-2 border-white shadow-[0_0_0_1px_rgba(0,0,0,0.3)]"></div>
98
- <div class="absolute left-1/2 top-0 h-full w-px bg-white/80 -translate-x-px"></div>
99
- <div class="absolute top-1/2 left-0 w-full h-px bg-white/80 -translate-y-px"></div>
169
+ <div class="absolute left-1/2 top-0 h-full w-px -translate-x-px bg-white/80"></div>
170
+ <div class="absolute top-1/2 left-0 h-px w-full -translate-y-px bg-white/80"></div>
100
171
  </div>
101
172
  </div>
102
173
 
103
- <Button variant="outline" class="w-full" size="sm" onclick={save} disabled={saving}>
104
- {lang[interfaceLanguage.current].save}
105
- </Button>
174
+ <!-- Readout bar -->
175
+ <div class="flex items-center gap-3">
176
+ <span class="font-mono text-xs text-text-secondary">
177
+ X: {xPercent}%&ensp;Y: {yPercent}%
178
+ </span>
179
+ <Button variant="outline" size="sm" onclick={save} disabled={saving || !isDirty}>
180
+ {t.save}
181
+ </Button>
182
+ </div>
183
+
184
+ <!-- Instructions -->
185
+ <div class="grid grid-cols-3 gap-2">
186
+ <div class="flex flex-col items-center gap-1 rounded-md bg-surface px-2 py-2 text-center">
187
+ <Click class="h-4 w-4 text-text-light" />
188
+ <span class="text-[11px] font-semibold text-text">{t.mouse}</span>
189
+ <span class="text-[10px] leading-tight text-text-light">{t.mouseDesc}</span>
190
+ </div>
191
+ <div class="flex flex-col items-center gap-1 rounded-md bg-surface px-2 py-2 text-center">
192
+ <Keyboard class="h-4 w-4 text-text-light" />
193
+ <span class="text-[11px] font-semibold text-text">{t.keyboard}</span>
194
+ <span class="text-[10px] leading-tight text-text-light">{t.keyboardDesc}</span>
195
+ </div>
196
+ <div class="flex flex-col items-center gap-1 rounded-md bg-surface px-2 py-2 text-center">
197
+ <HandFinger class="h-4 w-4 text-text-light" />
198
+ <span class="text-[11px] font-semibold text-text">{t.touch}</span>
199
+ <span class="text-[10px] leading-tight text-text-light">{t.touchDesc}</span>
200
+ </div>
201
+ </div>
106
202
  </div>