11import type { Impact , PinTarget , ScanReport , Violation , ViolationNode } from '../shared/protocol.ts'
2- import type { TabId } from './components/header.tsx'
32import type { PinsApi , RouteGroupModel } from './components/violations.tsx'
43import { batch , createEffect , createMemo , createSignal , Match , on , Show , Switch } from 'solid-js'
54import { emptyCounts } from '../shared/protocol.ts'
6- import { Dashboard } from './components/dashboard.tsx'
75import { Header , MetaLine } from './components/header.tsx'
86import { CheckCircle , PlugIcon } from './components/icons.tsx'
7+ import { SummaryBar } from './components/summary.tsx'
98import { ruleCardId , ViolationList } from './components/violations.tsx'
109import { createA11yChannel } from './lib/channel.ts'
1110import { connectDevframeState } from './lib/devframe.ts'
@@ -24,7 +23,6 @@ export function App() {
2423 const channel = createA11yChannel ( )
2524 const devframe = connectDevframeState ( )
2625
27- const [ activeTab , setActiveTab ] = createSignal < TabId > ( 'dashboard' )
2826 const [ filter , setFilter ] = createSignal < Impact | null > ( null )
2927 const [ showBestPractice , setShowBestPractice ] = createSignal ( true )
3028 const [ expandedRoutes , setExpandedRoutes ] = createSignal < Set < string > > ( new Set ( ) )
@@ -68,16 +66,7 @@ export function App() {
6866 const totalRules = createMemo ( ( ) =>
6967 routes ( ) . reduce ( ( n , r ) => n + r . violations . filter ( includeViolation ) . length , 0 ) )
7068
71- const routeSummaries = createMemo ( ( ) =>
72- routes ( ) . map ( r => ( {
73- route : r . route ,
74- active : r . route === activeRoute ( ) ,
75- ruleCount : r . violations . filter ( includeViolation ) . length ,
76- nodeCount : r . violations . filter ( includeViolation ) . reduce ( ( m , v ) => m + v . nodes . length , 0 ) ,
77- } ) ) )
78-
79- // Grouped, filtered violations for the Violations tab — active route first,
80- // empty groups dropped.
69+ // Grouped, filtered violations — active route first, empty groups dropped.
8170 const groups = createMemo < RouteGroupModel [ ] > ( ( ) => {
8271 const models = routes ( ) . map ( r => ( {
8372 report : r ,
@@ -183,14 +172,15 @@ export function App() {
183172 if ( cfg && act . dockId !== cfg . dockId )
184173 return
185174 const params = act . params ?? { }
186- const tab = params . tab === 'dashboard' ? 'dashboard' : 'violations'
187- setActiveTab ( tab )
188- if ( tab === 'dashboard' )
189- return
190175 const route = typeof params . route === 'string' ? params . route : undefined
191176 const ruleId = typeof params . ruleId === 'string' ? params . ruleId : undefined
192- if ( ! route )
177+
178+ // A dashboard/summary activation (or one with no target) just scrolls to top.
179+ if ( params . tab === 'dashboard' || ! route ) {
180+ document . querySelector ( '.scroll' ) ?. scrollTo ( { top : 0 , behavior : 'smooth' } )
193181 return
182+ }
183+
194184 batch ( ( ) => {
195185 setExpandedRoutes ( prev => new Set ( prev ) . add ( route ) )
196186 if ( ruleId )
@@ -205,7 +195,6 @@ export function App() {
205195 return [ ...prev , ...v . nodes . filter ( n => ! have . has ( n . id ) ) . map ( n => nodePin ( v , n ) ) ]
206196 } )
207197 }
208- // Scroll the card into view once it has rendered.
209198 setTimeout ( ( ) => document . getElementById ( ruleCardId ( route , ruleId ) ) ?. scrollIntoView ( { block : 'center' , behavior : 'smooth' } ) , 60 )
210199 }
211200 } , { defer : true } ) )
@@ -234,11 +223,6 @@ export function App() {
234223 return next
235224 } )
236225 }
237- function selectRoute ( route : string ) {
238- setActiveTab ( 'violations' )
239- setExpandedRoutes ( prev => new Set ( prev ) . add ( route ) )
240- setTimeout ( ( ) => document . getElementById ( `group-${ encodeURIComponent ( route ) } ` ) ?. scrollIntoView ( { block : 'start' , behavior : 'smooth' } ) , 60 )
241- }
242226
243227 const announce = ( ) => {
244228 if ( ! channel . state ( ) )
@@ -251,8 +235,6 @@ export function App() {
251235 < Header
252236 agentReady = { channel . agentReady ( ) }
253237 scanning = { channel . scanning ( ) }
254- activeTab = { activeTab ( ) }
255- onTab = { setActiveTab }
256238 onRescan = { channel . rescan }
257239 />
258240 < MetaLine
@@ -289,25 +271,7 @@ export function App() {
289271 </ div >
290272 </ Match >
291273
292- { /* Dashboard. */ }
293- < Match when = { activeTab ( ) === 'dashboard' } >
294- < Dashboard
295- counts = { chipCounts ( ) }
296- filter = { filter ( ) }
297- onToggleFilter = { toggleFilter }
298- totalNodes = { totalNodes ( ) }
299- totalRules = { totalRules ( ) }
300- routes = { routeSummaries ( ) }
301- onSelectRoute = { selectRoute }
302- autoScan = { autoScan ( ) }
303- onToggleAutoScan = { setAutoScan }
304- showBestPractice = { showBestPractice ( ) }
305- onToggleBestPractice = { setShowBestPractice }
306- onClearAll = { channel . clearAll }
307- />
308- </ Match >
309-
310- { /* Violations — clean. */ }
274+ { /* Report in, nothing to flag. */ }
311275 < Match when = { totalFilterable ( ) === 0 } >
312276 < div class = "state state--clean" >
313277 < CheckCircle class = "state__glyph" />
@@ -319,43 +283,48 @@ export function App() {
319283 </ div >
320284 </ Match >
321285
322- { /* Violations — filtered to empty. */ }
323- < Match when = { shownViolations ( ) === 0 } >
324- < div class = "state" >
325- < CheckCircle class = "state__glyph" />
326- < p class = "state__title" > Nothing matches the filter</ p >
327- < p class = "state__body" >
328- { totalFilterable ( ) }
329- { ' ' }
330- { totalFilterable ( ) === 1 ? 'rule' : 'rules' }
331- { ' ' }
332- at other severities. Clear the filter to see them.
333- </ p >
334- </ div >
335- </ Match >
286+ { /* Single page: summary band + grouped violations. */ }
287+ < Match when = { totalFilterable ( ) > 0 } >
288+ < SummaryBar
289+ counts = { chipCounts ( ) }
290+ filter = { filter ( ) }
291+ onToggleFilter = { toggleFilter }
292+ totalNodes = { totalNodes ( ) }
293+ totalRules = { totalRules ( ) }
294+ routeCount = { routes ( ) . length }
295+ autoScan = { autoScan ( ) }
296+ onToggleAutoScan = { setAutoScan }
297+ showBestPractice = { showBestPractice ( ) }
298+ onToggleBestPractice = { setShowBestPractice }
299+ onClearAll = { channel . clearAll }
300+ />
336301
337- { /* Violations — grouped list. */ }
338- < Match when = { shownViolations ( ) > 0 } >
339- < Show when = { shownViolations ( ) !== totalFilterable ( ) } >
340- < p class = "filtered-note" >
341- Showing
342- { ' ' }
343- { shownViolations ( ) }
344- { ' of ' }
345- { totalFilterable ( ) }
346- { ' rules' }
347- </ p >
302+ < Show
303+ when = { shownViolations ( ) > 0 }
304+ fallback = { (
305+ < div class = "state" >
306+ < CheckCircle class = "state__glyph" />
307+ < p class = "state__title" > Nothing matches the filter</ p >
308+ < p class = "state__body" >
309+ { totalFilterable ( ) }
310+ { ' ' }
311+ { totalFilterable ( ) === 1 ? 'rule' : 'rules' }
312+ { ' at other severities. Clear the filter to see them.' }
313+ </ p >
314+ </ div >
315+ ) }
316+ >
317+ < ViolationList
318+ groups = { groups ( ) }
319+ collapsedRoutes = { collapsedRoutes ( ) }
320+ onToggleGroup = { toggleGroup }
321+ onClearRoute = { channel . clearRoute }
322+ expandedRules = { expandedRules ( ) }
323+ onToggleRule = { toggleRule }
324+ channel = { channel }
325+ pins = { pinsApi }
326+ />
348327 </ Show >
349- < ViolationList
350- groups = { groups ( ) }
351- collapsedRoutes = { collapsedRoutes ( ) }
352- onToggleGroup = { toggleGroup }
353- onClearRoute = { channel . clearRoute }
354- expandedRules = { expandedRules ( ) }
355- onToggleRule = { toggleRule }
356- channel = { channel }
357- pins = { pinsApi }
358- />
359328 </ Match >
360329 </ Switch >
361330 </ div >
0 commit comments