← back to Commercialrealestate
a11y(index): accessibility pass (DTD verdict A, 2/3 Codex+Claude; Qwen dissent C=export-compare) — aria-label + aria-pressed on ★ shortlist buttons, scope=col on list + compare table headers, :focus-visible keyboard focus rings, @media prefers-reduced-motion, and Escape-to-close on the compare + broker/property-history modals. Contrarian-gated, 0 errors.
589fdb59d72267c992cd28065ba23ce93dc7ac83 · 2026-07-02 14:17:49 -0700 · Steve Abrams
Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
Diff
commit 589fdb59d72267c992cd28065ba23ce93dc7ac83
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 2 14:17:49 2026 -0700
a11y(index): accessibility pass (DTD verdict A, 2/3 Codex+Claude; Qwen dissent C=export-compare) — aria-label + aria-pressed on ★ shortlist buttons, scope=col on list + compare table headers, :focus-visible keyboard focus rings, @media prefers-reduced-motion, and Escape-to-close on the compare + broker/property-history modals. Contrarian-gated, 0 errors.
Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
public/index.html | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/public/index.html b/public/index.html
index e3d3d2e..32b9cb3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,6 +8,8 @@
<style>
:root { --cols:3; --bg:#0e1116; --card:#161b22; --line:#2a313c; --ink:#e6edf3; --mut:#8b949e; --acc:#3fb950; --warn:#d29922; --bad:#f85149; --blue:#58a6ff; --drawerW:316px; }
* { box-sizing:border-box; }
+ :focus-visible { outline:2px solid var(--blue); outline-offset:2px; border-radius:4px; }
+ @media (prefers-reduced-motion: reduce){ *,*::before,*::after { animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important; scroll-behavior:auto !important; } }
body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; }
/* ---- top bar ---- */
@@ -687,9 +689,9 @@ function colCell(k,p){ const f=p.finance||{};
function renderList(rows){
const cols=listCols();
const arrow=k=>colSort&&colSort.key===k?(colSort.dir>0?' ▲':' ▼'):'';
- const head='<th class="stick sortable" data-sortk="addr">Property'+arrow('addr')+'</th>'+cols.map(c=>`<th class="sortable" data-sortk="${c.k}">${esc(c.label)}${arrow(c.k)}</th>`).join('');
+ const head='<th scope="col" class="stick sortable" data-sortk="addr" title="Sort by address">Property'+arrow('addr')+'</th>'+cols.map(c=>`<th scope="col" class="sortable" data-sortk="${c.k}" title="Sort by ${esc(c.label)}">${esc(c.label)}${arrow(c.k)}</th>`).join('');
const body=rows.map(p=>`<tr class="lrow" data-id="${p.id}">`
- +`<td class="stick"><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" title="Shortlist">★</button> <a class="la" href="${p.source}" target="_blank" rel="noopener noreferrer">${esc(p.address)}</a><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div></td>`
+ +`<td class="stick"><button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist">★</button> <a class="la" href="${p.source}" target="_blank" rel="noopener noreferrer">${esc(p.address)}</a><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div></td>`
+cols.map(c=>`<td>${colCell(c.k,p)}</td>`).join('')+`</tr>`).join('');
return `<div class="listwrap"><table class="listtbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
}
@@ -836,7 +838,7 @@ function card(p){
<div class="top">
<div><div class="rank">#${p.rank} · score</div><div class="addr">${p.address}</div><div class="city">${p.city}, CA ${p.zip||''}</div></div>
<div style="display:flex;align-items:flex-start;gap:8px">
- <button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" title="Shortlist this listing">★</button>
+ <button class="starbtn ${SHORT.has(p.id)?'on':''}" data-star="${p.id}" aria-label="${SHORT.has(p.id)?'Remove from':'Add to'} shortlist" aria-pressed="${SHORT.has(p.id)}" title="Shortlist this listing">★</button>
${vis('score')?`<div class="score">${p.composite}<small>fin ${p.financeScore}${p.qwenScore!=null?' · ai '+p.qwenScore:''}</small></div>`:''}
</div>
</div>
@@ -1086,7 +1088,7 @@ function openCompare(){
const ov=$('#cmpov'), body=$('#cmpbody');
const items=DATA.ranked.filter(p=>SHORT.has(p.id));
if(items.length<2){ body.innerHTML='<div class="cmphint">Star at least 2 listings (the ★ on any card) to compare them side by side.</div>'; ov.classList.add('on'); return; }
- const head='<tr><th class="cmpk"></th>'+items.map(p=>`<th>${esc(p.address)}<div class="cmpsub">${esc(p.city)}</div></th>`).join('')+'</tr>';
+ const head='<tr><th class="cmpk" scope="col"></th>'+items.map(p=>`<th scope="col">${esc(p.address)}<div class="cmpsub">${esc(p.city)}</div></th>`).join('')+'</tr>';
const rows=CMP.map(m=>`<tr><td class="cmpk">${m.l}</td>`+items.map(p=>`<td>${m.f(p)}</td>`).join('')+'</tr>').join('');
body.innerHTML=`<table class="cmptbl"><thead>${head}</thead><tbody>${rows}</tbody></table>`;
ov.classList.add('on');
@@ -1407,6 +1409,8 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
$('#cmpbtn').onclick=openCompare;
$('#heat').value=heat; $('#heat').onchange=()=>{ heat=$('#heat').value; localStorage.setItem('cre_heat',heat); render(); };
$('#cmpov').addEventListener('click',e=>{ if(e.target.id==='cmpov') e.currentTarget.classList.remove('on'); });
+ // a11y: Escape closes any open overlay (compare / broker / property-history modals)
+ document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ ['cmpov','bkov'].forEach(id=>{ const el=document.getElementById(id); if(el) el.classList.remove('on'); }); } });
window.DATA_READY = true;
recompute(); syncChips(); render();
← ac724c3 feat(index): empty-state quick-start chips (DTD verdict D, 2
·
back to Commercialrealestate
·
feat(condos): surface parity (DTD verdict C, 2/3 Codex+Claud 6d8e182 →