From bcd59c20c7da6208e937cac0a9b9f02ecf73b4f1 Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Sun, 14 Jun 2026 14:06:25 +0800 Subject: [PATCH] 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 --- apps/web/src/components/common/dropzone.tsx | 2 +- .../src/components/editor/tools/crop-tool.tsx | 2 +- .../src/components/editor/tools/move-tool.tsx | 4 +- .../editor/tools/selection-tool.tsx | 6 +- .../editor/tools/transform-tool.tsx | 4 +- .../web/src/components/files/file-details.tsx | 4 +- apps/web/src/components/layout/tool-panel.tsx | 5 +- .../tools/barcode-read-settings.tsx | 2 +- .../src/components/tools/collage-preview.tsx | 4 +- .../src/components/tools/document-view.tsx | 9 +- .../tools/pdf-to-image-settings.tsx | 68 ++------ apps/web/src/lib/tool-display-modes.ts | 2 +- apps/web/src/pages/fullscreen-grid-page.tsx | 15 +- apps/web/src/pages/home-page.tsx | 159 ++++++------------ apps/web/src/stores/editor-store.ts | 2 +- apps/web/src/styles/globals.css | 44 ++--- 16 files changed, 110 insertions(+), 222 deletions(-) diff --git a/apps/web/src/components/common/dropzone.tsx b/apps/web/src/components/common/dropzone.tsx index 8cf2d7ca..0ecc0e3d 100644 --- a/apps/web/src/components/common/dropzone.tsx +++ b/apps/web/src/components/common/dropzone.tsx @@ -1,4 +1,4 @@ -import { FileUp, Upload } from "lucide-react"; +import { FileImage, FileUp, Upload } from "lucide-react"; import { type DragEvent, useCallback, useEffect, useState } from "react"; import { useTranslation } from "@/contexts/i18n-context"; import { useUrlImport } from "@/hooks/use-url-import"; diff --git a/apps/web/src/components/editor/tools/crop-tool.tsx b/apps/web/src/components/editor/tools/crop-tool.tsx index 57f45707..7bd1a9a5 100644 --- a/apps/web/src/components/editor/tools/crop-tool.tsx +++ b/apps/web/src/components/editor/tools/crop-tool.tsx @@ -258,7 +258,7 @@ export function CropOverlay() { keepRatio={false} anchorSize={8} anchorStroke="#ffffff" - anchorFill="#3b82f6" + anchorFill="#E07832" anchorCornerRadius={1} borderStroke="#ffffff" borderStrokeWidth={1} diff --git a/apps/web/src/components/editor/tools/move-tool.tsx b/apps/web/src/components/editor/tools/move-tool.tsx index 9125d332..f81c8e29 100644 --- a/apps/web/src/components/editor/tools/move-tool.tsx +++ b/apps/web/src/components/editor/tools/move-tool.tsx @@ -469,10 +469,10 @@ export function MoveToolTransformer({ flipEnabled keepRatio={false} anchorSize={8} - anchorStroke="#3b82f6" + anchorStroke="#E07832" anchorFill="#ffffff" anchorCornerRadius={2} - borderStroke="#3b82f6" + borderStroke="#E07832" borderStrokeWidth={1} padding={2} /> diff --git a/apps/web/src/components/editor/tools/selection-tool.tsx b/apps/web/src/components/editor/tools/selection-tool.tsx index 68ca2872..d499c6ea 100644 --- a/apps/web/src/components/editor/tools/selection-tool.tsx +++ b/apps/web/src/components/editor/tools/selection-tool.tsx @@ -793,7 +793,7 @@ export function ActiveSelectionPreview({ }) { if (type === "lasso" && points.length >= 4) { return ( - + ); } @@ -812,7 +812,7 @@ export function ActiveSelectionPreview({ y={y + h / 2} radiusX={w / 2} radiusY={h / 2} - stroke="#3b82f6" + stroke="#E07832" strokeWidth={1} dash={[4, 4]} listening={false} @@ -826,7 +826,7 @@ export function ActiveSelectionPreview({ y={y} width={w} height={h} - stroke="#3b82f6" + stroke="#E07832" strokeWidth={1} dash={[4, 4]} listening={false} diff --git a/apps/web/src/components/editor/tools/transform-tool.tsx b/apps/web/src/components/editor/tools/transform-tool.tsx index f3665610..6969cfa3 100644 --- a/apps/web/src/components/editor/tools/transform-tool.tsx +++ b/apps/web/src/components/editor/tools/transform-tool.tsx @@ -295,10 +295,10 @@ export function TransformToolTransformer({ 300, 315, 330, 345, ]} anchorSize={8} - anchorStroke="#3b82f6" + anchorStroke="#E07832" anchorFill="#ffffff" anchorCornerRadius={2} - borderStroke="#3b82f6" + borderStroke="#E07832" borderStrokeWidth={1} borderDash={[4, 4]} padding={0} diff --git a/apps/web/src/components/files/file-details.tsx b/apps/web/src/components/files/file-details.tsx index 0e643758..2e0a992c 100644 --- a/apps/web/src/components/files/file-details.tsx +++ b/apps/web/src/components/files/file-details.tsx @@ -188,8 +188,8 @@ export function FileDetails({ mobile = false }: FileDetailsProps) { {/* Details card */}
-
-

+
+

{t.files.fileDetailsHeading}

diff --git a/apps/web/src/components/layout/tool-panel.tsx b/apps/web/src/components/layout/tool-panel.tsx index 0f435b41..1e0906e3 100644 --- a/apps/web/src/components/layout/tool-panel.tsx +++ b/apps/web/src/components/layout/tool-panel.tsx @@ -119,10 +119,7 @@ export function ToolPanel() { return (
{idx > 0 &&
} -
+
{ModalityIcon && ( diff --git a/apps/web/src/components/tools/barcode-read-settings.tsx b/apps/web/src/components/tools/barcode-read-settings.tsx index 4d3e6f67..882c4bd0 100644 --- a/apps/web/src/components/tools/barcode-read-settings.tsx +++ b/apps/web/src/components/tools/barcode-read-settings.tsx @@ -47,7 +47,7 @@ const FORMAT_LABELS: Record = { /** 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"; diff --git a/apps/web/src/components/tools/collage-preview.tsx b/apps/web/src/components/tools/collage-preview.tsx index e16c0046..12a08a2f 100644 --- a/apps/web/src/components/tools/collage-preview.tsx +++ b/apps/web/src/components/tools/collage-preview.tsx @@ -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"} diff --git a/apps/web/src/components/tools/document-view.tsx b/apps/web/src/components/tools/document-view.tsx index 62723d95..3670739e 100644 --- a/apps/web/src/components/tools/document-view.tsx +++ b/apps/web/src/components/tools/document-view.tsx @@ -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; diff --git a/apps/web/src/components/tools/pdf-to-image-settings.tsx b/apps/web/src/components/tools/pdf-to-image-settings.tsx index 7839098d..f1eb0981 100644 --- a/apps/web/src/components/tools/pdf-to-image-settings.tsx +++ b/apps/web/src/components/tools/pdf-to-image-settings.tsx @@ -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(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 (
- {/* PDF upload area */} - {!store.file ? ( - - ) : ( + {/* PDF info */} + {store.file && (

{store.file.name}

@@ -106,13 +71,6 @@ export function PdfToImageSettings() { ) : null}

-
)} diff --git a/apps/web/src/lib/tool-display-modes.ts b/apps/web/src/lib/tool-display-modes.ts index 186f2d03..d1416564 100644 --- a/apps/web/src/lib/tool-display-modes.ts +++ b/apps/web/src/lib/tool-display-modes.ts @@ -78,7 +78,7 @@ export const TOOL_DISPLAY_MODES: Record = { favicon: "before-after", "image-to-pdf": "before-after", "optimize-for-web": "before-after", - "pdf-to-image": "no-dropzone", + "pdf-to-image": "custom-results", // Adjustments extra "replace-color": "before-after", diff --git a/apps/web/src/pages/fullscreen-grid-page.tsx b/apps/web/src/pages/fullscreen-grid-page.tsx index c9ca44cf..a78a1974 100644 --- a/apps/web/src/pages/fullscreen-grid-page.tsx +++ b/apps/web/src/pages/fullscreen-grid-page.tsx @@ -219,10 +219,7 @@ export function FullscreenGridPage() { }>; return (
-
+
{SectionIcon && (
) : null; return activeMod ? ( -
+
{ModIcon && (
) ?? LayoutGrid; return ( -
+
{/* Header */}
("all"); + const TAB_MODALITIES = useMemo(() => MODALITIES.filter((m) => m.id !== "file"), []); + const hasFile = files.length > 0; // If no file uploaded, show default layout (tool panel + dropzone) @@ -127,54 +130,6 @@ export function HomePage() {
- {/* Quick action buttons - horizontal scroll */} -
- {QUICK_ACTION_IDS.map((id) => { - const tool = TOOLS.find((t) => t.id === id); - if (!tool) return null; - const Icon = - (ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? - ICON_MAP.FileImage; - const status = getToolStatus(id); - return ( - - ); - })} -
- {/* Full-width file preview */}
{files.length > 1 ? ( @@ -244,72 +199,52 @@ export function HomePage() {
- {/* Quick actions */} -
-

- {t.homePage.quickActions} -

-
- {QUICK_ACTION_IDS.map((id) => { - const tool = TOOLS.find((t) => t.id === id); - if (!tool) return null; - const Icon = - (ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? - ICON_MAP.FileImage; - const status = getToolStatus(id); - return ( - - ); - })} -
+ {/* Modality filter tabs */} +
+ + {TAB_MODALITIES.map((m) => { + const Icon = ICON_MAP[m.icon] as React.ComponentType<{ className?: string }>; + const isActive = modalityFilter === m.id; + const label = m.id === "document" ? "Docs" : m.name; + return ( + + ); + })}
- {/* All tools by modality and category */} + {/* Tools by modality and category */}
-

- {t.homePage.allTools} -

{MODALITIES.filter((m) => { if (m.id === "file") return false; + if (modalityFilter !== "all") { + if (modalityFilter === "document") return m.id === "document"; + return m.id === modalityFilter; + } const key = m.id; return TOOLS.some( (tool) => tool.modality === key || (key === "document" && tool.modality === "file"), diff --git a/apps/web/src/stores/editor-store.ts b/apps/web/src/stores/editor-store.ts index a4c41786..44ea20a3 100644 --- a/apps/web/src/stores/editor-store.ts +++ b/apps/web/src/stores/editor-store.ts @@ -191,7 +191,7 @@ export const useEditorStore = create()( editingTextId: null, // --- Shape settings --- - shapeFill: "#3b82f6" as string | null, + shapeFill: "#E07832" as string | null, shapeFillOpacity: 1, shapeStroke: "#000000" as string | null, shapeStrokeOpacity: 1, diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css index fc7d1576..165d0e5e 100644 --- a/apps/web/src/styles/globals.css +++ b/apps/web/src/styles/globals.css @@ -10,31 +10,35 @@ } @theme { - --color-primary: #2563eb; + --color-primary: #E07832; --color-primary-foreground: #ffffff; - --color-background: #ffffff; - --color-foreground: #0f172a; - --color-muted: #f1f5f9; - --color-muted-foreground: #64748b; - --color-border: #e2e8f0; + --color-primary-light: #F09550; + --color-primary-dark: #C06520; + --color-primary-subtle: #FFF5ED; + --color-background: #FAFAF7; + --color-foreground: #1A1814; + --color-muted: #F3EEE6; + --color-muted-foreground: #6B6560; + --color-border: #DDD6CC; --color-card: #ffffff; - --color-card-foreground: #0f172a; - --color-sidebar: #f8fafc; - --color-sidebar-foreground: #334155; - --color-accent: #3b82f6; - --color-destructive: #ef4444; + --color-card-foreground: #1A1814; + --color-sidebar: #F3EEE6; + --color-sidebar-foreground: #3D3832; + --color-accent: #F09550; + --color-destructive: #D44C47; + --color-success: #2D9D78; } .dark { - --color-background: #0f172a; - --color-foreground: #f8fafc; - --color-muted: #1e293b; - --color-muted-foreground: #94a3b8; - --color-border: #334155; - --color-card: #1e293b; - --color-card-foreground: #f8fafc; - --color-sidebar: #1e293b; - --color-sidebar-foreground: #cbd5e1; + --color-background: #1A1210; + --color-foreground: #F0EBE4; + --color-muted: #262018; + --color-muted-foreground: #9A908A; + --color-border: #382E26; + --color-card: #262018; + --color-card-foreground: #F0EBE4; + --color-sidebar: #262018; + --color-sidebar-foreground: #C5BAB0; } input[type="range"] {