Gallery
The detail-page photo ribbon, strip, panel and lightbox.
The shared photo viewer both detail pages open on, promoted out of them on its third pull (ADR 0024) and recomposed as a ribbon in ADR 0031. It owns the whole overview-and-gallery region as three stacked things plus a dialog.
1. The ribbon
A full-bleed scroller of whole photos, each at--ratio-photo. Height is the constant and width follows the ratio, so nothing is ever cropped at any viewport or photo count — which is the reverse of the usual arrangement, where a frame is fixed and the photograph is fitted into it.
The snap line is the page's content edge, so the photo at rest lines up with everything below it while its neighbours bleed offboth sides. That is the affordance: a photo cut by the edge says "there is more this way", in both directions, which is why there is no dot row and no counter. Prev/next arrows sit on the resting photo, aligned to the same content edge, and disable at the ends rather than hiding — an arrow that vanishes uncovers the photo's own button underneath, which is ADR 0030's hazard in a different costume.
2. The thumbnail strip
Width-capped, and the index to the ribbon: the ribbon says there is more, the strip says how much and lets you jump. It drives the ribbon and follows it. The optional floor plan's thumbnail is pinned outside the strip's carousel — by position, not byposition: sticky, which is inert inside a track that translates instead of scrolling.
3. The panel slot
Caller-provided, and below the photos since ADR 0031 — it was a garden panel beside the stage until the ribbon took the full width. On a listing page it is the identity bar: kind, address, accent, price, status, CTA. The preview fills it with a stand-in.
Motion, and the floor plan
The ribbon, the strip, the lightbox and the lightbox's own rail are each an Embla carousel — one motion mechanism end to end, no scrollers, so following a selection can never move the page. Under them sits a CSS scroll-snap fallback that Embla switches off as it takes over (data-embla), so the gallery still works with no JavaScript. All four are tracked and destroyed on astro:before-swap and re-init on astro:page-load, idempotent per root (ADR 0017).
Each photo is a button, and which button it is depends on where it is: a photo that is not at rest brings itself to rest, and only the one already in position opens the lightbox. The floor plan becomes the contained, labelled last frame. The listing page passes one; the property page passes none.
One composite, one tab stop (ADR 0024): tab into the ribbon and page it with ←/→. Re-tone the preview — the viewer reads only semantic tokens, so the strip and panel follow.
Props
| Prop | Type | Required |
|---|---|---|
| slides | GallerySlide[] | yes |
| floorPlan Optional floor plan — the labelled, contained last slide + sticky thumbnail. | { src: string; alt: string; full?: string; label: string } | — |
| label aria-label for the thumbnail list and the lightbox. | string | yes |
| prevLabel | string | yes |
| nextLabel | string | yes |
| zoomLabel | string | yes |
| closeLabel | string | yes |
| stageTransitionName view-transition name for the lead stage image (the listings→detail morph target, ADR 0017). Omitted where there's no morph (the property page), and applied as an inline style rather than with `transition:name` so that "omitted" means unnamed — see Overview's note on the directive's generated names, which would otherwise make every slide a shared element. | string | — |
| Field | Type | Required |
|---|---|---|
| src | string | yes |
| alt | string | yes |
| full Higher-resolution source for the zoom/lightbox view; falls back to `src`. | string | — |