Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions e2e/tools/viewer-fullscreen.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test, expect } from '@playwright/test';

// The full-screen expand is a shared wrapper (ExpandableViewer + useExpand)
// added to the viewer tools. The button renders regardless of loaded content,
// so we can assert the toggle without opening a file. Native fullscreen may be
// denied in headless, but the CSS-overlay fallback still flips the label.
for (const route of ['/tools/svg-viewer', '/tools/image-viewer']) {
test(`full-screen toggle works on ${route}`, async ({ page }) => {
await page.goto(route);
await page.waitForLoadState('networkidle').catch(() => {});

const expand = page.getByRole('button', { name: 'Full screen' });
await expect(async () => {
await expand.click();
await expect(page.getByRole('button', { name: 'Exit' })).toBeVisible({ timeout: 2000 });
}).toPass({ timeout: 30_000 });

await page.getByRole('button', { name: 'Exit' }).click();
await expect(page.getByRole('button', { name: 'Full screen' })).toBeVisible();
});
}
38 changes: 38 additions & 0 deletions src/components/ui/ExpandableViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { ReactNode } from 'react';
import { Maximize2, Minimize2 } from 'lucide-react';
import { useExpand } from '@/hooks/useExpand';
import type { Lang } from '@/i18n/config';

const LBL: Record<Lang, { expand: string; exit: string }> = {
en: { expand: 'Full screen', exit: 'Exit' },
id: { expand: 'Layar penuh', exit: 'Keluar' },
};

/**
* Wraps a viewer tool with a "Full screen" toggle. Reuses the shared useExpand
* hook (native Fullscreen API + iOS CSS-overlay fallback, Esc to exit) so any
* viewer can be read full-screen — handy on a phone. When expanded, the wrapper
* fills the viewport with an opaque background and scrolls its content.
*/
export function ExpandableViewer({ lang = 'en', children }: { lang?: Lang; children: ReactNode }) {
const { ref, expanded, enter, exit } = useExpand<HTMLDivElement>();
const t = LBL[lang] ?? LBL.en;
return (
<div ref={ref} className={expanded ? 'fixed inset-0 z-[60] overflow-auto bg-background p-4' : ''}>
<div className="mb-2 flex justify-end">
<button
type="button"
onClick={() => (expanded ? exit() : enter())}
aria-label={expanded ? t.exit : t.expand}
className="inline-flex items-center gap-1.5 border-2 border-border px-3 py-1.5 text-xs font-bold uppercase tracking-wide hover:bg-muted"
>
{expanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
{expanded ? t.exit : t.expand}
</button>
</div>
{children}
</div>
);
}

export default ExpandableViewer;
3 changes: 3 additions & 0 deletions src/islands/documents/DicomViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Dropzone } from '@/components/ui/Dropzone';
import { Alert } from '@/components/ui/Alert';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { applyWindowLevel, isUncompressed, rescale, parseFrameCount, clampFrameCount } from '@/tools/documents/dicom.lib';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import type { Lang } from '@/i18n/config';

interface Loaded {
Expand Down Expand Up @@ -134,6 +135,7 @@ export default function DicomViewer({ lang = 'en' }: { lang?: Lang }) {
}, [loaded, center, width, frame]);

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">{t.intro}</p>

Expand Down Expand Up @@ -199,5 +201,6 @@ export default function DicomViewer({ lang = 'en' }: { lang?: Lang }) {
</div>
)}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/documents/DocViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Dropzone } from '@/components/ui/Dropzone';
import { Button } from '@/components/ui/Button';
import { Alert } from '@/components/ui/Alert';
import { CopyButton } from '@/components/ui/CopyButton';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import { downloadService } from '@/services/download';
import type { Lang } from '@/i18n/config';

Expand Down Expand Up @@ -55,6 +56,7 @@ export default function DocViewer({ lang = 'en' }: { lang?: Lang }) {
const wordCount = text ? text.split(/\s+/).filter(Boolean).length : 0;

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">{t.intro}</p>

Expand Down Expand Up @@ -83,5 +85,6 @@ export default function DocViewer({ lang = 'en' }: { lang?: Lang }) {
</div>
)}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/documents/DocxViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FileText, Printer } from 'lucide-react';
import { Dropzone } from '@/components/ui/Dropzone';
import { Button } from '@/components/ui/Button';
import { Alert } from '@/components/ui/Alert';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import type { Lang } from '@/i18n/config';

const TR: Record<Lang, {
Expand Down Expand Up @@ -61,6 +62,7 @@ export default function DocxViewer({ lang = 'en' }: { lang?: Lang }) {
};

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<p className="text-sm text-muted-foreground print:hidden">{t.intro}</p>

Expand Down Expand Up @@ -91,5 +93,6 @@ export default function DocxViewer({ lang = 'en' }: { lang?: Lang }) {
className={`docx-viewer ${hasDoc ? 'max-h-[78vh] overflow-auto border-2 border-border bg-neutral-200 p-3 dark:bg-neutral-800 print:max-h-none print:overflow-visible print:border-0 print:bg-white print:p-0' : ''}`}
/>
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/documents/EmlViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { Dropzone } from '@/components/ui/Dropzone';
import { Alert } from '@/components/ui/Alert';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import { formatAddress, formatAddressList } from '@/tools/documents/eml.lib';
import { downloadService } from '@/services/download';
import type { Lang } from '@/i18n/config';
Expand Down Expand Up @@ -68,6 +69,7 @@ export default function EmlViewer({ lang = 'en' }: { lang?: Lang }) {
) : null;

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">{t.intro}</p>

Expand Down Expand Up @@ -117,5 +119,6 @@ export default function EmlViewer({ lang = 'en' }: { lang?: Lang }) {
</div>
)}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/documents/EpubReader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Dropzone } from '@/components/ui/Dropzone';
import { Button } from '@/components/ui/Button';
import { Alert } from '@/components/ui/Alert';
import { flattenToc, type FlatTocItem } from '@/tools/documents/epub-toc.lib';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import type { Lang } from '@/i18n/config';

const MIN_FONT = 70;
Expand Down Expand Up @@ -133,6 +134,7 @@ export default function EpubReader({ lang = 'en' }: { lang?: Lang }) {
};

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">{t.intro}</p>

Expand Down Expand Up @@ -202,5 +204,6 @@ export default function EpubReader({ lang = 'en' }: { lang?: Lang }) {
</div>
)}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/documents/GedcomViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo, useState } from 'react';
import { Dropzone } from '@/components/ui/Dropzone';
import { Alert } from '@/components/ui/Alert';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import { parseGedcom, displayName, type Gedcom, type Individual } from '@/tools/documents/gedcom.lib';
import type { Lang } from '@/i18n/config';

Expand Down Expand Up @@ -74,6 +75,7 @@ export default function GedcomViewer({ lang = 'en' }: { lang?: Lang }) {
);

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">{t.intro}</p>

Expand Down Expand Up @@ -132,5 +134,6 @@ export default function GedcomViewer({ lang = 'en' }: { lang?: Lang }) {
</div>
)}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/documents/IWorkViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Alert } from '@/components/ui/Alert';
import { findPreview } from '@/tools/documents/iwork.lib';
import { readSlideOutline, type SlideOutline } from '@/tools/documents/iwa.lib';
import { openPdfRenderer, type PdfRenderer } from '@/tools/pdf/render.lib';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import type { Lang } from '@/i18n/config';

const TR: Record<Lang, Record<string, string>> = {
Expand Down Expand Up @@ -147,6 +148,7 @@ export default function IWorkViewer({ lang = 'en' }: { lang?: Lang }) {
const label = (s: SlideOutline, i: number) => s.title || s.body[0] || `${t.slide} ${i + 1}`;

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">{t.intro}</p>

Expand Down Expand Up @@ -272,5 +274,6 @@ export default function IWorkViewer({ lang = 'en' }: { lang?: Lang }) {
<button onClick={reset} className="min-h-11 border-2 border-border px-3 py-1.5 text-sm font-medium hover:shadow-brutal">{t.another}</button>
)}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/documents/OdtViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FileType2, Printer } from 'lucide-react';
import { Dropzone } from '@/components/ui/Dropzone';
import { Button } from '@/components/ui/Button';
import { Alert } from '@/components/ui/Alert';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import type { Lang } from '@/i18n/config';

const TR: Record<Lang, {
Expand Down Expand Up @@ -94,6 +95,7 @@ export default function OdtViewer({ lang = 'en' }: { lang?: Lang }) {
const hasDoc = html !== '' || emptyDoc;

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<style>{DOC_CSS}</style>
<p className="text-sm text-muted-foreground print:hidden">{t.intro}</p>
Expand Down Expand Up @@ -131,5 +133,6 @@ export default function OdtViewer({ lang = 'en' }: { lang?: Lang }) {
</div>
)}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/documents/PptxViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { Dropzone } from '@/components/ui/Dropzone';
import { Alert } from '@/components/ui/Alert';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import type { PptxDoc, Shape } from '@/tools/documents/pptx.lib';
import type { Lang } from '@/i18n/config';

Expand Down Expand Up @@ -112,6 +113,7 @@ export default function PptxViewer({ lang = 'en' }: { lang?: Lang }) {
};

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">{t.intro}</p>

Expand Down Expand Up @@ -153,5 +155,6 @@ export default function PptxViewer({ lang = 'en' }: { lang?: Lang }) {
</div>
)}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/documents/SpreadsheetViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Dropzone } from '@/components/ui/Dropzone';
import { Button } from '@/components/ui/Button';
import { Alert } from '@/components/ui/Alert';
import { colLabel, readWorkbook, type SheetView } from '@/tools/documents/spreadsheet.lib';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import type { Lang } from '@/i18n/config';

const TR: Record<Lang, {
Expand Down Expand Up @@ -65,6 +66,7 @@ export default function SpreadsheetViewer({ lang = 'en' }: { lang?: Lang }) {
const colCount = sheet?.rows.reduce((m, r) => Math.max(m, r.length), 0) ?? 0;

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">{t.intro}</p>

Expand Down Expand Up @@ -128,5 +130,6 @@ export default function SpreadsheetViewer({ lang = 'en' }: { lang?: Lang }) {
</div>
)}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/image/ImageViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { formatBytes } from '@/tools/image/canvas.lib';
import { parseIcoEntries, type IcoEntry } from '@/tools/image/ico.lib';
import { readExifSummary, type ExifSummary } from '@/tools/image/exif.lib';
import { usePasteImage } from '@/hooks/usePasteImage';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import type { Lang } from '@/i18n/config';

const TR: Record<Lang, {
Expand Down Expand Up @@ -118,6 +119,7 @@ export default function ImageViewer({ lang = 'en' }: { lang?: Lang }) {
);

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<Dropzone onDrop={onDrop} accept="image/*,.ico" multiple={false}>
<div className="space-y-1">
Expand Down Expand Up @@ -158,5 +160,6 @@ export default function ImageViewer({ lang = 'en' }: { lang?: Lang }) {
</div>
)}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/image/SvgViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Alert } from '@/components/ui/Alert';
import { ImageResult } from '@/components/ui/ImageResult';
import { ZoomPane } from '@/components/ui/ZoomPane';
import { parseSvgSize, rasterizeSvg } from '@/tools/image/svg.lib';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import type { Lang } from '@/i18n/config';

const TR: Record<Lang, {
Expand Down Expand Up @@ -84,6 +85,7 @@ export default function SvgViewer({ lang = 'en' }: { lang?: Lang }) {
};

return (
<ExpandableViewer lang={lang}>
<div className="space-y-4">
<Dropzone onDrop={onDrop} accept="image/svg+xml,.svg" multiple={false}>
<div className="space-y-1">
Expand Down Expand Up @@ -138,5 +140,6 @@ export default function SvgViewer({ lang = 'en' }: { lang?: Lang }) {

{result && <ImageResult blob={result} filename={`image.${fmt === 'jpeg' ? 'jpg' : fmt}`} />}
</div>
</ExpandableViewer>
);
}
3 changes: 3 additions & 0 deletions src/islands/maps/GeoViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Button } from '@/components/ui/Button';
import { Alert } from '@/components/ui/Alert';
import { resolveStyle, MAP_STYLES, type StyleChoice } from '@/tools/geo/map-styles.lib';
import { parseGeoFile, computeBbox } from '@/tools/geo/geo-parse.lib';
import { ExpandableViewer } from '@/components/ui/ExpandableViewer';
import type { Lang } from '@/i18n/config';

const STYLE_KEY = 'gwt.map.style';
Expand Down Expand Up @@ -128,6 +129,7 @@ export default function GeoViewer({ lang = 'en' }: { lang?: Lang }) {
};

return (
<ExpandableViewer lang={lang}>
<div className="space-y-3">
<Dropzone onDrop={onDrop} accept=".geojson,.json,.gpx,.kml,application/geo+json,application/gpx+xml,application/vnd.google-earth.kml+xml" multiple={false}>
<div className="space-y-1">
Expand Down Expand Up @@ -166,5 +168,6 @@ export default function GeoViewer({ lang = 'en' }: { lang?: Lang }) {
</div>
)}
</div>
</ExpandableViewer>
);
}
Loading