Widths{fmtEighths(state.results.widths.min)} – {fmtEighths(state.results.widths.max)} (avg {fmtEighths(state.results.widths.avg)})
Sqft{state.results.sqft.toFixed(2)}
@@ -109,12 +142,42 @@
{#if state.results.sourceBelowFloor}
Source is below the 800px floor for the inventory PNG. Retake a larger photo.
{/if}
- {#if pngUrl && !state.results.sourceBelowFloor}
+
+
+ {#each lineupItems as item, i (i)}
+ {#if item.kind === 'png'}
+ -
+
+
+ {#if item.role === 'calibration'}
+ Clean calibration
+ {:else if item.role === 'overlay'}
+ Dimension overlay
+ {:else}
+ Batch photo {(item.batchIndex ?? 0) + 1}
+ {/if}
+
+
+ {:else}
+ -
+ Photo {item.batchIndex + 1} failed
+ {item.message}
+
+ {/if}
+ {/each}
+
+
+
+ {#if lineupItems.length === 0 && pngUrl && !state.results.sourceBelowFloor}
{/if}
+
+
+
+
{/if}
@@ -136,11 +199,29 @@
}
.primary:disabled { background: #333; color: #777; }
.ghost { flex: 1; padding: 14px; border-radius: 10px; border: 1px solid #444; background: transparent; color: #ccc; }
- .results { margin-top: 20px; }
+ .results { margin-top: 8px; }
.nums { display: flex; flex-direction: column; gap: 8px; }
.nums div { display: flex; justify-content: space-between; border-bottom: 1px solid #2a2a2a; padding-bottom: 6px; }
.nums span:first-child { color: #aaa; }
.warn { color: #ffb4a2; margin-top: 12px; font-size: 0.9rem; }
+ .lineup {
+ list-style: none; padding: 0; margin: 16px 0 0;
+ display: flex; flex-direction: column; gap: 14px;
+ }
+ .lineup-item.png {
+ text-align: center;
+ background-image: linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%);
+ background-size: 16px 16px; background-position: 0 0,0 8px,8px -8px,-8px 0;
+ background-color: #eee; border-radius: 8px; padding: 8px;
+ }
+ .lineup-item.png img { max-width: 100%; border-radius: 4px; display: block; margin: 0 auto; }
+ .role { display: block; margin-top: 8px; font-size: 0.85rem; color: #222; font-weight: 600; }
+ .lineup-item.error {
+ border: 1px solid #8a3a2a; background: #2a1512; border-radius: 8px;
+ padding: 12px 14px; display: flex; flex-direction: column; gap: 4px;
+ }
+ .err-title { color: #ffb4a2; font-weight: 700; font-size: 0.95rem; }
+ .err-msg { color: #e8cfc8; font-size: 0.9rem; }
.png-wrap { margin-top: 16px; text-align: center; background-image: linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%); background-size: 16px 16px; background-position: 0 0,0 8px,8px -8px,-8px 0; background-color: #eee; border-radius: 8px; padding: 8px; }
.png-wrap img { max-width: 100%; border-radius: 4px; }
diff --git a/frontend/src/capture/photosReadyLineup.ts b/frontend/src/capture/photosReadyLineup.ts
new file mode 100644
index 0000000..c3dfb5d
--- /dev/null
+++ b/frontend/src/capture/photosReadyLineup.ts
@@ -0,0 +1,75 @@
+import type { BatchPhotoResult } from './capture.types';
+
+/** One slot in the Photos-ready end screen (#64 / docs/PHOTOS.md). */
+export type PhotosReadyLineupItem =
+ | {
+ kind: 'png';
+ role: 'calibration' | 'overlay' | 'batch';
+ blob: Blob;
+ /** Capture-order index for non-calibration batch inventory PNGs. */
+ batchIndex?: number;
+ }
+ | {
+ kind: 'error';
+ batchIndex: number;
+ message: string;
+ };
+
+export interface PhotosReadyLineupInput {
+ inventoryPng?: Blob | null;
+ overlayPng?: Blob | null;
+ batchApply?: BatchPhotoResult[] | null;
+ /** When true, omit the clean calibration PNG (M1 floor warn path). */
+ sourceBelowFloor?: boolean;
+}
+
+/**
+ * Build the Photos-ready lineup:
+ * 1. Clean calibration inventory PNG
+ * 2. Dimension-overlay copy
+ * 3. Other batch inventory PNGs in capture order (batchApply, skip cal null)
+ * with error slots for empty mask / floor failures.
+ */
+export function buildPhotosReadyLineup(
+ input: PhotosReadyLineupInput,
+): PhotosReadyLineupItem[] {
+ const items: PhotosReadyLineupItem[] = [];
+ const { inventoryPng, overlayPng, batchApply, sourceBelowFloor } = input;
+
+ if (inventoryPng && !sourceBelowFloor) {
+ items.push({ kind: 'png', role: 'calibration', blob: inventoryPng });
+ }
+ if (overlayPng) {
+ items.push({ kind: 'png', role: 'overlay', blob: overlayPng });
+ }
+
+ if (batchApply) {
+ for (let i = 0; i < batchApply.length; i++) {
+ const slot = batchApply[i];
+ if (slot === null) continue; // calibration lives in results.inventoryPng
+ if (slot.status === 'ok') {
+ items.push({
+ kind: 'png',
+ role: 'batch',
+ blob: slot.inventoryPng,
+ batchIndex: i,
+ });
+ } else {
+ items.push({
+ kind: 'error',
+ batchIndex: i,
+ message: slot.message,
+ });
+ }
+ }
+ }
+
+ return items;
+}
+
+export function photosReadyPngAlt(item: Extract
): string {
+ if (item.role === 'calibration') return 'Calibration inventory PNG';
+ if (item.role === 'overlay') return 'Calibration dimension-overlay PNG';
+ const n = (item.batchIndex ?? 0) + 1;
+ return `Batch inventory PNG ${n}`;
+}