1- import type { Impact , PinTarget , ScanReport , Violation , ViolationNode } from '../shared/protocol.ts'
2- import type { PinsApi , RouteGroupModel } from './components/violations.tsx'
1+ import type { Impact , PinTarget , Violation , ViolationNode } from '../shared/protocol.ts'
2+ import type { SelectedItem } from './components/fix-prompts.tsx'
3+ import type { RouteGroupModel , SelectionApi } from './components/violations.tsx'
34import { batch , createEffect , createMemo , createSignal , Match , on , Show , Switch } from 'solid-js'
45import { emptyCounts } from '../shared/protocol.ts'
6+ import { FixPromptsDialog } from './components/fix-prompts.tsx'
57import { Header , MetaLine } from './components/header.tsx'
68import { CheckCircle , PlugIcon } from './components/icons.tsx'
79import { SummaryBar } from './components/summary.tsx'
@@ -12,12 +14,10 @@ import { connectDevframeState } from './lib/devframe.ts'
1214const SNIPPET = '<script type="module" src="…/inject.js"></script>'
1315const AUTOSCAN_KEY = 'devframes:plugin:a11y:autoscan'
1416
17+ const selKey = ( route : string , ruleId : string ) => `${ route } ::${ ruleId } `
1518function nodePin ( v : Violation , node : ViolationNode ) : PinTarget {
1619 return { nodeId : node . id , target : node . target , impact : v . impact , ruleId : v . ruleId }
1720}
18- function allPins ( report : ScanReport ) : PinTarget [ ] {
19- return report . violations . flatMap ( v => v . nodes . map ( n => nodePin ( v , n ) ) )
20- }
2121
2222export function App ( ) {
2323 const channel = createA11yChannel ( )
@@ -27,7 +27,10 @@ export function App() {
2727 const [ showBestPractice , setShowBestPractice ] = createSignal ( true )
2828 const [ expandedRoutes , setExpandedRoutes ] = createSignal < Set < string > > ( new Set ( ) )
2929 const [ expandedRules , setExpandedRules ] = createSignal < Set < string > > ( new Set ( ) )
30- const [ pins , setPins ] = createSignal < PinTarget [ ] > ( [ ] )
30+ // Selected violations (`route::ruleId`) — drives both the in-page highlight
31+ // and the "Generate fix prompts" dialog.
32+ const [ selected , setSelected ] = createSignal < Set < string > > ( new Set ( ) )
33+ const [ dialogOpen , setDialogOpen ] = createSignal ( false )
3134
3235 const storedAuto = ( ( ) => {
3336 try {
@@ -90,6 +93,53 @@ export function App() {
9093 return collapsed
9194 } )
9295
96+ // ── selection → highlight + fix-prompt context ────────────────────────────
97+ // Highlighted nodes, in a stable order, for numbered badges.
98+ const selectedPins = createMemo < PinTarget [ ] > ( ( ) => {
99+ const sel = selected ( )
100+ const out : PinTarget [ ] = [ ]
101+ for ( const report of routes ( ) ) {
102+ for ( const v of report . violations ) {
103+ if ( sel . has ( selKey ( report . route , v . ruleId ) ) ) {
104+ for ( const node of v . nodes )
105+ out . push ( nodePin ( v , node ) )
106+ }
107+ }
108+ }
109+ return out
110+ } )
111+
112+ const selectedItems = createMemo < SelectedItem [ ] > ( ( ) => {
113+ const sel = selected ( )
114+ const out : SelectedItem [ ] = [ ]
115+ for ( const report of routes ( ) ) {
116+ for ( const v of report . violations ) {
117+ if ( sel . has ( selKey ( report . route , v . ruleId ) ) )
118+ out . push ( { route : report . route , url : report . url , violation : v } )
119+ }
120+ }
121+ return out
122+ } )
123+
124+ const selectionApi : SelectionApi = {
125+ isSelected : ( route , ruleId ) => selected ( ) . has ( selKey ( route , ruleId ) ) ,
126+ toggle : ( route , ruleId ) => {
127+ const key = selKey ( route , ruleId )
128+ setSelected ( ( prev ) => {
129+ const next = new Set ( prev )
130+ if ( next . has ( key ) )
131+ next . delete ( key )
132+ else
133+ next . add ( key )
134+ return next
135+ } )
136+ } ,
137+ numberOf : ( nodeId ) => {
138+ const i = selectedPins ( ) . findIndex ( p => p . nodeId === nodeId )
139+ return i === - 1 ? null : i + 1
140+ } ,
141+ }
142+
93143 // ── config → agent, and initial auto-scan default ─────────────────────────
94144 let autoInit = false
95145 createEffect ( ( ) => {
@@ -120,50 +170,25 @@ export function App() {
120170 setExpandedRoutes ( prev => ( prev . has ( r ) ? prev : new Set ( prev ) . add ( r ) ) )
121171 } )
122172
123- // ── pins ──────────────────────────────────────────────────────────────────
124- createEffect ( ( ) => channel . setPins ( pins ( ) ) )
125-
126- // Pins reference live DOM, so clear them when the host route changes.
127- createEffect ( on ( activeRoute , ( ) => setPins ( [ ] ) , { defer : true } ) )
173+ // Push the highlight set (derived from the selection) to the in-page agent.
174+ createEffect ( ( ) => channel . setPins ( selectedPins ( ) ) )
128175
129- // defaultHighlight: pin a route's violations the first time it's scanned.
176+ // defaultHighlight: select all of a route's violations the first time it's scanned.
130177 const highlighted = new Set < string > ( )
131178 createEffect ( ( ) => {
132179 const cfg = devframe . config ( )
133180 const report = activeReport ( )
134181 if ( ! cfg ?. defaultHighlight || ! report || highlighted . has ( report . route ) )
135182 return
136183 highlighted . add ( report . route )
137- setPins ( allPins ( report ) )
184+ setSelected ( ( prev ) => {
185+ const next = new Set ( prev )
186+ for ( const v of report . violations )
187+ next . add ( selKey ( report . route , v . ruleId ) )
188+ return next
189+ } )
138190 } )
139191
140- const pinsApi : PinsApi = {
141- isPinned : nodeId => pins ( ) . some ( p => p . nodeId === nodeId ) ,
142- numberOf : ( nodeId ) => {
143- const i = pins ( ) . findIndex ( p => p . nodeId === nodeId )
144- return i === - 1 ? null : i + 1
145- } ,
146- isRulePinned : v => v . nodes . length > 0 && v . nodes . every ( n => pins ( ) . some ( p => p . nodeId === n . id ) ) ,
147- toggleNode : ( v , node ) => {
148- setPins ( prev => ( prev . some ( p => p . nodeId === node . id )
149- ? prev . filter ( p => p . nodeId !== node . id )
150- : [ ...prev , nodePin ( v , node ) ] ) )
151- } ,
152- toggleRule : ( v ) => {
153- const allPinned = v . nodes . length > 0 && v . nodes . every ( n => pins ( ) . some ( p => p . nodeId === n . id ) )
154- if ( allPinned ) {
155- const ids = new Set ( v . nodes . map ( n => n . id ) )
156- setPins ( prev => prev . filter ( p => ! ids . has ( p . nodeId ) ) )
157- }
158- else {
159- setPins ( ( prev ) => {
160- const have = new Set ( prev . map ( p => p . nodeId ) )
161- return [ ...prev , ...v . nodes . filter ( n => ! have . has ( n . id ) ) . map ( n => nodePin ( v , n ) ) ]
162- } )
163- }
164- } ,
165- }
166-
167192 // ── deep-linking from other docks (e.g. the messages feed) ────────────────
168193 createEffect ( on ( devframe . activation , ( act ) => {
169194 if ( ! act )
@@ -183,20 +208,13 @@ export function App() {
183208
184209 batch ( ( ) => {
185210 setExpandedRoutes ( prev => new Set ( prev ) . add ( route ) )
186- if ( ruleId )
211+ if ( ruleId ) {
187212 setExpandedRules ( prev => new Set ( prev ) . add ( `${ route } ::${ ruleId } ` ) )
188- } )
189- if ( ruleId ) {
190- const report = routes ( ) . find ( r => r . route === route )
191- const v = report ?. violations . find ( x => x . ruleId === ruleId )
192- if ( v ) {
193- setPins ( ( prev ) => {
194- const have = new Set ( prev . map ( p => p . nodeId ) )
195- return [ ...prev , ...v . nodes . filter ( n => ! have . has ( n . id ) ) . map ( n => nodePin ( v , n ) ) ]
196- } )
213+ setSelected ( prev => new Set ( prev ) . add ( selKey ( route , ruleId ) ) )
197214 }
215+ } )
216+ if ( ruleId )
198217 setTimeout ( ( ) => document . getElementById ( ruleCardId ( route , ruleId ) ) ?. scrollIntoView ( { block : 'center' , behavior : 'smooth' } ) , 60 )
199- }
200218 } , { defer : true } ) )
201219
202220 function toggleFilter ( impact : Impact ) {
@@ -235,6 +253,8 @@ export function App() {
235253 < Header
236254 agentReady = { channel . agentReady ( ) }
237255 scanning = { channel . scanning ( ) }
256+ selectedCount = { selectedItems ( ) . length }
257+ onGenerate = { ( ) => setDialogOpen ( true ) }
238258 onRescan = { channel . rescan }
239259 />
240260 < MetaLine
@@ -322,12 +342,16 @@ export function App() {
322342 expandedRules = { expandedRules ( ) }
323343 onToggleRule = { toggleRule }
324344 channel = { channel }
325- pins = { pinsApi }
345+ selection = { selectionApi }
326346 />
327347 </ Show >
328348 </ Match >
329349 </ Switch >
330350 </ div >
351+
352+ < Show when = { dialogOpen ( ) && selectedItems ( ) . length > 0 } >
353+ < FixPromptsDialog items = { selectedItems ( ) } onClose = { ( ) => setDialogOpen ( false ) } />
354+ </ Show >
331355 </ div >
332356 )
333357}
0 commit comments