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.
This commit is contained in:
+83
-2
@@ -25,10 +25,85 @@ A pluggable `StorageAdapter` loads/saves state (debounced). Inputs are normalize
|
||||
`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?(id, blob), clear?() }`.
|
||||
`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 1000–1300 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:
|
||||
@@ -39,7 +114,13 @@ A pluggable `StorageAdapter` loads/saves state (debounced). Inputs are normalize
|
||||
|
||||
## 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*`. All feature toggles via `features`.
|
||||
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'`,
|
||||
|
||||
+40
-2
@@ -25,7 +25,7 @@ designed to be **optional via flags** (`features={{ … }}`) — a consuming pro
|
||||
|
||||
## 2. Library, viewing & navigation
|
||||
- ✅ Library views: Years / Months / All Photos (Days grouping supported) — **date/time grouping**
|
||||
- ✅ Collections (Memories, Pinned, Albums, People, Featured, Shared, Recent Days, Trips, Utilities, Objects)
|
||||
- ✅ Collections (Albums, Objects, Shared Albums, Recent Days) — trimmed of the placeholder-only sections (Memories / Pinned / Featured Photos / People & Pets / Trips / Utilities) for host-app embedding
|
||||
- ✅ Adaptive responsive grid + zoom in/out, lazy images
|
||||
- ✅ **Lightbox** photo viewer (prev/next, keyboard, actions) — 🚧 polish to 100% macOS parity
|
||||
- ✅ **Video player** (native controls in lightbox) — 🚧 macOS-style custom controls/scrubber
|
||||
@@ -58,6 +58,15 @@ designed to be **optional via flags** (`features={{ … }}`) — a consuming pro
|
||||
location's photos in a **date-grouped sheet**; any GPS item (incl. screenshots) pins automatically
|
||||
- ✅ **Grid tab = Memories mosaic**: date sections, full-width **auto-sliding hero** + irregular varied tiles
|
||||
- ✅ **Mini-map** in the photo info panel
|
||||
- ✅ **Fly-to on pin click** (`flyTo`, never zooms out) + per-location **count badge** + **hover mini-slider** strip;
|
||||
the single-photo hover tooltip is edge-clamped (`--tip-dx` / `.apg-pin__tip--below`) and `user-select:none`
|
||||
- ✅ **Location-sheet filter**: search (shared `searchMedia`) + object-label chips + a single **date-range** control
|
||||
(`.apg-daterange` — presets *All dates / Last 7 days / Last 30 days / This year* + custom From/To), all AND-combined,
|
||||
with a live "N of M" count and one Clear-all
|
||||
- ✅ **Full-address reverse geocoding** — on opening a located item the Info panel reverse-geocodes once via OpenStreetMap
|
||||
Nominatim (`addressdetails=1`), parsing road/neighbourhood/suburb/city/county/state/postcode/country/countryCode/formatted
|
||||
into `GeoLocation` and persisting it via `updateMedia` (survives reload, never re-fetched). Host CSP must allow
|
||||
`nominatim.openstreetmap.org` in `connect-src`
|
||||
- Note: demo seed intentionally locates ~half the photos; real photos populate via EXIF GPS (§4)
|
||||
|
||||
## 6. Editor (photo & video)
|
||||
@@ -92,7 +101,8 @@ designed to be **optional via flags** (`features={{ … }}`) — a consuming pro
|
||||
> From a full feature audit vs. macOS Photos. HIGH = most-requested / highest value.
|
||||
- ✅ **Custom video player** (glass scrubber, buffered/played, volume, PiP, fullscreen, keyboard)
|
||||
- ⬜ **HIGH — Lightbox zoom/pan** (scroll / double-click / pinch) for detail viewing
|
||||
- ⬜ **HIGH — Editable metadata** in the Info panel (caption, keywords/tags, title; bulk edit)
|
||||
- ✅ **Editable metadata** in the Info panel — click‑to‑edit **caption** + multi‑line **`note`** (save on blur / Enter via
|
||||
`updateMedia`; `note` is folded into the search haystack). ⬜ still: keywords/tags editing, title, bulk edit
|
||||
- ⬜ **HIGH — Slideshow / Memories playback** (auto-advance + Ken Burns transitions)
|
||||
- ⬜ MED — Lightbox **filmstrip** of adjacent media; **timeline scrubber** to jump by date
|
||||
- ⬜ MED — **Keyboard-shortcut help** modal (`?`); album **drag-to-reorder**
|
||||
@@ -127,6 +137,34 @@ designed to be **optional via flags** (`features={{ … }}`) — a consuming pro
|
||||
- ✅ Docs: README + `docs/SETUP.md` + `docs/ARCHITECTURE.md` + this roadmap + project-local `CLAUDE.md`
|
||||
- ✅ Testing: Playwright e2e (manual runs); ⬜ automated CI suite
|
||||
|
||||
## 11. Embedding into a host app (CRM / host integration)
|
||||
> The SDK is no longer just the macOS-Photos demo — it embeds into a host product's shell. All of it is opt-in.
|
||||
- ✅ **Embedded layout** (`embedded`) — fills the host container (`height:100%`, `.apg--embedded`, `cursor:pointer`)
|
||||
instead of assuming a full viewport; overlays stay `position:fixed` and honour `--apg-overlay-top`
|
||||
- ✅ **Chrome suppression** (`chrome { titlebar, sidebar, toolbar, themeSwitcher }`, `DEFAULT_CHROME` exported) — hide the
|
||||
pieces the host already provides; `showWindowChrome` kept as a legacy alias of `chrome.titlebar`
|
||||
- ✅ **`hiddenViews: ViewId[]`** — drop sidebar rows + Collections cards (labels drop when empty; active hidden view falls back to `library`)
|
||||
- ✅ **`keyboardShortcuts`** flag — bind (or not) global shortcuts to `window`
|
||||
- ✅ **Host identity** (`currentUser: GalleryUser`) — identity-stamped **comments** (`authorId/author/authorAvatar`;
|
||||
only the author sees delete), **uploads** (`MediaItem.uploadedBy`, never overwritten on re-import), and **versions**
|
||||
(`addVersion`/`restoreVersion` stamp the author). Info panel shows an "Uploaded by" block + per-version author.
|
||||
`be-crm` still owns `owner_principal_id`; `uploadedBy` is display identity only
|
||||
- ✅ **`shareBaseUrl`** — host owns where share links point
|
||||
- ✅ **Full ThemeTokens map (50+ keys)** → CSS variables (backgrounds, cards, toolbar/menu glass, separators, hover/active,
|
||||
text tiers, glass border, font, shadows, tile-fav, overlay/editor bg, segmented pill, sidebar/toolbar sizing);
|
||||
`xLight/xDark` theme-paired or bare; sanitized against `url()/expression()/javascript:/<>{}`. **Live config** — theme /
|
||||
tokens / identity / chrome re-apply via `setConfig` without remounting
|
||||
- ✅ **Full-screen / maximise** (`defaultFullscreen`, store `fullscreen`/`setFullscreen`/`toggleFullscreen`, toolbar button) —
|
||||
adds `.apg--fullscreen` (`position:fixed; inset:0; z-index:1400; height:100dvh`); Escape un-maximises after the
|
||||
lightbox/editor/camera/modal/menu checks
|
||||
- ✅ **Server-backed Recently-Deleted lock** (`lockProvider: { status, set, verify }`) — replaces the device-local
|
||||
`apg:lock-hash` entirely; `lockConfigured` is the UI source of truth, `lockError` separates `'wrong-password'` from `'unavailable'`
|
||||
- ✅ **Incremental persistence** — `StorageAdapter.applyChanges(StateChanges)` (diff-based, called instead of `save()`;
|
||||
serialized writes, rollback-on-reject) + `putMedia(id, blob, meta) → { ref, url }` (durable bytes, sets
|
||||
`MediaItem.src`/`storageRef`); types `StateChanges` / `StoredBlob` exported
|
||||
- ✅ **Floating glass toolbar** + library-scale **dropdown** (`.apg-scalemenu`, replaces the Years/Months/All segmented)
|
||||
- ⬜ Real multi-user auth / permissions in the host (identity is display-only in the SDK; the backend is the authority)
|
||||
|
||||
## Web-meaningful UI (done 2026-06-29)
|
||||
- ✅ "All Projects / App Store" (iOS-only) → **"All Albums"** overview (My Albums + Create + Recent Days).
|
||||
- ✅ Sidebar Albums section: All Albums + New Album + user albums (indented).
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ Copy `.env.example` → `apps/web/.env.local` (gitignored — never commit). Key
|
||||
GEMINI_API_KEY=your-key
|
||||
GEMINI_IMAGE_MODEL=gemini-2.5-flash-image
|
||||
|
||||
# Supabase backend (optional, planned adapter):
|
||||
# Supabase backend (optional; adapter included — run docs/supabase-setup.sql once to activate):
|
||||
# NEXT_PUBLIC_SUPABASE_URL=...
|
||||
# SUPABASE_SERVICE_ROLE_KEY=... # server-side only
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user