11import { For , Show } from 'solid-js'
22import { Section , SectionDescription } from '@tanstack/devtools-ui'
33import { useStyles } from '../../styles/use-styles'
4- import { seoSeverityColor , type SeoSeverity } from './seo-severity'
4+ import { pickSeverityClass , type SeoSeverity } from './seo-severity'
55import type { SeoSectionSummary } from './seo-section-summary'
66
77type HeadingItem = {
@@ -50,14 +50,14 @@ function validateHeadings(headings: Array<HeadingItem>): Array<HeadingIssue> {
5050 } )
5151 } else if ( h1Count > 1 ) {
5252 issues . push ( {
53- severity : 'warning ' ,
53+ severity : 'error ' ,
5454 message : `Multiple H1 headings detected (${ h1Count } ).` ,
5555 } )
5656 }
5757
5858 if ( headings [ 0 ] && headings [ 0 ] . level !== 1 ) {
5959 issues . push ( {
60- severity : 'warning ' ,
60+ severity : 'error ' ,
6161 message : `First heading is ${ headings [ 0 ] . tag . toUpperCase ( ) } instead of H1.` ,
6262 } )
6363 }
@@ -66,15 +66,15 @@ function validateHeadings(headings: Array<HeadingItem>): Array<HeadingIssue> {
6666 const current = headings [ index ] !
6767 if ( ! current . text ) {
6868 issues . push ( {
69- severity : 'warning ' ,
69+ severity : 'error ' ,
7070 message : `${ current . tag . toUpperCase ( ) } is empty.` ,
7171 } )
7272 }
7373 if ( index > 0 ) {
7474 const previous = headings [ index - 1 ] !
7575 if ( current . level - previous . level > 1 ) {
7676 issues . push ( {
77- severity : 'warning ' ,
77+ severity : 'error ' ,
7878 message : `Skipped heading level from ${ previous . tag . toUpperCase ( ) } to ${ current . tag . toUpperCase ( ) } .` ,
7979 } )
8080 }
@@ -96,19 +96,66 @@ export function getHeadingStructureSummary(): SeoSectionSummary {
9696 }
9797}
9898
99- const HEADING_LEVEL_COLORS : Record < number , string > = {
100- 1 : '#60a5fa' ,
101- 2 : '#34d399' ,
102- 3 : '#a78bfa' ,
103- 4 : '#f59e0b' ,
104- 5 : '#f87171' ,
105- 6 : '#94a3b8' ,
99+ function headingIndentClass (
100+ s : ReturnType < ReturnType < typeof useStyles > > ,
101+ level : HeadingItem [ 'level' ] ,
102+ ) : string {
103+ switch ( level ) {
104+ case 1 :
105+ return s . seoHeadingTreeIndent1
106+ case 2 :
107+ return s . seoHeadingTreeIndent2
108+ case 3 :
109+ return s . seoHeadingTreeIndent3
110+ case 4 :
111+ return s . seoHeadingTreeIndent4
112+ case 5 :
113+ return s . seoHeadingTreeIndent5
114+ case 6 :
115+ return s . seoHeadingTreeIndent6
116+ }
117+ }
118+
119+ function headingTagClass (
120+ s : ReturnType < ReturnType < typeof useStyles > > ,
121+ level : HeadingItem [ 'level' ] ,
122+ ) : string {
123+ const base = s . seoHeadingTag
124+ switch ( level ) {
125+ case 1 :
126+ return `${ base } ${ s . seoHeadingTagL1 } `
127+ case 2 :
128+ return `${ base } ${ s . seoHeadingTagL2 } `
129+ case 3 :
130+ return `${ base } ${ s . seoHeadingTagL3 } `
131+ case 4 :
132+ return `${ base } ${ s . seoHeadingTagL4 } `
133+ case 5 :
134+ return `${ base } ${ s . seoHeadingTagL5 } `
135+ case 6 :
136+ return `${ base } ${ s . seoHeadingTagL6 } `
137+ }
106138}
107139
108140export function HeadingStructurePreviewSection ( ) {
109141 const styles = useStyles ( )
110142 const headings = extractHeadings ( )
111143 const issues = validateHeadings ( headings )
144+ const s = styles ( )
145+
146+ const issueBulletClass = ( sev : SeoSeverity ) =>
147+ `${ s . seoIssueBullet } ${ pickSeverityClass ( sev , {
148+ error : s . seoIssueBulletError ,
149+ warning : s . seoIssueBulletWarning ,
150+ info : s . seoIssueBulletInfo ,
151+ } ) } `
152+
153+ const issueBadgeClass = ( sev : SeoSeverity ) =>
154+ `${ s . seoIssueSeverityBadge } ${ pickSeverityClass ( sev , {
155+ error : s . seoIssueSeverityBadgeError ,
156+ warning : s . seoIssueSeverityBadgeWarning ,
157+ info : s . seoIssueSeverityBadgeInfo ,
158+ } ) } `
112159
113160 return (
114161 < Section >
@@ -117,114 +164,54 @@ export function HeadingStructurePreviewSection() {
117164 common hierarchy issues. This section scans once when opened.
118165 </ SectionDescription >
119166
120- { /* Heading tree */ }
121- < div class = { styles ( ) . serpPreviewBlock } >
122- < div
123- style = { {
124- display : 'flex' ,
125- 'align-items' : 'center' ,
126- 'justify-content' : 'space-between' ,
127- 'margin-bottom' : '10px' ,
128- } }
129- >
130- < div
131- class = { styles ( ) . serpPreviewLabel }
132- style = { { 'margin-bottom' : '0' } }
133- >
134- Heading tree
135- </ div >
136- < span style = { { 'font-size' : '11px' , color : '#6b7280' } } >
167+ < div class = { s . serpPreviewBlock } >
168+ < div class = { s . seoHeadingTreeHeaderRow } >
169+ < div class = { s . serpPreviewLabelFlat } > Heading tree</ div >
170+ < span class = { s . seoHeadingTreeCount } >
137171 { headings . length } heading{ headings . length === 1 ? '' : 's' }
138172 </ span >
139173 </ div >
140174 < Show
141175 when = { headings . length > 0 }
142176 fallback = {
143- < div class = { styles ( ) . seoMissingTagsSection } >
177+ < div class = { s . seoMissingTagsSection } >
144178 No headings found on this page.
145179 </ div >
146180 }
147181 >
148- < ul
149- style = { {
150- margin : '0' ,
151- padding : '0' ,
152- 'list-style' : 'none' ,
153- display : 'flex' ,
154- 'flex-direction' : 'column' ,
155- gap : '3px' ,
156- } }
157- >
182+ < ul class = { s . seoHeadingTreeList } >
158183 < For each = { headings } >
159- { ( heading ) => {
160- const color = HEADING_LEVEL_COLORS [ heading . level ] ?? '#94a3b8'
161- return (
162- < li
163- style = { {
164- display : 'flex' ,
165- gap : '8px' ,
166- 'align-items' : 'baseline' ,
167- 'padding-left' : `${ ( heading . level - 1 ) * 14 } px` ,
168- } }
184+ { ( heading ) => (
185+ < li
186+ class = { `${ s . seoHeadingTreeItem } ${ headingIndentClass ( s , heading . level ) } ` }
187+ >
188+ < span class = { headingTagClass ( s , heading . level ) } >
189+ { heading . tag . toUpperCase ( ) }
190+ </ span >
191+ < span
192+ class = {
193+ heading . text ? s . seoHeadingLineText : s . seoHeadingLineTextEmpty
194+ }
169195 >
170- < span
171- style = { {
172- display : 'inline-flex' ,
173- 'align-items' : 'center' ,
174- 'justify-content' : 'center' ,
175- 'min-width' : '26px' ,
176- height : '16px' ,
177- 'border-radius' : '3px' ,
178- 'font-size' : '10px' ,
179- 'font-weight' : '700' ,
180- 'letter-spacing' : '0.03em' ,
181- background : `${ color } 18` ,
182- color,
183- 'flex-shrink' : '0' ,
184- 'font-family' : 'monospace' ,
185- } }
186- >
187- { heading . tag . toUpperCase ( ) }
188- </ span >
189- < span
190- class = { styles ( ) . seoIssueText }
191- style = { {
192- 'font-size' : '12px' ,
193- 'font-style' : heading . text ? 'normal' : 'italic' ,
194- opacity : heading . text ? 1 : 0.65 ,
195- } }
196- >
197- { heading . text || '(empty)' }
198- </ span >
199- </ li >
200- )
201- } }
196+ { heading . text || '(empty)' }
197+ </ span >
198+ </ li >
199+ ) }
202200 </ For >
203201 </ ul >
204202 </ Show >
205203 </ div >
206204
207- { /* Structure issues */ }
208205 < Show when = { issues . length > 0 } >
209- < div class = { styles ( ) . serpPreviewBlock } >
210- < div class = { styles ( ) . serpPreviewLabel } > Structure issues</ div >
211- < ul class = { styles ( ) . seoIssueList } >
206+ < div class = { s . serpPreviewBlock } >
207+ < div class = { s . serpPreviewLabel } > Structure issues</ div >
208+ < ul class = { s . seoIssueList } >
212209 < For each = { issues } >
213210 { ( issue ) => (
214- < li class = { styles ( ) . seoIssueRow } >
215- < span
216- class = { styles ( ) . seoIssueBullet }
217- style = { { color : seoSeverityColor ( issue . severity ) } }
218- >
219- ●
220- </ span >
221- < span class = { styles ( ) . seoIssueMessage } > { issue . message } </ span >
222- < span
223- class = { styles ( ) . seoIssueSeverityBadge }
224- style = { { color : seoSeverityColor ( issue . severity ) } }
225- >
226- { issue . severity }
227- </ span >
211+ < li class = { s . seoIssueRow } >
212+ < span class = { issueBulletClass ( issue . severity ) } > ●</ span >
213+ < span class = { s . seoIssueMessage } > { issue . message } </ span >
214+ < span class = { issueBadgeClass ( issue . severity ) } > { issue . severity } </ span >
228215 </ li >
229216 ) }
230217 </ For >
0 commit comments