mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(web): apply Otter Orange design system and UI improvements
Adopt the landing page's Otter Orange (#E07832) design system across the web app, replacing the generic blue (#2563eb) theme. Light mode uses warm cream/stone neutrals, dark mode uses deep brown tones. UI changes: - Remove Quick Actions section from home page - Add modality filter tabs to file-uploaded view - Remove colored left-border accents from tool panels and grid cards - Use standard dropzone for pdf-to-image (custom-results mode) - Fix PDF document viewer to use ArrayBuffer instead of blob URL - Fix missing FileImage import in dropzone
This commit is contained in:
@@ -47,7 +47,7 @@ const FORMAT_LABELS: Record<string, string> = {
|
||||
/** Badge colors by barcode family. */
|
||||
function getBadgeColor(type: string): string {
|
||||
if (type.includes("QR") || type === "Aztec" || type === "DataMatrix" || type === "MaxiCode")
|
||||
return "bg-blue-500/15 text-blue-600 dark:text-blue-400";
|
||||
return "bg-primary/15 text-primary-dark dark:text-primary-light";
|
||||
if (type.includes("EAN") || type.includes("UPC") || type.includes("DataBar"))
|
||||
return "bg-green-500/15 text-green-600 dark:text-green-400";
|
||||
if (type === "PDF417") return "bg-purple-500/15 text-purple-600 dark:text-purple-400";
|
||||
|
||||
@@ -459,7 +459,7 @@ function CollageCell({
|
||||
className={cn(
|
||||
"relative overflow-hidden transition-shadow",
|
||||
isSelected && "ring-2 ring-primary ring-offset-1",
|
||||
isOver && !isSelected && "ring-2 ring-blue-500",
|
||||
isOver && !isSelected && "ring-2 ring-primary",
|
||||
!image && "border-2 border-dashed border-border/50",
|
||||
image && !isSelected && "cursor-grab",
|
||||
image && isSelected && "cursor-grab active:cursor-grabbing",
|
||||
@@ -521,7 +521,7 @@ function CollageCell({
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded px-1.5 py-1 text-[11px] font-medium backdrop-blur-sm transition-colors",
|
||||
transform.objectFit === "contain"
|
||||
? "bg-blue-500/80 text-white hover:bg-blue-500/90"
|
||||
? "bg-primary/80 text-white hover:bg-primary/90"
|
||||
: "bg-black/50 text-white hover:bg-black/70",
|
||||
)}
|
||||
title={transform.objectFit === "cover" ? "Fit entire image" : "Fill cell"}
|
||||
|
||||
@@ -29,14 +29,17 @@ export function DocumentView() {
|
||||
|
||||
/* C+D: cancel in-flight renders and destroy the doc proxy on cleanup. */
|
||||
useEffect(() => {
|
||||
if (!src || !canvasRef.current) return;
|
||||
if (!canvasRef.current) return;
|
||||
const file = entry?.file;
|
||||
if (!file && !src) return;
|
||||
let cancelled = false;
|
||||
let doc: pdfjs.PDFDocumentProxy | undefined;
|
||||
let renderTask: pdfjs.RenderTask | undefined;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
doc = await pdfjs.getDocument({ url: src }).promise;
|
||||
const source = file ? { data: new Uint8Array(await file.arrayBuffer()) } : { url: src! };
|
||||
doc = await pdfjs.getDocument(source).promise;
|
||||
if (cancelled) return;
|
||||
setPageCount(doc.numPages);
|
||||
const pdfPage = await doc.getPage(Math.min(page, doc.numPages));
|
||||
@@ -60,7 +63,7 @@ export function DocumentView() {
|
||||
renderTask?.cancel();
|
||||
doc?.loadingTask.destroy();
|
||||
};
|
||||
}, [src, page]);
|
||||
}, [src, page, entry?.file]);
|
||||
|
||||
if (!entry) return null;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Download, FileUp, Loader2, X } from "lucide-react";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Download, Loader2 } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { format } from "@/lib/format";
|
||||
import { useFileStore } from "@/stores/file-store";
|
||||
import { usePdfToImageStore } from "@/stores/pdf-to-image-store";
|
||||
|
||||
const FORMAT_OPTIONS = [
|
||||
@@ -41,57 +41,22 @@ const LOSSY_FORMATS = ["jpg", "webp", "avif", "heic", "heif", "jxl"];
|
||||
export function PdfToImageSettings() {
|
||||
const { t } = useTranslation();
|
||||
const store = usePdfToImageStore();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const file = useFileStore((s) => s.entries[s.selectedIndex]?.file);
|
||||
|
||||
useEffect(() => {
|
||||
if (file && file !== store.file) {
|
||||
store.setFile(file);
|
||||
store.loadPreview(file);
|
||||
}
|
||||
}, [file, store]);
|
||||
|
||||
const isLossy = LOSSY_FORMATS.includes(store.format);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(files: FileList | null) => {
|
||||
const pdfFile = files?.[0];
|
||||
if (!pdfFile) return;
|
||||
store.setFile(pdfFile);
|
||||
store.loadPreview(pdfFile);
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
handleFileChange(e.dataTransfer.files);
|
||||
},
|
||||
[handleFileChange],
|
||||
);
|
||||
|
||||
const handleRemoveFile = useCallback(() => {
|
||||
store.setFile(null);
|
||||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||||
}, [store]);
|
||||
|
||||
const selectedCount = store.selectedPages.size;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* PDF upload area */}
|
||||
{!store.file ? (
|
||||
<button
|
||||
type="button"
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="border-2 border-dashed border-border rounded-lg p-6 text-center cursor-pointer hover:border-primary/50 transition-colors w-full"
|
||||
>
|
||||
<FileUp className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">{t.toolSettings["pdf-to-image"].dropPdf}</p>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="application/pdf"
|
||||
className="hidden"
|
||||
onChange={(e) => handleFileChange(e.target.files)}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
{/* PDF info */}
|
||||
{store.file && (
|
||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{store.file.name}</p>
|
||||
@@ -106,13 +71,6 @@ export function PdfToImageSettings() {
|
||||
) : null}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveFile}
|
||||
className="p-1 hover:bg-background rounded"
|
||||
>
|
||||
<X className="h-4 w-4 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user