Skip to content

Commit 5c1bb80

Browse files
author
Iskander-Agent
committed
feat: add affiliation filter + clickable detail drawer
- Affiliation dropdown above scorecard (31 unique orgs, dynamically populated with counts) - Side drawer opens on row click OR bubble-chart click; shows name, role, score badge with label, full summary, key source, and numbered list of all sources - Drawer closes on backdrop click, X button, or Escape key; restores body scroll and focus - Combines cleanly with existing search + score pill filters - Stop-propagation on row View link so it opens source instead of drawer - Mobile: filter row stacks, drawer goes full-width - XSS-safe: all dev fields run through escapeHtml() in templating Closes deliverable #1 of bounty #33: clickable bubbles, search, filter by affiliation.
1 parent e3e8f46 commit 5c1bb80

1 file changed

Lines changed: 185 additions & 18 deletions

File tree

‎public/index.html‎

Lines changed: 185 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,51 @@
189189
tr{transition:background 0.1s}
190190
.stat-cell{transition:background 0.15s,transform 0.1s}
191191
.stat-cell:active{transform:scale(0.98)}
192+
193+
/* Filter row (search + affiliation) */
194+
.filter-row{display:grid;grid-template-columns:1fr 240px;gap:0.75rem;margin-bottom:1rem}
195+
@media(max-width:640px){.filter-row{grid-template-columns:1fr}}
196+
.search-wrap{position:relative;margin-bottom:0}
197+
.affil-wrap{position:relative}
198+
.affil-select{width:100%;padding:0.6rem 2.25rem 0.6rem 2.5rem;background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text);font-size:0.85rem;font-family:'Inter',sans-serif;outline:none;transition:border-color 0.15s;appearance:none;cursor:pointer;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}
199+
.affil-select:focus{border-color:var(--accent)}
200+
.affil-select option{background:var(--bg-elevated);color:var(--text)}
201+
.affil-icon{position:absolute;left:0.85rem;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:0.7rem;pointer-events:none;font-weight:700;letter-spacing:0.06em}
202+
.affil-caret{position:absolute;right:0.85rem;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:0.65rem;pointer-events:none}
203+
204+
/* Clickable rows */
205+
tbody tr{cursor:pointer}
206+
tbody tr.is-active td{background:var(--bg-hover)}
207+
208+
/* Detail drawer */
209+
.drawer-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.55);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);opacity:0;pointer-events:none;transition:opacity 0.2s ease;z-index:50}
210+
.drawer-backdrop.is-open{opacity:1;pointer-events:auto}
211+
.drawer{position:fixed;top:0;right:0;height:100vh;height:100dvh;width:min(440px,100vw);background:var(--bg-elevated);border-left:1px solid var(--border);box-shadow:-12px 0 40px rgba(0,0,0,0.4);transform:translateX(100%);transition:transform 0.25s cubic-bezier(0.32,0.72,0,1);z-index:60;display:flex;flex-direction:column;overflow:hidden}
212+
.drawer.is-open{transform:translateX(0)}
213+
.drawer-head{padding:1.25rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;flex-shrink:0}
214+
.drawer-head-meta{font-size:0.7rem;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;color:var(--text-muted);margin-bottom:0.5rem;font-family:'JetBrains Mono',monospace}
215+
.drawer-name{font-size:1.25rem;font-weight:700;letter-spacing:-0.02em;line-height:1.25;margin-bottom:0.35rem}
216+
.drawer-affil{font-size:0.85rem;color:var(--text-secondary);line-height:1.5}
217+
.drawer-role{font-size:0.75rem;color:var(--text-muted);margin-top:0.2rem}
218+
.drawer-close{flex-shrink:0;width:32px;height:32px;border-radius:var(--radius-xs);background:transparent;border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:1.1rem;line-height:1;transition:all 0.15s;font-family:'Inter',sans-serif}
219+
.drawer-close:hover{background:var(--bg-hover);color:var(--text);border-color:var(--border-light)}
220+
.drawer-body{padding:1.5rem;overflow-y:auto;flex:1;-webkit-overflow-scrolling:touch}
221+
.drawer-row{display:flex;align-items:center;gap:0.75rem;margin-bottom:1.5rem;flex-wrap:wrap}
222+
.drawer-row .badge{width:2.5rem;height:2.5rem;font-size:1rem}
223+
.drawer-row-label{font-size:0.85rem;font-weight:600}
224+
.drawer-row-meta{font-size:0.75rem;color:var(--text-muted);font-family:'JetBrains Mono',monospace}
225+
.drawer-section{margin-bottom:1.5rem}
226+
.drawer-section:last-child{margin-bottom:0}
227+
.drawer-section-label{font-size:0.65rem;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;color:var(--text-muted);margin-bottom:0.5rem}
228+
.drawer-summary{font-size:0.9rem;line-height:1.7;color:var(--text)}
229+
.drawer-summary.is-empty{color:var(--text-muted);font-style:italic}
230+
.drawer-key-source{display:inline-flex;align-items:center;gap:0.4rem;padding:0.45rem 0.75rem;background:rgba(247,147,26,0.08);border:1px solid rgba(247,147,26,0.2);border-radius:var(--radius-xs);color:var(--accent);font-size:0.8rem;font-weight:500;text-decoration:none;transition:all 0.15s;word-break:break-all}
231+
.drawer-key-source:hover{background:rgba(247,147,26,0.14);border-color:rgba(247,147,26,0.35)}
232+
.drawer-sources{display:flex;flex-direction:column;gap:0.4rem}
233+
.drawer-source{display:flex;align-items:flex-start;gap:0.5rem;padding:0.5rem 0.65rem;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xs);color:var(--text-secondary);font-size:0.78rem;text-decoration:none;line-height:1.5;word-break:break-all;transition:all 0.15s}
234+
.drawer-source:hover{border-color:var(--border-light);color:var(--text);background:var(--bg-hover)}
235+
.drawer-source-num{flex-shrink:0;font-family:'JetBrains Mono',monospace;color:var(--text-muted);font-size:0.7rem;padding-top:0.05rem;min-width:1.25rem}
236+
.drawer-empty{font-size:0.8rem;color:var(--text-muted);font-style:italic}
192237
</style>
193238
</head>
194239
<body>
@@ -275,10 +320,19 @@ <h2 class="section-title">Scorecard</h2>
275320
</div>
276321
<div class="filters" id="filters"></div>
277322
</div>
323+
<div class="filter-row">
278324
<div class="search-wrap">
279325
<span class="search-icon">&#x2315;</span>
280326
<input type="text" class="search-input" id="search" placeholder="Search by name, affiliation, or summary...">
281327
</div>
328+
<div class="affil-wrap">
329+
<span class="affil-icon">ORG</span>
330+
<select class="affil-select" id="affil-select" aria-label="Filter by affiliation">
331+
<option value="all">All affiliations</option>
332+
</select>
333+
<span class="affil-caret">&#x25BE;</span>
334+
</div>
335+
</div>
282336
<div class="table-wrap">
283337
<div class="table-scroll">
284338
<table>
@@ -370,6 +424,35 @@ <h4>Ecosystem</h4>
370424

371425
</div>
372426

427+
<!-- Detail drawer -->
428+
<div class="drawer-backdrop" id="drawer-backdrop" aria-hidden="true"></div>
429+
<aside class="drawer" id="drawer" role="dialog" aria-modal="true" aria-labelledby="drawer-name" tabindex="-1">
430+
<div class="drawer-head">
431+
<div>
432+
<div class="drawer-head-meta" id="drawer-meta">Developer</div>
433+
<div class="drawer-name" id="drawer-name"></div>
434+
<div class="drawer-affil" id="drawer-affil"></div>
435+
<div class="drawer-role" id="drawer-role"></div>
436+
</div>
437+
<button type="button" class="drawer-close" id="drawer-close" aria-label="Close detail panel">&times;</button>
438+
</div>
439+
<div class="drawer-body">
440+
<div class="drawer-row" id="drawer-score-row"></div>
441+
<div class="drawer-section">
442+
<div class="drawer-section-label">Position Summary</div>
443+
<div class="drawer-summary" id="drawer-summary"></div>
444+
</div>
445+
<div class="drawer-section">
446+
<div class="drawer-section-label">Key Source</div>
447+
<div id="drawer-key-source-wrap"></div>
448+
</div>
449+
<div class="drawer-section">
450+
<div class="drawer-section-label">All Sources</div>
451+
<div class="drawer-sources" id="drawer-sources"></div>
452+
</div>
453+
</div>
454+
</aside>
455+
373456
<script>
374457
fetch('/data.json').then(r=>r.json()).then(data=>{
375458
const devs=data.developers;
@@ -448,35 +531,55 @@ <h4>Ecosystem</h4>
448531

449532
// State
450533
let activeScore='all';
534+
let activeAffiliation='all';
451535
let searchQuery='';
536+
let activeDevName=null;
537+
538+
// Affiliation dropdown — populate from data
539+
const affilSelect=document.getElementById('affil-select');
540+
const affilCounts={};
541+
devs.forEach(d=>{const a=(d.affiliation||'').trim();if(a)affilCounts[a]=(affilCounts[a]||0)+1;});
542+
Object.keys(affilCounts).sort((a,b)=>a.localeCompare(b)).forEach(a=>{
543+
const opt=document.createElement('option');
544+
opt.value=a;
545+
opt.textContent=`${a} (${affilCounts[a]})`;
546+
affilSelect.appendChild(opt);
547+
});
548+
549+
function escapeHtml(s){return String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]);}
452550

453551
function render(){
454552
const tbody=document.getElementById('table-body');
455553
tbody.innerHTML='';
554+
const q=searchQuery.toLowerCase();
456555
const filtered=devs.filter(dev=>{
457556
const scoreMatch=activeScore==='all'||dev.quantum_urgency_score===parseInt(activeScore);
458-
const q=searchQuery.toLowerCase();
459-
const textMatch=!q||dev.name.toLowerCase().includes(q)||dev.affiliation.toLowerCase().includes(q)||(dev.summary&&dev.summary.toLowerCase().includes(q));
460-
return scoreMatch&&textMatch;
557+
const affilMatch=activeAffiliation==='all'||dev.affiliation===activeAffiliation;
558+
const textMatch=!q||dev.name.toLowerCase().includes(q)||(dev.affiliation||'').toLowerCase().includes(q)||(dev.summary&&dev.summary.toLowerCase().includes(q));
559+
return scoreMatch&&affilMatch&&textMatch;
461560
});
462561
document.getElementById('showing-count').textContent=`Showing ${filtered.length} of ${total}`;
463562
if(!filtered.length){
464563
tbody.innerHTML='<tr><td colspan="6" class="empty">No developers match your filters.</td></tr>';
465564
return;
466565
}
566+
const frag=document.createDocumentFragment();
467567
filtered.forEach(dev=>{
468-
const src=dev.key_source?`<a href="${dev.key_source}" class="source-link" target="_blank" rel="noopener">View</a>`:'<span style="color:var(--text-muted)">--</span>';
568+
const src=dev.key_source?`<a href="${escapeHtml(dev.key_source)}" class="source-link" target="_blank" rel="noopener" onclick="event.stopPropagation()">View</a>`:'<span style="color:var(--text-muted)">--</span>';
469569
const notable=dev.notable?'<span class="notable-tag">notable</span>':'';
470570
const rank=dev.notable?'+':dev.rank;
471-
tbody.innerHTML+=`<tr>
472-
<td class="td-rank">${rank}</td>
473-
<td class="td-name">${dev.name}${notable}</td>
474-
<td class="td-affil">${dev.affiliation}</td>
571+
const tr=document.createElement('tr');
572+
if(activeDevName===dev.name)tr.classList.add('is-active');
573+
tr.innerHTML=`<td class="td-rank">${rank}</td>
574+
<td class="td-name">${escapeHtml(dev.name)}${notable}</td>
575+
<td class="td-affil">${escapeHtml(dev.affiliation)}</td>
475576
<td><span class="badge badge-${dev.quantum_urgency_score}">${dev.quantum_urgency_score}</span></td>
476-
<td class="td-summary">${dev.summary||'No public quantum position identified.'}</td>
477-
<td>${src}</td>
478-
</tr>`;
577+
<td class="td-summary">${escapeHtml(dev.summary||'No public quantum position identified.')}</td>
578+
<td>${src}</td>`;
579+
tr.addEventListener('click',()=>openDrawer(dev));
580+
frag.appendChild(tr);
479581
});
582+
tbody.appendChild(frag);
480583
}
481584

482585
// Filter clicks (pills)
@@ -511,6 +614,75 @@ <h4>Ecosystem</h4>
511614
render();
512615
});
513616

617+
// Affiliation filter
618+
affilSelect.addEventListener('change',e=>{
619+
activeAffiliation=e.target.value;
620+
render();
621+
});
622+
623+
// === DETAIL DRAWER ===
624+
const drawer=document.getElementById('drawer');
625+
const drawerBackdrop=document.getElementById('drawer-backdrop');
626+
const drawerCloseBtn=document.getElementById('drawer-close');
627+
let lastFocused=null;
628+
629+
function openDrawer(dev){
630+
activeDevName=dev.name;
631+
const scoreLabelMap={5:'Urgent',4:'Proactive',3:'Cautious',2:'Dismissive',1:'No View'};
632+
document.getElementById('drawer-meta').textContent=dev.notable?'Notable Contributor':`Influence Rank #${dev.rank}`;
633+
document.getElementById('drawer-name').textContent=dev.name;
634+
document.getElementById('drawer-affil').textContent=dev.affiliation||'Independent';
635+
const roleEl=document.getElementById('drawer-role');
636+
if(dev.role){roleEl.textContent=dev.role;roleEl.style.display='block';}else{roleEl.style.display='none';}
637+
const vol=dev.pq_work_volume||0;
638+
const volLabel=vol===0?'no PQ work':vol===1?'1 PQ artifact':`${vol} PQ artifacts`;
639+
document.getElementById('drawer-score-row').innerHTML=
640+
`<span class="badge badge-${dev.quantum_urgency_score}">${dev.quantum_urgency_score}</span>`+
641+
`<div><div class="drawer-row-label" style="color:${({5:'var(--red)',4:'var(--orange)',3:'var(--yellow)',2:'var(--blue)',1:'var(--gray)'})[dev.quantum_urgency_score]}">${scoreLabelMap[dev.quantum_urgency_score]}</div>`+
642+
`<div class="drawer-row-meta">${volLabel}${dev.notable?' &middot; notable':''}</div></div>`;
643+
const summaryEl=document.getElementById('drawer-summary');
644+
if(dev.summary){summaryEl.textContent=dev.summary;summaryEl.classList.remove('is-empty');}
645+
else{summaryEl.textContent='No public quantum position identified.';summaryEl.classList.add('is-empty');}
646+
const ksWrap=document.getElementById('drawer-key-source-wrap');
647+
ksWrap.innerHTML=dev.key_source
648+
?`<a class="drawer-key-source" href="${escapeHtml(dev.key_source)}" target="_blank" rel="noopener">&#x2197; ${escapeHtml(dev.key_source)}</a>`
649+
:'<div class="drawer-empty">No key source on file.</div>';
650+
const sourcesEl=document.getElementById('drawer-sources');
651+
const sources=Array.isArray(dev.sources)?dev.sources.filter(Boolean):[];
652+
if(sources.length){
653+
sourcesEl.innerHTML=sources.map((s,i)=>`<a class="drawer-source" href="${escapeHtml(s)}" target="_blank" rel="noopener"><span class="drawer-source-num">${String(i+1).padStart(2,'0')}</span><span>${escapeHtml(s)}</span></a>`).join('');
654+
}else{
655+
sourcesEl.innerHTML='<div class="drawer-empty">No additional sources listed.</div>';
656+
}
657+
lastFocused=document.activeElement;
658+
drawer.classList.add('is-open');
659+
drawerBackdrop.classList.add('is-open');
660+
drawer.setAttribute('aria-hidden','false');
661+
drawerBackdrop.setAttribute('aria-hidden','false');
662+
document.body.style.overflow='hidden';
663+
setTimeout(()=>drawerCloseBtn.focus(),60);
664+
// re-render to highlight the active row
665+
render();
666+
}
667+
668+
function closeDrawer(){
669+
activeDevName=null;
670+
drawer.classList.remove('is-open');
671+
drawerBackdrop.classList.remove('is-open');
672+
drawer.setAttribute('aria-hidden','true');
673+
drawerBackdrop.setAttribute('aria-hidden','true');
674+
document.body.style.overflow='';
675+
if(lastFocused&&typeof lastFocused.focus==='function')lastFocused.focus();
676+
render();
677+
}
678+
679+
drawerCloseBtn.addEventListener('click',closeDrawer);
680+
drawerBackdrop.addEventListener('click',closeDrawer);
681+
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&drawer.classList.contains('is-open'))closeDrawer();});
682+
683+
drawer.setAttribute('aria-hidden','true');
684+
drawerBackdrop.setAttribute('aria-hidden','true');
685+
514686
render();
515687

516688
// === BUBBLE CHART ===
@@ -661,13 +833,8 @@ <h4>Ecosystem</h4>
661833
const my = (e.clientY - rect.top) * (canvas.height / dpr) / rect.height;
662834
const dev = getDevAtPos(mx, my);
663835
if (dev) {
664-
searchQuery = dev.name;
665-
activeScore = 'all';
666-
document.getElementById('search').value = dev.name;
667-
document.querySelectorAll('.pill').forEach(b => { b.classList.remove('active'); if (b.dataset.score === 'all') b.classList.add('active'); });
668-
document.querySelectorAll('.stat-cell').forEach(c => c.classList.remove('active'));
669-
render();
670-
document.querySelector('.table-section')?.scrollIntoView({ behavior: 'smooth' });
836+
tooltip.style.display = 'none';
837+
openDrawer(dev);
671838
}
672839
});
673840

0 commit comments

Comments
 (0)