diff --git a/docs/UAT-PRACTICE.md b/docs/UAT-PRACTICE.md index 21b0878..d5e39fd 100644 --- a/docs/UAT-PRACTICE.md +++ b/docs/UAT-PRACTICE.md @@ -121,7 +121,10 @@ hard stop. Switch to a supported browser. 3. **Wood categories.** Pick at least one from the synced list. 4. **Edge type, figure, grade.** Pick from the synced lists. One edge type, at least one figure, exactly one grade. -5. **Thickness band.** Pick the Woo band that the measured thickness rounds up into. +5. **Thickness band.** Not a control — it is derived automatically from the measured + thickness (the thickness-in rounds up into the synced Woo band, `TV-9` / `QUALITY-95`). + It displays read-only on the Review screen; fix it upstream by correcting the thickness + on capture/measure if the band looks wrong. 6. **Moisture.** Keep the default (kiln-dried) or change it. 7. **fig-* tags.** Optional; pick any from the synced list. 8. **feat-* tags.** Zero or more. Only pick existing `feat-*` tags. diff --git a/docs/contracts/UX.md b/docs/contracts/UX.md index 8881014..439847b 100644 --- a/docs/contracts/UX.md +++ b/docs/contracts/UX.md @@ -55,8 +55,11 @@ Cites REVIEW-5, REVIEW-15, REVIEW-20, METADATA-45. with an "AI" badge; below it the field starts empty. The badge is presentation only and clears on edit. - **Required set.** Exactly one species and at least one wood-category leaf; one Edge Type, at - least one Figure, exactly one Grade, one Thickness band, one Moisture; fig-* and feat-* tags - optional; price; title, short_description, and description. + least one Figure, exactly one Grade, one Moisture; fig-* and feat-* tags optional; price; + title, short_description, and description. The **Thickness band is not an owner control** — it + is derived deterministically from `thickness_in` (the value rounds up into the synced Woo band, + `TV-9` / `QUALITY-95`), displayed read-only, and satisfies the readiness requirement + (`REVIEW-5`) via that derived value. - **Primary category.** The owner declares one primary wood category and any secondary ones; the default is the first selected. - **Inline nudges.** Every empty required field shows an inline nudge as the owner works; there is diff --git a/frontend/src/create/review/MultiSelect.svelte b/frontend/src/create/review/MultiSelect.svelte index 7f818ed..462acca 100644 --- a/frontend/src/create/review/MultiSelect.svelte +++ b/frontend/src/create/review/MultiSelect.svelte @@ -9,7 +9,7 @@ // (METADATA-10: "parents group only and are never assigned"); only the // leaf rows are selectable. Filtering hides a parent group when none of // its leaves match. - import { createEventDispatcher } from 'svelte'; + import { createEventDispatcher, onDestroy } from 'svelte'; export interface Opt { id: number; name: string; ai: boolean } export interface Group { parent: { id: number; name: string }; leaves: Opt[] } @@ -23,16 +23,30 @@ export let placeholder = 'Choose…'; export let testid = ''; export let onToggle: (id: number) => void; + // #90: commit (persist + re-review) after a toggle settles. The parent wires this + // so multi-select changes flush to the slab immediately — no need to touch another + // field first. Debounced so a quick multi-tap doesn't fire a commit per click. + export let onCommit: (() => void) | undefined = undefined; const dispatch = createEventDispatcher(); let open = false; let filter = ''; let focused: number | null = null; + let commitTimer: ReturnType | undefined; const grouped = groups.length > 0; // Effective selectable set (leaves when grouped, options when flat). $: allOpts = (grouped ? groups.flatMap((g) => g.leaves) : options) as Opt[]; + // #90: the closed state must show WHAT is selected (names), not just a count. + // Truncate long lists so the button stays one line. + $: selectedNames = allOpts.filter((o) => selected.includes(o.id)).map((o) => o.name); + $: summary = + selectedNames.length === 0 + ? '' + : selectedNames.length <= 2 + ? selectedNames.join(', ') + : `${selectedNames.slice(0, 2).join(', ')} +${selectedNames.length - 2}`; $: q = filter.trim().toLowerCase(); $: filteredFlat = q ? allOpts.filter((o) => o.name.toLowerCase().includes(q)) : allOpts; // Grouped: keep a parent only if at least one of its leaves survives the filter. @@ -44,24 +58,46 @@ $: count = selected.length; $: allFilteredSelected = filteredFlat.length > 0 && filteredFlat.every((o) => selected.includes(o.id)); + function scheduleCommit() { + if (typeof onCommit !== 'function') return; + if (commitTimer) clearTimeout(commitTimer); + commitTimer = setTimeout(() => { commitTimer = undefined; onCommit(); }, 250); + } function toggle(id: number) { onToggle(id); focused = id; + scheduleCommit(); // #90: persist + re-review without waiting for another field } function selectAll() { for (const o of filteredFlat) if (!selected.includes(o.id)) onToggle(o.id); + scheduleCommit(); } function clearAll() { for (const o of [...filteredFlat]) if (selected.includes(o.id)) onToggle(o.id); + scheduleCommit(); + } + function close() { + open = false; + filter = ''; + if (commitTimer) { clearTimeout(commitTimer); commitTimer = undefined; } + if (typeof onCommit === 'function') onCommit(); // flush immediately on Done + dispatch('commit'); } - function close() { open = false; filter = ''; dispatch('commit'); } function onKey(e: KeyboardEvent) { if (e.key === 'Escape' && open) open = false; } + function destroy() { if (commitTimer) clearTimeout(commitTimer); } + onDestroy(destroy);
@@ -129,6 +165,7 @@ .ms { border: 1px solid #444; border-radius: 3px; background: #1a1a1a; overflow: hidden; } .ms-btn { width: 100%; display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 9px 10px; background: #1d1d1d; border: 0; color: #eee; font-size: .9rem; cursor: pointer; text-align: left; } + .ms-btn .ms-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #eee; } .ms-btn .ms-c { color: #00e5ff; font-weight: 700; font-size: .8rem; white-space: nowrap; } .ms-arrow { color: #777; } .ms-panel { border-top: 1px solid #333; background: #161616; } diff --git a/frontend/src/create/review/Review.svelte b/frontend/src/create/review/Review.svelte index f8f3cc2..fd46375 100644 --- a/frontend/src/create/review/Review.svelte +++ b/frontend/src/create/review/Review.svelte @@ -1,5 +1,5 @@