fix(gate-b): Svelte 5 event directives + label/input association for Select Photos

- Add id='photo-file-input' to file input and for='photo-file-input' to label
- Convert on:change → onchange, on:click → onclick, on:input → oninput
  (Svelte 5 requires native DOM event properties, not on: directives)
- Add Playwright BVT suite confirming file selection → mask step advance
- Test image: 100x60 PNG with green border (hue=66°) and white interior
This commit is contained in:
grokbot_ty
2026-09-07 19:38:46 +00:00
parent 2a8b0e5518
commit 17b6e6ccd6
6 changed files with 235 additions and 24 deletions
+24 -24
View File
@@ -453,8 +453,8 @@
<h2>Capture Photos</h2>
<p class="instruction">Select 1–5 photos of the slab. Supported: JPG, PNG, WEBP.</p>
<div class="drop-zone">
<input type="file" multiple accept="image/*" on:change={(e) => handleFiles(e.target.files)} class="file-input" />
<label class="file-label">
<input type="file" id="photo-file-input" multiple accept="image/*" onchange={(e) => handleFiles(e.target.files)} class="file-input" />
<label for="photo-file-input" class="file-label">
<span class="camera-icon">📷</span>
<span>Select Photos</span>
</label>
@@ -468,7 +468,7 @@
</div>
{/each}
</div>
<button class="btn btn-primary" on:click={() => state.step = 'mask'} disabled={state.photos.length === 0}>
<button class="btn btn-primary" onclick={() => state.step = 'mask'} disabled={state.photos.length === 0}>
Process Photos →
</button>
{/if}
@@ -484,7 +484,7 @@
<div class="knobs-panel">
<div class="knob-group">
<label>Sheet Mode</label>
<select value={getMaskKnobs().sheet} on:change={(e) => {
<select value={getMaskKnobs().sheet} onchange={(e) => {
const knobs = getMaskKnobs();
applyMaskKnobs({ ...knobs, sheet: e.target.value as KnobSettings['sheet'] });
maskCanvas && drawMaskOverlay(maskCanvas, currentPhoto()!);
@@ -497,7 +497,7 @@
<div class="knob-group">
<label>Sensitivity: {getMaskKnobs().sensitivity.toFixed(2)}</label>
<input type="range" min="0" max="1" step="0.01" value={getMaskKnobs().sensitivity}
on:input={(e) => {
oninput={(e) => {
const knobs = getMaskKnobs();
applyMaskKnobs({ ...knobs, sensitivity: parseFloat(e.target.value) });
maskCanvas && drawMaskOverlay(maskCanvas, currentPhoto()!);
@@ -506,7 +506,7 @@
<div class="knob-group">
<label>Edge Offset: {getMaskKnobs().edgeOffset}</label>
<input type="range" min="-20" max="20" step="1" value={getMaskKnobs().edgeOffset}
on:input={(e) => {
oninput={(e) => {
const knobs = getMaskKnobs();
applyMaskKnobs({ ...knobs, edgeOffset: parseInt(e.target.value) });
maskCanvas && drawMaskOverlay(maskCanvas, currentPhoto()!);
@@ -515,7 +515,7 @@
<div class="knob-group">
<label>Feather: {getMaskKnobs().feather}</label>
<input type="range" min="0" max="10" step="1" value={getMaskKnobs().feather}
on:input={(e) => {
oninput={(e) => {
const knobs = getMaskKnobs();
applyMaskKnobs({ ...knobs, feather: parseInt(e.target.value) });
maskCanvas && drawMaskOverlay(maskCanvas, currentPhoto()!);
@@ -523,7 +523,7 @@
</div>
<div class="photo-selector">
<label>Photo:</label>
<select value={state.currentPhotoIndex} on:change={(e) => {
<select value={state.currentPhotoIndex} onchange={(e) => {
state.currentPhotoIndex = parseInt(e.target.value);
maskCanvas && drawMaskOverlay(maskCanvas, currentPhoto()!);
}}>
@@ -535,8 +535,8 @@
</div>
</div>
<div class="btn-row">
<button class="btn btn-secondary" on:click={() => state.step = 'capture'}>← Back</button>
<button class="btn btn-primary" on:click={confirmMaskStep}>Confirm Mask →</button>
<button class="btn btn-secondary" onclick={() => state.step = 'capture'}>← Back</button>
<button class="btn btn-primary" onclick={confirmMaskStep}>Confirm Mask →</button>
</div>
</div>
{/if}
@@ -551,8 +551,8 @@
<div class="knob-group">
<label>Rotation</label>
<div class="rotation-controls">
<button class="btn btn-small" on:click={() => { rotateAxisStep(-90); axisCanvas && drawAxisOverlay(axisCanvas, currentPhoto()!); }}>↺ −90°</button>
<button class="btn btn-small" on:click={() => { rotateAxisStep(90); axisCanvas && drawAxisOverlay(axisCanvas, currentPhoto()!); }}>↻ +90°</button>
<button class="btn btn-small" onclick={() => { rotateAxisStep(-90); axisCanvas && drawAxisOverlay(axisCanvas, currentPhoto()!); }}>↺ −90°</button>
<button class="btn btn-small" onclick={() => { rotateAxisStep(90); axisCanvas && drawAxisOverlay(axisCanvas, currentPhoto()!); }}>↻ +90°</button>
</div>
</div>
<div class="knob-group">
@@ -561,7 +561,7 @@
</div>
<div class="photo-selector">
<label>Photo:</label>
<select value={state.currentPhotoIndex} on:change={(e) => {
<select value={state.currentPhotoIndex} onchange={(e) => {
state.currentPhotoIndex = parseInt(e.target.value);
axisCanvas && drawAxisOverlay(axisCanvas, currentPhoto()!);
}}>
@@ -573,8 +573,8 @@
</div>
</div>
<div class="btn-row">
<button class="btn btn-secondary" on:click={() => state.step = 'mask'}>← Back</button>
<button class="btn btn-primary" on:click={confirmAxisStep}>Confirm Axis →</button>
<button class="btn btn-secondary" onclick={() => state.step = 'mask'}>← Back</button>
<button class="btn btn-primary" onclick={confirmAxisStep}>Confirm Axis →</button>
</div>
</div>
{/if}
@@ -587,19 +587,19 @@
<div class="form-group">
<label>Length (inches)</label>
<input type="number" step="0.125" min="0.125" value={state.lengthIn}
on:input={(e) => updateLengthIn(e.target.value)} />
oninput={(e) => updateLengthIn(e.target.value)} />
<small>1/8" steps</small>
</div>
<div class="form-group">
<label>Thickness (inches)</label>
<input type="number" step="0.125" min="0.125" value={state.thicknessIn}
on:input={(e) => updateThicknessIn(e.target.value)} />
oninput={(e) => updateThicknessIn(e.target.value)} />
<small>1/8" steps</small>
</div>
<div class="form-group">
<label>SKU</label>
<input type="text" value={state.sku}
on:input={(e) => updateSku(e.target.value)}
oninput={(e) => updateSku(e.target.value)}
class:invalid={!state.skuValidation.valid} />
<small>{state.skuValidation.valid ? '✓ Valid' : state.skuValidation.errors.join(', ')}</small>
</div>
@@ -633,8 +633,8 @@
<div class="warning">⚠️ Source image is too small for 1600px minimum output. Consider retaking photos.</div>
{/if}
<div class="btn-row">
<button class="btn btn-secondary" on:click={() => state.step = 'axis'}>← Back</button>
<button class="btn btn-primary" on:click={continueToReview}
<button class="btn btn-secondary" onclick={() => state.step = 'axis'}>← Back</button>
<button class="btn btn-primary" onclick={continueToReview}
disabled={!isSkuValid(state.sku) || state.lengthIn <= 0 || state.thicknessIn <= 0}>
Continue →
</button>
@@ -652,13 +652,13 @@
<h3>✓ Upload Successful</h3>
<p>Slab ID: <code>{state.reviewData?.id}</code></p>
<p>Server Rev: {state.reviewData?.serverRev}</p>
<button class="btn btn-primary" on:click={goToCapture}>Start New Slab</button>
<button class="btn btn-primary" onclick={goToCapture}>Start New Slab</button>
</div>
{:else if state.uploadError}
<div class="error">
<h3>✗ Upload Failed</h3>
<p>{state.uploadError}</p>
<button class="btn btn-secondary" on:click={() => state.uploadError = null}>Dismiss</button>
<button class="btn btn-secondary" onclick={() => state.uploadError = null}>Dismiss</button>
</div>
{:else}
<div class="review-grid">
@@ -681,8 +681,8 @@
</div>
</div>
<div class="btn-row">
<button class="btn btn-secondary" on:click={() => state.step = 'numbers'}>← Back</button>
<button class="btn btn-primary" on:click={uploadDraft} disabled={state.uploading}>
<button class="btn btn-secondary" onclick={() => state.step = 'numbers'}>← Back</button>
<button class="btn btn-primary" onclick={uploadDraft} disabled={state.uploading}>
{state.uploading ? 'Uploading...' : 'Upload Draft'}
</button>
</div>