| <script lang="ts"> |
| import type { WorkBook } from 'xlsx'; |
| import DOMPurify from 'dompurify'; |
| |
| import { getContext, onMount, tick } from 'svelte'; |
| |
| import { formatFileSize, getLineCount } from '$lib/utils'; |
| import { WEBUI_API_BASE_URL } from '$lib/constants'; |
| import { settings } from '$lib/stores'; |
| import { getKnowledgeById } from '$lib/apis/knowledge'; |
| import { getFileById, getFileContentById } from '$lib/apis/files'; |
| |
| import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte'; |
| import Markdown from '$lib/components/chat/Messages/Markdown.svelte'; |
| |
| const i18n = getContext('i18n'); |
| |
| const CONTENT_PREVIEW_LIMIT = 10000; |
| let expandedContent = false; |
| |
| import Modal from './Modal.svelte'; |
| import XMark from '../icons/XMark.svelte'; |
| import Switch from './Switch.svelte'; |
| import Tooltip from './Tooltip.svelte'; |
| import dayjs from 'dayjs'; |
| import Spinner from './Spinner.svelte'; |
| import PDFViewer from './PDFViewer.svelte'; |
| import PanzoomContainer from './PanzoomContainer.svelte'; |
| import Reset from '../icons/Reset.svelte'; |
| |
| export let item; |
| export let show = false; |
| export let edit = false; |
| |
| let enableFullContent = false; |
| let loading = false; |
| |
| let isPDF = false; |
| let isAudio = false; |
| let isImage = false; |
| let isExcel = false; |
| let isDocx = false; |
| let isPptx = false; |
| |
| let selectedTab = ''; |
| let excelWorkbook: WorkBook | null = null; |
| let excelSheetNames: string[] = []; |
| let selectedSheet = ''; |
| let excelHtml = ''; |
| let excelError = ''; |
| let rowCount = 0; |
| |
| |
| let docxHtml = ''; |
| let docxError = ''; |
| |
| |
| let pptxSlides: string[] = []; |
| let pptxCurrentSlide = 0; |
| let pptxError = ''; |
| |
| let panzoomRef: PanzoomContainer; |
| const resetImageView = () => { |
| panzoomRef?.reset(); |
| }; |
| |
| $: isPDF = |
| item?.meta?.content_type === 'application/pdf' || |
| (item?.name && item?.name.toLowerCase().endsWith('.pdf')); |
| |
| $: isMarkdown = |
| item?.meta?.content_type === 'text/markdown' || |
| (item?.name && item?.name.toLowerCase().endsWith('.md')); |
| |
| $: isCode = |
| item?.name && |
| (item.name.toLowerCase().endsWith('.py') || |
| item.name.toLowerCase().endsWith('.js') || |
| item.name.toLowerCase().endsWith('.ts') || |
| item.name.toLowerCase().endsWith('.java') || |
| item.name.toLowerCase().endsWith('.html') || |
| item.name.toLowerCase().endsWith('.css') || |
| item.name.toLowerCase().endsWith('.json') || |
| item.name.toLowerCase().endsWith('.cpp') || |
| item.name.toLowerCase().endsWith('.c') || |
| item.name.toLowerCase().endsWith('.h') || |
| item.name.toLowerCase().endsWith('.sh') || |
| item.name.toLowerCase().endsWith('.bash') || |
| item.name.toLowerCase().endsWith('.yaml') || |
| item.name.toLowerCase().endsWith('.yml') || |
| item.name.toLowerCase().endsWith('.xml') || |
| item.name.toLowerCase().endsWith('.sql') || |
| item.name.toLowerCase().endsWith('.go') || |
| item.name.toLowerCase().endsWith('.rs') || |
| item.name.toLowerCase().endsWith('.php') || |
| item.name.toLowerCase().endsWith('.rb')); |
| |
| $: isAudio = |
| (item?.meta?.content_type ?? '').startsWith('audio/') || |
| (item?.name && item?.name.toLowerCase().endsWith('.mp3')) || |
| (item?.name && item?.name.toLowerCase().endsWith('.wav')) || |
| (item?.name && item?.name.toLowerCase().endsWith('.ogg')) || |
| (item?.name && item?.name.toLowerCase().endsWith('.m4a')) || |
| (item?.name && item?.name.toLowerCase().endsWith('.webm')); |
| |
| $: isImage = |
| (item?.meta?.content_type ?? '').startsWith('image/') || |
| (item?.name && |
| (item.name.toLowerCase().endsWith('.png') || |
| item.name.toLowerCase().endsWith('.jpg') || |
| item.name.toLowerCase().endsWith('.jpeg') || |
| item.name.toLowerCase().endsWith('.gif') || |
| item.name.toLowerCase().endsWith('.webp') || |
| item.name.toLowerCase().endsWith('.svg') || |
| item.name.toLowerCase().endsWith('.bmp') || |
| item.name.toLowerCase().endsWith('.ico'))); |
| |
| $: isExcel = |
| item?.meta?.content_type === 'application/vnd.ms-excel' || |
| item?.meta?.content_type === |
| 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || |
| item?.meta?.content_type === 'text/csv' || |
| item?.meta?.content_type === 'application/csv' || |
| (item?.name && |
| (item.name.toLowerCase().endsWith('.xls') || |
| item.name.toLowerCase().endsWith('.xlsx') || |
| item.name.toLowerCase().endsWith('.csv'))); |
| |
| $: isDocx = |
| item?.meta?.content_type === |
| 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || |
| (item?.name && item.name.toLowerCase().endsWith('.docx')); |
| |
| $: isPptx = |
| item?.meta?.content_type === |
| 'application/vnd.openxmlformats-officedocument.presentationml.presentation' || |
| (item?.name && item.name.toLowerCase().endsWith('.pptx')); |
| |
| const loadExcelContent = async () => { |
| try { |
| excelError = ''; |
| const [arrayBuffer, { read }] = await Promise.all([ |
| getFileContentById(item.id), |
| import('xlsx') |
| ]); |
| excelWorkbook = read(arrayBuffer, { type: 'array' }); |
| excelSheetNames = excelWorkbook.SheetNames; |
| |
| if (excelSheetNames.length > 0) { |
| selectedSheet = excelSheetNames[0]; |
| await renderExcelSheet(); |
| } |
| } catch (error) { |
| console.error('Error loading Excel/CSV file:', error); |
| excelError = $i18n.t('Failed to load Excel/CSV file. Please try downloading it instead.'); |
| } |
| }; |
| |
| const renderExcelSheet = async () => { |
| if (!excelWorkbook || !selectedSheet) return; |
| const { excelToTable } = await import('$lib/utils/excelToTable'); |
| const worksheet = excelWorkbook.Sheets[selectedSheet]; |
| const result = await excelToTable(worksheet); |
| excelHtml = DOMPurify.sanitize(result.html); |
| rowCount = result.rowCount; |
| }; |
| |
| $: if (selectedSheet && excelWorkbook) { |
| renderExcelSheet(); |
| } |
| |
| const loadDocxContent = async () => { |
| try { |
| docxError = ''; |
| const [arrayBuffer, mammoth] = await Promise.all([ |
| getFileContentById(item.id), |
| import('mammoth') |
| ]); |
| const result = await mammoth.convertToHtml({ arrayBuffer }); |
| docxHtml = DOMPurify.sanitize(result.value); |
| } catch (error) { |
| console.error('Error loading DOCX file:', error); |
| docxError = $i18n.t('Failed to load DOCX file. Please try downloading it instead.'); |
| } |
| }; |
| |
| const loadPptxContent = async () => { |
| try { |
| pptxError = ''; |
| const [arrayBuffer, { pptxToImages }] = await Promise.all([ |
| getFileContentById(item.id), |
| import('$lib/utils/pptxToHtml') |
| ]); |
| const result = await pptxToImages(arrayBuffer); |
| pptxSlides = result.images; |
| pptxCurrentSlide = 0; |
| } catch (error) { |
| console.error('Error loading PPTX file:', error); |
| pptxError = $i18n.t('Failed to load PPTX file. Please try downloading it instead.'); |
| } |
| }; |
| |
| const loadContent = async () => { |
| selectedTab = ''; |
| expandedContent = false; |
| if (item?.type === 'collection') { |
| loading = true; |
| |
| const knowledge = await getKnowledgeById(localStorage.token, item.id).catch((e) => { |
| console.error('Error fetching knowledge base:', e); |
| return null; |
| }); |
| |
| if (knowledge) { |
| item.files = knowledge.files || []; |
| } |
| loading = false; |
| } else if (item?.type === 'file') { |
| loading = true; |
| |
| const file = await getFileById(localStorage.token, item.id).catch((e) => { |
| console.error('Error fetching file:', e); |
| return null; |
| }); |
| |
| if (file) { |
| item.file = file || {}; |
| } |
| |
| |
| if (isExcel) { |
| await loadExcelContent(); |
| } |
| if (isDocx) { |
| await loadDocxContent(); |
| } |
| if (isPptx) { |
| await loadPptxContent(); |
| } |
| |
| loading = false; |
| } |
| |
| await tick(); |
| }; |
| |
| $: if (show) { |
| loadContent(); |
| } |
| |
| onMount(() => { |
| console.log(item); |
| if (item?.context === 'full') { |
| enableFullContent = true; |
| } |
| }); |
| </script> |
|
|
| <Modal bind:show size="lg"> |
| <div class="font-primary px-4.5 py-3.5 w-full flex flex-col justify-center dark:text-gray-400"> |
| <div class=" pb-2"> |
| <div class="flex items-start justify-between"> |
| <div> |
| <div class=" font-medium text-lg dark:text-gray-100"> |
| <a |
| href="#" |
| class="hover:underline line-clamp-1" |
| on:click|preventDefault={() => { |
| if (item.type === 'file' || item.url) { |
| let fileId = item?.id ?? item?.tempId; |
| window.open( |
| item.type === 'file' |
| ? item?.url?.startsWith('http') |
| ? item.url |
| : `${WEBUI_API_BASE_URL}/files/${fileId}/content` |
| : item.url, |
| '_blank' |
| ); |
| } |
| }} |
| > |
| {item?.name ?? 'File'} |
| </a> |
| </div> |
| </div> |
| |
| <div> |
| <button |
| on:click={() => { |
| show = false; |
| }} |
| > |
| <XMark /> |
| </button> |
| </div> |
| </div> |
|
|
| <div> |
| <div class="flex flex-col items-center md:flex-row gap-1 justify-between w-full"> |
| <div class=" flex flex-wrap text-xs gap-1 text-gray-500"> |
| {#if item?.type === 'collection'} |
| {#if item?.type} |
| <div class="capitalize shrink-0">{item.type}</div> |
| • |
| {/if} |
| |
| {#if item?.description} |
| <div class="line-clamp-1">{item.description}</div> |
| • |
| {/if} |
| |
| {#if item?.created_at} |
| <div class="capitalize shrink-0"> |
| {dayjs(item.created_at * 1000).format('LL')} |
| </div> |
| {/if} |
| {/if} |
| |
| {#if item.size} |
| <div class="capitalize shrink-0">{formatFileSize(item.size)}</div> |
| • |
| {/if} |
| |
| {#if item?.file?.data?.content} |
| <div class="capitalize shrink-0"> |
| {#if isExcel && rowCount > 0 && selectedTab === 'preview'} |
| {$i18n.t('{{COUNT}} Rows', { |
| COUNT: rowCount |
| })} |
| {:else} |
| {$i18n.t('{{COUNT}} extracted lines', { |
| COUNT: getLineCount(item?.file?.data?.content ?? '') |
| })} |
| {/if} |
| </div> |
| |
| <div class="flex items-center gap-1 shrink-0"> |
| • {$i18n.t('Formatting may be inconsistent from source.')} |
| </div> |
| {/if} |
| |
| {#if item?.knowledge} |
| <div class="capitalize shrink-0"> |
| {$i18n.t('Knowledge Base')} |
| </div> |
| {/if} |
| </div> |
| |
| {#if edit} |
| <div class=" self-end"> |
| <Tooltip |
| content={enableFullContent |
| ? $i18n.t( |
| 'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.' |
| ) |
| : $i18n.t( |
| 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.' |
| )} |
| > |
| <div class="flex items-center gap-1.5 text-xs"> |
| {#if enableFullContent} |
| {$i18n.t('Using Entire Document')} |
| {:else} |
| {$i18n.t('Using Focused Retrieval')} |
| {/if} |
| <Switch |
| bind:state={enableFullContent} |
| on:change={(e) => { |
| item.context = e.detail ? 'full' : undefined; |
| }} |
| /> |
| </div> |
| </Tooltip> |
| </div> |
| {/if} |
| </div> |
| </div> |
| </div> |
|
|
| <div class="max-h-[75vh] overflow-auto"> |
| {#if !loading} |
| {#if item?.type === 'collection'} |
| <div> |
| {#each item?.files as file} |
| <div class="flex items-center gap-2 mb-2"> |
| <div class="flex-shrink-0 text-xs"> |
| {file?.meta?.name} |
| </div> |
| </div> |
| {/each} |
| </div> |
| {/if} |
| |
| {#if isAudio || isPDF || isExcel || isCode || isMarkdown || isDocx || isPptx} |
| <div |
| class="flex mb-2.5 scrollbar-none overflow-x-auto w-full border-b border-gray-50 dark:border-gray-850/30 text-center text-sm font-medium bg-transparent dark:text-gray-200" |
| > |
| <button |
| class="min-w-fit py-1.5 px-4 border-b {selectedTab === '' |
| ? ' ' |
| : ' border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition" |
| type="button" |
| on:click={() => { |
| selectedTab = ''; |
| }}>{$i18n.t('Content')}</button |
| > |
| |
| <button |
| class="min-w-fit py-1.5 px-4 border-b {selectedTab === 'preview' |
| ? ' ' |
| : ' border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition" |
| type="button" |
| on:click={() => { |
| selectedTab = 'preview'; |
| }}>{$i18n.t('Preview')}</button |
| > |
| </div> |
| {/if} |
|
|
| {#if isImage} |
| <div class="relative w-full max-h-[70vh] overflow-hidden"> |
| <div class="absolute top-2 right-2 z-10"> |
| <Tooltip content={$i18n.t('Reset view')}> |
| <button |
| class="p-1.5 rounded-lg bg-white/80 dark:bg-gray-850/80 backdrop-blur-sm shadow-sm hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400" |
| on:click={resetImageView} |
| > |
| <Reset className="size-4" /> |
| </button> |
| </Tooltip> |
| </div> |
| <PanzoomContainer bind:this={panzoomRef}> |
| <img |
| src={`${WEBUI_API_BASE_URL}/files/${item.id}/content`} |
| alt={item?.name ?? 'Image'} |
| class="w-full object-contain rounded-lg" |
| loading="lazy" |
| draggable="false" |
| /> |
| </PanzoomContainer> |
| </div> |
| {:else if selectedTab === ''} |
| {#if item?.file?.data} |
| {@const rawContent = (item?.file?.data?.content ?? '').trim() || 'No content'} |
| {@const isTruncated = |
| ($settings?.renderMarkdownInPreviews ?? true) && |
| rawContent.length > CONTENT_PREVIEW_LIMIT && |
| !expandedContent} |
| {#if $settings?.renderMarkdownInPreviews ?? true} |
| <div |
| class="max-h-96 overflow-scroll scrollbar-hidden text-sm prose dark:prose-invert max-w-full" |
| > |
| <Markdown |
| content={isTruncated ? rawContent.slice(0, CONTENT_PREVIEW_LIMIT) : rawContent} |
| id="file-preview" |
| /> |
| </div> |
| {#if isTruncated} |
| <button |
| class="mt-1 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition" |
| on:click={() => { |
| expandedContent = true; |
| }} |
| > |
| {$i18n.t('Show all ({{COUNT}} characters)', { |
| COUNT: rawContent.length.toLocaleString() |
| })} |
| </button> |
| {/if} |
| {:else} |
| <div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap"> |
| {rawContent} |
| </div> |
| {/if} |
| {:else if item?.content} |
| {@const rawContent = (item?.content ?? '').trim() || 'No content'} |
| {@const isTruncated = |
| ($settings?.renderMarkdownInPreviews ?? true) && |
| rawContent.length > CONTENT_PREVIEW_LIMIT && |
| !expandedContent} |
| {#if $settings?.renderMarkdownInPreviews ?? true} |
| <div |
| class="max-h-96 overflow-scroll scrollbar-hidden text-sm prose dark:prose-invert max-w-full" |
| > |
| <Markdown |
| content={isTruncated ? rawContent.slice(0, CONTENT_PREVIEW_LIMIT) : rawContent} |
| id="file-preview-content" |
| /> |
| </div> |
| {#if isTruncated} |
| <button |
| class="mt-1 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition" |
| on:click={() => { |
| expandedContent = true; |
| }} |
| > |
| {$i18n.t('Show all ({{COUNT}} characters)', { |
| COUNT: rawContent.length.toLocaleString() |
| })} |
| </button> |
| {/if} |
| {:else} |
| <div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap"> |
| {rawContent} |
| </div> |
| {/if} |
| {/if} |
| {:else if selectedTab === 'preview'} |
| {#if isAudio} |
| <audio |
| src={`${WEBUI_API_BASE_URL}/files/${item.id}/content`} |
| class="w-full border-0 rounded-lg mb-2" |
| controls |
| playsinline |
| /> |
| {:else if isPDF} |
| <PDFViewer |
| url={`${WEBUI_API_BASE_URL}/files/${item.id}/content`} |
| className="w-full h-[70vh] border-0 rounded-lg" |
| /> |
| {:else if isExcel} |
| {#if excelError} |
| <div class="text-red-500 text-sm p-4"> |
| {excelError} |
| </div> |
| {:else} |
| {#if excelSheetNames.length > 1} |
| <div |
| class="flex mb-2.5 scrollbar-none overflow-x-auto w-full border-b border-gray-50 dark:border-gray-850/30 text-center text-sm font-medium bg-transparent dark:text-gray-200" |
| > |
| {#each excelSheetNames as sheetName} |
| <button |
| class="min-w-fit py-1.5 px-4 border-b {selectedSheet === sheetName |
| ? ' ' |
| : ' border-transparent text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'} transition" |
| type="button" |
| on:click={() => { |
| selectedSheet = sheetName; |
| }}>{sheetName}</button |
| > |
| {/each} |
| </div> |
| {/if} |
|
|
| {#if excelHtml} |
| <div class="office-preview overflow-auto max-h-[60vh]"> |
| {@html excelHtml} |
| </div> |
| {:else} |
| <div class="text-gray-500 text-sm p-4">No content available</div> |
| {/if} |
| {/if} |
| {:else if isCode} |
| <div class="max-h-[60vh] overflow-scroll scrollbar-hidden text-sm relative"> |
| <CodeBlock |
| code={item.file.data.content} |
| lang={item.name.split('.').pop()} |
| token={null} |
| edit={false} |
| run={false} |
| save={false} |
| /> |
| </div> |
| {:else if isMarkdown} |
| <div |
| class="max-h-[60vh] overflow-scroll scrollbar-hidden text-sm prose dark:prose-invert max-w-full" |
| > |
| <Markdown content={item.file.data.content} id="markdown-viewer" /> |
| </div> |
| {:else if isDocx} |
| {#if docxError} |
| <div class="text-red-500 text-sm p-4">{docxError}</div> |
| {:else if docxHtml} |
| <div |
| class="office-preview max-h-[60vh] overflow-auto p-4 prose dark:prose-invert max-w-full text-sm" |
| > |
| {@html docxHtml} |
| </div> |
| {:else} |
| <div class="text-gray-500 text-sm p-4">No content available</div> |
| {/if} |
| {:else if isPptx} |
| {#if pptxError} |
| <div class="text-red-500 text-sm p-4">{pptxError}</div> |
| {:else if pptxSlides.length > 0} |
| <div class="max-h-[60vh] overflow-auto"> |
| <div class="flex justify-center p-4"> |
| <img |
| src={pptxSlides[pptxCurrentSlide]} |
| alt="Slide {pptxCurrentSlide + 1}" |
| class="max-w-full max-h-[50vh] object-contain rounded-md shadow-lg" |
| draggable="false" |
| /> |
| </div> |
| {#if pptxSlides.length > 1} |
| <div class="flex items-center justify-center gap-3 pb-3 text-sm text-gray-500"> |
| <button |
| class="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30" |
| disabled={pptxCurrentSlide === 0} |
| on:click={() => (pptxCurrentSlide = Math.max(0, pptxCurrentSlide - 1))} |
| > |
| <svg |
| xmlns="http://www.w3.org/2000/svg" |
| viewBox="0 0 20 20" |
| fill="currentColor" |
| class="size-5" |
| > |
| <path |
| fill-rule="evenodd" |
| d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" |
| clip-rule="evenodd" |
| /> |
| </svg> |
| </button> |
| <span>{pptxCurrentSlide + 1} / {pptxSlides.length}</span> |
| <button |
| class="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30" |
| disabled={pptxCurrentSlide === pptxSlides.length - 1} |
| on:click={() => |
| (pptxCurrentSlide = Math.min(pptxSlides.length - 1, pptxCurrentSlide + 1))} |
| > |
| <svg |
| xmlns="http://www.w3.org/2000/svg" |
| viewBox="0 0 20 20" |
| fill="currentColor" |
| class="size-5" |
| > |
| <path |
| fill-rule="evenodd" |
| d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" |
| clip-rule="evenodd" |
| /> |
| </svg> |
| </button> |
| </div> |
| {/if} |
| </div> |
| {:else} |
| <div class="text-gray-500 text-sm p-4">No content available</div> |
| {/if} |
| {:else} |
| <div class="max-h-96 overflow-scroll scrollbar-hidden text-xs whitespace-pre-wrap"> |
| {(item?.file?.data?.content ?? '').trim() || 'No content'} |
| </div> |
| {/if} |
| {/if} |
| {:else} |
| <div class="flex items-center justify-center py-6"> |
| <Spinner className="size-5" /> |
| </div> |
| {/if} |
| </div> |
| </div> |
| </Modal> |
|
|
| <style> |
| :global(.excel-table-container table) { |
| width: 100%; |
| border-collapse: collapse; |
| font-size: 0.875rem; |
| line-height: 1.25rem; |
| } |
| |
| :global(.excel-table-container table td), |
| :global(.excel-table-container table th) { |
| border-width: 1px; |
| border-style: solid; |
| border-color: var(--color-gray-300, #cdcdcd); |
| padding: 0.5rem 0.75rem; |
| text-align: left; |
| } |
| |
| :global(.dark .excel-table-container table td), |
| :global(.dark .excel-table-container table th) { |
| border-color: var(--color-gray-600, #676767); |
| } |
| |
| :global(.excel-table-container table th) { |
| background-color: var(--color-gray-100, #ececec); |
| font-weight: 600; |
| } |
| |
| :global(.dark .excel-table-container table th) { |
| background-color: var(--color-gray-800, #333); |
| color: var(--color-gray-100, #ececec); |
| } |
| |
| :global(.excel-table-container table tr:nth-child(even)) { |
| background-color: var(--color-gray-50, #f9f9f9); |
| } |
| |
| :global(.dark .excel-table-container table tr:nth-child(even)) { |
| background-color: rgba(38, 38, 38, 0.5); |
| } |
| |
| :global(.excel-table-container table tr:hover) { |
| background-color: var(--color-gray-100, #ececec); |
| } |
| |
| :global(.dark .excel-table-container table tr:hover) { |
| background-color: rgba(51, 51, 51, 0.5); |
| } |
| </style> |
|
|