app
Widget Registry
Public Made by Adomby adom
The reviewed registry of widgets renderable from plain markdown on any wiki page
← Commit history
adom-stock: vendor / qty / price pills on one line (Ray)
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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);