mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat: add editor polish (mobile handling, loading states, accessibility)
- Show desktop-recommended message on mobile viewports (<768px) - Add LoadingOverlay component for canvas loading/progress states - Add ARIA tablist/tab/aria-selected to right panel tabs - Add "Open in Editor" link in review panel after tool processing
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { TOOLS } from "@snapotter/shared";
|
||||
import { ArrowRight, ChevronDown, ChevronRight, Download, FileImage, Undo2 } from "lucide-react";
|
||||
import {
|
||||
ArrowRight,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Download,
|
||||
FileImage,
|
||||
PenTool,
|
||||
Undo2,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { formatFileSize, triggerDownload } from "@/lib/download";
|
||||
import { ICON_MAP } from "@/lib/icon-map";
|
||||
import { getSuggestedTools } from "@/lib/suggested-tools";
|
||||
@@ -103,6 +111,15 @@ export function ReviewPanel({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Open in Editor */}
|
||||
<Link
|
||||
to={`/editor?url=${encodeURIComponent(downloadUrl)}`}
|
||||
className="flex items-center justify-center gap-1.5 w-full py-2 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted text-xs font-medium"
|
||||
>
|
||||
<PenTool className="h-3.5 w-3.5" />
|
||||
Open in Editor
|
||||
</Link>
|
||||
|
||||
{/* Suggested tools */}
|
||||
{suggestedTools.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// apps/web/src/components/editor/common/loading-overlay.tsx
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEditorStore } from "@/stores/editor-store";
|
||||
|
||||
export function LoadingOverlay() {
|
||||
const loadingState = useEditorStore((s) => s.loadingState);
|
||||
const setLoadingState = useEditorStore((s) => s.setLoadingState);
|
||||
|
||||
if (!loadingState) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background/60 backdrop-blur-sm">
|
||||
<div className="flex flex-col items-center gap-3 p-6 rounded-xl bg-card border border-border shadow-lg min-w-[200px]">
|
||||
{/* Operation name */}
|
||||
<p className="text-sm font-medium text-foreground">{loadingState.operation}</p>
|
||||
|
||||
{/* Progress bar */}
|
||||
{loadingState.progress !== null && (
|
||||
<div className="w-full h-1.5 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full bg-primary transition-all duration-200",
|
||||
loadingState.progress < 0 && "animate-pulse w-full",
|
||||
)}
|
||||
style={
|
||||
loadingState.progress >= 0
|
||||
? { width: `${Math.min(100, loadingState.progress)}%` }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Indeterminate spinner when no progress */}
|
||||
{loadingState.progress === null && (
|
||||
<div className="w-5 h-5 border-2 border-muted border-t-primary rounded-full animate-spin" />
|
||||
)}
|
||||
|
||||
{/* Cancel button */}
|
||||
{loadingState.cancellable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLoadingState(null)}
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground mt-1"
|
||||
aria-label="Cancel operation"
|
||||
>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import { useEditorShortcuts } from "@/hooks/use-editor-shortcuts";
|
||||
import { useEditorStore } from "@/stores/editor-store";
|
||||
import type { CanvasObject } from "@/types/editor";
|
||||
import { BrushCursorOverlay, useEditorCursor } from "./common/custom-cursor";
|
||||
import { LoadingOverlay } from "./common/loading-overlay";
|
||||
import { useBrushTool } from "./tools/brush-tool";
|
||||
import { useEraserTool } from "./tools/eraser-tool";
|
||||
import { useFillTool } from "./tools/fill-tool";
|
||||
@@ -391,6 +392,7 @@ export function EditorCanvas() {
|
||||
)}
|
||||
</Stage>
|
||||
<BrushCursorOverlay containerRef={containerRef} />
|
||||
<LoadingOverlay />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,11 +40,13 @@ export function EditorRightPanel() {
|
||||
{sourceImageUrl && <NavigatorPanel />}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex items-center border-b border-border">
|
||||
<div className="flex items-center border-b border-border" role="tablist">
|
||||
{TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === tab.id}
|
||||
onClick={() => setTab(tab.id)}
|
||||
className={cn(
|
||||
"flex-1 py-2 text-xs font-medium text-center transition-colors",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// apps/web/src/pages/editor-page.tsx
|
||||
import { Monitor } from "lucide-react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { WelcomeScreen } from "@/components/editor/common/welcome-screen";
|
||||
import { EditorCanvas } from "@/components/editor/editor-canvas";
|
||||
@@ -6,9 +7,11 @@ import { EditorOptionsBar } from "@/components/editor/editor-options-bar";
|
||||
import { EditorRightPanel } from "@/components/editor/editor-right-panel";
|
||||
import { EditorStatusBar } from "@/components/editor/editor-status-bar";
|
||||
import { EditorToolbar } from "@/components/editor/editor-toolbar";
|
||||
import { useMobile } from "@/hooks/use-mobile";
|
||||
import { useEditorStore } from "@/stores/editor-store";
|
||||
|
||||
export function EditorPage() {
|
||||
const isMobile = useMobile();
|
||||
const sourceImageUrl = useEditorStore((s) => s.sourceImageUrl);
|
||||
const isDirty = useEditorStore((s) => s.isDirty);
|
||||
const loadImage = useEditorStore((s) => s.loadImage);
|
||||
@@ -59,6 +62,19 @@ export function EditorPage() {
|
||||
}
|
||||
}, [loadImage]);
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
|
||||
<Monitor size={48} className="text-muted-foreground mb-4" />
|
||||
<h2 className="text-lg font-semibold text-foreground mb-2">Desktop Recommended</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-sm">
|
||||
The image editor works best on desktop screens. Please switch to a device with a larger
|
||||
display for the full editing experience.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<EditorOptionsBar />
|
||||
|
||||
Reference in New Issue
Block a user