← back to All Designerwallcoverings
Every SKU card gets a smart face button: live price check (tier-labeled cost) → stored stock → Email Vendor fallback; shared per-card popovers + price-forward live render
750e7f4cbf12118d83bea33d3042128e1f970c38 · 2026-07-15 14:31:33 -0700 · Steve Abrams
Files touched
Diff
commit 750e7f4cbf12118d83bea33d3042128e1f970c38
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jul 15 14:31:33 2026 -0700
Every SKU card gets a smart face button: live price check (tier-labeled cost) → stored stock → Email Vendor fallback; shared per-card popovers + price-forward live render
---
public/index.html | 42 +++++++++++++++++++++++++++++++++++++-----
1 file changed, 37 insertions(+), 5 deletions(-)
diff --git a/public/index.html b/public/index.html
index 0612f0f..219c581 100644
--- a/public/index.html
+++ b/public/index.html
@@ -209,6 +209,13 @@
.mailpop a.mgo { text-align:center; text-decoration:none; }
.mailpop .mstat { font-size:calc(10px*var(--fs)); line-height:1.4; }
.mailpop .mstat.ok { color:#7fc98a; } .mailpop .mstat.err { color:#e08b8b; }
+ /* smart FACE button — the ONE always-present per-card action, full-width row under Website.
+ Inherits livebtn amber / mailbtn blue from the rules above; stock chip tints in/out. */
+ .acts .im.face { flex-basis:100%; text-align:center; }
+ .im.face.in { color:#7fd0a0; } .im.face.out { color:#e08a8a; }
+ /* shared popovers (stock/live/mail) now sit at the END of .acts — one set per card,
+ used by both the face button and the Internal-panel copies */
+ .acts > .stockpop, .acts > .livepop, .acts > .mailpop { flex-basis:100%; width:100%; }
/* list-view compact link cluster */
td.acts-td { white-space:nowrap; }
td.acts-td a, td.acts-td .intbtn { display:inline-block; font-size:11px; font-weight:600; padding:3px 8px; border-radius:5px; text-decoration:none; border:1px solid var(--line); margin:0 4px 3px 0; background:transparent; color:var(--accent); cursor:pointer; font-family:inherit; }
@@ -746,8 +753,26 @@ function actButtonsHTML(r) {
// Email Vendor = the stock & price path EVERY vendor gets (Steve 2026-07-15: no dead-end
// buttons — live_capable vendors scrape, everyone can be emailed; also a confirmation path
// for the live ones). Opens a compose panel; "Create Gmail draft" NEVER auto-sends.
- const mail = `<button class="im mailbtn" type="button" data-sku="${esc(r.sku || '')}" data-vendor="${esc(r.vendor || '')}" data-mfr="${esc(r.mfr_number || '')}" data-pat="${esc(r.title || r.pattern || '')}" title="Compose a stock & price inquiry to the vendor rep — saves a Gmail DRAFT, never auto-sends">Email Vendor ✉</button><div class="mailpop" hidden></div>`;
- return `${web}<button class="intbtn" type="button">Internal ▾</button><div class="intpanel" hidden>${admin}${viewer}${item}${stock}${live}${mail}${push}${stockPopHTML(r.stock)}<div class="livepop" hidden></div></div>`;
+ const mail = `<button class="im mailbtn" type="button" data-sku="${esc(r.sku || '')}" data-vendor="${esc(r.vendor || '')}" data-mfr="${esc(r.mfr_number || '')}" data-pat="${esc(r.title || r.pattern || '')}" title="Compose a stock & price inquiry to the vendor rep — saves a Gmail DRAFT, never auto-sends">Email Vendor ✉</button>`;
+ // Smart FACE button — the ONE always-present per-card action (DTD verdict A, 2026-07-15).
+ // Priority: live check (tier-labeled with its real cost) → stored-stock chip → Email Vendor.
+ // Reuses livebtn / stockbtn / mailbtn classes so the delegated handleActsClick routes it
+ // with NO new event wiring; the popovers below are SHARED with the Internal-panel copies.
+ let face;
+ if (r.live_capable) {
+ const label = r.live_tier === 'DB_AUTHORITATIVE' ? 'Price ⚡ $0 · instant'
+ : r.live_tier === 'PUBLIC_STOCK' ? 'Check Live · ~$0.03'
+ : 'Check Live · ~$0.15–0.30';
+ face = `<button class="im livebtn face" type="button" data-sku="${esc(r.sku || '')}" title="${esc(r.live_reason || '')} · result cached 10 min">${label}</button>`;
+ } else if (r.stock) {
+ const inS = !!r.stock.in_stock;
+ face = `<button class="im stockbtn face ${inS ? 'in' : 'out'}" type="button" title="stored snapshot${r.stock.as_of ? ' · as of ' + esc(r.stock.as_of) : ''}">Stock ${inS ? '✓' : '✗'} (stored)</button>`;
+ } else {
+ face = `<button class="im mailbtn face" type="button" data-sku="${esc(r.sku || '')}" data-vendor="${esc(r.vendor || '')}" data-mfr="${esc(r.mfr_number || '')}" data-pat="${esc(r.title || r.pattern || '')}" title="Compose a stock & price inquiry to the vendor rep — saves a Gmail DRAFT, never auto-sends">Email Vendor ✉</button>`;
+ }
+ // Shared popovers (stock / live / mail) sit at the END of the .acts container — ONE set per
+ // card, resolved by closest('.acts, td.acts-td') from face AND panel buttons alike.
+ return `${web}${face}<button class="intbtn" type="button">Internal ▾</button><div class="intpanel" hidden>${admin}${viewer}${item}${stock}${live}${mail}${push}</div>${stockPopHTML(r.stock)}<div class="livepop" hidden></div><div class="mailpop" hidden></div>`;
}
// Delegated within an .acts container (which stops propagation so the card's own
@@ -756,7 +781,7 @@ function handleActsClick(e) {
const intbtn = e.target.closest('.intbtn');
if (intbtn) { const p = intbtn.parentElement.querySelector('.intpanel'); if (p) { p.hidden = !p.hidden; intbtn.textContent = p.hidden ? 'Internal ▾' : 'Internal ▴'; } return; }
const sb = e.target.closest('.stockbtn');
- if (sb) { const pop = sb.closest('.intpanel').querySelector('.stockpop'); if (pop) pop.hidden = !pop.hidden; return; }
+ if (sb) { const pop = sb.closest('.acts, td.acts-td').querySelector('.stockpop'); if (pop) pop.hidden = !pop.hidden; return; }
const lb = e.target.closest('.livebtn');
if (lb) { fireLiveCheck(lb); return; }
const mb = e.target.closest('.mailbtn');
@@ -821,6 +846,13 @@ function renderLive(pop, d) {
if (d.rate_limited) { pop.innerHTML = `<span class="sdot out">●</span> ${esc(d.reason || 'rate limited')}${d.retry_after_s ? ` · retry in ${d.retry_after_s}s` : ''}`; return; }
if (d.live_available === false) { pop.innerHTML = `<span class="sline mut">🔒 ${esc(d.reason || 'live check unavailable for this vendor')}</span>`; return; }
if (!d.ok) { pop.innerHTML = `<span class="sdot out">●</span> ${esc(d.reason || 'live scrape failed')}${d.cost_usd ? ` · <b class="lcost">$${Number(d.cost_usd).toFixed(2)} live</b>` : ''}`; return; }
+ // DB_AUTHORITATIVE returns a price with no availability read — lead with the price,
+ // never "Availability: unknown" (frontend iteration 2 of the Phase-1 plan).
+ if (d.in_stock == null && d.price != null) {
+ const pcost = d.cached ? 'cached · $0' : '$' + Number(d.cost_usd || 0).toFixed(2);
+ pop.innerHTML = `<span class="sdot in">●</span> Authoritative price <b>$${Number(d.price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</b><div class="sline mut">as of ${esc(fmtWhen(d.as_of))} · <b class="lcost">${pcost}</b></div>`;
+ return;
+ }
const dot = d.in_stock === true ? '<span class="sdot in">●</span> In stock (live)'
: d.in_stock === false ? '<span class="sdot out">●</span> Out of stock (live)'
: `<span class="sdot">●</span> Availability: ${esc(d.availability || 'unknown')}`;
@@ -833,7 +865,7 @@ function renderLive(pop, d) {
}
function fireLiveCheck(lb) {
if (lb.dataset.busy === '1') return; // client debounce — no double-fire
- const panel = lb.closest('.intpanel'); const pop = panel.querySelector('.livepop');
+ const pop = lb.closest('.acts, td.acts-td').querySelector('.livepop');
const sku = lb.dataset.sku;
pop.hidden = false; pop.className = 'livepop';
pop.innerHTML = '<span class="lspin">◌</span> Checking live vendor portal…';
@@ -868,7 +900,7 @@ function inquiryDefaults(d) { // d = { vendor, sku, mfr, pat } (all strings, pos
}
const CONTACT_CACHE = {}; // vendor → {email} (session cache so repeat opens don't refetch)
function toggleMailPop(mb) {
- const pop = mb.parentElement.querySelector('.mailpop');
+ const pop = mb.closest('.acts, td.acts-td').querySelector('.mailpop');
if (!pop) return;
if (!pop.hidden) { pop.hidden = true; return; }
pop.hidden = false;
← f9158ab Kravet family gets a $0 authoritative-price live check (krav
·
back to All Designerwallcoverings
·
coverage hot-reload: re-read live-stock-coverage.json on mti add8bb3 →