← Commit history

v1.3.5: The header ? is a real toggle, and the prompts panel reads properly on a phone

John Lauer ·2daa78aed4 ·1mo ago ·parent c5431e5
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"-    }   ]-}\ 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": "The header ? is a real toggle, and the prompts panel reads properly on a phone"+  "org": "adom" }\ No newline at end of file
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.">&ldquo;<span class="txt">'+escape(p).replace(/parts search/i,'<em>parts search</em>')+'</span>&rdquo;'+  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">&ldquo;'+escape(p).replace(/parts search/i,'<em>parts search</em>')+'&rdquo;</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},