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.1.2: Try it buttons return as copy-to-clipboard with a confirming toast
7 files changed
+44−101
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-parts-search"-version = "1.1.1"+version = "1.1.2" dependencies = [ "base64", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-parts-search"-version = "1.1.1"+version = "1.1.2" edition = "2021" description = "Parts Search Electronics component search — CLI, backend HTTP service, and Hydrogen webview app" license = "MIT"
VERSION+1−1@@ -1 +1 @@-1.1.1+1.1.2
package.json+1−40@@ -38,44 +38,5 @@ "SKILL.md", "BUILD-SKILL.md", "CHANGELOG.md"- ],- "changelog": "Try it buttons return as copy-to-clipboard with a confirming toast",- "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": "Try it buttons return as copy-to-clipboard with a confirming toast"+ "org": "adom" }
src/app/ui.html+31−8@@ -91,7 +91,10 @@ main{max-width:1320px;margin:0 auto;padding:16px 16px 40px} .say-phrase em{font-style:normal;color:var(--accent)} .say-phrase:hover{background:var(--overlay)} .say-phrase:active{transform:translateY(1px) scale(.98)}-.say-phrase:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}+.say-try{display:inline-flex;align-items:center;padding:0 12px;border:none;border-left:1px solid var(--border);background:none;color:var(--text-2);font:600 12px var(--brand-heading);cursor:pointer;transition:background .15s ease,color .15s ease,transform .06s ease}+.say-try:hover{background:var(--overlay);color:var(--accent)}+.say-try:active{transform:translateY(1px)}+.say-phrase:focus-visible,.say-try:focus-visible{outline:2px solid var(--accent);outline-offset:-2px} /* Search bar */ .searchwrap{position:relative;border-radius:14px;padding:1px;background:var(--border);transition:background .2s ease;box-shadow:0 12px 40px rgba(0,0,0,.35)}@@ -315,6 +318,7 @@ select.sel:disabled{opacity:.5;cursor:not-allowed} .seg button{min-width:40px;height:36px} .toggle-row{min-height:44px} .say-phrase{min-height:40px}+ .say-try{padding:0 14px} .app-footer .f-hide{display:none} .recommendation{padding:12px;flex-wrap:wrap} .examples{display:none}@@ -325,7 +329,7 @@ select.sel:disabled{opacity:.5;cursor:not-allowed} .settings-body{max-height:calc(100vh - 50px)} } @media (hover:none),(pointer:coarse){- .iconbtn,.btn{min-width:44px;min-height:44px}+ .iconbtn,.btn,.say-try{min-width:44px;min-height:44px} } </style> </head>@@ -660,14 +664,33 @@ let SAY=[]; function renderSay(list){ if(JSON.stringify(list)===JSON.stringify(SAY)&&$('#sayList').children.length)return; SAY=list;const host=$('#sayList');$('#say').hidden=!settings.showSayStrip;- host.innerHTML=SAY.map((p,i)=>'<span class="say-item"><button type="button" class="say-phrase" id="say-copy-'+i+'" data-i="'+i+'" data-tooltip="Copy this phrase to the clipboard, then paste it into any Adom AI thread."><span class="txt">'+escape(p).replace(/parts search/i,'<em>parts search</em>')+'</span></button></span>').join('');+ host.innerHTML=SAY.map((p,i)=>'<span class="say-item"><button type="button" class="say-phrase" id="say-copy-'+i+'" data-i="'+i+'" data-tooltip="Copy this phrase to the clipboard, then paste it into any Adom AI thread."><span class="txt">'+escape(p).replace(/parts search/i,'<em>parts search</em>')+'</span></button><button type="button" class="say-try" id="say-try-'+i+'" data-i="'+i+'" data-tooltip="Copy this phrase to the clipboard, ready to paste into any AI thread. Nothing is sent anywhere.">Try it</button></span>').join('');+}+function copyText(t){+ // The async clipboard API can hang forever in an unfocused window+ // ("document is not focused"), so: no focus = go straight to the+ // execCommand fallback while the click gesture is still live, and a+ // focused writeText still races an 800ms timer as a belt and braces.+ const fallback=()=>{try{const ta=document.createElement('textarea');ta.value=t;ta.style.cssText='position:fixed;opacity:0';document.body.appendChild(ta);ta.select();const ok=document.execCommand('copy');ta.remove();return ok;}catch{return false;}};+ return new Promise(res=>{+ if(!navigator.clipboard||!document.hasFocus()){res(fallback());return;}+ let done=false;+ const timer=setTimeout(()=>{if(!done){done=true;res(fallback());}},800);+ navigator.clipboard.writeText(t).then(+ ()=>{if(!done){done=true;clearTimeout(timer);res(true);}},+ ()=>{if(!done){done=true;clearTimeout(timer);res(fallback());}});+ }); } $('#sayList').addEventListener('click',async e=>{- const ph=e.target.closest('.say-phrase');- if(!ph)return;- const p=SAY[+ph.dataset.i],item=ph.closest('.say-item');try{await navigator.clipboard.writeText(p);}catch{}- item.classList.add('copied');const old=ph.innerHTML;ph.innerHTML='<span class="txt">copied</span>';setTimeout(()=>{item.classList.remove('copied');ph.innerHTML=old;},1200);- toast('Copied: '+p,'success');+ const ph=e.target.closest('.say-phrase'),tr=e.target.closest('.say-try');+ if(!ph&&!tr)return;+ const item=(ph||tr).closest('.say-item');+ const p=SAY[+(ph||tr).dataset.i];+ const ok=await copyText(p);+ const phEl=item.querySelector('.say-phrase');+ item.classList.add('copied');const old=phEl.innerHTML;phEl.innerHTML='<span class="txt">copied</span>';setTimeout(()=>{item.classList.remove('copied');phEl.innerHTML=old;},1200);+ if(!ok){toast('Could not reach the clipboard. Select the phrase and copy it by hand.','error');return;}+ toast(tr?'Copied to clipboard. Paste it into any AI thread.':'Copied: '+p,'success'); }); // ---- Rendering ----
src/cli.rs+1−1@@ -142,7 +142,7 @@ pub fn run_ui(cmd: UiCmd) -> Result<(), String> { } UiCmd::Click { id, value, port } => { let v = post_json(&format!("http://127.0.0.1:{}/api/click", port), serde_json::json!({"id": id, "value": value})).map_err(|_| not_running_hint(port))?;- if v.get("ok").and_then(|b| b.as_bool()) == Some(true) { crate::ok(format!("click queued for the page: {}", id)); crate::hint("ids: search, instock, settings, close-settings, restore, limit (with --value 3|5|10), policy (--value mouser|cheapest), say-close, say, say-copy-<n>, say-try-<n>"); Ok(()) }+ if v.get("ok").and_then(|b| b.as_bool()) == Some(true) { crate::ok(format!("click queued for the page: {}", id)); crate::hint("ids: search, instock, settings, close-settings, restore, limit (with --value 3|5|10), policy (--value mouser|cheapest), say-close, say, say-copy-<n>, say-try-<n>, say-try-<n>"); Ok(()) } else { Err(format!("click refused: {}", v)) } } UiCmd::Search { keyword, port } => {