From e22ffa4ec95cf6ce867cc86b60dd610fa8c82af8 Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Fri, 8 May 2026 23:30:27 +0800 Subject: [PATCH] fix: editor native layout, UI polish, meme templates auth, pipeline export, install queuing - Remove editor from AppLayout so it owns the full viewport with no sidebar/padding - Add back-arrow in menu bar for navigation, restyle menu bar to match native app feel - Slim left toolbar from 48px to 36px with smaller icons and subtler dividers - Fix options bar overlapping canvas via shrink-0 and overflow-hidden - Fix opacity/blend controls clipping in right panel with min-w-0 on flex children - Add global range slider CSS for consistent thin-track + round-thumb styling - Make meme template routes public so img tags can load without Bearer auth - Show pipeline export buttons in default compact view, not just expanded hover - Queue individual AI feature installs instead of erroring when one is active --- apps/api/src/plugins/auth.ts | 1 + apps/web/src/App.tsx | 9 +-- .../components/editor/common/fill-dialog.tsx | 2 +- .../components/editor/common/icon-button.tsx | 4 +- .../components/editor/common/slider-row.tsx | 2 +- .../src/components/editor/editor-menu-bar.tsx | 76 +++++++++++-------- .../components/editor/editor-options-bar.tsx | 8 +- .../components/editor/editor-right-panel.tsx | 2 +- .../components/editor/editor-status-bar.tsx | 2 +- .../src/components/editor/editor-toolbar.tsx | 4 +- .../editor/options/brush-options.tsx | 6 +- .../editor/options/clone-stamp-options.tsx | 6 +- .../editor/options/dodge-burn-options.tsx | 6 +- .../editor/options/fill-options.tsx | 2 +- .../editor/options/gradient-options.tsx | 2 +- .../editor/options/pixel-brush-options.tsx | 4 +- .../editor/options/shape-options.tsx | 4 +- .../components/editor/panels/layers-panel.tsx | 16 ++-- .../editor/panels/navigator-panel.tsx | 7 +- apps/web/src/pages/automate-page.tsx | 37 ++++++--- apps/web/src/pages/editor-page.tsx | 2 +- apps/web/src/stores/features-store.ts | 30 ++++++++ apps/web/src/styles/globals.css | 43 +++++++++++ 23 files changed, 181 insertions(+), 94 deletions(-) diff --git a/apps/api/src/plugins/auth.ts b/apps/api/src/plugins/auth.ts index 03cdca1b..e0ed6775 100644 --- a/apps/api/src/plugins/auth.ts +++ b/apps/api/src/plugins/auth.ts @@ -765,6 +765,7 @@ const PUBLIC_PATHS = [ "/api/v1/jobs/", "/api/docs", "/api/v1/openapi.yaml", + "/api/v1/meme-templates/", ]; function isPublicRoute(url: string): boolean { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 55ce7e52..387cc4ee 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -228,14 +228,7 @@ export function App() { } /> } /> } /> - - - - } - /> + } /> } /> } /> diff --git a/apps/web/src/components/editor/common/fill-dialog.tsx b/apps/web/src/components/editor/common/fill-dialog.tsx index bf66d3a4..e05ac161 100644 --- a/apps/web/src/components/editor/common/fill-dialog.tsx +++ b/apps/web/src/components/editor/common/fill-dialog.tsx @@ -162,7 +162,7 @@ export function FillDialog({ open, onClose }: FillDialogProps) { max={100} value={opacity} onChange={(e) => setOpacity(Number(e.target.value))} - className="flex-1 h-1 accent-primary" + className="flex-1" /> onChange(Number(e.target.value))} - className="flex-1 h-1 accent-primary cursor-pointer" + className="flex-1 cursor-pointer" /> (null); const barRef = useRef(null); const close = useCallback(() => setOpenMenu(null), []); + const navigate = useNavigate(); useEffect(() => { if (!openMenu) return; @@ -465,40 +467,50 @@ export function EditorMenuBar(props: MenuBarCallbacks) { return (
- {menus.map((menu) => ( -
- - {openMenu === menu.testId && ( -
navigate("/")} + className="flex items-center gap-1.5 px-2.5 h-full text-muted-foreground hover:text-foreground hover:bg-muted transition-colors border-r border-border" + title="Back to SnapOtter" + > + + +
+ {menus.map((menu) => ( +
+
- )} -
- ))} + {menu.label} + + {openMenu === menu.testId && ( +
+ {menu.items.map((item) => ( + + ))} +
+ )} +
+ ))} +
); } diff --git a/apps/web/src/components/editor/editor-options-bar.tsx b/apps/web/src/components/editor/editor-options-bar.tsx index d24def5d..ac0d5e39 100644 --- a/apps/web/src/components/editor/editor-options-bar.tsx +++ b/apps/web/src/components/editor/editor-options-bar.tsx @@ -84,15 +84,15 @@ export function EditorOptionsBar() { const foregroundColor = useEditorStore((s) => s.foregroundColor); return ( -
- +
+ {activeTool .replace(/-/g, " ") .replace(/^shape /, "") .replace(/\b\w/g, (c) => c.toUpperCase())} -
-
+
+
{OptionsComponent && } {activeTool === "eyedropper" && ( {/* Tab content */} -
+
{activeTab === "layers" && } {activeTab === "adjustments" && } {activeTab === "history" && } diff --git a/apps/web/src/components/editor/editor-status-bar.tsx b/apps/web/src/components/editor/editor-status-bar.tsx index 10538a3d..0ed8e05b 100644 --- a/apps/web/src/components/editor/editor-status-bar.tsx +++ b/apps/web/src/components/editor/editor-status-bar.tsx @@ -11,7 +11,7 @@ export function EditorStatusBar() { const zoomPercent = Math.round(zoom * 100); return ( -
+
{sourceImageUrl && ( <> diff --git a/apps/web/src/components/editor/editor-toolbar.tsx b/apps/web/src/components/editor/editor-toolbar.tsx index 9138e31c..9b59f730 100644 --- a/apps/web/src/components/editor/editor-toolbar.tsx +++ b/apps/web/src/components/editor/editor-toolbar.tsx @@ -112,10 +112,10 @@ export function EditorToolbar() { const sourceImageUrl = useEditorStore((s) => s.sourceImageUrl); return ( -
+
{TOOL_GROUPS.map((group, gi) => (
- {gi > 0 &&
} + {gi > 0 &&
} {group.tools.map((t) => ( setBrushSize(Number(e.target.value))} - className="w-20 h-1 accent-primary" + className="w-20" /> setBrushOpacity(Number(e.target.value) / 100)} - className="w-20 h-1 accent-primary" + className="w-20" /> setBrushHardness(Number(e.target.value) / 100)} - className="w-20 h-1 accent-primary" + className="w-20" /> setBrushSize(Number(e.target.value))} - className="w-20 h-1 accent-primary" + className="w-20" /> setBrushOpacity(Number(e.target.value) / 100)} - className="w-20 h-1 accent-primary" + className="w-20" /> setBrushHardness(Number(e.target.value) / 100)} - className="w-20 h-1 accent-primary" + className="w-20" /> setBrushSize(Number(e.target.value))} - className="w-16 h-1 accent-primary" + className="w-16" /> setDodgeBurnExposure(Number(e.target.value))} - className="w-16 h-1 accent-primary" + className="w-16" /> setSpongeFlow(Number(e.target.value))} - className="w-16 h-1 accent-primary" + className="w-16" /> setFillTolerance(Number(e.target.value))} - className="w-20 h-1 accent-primary" + className="w-20" /> setGradientOpacity(Number(e.target.value) / 100)} - className="w-20 h-1 accent-primary" + className="w-20" /> setBrushSize(Number(e.target.value))} - className="w-20 h-1 accent-primary" + className="w-20" /> setPixelBrushStrength(Number(e.target.value))} - className="w-20 h-1 accent-primary" + className="w-20" /> setShapeStrokeWidth(Number(e.target.value))} - className="w-16 h-1 accent-primary" + className="w-16" /> setShapeCornerRadius(Number(e.target.value))} - className="w-16 h-1 accent-primary" + className="w-16" /> void }) { return ( -
+
@@ -269,7 +269,7 @@ function BlendModeSelect({ value, onChange }: { value: string; onChange: (mode: id="blend-mode-select" value={value} onChange={(e) => onChange(e.target.value)} - className="flex-1 h-7 text-xs bg-muted border border-border rounded px-1.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary" + className="flex-1 min-w-0 h-7 text-xs bg-muted border border-border rounded px-1.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary" data-testid="blend-mode-select" > {BLEND_MODES.map((mode) => ( @@ -289,7 +289,7 @@ function BlendModeSelect({ value, onChange }: { value: string; onChange: (mode: function OpacitySlider({ value, onChange }: { value: number; onChange: (v: number) => void }) { const percent = Math.round(value * 100); return ( -
+
@@ -300,10 +300,10 @@ function OpacitySlider({ value, onChange }: { value: number; onChange: (v: numbe max={100} value={percent} onChange={(e) => onChange(Number(e.target.value) / 100)} - className="flex-1" + className="flex-1 min-w-0" data-testid="layer-opacity-slider" /> - + {percent}%
@@ -990,7 +990,7 @@ function EffectSlider({ onChange: (v: number) => void; }) { return ( -
+
{label} onChange(Number(e.target.value))} - className="flex-1" + className="flex-1 min-w-0" /> - + {value} {suffix} diff --git a/apps/web/src/components/editor/panels/navigator-panel.tsx b/apps/web/src/components/editor/panels/navigator-panel.tsx index 0426229a..bd85f847 100644 --- a/apps/web/src/components/editor/panels/navigator-panel.tsx +++ b/apps/web/src/components/editor/panels/navigator-panel.tsx @@ -2,7 +2,6 @@ import { Minus, Plus } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; -import { cn } from "@/lib/utils"; import { useEditorStore } from "@/stores/editor-store"; const THUMBNAIL_MAX_HEIGHT = 80; @@ -257,11 +256,7 @@ export function NavigatorPanel() { step={0.001} value={Math.log(zoom / MIN_ZOOM) / Math.log(MAX_ZOOM / MIN_ZOOM)} onChange={handleZoomSlider} - className={cn( - "flex-1 h-1 appearance-none rounded-full bg-muted", - "[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-2.5 [&::-webkit-slider-thumb]:h-2.5", - "[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-foreground [&::-webkit-slider-thumb]:cursor-pointer", - )} + className="flex-1" /> @@ -652,17 +654,28 @@ export function AutomatePage() {
) : ( -
+
{savedPipelines.slice(0, 3).map((p) => ( - +
+ + +
))} {savedPipelines.length > 3 && (