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:
SnapOtter
2026-06-14 14:06:25 +08:00
parent b8b6b0a44a
commit bcd59c20c7
16 changed files with 110 additions and 222 deletions
+1 -1
View File
@@ -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 { type DragEvent, useCallback, useEffect, useState } from "react";
import { useTranslation } from "@/contexts/i18n-context"; import { useTranslation } from "@/contexts/i18n-context";
import { useUrlImport } from "@/hooks/use-url-import"; import { useUrlImport } from "@/hooks/use-url-import";
@@ -258,7 +258,7 @@ export function CropOverlay() {
keepRatio={false} keepRatio={false}
anchorSize={8} anchorSize={8}
anchorStroke="#ffffff" anchorStroke="#ffffff"
anchorFill="#3b82f6" anchorFill="#E07832"
anchorCornerRadius={1} anchorCornerRadius={1}
borderStroke="#ffffff" borderStroke="#ffffff"
borderStrokeWidth={1} borderStrokeWidth={1}
@@ -469,10 +469,10 @@ export function MoveToolTransformer({
flipEnabled flipEnabled
keepRatio={false} keepRatio={false}
anchorSize={8} anchorSize={8}
anchorStroke="#3b82f6" anchorStroke="#E07832"
anchorFill="#ffffff" anchorFill="#ffffff"
anchorCornerRadius={2} anchorCornerRadius={2}
borderStroke="#3b82f6" borderStroke="#E07832"
borderStrokeWidth={1} borderStrokeWidth={1}
padding={2} padding={2}
/> />
@@ -793,7 +793,7 @@ export function ActiveSelectionPreview({
}) { }) {
if (type === "lasso" && points.length >= 4) { if (type === "lasso" && points.length >= 4) {
return ( return (
<Line points={points} stroke="#3b82f6" strokeWidth={1} dash={[4, 4]} listening={false} /> <Line points={points} stroke="#E07832" strokeWidth={1} dash={[4, 4]} listening={false} />
); );
} }
@@ -812,7 +812,7 @@ export function ActiveSelectionPreview({
y={y + h / 2} y={y + h / 2}
radiusX={w / 2} radiusX={w / 2}
radiusY={h / 2} radiusY={h / 2}
stroke="#3b82f6" stroke="#E07832"
strokeWidth={1} strokeWidth={1}
dash={[4, 4]} dash={[4, 4]}
listening={false} listening={false}
@@ -826,7 +826,7 @@ export function ActiveSelectionPreview({
y={y} y={y}
width={w} width={w}
height={h} height={h}
stroke="#3b82f6" stroke="#E07832"
strokeWidth={1} strokeWidth={1}
dash={[4, 4]} dash={[4, 4]}
listening={false} listening={false}
@@ -295,10 +295,10 @@ export function TransformToolTransformer({
300, 315, 330, 345, 300, 315, 330, 345,
]} ]}
anchorSize={8} anchorSize={8}
anchorStroke="#3b82f6" anchorStroke="#E07832"
anchorFill="#ffffff" anchorFill="#ffffff"
anchorCornerRadius={2} anchorCornerRadius={2}
borderStroke="#3b82f6" borderStroke="#E07832"
borderStrokeWidth={1} borderStrokeWidth={1}
borderDash={[4, 4]} borderDash={[4, 4]}
padding={0} padding={0}
@@ -188,8 +188,8 @@ export function FileDetails({ mobile = false }: FileDetailsProps) {
{/* Details card */} {/* Details card */}
<div className="flex-1"> <div className="flex-1">
<div className="rounded-lg border border-border overflow-hidden"> <div className="rounded-lg border border-border overflow-hidden">
<div className="bg-blue-500/10 border-b border-border px-3 py-2"> <div className="bg-primary/10 border-b border-border px-3 py-2">
<h4 className="text-sm font-semibold text-blue-600 dark:text-blue-400"> <h4 className="text-sm font-semibold text-primary-dark dark:text-primary-light">
{t.files.fileDetailsHeading} {t.files.fileDetailsHeading}
</h4> </h4>
</div> </div>
@@ -119,10 +119,7 @@ export function ToolPanel() {
return ( return (
<div key={modality.id} className={isLast ? "" : "mb-2"}> <div key={modality.id} className={isLast ? "" : "mb-2"}>
{idx > 0 && <hr className="border-border mb-4" />} {idx > 0 && <hr className="border-border mb-4" />}
<div <div className="flex items-center gap-2 mt-4 mb-3 pl-1">
className="flex items-center gap-2 mt-4 mb-3 pl-2.5 border-l-[3px] rounded-sm"
style={{ borderColor: modality.color }}
>
{ModalityIcon && ( {ModalityIcon && (
<span className="shrink-0" style={{ color: modality.color }}> <span className="shrink-0" style={{ color: modality.color }}>
<ModalityIcon className="h-4.5 w-4.5" /> <ModalityIcon className="h-4.5 w-4.5" />
@@ -47,7 +47,7 @@ const FORMAT_LABELS: Record<string, string> = {
/** Badge colors by barcode family. */ /** Badge colors by barcode family. */
function getBadgeColor(type: string): string { function getBadgeColor(type: string): string {
if (type.includes("QR") || type === "Aztec" || type === "DataMatrix" || type === "MaxiCode") 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")) if (type.includes("EAN") || type.includes("UPC") || type.includes("DataBar"))
return "bg-green-500/15 text-green-600 dark:text-green-400"; 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"; if (type === "PDF417") return "bg-purple-500/15 text-purple-600 dark:text-purple-400";
@@ -459,7 +459,7 @@ function CollageCell({
className={cn( className={cn(
"relative overflow-hidden transition-shadow", "relative overflow-hidden transition-shadow",
isSelected && "ring-2 ring-primary ring-offset-1", 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 && "border-2 border-dashed border-border/50",
image && !isSelected && "cursor-grab", image && !isSelected && "cursor-grab",
image && isSelected && "cursor-grab active:cursor-grabbing", image && isSelected && "cursor-grab active:cursor-grabbing",
@@ -521,7 +521,7 @@ function CollageCell({
className={cn( className={cn(
"flex items-center gap-1 rounded px-1.5 py-1 text-[11px] font-medium backdrop-blur-sm transition-colors", "flex items-center gap-1 rounded px-1.5 py-1 text-[11px] font-medium backdrop-blur-sm transition-colors",
transform.objectFit === "contain" 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", : "bg-black/50 text-white hover:bg-black/70",
)} )}
title={transform.objectFit === "cover" ? "Fit entire image" : "Fill cell"} 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. */ /* C+D: cancel in-flight renders and destroy the doc proxy on cleanup. */
useEffect(() => { useEffect(() => {
if (!src || !canvasRef.current) return; if (!canvasRef.current) return;
const file = entry?.file;
if (!file && !src) return;
let cancelled = false; let cancelled = false;
let doc: pdfjs.PDFDocumentProxy | undefined; let doc: pdfjs.PDFDocumentProxy | undefined;
let renderTask: pdfjs.RenderTask | undefined; let renderTask: pdfjs.RenderTask | undefined;
(async () => { (async () => {
try { 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; if (cancelled) return;
setPageCount(doc.numPages); setPageCount(doc.numPages);
const pdfPage = await doc.getPage(Math.min(page, doc.numPages)); const pdfPage = await doc.getPage(Math.min(page, doc.numPages));
@@ -60,7 +63,7 @@ export function DocumentView() {
renderTask?.cancel(); renderTask?.cancel();
doc?.loadingTask.destroy(); doc?.loadingTask.destroy();
}; };
}, [src, page]); }, [src, page, entry?.file]);
if (!entry) return null; if (!entry) return null;
@@ -1,7 +1,7 @@
import { Download, FileUp, Loader2, X } from "lucide-react"; import { Download, Loader2 } from "lucide-react";
import { useCallback, useRef } from "react"; import { useEffect } from "react";
import { useTranslation } from "@/contexts/i18n-context"; 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"; import { usePdfToImageStore } from "@/stores/pdf-to-image-store";
const FORMAT_OPTIONS = [ const FORMAT_OPTIONS = [
@@ -41,57 +41,22 @@ const LOSSY_FORMATS = ["jpg", "webp", "avif", "heic", "heif", "jxl"];
export function PdfToImageSettings() { export function PdfToImageSettings() {
const { t } = useTranslation(); const { t } = useTranslation();
const store = usePdfToImageStore(); 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 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; const selectedCount = store.selectedPages.size;
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* PDF upload area */} {/* PDF info */}
{!store.file ? ( {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>
) : (
<div className="flex items-center gap-2 p-3 rounded-lg bg-muted"> <div className="flex items-center gap-2 p-3 rounded-lg bg-muted">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{store.file.name}</p> <p className="text-sm font-medium truncate">{store.file.name}</p>
@@ -106,13 +71,6 @@ export function PdfToImageSettings() {
) : null} ) : null}
</p> </p>
</div> </div>
<button
type="button"
onClick={handleRemoveFile}
className="p-1 hover:bg-background rounded"
>
<X className="h-4 w-4 text-muted-foreground" />
</button>
</div> </div>
)} )}
+1 -1
View File
@@ -78,7 +78,7 @@ export const TOOL_DISPLAY_MODES: Record<string, DisplayMode> = {
favicon: "before-after", favicon: "before-after",
"image-to-pdf": "before-after", "image-to-pdf": "before-after",
"optimize-for-web": "before-after", "optimize-for-web": "before-after",
"pdf-to-image": "no-dropzone", "pdf-to-image": "custom-results",
// Adjustments extra // Adjustments extra
"replace-color": "before-after", "replace-color": "before-after",
+3 -12
View File
@@ -219,10 +219,7 @@ export function FullscreenGridPage() {
}>; }>;
return ( return (
<section key={section.modality.id}> <section key={section.modality.id}>
<div <div className="flex items-center gap-3 mb-5 pb-3 border-b border-border">
className="flex items-center gap-3 mb-5 pb-3 border-b-2"
style={{ borderColor: section.modality.color }}
>
{SectionIcon && ( {SectionIcon && (
<div <div
className="p-2 rounded-lg" className="p-2 rounded-lg"
@@ -268,10 +265,7 @@ export function FullscreenGridPage() {
? (ICON_MAP[activeMod.icon] as React.ComponentType<{ className?: string }>) ? (ICON_MAP[activeMod.icon] as React.ComponentType<{ className?: string }>)
: null; : null;
return activeMod ? ( return activeMod ? (
<div <div className="flex items-center gap-3 mb-6 pb-3 border-b border-border">
className="flex items-center gap-3 mb-6 pb-3 border-b-2"
style={{ borderColor: activeMod.color }}
>
{ModIcon && ( {ModIcon && (
<div <div
className="p-2 rounded-lg" className="p-2 rounded-lg"
@@ -331,10 +325,7 @@ function CategoryCard({
(ICON_MAP[category.icon] as React.ComponentType<{ className?: string }>) ?? LayoutGrid; (ICON_MAP[category.icon] as React.ComponentType<{ className?: string }>) ?? LayoutGrid;
return ( return (
<div <div className="rounded-xl border border-border bg-card overflow-hidden" style={undefined}>
className="rounded-xl border border-border bg-card overflow-hidden shadow-sm hover:shadow-md transition-shadow"
style={accentColor ? { borderLeftWidth: 3, borderLeftColor: accentColor } : undefined}
>
{/* Header */} {/* Header */}
<div <div
className="px-4 py-3 flex items-center gap-3" className="px-4 py-3 flex items-center gap-3"
+47 -112
View File
@@ -5,8 +5,8 @@ import {
TOOL_BUNDLE_MAP, TOOL_BUNDLE_MAP,
TOOLS, TOOLS,
} from "@snapotter/shared"; } from "@snapotter/shared";
import { Clock, Download, FileArchive, Loader2 } from "lucide-react"; import { Clock, Download, FileArchive, LayoutGrid, Loader2 } from "lucide-react";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { ImageViewer } from "@/components/common/image-viewer"; import { ImageViewer } from "@/components/common/image-viewer";
import { MultiImageViewer } from "@/components/common/multi-image-viewer"; import { MultiImageViewer } from "@/components/common/multi-image-viewer";
@@ -17,12 +17,12 @@ import { useTranslation } from "@/contexts/i18n-context";
import { useMobile } from "@/hooks/use-mobile"; import { useMobile } from "@/hooks/use-mobile";
import { ICON_MAP } from "@/lib/icon-map"; import { ICON_MAP } from "@/lib/icon-map";
import { getCategoryName, getModalityName, getToolName } from "@/lib/tool-i18n"; import { getCategoryName, getModalityName, getToolName } from "@/lib/tool-i18n";
import { cn } from "@/lib/utils";
import { useFeaturesStore } from "@/stores/features-store"; import { useFeaturesStore } from "@/stores/features-store";
import { useFileStore } from "@/stores/file-store"; import { useFileStore } from "@/stores/file-store";
import { useSettingsStore } from "@/stores/settings-store"; import { useSettingsStore } from "@/stores/settings-store";
// Tools shown prominently as "quick actions" at the top // Tools shown prominently as "quick actions" at the top
const QUICK_ACTION_IDS = ["resize", "compress", "convert", "remove-background"];
let hasAppliedDefaultRedirect = false; let hasAppliedDefaultRedirect = false;
@@ -96,6 +96,9 @@ export function HomePage() {
[setFiles], [setFiles],
); );
const [modalityFilter, setModalityFilter] = useState<string>("all");
const TAB_MODALITIES = useMemo(() => MODALITIES.filter((m) => m.id !== "file"), []);
const hasFile = files.length > 0; const hasFile = files.length > 0;
// If no file uploaded, show default layout (tool panel + dropzone) // If no file uploaded, show default layout (tool panel + dropzone)
@@ -127,54 +130,6 @@ export function HomePage() {
</button> </button>
</div> </div>
{/* Quick action buttons - horizontal scroll */}
<div className="flex overflow-x-auto gap-2 px-4 py-3 border-b border-border scrollbar-none">
{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 (
<button
key={id}
type="button"
onClick={() => navigate(tool.route)}
className="flex items-center gap-2 px-3 py-2 rounded-xl border border-border hover:border-primary hover:bg-primary/5 transition-colors shrink-0"
>
<div className="p-1 rounded-lg bg-primary/10 text-primary">
<Icon className="h-4 w-4" />
</div>
<span className="text-xs font-medium text-foreground whitespace-nowrap">
{getToolName(t, tool.id, tool.name)}
</span>
{status === "not_installed" && (
<>
<Download className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
<span className="sr-only">{t.a11y.notInstalled}</span>
</>
)}
{status === "queued" && (
<>
<Clock className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
<span className="sr-only">{t.a11y.queued}</span>
</>
)}
{status === "installing" && (
<>
<Loader2
className="h-3.5 w-3.5 text-muted-foreground animate-spin"
aria-hidden="true"
/>
<span className="sr-only">{t.a11y.installing}</span>
</>
)}
</button>
);
})}
</div>
{/* Full-width file preview */} {/* Full-width file preview */}
<div className="flex-1 flex items-center justify-center p-4 min-h-0"> <div className="flex-1 flex items-center justify-center p-4 min-h-0">
{files.length > 1 ? ( {files.length > 1 ? (
@@ -244,72 +199,52 @@ export function HomePage() {
</button> </button>
</div> </div>
{/* Quick actions */} {/* Modality filter tabs */}
<div className="p-4 border-b border-border"> <div className="flex flex-wrap gap-1 px-4 pt-3 pb-2 border-b border-border sticky top-0 bg-background z-10">
<h3 className="text-xs font-semibold uppercase text-muted-foreground tracking-wider mb-3"> <button
{t.homePage.quickActions} type="button"
</h3> onClick={() => setModalityFilter("all")}
<div className="grid grid-cols-2 gap-2"> className={cn(
{QUICK_ACTION_IDS.map((id) => { "flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium transition-colors",
const tool = TOOLS.find((t) => t.id === id); modalityFilter === "all"
if (!tool) return null; ? "bg-primary/10 text-primary"
const Icon = : "text-muted-foreground hover:text-foreground hover:bg-muted",
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? )}
ICON_MAP.FileImage; >
const status = getToolStatus(id); <LayoutGrid className="h-3.5 w-3.5" />
return ( <span>All</span>
<button </button>
key={id} {TAB_MODALITIES.map((m) => {
type="button" const Icon = ICON_MAP[m.icon] as React.ComponentType<{ className?: string }>;
onClick={() => navigate(tool.route)} const isActive = modalityFilter === m.id;
className="flex items-center gap-2 p-3 rounded-xl border border-border hover:border-primary hover:bg-primary/5 transition-colors text-start" const label = m.id === "document" ? "Docs" : m.name;
> return (
<div className="p-1.5 rounded-lg bg-primary/10 text-primary"> <button
<Icon className="h-4 w-4" /> key={m.id}
</div> type="button"
<span className="text-xs font-medium text-foreground"> onClick={() => setModalityFilter(m.id)}
{getToolName(t, tool.id, tool.name)} className={cn(
</span> "flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium transition-colors",
{status === "not_installed" && ( isActive
<> ? "bg-primary/10 text-primary"
<Download : "text-muted-foreground hover:text-foreground hover:bg-muted",
className="h-3.5 w-3.5 text-muted-foreground ms-auto" )}
aria-hidden="true" >
/> {Icon && <Icon className="h-3.5 w-3.5" />}
<span className="sr-only">{t.a11y.notInstalled}</span> <span>{label}</span>
</> </button>
)} );
{status === "queued" && ( })}
<>
<Clock
className="h-3.5 w-3.5 text-muted-foreground ms-auto"
aria-hidden="true"
/>
<span className="sr-only">{t.a11y.queued}</span>
</>
)}
{status === "installing" && (
<>
<Loader2
className="h-3.5 w-3.5 text-muted-foreground ms-auto animate-spin"
aria-hidden="true"
/>
<span className="sr-only">{t.a11y.installing}</span>
</>
)}
</button>
);
})}
</div>
</div> </div>
{/* All tools by modality and category */} {/* Tools by modality and category */}
<div className="p-4"> <div className="p-4">
<h3 className="text-xs font-semibold uppercase text-muted-foreground tracking-wider mb-3">
{t.homePage.allTools}
</h3>
{MODALITIES.filter((m) => { {MODALITIES.filter((m) => {
if (m.id === "file") return false; if (m.id === "file") return false;
if (modalityFilter !== "all") {
if (modalityFilter === "document") return m.id === "document";
return m.id === modalityFilter;
}
const key = m.id; const key = m.id;
return TOOLS.some( return TOOLS.some(
(tool) => tool.modality === key || (key === "document" && tool.modality === "file"), (tool) => tool.modality === key || (key === "document" && tool.modality === "file"),
+1 -1
View File
@@ -191,7 +191,7 @@ export const useEditorStore = create<EditorState & EditorStateExtensions>()(
editingTextId: null, editingTextId: null,
// --- Shape settings --- // --- Shape settings ---
shapeFill: "#3b82f6" as string | null, shapeFill: "#E07832" as string | null,
shapeFillOpacity: 1, shapeFillOpacity: 1,
shapeStroke: "#000000" as string | null, shapeStroke: "#000000" as string | null,
shapeStrokeOpacity: 1, shapeStrokeOpacity: 1,
+24 -20
View File
@@ -10,31 +10,35 @@
} }
@theme { @theme {
--color-primary: #2563eb; --color-primary: #E07832;
--color-primary-foreground: #ffffff; --color-primary-foreground: #ffffff;
--color-background: #ffffff; --color-primary-light: #F09550;
--color-foreground: #0f172a; --color-primary-dark: #C06520;
--color-muted: #f1f5f9; --color-primary-subtle: #FFF5ED;
--color-muted-foreground: #64748b; --color-background: #FAFAF7;
--color-border: #e2e8f0; --color-foreground: #1A1814;
--color-muted: #F3EEE6;
--color-muted-foreground: #6B6560;
--color-border: #DDD6CC;
--color-card: #ffffff; --color-card: #ffffff;
--color-card-foreground: #0f172a; --color-card-foreground: #1A1814;
--color-sidebar: #f8fafc; --color-sidebar: #F3EEE6;
--color-sidebar-foreground: #334155; --color-sidebar-foreground: #3D3832;
--color-accent: #3b82f6; --color-accent: #F09550;
--color-destructive: #ef4444; --color-destructive: #D44C47;
--color-success: #2D9D78;
} }
.dark { .dark {
--color-background: #0f172a; --color-background: #1A1210;
--color-foreground: #f8fafc; --color-foreground: #F0EBE4;
--color-muted: #1e293b; --color-muted: #262018;
--color-muted-foreground: #94a3b8; --color-muted-foreground: #9A908A;
--color-border: #334155; --color-border: #382E26;
--color-card: #1e293b; --color-card: #262018;
--color-card-foreground: #f8fafc; --color-card-foreground: #F0EBE4;
--color-sidebar: #1e293b; --color-sidebar: #262018;
--color-sidebar-foreground: #cbd5e1; --color-sidebar-foreground: #C5BAB0;
} }
input[type="range"] { input[type="range"] {