fix(review): UI nits — margins, buttons, moisture, live validation

Post-publish: hide Back while publishing; drop Settings/Slabs chrome on
done (remove unused white Slabs control). Review/App horizontal padding.
Unify Create desc / View prompt / Create title|short as .btn-sec.
Moisture kd default: sync select after form mount; persist via commit.
Ready/missing update on input (debounced) without blur.
This commit is contained in:
grokbot_ty
2026-09-26 11:08:08 -07:00
parent 88278a55e8
commit 5651a38058
4 changed files with 91 additions and 39 deletions
+17 -14
View File
@@ -11,7 +11,6 @@
import MaskPanel from './capture/MaskPanel.svelte'; import MaskPanel from './capture/MaskPanel.svelte';
import MeasurePanel from './capture/MeasurePanel.svelte'; import MeasurePanel from './capture/MeasurePanel.svelte';
import Settings from './create/settings/Settings.svelte'; import Settings from './create/settings/Settings.svelte';
import SlabList from './create/slabs/SlabList.svelte';
import Review from './create/review/Review.svelte'; import Review from './create/review/Review.svelte';
import Publish from './create/publish/Publish.svelte'; import Publish from './create/publish/Publish.svelte';
import { import {
@@ -27,7 +26,6 @@
let busy = false; let busy = false;
let pngUrl: string | null = null; let pngUrl: string | null = null;
let showSettings = false; let showSettings = false;
let showSlabs = false;
let curFlow: import('./create/flow').Flow = { phase: 'capture' }; let curFlow: import('./create/flow').Flow = { phase: 'capture' };
$: curFlow = $flow; $: curFlow = $flow;
@@ -238,16 +236,14 @@
{/if} {/if}
{/if} {/if}
<!-- Separate pages, never in the flow (UX.md §1). --> <!-- Settings only (dev Slabs list removed — unused white control). Hidden after publish. -->
<div style="margin-top:1rem;display:flex;gap:8px"> {#if curFlow.phase !== 'done'}
<button on:click={() => (showSettings = !showSettings)} data-testid="open-settings">Settings</button> <div class="app-tools">
<button on:click={() => (showSlabs = !showSlabs)} data-testid="open-slabs">Slabs</button> <button type="button" class="tool" on:click={() => (showSettings = !showSettings)} data-testid="open-settings">Settings</button>
</div> </div>
{#if showSettings}
<Settings />
{/if} {/if}
{#if showSlabs} {#if showSettings && curFlow.phase !== 'done'}
<SlabList /> <Settings />
{/if} {/if}
</main> </main>
{/if} {/if}
@@ -258,17 +254,24 @@
background: #121212; background: #121212;
font-family: system-ui, -apple-system, Segoe UI, sans-serif; font-family: system-ui, -apple-system, Segoe UI, sans-serif;
} }
.app, .hardstop { max-width: 720px; margin: 0 auto; min-height: 100vh; } .app, .hardstop { max-width: 720px; margin: 0 auto; min-height: 100vh; box-sizing: border-box; }
.app { padding: 12px 16px 24px; }
.hardstop { padding: 48px 24px; color: #eee; text-align: center; } .hardstop { padding: 48px 24px; color: #eee; text-align: center; }
.hardstop h1 { font-size: 1.6rem; } .hardstop h1 { font-size: 1.6rem; }
.detail { color: #888; font-size: 0.85rem; } .detail { color: #888; font-size: 0.85rem; }
.panel { padding: 16px; color: #eee; } .panel { padding: 4px 0 16px; color: #eee; }
.done .ok { color: #7fd37f; font-weight: 600; } .done .ok { color: #7fd37f; font-weight: 600; }
.row { display: flex; gap: 10px; margin-top: 16px; } .row { display: flex; gap: 10px; margin-top: 16px; }
.primary { .primary {
flex: 1; padding: 14px; font-size: 1.05rem; border-radius: 10px; border: 0; flex: 1; padding: 14px; font-size: 1rem; border-radius: 3px; border: 0;
background: #00e5ff; color: #002; font-weight: 700; background: #00e5ff; color: #002; font-weight: 700;
} }
.busy { color: #aaa; } .busy { color: #aaa; }
.save-err { margin-top: 12px; color: #ffb4a2; font-weight: 600; } .save-err { margin-top: 12px; color: #ffb4a2; font-weight: 600; }
.app-tools { margin-top: 1rem; display: flex; gap: 8px; }
.tool {
box-sizing: border-box; padding: 8px 12px; border-radius: 3px; border: 1px solid #444;
background: #1d1d1d; color: #ccc; font-size: .82rem; font-weight: 600; cursor: pointer;
}
.tool:hover { background: #242424; }
</style> </style>
+9 -5
View File
@@ -77,11 +77,15 @@
<h1>Publish to WooCommerce</h1> <h1>Publish to WooCommerce</h1>
<p class="meta">SKU {slab?.sku || '—'} · {slab?.bdft != null ? slab.bdft.toFixed(1) : '—'} bdft · {slab?.price != null ? `$${slab.price.toFixed(2)}` : '—'}</p> <p class="meta">SKU {slab?.sku || '—'} · {slab?.bdft != null ? slab.bdft.toFixed(1) : '—'} bdft · {slab?.price != null ? `$${slab.price.toFixed(2)}` : '—'}</p>
{#if st.phase === 'idle' || st.phase === 'working'} {#if st.phase === 'idle'}
<button type="button" class="primary" data-testid="publish-btn" on:click={doPublish} disabled={st.phase === 'working'}> <button type="button" class="primary" data-testid="publish-btn" on:click={doPublish}>
{st.phase === 'working' ? 'Publishing…' : 'Publish'} Publish
</button>
<button type="button" class="ghost" data-testid="back-review" on:click={backToReview}>Back to Review</button>
{:else if st.phase === 'working'}
<button type="button" class="primary" data-testid="publish-btn" disabled>
Publishing…
</button> </button>
<button type="button" class="ghost" data-testid="back-review" on:click={backToReview} disabled={st.phase === 'working'}>Back to Review</button>
{/if} {/if}
{#if st.phase === 'success'} {#if st.phase === 'success'}
@@ -119,7 +123,7 @@
{/if} {/if}
<style> <style>
.pub { max-width: 520px; margin: 0 auto; color: #eee; } .pub { max-width: 520px; margin: 0 auto; color: #eee; padding: 4px 4px 16px; box-sizing: border-box; }
h1 { font-size: 1.3rem; margin: 0 0 6px; } h2 { font-size: 1.1rem; margin: 0 0 6px; } h1 { font-size: 1.3rem; margin: 0 0 6px; } h2 { font-size: 1.1rem; margin: 0 0 6px; }
.meta { color: #888; font-size: .82rem; margin: 4px 0 10px; } .meta { color: #888; font-size: .82rem; margin: 4px 0 10px; }
.primary { width: 100%; padding: 14px; font-size: 1rem; border-radius: 3px; border: 0; background: #00e5ff; color: #002; font-weight: 700; margin-top: 12px; } .primary { width: 100%; padding: 14px; font-size: 1rem; border-radius: 3px; border: 0; background: #00e5ff; color: #002; font-weight: 700; margin-top: 12px; }
+53 -20
View File
@@ -163,6 +163,23 @@
// a stale "not ready" review clobbers the final ready one — the // a stale "not ready" review clobbers the final ready one — the
// continue-to-Publish button stays disabled even though the status reads ready. // continue-to-Publish button stays disabled even though the status reads ready.
let commitToken = 0; let commitToken = 0;
// Live ready/missing: commit on input/change without requiring blur (extend Create-desc gate).
let fieldCommitTimer: ReturnType<typeof setTimeout> | undefined;
let descCommitTimer: ReturnType<typeof setTimeout> | undefined;
function scheduleFieldCommit() {
if (fieldCommitTimer) clearTimeout(fieldCommitTimer);
fieldCommitTimer = setTimeout(() => {
fieldCommitTimer = undefined;
void commitReview();
}, 250);
}
function scheduleDescCommit() {
if (descCommitTimer) clearTimeout(descCommitTimer);
descCommitTimer = setTimeout(() => {
descCommitTimer = undefined;
void commitDescription();
}, 250);
}
// --- Inference --- // --- Inference ---
let genBusy = false; let genBusy = false;
@@ -257,6 +274,8 @@
onDestroy(() => { onDestroy(() => {
for (const u of photoUrls) URL.revokeObjectURL(u); for (const u of photoUrls) URL.revokeObjectURL(u);
photoUrls = []; photoUrls = [];
if (fieldCommitTimer) clearTimeout(fieldCommitTimer);
if (descCommitTimer) clearTimeout(descCommitTimer);
}); });
// Whole-card tap = keep/remove toggle. A real drag (>6px) reorders and does NOT // Whole-card tap = keep/remove toggle. A real drag (>6px) reorders and does NOT
@@ -343,7 +362,7 @@
edgeSel = s.edge_type_term_id != null ? String(s.edge_type_term_id) : ''; edgeSel = s.edge_type_term_id != null ? String(s.edge_type_term_id) : '';
figureIds = [...(s.figure_term_ids ?? [])]; figureIds = [...(s.figure_term_ids ?? [])];
gradeSel = s.grade_term_id != null ? String(s.grade_term_id) : ''; gradeSel = s.grade_term_id != null ? String(s.grade_term_id) : '';
// Moisture default Kiln dried (slug kd) when empty — persist via commitReview after load. // Moisture default Kiln dried (slug kd) when empty — mount commitReview persists it.
{ {
const moistAttr = t?.attributes.find((a) => a.name.toLowerCase() === 'moisture'); const moistAttr = t?.attributes.find((a) => a.name.toLowerCase() === 'moisture');
const moistTerms = (moistAttr?.terms ?? []).map((x) => ({ const moistTerms = (moistAttr?.terms ?? []).map((x) => ({
@@ -351,6 +370,8 @@
})); }));
const applied = applyMoistureDefault(s.moisture_term_id, moistTerms); const applied = applyMoistureDefault(s.moisture_term_id, moistTerms);
moistureSel = applied === '' ? '' : String(applied); moistureSel = applied === '' ? '' : String(applied);
// Keep native <select> in sync when we programmatically default empty → kd.
void syncSelects();
} }
featTagIds = [...(s.feat_tag_ids ?? [])]; featTagIds = [...(s.feat_tag_ids ?? [])];
title = s.title ?? ''; title = s.title ?? '';
@@ -650,6 +671,8 @@
loadError = e instanceof ApiError ? e.message : 'Could not load the slab.'; loadError = e instanceof ApiError ? e.message : 'Could not load the slab.';
} finally { } finally {
loading = false; loading = false;
// Form mounts after loading flips false — sync selects (incl. moisture kd default).
void syncSelects();
} }
}); });
@@ -833,8 +856,8 @@
<div class="field"> <div class="field">
<label class="lbl" for="title">Title</label> <label class="lbl" for="title">Title</label>
<div class="create-row"> <div class="create-row">
<input id="title" class="inp" data-testid="title" bind:value={title} /> <input id="title" class="inp" data-testid="title" bind:value={title} on:input={scheduleFieldCommit} />
<button type="button" class="create-btn" data-testid="create-title" <button type="button" class="btn-sec" data-testid="create-title"
disabled={!canCreateTitle || createBusy} on:click={createTitle}> disabled={!canCreateTitle || createBusy} on:click={createTitle}>
{createBusy ? 'Building…' : 'Create title'} {createBusy ? 'Building…' : 'Create title'}
</button> </button>
@@ -843,8 +866,8 @@
<div class="field"> <div class="field">
<label class="lbl" for="short-desc">Short description</label> <label class="lbl" for="short-desc">Short description</label>
<div class="create-row"> <div class="create-row">
<input id="short-desc" class="inp" data-testid="short-desc" bind:value={shortDescription} /> <input id="short-desc" class="inp" data-testid="short-desc" bind:value={shortDescription} on:input={scheduleFieldCommit} />
<button type="button" class="create-btn" data-testid="create-short-desc" <button type="button" class="btn-sec" data-testid="create-short-desc"
disabled={!canCreateTitle || createBusy} on:click={createTitle}> disabled={!canCreateTitle || createBusy} on:click={createTitle}>
{createBusy ? 'Building…' : 'Create short desc'} {createBusy ? 'Building…' : 'Create short desc'}
</button> </button>
@@ -852,11 +875,11 @@
</div> </div>
<div class="field"> <div class="field">
<label class="lbl" for="desc">Description</label> <label class="lbl" for="desc">Description</label>
<textarea id="desc" class="inp ta" data-testid="desc" rows="3" bind:value={description} on:blur={commitDescription}></textarea> <textarea id="desc" class="inp ta" data-testid="desc" rows="3" bind:value={description} on:input={scheduleDescCommit} on:blur={commitDescription}></textarea>
<div class="gen"> <div class="gen">
<button <button
type="button" type="button"
class="gen-btn" class="btn-sec"
data-testid="create-desc" data-testid="create-desc"
on:click={generateContent} on:click={generateContent}
disabled={!canCreateDesc || genBusy} disabled={!canCreateDesc || genBusy}
@@ -866,7 +889,7 @@
</button> </button>
<button <button
type="button" type="button"
class="view-prompt-btn" class="btn-sec"
data-testid="view-prompt" data-testid="view-prompt"
on:click={() => { if (viewPromptOpen) viewPromptOpen = false; else void loadViewPrompt(); }} on:click={() => { if (viewPromptOpen) viewPromptOpen = false; else void loadViewPrompt(); }}
disabled={viewPromptBusy} disabled={viewPromptBusy}
@@ -917,11 +940,11 @@
<label class="lbl" for="price">Price ($) <label class="lbl" for="price">Price ($)
{#if review?.price_suggestion != null}<span class="sug">suggested ${review.price_suggestion.toFixed(2)}</span>{/if} {#if review?.price_suggestion != null}<span class="sug">suggested ${review.price_suggestion.toFixed(2)}</span>{/if}
</label> </label>
<input id="price" class="inp" data-testid="price" type="number" step="0.01" bind:value={price} on:change={commitReview} /> <input id="price" class="inp" data-testid="price" type="number" step="0.01" bind:value={price} on:input={scheduleFieldCommit} on:change={commitReview} />
</div> </div>
<div class="field"> <div class="field">
<label class="lbl" for="sku">SKU <span class="sug">last field, 3–24 chars</span></label> <label class="lbl" for="sku">SKU <span class="sug">last field, 3–24 chars</span></label>
<input id="sku" class="inp" data-testid="sku" placeholder="SLAB-…" bind:value={sku} on:blur={commitReview} /> <input id="sku" class="inp" data-testid="sku" placeholder="SLAB-…" bind:value={sku} on:input={scheduleFieldCommit} on:blur={commitReview} />
{#if sku && !/^[A-Za-z0-9-]{3,24}$/.test(sku)} {#if sku && !/^[A-Za-z0-9-]{3,24}$/.test(sku)}
<p class="nudge">SKU must be 3–24 alphanumerics or hyphens.</p> <p class="nudge">SKU must be 3–24 alphanumerics or hyphens.</p>
{/if} {/if}
@@ -961,7 +984,7 @@
{/if} {/if}
<style> <style>
.review { max-width: 560px; margin: 0 auto; color: #eee; } .review { max-width: 560px; margin: 0 auto; color: #eee; padding: 4px 4px 20px; box-sizing: border-box; }
h1 { font-size: 1.3rem; margin: 0 0 12px; } h1 { font-size: 1.3rem; margin: 0 0 12px; }
.math div { display: flex; justify-content: space-between; border-bottom: 1px solid #2a2a2a; padding: 5px 0; font-size: .85rem; } .math div { display: flex; justify-content: space-between; border-bottom: 1px solid #2a2a2a; padding: 5px 0; font-size: .85rem; }
.math dt { color: #9a9a9a; margin: 0; } .math dd { margin: 0; font-variant-numeric: tabular-nums; } .math dt { color: #9a9a9a; margin: 0; } .math dd { margin: 0; font-variant-numeric: tabular-nums; }
@@ -976,15 +999,25 @@
.field { margin-bottom: 12px; } .field { margin-bottom: 12px; }
.nudge { color: #e8a; font-size: .74rem; margin: 4px 0 0; } .nudge { color: #e8a; font-size: .74rem; margin: 4px 0 0; }
.sug { color: #aaa; font-size: .74rem; font-weight: 400; margin-left: 6px; } .sug { color: #aaa; font-size: .74rem; font-weight: 400; margin-left: 6px; }
.gen { margin-top: 8px; } .gen-btn { padding: 8px 12px; border: 0; border-radius: 3px; background: #00e5ff; color: #002; font-weight: 600; font-size: .82rem; } .gen { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px; }
/* Lock chrome matches agreed #90 wireframe: muted dark green, not jarring neon. */ /* Unified secondary actions: Create title/short/desc, View prompt — same size & label weight. */
/* #89: explicit "Create" build buttons beside the title / short-desc fields. */
.create-row { display: flex; gap: 8px; align-items: stretch; } .create-row { display: flex; gap: 8px; align-items: stretch; }
.create-row .inp { flex: 1; } .create-row .inp { flex: 1; }
.create-btn { box-sizing: border-box; padding: 8px 12px; border: 1px solid #333; border-radius: 3px; .btn-sec { box-sizing: border-box; padding: 9px 12px; border: 1px solid #444; border-radius: 3px;
background: #1d1d1d; color: #eee; font-size: .82rem; font-weight: 600; cursor: pointer; white-space: nowrap; } background: #1d1d1d; color: #eee; font-size: .82rem; font-weight: 600; cursor: pointer; white-space: nowrap; }
.create-btn:hover { background: #242424; } .btn-sec:hover { background: #242424; }
.create-btn:disabled { background: #222; color: #666; border-color: #333; cursor: not-allowed; } .btn-sec:disabled { background: #222; color: #666; border-color: #333; cursor: not-allowed; }
.prompt-panel { margin-top: 10px; border: 1px solid #333; border-radius: 3px; background: #141414; padding: 10px 12px; }
.prompt-head { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 8px; }
.prompt-title { font-size: .78rem; color: #9a9a9a; flex: 1; min-width: 120px; }
.mini { box-sizing: border-box; padding: 6px 10px; border: 1px solid #444; border-radius: 3px;
background: #1d1d1d; color: #ccc; font-size: .74rem; font-weight: 600; cursor: pointer; }
.mini:disabled { color: #666; cursor: not-allowed; }
.prompt-text { margin: 0; padding: 10px; background: #0f0f0f; border: 1px solid #2a2a2a; border-radius: 3px;
color: #ddd; font-size: .72rem; line-height: 1.45; white-space: pre-wrap; word-break: break-word; max-height: 280px; overflow: auto; }
.prompt-photos { margin-top: 10px; }
.prompt-photos-lbl { font-size: .72rem; color: #888; margin-bottom: 4px; }
.prompt-photos ul { margin: 0; padding-left: 18px; color: #aaa; font-size: .72rem; }
/* Primary wood category — inline under wood MultiSelect (REVIEW-15 / #90 wireframe). */ /* Primary wood category — inline under wood MultiSelect (REVIEW-15 / #90 wireframe). */
.primary-set { margin-top: 8px; padding: 10px 12px; border: 1px solid #333; border-radius: 3px; background: #141414; } .primary-set { margin-top: 8px; padding: 10px 12px; border: 1px solid #333; border-radius: 3px; background: #141414; }
@@ -1003,9 +1036,9 @@
.status.ok { background: #122a12; color: #7fd37f; } .status.ok { background: #122a12; color: #7fd37f; }
.missing { color: #999; font-size: .76rem; margin: 6px 0 0; } .missing { color: #999; font-size: .76rem; margin: 6px 0 0; }
.row { display: flex; gap: 10px; margin-top: 16px; } .row { display: flex; gap: 10px; margin-top: 16px; }
.primary { flex: 1; padding: 14px; font-size: 1rem; border-radius: 3px; border: 0; background: #00e5ff; color: #002; font-weight: 700; } .primary { flex: 1; padding: 14px; font-size: 1rem; border-radius: 3px; border: 0; background: #00e5ff; color: #002; font-weight: 700; cursor: pointer; }
.primary:disabled { background: #333; color: #777; } .primary:disabled { background: #333; color: #777; cursor: not-allowed; }
.ghost { padding: 14px 18px; border-radius: 3px; border: 1px solid #444; background: transparent; color: #ccc; } .ghost { box-sizing: border-box; padding: 14px 18px; border-radius: 3px; border: 1px solid #444; background: transparent; color: #ccc; font-size: .9rem; font-weight: 600; cursor: pointer; }
.err { color: #e88; } .busy { color: #aaa; } .err { color: #e88; } .busy { color: #aaa; }
/* #90 Photo strip */ /* #90 Photo strip */
@@ -44,3 +44,15 @@ test('Review UI: View prompt + Create desc gate wiring', () => {
// Description is not part of the Create-desc gate label list // Description is not part of the Create-desc gate label list
assert.equal(reviewSrc.includes("Create desc needs: description"), false); assert.equal(reviewSrc.includes("Create desc needs: description"), false);
}); });
test('Review UI: live field commit + unified secondary buttons + moisture default', () => {
assert.match(reviewSrc, /scheduleFieldCommit/);
assert.match(reviewSrc, /scheduleDescCommit/);
assert.match(reviewSrc, /class="btn-sec"/);
assert.match(reviewSrc, /applyMoistureDefault/);
assert.match(reviewSrc, /void syncSelects\(\)/);
// No leftover white unstyled view-prompt class
assert.equal(reviewSrc.includes('view-prompt-btn'), false);
assert.equal(reviewSrc.includes('create-btn'), false);
});