Image Editor - Pintura-Style Redesign
Complete redesign plan for the image editor to match Pintura UX with intelligent bottom toolbar and simplified architecture.
Last updated: 11/15/2025
Image Editor - Pintura-Style Redesign
Goal: Transform the current complex multi-toolbar editor into a clean, Pintura-inspired experience with intelligent context-aware controls.
π― Core UX Principles (Pintura-Inspired)
1. Single Bottom Toolbar
- All controls at the bottom (desktop & mobile)
- Toolbar adapts intelligently based on active tool
- No top toolbars cluttering the canvas
- Clean, distraction-free editing area
2. Left Sidebar for Tool Selection
- Vertical tool icons on the left (desktop)
- Horizontal tool icons at bottom (mobile)
- Active tool highlighted
- Tooltips on hover
3. Context-Aware Bottom Toolbar
Each tool shows only relevant controls in the bottom toolbar:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β CANVAS (Full Screen) β
β β
β [Tools] β
β Crop β
β Adjust β
β Filter β
β Annotate β
β Blur β
β Watermark β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[Tool-Specific Controls] [Undo] [Redo] [Save]
π Current Problems
Too Many Toolbars (Consolidate These)
Current Structure (OVERCOMPLICATED):
- β
EditorTopToolbar.svelte- Generic top controls - β
CropTopToolbar.svelte- Crop-specific top controls - β
CropBottomBar.svelte- Crop-specific bottom controls - β
FineTuneTopToolbar.svelte- FineTune top controls - β
FineTuneBottomBar.svelte- FineTune bottom controls - β
BlurTopToolbar.svelte- Blur top controls - β
WatermarkTopToolbar.svelte- Watermark top controls - β
AnnotateTopToolbar.svelte- Annotate top controls - β
MobileToolbar.svelte- Mobile-specific controls
This is 9 different toolbar components!
New Structure (SIMPLIFIED)
Single Adaptive Toolbar:
- β
EditorToolbar.svelte- ONE intelligent bottom toolbar- Adapts to show tool-specific controls
- Always shows: Undo, Redo, Save, Cancel
- Tool-specific section changes dynamically
Sidebar:
- β
EditorSidebar.svelte- Keep (already good)
π¨ New Component Architecture
File Structure
src/components/ImageEditor/
βββ ImageEditor.svelte # Main component (modal-ready)
βββ EditorSidebar.svelte # Tool selection sidebar
βββ EditorToolbar.svelte # β¨ NEW: Single adaptive bottom toolbar
βββ EditorCanvas.svelte # Canvas wrapper (keep as is)
β
βββ tools/ # Tool-specific logic
β βββ Crop.svelte
β βββ FineTune.svelte
β βββ Blur.svelte
β βββ Annotate.svelte
β βββ Watermark.svelte
β βββ FocalPoint.svelte # β¨ NEW
β
βββ toolbars/ # β¨ NEW: Tool-specific control panels
βββ CropControls.svelte
βββ FineTuneControls.svelte
βββ BlurControls.svelte
βββ AnnotateControls.svelte
βββ WatermarkControls.svelte
βββ FocalPointControls.svelte
π§ EditorToolbar.svelte (New Single Toolbar)
Layout Structure
<div class="fixed bottom-0 left-0 right-0 border-t bg-surface-100 p-4 dark:bg-surface-800">
<div class="mx-auto flex max-w-7xl items-center justify-between gap-4">
<!-- Left: Tool-Specific Controls (Dynamic) -->
<div class="flex flex-1 items-center gap-3">
{#if activeTool === 'crop'}
<CropControls {cropToolRef} />
{:else if activeTool === 'finetune'}
<FineTuneControls {fineTuneRef} />
{:else if activeTool === 'blur'}
<BlurControls {blurToolRef} />
{:else if activeTool === 'annotate'}
<AnnotateControls {annotateToolRef} />
{:else if activeTool === 'watermark'}
<WatermarkControls {watermarkToolRef} />
{/if}
</div>
<!-- Right: Global Actions -->
<div class="flex items-center gap-2">
<button onclick={undo} disabled={!canUndo} class="btn-icon">
<iconify-icon icon="mdi:undo" />
</button>
<button onclick={redo} disabled={!canRedo} class="btn-icon">
<iconify-icon icon="mdi:redo" />
</button>
<div class="h-8 w-px bg-surface-300" />
<button onclick={cancel} class="variant-ghost btn">Cancel</button>
<button onclick={save} class="variant-filled-primary btn">Save</button>
</div>
</div>
</div>
ποΈ Tool-Specific Control Components
CropControls.svelte
Purpose: Show crop-specific controls (rotation, flip, aspect ratio, shape)
<script lang="ts">
let { cropToolRef } = $props();
</script>
<div class="flex items-center gap-2">
<!-- Aspect Ratio Presets -->
<div class="btn-group">
<button class="btn-sm" onclick={() => cropToolRef.setAspectRatio(null)}>Free</button>
<button class="btn-sm" onclick={() => cropToolRef.setAspectRatio(1)}>1:1</button>
<button class="btn-sm" onclick={() => cropToolRef.setAspectRatio(16 / 9)}>16:9</button>
<button class="btn-sm" onclick={() => cropToolRef.setAspectRatio(4 / 3)}>4:3</button>
</div>
<div class="h-6 w-px bg-surface-300" />
<!-- Shape -->
<div class="btn-group">
<button class="btn-sm" onclick={() => cropToolRef.setCropShape('rectangle')}>
<iconify-icon icon="mdi:rectangle-outline" />
</button>
<button class="btn-sm" onclick={() => cropToolRef.setCropShape('circular')}>
<iconify-icon icon="mdi:circle-outline" />
</button>
</div>
<div class="h-6 w-px bg-surface-300" />
<!-- Rotate & Flip -->
<button class="btn-icon-sm" onclick={() => cropToolRef.rotateLeft()} title="Rotate Left">
<iconify-icon icon="mdi:rotate-left" />
</button>
<button class="btn-icon-sm" onclick={() => cropToolRef.flipHorizontal()} title="Flip Horizontal">
<iconify-icon icon="mdi:flip-horizontal" />
</button>
<div class="h-6 w-px bg-surface-300" />
<!-- Apply -->
<button class="variant-filled-success btn" onclick={() => cropToolRef.apply()}> Apply Crop </button>
</div>
FineTuneControls.svelte
Purpose: Show adjustment sliders (brightness, contrast, saturation, etc.)
<script lang="ts">
let { fineTuneRef, activeAdjustment } = $props();
const adjustments = [
{ key: 'brightness', label: 'Brightness', icon: 'mdi:brightness-6' },
{ key: 'contrast', label: 'Contrast', icon: 'mdi:contrast-box' },
{ key: 'saturation', label: 'Saturation', icon: 'mdi:palette' },
{ key: 'exposure', label: 'Exposure', icon: 'mdi:brightness-7' }
];
</script>
<div class="flex w-full items-center gap-3">
<!-- Adjustment Selector -->
<select bind:value={activeAdjustment} class="select w-40">
{#each adjustments as adj}
<option value={adj.key}>{adj.label}</option>
{/each}
</select>
<!-- Slider -->
<div class="flex-1">
<input
type="range"
min="-100"
max="100"
step="1"
value={fineTuneRef.getValue(activeAdjustment)}
oninput={(e) => fineTuneRef.adjust(activeAdjustment, e.target.value)}
class="range"
/>
</div>
<!-- Reset -->
<button class="btn-sm" onclick={() => fineTuneRef.reset(activeAdjustment)}> Reset </button>
</div>
BlurControls.svelte
<script lang="ts">
let { blurToolRef, blurStrength } = $props();
</script>
<div class="flex w-96 items-center gap-3">
<span class="text-sm">Blur Strength</span>
<input
type="range"
min="0"
max="30"
step="1"
bind:value={blurStrength}
oninput={(e) => blurToolRef.setBlurStrength(e.target.value)}
class="range flex-1"
/>
<span class="w-12 text-sm">{blurStrength}px</span>
</div>
AnnotateControls.svelte
<script lang="ts">
let { annotateToolRef, currentTool, strokeColor, fillColor } = $props();
</script>
<div class="flex items-center gap-2">
<!-- Tool Selection -->
<div class="btn-group">
<button class:active={currentTool === 'text'} onclick={() => annotateToolRef.setTool('text')}>
<iconify-icon icon="mdi:format-text" />
</button>
<button class:active={currentTool === 'arrow'} onclick={() => annotateToolRef.setTool('arrow')}>
<iconify-icon icon="mdi:arrow-top-right" />
</button>
<button class:active={currentTool === 'rectangle'} onclick={() => annotateToolRef.setTool('rectangle')}>
<iconify-icon icon="mdi:rectangle-outline" />
</button>
<button class:active={currentTool === 'circle'} onclick={() => annotateToolRef.setTool('circle')}>
<iconify-icon icon="mdi:circle-outline" />
</button>
</div>
<div class="h-6 w-px bg-surface-300" />
<!-- Color Pickers -->
<input type="color" bind:value={strokeColor} class="h-10 w-10" title="Stroke Color" />
<input type="color" bind:value={fillColor} class="h-10 w-10" title="Fill Color" />
</div>
WatermarkControls.svelte
<script lang="ts">
let { watermarkToolRef, watermarks } = $props();
</script>
<div class="flex items-center gap-3">
<span class="text-sm">Watermark</span>
<select onchange={(e) => watermarkToolRef.selectWatermark(e.target.value)} class="select w-48">
<option value="">None</option>
{#each watermarks as wm}
<option value={wm.id}>{wm.name}</option>
{/each}
</select>
<!-- Position -->
<div class="btn-group">
<button onclick={() => watermarkToolRef.setPosition('top-left')}>TL</button>
<button onclick={() => watermarkToolRef.setPosition('top-right')}>TR</button>
<button onclick={() => watermarkToolRef.setPosition('bottom-left')}>BL</button>
<button onclick={() => watermarkToolRef.setPosition('bottom-right')}>BR</button>
</div>
<!-- Opacity -->
<input type="range" min="0" max="100" oninput={(e) => watermarkToolRef.setOpacity(e.target.value / 100)} class="range w-32" />
</div>
π± Mobile Adaptation
Bottom Toolbar (Mobile)
On mobile, the same EditorToolbar.svelte stacks vertically:
{#if isMobile}
<div class="fixed bottom-0 left-0 right-0 space-y-3 bg-surface-100 p-3">
<!-- Tool Selection (Horizontal Scroll) -->
<div class="flex gap-2 overflow-x-auto pb-2">
<button class:active={activeTool === 'crop'} onclick={() => selectTool('crop')}>
<iconify-icon icon="mdi:crop" />
<span>Crop</span>
</button>
<!-- ... other tools ... -->
</div>
<!-- Tool-Specific Controls -->
<div class="w-full">
{#if activeTool === 'crop'}
<CropControls {cropToolRef} mobile />
{/if}
<!-- ... other tool controls ... -->
</div>
<!-- Actions -->
<div class="flex gap-2">
<button class="btn flex-1">Cancel</button>
<button class="variant-filled-primary btn flex-1">Save</button>
</div>
</div>
{/if}
π― Implementation Steps
Phase 1: Create New Components (Week 1)
- β
Create
EditorToolbar.svelte(single bottom toolbar) - β
Create
toolbars/CropControls.svelte - β
Create
toolbars/FineTuneControls.svelte - β
Create
toolbars/BlurControls.svelte - β
Create
toolbars/AnnotateControls.svelte - β
Create
toolbars/WatermarkControls.svelte
Phase 2: Update Main Editor (Week 1)
- β
Remove all top toolbar logic from
ImageEditor.svelte - β
Replace with single
<EditorToolbar />at bottom - β Update layout to full-screen canvas
- β Test responsive behavior (desktop/mobile)
Phase 3: Delete Old Components (Week 1)
- β
Delete
CropTopToolbar.svelte - β
Delete
CropBottomBar.svelte - β
Delete
FineTuneTopToolbar.svelte - β
Delete
FineTuneBottomBar.svelte - β
Delete
BlurTopToolbar.svelte - β
Delete
WatermarkTopToolbar.svelte - β
Delete
AnnotateTopToolbar.svelte - β
Delete
MobileToolbar.svelte
Phase 4: Polish & Test (Week 2)
- β 100% Tailwind CSS (remove custom classes)
- β Fix all TypeScript errors
- β Add smooth transitions between tools
- β Test all tools thoroughly
- β Accessibility audit (ARIA labels, keyboard nav)
- β Mobile testing
π¨ Visual Design (Pintura-Inspired)
Color Scheme
- Canvas Background:
bg-surface-50 dark:bg-surface-900 - Toolbar:
bg-white dark:bg-surface-800 border-t border-surface-200 dark:border-surface-700 - Active Tool:
bg-primary-500 text-white - Buttons: Tailwindβs button variants
Spacing
- Toolbar Padding:
p-4(desktop),p-3(mobile) - Control Gaps:
gap-2(small),gap-3(medium),gap-4(large) - Canvas: Full-screen with toolbar height offset
Typography
- Tool Labels:
text-sm font-medium - Values:
text-xs text-surface-600
π Before vs After Comparison
Before (Current - Messy)
βββββββββββββββββββββββββββββββββββββββββββ
β [Upload] [Filename] [Undo][Redo][Save]β β Generic Top Toolbar
βββββββββββββββββββββββββββββββββββββββββββ€
β [Rotate][Flip][Aspect][Shape][Done] β β Crop Top Toolbar
βββββββββββββββββββββββββββββββββββββββββββ€
β β
β CANVAS (Cramped) β
β β
βββββββββββββββββββββββββββββββββββββββββββ€
β [Rotation Slider] [Scale Slider] β β Crop Bottom Bar
βββββββββββββββββββββββββββββββββββββββββββ
After (Pintura-Style - Clean)
βββββββββββββββββββββββββββββββββββββββββββ
β β
β β
β CANVAS (Full Screen) β
β β
β [Crop] β β Left Sidebar
β [Adjust] β
β [Filter] β
β β
βββββββββββββββββββββββββββββββββββββββββββ€
β [Aspect][Shape][Rotate][Flip][Apply] β β Single Bottom Toolbar
β [Undo][Redo][Save] β (Context-Aware)
βββββββββββββββββββββββββββββββββββββββββββ
β Success Criteria
- Single Bottom Toolbar - No more top toolbars cluttering the view
- Tool Context Switching - Toolbar adapts to show only relevant controls
- 100% Tailwind - No custom CSS classes
- Zero TypeScript Errors - Proper types for all props
- Mobile-First - Same UX paradigm on mobile
- Accessibility - ARIA labels, keyboard shortcuts
- Performance - Smooth transitions, no lag
π Related Documentation
Status: β³ Planning Complete - Ready for Implementation
Estimated Time: 2 weeks
Priority: High - Critical UX improvement
π§© Engineering Plan (Summary)
- Architecture:
ImageEditor.svelteorchestrates tools on a Konva stage; each tool is an isolated component; state is centralized inimageEditorStore. - Refactor Focus: Move tool-specific state into tools; define a unified tool API (activate, deactivate, apply, reset); reduce duplication between desktop/mobile via CSS-driven layout.
- Type Safety: Add strict types for tool props/events; type Konva objects (
Stage,Layer,Image,Group); avoidanyin callbacks and event payloads. - Responsiveness: Single DOM structure with CSS Grid/Flex; mobile keeps same model with the bottom toolbar.
π₯οΈ Server Processing & API
- Endpoint:
POST /api/media/process - Request shape (operations first):
{
"mediaId": "<id>",
"operations": [
{ "type": "rotate", "angle": 90 },
{ "type": "crop", "x": 100, "y": 50, "width": 800, "height": 600 },
{ "type": "watermark", "watermarkId": "<wm_id>", "position": "bottom-right", "opacity": 0.6 }
]
}
- Focal Point is metadata (not an operation) and is persisted on the media document as
{ focalPoint: { x, y } }via a metadata PATCH. - Server uses
sharpto apply operations in sequence; saves a new variant or overwrites based on configuration.
π Integration Workflow
- MediaUpload: After selecting or uploading, βEditβ opens the editor modal with the image; on Apply, the client posts to
/api/media/process, then updates the widget with the processed asset; focal point is saved via metadata PATCH. - MediaGallery: βEditβ action opens the same modal with the selected image; follows the same flow as above.
π Related Documentation
- API: /docs/api/Media_API.mdx
- Database & Performance: /docs/database/Performance_Architecture.mdx