1- import { useEffect , useMemo , useState } from 'react' ;
1+ import { useEffect , useMemo , useRef , useState } from 'react' ;
22
33import { Timeline } from '@/components/timeline' ;
44import { Badge } from '@/components/ui/badge' ;
@@ -29,6 +29,17 @@ type StackId = (typeof STACKS)[number]['id'];
2929
3030const DEFAULT_STACK : StackId = 'rspack' ;
3131
32+ const GITHUB_REPO_URL = 'https://github.com/rspack-contrib/rstack-ecosystem-ci' ;
33+
34+ const RSTACK_REPOS = [
35+ { label : 'Rspack' , url : 'https://github.com/web-infra-dev/rspack' } ,
36+ { label : 'Rsbuild' , url : 'https://github.com/web-infra-dev/rsbuild' } ,
37+ { label : 'Rslib' , url : 'https://github.com/web-infra-dev/rslib' } ,
38+ { label : 'Rstest' , url : 'https://github.com/web-infra-dev/rstest' } ,
39+ { label : 'Rsdoctor' , url : 'https://github.com/web-infra-dev/rsdoctor' } ,
40+ { label : 'Rslint' , url : 'https://github.com/web-infra-dev/rslint' } ,
41+ ] as const ;
42+
3243// Get URL parameters
3344function getUrlParams ( ) {
3445 const params = new URLSearchParams ( window . location . search ) ;
@@ -52,6 +63,7 @@ function setUrlParams(stack: StackId, suite: string) {
5263export default function App ( ) {
5364 const historySource = history as Record < StackId , EcosystemCommitHistory > ;
5465
66+ const [ isRepoMenuOpen , setIsRepoMenuOpen ] = useState ( false ) ;
5567 const [ selectedStack , setSelectedStack ] = useState < StackId > ( ( ) => {
5668 const urlParams = getUrlParams ( ) ;
5769 if ( urlParams . stack && STACKS . some ( ( s ) => s . id === urlParams . stack ) ) {
@@ -64,6 +76,7 @@ export default function App() {
6476 const urlParams = getUrlParams ( ) ;
6577 return urlParams . suite ;
6678 } ) ;
79+ const repoMenuRef = useRef < HTMLDivElement | null > ( null ) ;
6780
6881 const historyByStack = useMemo ( ( ) => {
6982 const map = { } as Record < StackId , EcosystemCommitHistory > ;
@@ -83,6 +96,32 @@ export default function App() {
8396 setUrlParams ( selectedStack , selectedSuite ) ;
8497 } , [ selectedStack , selectedSuite ] ) ;
8598
99+ useEffect ( ( ) => {
100+ if ( ! isRepoMenuOpen ) {
101+ return ;
102+ }
103+
104+ function handleClick ( event : MouseEvent ) {
105+ if ( ! repoMenuRef . current ?. contains ( event . target as Node ) ) {
106+ setIsRepoMenuOpen ( false ) ;
107+ }
108+ }
109+
110+ function handleKey ( event : KeyboardEvent ) {
111+ if ( event . key === 'Escape' ) {
112+ setIsRepoMenuOpen ( false ) ;
113+ }
114+ }
115+
116+ document . addEventListener ( 'mousedown' , handleClick ) ;
117+ document . addEventListener ( 'keydown' , handleKey ) ;
118+
119+ return ( ) => {
120+ document . removeEventListener ( 'mousedown' , handleClick ) ;
121+ document . removeEventListener ( 'keydown' , handleKey ) ;
122+ } ;
123+ } , [ isRepoMenuOpen ] ) ;
124+
86125 const selectedStackMeta = useMemo (
87126 ( ) => STACKS . find ( ( stack ) => stack . id === selectedStack ) ,
88127 [ selectedStack ] ,
@@ -125,6 +164,69 @@ export default function App() {
125164 </ div >
126165 </ div >
127166 < div className = "flex flex-col items-stretch gap-4 sm:w-72" >
167+ < div className = "flex items-center justify-end gap-2" >
168+ < a
169+ href = { GITHUB_REPO_URL }
170+ target = "_blank"
171+ rel = "noreferrer"
172+ className = "inline-flex h-9 w-9 items-center justify-center rounded-full border border-border/40 bg-white/5 text-white/80 transition hover:bg-white/10 hover:text-white"
173+ aria-label = "Open GitHub repository"
174+ >
175+ < svg
176+ aria-hidden
177+ viewBox = "0 0 24 24"
178+ className = "h-5 w-5"
179+ fill = "currentColor"
180+ >
181+ < path d = "M12 0C5.37 0 0 5.48 0 12.24c0 5.41 3.44 9.99 8.2 11.61.6.12.82-.27.82-.59 0-.29-.01-1.05-.02-2.05-3.34.75-4.04-1.65-4.04-1.65-.55-1.43-1.35-1.81-1.35-1.81-1.1-.77.08-.75.08-.75 1.22.09 1.86 1.28 1.86 1.28 1.08 1.9 2.83 1.35 3.52 1.03.11-.81.42-1.35.76-1.66-2.67-.31-5.47-1.37-5.47-6.12 0-1.35.47-2.45 1.24-3.31-.13-.31-.54-1.56.12-3.26 0 0 1-.33 3.3 1.26a11.1 11.1 0 0 1 3-.41c1.02 0 2.05.14 3 .41 2.3-1.59 3.3-1.26 3.3-1.26.66 1.7.25 2.95.12 3.26.77.86 1.24 1.96 1.24 3.31 0 4.76-2.8 5.8-5.48 6.11.43.39.81 1.17.81 2.36 0 1.7-.02 3.07-.02 3.48 0 .32.22.71.82.59C20.56 22.23 24 17.65 24 12.24 24 5.48 18.63 0 12 0Z" />
182+ </ svg >
183+ </ a >
184+
185+ < div className = "relative" ref = { repoMenuRef } >
186+ < button
187+ type = "button"
188+ className = "inline-flex items-center gap-1 rounded-full border border-border/40 bg-white/5 px-3 py-1.5 text-xs font-semibold uppercase tracking-[0.18em] text-white/80 transition hover:bg-white/10 hover:text-white"
189+ aria-haspopup = "menu"
190+ aria-expanded = { isRepoMenuOpen }
191+ onClick = { ( ) => setIsRepoMenuOpen ( ( open ) => ! open ) }
192+ >
193+ Rstack
194+ < svg
195+ aria-hidden
196+ viewBox = "0 0 12 12"
197+ className = { `h-3 w-3 transition-transform ${ isRepoMenuOpen ? 'rotate-180' : '' } ` }
198+ fill = "none"
199+ >
200+ < path
201+ d = "M2.2 4.2 6 8l3.8-3.8"
202+ stroke = "currentColor"
203+ strokeWidth = "1.4"
204+ strokeLinecap = "round"
205+ strokeLinejoin = "round"
206+ />
207+ </ svg >
208+ </ button >
209+ { isRepoMenuOpen ? (
210+ < div className = "absolute right-0 z-20 mt-2 w-44 rounded-lg border border-border/40 bg-black/90 p-1.5 shadow-lg backdrop-blur" >
211+ < ul className = "flex flex-col gap-1" >
212+ { RSTACK_REPOS . map ( ( repo ) => (
213+ < li key = { repo . url } >
214+ < a
215+ href = { repo . url }
216+ target = "_blank"
217+ rel = "noreferrer"
218+ className = "block rounded-md px-2.5 py-1.5 text-sm text-foreground/85 transition hover:bg-white/10 hover:text-white"
219+ onClick = { ( ) => setIsRepoMenuOpen ( false ) }
220+ >
221+ { repo . label }
222+ </ a >
223+ </ li >
224+ ) ) }
225+ </ ul >
226+ </ div >
227+ ) : null }
228+ </ div >
229+ </ div >
128230 < Select
129231 value = { selectedStack }
130232 onValueChange = { ( value ) => setSelectedStack ( value as StackId ) }
0 commit comments