This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Mason Gallery is a masonry-layout image viewer shipped as three targets from a single monorepo: a Tauri desktop app, a browser SPA, and an npm CLI. All three consume a shared React component library (packages/core).
| Package | Purpose | Key Tech |
|---|---|---|
packages/core |
Shared UI components, stores, types, i18n | React 19, MUI 7, Zustand, Wouter, typesafe-i18n |
packages/desktop |
Native desktop app | Tauri 2, Vite, Tailwind CSS 4 |
packages/web |
Browser SPA | Vite, Tailwind CSS 4, File System Access API |
packages/cli |
npm package that serves the web build locally | Node.js, sirv |
bun install # install dependencies
# Development
bun run dev:desktop # Tauri desktop with hot reload (requires Rust + Tauri prereqs)
bun run dev:web # Vite web dev server
# Production builds
bun run build:desktop # Tauri production build
bun run build:web # Web SPA build
bun run build:cli # Builds web then bundles CLI
# Quality
bun run check # biome ci . && tsc --build
bun run format # biome format --write .Desktop development requires Rust and Tauri v2 prerequisites.
The core pattern is a PlatformService interface (core/src/types/platform.ts) that abstracts file system access, image scanning, settings persistence, and platform capabilities. Each target implements it:
- Desktop:
TauriPlatformService— native file access via Tauri plugins, settings via@tauri-apps/plugin-store - Web:
WebPlatformService— File System Access API, blob URLs, localStorage
Entry points (desktop/src/main.tsx, web/src/main.tsx) create the appropriate service and pass it into the shared Shell component from core.
The backend is layered as commands → services → database + local Axum HTTP server.
Service layer (src-tauri/src/services/):
source_service— unified CRUD oversourcestable (archive + folder, keyed byorigin_path), migration detection via reverse path-segment scoring, identity-segment computationarchive_service— opens archive readers (delegates toarchive.rs), lists entries, extracts entries to disk; returnsExtractResultvariants (cached / freshly extracted / tempfile-for-no-cache)thumbnail_service— resolves(sourceHash, entryHash, width)to an on-disk thumbnail path (lookup only) and generates thumbnails during archive scansimage_service— resolvesarchive:///URIs and bare filesystem paths to bytes, enforcing the per-sourceCachePolicy(extractedno-cache/lru-capped/unlimited, thumbnails retain mode). Guards concurrent extraction of the same entry behindExtractLocks = Arc<DashMap<String, Arc<tokio::sync::Mutex<()>>>>so simultaneous viewer opens trigger exactly one extraction.
SQLite schema (src-tauri/src/database.rs): sources (unified), thumbnails (per width), extracted (per entry with last_accessed for LRU), passwords (encrypted archive unlocks), schema_meta (version marker — on legacy-schema detection, the old cache dir is wiped and recreated).
Local Axum server (src-tauri/src/server.rs): serves two split endpoints:
GET /image— always returns originals.image_handlerdelegates toimage_service::resolve_original.GET /thumb?source=...&entry=...&w=...— thumbnail lookup only (no generation); returns 404 on miss.
Tauri commands (src-tauri/src/commands.rs + archive_commands.rs):
scan_directory,scan_archive— emit batches over events.scan_directorynow inline-expands archives it encounters duringwalkdir: unlocked archives stream their entries (with thumbnails) into the sameimages:batchstream at the archive's sort position; locked archives emit a singlelocked: trueWImage placeholder keyed byarchive:///<path>for click-to-unlock UX.request_thumbnail,cancel_thumbnail— on-demand thumbnail generation for the lazy folder pipeline; results delivered via theimages:thumbnailsevent.clear_thumbnails(sourceId?),clear_extracted(sourceId?)— split cache-clear (replaces oldclear_cache)check_migration,confirm_migration,pin_cache,set_cache_policy,set_source_policy,get_cache_stats,unlock_archive,delete_to_trash, etc.
Lazy folder-thumbnail pipeline (folderThumbnails: "off" | "lazy" setting): When "lazy", grid tiles use an IntersectionObserver + 150ms dwell gate to call request_thumbnail(sourceId, entryPath). A long-running tokio task drains a LIFO queue with a 4-permit semaphore, runs generation on spawn_blocking, and emits images:thumbnails (skipped on cancel). The frontend patches the specific entry in-place via useViewerStore.patchThumbnails, so the positioner never recomputes. Requests below cachePolicy.extracted.minFileSize return skipped: true and are remembered in skippedThumbs to suppress re-requests. Archives (which always get thumbnails at scan time) and loose files with pre-existing thumbnails are gated out of the lazy request path.
PlatformService exposes two URL builders that the UI picks between based on intent:
getImageUrl(source)— full-resolution original; used byImageViewergetThumbUrl(thumbId)— small pre-generated thumbnail;thumbIdis an opaquemg-thumb:///<sourceHash>/<entryHash>?w=<width>URI. Used byWaterfallGridvia a multi-widthsrcSet.
Scan batches include thumbnails?: Thumbnail[] (array of widths) on each image. The grid constructs srcSet/sizes from this array; the viewer ignores it and calls getImageUrl directly. This ensures no thumbnail can ever be served in the viewer, and high-DPI displays pick the appropriate thumbnail width automatically.
Zustand stores in core/src/stores/:
useAppStore— folder selection, UI toggles, archive migration/password prompts, directory tree stateuseSettingsStore— image formats, sort method, language, column breakpoints,cachePolicy,thumbnailSizes, password storage mode (persisted via platform service;setCachePolicyalso syncs to the Rust backend)useViewerStore— current image batch, scan progress, viewer open state, relayout signaling
Hash-based routing via wouter: / (image grid), /about (about page).
typesafe-i18n with English and Traditional Chinese (core/src/i18n/{en,zh}/).
- Formatter/Linter: Biome — 2-space indent, double quotes, semicolons
- TypeScript: Strict mode, ES2022 target, bundler module resolution
- Path alias:
@/maps topackages/core/src/in both Vite configs and tsconfigs
The project uses OpenSpec (openspec/) for spec-driven development. Specs live in openspec/specs/, changes in openspec/changes/. Use the /opsx:propose, /opsx:apply, /opsx:explore, and /opsx:archive slash commands to drive the workflow.