Spaces:
Running
Running
feat: add accessible mobile surface finder
#6
by betterwithage - opened
killinchu_experience.py
CHANGED
|
@@ -131,6 +131,13 @@ def build_manifest(ns: str = "killinchu") -> dict[str, Any]:
|
|
| 131 |
"by_data_state": dict(sorted(states.items())),
|
| 132 |
"safe_get_probes": sum(1 for row in surfaces if row["source"]["safe_probe"]),
|
| 133 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
"honesty": "Declared states are contracts, not reachability claims. The UI probes only allow-listed GET endpoints and reports HTTP/freshness separately.",
|
| 135 |
"surfaces": surfaces,
|
| 136 |
}
|
|
@@ -150,13 +157,16 @@ def register(app: "FastAPI", ns: str = "killinchu") -> dict[str, Any]:
|
|
| 150 |
return {"module": __name__, "registered": [f"GET {path}"], "surface_count": len(EXPERIENCE_SURFACES)}
|
| 151 |
|
| 152 |
|
| 153 |
-
EXPERIENCE_LAYER_HTML = r"""<!-- __KILLINCHU_EXPERIENCE_LAYER_V1__ -->
|
| 154 |
<style>
|
| 155 |
#szl-data-passport{display:grid;grid-template-columns:minmax(0,1.35fr) repeat(3,minmax(125px,1fr));gap:1px;margin:.65rem 0 .9rem;border:1px solid rgba(201,183,135,.28);border-radius:10px;overflow:hidden;background:#24282f}
|
| 156 |
.szl-passport-cell{min-width:0;background:#101318;padding:.62rem .72rem}.szl-passport-k{display:block;color:#8f9aa8;font:600 9px/1.2 var(--mono,monospace);letter-spacing:.11em;text-transform:uppercase;margin-bottom:.3rem}.szl-passport-v{display:block;color:#d9dee5;font:500 11px/1.45 var(--mono,monospace);overflow-wrap:anywhere}.szl-passport-v strong{color:#7fd6c7}.szl-passport-boundary{grid-column:1/-1;border-top:1px solid #24282f}.szl-passport-boundary .szl-passport-v{font-family:var(--sans,system-ui,sans-serif);font-size:12px;color:#b8c0ca}.szl-passport-live{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:6px;background:#c9a05f;vertical-align:1px}.szl-passport-live[data-ok="true"]{background:#5fb3a3}.szl-passport-live[data-ok="false"]{background:#b06a5a}
|
|
|
|
|
|
|
| 157 |
@media(max-width:880px){#szl-data-passport{grid-template-columns:1fr 1fr}.szl-passport-boundary{grid-column:1/-1}}
|
| 158 |
@media(max-width:520px){#szl-data-passport{grid-template-columns:1fr}.szl-passport-boundary{grid-column:auto}}
|
| 159 |
-
@media(
|
|
|
|
| 160 |
</style>
|
| 161 |
<script>
|
| 162 |
(function(){
|
|
@@ -170,6 +180,26 @@ EXPERIENCE_LAYER_HTML = r"""<!-- __KILLINCHU_EXPERIENCE_LAYER_V1__ -->
|
|
| 170 |
return manifestPromise;
|
| 171 |
}
|
| 172 |
function rowFor(key){var xs=(manifest&&manifest.surfaces)||[];for(var i=0;i<xs.length;i++)if(xs[i].id===key)return xs[i];return null;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
function stamp(d){return d&&(d.observed_at||d.fetched_at||d.as_of||d.ts||d.timestamp||d.generated_at)||null;}
|
| 174 |
function sourceLabel(d){return d&&(d.data_label||d.status_label||d.mode||d.label||d.status)||null;}
|
| 175 |
function cell(k,v,cls){return '<div class="szl-passport-cell '+(cls||'')+'"><span class="szl-passport-k">'+esc(k)+'</span><span class="szl-passport-v">'+v+'</span></div>';}
|
|
@@ -215,7 +245,7 @@ EXPERIENCE_LAYER_HTML = r"""<!-- __KILLINCHU_EXPERIENCE_LAYER_V1__ -->
|
|
| 215 |
var original=window.go; window.go=function(view){var out=original.apply(this,arguments);activateNav();mount(view);return out;}; window.go.__kcExperience=true; return true;
|
| 216 |
}
|
| 217 |
function boot(){
|
| 218 |
-
activateNav(); wrapGo(); loadManifest().then(function(){mount((location.hash||'#tracks').slice(1));}).catch(function(){});
|
| 219 |
var attempts=0, iv=setInterval(function(){activateNav();if(wrapGo()||++attempts>40)clearInterval(iv);},100);
|
| 220 |
try{new MutationObserver(function(){activateNav();}).observe(document.querySelector('.side')||document.body,{childList:true,subtree:true});}catch(e){}
|
| 221 |
}
|
|
|
|
| 131 |
"by_data_state": dict(sorted(states.items())),
|
| 132 |
"safe_get_probes": sum(1 for row in surfaces if row["source"]["safe_probe"]),
|
| 133 |
},
|
| 134 |
+
"operator_ui": {
|
| 135 |
+
"surface_finder": "ENABLED",
|
| 136 |
+
"keyboard_shortcut": "/",
|
| 137 |
+
"escape_clears_filter": True,
|
| 138 |
+
"minimum_touch_target_css_px": 44,
|
| 139 |
+
"read_only": True,
|
| 140 |
+
},
|
| 141 |
"honesty": "Declared states are contracts, not reachability claims. The UI probes only allow-listed GET endpoints and reports HTTP/freshness separately.",
|
| 142 |
"surfaces": surfaces,
|
| 143 |
}
|
|
|
|
| 157 |
return {"module": __name__, "registered": [f"GET {path}"], "surface_count": len(EXPERIENCE_SURFACES)}
|
| 158 |
|
| 159 |
|
| 160 |
+
EXPERIENCE_LAYER_HTML = r"""<!-- __KILLINCHU_EXPERIENCE_LAYER_V1__ --><!-- __KILLINCHU_SURFACE_FINDER_V2__ -->
|
| 161 |
<style>
|
| 162 |
#szl-data-passport{display:grid;grid-template-columns:minmax(0,1.35fr) repeat(3,minmax(125px,1fr));gap:1px;margin:.65rem 0 .9rem;border:1px solid rgba(201,183,135,.28);border-radius:10px;overflow:hidden;background:#24282f}
|
| 163 |
.szl-passport-cell{min-width:0;background:#101318;padding:.62rem .72rem}.szl-passport-k{display:block;color:#8f9aa8;font:600 9px/1.2 var(--mono,monospace);letter-spacing:.11em;text-transform:uppercase;margin-bottom:.3rem}.szl-passport-v{display:block;color:#d9dee5;font:500 11px/1.45 var(--mono,monospace);overflow-wrap:anywhere}.szl-passport-v strong{color:#7fd6c7}.szl-passport-boundary{grid-column:1/-1;border-top:1px solid #24282f}.szl-passport-boundary .szl-passport-v{font-family:var(--sans,system-ui,sans-serif);font-size:12px;color:#b8c0ca}.szl-passport-live{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:6px;background:#c9a05f;vertical-align:1px}.szl-passport-live[data-ok="true"]{background:#5fb3a3}.szl-passport-live[data-ok="false"]{background:#b06a5a}
|
| 164 |
+
#szl-surface-finder{position:sticky;top:-1.1rem;z-index:4;margin:0 -.15rem .8rem;padding:.55rem .15rem .65rem;background:linear-gradient(180deg,var(--rail,#0b0b0b) 82%,rgba(11,11,11,0));}
|
| 165 |
+
.szl-finder-row{display:grid;grid-template-columns:minmax(0,1fr) 44px;gap:.35rem;align-items:center}.szl-finder-label{display:block;margin:0 0 .35rem;color:var(--gold,#c9b787);font:600 9px/1.2 var(--mono,monospace);letter-spacing:.13em;text-transform:uppercase}.szl-finder-input{width:100%;height:44px!important;min-width:0;padding:.55rem .65rem!important;border:1px solid var(--gold-line,rgba(201,183,135,.15))!important;border-radius:7px!important;background:#080808!important;color:var(--cream,#f5f5f5)!important;font:500 12px/1.2 var(--mono,monospace)!important}.szl-finder-input::placeholder{color:#7f8790}.szl-finder-clear{width:44px;height:44px;border:1px solid var(--gold-line,rgba(201,183,135,.15));border-radius:7px;background:var(--gold-soft,rgba(201,183,135,.04));color:var(--gold,#c9b787);font:600 16px/1 var(--mono,monospace);cursor:pointer}.szl-finder-status{display:block;min-height:1.3em;margin:.35rem .1rem 0;color:#8f9aa8;font:500 9px/1.3 var(--mono,monospace);letter-spacing:.04em}.side.szl-search-active .nav-group{display:none!important}.side.szl-search-active .nav-item:not(.szl-search-hidden){display:flex!important}.side .nav-item.szl-search-hidden{display:none!important}
|
| 166 |
@media(max-width:880px){#szl-data-passport{grid-template-columns:1fr 1fr}.szl-passport-boundary{grid-column:1/-1}}
|
| 167 |
@media(max-width:520px){#szl-data-passport{grid-template-columns:1fr}.szl-passport-boundary{grid-column:auto}}
|
| 168 |
+
@media(max-width:820px){#szl-surface-finder{top:-1.1rem}.szl-finder-input{font-size:16px!important}}
|
| 169 |
+
@media(prefers-reduced-motion:reduce){#szl-data-passport *,#szl-surface-finder *{scroll-behavior:auto!important;transition:none!important}}
|
| 170 |
</style>
|
| 171 |
<script>
|
| 172 |
(function(){
|
|
|
|
| 180 |
return manifestPromise;
|
| 181 |
}
|
| 182 |
function rowFor(key){var xs=(manifest&&manifest.surfaces)||[];for(var i=0;i<xs.length;i++)if(xs[i].id===key)return xs[i];return null;}
|
| 183 |
+
function mountFinder(){
|
| 184 |
+
var side=document.querySelector('.side');if(!side||document.getElementById('szl-surface-finder'))return;
|
| 185 |
+
var brand=side.querySelector('.brand'),box=document.createElement('section');box.id='szl-surface-finder';box.setAttribute('role','search');box.setAttribute('aria-label','Find a Killinchu surface');
|
| 186 |
+
box.innerHTML='<label class="szl-finder-label" for="szl-surface-query">Find a surface <span aria-hidden="true">/</span></label><div class="szl-finder-row"><input id="szl-surface-query" class="szl-finder-input" type="search" inputmode="search" enterkeyhint="go" autocomplete="off" placeholder="Search capabilities or routes" aria-describedby="szl-surface-count"><button id="szl-surface-clear" class="szl-finder-clear" type="button" aria-label="Clear surface filter">×</button></div><span id="szl-surface-count" class="szl-finder-status" role="status" aria-live="polite"></span>';
|
| 187 |
+
if(brand&&brand.nextSibling)side.insertBefore(box,brand.nextSibling);else side.insertBefore(box,side.firstChild);
|
| 188 |
+
var input=box.querySelector('#szl-surface-query'),clear=box.querySelector('#szl-surface-clear'),status=box.querySelector('#szl-surface-count');
|
| 189 |
+
function items(){return Array.prototype.slice.call(side.querySelectorAll('.nav-item'));}
|
| 190 |
+
function apply(){
|
| 191 |
+
var q=(input.value||'').trim().toLowerCase(),matches=0;
|
| 192 |
+
side.classList.toggle('szl-search-active',!!q);
|
| 193 |
+
items().forEach(function(node){var hay=(node.textContent+' '+(node.getAttribute('title')||'')+' '+(node.dataset.view||'')+' '+(node.getAttribute('href')||'')).toLowerCase(),hit=!q||hay.indexOf(q)>=0;node.classList.toggle('szl-search-hidden',!hit);if(hit)matches++;});
|
| 194 |
+
status.textContent=q?(matches+' matching surface'+(matches===1?'':'s')+' · Enter opens the first'):(items().length+' surfaces available · press / to search');
|
| 195 |
+
return matches;
|
| 196 |
+
}
|
| 197 |
+
input.addEventListener('input',apply);input.addEventListener('keydown',function(e){if(e.key==='Enter'&&input.value.trim()){var first=side.querySelector('.nav-item:not(.szl-search-hidden)');if(first){e.preventDefault();first.click();input.value='';apply();if(window.matchMedia&&window.matchMedia('(max-width:820px)').matches&&window.toggleSide)window.toggleSide(false);}}else if(e.key==='Escape'){input.value='';apply();input.blur();}});
|
| 198 |
+
clear.addEventListener('click',function(){input.value='';apply();input.focus();});
|
| 199 |
+
document.addEventListener('keydown',function(e){var tag=(e.target&&e.target.tagName||'').toLowerCase();if(e.key==='/'&&!e.ctrlKey&&!e.metaKey&&!e.altKey&&tag!=='input'&&tag!=='textarea'&&tag!=='select'){e.preventDefault();input.focus();}else if(e.key==='Escape'&&document.activeElement!==input&&input.value){input.value='';apply();}});
|
| 200 |
+
try{new MutationObserver(function(records){if(records.some(function(r){return r.addedNodes&&r.addedNodes.length;}))apply();}).observe(side,{childList:true});}catch(e){}
|
| 201 |
+
apply();
|
| 202 |
+
}
|
| 203 |
function stamp(d){return d&&(d.observed_at||d.fetched_at||d.as_of||d.ts||d.timestamp||d.generated_at)||null;}
|
| 204 |
function sourceLabel(d){return d&&(d.data_label||d.status_label||d.mode||d.label||d.status)||null;}
|
| 205 |
function cell(k,v,cls){return '<div class="szl-passport-cell '+(cls||'')+'"><span class="szl-passport-k">'+esc(k)+'</span><span class="szl-passport-v">'+v+'</span></div>';}
|
|
|
|
| 245 |
var original=window.go; window.go=function(view){var out=original.apply(this,arguments);activateNav();mount(view);return out;}; window.go.__kcExperience=true; return true;
|
| 246 |
}
|
| 247 |
function boot(){
|
| 248 |
+
mountFinder(); activateNav(); wrapGo(); loadManifest().then(function(){mount((location.hash||'#tracks').slice(1));}).catch(function(){});
|
| 249 |
var attempts=0, iv=setInterval(function(){activateNav();if(wrapGo()||++attempts>40)clearInterval(iv);},100);
|
| 250 |
try{new MutationObserver(function(){activateNav();}).observe(document.querySelector('.side')||document.body,{childList:true,subtree:true});}catch(e){}
|
| 251 |
}
|
tests/test_killinchu_experience_contract.py
CHANGED
|
@@ -63,6 +63,27 @@ class ExperienceManifestTests(unittest.TestCase):
|
|
| 63 |
self.assertNotIn("method:'POST'", html)
|
| 64 |
self.assertIn("row.source.safe_probe", html)
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
if __name__ == "__main__":
|
| 68 |
unittest.main()
|
|
|
|
| 63 |
self.assertNotIn("method:'POST'", html)
|
| 64 |
self.assertIn("row.source.safe_probe", html)
|
| 65 |
|
| 66 |
+
def test_mobile_surface_finder_is_keyboard_and_touch_accessible(self):
|
| 67 |
+
html = experience.EXPERIENCE_LAYER_HTML
|
| 68 |
+
self.assertIn("__KILLINCHU_SURFACE_FINDER_V2__", html)
|
| 69 |
+
self.assertIn("role','search'", html)
|
| 70 |
+
self.assertIn('type="search"', html)
|
| 71 |
+
self.assertIn('aria-label="Clear surface filter"', html)
|
| 72 |
+
self.assertIn('aria-live="polite"', html)
|
| 73 |
+
self.assertIn("if(e.key==='/'", html)
|
| 74 |
+
self.assertIn("e.key==='Escape'", html)
|
| 75 |
+
self.assertIn("height:44px", html)
|
| 76 |
+
self.assertIn("max-width:820px", html)
|
| 77 |
+
self.assertNotIn("method:'POST'", html)
|
| 78 |
+
|
| 79 |
+
def test_manifest_declares_the_operator_ui_contract(self):
|
| 80 |
+
contract = self.manifest["operator_ui"]
|
| 81 |
+
self.assertEqual("ENABLED", contract["surface_finder"])
|
| 82 |
+
self.assertEqual("/", contract["keyboard_shortcut"])
|
| 83 |
+
self.assertTrue(contract["escape_clears_filter"])
|
| 84 |
+
self.assertEqual(44, contract["minimum_touch_target_css_px"])
|
| 85 |
+
self.assertTrue(contract["read_only"])
|
| 86 |
+
|
| 87 |
|
| 88 |
if __name__ == "__main__":
|
| 89 |
unittest.main()
|