← Commit history

adom-stock: vendor / qty / price pills on one line (Ray)

Colby Knox ·e56dfa3bd2 ·28d ago ·parent 1f74387
1 file changed +23−24
widgets/stock-widget.js+23−24
@@ -3,14 +3,12 @@  * src is the page's stock route: /api/pages/OWNER/SLUG/stock */ (() => {   const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));-  const CSS = `:host{display:block;color:#e6edf3;font:13px/1.45 system-ui,sans-serif;--accent:#00b8b0;--muted:#8b949e;--border:#30363d;--bg:#0d1117;--surface:#161b22}-*{box-sizing:border-box}.wrap{background:var(--bg);border:1px solid var(--border);border-radius:10px;overflow:hidden}-.head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:8px 12px;background:var(--surface);border-bottom:1px solid var(--border)}-.head h3{margin:0;font-size:13px;font-weight:600;letter-spacing:.02em}.head .when{color:var(--muted);font-size:11px}-table{width:100%;border-collapse:collapse}th,td{padding:7px 12px;text-align:left;white-space:nowrap}th{color:var(--muted);font-weight:500;font-size:11px;text-transform:uppercase;letter-spacing:.06em}-tr+tr td{border-top:1px solid var(--border)}td.num{font-variant-numeric:tabular-nums;text-align:right}th.num{text-align:right}-.in{color:var(--accent)}.out{color:var(--muted)}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}-.note{padding:10px 12px;color:var(--muted)}.pn{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--muted)}`;+  const CSS = `:host{display:block;color:#e6edf3;font:12px/1.3 system-ui,sans-serif;--accent:#00b8b0;--muted:#8b949e;--border:#30363d;--bg:#0d1117;--surface:#161b22}+*{box-sizing:border-box}.wrap{display:flex;flex-wrap:wrap;align-items:center;gap:6px}+.pill{display:inline-flex;align-items:center;gap:6px;padding:3px 9px;border:1px solid var(--border);border-radius:999px;background:var(--surface);color:#e6edf3;text-decoration:none;white-space:nowrap;font-variant-numeric:tabular-nums}+a.pill:hover{border-color:var(--accent)}.pill .v{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.04em}.pill .q{color:var(--accent)}.pill .q.out{color:var(--muted)}.pill .p{color:#e6edf3}+.pill.dim{color:var(--muted)}.pill.dim .q{color:var(--muted)}+.when{flex-basis:100%;color:var(--muted);font-size:11px;margin-top:2px}.when a{color:var(--accent);text-decoration:none}.note{color:var(--muted)}`;   const VENDORS = ['mouser', 'digikey', 'lcsc'];   const ago = iso => { if (!iso) return ''; const m = Math.max(0, (Date.now() - Date.parse(iso)) / 60000); return m < 90 ? `checked ${Math.round(m)} min ago` : m < 2880 ? `checked ${Math.round(m / 60)} h ago` : `checked ${Math.round(m / 1440)} d ago`; };   const num = n => Number(n).toLocaleString();@@ -18,7 +16,7 @@ tr+tr td{border-top:1px solid var(--border)}td.num{font-variant-numeric:tabular-     constructor() { super(); this.attachShadow({ mode: 'open' }); }     async connectedCallback() {       const root = this.shadowRoot;-      root.innerHTML = `<style>${CSS}</style><div class="wrap"><div class="head"><h3>${esc(this.getAttribute('heading') || 'Stock and price')}</h3><span class="when">loading</span></div><div class="note">Asking the distributors</div></div>`;+      root.innerHTML = `<style>${CSS}</style><div class="wrap"><span class="note">checking the distributors</span><span class="when"></span></div>`;       let data = null, err = null;       const inline = this.querySelector('script[type="application/json"]');       if (inline) { try { data = JSON.parse(inline.textContent); } catch (e) { err = 'bad inline data'; } }@@ -41,23 +39,24 @@ tr+tr td{border-top:1px solid var(--border)}td.num{font-variant-numeric:tabular-       catch (e) { return { data: null, err: 'could not load stock' }; }     }     render(d, err) {-      const root = this.shadowRoot, wrap = root.querySelector('.wrap'), when = root.querySelector('.when');-      const body = wrap.querySelector('.note') || wrap.querySelector('table');-      if (err || !d) { when.textContent = ''; body.outerHTML = `<div class="note">${esc(err || 'no data')}</div>`; return; }-      if (d.unavailable || d.skipped) { when.textContent = ''; body.outerHTML = `<div class="note">${esc(d.unavailable || d.skipped)}</div>`; return; }+      const root = this.shadowRoot, wrap = root.querySelector('.wrap');+      const note = (t) => { wrap.innerHTML = `<span class="note">${esc(t)}</span>`; };+      if (err || !d) return note(err || 'no data');+      if (d.unavailable || d.skipped) return note(d.unavailable || d.skipped);       const anyRow = Object.values(d.vendors || {}).some(Boolean);-      if (!anyRow && (d.login_required || d.signed_in === false)) { when.textContent = ''; body.outerHTML = `<div class="note">Sign in to check live stock and price (lookups are counted per person).</div>`; return; }-      const budget = Object.values(d.vendors || {}).some(r => r && r.error && /budget/.test(r.error));-      const rows = VENDORS.filter(v => d.vendors && d.vendors[v] !== undefined).map(v => {-        const r = d.vendors[v]; const label = r && r.label || v;-        if (!r) return `<tr><td>${esc(label)}</td><td class="out" colspan="3">not asked yet</td><td></td></tr>`;-        if (r.error) return `<tr><td>${esc(label)}</td><td class="out" colspan="3">${/budget/.test(r.error) ? 'daily lookups used up, try tomorrow' : /PARTS_API_KEY/.test(r.error) ? 'lookups not configured' : 'lookup failed'}</td><td></td></tr>`;-        if (!r.found) return `<tr><td>${esc(label)}</td><td class="out" colspan="3">not listed</td><td></td></tr>`;-        const link = r.product_url ? `<a href="${esc(r.product_url)}" target="_blank" rel="noopener">open</a>` : '';-        return `<tr><td>${esc(label)}<br><span class="pn">${esc(r.part || r.mpn || '')}</span></td><td class="num ${r.stock > 0 ? 'in' : 'out'}">${r.stock > 0 ? num(r.stock) + ' in stock' : 'out of stock'}</td><td class="num">${esc(r.unit_price || '')}</td><td class="num">${esc(r.price_100 || '')}</td><td>${link}</td></tr>`;+      if (!anyRow && (d.login_required || d.signed_in === false)) return note('Sign in to check live stock and price (lookups are counted per person).');+      const num = n => Number(n).toLocaleString();+      const pills = VENDORS.filter(v => d.vendors && d.vendors[v] !== undefined).map(v => {+        const r = d.vendors[v]; const label = (r && r.label) || v;+        if (!r) return `<span class="pill dim"><span class="v">${esc(label)}</span><span class="q">not asked</span></span>`;+        if (r.error) return `<span class="pill dim" title="${esc(r.error)}"><span class="v">${esc(label)}</span><span class="q">${/budget/.test(r.error) ? 'daily lookups used' : /PARTS_API_KEY/.test(r.error) ? 'not configured' : 'lookup failed'}</span></span>`;+        if (!r.found) return `<span class="pill dim"><span class="v">${esc(label)}</span><span class="q">not listed</span></span>`;+        const inner = `<span class="v">${esc(label)}</span><span class="q ${r.stock > 0 ? '' : 'out'}">${r.stock > 0 ? num(r.stock) : 'out of stock'}</span>${r.unit_price ? `<span class="p">${esc(r.unit_price)}</span>` : ''}`;+        const title = `${label}: ${r.part || r.mpn || ''}${r.price_100 ? `, ${r.price_100} at 100` : ''}${r.lead_time ? `, lead ${r.lead_time}` : ''}`;+        return r.product_url ? `<a class="pill" href="${esc(r.product_url)}" target="_blank" rel="noopener" title="${esc(title)}">${inner}</a>` : `<span class="pill" title="${esc(title)}">${inner}</span>`;       });-      when.innerHTML = esc(ago(d.checked_at)) + (d.checked_at && d.signed_in !== false && !d.login_required ? ' <a class="refresh" href="#" title="Ask the distributors again (counts against the daily lookup budget)">refresh</a>' : d.login_required ? ' (sign in to refresh)' : '');-      body.outerHTML = `<table><thead><tr><th>Distributor</th><th class="num">Stock</th><th class="num">Unit</th><th class="num">At 100</th><th></th></tr></thead><tbody>${rows.join('')}</tbody></table>`;+      const refresh = d.checked_at && d.signed_in !== false && !d.login_required ? ' <a class="refresh" href="#" title="Ask the distributors again (counts against the daily lookup budget)">refresh</a>' : d.login_required ? ' (sign in to refresh)' : '';+      wrap.innerHTML = pills.join('') + `<span class="when">${esc(ago(d.checked_at))}${refresh}</span>`;     }   }   if (!customElements.get('adom-stock')) customElements.define('adom-stock', AdomStock);