Add complete i18n infrastructure with 21 supported languages:
English, Simplified Chinese, Traditional Chinese, Japanese, Korean,
Spanish, French, Italian, Brazilian Portuguese, German, Dutch, Swedish,
Russian, Polish, Ukrainian, Arabic (RTL), Turkish, Hindi, Vietnamese,
Indonesian, and Thai.
- I18nProvider context with three-tier locale detection
(user preference > navigator.languages > instance default > English)
- ~1500 translation keys per locale with TypeScript-enforced completeness
- Dynamic code-splitting: only the active locale is loaded at runtime
- Language selectors in footer, login page, settings, and mobile sidebar
- Arabic RTL support with CSS logical properties across all components
- Tool names, descriptions, and categories translated via i18n helpers
- Public API endpoint GET /api/v1/config/locale for instance default
- Multi-script font stack (CJK, Arabic, Devanagari, Thai, Cyrillic)
- format() and plural() helpers for interpolation and pluralization
- API error translation mapping (translateApiError)
- 36 Playwright e2e tests verifying all 21 locales load correctly
- 25 unit tests for format, plural, locale detection, and completeness
- Updated translations.md docs and CLAUDE.md conventions
Background images, device frames, custom shadows, and watermark text
were not rendering in the right-pane preview. The preview now updates
in real time for all settings: gradient/solid/image backgrounds, macOS/
Windows/Browser frame chrome, iPhone/MacBook/iPad frame indicators,
custom shadow parameters, and watermark text overlay.
Also fixes a React StrictMode effect-ordering race where the parent
tool-page reset cleared preview state set by the child Settings
component on initial mount.
The preview endpoint now returns X-Original-Width/Height headers with
dimensions read from Sharp metadata (or ExifTool for RAW files). The
frontend stores these in FileEntry and the ImageViewer prefers them over
the browser's naturalWidth/naturalHeight, which reflects the resized
preview rather than the original sensor dimensions.
Pass onUrlImport handler through AppLayout to Dropzone so URL-imported
files are loaded on the home page. Add stopPropagation on the modal
overlay to prevent clicks from reaching elements underneath.
The thumbnail strip expanded beyond the right pane because the flex
section lacked min-w-0, causing a page-level horizontal scrollbar.
Added min-w-0 to both mobile and desktop image area sections.
For non-previewable formats (DDS, TIFF, EXR, PSD, RAW, etc.), show a
file info card with format and size instead of passing the blob URL to
an img tag that fails and shows an infinite "Loading preview..." spinner.
Also changed the ImageViewer error state from a misleading spinner to a
static "Preview not available" message.
AVIF was missing from expandAccept() and every inline accept attribute,
causing the OS file picker to gray out .avif files. Drag-and-drop was
unaffected because it bypasses the accept filter. Added .avif to
dropzone.tsx expandAccept (covers all Dropzone pickers) plus 12 inline
accept strings across tool settings, pages, and file-upload-area.
- Remove duplicate "Files" heading (section header + inline count)
- Space "Show more" and "Clear all" apart on the same row
- Replace bare number input with number + KB/MB dropdown for target
size, defaulting to KB; MB values are converted to KB for the API
- Hide native number input spin buttons for a cleaner look
Replace the single-file pill with a full file list showing name
(truncated), format badge, and size for each file. First 5 files
are visible by default with a "show more" toggle for the rest.
Clicking a file selects it in the viewer. Applies to all tools.
- 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
Add import button to saved pipelines section with file picker,
validation, and error display with auto-dismiss. Section now
always renders with empty state text when no pipelines exist.
Recovers the editor menu bar (File/Edit/Image/Layer/Select/Filter/View)
from orphaned commit b07ecd5 that was lost during a rebase. Integrates
it into the current editor-page layout with PSD/TGA/EXR/HDR file open
support and new document dialog.
Canvas rendering:
- Fix Konva filter application order (filters before cache)
- Implement 6 missing filters (motionBlur, radialBlur, surfaceBlur, vignette, grain, sharpen)
- Implement exposure, vibrance, warmth adjustments as custom Konva filters
- Apply layer blend modes via globalCompositeOperation
- Apply object effects (drop shadow, outer glow, stroke) to all shapes
- Mount SmartGuidesOverlay during move tool drag
- Clip pixel grid to visible viewport (200-line cap for performance)
Store logic:
- resizeImage now scales all objects proportionally (points, radii, fontSize)
- rotate/flip/trim handle line/arrow points arrays and center-based objects
- applyCrop creates cropped source image via offscreen canvas
- invertSelection creates mask from bounds when no mask exists
- cutObjects uses single atomic set() to prevent race conditions
- sendToBack respects layer ordering in multi-layer documents
- Add batchNudge() and commitHistory() for undoable nudge operations
- Add updateLayerThumbnail() method
Tool hooks:
- Fix clone stamp/dodge/burn perf (toDataURL only on mouseUp, not every move)
- Fix magic wand zoom/pixelRatio with explicit stage.toCanvas() viewport
- Fix eyedropper sampling with unzoomed canvas export
- Fix selection tool stale closure via isDrawingRef
- Implement polygonal lasso (click-to-place vertices, double-click to close)
- Implement selection subtract mode (geometric and mask-based)
- Implement gradient live preview during drag
- Fix transform/move tool to persist changes and handle ellipse/polygon/star
UI wiring:
- Mount rulers and guidelines in editor page
- Wire histogram with live canvas imageData
- Wire autosave recovery with blob-to-dataURL conversion
- Wire fill dialog to Shift+Backspace shortcut
- Wire eyedropper and transform options to options bar
- Fix history panel undo/redo button reactive state via useSyncExternalStore
- Fix layer row name click to select layer (timer-based click/dblclick)
- Fix zoom animation coordinate drift with progressive store sync
- Fix copy merged to use Konva stage composite export
Tests:
- 49 new unit tests (store fixes + konva filters)
- 8 new E2E test files with 39 test cases
The default-view redirect in HomePage fired on every mount, not just the
initial page load. A module-level flag now gates the redirect so it only
applies once per session, allowing users to switch to sidebar view when
grid is the default.
Closes#128
The install prompt and error messages were showing the underlying AI
bundle name (e.g. "Background Removal") instead of the actual tool name
(e.g. "Passport Photo"). Now the UI shows the correct tool name and
description while still installing the correct bundle behind the scenes.
Closes#130
CRITICAL fixes:
- #14: Render source image on canvas via Konva Image + use-image hook
- #3: Wire move tool handlers (onClick, onDragEnd, onTransformEnd, draggable)
to all CanvasObjectRenderer shapes
- #4: Implement image object rendering for fill/gradient output
- #2: Show fallback text in histogram panel when no imageData provided
- #1: Forward all args in zundo handleSet debounce wrapper
HIGH fixes:
- #5: Track raw screen cursor position for brush overlay instead of
using canvas-space coordinates
- #6: Export dialog uses Konva stage.toDataURL via module-level ref
instead of DOM querySelector for correct export at any zoom/pan
- #7: Add _historyVersion increment to setAdjustment, resetAdjustments,
toggleFilter, and setFilterParam for undo tracking
- #8: Include lastAction in partialize so history labels display correctly
MEDIUM fixes:
- #10: Move useEditorShortcuts from EditorCanvas to EditorPage with
save/export callbacks
- #11: Remove _historyVersion increment from updateObject to prevent
brush strokes from flooding undo history
- #12: Apply Konva filters (Brighten, Contrast, HSL, Blur, Grayscale,
Sepia, Invert, Pixelate, Emboss, Posterize, Noise, Solarize,
Threshold, Kaleidoscope) to source image node based on store state
- 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
Create the four-zone editor layout shell (toolbar, options bar, status
bar, right panel), welcome screen with drag-and-drop and new document
dialog, and the main editor page with clipboard paste and URL loading.
Main page tool cards and home page tool lists only subscribed to
server-side bundle state, which only reflects the actively downloading
feature. Queued features appeared as plain download icons instead of
showing their queued/installing status. Now subscribes to client-side
installing and queued state from the features store, matching the
settings page behavior.
Code fixes:
- Sidebar state bleed: reset file store on HomePage mount
- restore-photo: raise error instead of silently skipping colorize
when DDColor model missing
- PaddleOCR OOM: cap input images to 2048px before OCR inference
- Torch CPU optimization: use --index-url .../whl/cpu on CPU nodes
Test fixes:
- upscale: add exact:true to scale factor button locators
- smart-crop: add exact:true to "Pad to square" locator
- colorize: use regex for model button names (Best/Balanced/Fast)
- enhance-faces: use .first() for ambiguous percentage display
- passport-photo: fix DPI locator, .or() compound, generate fallback
- people: update maxUsers assertions for unlimited (0) default
- automate: "Save Pipeline" → "Save" matching actual button text
- tools.test: add resize to Sharp mock chain for OCR tests
- Add Cloudflare Pages deployment for landing page (snapotter.com) and
docs (docs.snapotter.com)
- Create deploy-landing.yml and update deploy-docs.yml workflows
- Update CI to ignore apps/landing/** paths
- Fix logo transparency (remove white background) across all apps
- Recreate social-preview.png with SnapOtter branding
- Update all docs URLs from GitHub Pages to docs.snapotter.com
- Update VitePress config: light theme default, fix llms.txt paths
- Add .vitepress/cache/ and .env.* to gitignore
- Isolate test web server on port 2349 so E2E tests run alongside Docker
- Use fresh SQLite database per test run via DB_PATH, fixing missing
roles/audit_log tables from stale migration state
- Fix analytics consent redirect loop when ANALYTICS_ENABLED=false by
auto-declining consent instead of bare redirect
- Dismiss analytics consent via API in auth setup and RBAC test user
creation so the consent page never blocks UI tests
- Make Vite port and proxy target configurable via env vars
* feat: allow multi-file selection for automation pipeline
Add two ways to import server-stored files into the pipeline:
1. Files page: "Pipeline" bulk action button and "Open in Pipeline"
button in file details panel — navigates to /automate with selected
file IDs via React Router state.
2. Automate page: "Import from Library" button opens a modal with
thumbnails, search, and multi-select checkboxes to pick files from
the user's server-stored library.
Both paths download the selected files and load them into the existing
useFileStore, reusing the batch pipeline processing infrastructure.
Closes#35
* fix: resolve 8 pre-existing test failures across unit and integration suites
- file-validation.ts: Return valid:false when Sharp fails to read
metadata for standard formats (PNG, JPEG, BMP) instead of silently
accepting corrupt buffers. CLI-decoded formats already skip Sharp.
- pipeline.ts: Enforce hard cap of 20 steps via .max() instead of
relying on MAX_PIPELINE_STEPS env var (default 0 = unlimited).
Tighten name limit to 100 chars and description to 500 chars to
match test expectations.
- env.ts: Change MAX_LOGO_SIZE_KB default from 2048 to 500 to match
the branding upload size limit the tests verify.
The <select> for Default Tool View was an uncontrolled dead control with
no value binding, no onChange handler, and no save mechanism. This wires
it up end-to-end:
- Add defaultToolView to the Zustand settings store
- Load the persisted value from the settings API on mount
- Bind the <select> with value/onChange
- Add Save Settings button mirroring SystemSection's pattern
- Redirect home page to /fullscreen when defaultToolView is "fullscreen"
Closes#75
Extends the platform to handle 7 new image format families alongside
the existing AVIF support gap-fill. Uses the established HEIC decoder
pattern (CLI decode → PNG → Sharp) for formats Sharp can't handle
natively: Camera RAW via dcraw_emu/LibRaw, PSD/TGA/EXR/HDR via
ImageMagick. JXL and ICO are Sharp-native. Adds server-side preview
for non-browser-displayable formats and JXL as a new convert output
target. All 27 validateImageBuffer callers updated with filename for
extension-based format detection.
Restructure the pipeline builder UX from a cramped sidebar to a
spacious two-pane layout:
- Left pane: categorized, searchable Tool Palette (click-to-add)
- Right pane: Pipeline Canvas with sortable steps, inline action bar,
and collapsible preview panel with before/after slider
- Mobile: full-screen canvas with FAB + bottom sheet for tool selection,
image preview, and thumbnail strip (previously hidden entirely)
- Fix nested <button> HTML violation in SortableStep
- Fix stray semicolon rendering in step cards