feat: add DOCX/XLSX/PPTX file preview
- DOCX: mammoth converts to semantic HTML (prose preview) - XLSX: xlsx library extended to FileNav with sheet tabs at bottom - PPTX: custom canvas renderer produces PNG images per slide with panzoom zoom/pan and slide navigation Changes: - New: src/lib/utils/pptxToHtml.ts (canvas-based PPTX renderer) - FileNav.svelte: office format detection, blob download, conversion - FilePreview.svelte: office rendering branches, sheet tabs, slide viewer - FileItemModal.svelte: DOCX/PPTX preview tabs - package.json: added mammoth dependency
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
import FileNavToolbar from './FileNav/FileNavToolbar.svelte';
|
||||
import FilePreview from './FileNav/FilePreview.svelte';
|
||||
import FileEntryRow from './FileNav/FileEntryRow.svelte';
|
||||
import PortList from './FileNav/PortList.svelte';
|
||||
import XTerminal from './XTerminal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -93,6 +94,14 @@
|
||||
let fileLoading = false;
|
||||
let filePreviewRef: FilePreview;
|
||||
|
||||
// ── Office preview state ────────────────────────────────────────────
|
||||
let fileOfficeHtml: string | null = null;
|
||||
let fileOfficeSlides: string[] | null = null;
|
||||
let currentSlide = 0;
|
||||
let excelSheetNames: string[] = [];
|
||||
let selectedExcelSheet = '';
|
||||
let excelWorkbook: import('xlsx').WorkBook | null = null;
|
||||
|
||||
// ── File preview toolbar state (bound from FilePreview) ─────────────
|
||||
let editing = false;
|
||||
let showRaw = false;
|
||||
@@ -101,12 +110,15 @@
|
||||
const MD_EXTS = new Set(['md', 'markdown', 'mdx']);
|
||||
const CSV_EXTS = new Set(['csv', 'tsv']);
|
||||
const HTML_EXTS = new Set(['html', 'htm']);
|
||||
const OFFICE_EXTS = new Set(['docx', 'xlsx', 'pptx']);
|
||||
const getFileExt = (path: string | null) => path?.split('.').pop()?.toLowerCase() ?? '';
|
||||
|
||||
$: isMarkdown = MD_EXTS.has(getFileExt(selectedFile));
|
||||
$: isCsv = CSV_EXTS.has(getFileExt(selectedFile));
|
||||
$: isHtml = HTML_EXTS.has(getFileExt(selectedFile));
|
||||
$: isTextFile = fileContent !== null && fileImageUrl === null && filePdfData === null;
|
||||
$: isOfficeFile = OFFICE_EXTS.has(getFileExt(selectedFile));
|
||||
$: isTextFile =
|
||||
fileContent !== null && fileImageUrl === null && filePdfData === null && !isOfficeFile;
|
||||
|
||||
// ── Upload / folder creation ─────────────────────────────────────────
|
||||
let isDragOver = false;
|
||||
@@ -169,6 +181,7 @@
|
||||
const IMAGE_EXTS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif']);
|
||||
const isImage = (path: string) => IMAGE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? '');
|
||||
const isPdf = (path: string) => path.split('.').pop()?.toLowerCase() === 'pdf';
|
||||
const isOffice = (path: string) => OFFICE_EXTS.has(path.split('.').pop()?.toLowerCase() ?? '');
|
||||
|
||||
const buildBreadcrumbs = (path: string) =>
|
||||
path
|
||||
@@ -192,6 +205,12 @@
|
||||
fileImageUrl = null;
|
||||
}
|
||||
filePdfData = null;
|
||||
fileOfficeHtml = null;
|
||||
fileOfficeSlides = null;
|
||||
currentSlide = 0;
|
||||
excelSheetNames = [];
|
||||
selectedExcelSheet = '';
|
||||
excelWorkbook = null;
|
||||
};
|
||||
|
||||
// ── Directory operations ─────────────────────────────────────────────
|
||||
@@ -244,6 +263,41 @@
|
||||
} else if (isPdf(filePath)) {
|
||||
const result = await downloadFileBlob(terminal.url, terminal.key, filePath);
|
||||
if (result) filePdfData = await result.blob.arrayBuffer();
|
||||
} else if (isOffice(filePath)) {
|
||||
const result = await downloadFileBlob(terminal.url, terminal.key, filePath);
|
||||
if (result) {
|
||||
const ext = getFileExt(filePath);
|
||||
const arrayBuffer = await result.blob.arrayBuffer();
|
||||
try {
|
||||
if (ext === 'docx') {
|
||||
const mammoth = await import('mammoth');
|
||||
const res = await mammoth.convertToHtml({ arrayBuffer });
|
||||
const DOMPurify = (await import('dompurify')).default;
|
||||
fileOfficeHtml = DOMPurify.sanitize(res.value);
|
||||
} else if (ext === 'xlsx') {
|
||||
const XLSX = await import('xlsx');
|
||||
const wb = XLSX.read(new Uint8Array(arrayBuffer), { type: 'array' });
|
||||
excelWorkbook = wb;
|
||||
excelSheetNames = wb.SheetNames;
|
||||
if (excelSheetNames.length > 0) {
|
||||
selectedExcelSheet = excelSheetNames[0];
|
||||
const ws = wb.Sheets[selectedExcelSheet];
|
||||
const DOMPurify = (await import('dompurify')).default;
|
||||
fileOfficeHtml = DOMPurify.sanitize(
|
||||
XLSX.utils.sheet_to_html(ws, { id: 'excel-table', editable: false, header: '' })
|
||||
);
|
||||
}
|
||||
} else if (ext === 'pptx') {
|
||||
const { pptxToImages } = await import('$lib/utils/pptxToHtml');
|
||||
const result = await pptxToImages(arrayBuffer);
|
||||
fileOfficeSlides = result.images;
|
||||
currentSlide = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to render Office file:', e);
|
||||
fileContent = `Error previewing file: ${e instanceof Error ? e.message : 'Unknown error'}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fileContent = await readFile(terminal.url, terminal.key, filePath);
|
||||
}
|
||||
@@ -536,7 +590,7 @@
|
||||
onUploadFiles={handleUploadFiles}
|
||||
onMove={handleMove}
|
||||
>
|
||||
{#if fileImageUrl !== null}
|
||||
{#if fileImageUrl !== null || (fileOfficeSlides !== null && fileOfficeSlides.length > 0)}
|
||||
<Tooltip content={$i18n.t('Reset view')}>
|
||||
<button
|
||||
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
@@ -664,6 +718,7 @@
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('Download')}>
|
||||
<button
|
||||
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
|
||||
@@ -696,11 +751,26 @@
|
||||
bind:editing
|
||||
bind:showRaw
|
||||
bind:saving
|
||||
bind:currentSlide
|
||||
{selectedFile}
|
||||
{fileLoading}
|
||||
{fileImageUrl}
|
||||
{filePdfData}
|
||||
{fileContent}
|
||||
{fileOfficeHtml}
|
||||
{fileOfficeSlides}
|
||||
{excelSheetNames}
|
||||
{selectedExcelSheet}
|
||||
onSheetChange={async (sheet) => {
|
||||
if (!excelWorkbook) return;
|
||||
selectedExcelSheet = sheet;
|
||||
const XLSX = await import('xlsx');
|
||||
const ws = excelWorkbook.Sheets[sheet];
|
||||
const DOMPurify = (await import('dompurify')).default;
|
||||
fileOfficeHtml = DOMPurify.sanitize(
|
||||
XLSX.utils.sheet_to_html(ws, { id: 'excel-table', editable: false, header: '' })
|
||||
);
|
||||
}}
|
||||
{overlay}
|
||||
onSave={async (content) => {
|
||||
const terminal = selectedTerminal;
|
||||
@@ -797,6 +867,13 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Port detection -->
|
||||
{#if selectedTerminal && !selectedFile}
|
||||
<div class="shrink-0 border-t border-gray-100 dark:border-gray-800">
|
||||
<PortList baseUrl={selectedTerminal.url} apiKey={selectedTerminal.key} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Terminal bottom panel -->
|
||||
{#if terminalEnabled}
|
||||
<div class="shrink-0 border-t border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-850">
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
export let filePdfData: ArrayBuffer | null = null;
|
||||
export let fileContent: string | null = null;
|
||||
|
||||
// Office preview props
|
||||
export let fileOfficeHtml: string | null = null;
|
||||
export let fileOfficeSlides: string[] | null = null;
|
||||
export let currentSlide = 0;
|
||||
export let excelSheetNames: string[] = [];
|
||||
export let selectedExcelSheet = '';
|
||||
export let onSheetChange: ((sheet: string) => void) | null = null;
|
||||
|
||||
export let overlay = false;
|
||||
|
||||
export let onSave: ((content: string) => Promise<void>) | null = null;
|
||||
@@ -124,7 +132,8 @@
|
||||
pzInstance = panzoom(node, {
|
||||
bounds: true,
|
||||
boundsPadding: 0.1,
|
||||
zoomSpeed: 0.065
|
||||
zoomSpeed: 0.065,
|
||||
zoomDoubleClickSpeed: 1
|
||||
});
|
||||
};
|
||||
|
||||
@@ -152,7 +161,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex-1 {fileImageUrl !== null
|
||||
class="flex-1 {fileImageUrl !== null || (fileOfficeSlides !== null && fileOfficeSlides.length > 0)
|
||||
? 'overflow-hidden'
|
||||
: 'overflow-y-auto'} min-h-0 relative h-full"
|
||||
>
|
||||
@@ -170,6 +179,61 @@
|
||||
</div>
|
||||
{:else if filePdfData !== null}
|
||||
<PDFViewer bind:this={pdfViewerRef} data={filePdfData} className="w-full h-full" />
|
||||
{:else if fileOfficeHtml !== null}
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="office-preview overflow-auto flex-1 min-h-0 p-3">
|
||||
{@html fileOfficeHtml}
|
||||
</div>
|
||||
{#if excelSheetNames.length > 1}
|
||||
<div class="flex items-center gap-1 py-1.5 px-3 border-t border-gray-100 dark:border-gray-800 overflow-x-auto">
|
||||
{#each excelSheetNames as sheet}
|
||||
<button
|
||||
class="shrink-0 px-3 py-1 text-xs rounded-md transition-colors
|
||||
{selectedExcelSheet === sheet
|
||||
? 'bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 font-medium'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'}"
|
||||
on:click={() => onSheetChange?.(sheet)}
|
||||
>
|
||||
{sheet}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if fileOfficeSlides !== null && fileOfficeSlides.length > 0}
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="w-full flex-1 min-h-0 flex items-center justify-center overflow-hidden" use:initImagePanzoom>
|
||||
<img
|
||||
src={fileOfficeSlides[currentSlide]}
|
||||
alt="Slide {currentSlide + 1}"
|
||||
class="max-w-full max-h-full object-contain p-3"
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
{#if fileOfficeSlides.length > 1}
|
||||
<div class="flex items-center justify-center gap-3 py-2 px-3 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-500">
|
||||
<button
|
||||
class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30"
|
||||
disabled={currentSlide === 0}
|
||||
on:click={() => { resetImageView(); currentSlide = Math.max(0, currentSlide - 1); }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
|
||||
<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>{currentSlide + 1} / {fileOfficeSlides.length}</span>
|
||||
<button
|
||||
class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 disabled:opacity-30"
|
||||
disabled={currentSlide === fileOfficeSlides.length - 1}
|
||||
on:click={() => { resetImageView(); currentSlide = Math.min(fileOfficeSlides.length - 1, currentSlide + 1); }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4">
|
||||
<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 if fileContent !== null}
|
||||
{#if isHtml && !showRaw}
|
||||
{#if overlay}
|
||||
@@ -226,7 +290,7 @@
|
||||
class="text-xs font-mono text-gray-800 dark:text-gray-200 whitespace-pre-wrap break-all leading-relaxed p-3">{fileContent}</pre>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="text-sm text-gray-400 text-center pt-8">
|
||||
<div class="text-xs text-gray-400 text-center pt-8">
|
||||
{$i18n.t('Could not read file.')}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -284,4 +348,62 @@
|
||||
:global(.dark) .csv-row-num {
|
||||
color: #6b7280;
|
||||
}
|
||||
/* ── Office preview styles ──────────────────────────────────────── */
|
||||
:global(.office-preview) {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
:global(.dark .office-preview) {
|
||||
color: #e5e7eb;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
:global(.office-preview table) {
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
:global(.office-preview table td),
|
||||
:global(.office-preview table th) {
|
||||
border: 1px solid rgba(128, 128, 128, 0.25);
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
:global(.dark .office-preview table td),
|
||||
:global(.dark .office-preview table th) {
|
||||
border-color: rgba(128, 128, 128, 0.35);
|
||||
}
|
||||
:global(.office-preview table th) {
|
||||
background: rgba(243, 244, 246, 0.95);
|
||||
font-weight: 600;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
:global(.dark .office-preview table th) {
|
||||
background: rgba(31, 41, 55, 0.95);
|
||||
}
|
||||
:global(.office-preview table tr:nth-child(even)) {
|
||||
background: rgba(128, 128, 128, 0.04);
|
||||
}
|
||||
:global(.office-preview table tr:hover) {
|
||||
background: rgba(59, 130, 246, 0.06);
|
||||
}
|
||||
:global(.dark .office-preview table tr:hover) {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
:global(.office-preview img) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
:global(.office-preview h1) { font-size: 1.5rem; font-weight: 700; margin: 0.75em 0 0.5em; }
|
||||
:global(.office-preview h2) { font-size: 1.25rem; font-weight: 600; margin: 0.75em 0 0.5em; }
|
||||
:global(.office-preview h3) { font-size: 1.1rem; font-weight: 600; margin: 0.5em 0 0.25em; }
|
||||
:global(.office-preview p) { margin: 0.25em 0; }
|
||||
:global(.office-preview ul),
|
||||
:global(.office-preview ol) { padding-left: 1.5em; margin: 0.5em 0; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
let isAudio = false;
|
||||
let isImage = false;
|
||||
let isExcel = false;
|
||||
let isDocx = false;
|
||||
let isPptx = false;
|
||||
|
||||
let selectedTab = '';
|
||||
let excelWorkbook: WorkBook | null = null;
|
||||
@@ -49,6 +51,15 @@
|
||||
let excelError = '';
|
||||
let rowCount = 0;
|
||||
|
||||
// DOCX state
|
||||
let docxHtml = '';
|
||||
let docxError = '';
|
||||
|
||||
// PPTX state
|
||||
let pptxSlides: string[] = [];
|
||||
let pptxCurrentSlide = 0;
|
||||
let pptxError = '';
|
||||
|
||||
let pzInstance: PanZoom | null = null;
|
||||
|
||||
const initImagePanzoom = (node: HTMLElement) => {
|
||||
@@ -128,6 +139,16 @@
|
||||
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 = '';
|
||||
@@ -170,6 +191,37 @@
|
||||
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;
|
||||
@@ -201,6 +253,12 @@
|
||||
if (isExcel) {
|
||||
await loadExcelContent();
|
||||
}
|
||||
if (isDocx) {
|
||||
await loadDocxContent();
|
||||
}
|
||||
if (isPptx) {
|
||||
await loadPptxContent();
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
@@ -358,7 +416,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isAudio || isPDF || isExcel || isCode || isMarkdown}
|
||||
{#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"
|
||||
>
|
||||
@@ -534,6 +592,56 @@
|
||||
>
|
||||
<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'}
|
||||
|
||||
Reference in New Issue
Block a user