← Commit history

v1.1.2: Try it buttons return as copy-to-clipboard with a confirming toast

John Lauer ·258f3616f9 ·1mo ago ·parent 6d254d7
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"-    }   ]-}\ No newline at end of file+}
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" }\ No newline at end of file
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 } => {