Files
advance-photo-gallery-web-sdk/docs/ARCHITECTURE.md
T
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

131 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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_id``uploadedBy` is display-only.
`defaultFullscreen` seeds `fullscreen` in the store (`setFullscreen` / `toggleFullscreen`; a toolbar
button next to Info drives it). It adds `.apg--fullscreen``position: 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`](../packages/photo-sdk/README.md#embedding-in-a-host-app).
## 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 `flyTo`s 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`](../packages/photo-sdk/README.md#theme-tokens--css-variables).
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`](ROADMAP.md) for status of every capability.