Files
KaushikRK99 7673fc63ea feat: enhance styling and layout for toolbar, map components, and editor
- Updated toolbar styles for improved layout and appearance, including padding and margin adjustments.
- Introduced new styles for scale menu and compact dropdowns.
- Enhanced map pin tooltips with better positioning and hover effects.
- Added new styles for map sheet filters, including search and date range controls.
- Improved editor styles for a more cohesive look and feel, including transport controls for video editing.
- Expanded GeoLocation interface to include additional address fields and formatted address.
- Updated MediaItem interface to include storage reference and uploadedBy fields.
- Enhanced MediaComment interface with author details for better user identification.
- Introduced GalleryUser interface to represent signed-in users in the host app.
2026-07-23 07:34:20 +05:30

10 KiB
Raw Permalink Blame History

Architecture

Monorepo

packages/photo-sdk/   # the product (React + TS, self-contained CSS, no ML deps)
  src/
    components/       # PhotoGallery, AppShell, Sidebar, TopToolbar, views/, editor/, Lightbox,
                      # MediaGrid, PhotoTile, ContextMenu, Modal, SelectionBar, AIAnalyzer, AnnotationLayer
    store/            # Zustand store (createGalleryStore) + selectors + React context
    adapters/         # StorageAdapter interface + localStorage default (Supabase planned)
    ai/               # AIProvider interface (detect/faces/ocr/caption/embed/generativeEdit) + helpers
    lib/              # classify, smartAlbums, grouping, edits, media (import/EXIF), format, download
    icons/            # original SF-style SVG set
    styles/sdk.css    # design tokens (light/dark/semi-dark) + components, prefixed .apg-
apps/web/             # Next.js demo
  src/app/            # landing, /gallery, /api/ai/edit (Gemini proxy), layout, middleware (nonce CSP)
  src/lib/ai/         # tensorflowProvider (coco-ssd), createDemoAIProvider (+ Gemini generativeEdit)
  src/lib/seed.ts     # demo dataset (Picsum images + local video + synthetic metadata)

Data flow

<PhotoGallery> → creates a per-instance Zustand store → GalleryProvider (context) → AppShell → Sidebar + Toolbar + ViewRouter. The store holds media/albums/people/selection/view/theme/AI status. A pluggable StorageAdapter loads/saves state (debounced). Inputs are normalized + sanitized through normalizeMediaItem (URL-scheme allow-list, string coercion) on both the photos prop and adapter load.

Adapters (storage) — all optional

StorageAdapter = { name, load(), save(state), putBlob?, clear?, applyChanges?, putMedia? }.

  • Default: createLocalStorageAdapter() (metadata in localStorage, blobs in IndexedDB).
  • applyChanges?(changes: StateChanges)incremental persistence. When present the store calls it instead of save(), sending only entities whose object reference changed since the last successful persist (upsert* / remove* per collection, plus labelAliases / deletedLabels on a shallow diff). The store keeps per-instance id→reference snapshots seeded from load(), serializes overlapping writes, and rolls the snapshot back on rejection so the next persist retries the same diff.
  • putMedia?(id, blob, { name, mime }): Promise<{ ref, url }>durable bytes, preferred over putBlob. url goes to MediaItem.src (may be a short-lived signed URL); ref goes to MediaItem.storageRef and is what survives a reload.
  • Planned: Supabase (Postgres + Storage), S3/R2/GCS/Azure/MinIO, REST/GraphQL. Swap via the adapter prop.

Embedding in a host app

The gallery is standalone-by-default but fully suppressible. GalleryConfig carries embedded, chrome { titlebar, sidebar, toolbar, themeSwitcher } (DEFAULT_CHROME), keyboardShortcuts, currentUser: GalleryUser and shareBaseUrl; <PhotoGallery> exposes each as a prop and pushes the recomputed config back into the store via setConfig on every change, so theme / tokens / identity are live rather than mount-time-only. AppShell skips <Sidebar> / <TopToolbar> per chrome, and useKeyboardShortcuts(enabled) takes a flag (never a conditional hook call). embedded adds .apg--embedded (height: 100%, cursor: pointer on controls); overlays stay position: fixed, and the Info panel honours --apg-overlay-top. hiddenViews: ViewId[] (default []) drops sidebar rows and Collections cards (e.g. screenshots, sys:documents); a section label is omitted when all its rows are hidden, and AppShell falls back to library if the active view becomes hidden. With currentUser set, comments carry { authorId, author, authorAvatar } and only the author may delete — a client mirror of the server rule. The same identity is stamped as display metadata on MediaItem.uploadedBy (imports + camera capture, never overwriting an existing value) and on each MediaVersion (addVersion / restoreVersion{ authorId, author, authorAvatar }); the Info panel surfaces both. be-crm still owns owner_principal_iduploadedBy is display-only. defaultFullscreen seeds fullscreen in the store (setFullscreen / toggleFullscreen; a toolbar button next to Info drives it). It adds .apg--fullscreenposition: fixed; inset: 0; z-index: 1400; height: 100dvh — so the gallery fills the viewport whatever height the host gave its container. The overlays needed no change: position: fixed; inset: 0 resolves against the viewport, which full screen now coincides with, and z-index: 1400 opens a stacking context their 10001300 layers live inside. Escape un-maximises, but only after the lightbox/editor/camera/modal/context-menu check and after clearing an object focus or selection. lockProvider makes the Recently Deleted lock server-backed and per-user: status / set(password | null) / verify. When present the store never reads or writes the apg:lock-hash localStorage entry — lockConfigured (seeded from status() in init()) is the UI's source of truth, and lockError separates 'wrong-password' (verify resolved false) from 'unavailable' (verify rejected). Without a provider the localStorage path is byte-for-byte unchanged. Full prop/token tables: packages/photo-sdk/README.md.

Map view

Leaflet is dynamically imported (never at module scope, so SSR is safe) and driven through refs. clusterByLocation groups located media by coordinate precision that rises with zoom, so pins split apart as you go in. Clicking a pin flyTos it — max(currentZoom, 14) for a lone photo, max(…, 12) for a cluster, never zooming out — then pans up by half the sheet height so the pin clears the sheet that is about to cover the bottom half. Multi-photo pins carry a total-count badge and a hover mini-slider: a strip of up to 5 thumbnails plus a +N chip that auto-advances the preview every 900 ms while hovered. The single-photo hover tooltip (.apg-pin__tip) is user-select:none and edge-clamped — MapView measures it and sets --tip-dx to slide it back inside the map, adding .apg-pin__tip--below to flip it under the pin near the top edge — so it can't overflow or get clipped. The strip is built with plain DOM (matching the marker code, and keeping place names / filenames on textContent, never innerHTML); every interval is registered in a set that marker teardown, mode changes and unmount all drain, because a leaked one would mutate detached DOM forever. The location sheet has its own filter bar: a search field reusing searchMedia (so "objects" mean the same thing as in the global search), a single date-range control (.apg-daterange — a button that opens a popover of presets All dates / Last 7 days / Last 30 days / This year plus a custom From/To pair, all driving one takenAt from/to state), and a row of object-label chips built from the cluster — all AND-combined, with a live "N of M photos" count, one clear button that resets everything, and an empty state when nothing survives.

Toolbar & Info panel

The top toolbar floats as a rounded glass bar (margins all round, --apg-radius-lg, --apg-shadow-md), matching the sidebar, in embedded and full-screen alike. It stays in normal flow inside .apg-main, so .apg-viewport begins below it and scroll behaviour is unchanged — no overlap, no manual top-padding. The library-scale segmented is now a compact .apg-scalemenu dropdown that reuses the .apg-menu popover (menu role, checkmarks, arrow-key nav), which also removes the overlap it used to have with the zoom-out button. The Info panel adds: a real inline <video> preview (.apg-info__thumb--video) at the top for video items (not a frozen poster <img>); an "Uploaded by" identity block and per-version author; comments rendered as cards (.apg-comment) with a divider before the compose form; click-to-edit caption and multi-line note (saved on blur/Enter via updateMedia, and note is in the search haystack); and full-address reverse geocoding. On opening a located item whose location.formatted is unset, it fetches OpenStreetMap Nominatim once (addressdetails=1), parses road/…/countryCode/formatted into GeoLocation, and persists via updateMedia (survives reload, never re-fetched — an in-flight/failed id guard prevents loops and double-fetches). A hardened host CSP must allow nominatim.openstreetmap.org in connect-src.

AI providers — all optional, free unless noted

AIProvider methods (each optional): detectObjects, detectFaces, caption, ocr, embedImage, embedText, generativeEdit. The SDK ships no ML deps; providers are injected by the host:

  • tensorflowProvider (COCO-SSD) — free, in-browser object detection.
  • createDemoAIProvider — combines detection + Gemini generativeEdit (proxied through /api/ai/edit).
  • Planned: face-api.js (faces), tesseract.js (OCR), transformers.js / Hugging Face (semantic search, captions). AIAnalyzer (headless) runs detection across the library and writes objects/objectLabels back.

Theming

CSS variables on .apg[data-theme]. Themes: light, dark, semi-dark (glass sidebar + light content). Customizable via props → CSS vars: --apg-accent, --apg-radius*, plus the full ThemeTokens map (backgrounds, cards, toolbar/menu glass, separators, hover/active, text tiers, glass border, font, shadows, tile-favourite, overlay/editor backgrounds, segmented pill, sidebar width, toolbar height) — xLight/xDark for theme-paired values, bare names for theme-independent ones. Applied as inline CSS vars on the root, filtered through a url()/expression()/javascript:/<>{} guard. Token → variable table: packages/photo-sdk/README.md. All feature toggles via features.

Security

Per-request nonce CSP in apps/web/src/middleware.ts (script-src 'self' 'nonce-…' 'strict-dynamic', no unsafe-inline), plus X-Frame-Options/nosniff/Referrer/Permissions-Policy. API keys are server-side only (read in route handlers; never NEXT_PUBLIC_). Imports are type/scheme validated.

See ROADMAP.md for status of every capability.