feat(ui): Jeff-facing copy + 3:4 inventory thumbs + fixed overlay font

Locked UI/copy pass on proto-per-user-woo-creds (Settings/Woo gate polish out of scope):
- Inventory PNG thumbs at 3:4 contain (Review, Publish preview, Photos ready); stop 1:1 cover squaring
- Dimension overlay labels fixed OVERLAY_FONT_PX=36 for every product
- Page 1–3 Jeff-plain copy; drop Measure red tech essays / px scale rows
- Review: lock SVG, consistent red missing/not-ready, View prompt gated (Settings default off)
This commit is contained in:
grokbot_ty
2026-09-27 09:04:47 -07:00
parent 14dcdc43e6
commit 20a7d7d9d5
21 changed files with 231 additions and 177 deletions
+21 -43
View File
@@ -7,7 +7,6 @@
import { clampTiltDeg, defaultTicks } from './tapeGeometry';
import PhotoStage from './PhotoStage.svelte';
import { getCropFill, setCropFill } from '../settings/cropFill';
import { cropMarginFraction } from '../png/crop';
import { getTaxonomy } from '../api/slabs';
import { speciesLeavesFromTaxonomy } from '../create/settings/speciesLeaves';
import {
@@ -163,8 +162,7 @@
<h1>{photosReady ? 'Photos ready' : 'Measure'}</h1>
{#if !photosReady}
<p class="hint">Orient the slab on the fixed tape line, then enter length, thickness, and species.</p>
<p class="nudge">The black line is <b>fixed</b>: tilting, dragging and zooming the slab never move it. It stays in the middle of the rectangle.</p>
<p class="hint">Line up the slab with the black line, then enter length, thickness, and species.</p>
<div class="mode-toggle" role="group" aria-label="Stage gesture mode">
<button
@@ -184,13 +182,6 @@
on:click={() => (gestureMode = 'rotate')}
>Rotate</button>
</div>
<p class="nudge mode-hint">
{#if gestureMode === 'zoompan'}
Pinch to zoom and drag to pan. Rotation gestures are ignored — switch to Rotate or use the tilt slider.
{:else}
Two-finger twist to rotate (or use the tilt slider). Pan and pinch-zoom are ignored in this mode.
{/if}
</p>
<!-- Same binding as Mask/Axis: oriented source + approved bitset.
PhotoStage paints opaque checkerboard on non-slab (iOS-safe under the
@@ -209,7 +200,7 @@
<div class="controls">
<div>
<p class="ctl-label">Tilt the slab</p>
<p class="ctl-label">Tilt</p>
<div class="angle">
<button type="button" class="thumb" aria-label="Tilt left" on:click={() => nudgeTilt(-1)}>−</button>
<input
@@ -220,11 +211,10 @@
<button type="button" class="thumb" aria-label="Tilt right" on:click={() => nudgeTilt(1)}>+</button>
<span class="val">{tiltShown}°</span>
</div>
<p class="nudge">±90° · slider always works · in Rotate mode: ← → tilt 1° · Shift+← → 0.1° · two-finger twist</p>
</div>
<div>
<p class="ctl-label">Zoom the slab</p>
<p class="ctl-label">Zoom</p>
<div class="angle">
<button type="button" class="thumb" aria-label="Zoom out" on:click={() => nudgeZoom(-1)}>−</button>
<input
@@ -236,40 +226,30 @@
<span class="val">{Math.round(tape.zoom * 100)}%</span>
</div>
<div class="row tight">
<button type="button" class="ghost" on:click={fit}>Fit — 100%, centred</button>
<button type="button" class="ghost" on:click={fit}>Reset view</button>
</div>
</div>
<div>
<p class="ctl-label">Crop fill % <span class="sub">global setting</span></p>
<p class="ctl-label">Frame fill</p>
<div class="croprow">
<input
type="range" min="50" max="100" step="1"
value={Math.round(cropFill * 100)}
aria-label="Crop fill percent"
aria-label="Frame fill"
on:input={(e) => onFill(Number(e.currentTarget.value))}
/>
<span class="val">{Math.round(cropFill * 100)}%</span>
</div>
<p class="nudge">
settings.crop_fill (default 90). At {Math.round(cropFill * 100)}% the overlay ends sit
{(cropMarginFraction(cropFill) * 100).toFixed(1)}% in from top and bottom.
The 3:4 aspect never changes. Stored PNGs are not re-cropped.
</p>
</div>
<div class="nums">
<div><span>Tape span between ticks</span><span class="tab">{tape.spanPx.toFixed(0)} px</span></div>
<div><span>Scale (once L entered)</span><span class="tab dim">span / L → px/in</span></div>
</div>
</div>
<div class="fields">
<div class="field">
<span class="label">Length L</span>
<span class="label">Length (in)</span>
<input
type="number"
aria-label="Length L"
aria-label="Length (in)"
inputmode="decimal"
step="0.125"
min="0"
@@ -335,34 +315,33 @@
{:else if state.results}
<div class="results" aria-label="Photos ready">
<div class="nums">
<div><span>Widths</span><span>{fmtEighths(state.results.widths.min)} – {fmtEighths(state.results.widths.max)} (avg {fmtEighths(state.results.widths.avg)})</span></div>
<div><span>Sqft</span><span>{state.results.sqft.toFixed(2)}</span></div>
<div><span>Bdft</span><span>{state.results.bdft.toFixed(2)}</span></div>
<div><span>Scale</span><span>{state.results.scale.toFixed(2)} px/in</span></div>
<div><span>Widths (avg {fmtEighths(state.results.widths.avg)})</span><span>{fmtEighths(state.results.widths.min)} – {fmtEighths(state.results.widths.max)}</span></div>
<div><span>Square feet</span><span>{state.results.sqft.toFixed(2)}</span></div>
<div><span>Board feet</span><span>{state.results.bdft.toFixed(2)}</span></div>
<div data-testid="photos-ready-species"><span>Species</span><span>{speciesOpts.find((o) => o.id === state.speciesId)?.name ?? (state.speciesId != null ? `#${state.speciesId}` : '—')}</span></div>
</div>
{#if state.results.sourceBelowBlock}
<div class="warn" role="alert" data-testid="size-floor-block">
Source shorter side is below 300px — inventory PNG blocked. Retake a larger photo.
This photo is too small. Take a closer or larger photo.
</div>
{:else if state.results.sourceBelowWarn}
<div class="warn" data-testid="size-floor-warn">
Source shorter side is under 800px (300–799). Photo included; consider a closer/larger shot for best quality.
This photo is a bit small. A closer shot will look sharper on the listing.
</div>
{/if}
<ol class="lineup" aria-label="Inventory PNG lineup">
<ol class="lineup" aria-label="Photos for the listing">
{#each lineupItems as item, i (i)}
{#if item.kind === 'png'}
<li class="lineup-item png">
<img src={lineupUrls[i]} alt={photosReadyPngAlt(item)} />
<span class="role">
{#if item.role === 'calibration'}
Clean calibration
Reference photo
{:else if item.role === 'overlay'}
Dimension overlay
Photo with measurements
{:else}
Batch photo {(item.batchIndex ?? 0) + 1}
Photo {(item.batchIndex ?? 0) + 1}
{/if}
</span>
{#if item.warn}
@@ -381,7 +360,7 @@
<!-- Legacy single-PNG fallback only when lineup helper yielded nothing but pngUrl exists -->
{#if lineupItems.length === 0 && pngUrl && !state.results.sourceBelowBlock}
<div class="png-wrap">
<img src={pngUrl} alt="Inventory PNG" />
<img src={pngUrl} alt="Listing photo" />
</div>
{/if}
</div>
@@ -411,8 +390,7 @@
}
.mode-btn + .mode-btn { border-left: 1px solid #444; }
.mode-btn.active { background: #00e5ff; color: #002; }
.mode-hint { margin: 0 0 10px; }
.nudge { color: #e8a; font-size: 0.74rem; margin: 6px 0 0; line-height: 1.45; }
.nudge { color: #aaa; font-size: 0.8rem; margin: 6px 0 0; line-height: 1.45; }
.controls { margin-top: 16px; display: flex; flex-direction: column; gap: 14px; }
.ctl-label { font-size: 0.78rem; color: #aaa; margin: 0 0 6px; }
.ctl-label .sub { font-size: 0.7rem; color: #777; }
@@ -461,7 +439,7 @@
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; }
.lineup-item.png img { width: 100%; aspect-ratio: 3 / 4; object-fit: contain; 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;
@@ -470,5 +448,5 @@
.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; }
.png-wrap img { width: 100%; aspect-ratio: 3 / 4; object-fit: contain; border-radius: 4px; }
</style>