← Commit history

Register adom-stock: live distributor stock and price for hardware pages (adom/wiki#186)

Colby Knox ·47527fa63f ·28d ago ·parent 4a95679
3 files changed +60−1
README.md+2−1
@@ -45,7 +45,7 @@ Registry caps: 32 widgets, 12 attrs each. A malformed entry is dropped, never br  ## Current widgets -Five load from adom/component-widgets and take a JSON `src`; adom-symbol-variants loads from adom/adom-symbol and takes a component page.json `src`:+Five load from adom/component-widgets and take a JSON `src`; adom-stock loads from this page and takes a stock route `src`; adom-symbol-variants loads from adom/adom-symbol and takes a component page.json `src`:  | tag | shows | |---|---|@@ -56,3 +56,4 @@ Five load from adom/component-widgets and take a JSON `src`; adom-symbol-variant | `::adom-symbol-variants{src="..."}` | alternate schematic-symbol layouts (2-column, square, grouped, chip outline), generated in the browser | | `::adom-footprint-variants{src="..."}` | footprint-variant family (baseline, heatsink vias, 300 µm solder balls, rounded pads), parsed live from the page's `.kicad_mod` files; takes an adom-footprint `variants.json` `src` — loads from john/footprint-widgets | | `::adom-table{src="..."}` | any JSON array as a filterable, sortable table — generic, not component-specific; `columns` `heading` `filter` `facet` `sort` `dim` `mark` `colorby` |+| `::adom-stock{src="/api/pages/OWNER/SLUG/stock"}` | live stock and price at Mouser, DigiKey and LCSC for a hardware page, from the wiki's cached vendor lookups (adom/wiki#186); component pages show it in their sidebar by themselves; `heading` |
widgets.json+11
@@ -88,5 +88,16 @@       "src",       "mpn"     ]+  },+  {+    "name": "adom-stock",+    "mode": "frontend",+    "script": "https://wiki.adom.inc/adom/widgets/render/widgets/stock-widget.js",+    "element": "adom-stock",+    "attrs": [+      "src",+      "heading"+    ],+    "height": 168   } ]
widgets/stock-widget.jsadded+47
@@ -0,0 +1,47 @@+/* <adom-stock>: live distributor stock and price for a component page (adom/wiki#186). MIT.+ * Data: an inline <script type="application/json"> child first, else fetch(src).+ * 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 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();+  class AdomStock extends HTMLElement {+    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>`;+      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'; } }+      const src = this.getAttribute('src');+      if (!data && src) { try { const r = await fetch(src, { credentials: 'include' }); data = await r.json(); if (!r.ok) err = data && data.error ? data.error : `stock route answered ${r.status}`; } catch (e) { err = 'could not load stock'; } }+      this.render(data, err);+    }+    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 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">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>`;+      });+      when.textContent = (d.stale ? 'refreshing, ' : '') + ago(d.checked_at);+      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>`;+    }+  }+  if (!customElements.get('adom-stock')) customElements.define('adom-stock', AdomStock);+})();