From 85a61a7c96ef0e9ce7117cf9aa89cca945843bd5 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Wed, 28 Jan 2026 17:32:59 +0100 Subject: [PATCH] refactor: Componentize HorizontalScrollCarousel --- .../ChatAttachmentsList.svelte | 186 ++++++------------ .../app/misc/HorizontalScrollCarousel.svelte | 93 +++++++++ .../src/lib/components/app/misc/index.ts | 7 + 3 files changed, 163 insertions(+), 123 deletions(-) create mode 100644 tools/server/webui/src/lib/components/app/misc/HorizontalScrollCarousel.svelte diff --git a/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList.svelte b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList.svelte index 66bd59567f..deb8cb9eef 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList.svelte @@ -2,14 +2,15 @@ import { ChatAttachmentMcpPrompt, ChatAttachmentThumbnailImage, - ChatAttachmentThumbnailFile + ChatAttachmentThumbnailFile, + HorizontalScrollCarousel, + DialogChatAttachmentPreview, + DialogChatAttachmentsViewAll } from '$lib/components/app'; import { Button } from '$lib/components/ui/button'; - import { ChevronLeft, ChevronRight } from '@lucide/svelte'; - import { DialogChatAttachmentPreview, DialogChatAttachmentsViewAll } from '$lib/components/app'; - import { getAttachmentDisplayItems } from '$lib/utils'; import { AttachmentType } from '$lib/enums'; import type { DatabaseMessageExtraMcpPrompt } from '$lib/types'; + import { getAttachmentDisplayItems } from '$lib/utils'; interface Props { class?: string; @@ -47,12 +48,10 @@ let displayItems = $derived(getAttachmentDisplayItems({ uploadedFiles, attachments })); - let canScrollLeft = $state(false); - let canScrollRight = $state(false); + let carouselRef: HorizontalScrollCarousel | undefined = $state(); let isScrollable = $state(false); let previewDialogOpen = $state(false); let previewItem = $state(null); - let scrollContainer: HTMLDivElement | undefined = $state(); let showViewAll = $derived(limitToSingleRow && displayItems.length > 0 && isScrollable); let viewAllDialogOpen = $state(false); @@ -71,41 +70,9 @@ previewDialogOpen = true; } - function scrollLeft(event?: MouseEvent) { - event?.stopPropagation(); - event?.preventDefault(); - - if (!scrollContainer) return; - - scrollContainer.scrollBy({ left: scrollContainer.clientWidth * -0.67, behavior: 'smooth' }); - } - - function scrollRight(event?: MouseEvent) { - event?.stopPropagation(); - event?.preventDefault(); - - if (!scrollContainer) return; - - scrollContainer.scrollBy({ left: scrollContainer.clientWidth * 0.67, behavior: 'smooth' }); - } - - function updateScrollButtons() { - if (!scrollContainer) return; - - const { scrollLeft, scrollWidth, clientWidth } = scrollContainer; - - canScrollLeft = scrollLeft > 0; - canScrollRight = scrollLeft < scrollWidth - clientWidth - 1; - isScrollable = scrollWidth > clientWidth; - } - $effect(() => { - if (scrollContainer && displayItems.length) { - scrollContainer.scrollLeft = 0; - - setTimeout(() => { - updateScrollButtons(); - }, 0); + if (carouselRef && displayItems.length) { + carouselRef.resetScroll(); } }); @@ -113,93 +80,66 @@ {#if displayItems.length > 0}
{#if limitToSingleRow} -
- - -
- {#each displayItems as item (item.id)} - {#if item.isMcpPrompt} - {@const mcpPrompt = - item.attachment?.type === AttachmentType.MCP_PROMPT - ? (item.attachment as DatabaseMessageExtraMcpPrompt) - : item.uploadedFile?.mcpPrompt - ? { - type: AttachmentType.MCP_PROMPT as const, - name: item.name, - serverName: item.uploadedFile.mcpPrompt.serverName, - promptName: item.uploadedFile.mcpPrompt.promptName, - content: item.textContent ?? '', - arguments: item.uploadedFile.mcpPrompt.arguments - } - : null} - {#if mcpPrompt} - onFileRemove(item.id) : undefined} - /> - {/if} - {:else if item.isImage && item.preview} - openPreview(item, event)} - /> - {:else} - openPreview(item, event)} + isLoading={item.isLoading} + loadError={item.loadError} + onRemove={onFileRemove ? () => onFileRemove(item.id) : undefined} /> {/if} - {/each} -
- - -
+ {:else if item.isImage && item.preview} + openPreview(item, event)} + /> + {:else} + openPreview(item, event)} + /> + {/if} + {/each} + {#if showViewAll}
diff --git a/tools/server/webui/src/lib/components/app/misc/HorizontalScrollCarousel.svelte b/tools/server/webui/src/lib/components/app/misc/HorizontalScrollCarousel.svelte new file mode 100644 index 0000000000..e302f83e11 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/misc/HorizontalScrollCarousel.svelte @@ -0,0 +1,93 @@ + + +
+ + +
+ {@render children?.()} +
+ + +
diff --git a/tools/server/webui/src/lib/components/app/misc/index.ts b/tools/server/webui/src/lib/components/app/misc/index.ts index dc61b519bb..08c6d35b6f 100644 --- a/tools/server/webui/src/lib/components/app/misc/index.ts +++ b/tools/server/webui/src/lib/components/app/misc/index.ts @@ -28,3 +28,10 @@ export { default as TruncatedText } from './TruncatedText.svelte'; * - Mode-specific UI (export vs import) */ export { default as ConversationSelection } from './ConversationSelection.svelte'; + +/** + * Horizontal scrollable carousel with navigation arrows. + * Used for displaying items in a horizontally scrollable container + * with left/right navigation buttons that appear on hover. + */ +export { default as HorizontalScrollCarousel } from './HorizontalScrollCarousel.svelte';