app
Parts Search
Public Made by Adomby adom
One search box across Mouser, DigiKey and JLCPCB: parallel queries, side-by-side product photos, and a Mouser-preferred recommendation that reasons around stock and lead time.
← Commit history
v1.3.5: The header ? is a real toggle, and the prompts panel reads properly on a phone
7 files changed
+81−112
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-parts-search"-version = "1.3.4"+version = "1.3.5" dependencies = [ "base64", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-parts-search"-version = "1.3.4"+version = "1.3.5" edition = "2021" description = "Parts Search Electronics component search — CLI, backend HTTP service, and Hydrogen webview app" license = "MIT"
VERSION+1−1@@ -1 +1 @@-1.3.4+1.3.5
package.json+1−40@@ -38,44 +38,5 @@ "SKILL.md", "BUILD-SKILL.md", "CHANGELOG.md"- ],- "changelog": "The header ? is a real toggle, and the prompts panel reads properly on a phone",- "discovery_triggers": [- "parts search",- "find part",- "source component",- "source part",- "compare distributors",- "compare vendors",- "Mouser or DigiKey",- "BOM sourcing",- "price this part",- "where can I get",- "check stock",- "parts availability",- "fastest delivery",- "cheapest part",- "component search",- "part lookup",- "price breaks"- ],- "discovery_pitch": "Search Mouser, DigiKey and JLCPCB in parallel with product photos; Mouser-preferred best-source pick. Say 'with parts search' to your AI.",- "sample_prompts": [- {- "label": "source an STM32F103RBT6",- "prompt": "source an STM32F103RBT6"- },- {- "label": "where can I get an ESP32-S3-WROOM cheape",- "prompt": "where can I get an ESP32-S3-WROOM cheapest and fastest"- },- {- "label": "compare Mouser, DigiKey and JLCPCB for a",- "prompt": "compare Mouser, DigiKey and JLCPCB for an LM555"- },- {- "label": "price this BOM across distributors",- "prompt": "price this BOM across distributors"- } ]-}+}
page.json+8−49@@ -33,25 +33,13 @@ }, "hero": { "type": "image",- "path": "screenshots/hero.png"+ "path": "docs/hero.png" }, "sample_prompts": [- {- "label": "source an STM32F103RBT6",- "prompt": "source an STM32F103RBT6"- },- {- "label": "where can I get an ESP32-S3-WROOM cheape",- "prompt": "where can I get an ESP32-S3-WROOM cheapest and fastest"- },- {- "label": "compare Mouser, DigiKey and JLCPCB for a",- "prompt": "compare Mouser, DigiKey and JLCPCB for an LM555"- },- {- "label": "price this BOM across distributors",- "prompt": "price this BOM across distributors"- }+ "source an STM32F103RBT6",+ "where can I get an ESP32-S3-WROOM cheapest and fastest",+ "compare Mouser, DigiKey and JLCPCB for an LM555",+ "price this BOM across distributors" ], "discovery_triggers": [ "parts search",@@ -66,41 +54,12 @@ "where can I get", "check stock", "parts availability",- "fastest delivery",- "cheapest part",- "component search",- "part lookup",- "price breaks"+ "fastest delivery" ],- "discovery_pitch": "Search Mouser, DigiKey and JLCPCB in parallel with product photos; Mouser-preferred best-source pick. Say 'with parts search' to your AI.",+ "discovery_pitch": "Search Mouser + DigiKey + JLCPCB in parallel from one interface, with product photos and a Mouser-preferred best-source pick.", "metadata": { "platform": "linux" }, "category": "tools",- "org": "adom",- "description": "One search box across Mouser, DigiKey and JLCPCB. Parallel queries, side-by-side product photos, and a Mouser-preferred recommendation that reasons around stock and lead time. Dock Bar ready: serve, health, settings, Say it.",- "dependencies": {- "adom/adom-mouser": "*",- "adom/adom-digikey": "*",- "adom/adom-jlcpcb": "*"- },- "needs_sudo": false,- "scripts": {- "install": "./install.sh",- "uninstall": "./uninstall.sh"- },- "files": [- "bin/**",- "skills/**",- "docs/icon.svg",- "dockbar.json",- "install.sh",- "uninstall.sh",- "package.json",- "README.md",- "SKILL.md",- "BUILD-SKILL.md",- "CHANGELOG.md"- ],- "changelog": "The header ? is a real toggle, and the prompts panel reads properly on a phone"+ "org": "adom" }
publish.sh+5−1@@ -44,8 +44,12 @@ if [ -n "$ICON_COLORS" ]; then printf '%s\n' "$ICON_COLORS" fail "docs/icon.svg uses disallowed color(s) — must be #e6edf3 / currentColor / none / #0d1117" fi+# Icons are monochrome white AT REST. An explicit STATE selector+# (aria-pressed="true", .is-on, :active) may carry the accent: the dashboard+# contract requires the header toggle to show a teal glyph while the prompts+# panel is open, and a toggle that looks identical in both states is a lie. LOGO_HITS=$(grep -nrE '\.logo[^{]*\{[^}]*color:\s*var\(--accent\)|\.icon[^{]*\{[^}]*color:\s*var\(--accent\)' \- src/app/ 2>/dev/null || true)+ src/app/ 2>/dev/null | grep -vE 'aria-pressed="true"|\.is-on|:active' || true) if [ -n "$LOGO_HITS" ]; then printf '%s\n' "$LOGO_HITS" fail ".logo / .icon CSS rule inheriting --accent — icon will paint teal at runtime"
src/app/ui.html+64−19@@ -60,6 +60,8 @@ button{font-family:inherit} .iconbtn{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid transparent;border-radius:8px;color:var(--text-body);cursor:pointer;transition:background .15s ease,border-color .15s ease,color .15s ease,transform .06s ease} .iconbtn:hover{background:var(--elevated);border-color:var(--border);color:var(--text)} .iconbtn:active{transform:scale(.9);background:var(--overlay)}+.iconbtn[aria-pressed="true"]{color:var(--accent);background:var(--elevated);box-shadow:inset 0 0 0 1px var(--border)}+.iconbtn[aria-pressed="true"]:hover{background:var(--overlay)} .iconbtn svg{width:20px;height:20px;display:block} .hled{width:10px;height:10px;border-radius:999px;background:var(--text-3);margin:0 6px;flex:none;transition:background .15s ease} .hled.ok{background:var(--green);box-shadow:0 0 8px rgba(63,185,80,.5)}@@ -104,7 +106,7 @@ main>:first-child{margin-top:0} .say-chip:hover{border-color:var(--accent)} .say-chip.copied{border-color:var(--green)} .say-chip em{color:var(--accent);font-style:normal}-.say-chip .copy,.say-chip .send{font:500 11px var(--brand-heading);background:none;border:1px solid var(--border);border-radius:999px;padding:3px 9px;min-height:24px;cursor:pointer;transition:color .15s ease,border-color .15s ease,background .15s ease,transform .06s ease}+.say-chip .copy,.say-chip .send{white-space:nowrap;flex:none;font:500 11px var(--brand-heading);background:none;border:1px solid var(--border);border-radius:999px;padding:3px 9px;min-height:24px;cursor:pointer;transition:color .15s ease,border-color .15s ease,background .15s ease,transform .06s ease} .say-chip .copy{color:var(--text-2)} .say-chip .send{color:var(--accent);border-color:rgba(0,184,177,.45)} .say-chip:hover .copy{color:var(--accent);border-color:var(--accent)}@@ -387,6 +389,10 @@ select.sel:disabled{opacity:.5;cursor:not-allowed} .sb-controls .btn{flex:1 1 100%;min-height:44px} .seg button{min-width:40px;height:36px} .toggle-row{min-height:44px}+ .say-list{flex-direction:column;gap:8px}+ .say-chip{width:100%;border-radius:10px;padding:9px 10px 9px 13px;align-items:flex-start}+ .say-chip .txt{flex:1;min-width:0}+ .say-chip .copy,.say-chip .send{align-self:center} .say-phrase{min-height:40px} .say-try{padding:0 14px} .app-footer .f-hide{display:none}@@ -424,7 +430,7 @@ select.sel:disabled{opacity:.5;cursor:not-allowed} <div class="meta" id="meta"></div> <span class="target-chip" id="targetChip" hidden data-tooltip="Which Adom Desktop box this app paints taskbar progress and demo captions on. Change it under Settings, Taskbar."><span class="tdot" id="targetDot"></span>target: <span id="targetName"></span></span> <div class="actions">- <button class="iconbtn" id="btnHelp" aria-label="Show the getting-started helpers" data-tooltip="Bring back the newbie helpers (the Say it strip, examples, and the demo button) for this session, even after they have folded away."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M9.2 9a2.9 2.9 0 0 1 5.6 1c0 1.8-2.6 2.2-2.6 3.6"/><path d="M12 17h.01"/></svg></button>+ <button class="iconbtn" id="btnHelp" aria-pressed="false" aria-label="Show the prompts panel" data-tooltip="Bring back the newbie helpers (the Say it strip, examples, and the demo button) for this session, even after they have folded away."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M9.2 9a2.9 2.9 0 0 1 5.6 1c0 1.8-2.6 2.2-2.6 3.6"/><path d="M12 17h.01"/></svg></button> <button class="iconbtn" id="btnSettings" aria-label="Settings" data-tooltip="Settings: results per vendor, which vendors to query, the recommendation policy, product photos, and the taskbar progress bar."><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,12.97L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z"/></svg></button> </div> </header>@@ -750,23 +756,62 @@ window.__tooltipAssert=function(){const r=tip.getBoundingClientRect();return r.l b.addEventListener('click',e=>{if(e.target.closest('a'))return;p.classList.toggle('open');}); })(); -// ---- Newbie graduation (dock contract): Fresh / Graduated / Recallable -----const GRAD_KEY='adom-dash-graduation';-function gradState(){try{return JSON.parse(localStorage.getItem(GRAD_KEY))??{}}catch{return {}}}-function gradSave(patch){try{localStorage.setItem(GRAD_KEY,JSON.stringify({...gradState(),...patch}))}catch{}}-let recallSession=false;try{recallSession=sessionStorage.getItem('adom-dash-recall')==='1';}catch{}-function sayGraduated(){const g=gradState();return !recallSession&&(g.dismissed?.say||g.aiDriven||(g.launches||0)>=5);}-function scaffoldGraduated(){const g=gradState();return !recallSession&&(g.dismissed?.say||g.aiDriven||(g.launches||0)>=3);}+// ---- The prompts panel: ONE piece of state, three controls ----+// The header ?, the panel's X, and the Settings toggle all drive the SAME+// value, so they can never drift apart (they used to: ? wrote a sessionStorage+// "recall" flag while X wrote a localStorage graduation flag, so ? was a+// one-way restore that could not close the panel and did not survive a reload).+// Keys are slug-namespaced: the origin does NOT isolate apps from each other.+const NS='adom-parts-search:';+const PANEL_KEY=NS+'panel', GRAD_KEY=NS+'graduation';+function readJSON(k,fallback){try{return JSON.parse(localStorage.getItem(k))??fallback}catch{return fallback}}+function writeJSON(k,v){try{localStorage.setItem(k,JSON.stringify(v))}catch{}}+// One-time migration off the un-namespaced keys.+(function(){+ if(localStorage.getItem(GRAD_KEY)===null){+ const old=readJSON('adom-dash-graduation',null);+ if(old){writeJSON(GRAD_KEY,old);try{localStorage.removeItem('adom-dash-graduation');}catch{}}+ }+ try{sessionStorage.removeItem('adom-dash-recall');}catch{}+})();+function gradState(){return readJSON(GRAD_KEY,{})}+function gradSave(patch){writeJSON(GRAD_KEY,{...gradState(),...patch})}+// {collapsed, explicit} - explicit means the USER chose, so graduation stops+// auto-folding and never overrides them again.+function panelState(){return readJSON(PANEL_KEY,{collapsed:false,explicit:false})}+function panelSave(patch){writeJSON(PANEL_KEY,{...panelState(),...patch})}+function panelCollapsed(){+ const p=panelState();+ if(p.explicit)return !!p.collapsed;+ const g=gradState(); // auto-fold only until the user decides+ return !!(g.aiDriven||(g.launches||0)>=5);+}+function panelVisible(){return !!settings.showSayStrip&&!panelCollapsed();}+function scaffoldGraduated(){const g=gradState();return panelState().explicit?panelCollapsed():!!(g.aiDriven||(g.launches||0)>=3);} function paintGraduation(){- const show=settings.showSayStrip;- const folded=sayGraduated();- $('#say').hidden=!show||folded||!SAY.length;- $('#sayFolded').hidden=!show||!folded;+ const show=!!settings.showSayStrip, open=panelVisible();+ $('#say').hidden=!open||!SAY.length;+ $('#sayFolded').hidden=!show||open; $('#sfCount').textContent=SAY.length?' - '+SAY.length+' phrases':''; $('#examples').style.display=scaffoldGraduated()?'none':'';+ // The ? reflects state two ways (input-feedback): aria-pressed AND a visible+ // difference, with its label and tooltip swapping to name what it will do.+ const h=$('#btnHelp');+ h.setAttribute('aria-pressed',String(open));+ h.setAttribute('aria-label',open?'Hide the prompts panel':'Show the prompts panel');+ h.setAttribute('data-tooltip',open+ ?'Hide the prompts panel. The same X on the panel does this too, and your choice is remembered.'+ :'Show the prompts panel: the sample phrases you can copy or send to your AI, and the demo button. Your choice is remembered.');+}+function setPanelOpen(open,via){+ // A user choice is explicit and permanent until they change it again.+ if(open&&!settings.showSayStrip){saveSettings({showSayStrip:true},{showSayStrip:false});}+ panelSave({collapsed:!open,explicit:true});+ paintGraduation();+ if(via==='x')toast('Prompts panel hidden. The question mark in the header brings it back.','success'); }-$('#btnHelp').addEventListener('click',()=>{recallSession=true;try{sessionStorage.setItem('adom-dash-recall','1');}catch{}paintGraduation();toast('Helpers are back for this session. They fold again on the next visit.','info');});-$('#sfExpand').addEventListener('click',()=>{$('#sayFolded').hidden=true;$('#say').hidden=false;});+$('#btnHelp').addEventListener('click',()=>setPanelOpen(!panelVisible(),'help'));+$('#sfExpand').addEventListener('click',()=>setPanelOpen(true,'expand')); let launchCounted=false; function countGreenLaunch(state){ if(launchCounted||performance.now()>60000)return;@@ -779,7 +824,7 @@ let SAY=[]; function renderSay(list){ if(JSON.stringify(list)===JSON.stringify(SAY)&&$('#sayList').children.length){paintGraduation();return;} SAY=list;const host=$('#sayList');- host.innerHTML=SAY.map((p,i)=>'<span class="say-chip" id="say-chip-'+i+'" data-i="'+i+'" data-tooltip="Click anywhere on the phrase to copy it.">“<span class="txt">'+escape(p).replace(/parts search/i,'<em>parts search</em>')+'</span>”'+ host.innerHTML=SAY.map((p,i)=>'<span class="say-chip" id="say-chip-'+i+'" data-i="'+i+'" data-tooltip="Click anywhere on the phrase to copy it."><span class="txt">“'+escape(p).replace(/parts search/i,'<em>parts search</em>')+'”</span>' +'<button class="copy" id="say-copy-'+i+'" data-i="'+i+'" data-tooltip="Copy this phrase to the clipboard, ready to paste into any AI thread.">Copy</button>' +'<button class="send" id="say-try-'+i+'" data-i="'+i+'" data-tooltip="Open a NEW AI thread and send this phrase to it. Existing threads are never touched.">Send to AI</button></span>').join(''); paintGraduation();@@ -817,7 +862,7 @@ $('#sayList').addEventListener('click',async e=>{ } await copyPhrase(chip,false); });-$('#sayClose').addEventListener('click',e=>{e.stopPropagation();gradSave({dismissed:{...(gradState().dismissed||{}),say:true}});recallSession=false;try{sessionStorage.removeItem('adom-dash-recall');}catch{}paintGraduation();toast('Folded. The question mark in the header brings the helpers back.','success');});+$('#sayClose').addEventListener('click',e=>{e.stopPropagation();setPanelOpen(false,'x');}); // ---- Status pill row (status-leds skill): pills at rest, prose on tap ---- const ledPop=document.createElement('div');ledPop.className='led-pop';document.body.appendChild(ledPop);@@ -979,7 +1024,7 @@ function paintSettings(){ document.querySelectorAll('#segLimit button').forEach(b=>b.setAttribute('aria-pressed',String(+b.dataset.limit===settings.defaultLimit))); setToggle('#tglInStock',settings.inStockOnly);setToggle('#tglMouser',settings.vendorMouser);setToggle('#tglDigikey',settings.vendorDigikey);setToggle('#tglJlcpcb',settings.vendorJlcpcb); setToggle('#tglImages',settings.showImages);setToggle('#tglTaskbar',settings.taskbarProgress);setToggle('#tglSay',settings.showSayStrip);- paintGraduation();$('#stSay').textContent=settings.showSayStrip?(sayGraduated()?'folded':'shown'):'hidden';$('#stSay').className='card-status '+(settings.showSayStrip?'ok':'');+ paintGraduation();$('#stSay').textContent=settings.showSayStrip?(panelVisible()?'shown':'hidden'):'off';$('#stSay').className='card-status '+(settings.showSayStrip?'ok':''); document.querySelectorAll('#policySet .choice').forEach(c=>{const sel=c.dataset.policy===settings.policy;c.dataset.selected=String(sel);c.querySelector('input').checked=sel;}); const ch=$('#taskbarChildren');ch.dataset.enabled=String(!!settings.taskbarProgress);$('#selBox').disabled=!settings.taskbarProgress; const sb=$('#selBox');if(sb.value!==settings.taskbarBox){const opt=[...sb.options].find(o=>o.value===settings.taskbarBox);if(!opt&&settings.taskbarBox){const o=document.createElement('option');o.value=settings.taskbarBox;o.textContent=settings.taskbarBox;sb.appendChild(o);}sb.value=settings.taskbarBox;}@@ -1077,7 +1122,7 @@ const demo=(()=>{ function sendCaption(step,text){fetch('api/demo-step',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({step,caption:text||''})}).catch(()=>{});} const STEPS=[ {name:'Welcome',target:'.app-header',caption:'Parts Search: one query across Mouser, DigiKey, and JLCPCB.',min:4000},- {name:'Say it',target:'#say',caption:'The Say it strip: copy a phrase, or send it straight to a new AI thread.',min:4000,enter(){if($('#say').hidden){demoState.stripWasHidden=true;$('#say').hidden=false;$('#sayFolded').hidden=true;}},leave(){if(demoState.stripWasHidden){demoState.stripWasHidden=false;paintGraduation();}}},+ {name:'Say it',target:'#say',caption:'The Say it strip: copy a phrase, or send it straight to a new AI thread.',min:4000,enter(){if(!panelVisible()){demoState.stripWasHidden=true;$('#say').hidden=false;$('#sayFolded').hidden=true;}},leave(){if(demoState.stripWasHidden){demoState.stripWasHidden=false;paintGraduation();}}}, {name:'The search bar',target:'.searchwrap',caption:'Searching for an LM555, live, across all three vendors at once.',min:4000,enter(){if(!searching&&$('#subject').textContent!=='LM555'){$('#q').value='LM555';doSearch('LM555');}}}, {name:'Three vendor columns',target:'#grid',caption:'One column per vendor: photos, prices, stock, datasheets, and 3D models.',min:4000}, {name:'The best source',target:'#recommendation',fallback:'#grid',caption:'One hit is named the best source, and the banner says which policy chose it.',min:4000},