Skip to content

System

Gallery hero strip (All renders)

Locked behaviour for the per-shot still strip — do not regress without reading this.

Gallery hero strip (“All renders”)

Gallery shot detail → Images tab → “All renders” is a static list of thumbnails with app tick boxes (SelectionTickButton). One take is the hero still on disk. This page records the contract so we stop re-breaking it.

User-visible behaviour

RuleDetail
Fixed listTile count and order are fixed for the session on that shot. Picking a new hero does not add tiles or reorder them.
Tick only movesOnly the green tick moves between tiles. Thumbnails do not swap URLs when the hero changes.
Any tick always worksUser can pick A → B → A. Ticks are never disabled because they were selected before.
Tick never jumps backIf saving to disk fails, show an error — do not revert the tick to the previous slot.
PreviewClick the image (not the tick) to preview above; tick sets hero on disk.
No duplicate pixelsHero + byte-identical candidate collapse to one tile (same SHA-256).

Implementation map

PiecePath
UIsrc/components/GalleryStillTakesStrip.tsx
Thumbnail + tickGalleryRenderStripTile, SelectionTickTileCorner in SelectionTickButton.tsx (top-left tick, top-right type badge)
Strip list (dedupe, order)listGalleryStripTakes, dedupeStripTakesByContent, initialHeroStripIndex in src/lib/still-take-strip-order.ts
Thumbnail bytes for savereadGalleryStripSlotBytes, bytesToBase64 in src/lib/gallery-strip-slot-bytes.ts
Hero write APIPOST /api/project-still-lock — prefers imageBase64 from the thumbnail; optional sourceUrl for other callers
Parent shellGalleryInlinePreview in src/components/ShotsGalleryView.tsxkey={shot.id} on strip; do not refetch project-image-index on every hero change

Invariants (do not break)

  1. Snapshot once per shotsnapshottedForShot ref; never rebuild slots from a live index after the user starts ticking.
  2. Never setSelectedIndex(prev) on lock failure — selection is UI truth; disk catch-up is best-effort with an error message.
  3. Never disable the tick when selectedSelectionTickButton must not pass disabled={selected}; busy shows a spinner only.
  4. Do not gate strip mount on stillGroup — mount with key={shot.id} so a brief index null does not remount and resnapshot with extra archive files.
  5. Do not refetch image index on onStillLocked — refresh shot-assets row only (onAssetRefresh); index refetch grows the candidate list and caused “extra image” bugs.
  6. Do not rewrite slot URLs after lock — never set url / sourceUrl to savedUrl (shared hero path); that made every old tile show the current hero image.
  7. Lock uses thumbnail pixelsimageBase64 from readGalleryStripSlotBytes so stale paths still work when the browser shows the frame.
  8. Dedupe by content_sha256 — use listGalleryStripTakes, not raw listSavedTakesByRenderOrder, for the strip snapshot.

Anti-patterns we already hit

  • Rebuilding the strip from project-image-index after each lock (archives add rows → count 6→7→10).
  • Deleting candidate files on promote then locking by frozen path (“Could not read source image”).
  • Optimistic URL swap to /generated/<shot>.jpg on the promoted slot (duplicate-looking hero on every tile).
  • Custom tick disabled when selected (felt “can’t select again”).
  • stillIndexTick + refetch in parent (full strip flash).

Tests

  • src/lib/still-take-strip-order.test.tslistGalleryStripTakes, dedupeStripTakesByContent, initialHeroStripIndex.

Selection tick placement (app-wide)

All checkboxes and tile ticks use AppCheckbox / SelectionTickTileCorner from SelectionTickButton.tsx — selected = solid bg-emerald-600, no border. Tile ticks sit top-left (left-1.5 top-1.5). Type labels (Hero, Take, Clip) use TILE_TYPE_BADGE_CLASS at top-right. Do not use native <input type="checkbox"> or custom bordered/violet tick styles in app UI.