# Justified Rows — Build Pack

Component build specification for the dev team. Paste this into your build prompt, or work from it directly.

| | |
|---|---|
| Component | Photos / Listings / Justified Rows |
| ID | `photos-listings-justified-rows` |
| Cardinality | photo[] (the in-album gallery) |
| Container | 12C, full content width (1376) |
| Pattern | Flickr justified-layout |
| Sibling | Masonry Grid (`photos-listings-masonry`) |
| Version | WDS v3.0 |
| Generated | 2026-07-14 |

Justified Rows presents a set of photos of any size as a uniform wall: rows of a single shared height, each row scaled to fill the full content width. Deterministic, no cropping, flush left and right. This is the default album-listing layout.

## Parts

| # | Part | Data | Notes |
|---|------|------|-------|
| 1 | Row | — | 2 to 4 photos sharing one height; rows stack top to bottom |
| 2 | Photo cell | Web Original | Real aspect ratio, never cropped; width = rowHeight x aspect; links to the Viewer |
| 3 | More affordance | — | Per-photo overflow (`more_horiz`), top-right on hover, white over media |
| 4 | Sponsor watermark | Sponsor logo | Optional per-photo lockup, bottom-right |
| 5 | Gutter | — | Fixed 16px between cells and between rows |

## Algorithm

For each row, given content width `W`, target height `T`, min `MIN`, max `MAX`, gutter `G`, and count bounds 2..4:

1. Flow photos into the row left to right, tracking `Σ aspect` (aspect = width / height).
2. After each add, the width-filling height is `h = (W − G x (n − 1)) / Σ aspect`, where `n` = photos in the row.
3. Close the row when `n >= 2` and `h <= T`, or when `n == 4`.
4. Clamp `h` to `[MIN, MAX]`. Each photo's width = `h x aspect`; gutters are fixed.
5. Repeat to the end. The final partial row is left-aligned at `T`, not stretched.
6. Recompute on container resize.

```
target T   = 300   (to lock)
min height = 220
max height = 380
gutter G   = 16
per row    = 2..4
```

Pull each photo's dimensions from the album feed (or read `naturalWidth/naturalHeight` on load) to get aspect. The only fixed pixel dimension is the gutter; every height is computed, so the wall stays flush at any width.

## Implementation — use the open-source library

Don't reimplement the packing. Use Flickr's library; it is the reference for this pattern.

- Package: `justified-layout` (npm) — `npm install justified-layout`
- Repo / docs: https://github.com/flickr/justified-layout · https://flickr.github.io/justified-layout/
- Licence: open-source (MIT) — confirm in the repo LICENSE before shipping.

Usage: pass an array of aspect ratios (or `{width, height}` objects) plus config; it returns absolute geometry to position each photo.

```js
import justifiedLayout from 'justified-layout';

const geometry = justifiedLayout(
  photos.map(p => p.width / p.height),   // or [{width, height}, ...]
  {
    containerWidth: 1376,                // content width
    containerPadding: 0,                 // the page owns outer padding
    boxSpacing: 16,                      // our gutter (h + v)
    targetRowHeight: 300,                // our target
    targetRowHeightTolerance: 0.27,      // ~ min 220 / max 380 around 300
    showWidows: true,                    // keep the last partial row
  }
);
// geometry.containerHeight, geometry.boxes[i] = { top, left, width, height, aspectRatio }
```

### Config mapping (our rules → library options)

| Our rule | `justified-layout` option |
|----------|---------------------------|
| Content width 1376 | `containerWidth: 1376` |
| Gutter 16 | `boxSpacing: 16` |
| No outer padding | `containerPadding: 0` |
| Target height 300 | `targetRowHeight: 300` |
| Min 220 / max 380 | `targetRowHeightTolerance: ~0.27` |
| Keep last partial row | `showWidows: true` |
| 2 to 4 per row | not a native option |

**On the 2–4 per row rule:** the library has no per-row count cap (only `maxNumRows`). The 2–4 count is an *emergent* property of `targetRowHeight` on our 1376 width — a ~300px row fits roughly two to four landscape photos. If a hard cap is ever required, enforce it as a post-process (or fork), not via config. Treat 2–4 as an expectation, not a setting.

Render: absolutely position each photo from `geometry.boxes` (top / left / width / height) inside a container set to `geometry.containerHeight`. Recompute on resize. The on-page demo here is a lightweight illustration of the same maths; production should use the library.

## Spacing

Gutter 16 (`md`) between cells and rows, constant. Row heights are computed within `[220, 380]` around a 300 target. Defaults taken from the Sketch Album Listing artboard (rows measured 314 / 326 / 416 on the 1376 grid); tune and confirm.

## Typography

None. The listing carries no text; a photo's date, tags, session and event live in the Viewer.

## Notes

- No cropping: preserve each photo's real shape. This is the opposite of the fixed Crop containers used by Album Covers and card media.
- Dark-only for now; add the Light-mode surface once resolved in the master. Both modes required at ship.
- The alternative geometry is the Masonry Grid sibling (column-based). Justified Rows is the default.
