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:
+17
-14
@@ -11,7 +11,6 @@
|
||||
import MaskPanel from './capture/MaskPanel.svelte';
|
||||
import MeasurePanel from './capture/MeasurePanel.svelte';
|
||||
import Settings from './create/settings/Settings.svelte';
|
||||
import SlabList from './create/slabs/SlabList.svelte';
|
||||
import Review from './create/review/Review.svelte';
|
||||
import Publish from './create/publish/Publish.svelte';
|
||||
import {
|
||||
@@ -27,7 +26,6 @@
|
||||
let busy = false;
|
||||
let pngUrl: string | null = null;
|
||||
let showSettings = false;
|
||||
let showSlabs = false;
|
||||
|
||||
let curFlow: import('./create/flow').Flow = { phase: 'capture' };
|
||||
$: curFlow = $flow;
|
||||
@@ -238,16 +236,14 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Separate pages, never in the flow (UX.md §1). -->
|
||||
<div style="margin-top:1rem;display:flex;gap:8px">
|
||||
<button on:click={() => (showSettings = !showSettings)} data-testid="open-settings">Settings</button>
|
||||
<button on:click={() => (showSlabs = !showSlabs)} data-testid="open-slabs">Slabs</button>
|
||||
</div>
|
||||
{#if showSettings}
|
||||
<Settings />
|
||||
<!-- Settings only (dev Slabs list removed — unused white control). Hidden after publish. -->
|
||||
{#if curFlow.phase !== 'done'}
|
||||
<div class="app-tools">
|
||||
<button type="button" class="tool" on:click={() => (showSettings = !showSettings)} data-testid="open-settings">Settings</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if showSlabs}
|
||||
<SlabList />
|
||||
{#if showSettings && curFlow.phase !== 'done'}
|
||||
<Settings />
|
||||
{/if}
|
||||
</main>
|
||||
{/if}
|
||||
@@ -258,17 +254,24 @@
|
||||
background: #121212;
|
||||
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 h1 { font-size: 1.6rem; }
|
||||
.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; }
|
||||
.row { display: flex; gap: 10px; margin-top: 16px; }
|
||||
.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;
|
||||
}
|
||||
.busy { color: #aaa; }
|
||||
.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>
|
||||
|
||||
@@ -77,11 +77,15 @@
|
||||
<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>
|
||||
|
||||
{#if st.phase === 'idle' || st.phase === 'working'}
|
||||
<button type="button" class="primary" data-testid="publish-btn" on:click={doPublish} disabled={st.phase === 'working'}>
|
||||
{st.phase === 'working' ? 'Publishing…' : 'Publish'}
|
||||
{#if st.phase === 'idle'}
|
||||
<button type="button" class="primary" data-testid="publish-btn" on:click={doPublish}>
|
||||
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 type="button" class="ghost" data-testid="back-review" on:click={backToReview} disabled={st.phase === 'working'}>Back to Review</button>
|
||||
{/if}
|
||||
|
||||
{#if st.phase === 'success'}
|
||||
@@ -119,7 +123,7 @@
|
||||
{/if}
|
||||
|
||||
<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; }
|
||||
.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; }
|
||||
|
||||
@@ -163,6 +163,23 @@
|
||||
// a stale "not ready" review clobbers the final ready one — the
|
||||
// continue-to-Publish button stays disabled even though the status reads ready.
|
||||
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 ---
|
||||
let genBusy = false;
|
||||
@@ -257,6 +274,8 @@
|
||||
onDestroy(() => {
|
||||
for (const u of photoUrls) URL.revokeObjectURL(u);
|
||||
photoUrls = [];
|
||||
if (fieldCommitTimer) clearTimeout(fieldCommitTimer);
|
||||
if (descCommitTimer) clearTimeout(descCommitTimer);
|
||||
});
|
||||
|
||||
// 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) : '';
|
||||
figureIds = [...(s.figure_term_ids ?? [])];
|
||||
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 moistTerms = (moistAttr?.terms ?? []).map((x) => ({
|
||||
@@ -351,6 +370,8 @@
|
||||
}));
|
||||
const applied = applyMoistureDefault(s.moisture_term_id, moistTerms);
|
||||
moistureSel = applied === '' ? '' : String(applied);
|
||||
// Keep native <select> in sync when we programmatically default empty → kd.
|
||||
void syncSelects();
|
||||
}
|
||||
featTagIds = [...(s.feat_tag_ids ?? [])];
|
||||
title = s.title ?? '';
|
||||
@@ -650,6 +671,8 @@
|
||||
loadError = e instanceof ApiError ? e.message : 'Could not load the slab.';
|
||||
} finally {
|
||||
loading = false;
|
||||
// Form mounts after loading flips false — sync selects (incl. moisture kd default).
|
||||
void syncSelects();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -833,8 +856,8 @@
|
||||
<div class="field">
|
||||
<label class="lbl" for="title">Title</label>
|
||||
<div class="create-row">
|
||||
<input id="title" class="inp" data-testid="title" bind:value={title} />
|
||||
<button type="button" class="create-btn" data-testid="create-title"
|
||||
<input id="title" class="inp" data-testid="title" bind:value={title} on:input={scheduleFieldCommit} />
|
||||
<button type="button" class="btn-sec" data-testid="create-title"
|
||||
disabled={!canCreateTitle || createBusy} on:click={createTitle}>
|
||||
{createBusy ? 'Building…' : 'Create title'}
|
||||
</button>
|
||||
@@ -843,8 +866,8 @@
|
||||
<div class="field">
|
||||
<label class="lbl" for="short-desc">Short description</label>
|
||||
<div class="create-row">
|
||||
<input id="short-desc" class="inp" data-testid="short-desc" bind:value={shortDescription} />
|
||||
<button type="button" class="create-btn" data-testid="create-short-desc"
|
||||
<input id="short-desc" class="inp" data-testid="short-desc" bind:value={shortDescription} on:input={scheduleFieldCommit} />
|
||||
<button type="button" class="btn-sec" data-testid="create-short-desc"
|
||||
disabled={!canCreateTitle || createBusy} on:click={createTitle}>
|
||||
{createBusy ? 'Building…' : 'Create short desc'}
|
||||
</button>
|
||||
@@ -852,11 +875,11 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<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">
|
||||
<button
|
||||
type="button"
|
||||
class="gen-btn"
|
||||
class="btn-sec"
|
||||
data-testid="create-desc"
|
||||
on:click={generateContent}
|
||||
disabled={!canCreateDesc || genBusy}
|
||||
@@ -866,7 +889,7 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="view-prompt-btn"
|
||||
class="btn-sec"
|
||||
data-testid="view-prompt"
|
||||
on:click={() => { if (viewPromptOpen) viewPromptOpen = false; else void loadViewPrompt(); }}
|
||||
disabled={viewPromptBusy}
|
||||
@@ -917,11 +940,11 @@
|
||||
<label class="lbl" for="price">Price ($)
|
||||
{#if review?.price_suggestion != null}<span class="sug">suggested ${review.price_suggestion.toFixed(2)}</span>{/if}
|
||||
</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 class="field">
|
||||
<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)}
|
||||
<p class="nudge">SKU must be 3–24 alphanumerics or hyphens.</p>
|
||||
{/if}
|
||||
@@ -961,7 +984,7 @@
|
||||
{/if}
|
||||
|
||||
<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; }
|
||||
.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; }
|
||||
@@ -976,15 +999,25 @@
|
||||
.field { margin-bottom: 12px; }
|
||||
.nudge { color: #e8a; font-size: .74rem; margin: 4px 0 0; }
|
||||
.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; }
|
||||
/* Lock chrome matches agreed #90 wireframe: muted dark green, not jarring neon. */
|
||||
/* #89: explicit "Create" build buttons beside the title / short-desc fields. */
|
||||
.gen { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
/* Unified secondary actions: Create title/short/desc, View prompt — same size & label weight. */
|
||||
.create-row { display: flex; gap: 8px; align-items: stretch; }
|
||||
.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; }
|
||||
.create-btn:hover { background: #242424; }
|
||||
.create-btn:disabled { background: #222; color: #666; border-color: #333; cursor: not-allowed; }
|
||||
.btn-sec:hover { background: #242424; }
|
||||
.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-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; }
|
||||
.missing { color: #999; font-size: .76rem; margin: 6px 0 0; }
|
||||
.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:disabled { background: #333; color: #777; }
|
||||
.ghost { padding: 14px 18px; border-radius: 3px; border: 1px solid #444; background: transparent; color: #ccc; }
|
||||
.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; cursor: not-allowed; }
|
||||
.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; }
|
||||
|
||||
/* #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
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user