@@ -132,9 +132,22 @@ function CopyButton({ text }: { text: string }) {
132132 ) ;
133133}
134134
135- function resolveImageSrc ( src : string , rawBaseUrl : string ) : string {
136- if ( ! src || / ^ ( h t t p s ? : ) ? \/ \/ / i. test ( src ) ) return src ;
137- return rawBaseUrl + src . replace ( / ^ \. \/ / , '' ) ;
135+ function resolveUrl ( src : string , rawBaseUrl : string ) : string {
136+ if ( ! src ) return src ;
137+ if (
138+ / ^ ( h t t p s ? : ) ? \/ \/ / i. test ( src ) ||
139+ src . startsWith ( 'mailto:' ) ||
140+ src . startsWith ( '#' ) ||
141+ src . startsWith ( 'data:' )
142+ ) {
143+ return src ;
144+ }
145+ if ( ! rawBaseUrl ) return src ;
146+ try {
147+ return new URL ( src , rawBaseUrl ) . toString ( ) ;
148+ } catch {
149+ return src ;
150+ }
138151}
139152
140153function createMdComponents ( rawBaseUrl : string ) {
@@ -158,7 +171,7 @@ function createMdComponents(rawBaseUrl: string) {
158171 return < code className = { styles . inlineCode } > { children } </ code > ;
159172 } ,
160173 img ( { src, alt } : { src ?: string ; alt ?: string } ) {
161- const resolvedSrc = src ? resolveImageSrc ( src , rawBaseUrl ) : undefined ;
174+ const resolvedSrc = src ? resolveUrl ( src , rawBaseUrl ) : undefined ;
162175 return (
163176 < img
164177 src = { resolvedSrc }
@@ -168,6 +181,18 @@ function createMdComponents(rawBaseUrl: string) {
168181 />
169182 ) ;
170183 } ,
184+ a ( { href, children } : { href ?: string ; children ?: ReactNode } ) {
185+ const resolvedHref = href ? resolveUrl ( href , rawBaseUrl ) : undefined ;
186+ const isExternal = Boolean ( resolvedHref && / ^ h t t p s ? : \/ \/ / i. test ( resolvedHref ) ) ;
187+ return (
188+ < a
189+ href = { resolvedHref }
190+ { ...( isExternal ? { target : '_blank' , rel : 'noopener noreferrer' } : { } ) }
191+ >
192+ { children }
193+ </ a >
194+ ) ;
195+ } ,
171196 } ;
172197}
173198
@@ -193,13 +218,24 @@ export default function EcosystemItem(): ReactNode {
193218 setReadmeError ( false ) ;
194219 setActiveTab ( 0 ) ;
195220 setLogoFailed ( false ) ;
196- if ( ! rawUrl ) return ;
221+ if ( ! rawUrl ) {
222+ setReadmeLoading ( false ) ;
223+ return ;
224+ }
225+ const controller = new AbortController ( ) ;
197226 setReadmeLoading ( true ) ;
198- fetch ( rawUrl )
227+ fetch ( rawUrl , { signal : controller . signal } )
199228 . then ( ( r ) => ( r . ok ? r . text ( ) : Promise . reject ( new Error ( `HTTP ${ r . status } ` ) ) ) )
200- . then ( setReadmeRaw )
201- . catch ( ( ) => setReadmeError ( true ) )
202- . finally ( ( ) => setReadmeLoading ( false ) ) ;
229+ . then ( ( text ) => {
230+ setReadmeRaw ( text ) ;
231+ setReadmeLoading ( false ) ;
232+ } )
233+ . catch ( ( err : unknown ) => {
234+ if ( err instanceof DOMException && err . name === 'AbortError' ) return ;
235+ setReadmeError ( true ) ;
236+ setReadmeLoading ( false ) ;
237+ } ) ;
238+ return ( ) => controller . abort ( ) ;
203239 } , [ rawUrl ] ) ;
204240
205241 const parsed = useMemo (
@@ -348,27 +384,50 @@ export default function EcosystemItem(): ReactNode {
348384 { /* Tab bar */ }
349385 { hasTabs && (
350386 < div className = { styles . tabBar } >
351- < nav className = { styles . tabList } aria-label = "Section tabs" >
352- { parsed . sections . map ( ( section , idx ) => (
353- < button
354- key = { section . id }
355- className = {
356- idx === activeTab
357- ? `${ styles . tab } ${ styles . tabActive } `
358- : styles . tab
359- }
360- onClick = { ( ) => setActiveTab ( idx ) }
361- >
362- { section . title }
363- </ button >
364- ) ) }
387+ < nav
388+ className = { styles . tabList }
389+ role = "tablist"
390+ aria-label = "Section tabs"
391+ >
392+ { parsed . sections . map ( ( section , idx ) => {
393+ const isActive = idx === activeTab ;
394+ return (
395+ < button
396+ key = { section . id }
397+ id = { `section-tab-${ section . id } ` }
398+ role = "tab"
399+ type = "button"
400+ aria-selected = { isActive }
401+ aria-controls = { `section-panel-${ section . id } ` }
402+ tabIndex = { isActive ? 0 : - 1 }
403+ className = {
404+ isActive
405+ ? `${ styles . tab } ${ styles . tabActive } `
406+ : styles . tab
407+ }
408+ onClick = { ( ) => setActiveTab ( idx ) }
409+ >
410+ { section . title }
411+ </ button >
412+ ) ;
413+ } ) }
365414 </ nav >
366415 </ div >
367416 ) }
368417
369418 { /* Content card */ }
370419 { activeSection && (
371- < div className = { styles . contentCard } >
420+ < div
421+ className = { styles . contentCard }
422+ { ...( hasTabs
423+ ? {
424+ role : 'tabpanel' ,
425+ id : `section-panel-${ activeSection . id } ` ,
426+ 'aria-labelledby' : `section-tab-${ activeSection . id } ` ,
427+ tabIndex : 0 ,
428+ }
429+ : { } ) }
430+ >
372431 { ! hasTabs && (
373432 < h2 className = { styles . singleSectionTitle } >
374433 { activeSection . title }
0 commit comments