1- import { For , Show } from 'solid-js'
1+ import { For , Show , createSignal } from 'solid-js'
22import { Section , SectionDescription } from '@tanstack/devtools-ui'
33import { useStyles } from '../../styles/use-styles'
44import { pickSeverityClass , type SeoSeverity } from './seo-severity'
@@ -129,6 +129,32 @@ export function sortLinksForDisplay(links: Array<LinkRow>): Array<LinkRow> {
129129 )
130130}
131131
132+ const LINK_KIND_GROUPS : Array < LinkKind > = [
133+ 'internal' ,
134+ 'external' ,
135+ 'non-web' ,
136+ 'invalid' ,
137+ ]
138+
139+ function groupLinksByKindOrdered (
140+ links : Array < LinkRow > ,
141+ ) : Array < { kind : LinkKind ; items : Array < LinkRow > } > {
142+ const buckets = new Map < LinkKind , Array < LinkRow > > ( )
143+ for ( const k of LINK_KIND_GROUPS ) buckets . set ( k , [ ] )
144+ for ( const row of links ) buckets . get ( row . kind ) ! . push ( row )
145+ return LINK_KIND_GROUPS . filter ( ( k ) => buckets . get ( k ) ! . length > 0 ) . map (
146+ ( kind ) => ( { kind, items : buckets . get ( kind ) ! } ) ,
147+ )
148+ }
149+
150+ function linksAccordionTriggerId ( kind : LinkKind ) : string {
151+ return `seo-links-accordion-trigger-${ kind } `
152+ }
153+
154+ function linksAccordionPanelId ( kind : LinkKind ) : string {
155+ return `seo-links-accordion-panel-${ kind } `
156+ }
157+
132158/**
133159 * Link-level issues (capped) and totals for the SEO overview.
134160 */
@@ -169,6 +195,10 @@ export function LinksPreviewSection() {
169195 const styles = useStyles ( )
170196 const links = analyzeLinks ( )
171197 const linksForReport = sortLinksForDisplay ( links )
198+ const groups = groupLinksByKindOrdered ( linksForReport )
199+ const [ openKinds , setOpenKinds ] = createSignal < Set < LinkKind > > (
200+ new Set ( groups . map ( ( g ) => g . kind ) ) ,
201+ )
172202 const issueCount = links . reduce ( ( count , row ) => count + row . issues . length , 0 )
173203
174204 const counts = links . reduce (
@@ -187,6 +217,15 @@ export function LinksPreviewSection() {
187217 info : s . seoIssueBulletInfo ,
188218 } )
189219
220+ function toggleKind ( kind : LinkKind ) {
221+ setOpenKinds ( ( prev ) => {
222+ const next = new Set ( prev )
223+ if ( next . has ( kind ) ) next . delete ( kind )
224+ else next . add ( kind )
225+ return next
226+ } )
227+ }
228+
190229 return (
191230 < Section >
192231 < SectionDescription >
@@ -230,37 +269,84 @@ export function LinksPreviewSection() {
230269 >
231270 < div class = { s . serpPreviewBlock } >
232271 < div class = { s . serpPreviewLabel } > Links report</ div >
233- < ul class = { s . seoLinksReportList } >
234- < For each = { linksForReport } >
235- { ( row ) => (
236- < li class = { s . seoLinksReportItem } >
237- < div class = { s . seoLinksReportTopRow } >
238- < span class = { linkKindBadgeClass ( s , row . kind ) } >
239- { KIND_LABEL [ row . kind ] }
240- </ span >
241- < span class = { s . seoLinksAnchorText } >
242- { row . text || '(no text)' }
243- </ span >
244- </ div >
245- < div class = { s . seoLinksHrefLine } > { row . resolvedHref || row . href } </ div >
246- < Show when = { row . issues . length > 0 } >
247- < ul class = { s . seoIssueListNested } >
248- < For each = { row . issues } >
249- { ( issue ) => (
250- < li class = { s . seoIssueRowCompact } >
251- < span
252- class = { `${ s . seoIssueBullet } ${ bulletSev ( issue . severity ) } ` }
253- >
254- ●
255- </ span >
256- < span class = { s . seoIssueMessage } > { issue . message } </ span >
257- </ li >
258- ) }
259- </ For >
260- </ ul >
261- </ Show >
262- </ li >
263- ) }
272+ < ul class = { s . seoLinksAccordion } >
273+ < For each = { groups } >
274+ { ( group ) => {
275+ const expanded = ( ) => openKinds ( ) . has ( group . kind )
276+ return (
277+ < li class = { s . seoLinksAccordionSection } >
278+ < button
279+ type = "button"
280+ class = { s . seoLinksAccordionTrigger }
281+ aria-expanded = { expanded ( ) }
282+ aria-controls = { linksAccordionPanelId ( group . kind ) }
283+ id = { linksAccordionTriggerId ( group . kind ) }
284+ onClick = { ( ) => toggleKind ( group . kind ) }
285+ >
286+ < span class = { s . seoLinksAccordionTriggerLeft } >
287+ < span class = { linkKindBadgeClass ( s , group . kind ) } >
288+ { KIND_LABEL [ group . kind ] }
289+ </ span >
290+ < span class = { s . seoHealthLabelMuted } >
291+ { group . items . length } link{ group . items . length === 1 ? '' : 's' }
292+ </ span >
293+ </ span >
294+ < span
295+ class = { `${ s . seoLinksAccordionChevron } ${ expanded ( ) ? s . seoLinksAccordionChevronOpen : '' } ` }
296+ aria-hidden = "true"
297+ >
298+ ›
299+ </ span >
300+ </ button >
301+ < Show when = { expanded ( ) } >
302+ < div
303+ class = { s . seoLinksAccordionPanel }
304+ id = { linksAccordionPanelId ( group . kind ) }
305+ role = "region"
306+ aria-labelledby = { linksAccordionTriggerId ( group . kind ) }
307+ >
308+ < ul class = { s . seoLinksAccordionInnerList } >
309+ < For each = { group . items } >
310+ { ( row ) => (
311+ < li class = { s . seoLinksReportItem } >
312+ < div class = { s . seoLinksReportTopRow } >
313+ < span class = { linkKindBadgeClass ( s , row . kind ) } >
314+ { KIND_LABEL [ row . kind ] }
315+ </ span >
316+ < span class = { s . seoLinksAnchorText } >
317+ { row . text || '(no text)' }
318+ </ span >
319+ </ div >
320+ < div class = { s . seoLinksHrefLine } >
321+ { row . resolvedHref || row . href }
322+ </ div >
323+ < Show when = { row . issues . length > 0 } >
324+ < ul class = { s . seoIssueListNested } >
325+ < For each = { row . issues } >
326+ { ( issue ) => (
327+ < li class = { s . seoIssueRowCompact } >
328+ < span
329+ class = { `${ s . seoIssueBullet } ${ bulletSev ( issue . severity ) } ` }
330+ >
331+ ●
332+ </ span >
333+ < span class = { s . seoIssueMessage } >
334+ { issue . message }
335+ </ span >
336+ </ li >
337+ ) }
338+ </ For >
339+ </ ul >
340+ </ Show >
341+ </ li >
342+ ) }
343+ </ For >
344+ </ ul >
345+ </ div >
346+ </ Show >
347+ </ li >
348+ )
349+ } }
264350 </ For >
265351 </ ul >
266352 </ div >
0 commit comments