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.
- package/CHANGELOG.md +68 -0
- package/ROADMAP.md +18 -13
- package/dist/admin/api/accept-invite.js +1 -5
- package/dist/admin/api/invite.js +7 -16
- package/dist/admin/client/account/account-page.svelte +20 -50
- package/dist/admin/client/account/lang.d.ts +15 -23
- package/dist/admin/client/account/lang.js +51 -67
- package/dist/admin/client/account/preferences-section.svelte +26 -84
- package/dist/admin/client/account/profile-section.svelte +60 -40
- package/dist/admin/client/account/schema.d.ts +11 -3
- package/dist/admin/client/account/schema.js +25 -16
- package/dist/admin/client/account/security-section.svelte +139 -105
- package/dist/admin/client/account/sessions-section.svelte +35 -34
- package/dist/admin/client/admin/admin-after-login-layout-content.svelte +3 -5
- package/dist/admin/client/admin/admin-layout.svelte +3 -2
- package/dist/admin/client/admin/admin-preloader.svelte +36 -0
- package/dist/admin/client/admin/admin-preloader.svelte.d.ts +18 -0
- package/dist/admin/client/admin/dashboard-page.svelte +55 -41
- package/dist/admin/client/collection/a11y-score-cell.svelte +45 -0
- package/dist/admin/client/collection/a11y-score-cell.svelte.d.ts +6 -0
- package/dist/admin/client/collection/bulk-actions-bar.svelte +83 -0
- package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +9 -0
- package/dist/admin/client/collection/collection-entries.svelte +255 -260
- package/dist/admin/client/collection/collection-view.svelte.d.ts +4 -3
- package/dist/admin/client/collection/collection-view.svelte.js +9 -5
- package/dist/admin/client/collection/collection.svelte +22 -12
- package/dist/admin/client/collection/data-table.svelte +50 -39
- package/dist/admin/client/collection/data-table.svelte.d.ts +1 -0
- package/dist/admin/client/collection/date-cell.svelte +7 -5
- package/dist/admin/client/collection/date-cell.svelte.d.ts +1 -1
- package/dist/admin/client/collection/empty-state.svelte +28 -0
- package/dist/admin/client/collection/empty-state.svelte.d.ts +9 -0
- package/dist/admin/client/collection/entry-link.svelte +10 -4
- package/dist/admin/client/collection/entry-link.svelte.d.ts +1 -0
- package/dist/admin/client/collection/grid-view.svelte +21 -23
- package/dist/admin/client/collection/grid-view.svelte.d.ts +1 -2
- package/dist/admin/client/collection/row-actions.svelte +60 -0
- package/dist/admin/client/collection/row-actions.svelte.d.ts +9 -0
- package/dist/admin/client/collection/status-badge.svelte +7 -8
- package/dist/admin/client/collection/table-pagination.svelte +122 -79
- package/dist/admin/client/collection/table-pagination.svelte.d.ts +1 -0
- package/dist/admin/client/collection/table-toolbar.svelte +108 -88
- package/dist/admin/client/collection/table-toolbar.svelte.d.ts +8 -9
- package/dist/admin/client/entry/entry-form.svelte +109 -1
- package/dist/admin/client/entry/entry-header.svelte +96 -37
- package/dist/admin/client/entry/entry-header.svelte.d.ts +5 -0
- package/dist/admin/client/entry/entry.svelte +171 -60
- package/dist/admin/client/entry/header/a11y-validator.d.ts +46 -0
- package/dist/admin/client/entry/header/a11y-validator.js +311 -0
- package/dist/admin/client/entry/header/publish-panel.svelte +373 -131
- package/dist/admin/client/entry/header/publish-panel.svelte.d.ts +4 -0
- package/dist/admin/client/entry/header/save-indicator.svelte +33 -23
- package/dist/admin/client/entry/header/schedule-popover.svelte +1 -1
- package/dist/admin/client/entry/header/status-badge.svelte +25 -118
- package/dist/admin/client/entry/header/version-history-sheet.svelte +314 -98
- package/dist/admin/client/form/form-submission/form-submission.svelte +271 -83
- package/dist/admin/client/form/form-submission/submission-field.svelte +12 -12
- package/dist/admin/client/form/form-submissions.svelte +421 -139
- package/dist/admin/client/form/submission-link.svelte +8 -2
- package/dist/admin/client/form/submission-link.svelte.d.ts +1 -0
- package/dist/admin/client/form/submission-status-badge.svelte +18 -4
- package/dist/admin/client/form/submission-status-badge.svelte.d.ts +1 -0
- package/dist/admin/client/login/lang.d.ts +32 -0
- package/dist/admin/client/login/lang.js +66 -2
- package/dist/admin/client/login/login-form.svelte +237 -95
- package/dist/admin/client/login/login-form.svelte.d.ts +2 -17
- package/dist/admin/client/login/login-page.svelte +34 -98
- package/dist/admin/client/login/reset-password-page.svelte +235 -0
- package/dist/admin/client/login/reset-password-page.svelte.d.ts +4 -0
- package/dist/admin/client/login/schema.d.ts +15 -0
- package/dist/admin/client/login/schema.js +21 -0
- package/dist/admin/client/users/accept-invite-page.svelte +166 -37
- package/dist/admin/client/users/create-user-dialog.svelte +15 -7
- package/dist/admin/client/users/delete-user-dialog.svelte +81 -16
- package/dist/admin/client/users/delete-user-dialog.svelte.d.ts +4 -1
- package/dist/admin/client/users/edit-user-dialog.svelte +3 -0
- package/dist/admin/client/users/invite-user-dialog.svelte +16 -3
- package/dist/admin/client/users/lang.d.ts +27 -0
- package/dist/admin/client/users/lang.js +64 -10
- package/dist/admin/client/users/pending-invitations.svelte +59 -23
- package/dist/admin/client/users/users-page.svelte +471 -72
- package/dist/admin/components/accessibility/accessibility-overview.svelte +2 -7
- package/dist/admin/components/dashboard/a11y-gauge.svelte +90 -0
- package/dist/admin/components/dashboard/a11y-gauge.svelte.d.ts +18 -0
- package/dist/admin/components/dashboard/accessibility-hub.svelte +13 -12
- package/dist/admin/components/dashboard/form-submissions-widget.svelte +71 -113
- package/dist/admin/components/dashboard/index.d.ts +4 -2
- package/dist/admin/components/dashboard/index.js +4 -2
- package/dist/admin/components/dashboard/recent-activity.svelte +53 -75
- package/dist/admin/components/dashboard/recent-entries.svelte +94 -0
- package/dist/admin/components/dashboard/recent-entries.svelte.d.ts +18 -0
- package/dist/admin/components/dashboard/stat-card.svelte +2 -2
- package/dist/admin/components/dashboard/tip-of-the-day.svelte +109 -0
- package/dist/admin/components/dashboard/tip-of-the-day.svelte.d.ts +3 -0
- package/dist/admin/components/dashboard/welcome-header.svelte +45 -0
- package/dist/admin/components/dashboard/welcome-header.svelte.d.ts +3 -0
- package/dist/admin/components/fields/{array-field.svelte → blocks-field.svelte} +4 -4
- package/dist/admin/components/fields/{array-field.svelte.d.ts → blocks-field.svelte.d.ts} +5 -5
- package/dist/admin/components/fields/content-field.svelte +27 -0
- package/dist/admin/components/fields/content-field.svelte.d.ts +31 -0
- package/dist/admin/components/fields/field-renderer.svelte +9 -7
- package/dist/admin/components/fields/image-field.svelte +2 -2
- package/dist/admin/components/fields/media-field.svelte +2 -2
- package/dist/admin/components/fields/seo-field.svelte +205 -25
- package/dist/admin/components/fields/simple-array-field.svelte +289 -0
- package/dist/admin/components/fields/simple-array-field.svelte.d.ts +30 -0
- package/dist/admin/components/fields/slug-field.svelte +3 -2
- package/dist/admin/components/fields/standalone-field-renderer.svelte +148 -0
- package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +9 -0
- package/dist/admin/components/fields/text-field-wrapper.svelte +13 -1
- package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
- package/dist/admin/components/fields/url-field.svelte +5 -4
- package/dist/admin/components/layout/app-sidebar.svelte +27 -24
- package/dist/admin/components/layout/lang.d.ts +6 -0
- package/dist/admin/components/layout/lang.js +13 -1
- package/dist/admin/components/layout/layout-renderer.svelte +352 -0
- package/dist/admin/components/layout/layout-renderer.svelte.d.ts +14 -0
- package/dist/admin/components/layout/nav-breadcrumbs.svelte +4 -4
- package/dist/admin/components/layout/nav-collections.svelte +65 -36
- package/dist/admin/components/layout/nav-footer.svelte +31 -0
- package/dist/admin/components/layout/nav-footer.svelte.d.ts +18 -0
- package/dist/admin/components/layout/nav-forms.svelte +55 -30
- package/dist/admin/components/layout/nav-main.svelte +14 -52
- package/dist/admin/components/layout/nav-search.svelte +4 -3
- package/dist/admin/components/layout/nav-singletons.svelte +59 -17
- package/dist/admin/components/layout/nav-singletons.svelte.d.ts +17 -8
- package/dist/admin/components/layout/site-header.svelte +74 -13
- package/dist/admin/components/media/alt-input.svelte +32 -22
- package/dist/admin/components/media/bulk-action-bar.svelte +139 -150
- package/dist/admin/components/media/file/file-details.svelte +299 -217
- package/dist/admin/components/media/file/file-miniature.svelte +54 -41
- package/dist/admin/components/media/file/file-miniature.svelte.d.ts +1 -0
- package/dist/admin/components/media/file/file-preview.svelte +1 -1
- package/dist/admin/components/media/file-upload.svelte +24 -26
- package/dist/admin/components/media/files-list.svelte +112 -40
- package/dist/admin/components/media/files-list.svelte.d.ts +2 -0
- package/dist/admin/components/media/focal-point-input.svelte +122 -26
- package/dist/admin/components/media/media-library.svelte +127 -70
- package/dist/admin/components/media/media-search.svelte +6 -6
- package/dist/admin/components/media/media-sort.svelte +3 -1
- package/dist/admin/components/media/multi-file-summary.svelte +88 -68
- package/dist/admin/components/media/tag-combobox.svelte +141 -66
- package/dist/admin/components/media/tag-combobox.svelte.d.ts +1 -0
- package/dist/admin/components/media/tag-sidebar.svelte +139 -121
- package/dist/admin/components/tiptap/FigureNodeView.svelte +144 -15
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +254 -0
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte.d.ts +4 -0
- package/dist/admin/components/tiptap/SlashCommandPopup.svelte +212 -0
- package/dist/admin/components/tiptap/SlashCommandPopup.svelte.d.ts +8 -0
- package/dist/admin/components/tiptap/content-editor.svelte +280 -0
- package/dist/admin/components/tiptap/content-editor.svelte.d.ts +9 -0
- package/dist/admin/components/tiptap/editor-toolbar.svelte +230 -0
- package/dist/admin/components/tiptap/editor-toolbar.svelte.d.ts +16 -0
- package/dist/admin/components/tiptap/heading-a11y-plugin.d.ts +2 -0
- package/dist/admin/components/tiptap/heading-a11y-plugin.js +67 -0
- package/dist/admin/components/tiptap/image-dialog.svelte +172 -11
- package/dist/admin/components/tiptap/inline-block-node.d.ts +19 -0
- package/dist/admin/components/tiptap/inline-block-node.js +98 -0
- package/dist/admin/components/tiptap/link-dialog.svelte +9 -4
- package/dist/admin/components/tiptap/slash-command.d.ts +17 -0
- package/dist/admin/components/tiptap/slash-command.js +181 -0
- package/dist/admin/components/tiptap/structured-content-utils.d.ts +21 -0
- package/dist/admin/components/tiptap/structured-content-utils.js +150 -0
- package/dist/admin/components/tiptap/tiptap-editor.svelte +18 -190
- package/dist/admin/email/invite-template.d.ts +8 -0
- package/dist/admin/email/invite-template.js +99 -0
- package/dist/admin/email/reset-password-template.d.ts +7 -0
- package/dist/admin/email/reset-password-template.js +96 -0
- package/dist/admin/remote/ai.remote.d.ts +1 -0
- package/dist/admin/remote/ai.remote.js +4 -1
- package/dist/admin/remote/entry.remote.d.ts +8 -0
- package/dist/admin/remote/entry.remote.js +53 -4
- package/dist/admin/remote/preview.remote.js +2 -1
- package/dist/admin/shared/password-schema.d.ts +5 -0
- package/dist/admin/shared/password-schema.js +10 -0
- package/dist/admin/styles/admin.css +1530 -151
- package/dist/admin/utils/formatDate.d.ts +1 -0
- package/dist/admin/utils/formatDate.js +8 -0
- package/dist/admin/utils/roleLabel.d.ts +2 -0
- package/dist/admin/utils/roleLabel.js +13 -0
- package/dist/ai-claude/index.d.ts +2 -0
- package/dist/ai-claude/index.js +56 -0
- package/dist/cms/runtime/api.d.ts +6 -1
- package/dist/cms/runtime/api.js +3 -0
- package/dist/cms/runtime/schemas.d.ts +9 -1
- package/dist/cms/runtime/schemas.js +8 -0
- package/dist/cms/runtime/types.d.ts +82 -10
- package/dist/cms/runtime/types.js +4 -0
- package/dist/components/ui/accordion/accordion.stories.svelte +39 -0
- package/dist/components/ui/accordion/accordion.stories.svelte.d.ts +27 -0
- package/dist/components/ui/alert/alert.stories.svelte +53 -0
- package/dist/components/ui/alert/alert.stories.svelte.d.ts +27 -0
- package/dist/components/ui/alert/alert.svelte +5 -0
- package/dist/components/ui/alert/alert.svelte.d.ts +9 -0
- package/dist/components/ui/avatar/avatar.stories.svelte +16 -0
- package/dist/components/ui/avatar/avatar.stories.svelte.d.ts +27 -0
- package/dist/components/ui/badge/badge.stories.svelte +33 -0
- package/dist/components/ui/badge/badge.stories.svelte.d.ts +27 -0
- package/dist/components/ui/breadcrumb/breadcrumb.stories.svelte +33 -0
- package/dist/components/ui/breadcrumb/breadcrumb.stories.svelte.d.ts +27 -0
- package/dist/components/ui/button/button.stories.svelte +43 -0
- package/dist/components/ui/button/button.stories.svelte.d.ts +27 -0
- package/dist/components/ui/button/button.svelte +1 -2
- package/dist/components/ui/button/button.svelte.d.ts +0 -3
- package/dist/components/ui/button-group/button-group-separator.svelte.d.ts +1 -1
- package/dist/components/ui/card/card.stories.svelte +42 -0
- package/dist/components/ui/card/card.stories.svelte.d.ts +27 -0
- package/dist/components/ui/command/command.stories.svelte +51 -0
- package/dist/components/ui/command/command.stories.svelte.d.ts +27 -0
- package/dist/components/ui/dialog/dialog.stories.svelte +29 -0
- package/dist/components/ui/dialog/dialog.stories.svelte.d.ts +27 -0
- package/dist/components/ui/field/field-label.svelte.d.ts +1 -1
- package/dist/components/ui/field/field.stories.svelte +21 -0
- package/dist/components/ui/field/field.stories.svelte.d.ts +27 -0
- package/dist/components/ui/input/input.stories.svelte +40 -0
- package/dist/components/ui/input/input.stories.svelte.d.ts +27 -0
- package/dist/components/ui/input/input.svelte +2 -4
- package/dist/components/ui/item/item-separator.svelte.d.ts +1 -1
- package/dist/components/ui/label/label.stories.svelte +20 -0
- package/dist/components/ui/label/label.stories.svelte.d.ts +27 -0
- package/dist/components/ui/popover/popover.stories.svelte +29 -0
- package/dist/components/ui/popover/popover.stories.svelte.d.ts +27 -0
- package/dist/components/ui/select/select-group-heading.svelte.d.ts +1 -1
- package/dist/components/ui/select/select.stories.svelte +23 -0
- package/dist/components/ui/select/select.stories.svelte.d.ts +27 -0
- package/dist/components/ui/separator/separator.stories.svelte +24 -0
- package/dist/components/ui/separator/separator.stories.svelte.d.ts +27 -0
- package/dist/components/ui/sheet/sheet.stories.svelte +29 -0
- package/dist/components/ui/sheet/sheet.stories.svelte.d.ts +27 -0
- package/dist/components/ui/sidebar/sidebar-group.svelte +3 -3
- package/dist/components/ui/sidebar/sidebar-group.svelte.d.ts +2 -2
- package/dist/components/ui/sidebar/sidebar-menu-button.svelte +28 -30
- package/dist/components/ui/sidebar/sidebar-menu-button.svelte.d.ts +7 -7
- package/dist/components/ui/sidebar/sidebar-separator.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-trigger.svelte +4 -4
- package/dist/components/ui/sonner/sonner.stories.svelte +22 -0
- package/dist/components/ui/sonner/sonner.stories.svelte.d.ts +26 -0
- package/dist/components/ui/sonner/sonner.svelte +8 -2
- package/dist/components/ui/sonner/toast-demo.svelte +29 -0
- package/dist/components/ui/sonner/toast-demo.svelte.d.ts +6 -0
- package/dist/components/ui/textarea/textarea.stories.svelte +22 -0
- package/dist/components/ui/textarea/textarea.stories.svelte.d.ts +27 -0
- package/dist/components/ui/textarea/textarea.svelte +0 -2
- package/dist/components/ui/toggle/toggle.stories.svelte +22 -0
- package/dist/components/ui/toggle/toggle.stories.svelte.d.ts +27 -0
- package/dist/components/ui/toggle-group/toggle-group.stories.svelte +17 -0
- package/dist/components/ui/toggle-group/toggle-group.stories.svelte.d.ts +27 -0
- package/dist/components/ui/tooltip/tooltip.stories.svelte +26 -0
- package/dist/components/ui/tooltip/tooltip.stories.svelte.d.ts +27 -0
- package/dist/core/fields/fieldSchemaToTs.d.ts +1 -0
- package/dist/core/fields/fieldSchemaToTs.js +133 -1
- package/dist/core/fields/layoutUtils.d.ts +17 -0
- package/dist/core/fields/layoutUtils.js +149 -0
- package/dist/core/fields/structuredToHtml.d.ts +9 -0
- package/dist/core/fields/structuredToHtml.js +161 -0
- package/dist/core/server/entries/operations/create.js +2 -1
- package/dist/core/server/entries/operations/get.js +8 -6
- package/dist/core/server/entries/operations/update.d.ts +3 -0
- package/dist/core/server/entries/operations/update.js +30 -2
- package/dist/core/server/fields/queryStructuredContent.d.ts +15 -0
- package/dist/core/server/fields/queryStructuredContent.js +65 -0
- package/dist/core/server/fields/resolveImageFields.js +51 -2
- package/dist/core/server/fields/resolveRelationFields.js +2 -2
- package/dist/core/server/fields/resolveRichtextLinks.js +80 -13
- package/dist/core/server/fields/resolveUrlFields.js +57 -6
- package/dist/core/server/fields/slugResolver.d.ts +10 -0
- package/dist/core/server/fields/slugResolver.js +34 -0
- package/dist/core/server/generator/fields.js +15 -4
- package/dist/core/server/generator/generator.js +3 -2
- package/dist/files-local/index.js +126 -64
- package/dist/server/auth.d.ts +5 -0
- package/dist/server/auth.js +12 -1
- package/dist/sveltekit/components/structured-content.svelte +204 -0
- package/dist/sveltekit/components/structured-content.svelte.d.ts +21 -0
- package/dist/sveltekit/config.d.ts +13 -3
- package/dist/sveltekit/index.d.ts +3 -0
- package/dist/sveltekit/index.js +3 -0
- package/dist/sveltekit/server/handle.js +1 -0
- package/dist/types/config.d.ts +3 -0
- package/dist/types/fields.d.ts +19 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/layout.d.ts +54 -0
- package/dist/types/layout.js +6 -0
- package/dist/types/structured-content.d.ts +63 -0
- package/dist/types/structured-content.js +1 -0
- package/dist/updates/0.1.5/index.d.ts +2 -0
- package/dist/updates/0.1.5/index.js +18 -0
- package/dist/updates/0.2.0/index.d.ts +2 -0
- package/dist/updates/0.2.0/index.js +11 -0
- package/dist/updates/0.2.2/index.d.ts +2 -0
- package/dist/updates/0.2.2/index.js +13 -0
- package/dist/updates/0.5.0/index.d.ts +2 -0
- package/dist/updates/0.5.0/index.js +14 -0
- package/dist/updates/index.js +5 -1
- 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
|
-
|
|
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
|
-
|
|
23
|
-
<
|
|
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="
|
|
32
|
-
<Pdf class="
|
|
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="
|
|
37
|
-
<File class="
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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}
|
|
@@ -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
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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-
|
|
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-
|
|
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="
|
|
44
|
-
<svg
|
|
45
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
save: 'Zapisz
|
|
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
|
-
|
|
20
|
-
save: 'Save
|
|
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 =
|
|
43
|
-
focalY =
|
|
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(
|
|
129
|
+
toast.success(t.success);
|
|
64
130
|
} catch {
|
|
65
|
-
toast.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-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{
|
|
82
|
-
</
|
|
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
|
-
<!--
|
|
150
|
+
<!-- Focal point area -->
|
|
85
151
|
<div
|
|
86
152
|
bind:this={container}
|
|
87
|
-
|
|
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="
|
|
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
|
|
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
|
|
99
|
-
<div class="absolute top-1/2 left-0 w-full
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
174
|
+
<!-- Readout bar -->
|
|
175
|
+
<div class="flex items-center gap-3">
|
|
176
|
+
<span class="font-mono text-xs text-text-secondary">
|
|
177
|
+
X: {xPercent}% 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>
|