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.2.0: Narrated guided demo with a transport floaty
19 files changed
+179−94
Cargo.lock+1−1Cargo.toml+1−1VERSION+1−1package.json+1−40page.json+8−49publish.sh+1src/app/audio/demo-1.mp3src/app/audio/demo-2.mp3src/app/audio/demo-3.mp3src/app/audio/demo-4.mp3src/app/audio/demo-5.mp3src/app/audio/demo-6.mp3src/app/audio/demo-7.mp3src/app/audio/demo-8.mp3src/app/audio/narration.json+10src/app/mod.rs+23src/app/ui.html+116−1src/cli.rs+13−1src/main.rs+4Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-parts-search"-version = "1.1.3"+version = "1.2.0" dependencies = [ "base64", "clap",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-parts-search"-version = "1.1.3"+version = "1.2.0" 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.3+1.2.0
package.json+1−40@@ -38,44 +38,5 @@ "SKILL.md", "BUILD-SKILL.md", "CHANGELOG.md"- ],- "changelog": "Narrated guided demo with a transport floaty",- "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": "Narrated guided demo with a transport floaty"+ "org": "adom" }
publish.sh+1@@ -157,6 +157,7 @@ PAGE_FILES=(dockbar.json docs/icon.svg README.md SKILL.md BUILD-SKILL.md CHANGEL package.json page.json build.sh publish.sh install.sh uninstall.sh LICENSE src/main.rs src/cli.rs src/install.rs src/serve.rs src/settings.rs src/taskbar.rs src/vendors.rs src/app/mod.rs src/app/tab.rs src/app/ui.html skills/adom-parts-search-dock/SKILL.md)+for f in src/app/audio/demo-*.mp3 src/app/audio/narration.json; do [ -f "$f" ] && PAGE_FILES+=("$f"); done for f in src/app/fonts/*.woff2 screenshots/*.png docs/hero.png; do [ -f "$f" ] && PAGE_FILES+=("$f"); done adom-wiki repo push adom/adom-parts-search --files "${PAGE_FILES[@]}" -m "v$VERSION: $CHANGELOG_LINE" ok "page repo updated"
src/app/audio/demo-1.mp3added⋯ 1 unchanged line ⋯
src/app/audio/demo-2.mp3added⋯ 1 unchanged line ⋯
src/app/audio/demo-3.mp3added⋯ 1 unchanged line ⋯
src/app/audio/demo-4.mp3added⋯ 1 unchanged line ⋯
src/app/audio/demo-5.mp3added⋯ 1 unchanged line ⋯
src/app/audio/demo-6.mp3added⋯ 1 unchanged line ⋯
src/app/audio/demo-7.mp3added⋯ 1 unchanged line ⋯
src/app/audio/demo-8.mp3added⋯ 1 unchanged line ⋯
src/app/audio/narration.jsonadded+10@@ -0,0 +1,10 @@+{+ "1": "Welcome to Parts Search. One search box across Mouser, DigiKey, and JLCPCB: type a part once, and all three distributors answer side by side, with the best source picked for you. This quick demo walks the whole page. Use the controls in the corner to pause, skip, or exit at any time.",+ "2": "The fastest way to use Parts Search is to not touch it at all: ask your AI, using the words parts search. Each phrase here copies to your clipboard with one click, ready to paste into any Adom AI thread. Once you know the noun, close the strip with the X, and it stays closed.",+ "3": "This is the search bar. Type a part number or a keyword, pick how many hits you want from each vendor, and flip in stock only when you need parts today. Watch: we will search for an LM555 right now, live, against all three vendors at once.",+ "4": "Each vendor gets a column: product photos, part numbers, prices at one and at one hundred, live stock with a meter, lead times, and links to the product page, the datasheet, and a 3D model when one exists. A vendor that is down, or switched off in Settings, says so honestly, right in its column.",+ "5": "Above the columns, one hit is named the best source. Under the default policy, Mouser wins whenever it has stock. Switch to cheapest in stock in Settings, and the lowest in stock unit price wins across every vendor instead. The banner always says which policy chose the winner.",+ "6": "The cog opens Settings. Every card explains itself in full sentences: search defaults, which vendors to query, the recommendation policy, product photos, and a taskbar progress bar that paints on the Hydrogen button while a search runs. Every row shows its default, and restore defaults takes two clicks, so a stray tap can never wipe your setup.",+ "7": "Down here, live health. One LED per vendor backend, read from each service's own health endpoint, and the same lights the Dock Bar card shows. Green means a search will work, before you spend one finding out.",+ "8": "That is Parts Search. Ask your AI to find the cheapest LM358 with parts search, or just type into the box. Enjoy."+}
src/app/mod.rs+23@@ -22,6 +22,19 @@ const ICON_SVG: &str = include_str!("../../docs/icon.svg"); const DOCKBAR_JSON: &str = include_str!("../../dockbar.json"); /// Brand fonts, served same-origin: adom.inc sends no CORS header on /// its font files, so a cross-origin @font-face fails in every browser.+/// Demo narration, pre-rendered at build time with `adom-tts say` (house+/// voice + Adom pronunciation overrides), never synthesized at runtime.+const DEMO_AUDIO: &[(&str, &[u8])] = &[+ ("demo-1.mp3", include_bytes!("audio/demo-1.mp3")),+ ("demo-2.mp3", include_bytes!("audio/demo-2.mp3")),+ ("demo-3.mp3", include_bytes!("audio/demo-3.mp3")),+ ("demo-4.mp3", include_bytes!("audio/demo-4.mp3")),+ ("demo-5.mp3", include_bytes!("audio/demo-5.mp3")),+ ("demo-6.mp3", include_bytes!("audio/demo-6.mp3")),+ ("demo-7.mp3", include_bytes!("audio/demo-7.mp3")),+ ("demo-8.mp3", include_bytes!("audio/demo-8.mp3")),+];+ const FONTS: &[(&str, &[u8])] = &[ ("familjen-grotesk-latin-500-normal.woff2", include_bytes!("fonts/familjen-grotesk-latin-500-normal.woff2")), ("familjen-grotesk-latin-600-normal.woff2", include_bytes!("fonts/familjen-grotesk-latin-600-normal.woff2")),@@ -383,6 +396,16 @@ fn handle(mut req: Request, state: Arc<AppState>, no_tab: bool, tab_name: &str) } else if method == Method::Get && (ends("/favicon.svg") || ends("/icon.svg") || ends("/favicon.ico")) { let _ = req.respond(text_resp(ICON_SVG, "image/svg+xml")); return;+ } else if method == Method::Get && path.contains("/audio/") {+ let name = path.rsplit('/').next().unwrap_or("");+ if let Some((_, bytes)) = DEMO_AUDIO.iter().find(|(n, _)| *n == name) {+ let _ = req.respond(with_cors(Response::from_data(bytes.to_vec()).with_status_code(200)+ .with_header(Header::from_bytes("Content-Type", "audio/mpeg").unwrap())+ .with_header(Header::from_bytes("Cache-Control", "public, max-age=604800").unwrap())));+ } else {+ let _ = req.respond(json_resp(404, json!({"error": "no such clip"})));+ }+ return; } else if method == Method::Get && path.contains("/fonts/") { let name = path.rsplit('/').next().unwrap_or(""); if let Some((_, bytes)) = FONTS.iter().find(|(n, _)| *n == name) {
src/app/ui.html+116−1@@ -309,6 +309,27 @@ select.sel:disabled{opacity:.5;cursor:not-allowed} .restore-btn.armed{color:var(--red);border-color:rgba(248,81,73,.5);background:rgba(248,81,73,.08)} .restore-btn svg{width:16px;height:16px} +/* ---- Guided demo: spotlight + transport floaty ---- */+#demoSpot{position:fixed;z-index:calc(var(--z-toolbar) + 5);pointer-events:none;border:2px solid var(--accent);border-radius:12px;box-shadow:0 0 0 4px rgba(0,184,177,.25),0 0 0 9999px rgba(6,9,14,.5);transition:all .35s ease;display:none}+#demoSpot.on{display:block}+#demoHud{position:fixed;right:22px;bottom:44px;z-index:var(--z-floating-menu);width:264px;background:var(--overlay);border:1px solid var(--border);border-radius:12px;box-shadow:0 18px 50px rgba(0,0,0,.55);display:none;user-select:none}+#demoHud.on{display:block}+.dh-head{display:flex;align-items:center;gap:8px;padding:8px 10px 6px 12px;cursor:move;border-bottom:1px solid var(--border)}+.dh-title{font:600 12.5px var(--brand-heading);color:var(--text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}+.dh-step{font:11px var(--mono);color:var(--text-2);flex:none}+.dh-body{padding:8px 12px 10px}+.dh-name{font:600 13px var(--brand-heading);color:var(--accent);margin-bottom:6px}+.dh-prog{display:flex;gap:4px;margin-bottom:9px}+.dh-prog i{flex:1;height:4px;border-radius:999px;background:var(--border);transition:background .2s ease}+.dh-prog i.done{background:var(--accent)}+.dh-prog i.cur{background:var(--accent-hover)}+.dh-controls{display:flex;gap:6px;justify-content:center}+.dh-controls .iconbtn{width:36px;height:36px;background:var(--elevated);border-color:var(--border)}+.dh-controls .iconbtn.play{background:var(--accent);color:var(--on-accent)}+.dh-controls .iconbtn.play:hover{background:var(--accent-hover)}+.dh-controls .iconbtn.exit:hover{color:var(--red);border-color:rgba(248,81,73,.5)}+@media (max-width:720px){#demoHud{right:10px;left:10px;bottom:36px;width:auto}}+ /* ---- Responsive: reshape, do not shrink ---- */ @media (max-width:720px){ .brand .sub,.meta,.vsep{display:none}@@ -357,6 +378,7 @@ select.sel:disabled{opacity:.5;cursor:not-allowed} <div class="meta" id="meta"></div> <div class="actions"> <span class="hled" id="hled" data-tooltip="Vendor backend health. Green: Mouser, DigiKey, and JLCPCB all answered. Yellow: one is degraded or switched off. Red: none reachable. Pulsing teal: a search is running."></span>+ <button class="iconbtn" id="btnDemo" aria-label="Run the demo" data-tooltip="A two-minute narrated walk through the whole page: the Say it strip, a live search, the columns, the best-source banner, Settings, and the health lights. A floaty gives you pause, next, previous, and exit."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M10 8.5v7l5.5-3.5z"/></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 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5a3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97s-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1s.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64z"/></svg></button> </div> </header>@@ -420,6 +442,25 @@ select.sel:disabled{opacity:.5;cursor:not-allowed} </footer> </div> +<div id="demoSpot" aria-hidden="true"></div>+<div id="demoHud" role="dialog" aria-label="Demo controls">+ <div class="dh-head" id="dhHead">+ <span class="dh-title">Parts Search demo</span>+ <span class="dh-step" id="dhStep"></span>+ </div>+ <div class="dh-body">+ <div class="dh-name" id="dhName"></div>+ <div class="dh-prog" id="dhProg"></div>+ <div class="dh-controls">+ <button class="iconbtn" id="dhPrev" aria-label="Previous step" data-tooltip="Back one step (Left arrow)."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 6l-6 6 6 6"/></svg></button>+ <button class="iconbtn play" id="dhPlay" aria-label="Pause" data-tooltip="Pause or resume the narration and the auto-advance (Space)."><svg id="dhPlayIcon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 6h3v12H8zM13 6h3v12h-3z"/></svg></button>+ <button class="iconbtn" id="dhNext" aria-label="Next step" data-tooltip="Skip ahead one step (Right arrow)."><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>+ <button class="iconbtn exit" id="dhExit" aria-label="Exit the demo" data-tooltip="Stop the demo and put everything back (Escape)."><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg></button>+ </div>+ </div>+</div>+<audio id="demoAudio" preload="none"></audio>+ <div id="toasts" aria-live="polite"></div> <div id="lightbox" class="lightbox" role="dialog" aria-hidden="true" aria-label="Product photo">@@ -887,7 +928,7 @@ let lastSig=null,lastResults=null,lastClickTs=Date.now(),first=true; function clickById(id,value){ const map={search:()=>$('#go').click(),instock:()=>$('#instock').click(),settings:()=>{if(!dlg.open)$('#btnSettings').click();},'close-settings':()=>{if(dlg.open)dlg.close();},restore:()=>$('#btnRestore').click(), limit:()=>{const b=document.querySelector('#search-form .seg button[data-limit="'+value+'"]');if(b)b.click();},policy:()=>{const i=document.querySelector('#policySet input[value="'+value+'"]');if(i){i.checked=true;i.dispatchEvent(new Event('change',{bubbles:true}));}},- 'say-close':()=>$('#sayClose').click(),'say':()=>$('#tglSay').click(),'vendor-mouser':()=>$('#tglMouser').click(),'vendor-digikey':()=>$('#tglDigikey').click(),'vendor-jlcpcb':()=>$('#tglJlcpcb').click(),images:()=>$('#tglImages').click(),taskbar:()=>$('#tglTaskbar').click(),query:()=>{$('#q').value=value||'';$('#q').focus();}};+ demo:()=>demo.start(),'demo-next':()=>demo.next(),'demo-prev':()=>demo.prev(),'demo-pause':()=>demo.toggle(),'demo-exit':()=>demo.exit(),'say-close':()=>$('#sayClose').click(),'say':()=>$('#tglSay').click(),'vendor-mouser':()=>$('#tglMouser').click(),'vendor-digikey':()=>$('#tglDigikey').click(),'vendor-jlcpcb':()=>$('#tglJlcpcb').click(),images:()=>$('#tglImages').click(),taskbar:()=>$('#tglTaskbar').click(),query:()=>{$('#q').value=value||'';$('#q').focus();}}; if(map[id]){map[id]();console.log('ui click: '+id+(value?' '+value:''));return true;} const el=document.getElementById(id);if(el){el.click();console.log('ui click: #'+id);return true;} console.warn('ui click: no control with id "'+id+'"');return false;@@ -906,6 +947,80 @@ async function pollState(){ first=false; }catch{} }+// ---- Guided demo: pre-rendered narration, spotlight, transport floaty ----+const demo=(()=>{+ const audio=$('#demoAudio'),hud=$('#demoHud'),spot=$('#demoSpot');+ const STEPS=[+ {name:'Welcome',target:'.app-header'},+ {name:'Say it to your AI',target:'#say',enter(){if($('#say').hidden){demoState.stripWasHidden=true;$('#say').hidden=false;}},leave(){if(demoState.stripWasHidden){$('#say').hidden=true;demoState.stripWasHidden=false;}}},+ {name:'The search bar',target:'.searchwrap',enter(){if(!searching&&$('#subject').textContent!=='LM555'){$('#q').value='LM555';doSearch('LM555');}}},+ {name:'Three vendor columns',target:'#grid'},+ {name:'The best source',target:'#recommendation',fallback:'#grid'},+ {name:'Settings',target:'#dlgSettings',enter(){const d=$('#dlgSettings');if(!d.open){demoState.openedSettings=true;d.style.position='fixed';d.style.margin='0';d.style.left='50%';d.style.top='50%';d.style.transform='translate(-50%,-50%)';d.show();$('#settingsBody').scrollTop=0;}},leave(){const d=$('#dlgSettings');if(demoState.openedSettings&&d.open){d.close();d.style.cssText='';demoState.openedSettings=false;}}},+ {name:'Live health',target:'#appFooter'},+ {name:'That is Parts Search',target:'.app-header'},+ ];+ const demoState={active:false,step:-1,playing:true,stripWasHidden:false,openedSettings:false,raf:0};+ function paintSpot(){+ if(!demoState.active)return;+ const st=STEPS[demoState.step];+ if(!st){demoState.raf=requestAnimationFrame(paintSpot);return;}+ let el=document.querySelector(st.target);+ if(el&&st.target==='#recommendation'&&!el.firstElementChild)el=document.querySelector(st.fallback);+ if(!el||(el.getBoundingClientRect().width===0)){spot.classList.remove('on');}+ else{const r=el.getBoundingClientRect();spot.classList.add('on');spot.style.left=(r.left-6)+'px';spot.style.top=(r.top-6)+'px';spot.style.width=(r.width+12)+'px';spot.style.height=(r.height+12)+'px';}+ demoState.raf=requestAnimationFrame(paintSpot);+ }+ function paintHud(){+ $('#dhStep').textContent=(demoState.step+1)+' / '+STEPS.length;+ $('#dhName').textContent=STEPS[demoState.step].name;+ $('#dhProg').innerHTML=STEPS.map((_,i)=>'<i class="'+(i<demoState.step?'done':i===demoState.step?'cur':'')+'"></i>').join('');+ $('#dhPlayIcon').innerHTML=demoState.playing?'<path d="M8 6h3v12H8zM13 6h3v12h-3z"/>':'<path d="M9 6.5v11l8.5-5.5z"/>';+ $('#dhPlay').setAttribute('aria-label',demoState.playing?'Pause':'Play');+ }+ function goto(n){+ if(!demoState.active)return;+ const prev=STEPS[demoState.step];if(prev&&prev.leave)try{prev.leave();}catch{}+ if(n<0)n=0;+ if(n>=STEPS.length){exit();toast('Demo finished. Ask your AI for parts, or just type into the box.','success');return;}+ demoState.step=n;const st=STEPS[n];+ if(st.enter)try{st.enter();}catch(e){console.warn('demo step enter failed: '+e.message);}+ const el=document.querySelector(st.target);+ if(el&&st.target!=='#dlgSettings'&&st.target!=='.app-header'&&st.target!=='#appFooter')el.scrollIntoView({block:'center',behavior:'smooth'});+ if(st.target==='.app-header')$('#appBody').scrollTo({top:0,behavior:'smooth'});+ audio.src='audio/demo-'+(n+1)+'.mp3';+ if(demoState.playing)audio.play().catch(()=>{demoState.playing=false;paintHud();toast('Press Play on the demo floaty to hear the narration (browsers unlock audio on a click).','info');});+ paintHud();+ }+ audio.addEventListener('ended',()=>{if(demoState.active&&demoState.playing)setTimeout(()=>{if(demoState.active&&demoState.playing)goto(demoState.step+1);},500);});+ function start(){+ if(demoState.active)return;+ demoState.active=true;demoState.playing=true;hud.classList.add('on');+ paintSpot();goto(0);+ console.log('demo started');+ }+ function exit(){+ if(!demoState.active)return;+ const st=STEPS[demoState.step];if(st&&st.leave)try{st.leave();}catch{}+ demoState.active=false;demoState.step=-1;cancelAnimationFrame(demoState.raf);+ hud.classList.remove('on');spot.classList.remove('on');+ audio.pause();audio.removeAttribute('src');+ console.log('demo exited');+ }+ $('#dhNext').addEventListener('click',()=>goto(demoState.step+1));+ $('#dhPrev').addEventListener('click',()=>goto(demoState.step-1));+ $('#dhExit').addEventListener('click',exit);+ $('#dhPlay').addEventListener('click',()=>{demoState.playing=!demoState.playing;if(demoState.playing){(audio.src?audio.play():Promise.reject()).catch(()=>goto(demoState.step));}else audio.pause();paintHud();});+ document.addEventListener('keydown',e=>{if(!demoState.active)return;if(e.key==='Escape'&&!$('#dlgSettings').open){exit();}else if(e.key==='ArrowRight'){goto(demoState.step+1);}else if(e.key==='ArrowLeft'){goto(demoState.step-1);}else if(e.key===' '&&!e.target.closest('input,select')){e.preventDefault();$('#dhPlay').click();}});+ (function(){const head=$('#dhHead');let drag=null;+ head.addEventListener('mousedown',e=>{const r=hud.getBoundingClientRect();drag={dx:e.clientX-r.left,dy:e.clientY-r.top};hud.style.right='auto';hud.style.bottom='auto';e.preventDefault();});+ document.addEventListener('mousemove',e=>{if(!drag)return;hud.style.left=Math.max(0,Math.min(e.clientX-drag.dx,innerWidth-hud.offsetWidth))+'px';hud.style.top=Math.max(0,Math.min(e.clientY-drag.dy,innerHeight-hud.offsetHeight))+'px';});+ document.addEventListener('mouseup',()=>{drag=null;});+ })();+ return {start,exit,next:()=>goto(demoState.step+1),prev:()=>goto(demoState.step-1),toggle:()=>$('#dhPlay').click(),active:()=>demoState.active};+})();+$('#btnDemo').addEventListener('click',()=>demo.start());+ (function(){const ex=['STM32F103RBT6','LM358','ESP32-S3-WROOM-1','10k 0402','LDO 3.3V 500mA','USB-C receptacle'];const host=$('#examples');host.insertAdjacentHTML('beforeend',ex.map(e=>'<button type="button" class="ex" data-tooltip="Run this example search.">'+escape(e)+'</button>').join(''));host.addEventListener('click',e=>{const b=e.target.closest('.ex');if(!b)return;$('#q').value=b.textContent;doSearch(b.textContent);});})(); renderIdle();paintSettings();pollState();setInterval(pollState,500); </script>
src/cli.rs+13−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>, 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>, demo, demo-next, demo-prev, demo-pause, demo-exit, say-try-<n>"); Ok(()) } else { Err(format!("click refused: {}", v)) } } UiCmd::Search { keyword, port } => {@@ -156,6 +156,18 @@ pub fn run_ui(cmd: UiCmd) -> Result<(), String> { } } +pub fn run_demo(port: u16) -> Result<(), String> {+ let (_, started) = crate::serve::ensure(port)?;+ if started { crate::note!("page server started on :{}", port); }+ ensure_tab("Parts Search", port);+ let v = post_json(&format!("http://127.0.0.1:{}/api/click", port), serde_json::json!({"id": "demo"})).map_err(|_| not_running_hint(port))?;+ if v.get("ok").and_then(|b| b.as_bool()) == Some(true) {+ crate::ok("demo started on the page (narrated; the floaty in the corner pauses, skips, or exits it)");+ crate::hint("browsers only unlock audio after a real click; if the narration is silent, press Play on the floaty once");+ Ok(())+ } else { Err(format!("demo refused: {}", v)) }+}+ pub fn run_settings(cmd: SettingsCmd) -> Result<(), String> { match cmd { SettingsCmd::Get => {
src/main.rs+4@@ -100,6 +100,9 @@ enum Cmd { #[command(subcommand)] Settings(SettingsCmd), + /// Run the guided page demo (narrated; play/pause/next/prev/exit floaty on the page).+ Demo(PortArg),+ /// DEPRECATED: this feature moved to the `adom-chipfit` app. /// See `adom-parts-search align --help` for the one-liner. #[command(hide = false)]@@ -217,6 +220,7 @@ fn main() -> ExitCode { Cmd::Health(a) => if a.json { cli::run_status(a.port) } else { cli::run_health(a.port) }, Cmd::Ui(u) => cli::run_ui(u), Cmd::Settings(c) => cli::run_settings(c),+ Cmd::Demo(a) => cli::run_demo(a.port), Cmd::Align(_) => { warn("`adom-parts-search align` has moved to its own app: `adom-chipfit`."); hint(format!("Install it once per container: {CYAN}adom-wiki pkg install adom/adom-chipfit{RESET}"));