100100 margin : 1.5rem 0 ;
101101 }
102102 .card {
103+ appearance : none;
103104 border : 1px solid var (--border );
104105 border-radius : 0.5rem ;
105106 padding : 1rem ;
106107 background : var (--panel );
108+ color : inherit;
109+ cursor : pointer;
110+ font : inherit;
111+ text-align : left;
112+ transition :
113+ border-color 160ms ease,
114+ box-shadow 160ms ease,
115+ transform 160ms ease;
116+ }
117+ .card : hover {
118+ border-color : var (--brand );
119+ box-shadow : 0 0.6rem 1.5rem rgb (15 118 110 / 0.12 );
120+ transform : translateY (-1px );
121+ }
122+ .card : focus-visible {
123+ outline : 3px solid color-mix (in srgb, var (--brand ) 35% , transparent);
124+ outline-offset : 2px ;
125+ }
126+ .card [aria-pressed = "true" ] {
127+ border-color : var (--brand );
128+ box-shadow : inset 0 0 0 1px var (--brand );
107129 }
108130 .metric {
109131 font-size : 2rem ;
202224 color : var (--brand );
203225 font-weight : 700 ;
204226 }
227+ .filter-summary {
228+ min-height : 1.2rem ;
229+ color : var (--muted );
230+ font-size : 0.9rem ;
231+ }
232+ .filter-summary strong {
233+ color : var (--text );
234+ }
205235 </ style >
206236 </ head >
207237 < body >
@@ -218,6 +248,7 @@ <h1>Dashboard</h1>
218248 < section class ="grid " id ="metrics "> </ section >
219249
220250 < input id ="search " type ="search " placeholder ="Search path, target, title, description, owner, tags… " />
251+ < div class ="filter-summary " id ="filter-summary " aria-live ="polite "> </ div >
221252
222253 < table >
223254 < thead >
@@ -244,10 +275,12 @@ <h1>Dashboard</h1>
244275 const rowsEl = document . getElementById ( "rows" ) ;
245276 const metricsEl = document . getElementById ( "metrics" ) ;
246277 const searchEl = document . getElementById ( "search" ) ;
278+ const filterSummaryEl = document . getElementById ( "filter-summary" ) ;
247279 const generatedEl = document . getElementById ( "generated" ) ;
248280
249281 let allLinks = [ ] ;
250282 let registry = null ;
283+ const activeStates = new Set ( ) ;
251284
252285 main ( ) . catch ( ( error ) => {
253286 rowsEl . innerHTML = `<tr><td colspan="7">Failed to load registry: ${ escapeHtml ( error . message ) } </td></tr>` ;
@@ -259,13 +292,29 @@ <h1>Dashboard</h1>
259292 allLinks = flattenRegistry ( registry ) ;
260293 generatedEl . innerHTML = renderGeneratedFooter ( registry ) ;
261294 renderMetrics ( allLinks ) ;
262- renderRows ( allLinks ) ;
295+ applyFilters ( ) ;
263296 }
264297
265298 searchEl . addEventListener ( "input" , ( ) => {
266- const q = searchEl . value . trim ( ) . toLowerCase ( ) ;
267- const filtered = ! q ? allLinks : allLinks . filter ( ( link ) => JSON . stringify ( link ) . toLowerCase ( ) . includes ( q ) ) ;
268- renderRows ( filtered ) ;
299+ applyFilters ( ) ;
300+ } ) ;
301+
302+ metricsEl . addEventListener ( "click" , ( event ) => {
303+ const button = event . target . closest ( "button[data-filter-state]" ) ;
304+ if ( ! button ) return ;
305+
306+ const state = button . dataset . filterState ;
307+ if ( state === "total" ) {
308+ activeStates . clear ( ) ;
309+ searchEl . value = "" ;
310+ } else if ( activeStates . has ( state ) ) {
311+ activeStates . delete ( state ) ;
312+ } else {
313+ activeStates . add ( state ) ;
314+ }
315+
316+ renderMetrics ( allLinks ) ;
317+ applyFilters ( ) ;
269318 } ) ;
270319
271320 function flattenRegistry ( registry ) {
@@ -330,15 +379,39 @@ <h1>Dashboard</h1>
330379 metricsEl . innerHTML = items
331380 . map (
332381 ( key ) => `
333- <div class="card">
382+ <button class="card" type="button" data-filter-state=" ${ escapeAttr ( key ) } " aria-pressed=" ${ key !== "total" && activeStates . has ( key ) ? "true" : "false" } " title=" ${ key === "total" ? "Clear filters" : `Filter by ${ label ( key ) } ` } ">
334383 <div class="metric">${ counts [ key ] || 0 } </div>
335384 <div class="muted">${ label ( key ) } </div>
336- </div >
385+ </button >
337386 `
338387 )
339388 . join ( "" ) ;
340389 }
341390
391+ function applyFilters ( ) {
392+ const q = searchEl . value . trim ( ) . toLowerCase ( ) ;
393+ const filtered = allLinks . filter ( ( link ) => {
394+ if ( activeStates . size && ! activeStates . has ( effectiveState ( link ) ) ) return false ;
395+ if ( ! q ) return true ;
396+ return JSON . stringify ( link ) . toLowerCase ( ) . includes ( q ) ;
397+ } ) ;
398+
399+ renderRows ( filtered ) ;
400+ renderFilterSummary ( filtered . length , q ) ;
401+ }
402+
403+ function renderFilterSummary ( count , query ) {
404+ const filters = [ ] ;
405+ if ( query ) filters . push ( `search <strong>${ escapeHtml ( query ) } </strong>` ) ;
406+ if ( activeStates . size ) {
407+ filters . push ( `state <strong>${ [ ...activeStates ] . map ( label ) . join ( " or " ) } </strong>` ) ;
408+ }
409+
410+ filterSummaryEl . innerHTML = filters . length
411+ ? `${ count } matching route${ count === 1 ? "" : "s" } where ${ filters . join ( " and " ) } `
412+ : "" ;
413+ }
414+
342415 function renderRows ( links ) {
343416 if ( ! links . length ) {
344417 rowsEl . innerHTML = `<tr><td colspan="7" class="muted">No matching routes.</td></tr>` ;
0 commit comments