1- import { useState } from 'react' ;
1+ import { useRef , useState } from 'react' ;
22import { useLocation , useNavigate } from 'react-router-dom' ;
33import Button from '@cloudscape-design/components/button' ;
44import ButtonDropdown , { ButtonDropdownProps } from '@cloudscape-design/components/button-dropdown' ;
@@ -11,13 +11,72 @@ import { BLOG_URL, DOCS_URL, ROUTES, docsUrl } from '../routes';
1111const dstackGithubUrl = 'https://github.com/dstackai/dstack' ;
1212const externalIconAriaLabel = 'External link icon' ;
1313
14- // Primary links in the desktop top navigation. Documentation and Blog are served by
15- // MkDocs on the same origin, so they are plain links (full-page navigations).
14+ // Primary links in the desktop top navigation (plain same-origin MkDocs links).
1615const audienceNavItems : Array < { label : string ; href : string } > = [
1716 { label : 'Documentation' , href : DOCS_URL } ,
18- { label : 'Blog' , href : BLOG_URL } ,
1917] ;
2018
19+ // "Resources" top-nav dropdown: the blog landing plus its two main categories (all
20+ // same-origin MkDocs pages).
21+ const resourcesDropdownItems : ButtonDropdownProps . Items = [
22+ {
23+ id : 'case-studies' ,
24+ text : 'Case studies' ,
25+ secondaryText : 'How AI teams run training and inference with dstack.' ,
26+ href : `${ BLOG_URL } /case-studies/` ,
27+ } ,
28+ {
29+ id : 'benchmarks' ,
30+ text : 'Benchmarks' ,
31+ secondaryText : 'Comparing hardware, inference engines, and deployment setups for AI.' ,
32+ href : `${ BLOG_URL } /benchmarks/` ,
33+ } ,
34+ {
35+ id : 'blog' ,
36+ text : 'Blog' ,
37+ secondaryText : 'Major releases, industry reports, and product updates.' ,
38+ href : BLOG_URL ,
39+ } ,
40+ ] ;
41+
42+ // "Resources" dropdown that opens on hover and stays open while the cursor is over the
43+ // trigger OR the popup (the popup renders inside this wrapper, so hovering it still counts as
44+ // hovering the wrapper). Cloudscape's ButtonDropdown is click-only, so we open/close it by
45+ // reading aria-expanded and synthesizing a click on the trigger — click and keyboard keep
46+ // working unchanged. A short close delay bridges the gap between trigger and popup so moving
47+ // the cursor across it doesn't dismiss the menu. Desktop top-nav only (mobile uses SideNavigation).
48+ function ResourcesHoverMenu ( ) {
49+ const wrapRef = useRef < HTMLDivElement > ( null ) ;
50+ const closeTimer = useRef < number | undefined > ( undefined ) ;
51+
52+ const trigger = ( ) => wrapRef . current ?. querySelector ( 'button' ) ?? null ;
53+ const isOpen = ( ) => trigger ( ) ?. getAttribute ( 'aria-expanded' ) === 'true' ;
54+ const cancelClose = ( ) => {
55+ if ( closeTimer . current !== undefined ) {
56+ window . clearTimeout ( closeTimer . current ) ;
57+ closeTimer . current = undefined ;
58+ }
59+ } ;
60+ const openNow = ( ) => {
61+ cancelClose ( ) ;
62+ if ( ! isOpen ( ) ) trigger ( ) ?. click ( ) ;
63+ } ;
64+ const closeSoon = ( ) => {
65+ cancelClose ( ) ;
66+ closeTimer . current = window . setTimeout ( ( ) => {
67+ if ( isOpen ( ) ) trigger ( ) ?. click ( ) ;
68+ } , 140 ) ;
69+ } ;
70+
71+ return (
72+ < div ref = { wrapRef } className = "site-menu-dropdown-wrap" onMouseEnter = { openNow } onMouseLeave = { closeSoon } >
73+ < ButtonDropdown className = "site-menu-dropdown" items = { resourcesDropdownItems } ariaLabel = "Resources menu" >
74+ Resources
75+ </ ButtonDropdown >
76+ </ div >
77+ ) ;
78+ }
79+
2180// "Get started" dropdown items. secondaryText is shown under each label.
2281const productDropdownItems : ButtonDropdownProps . Items = [
2382 {
@@ -39,7 +98,18 @@ const productDropdownItems: ButtonDropdownProps.Items = [
3998// Items for the mobile slide-out navigation.
4099const mobileNavigationItems : SideNavigationProps . Item [ ] = [
41100 { type : 'link' , text : 'Documentation' , href : DOCS_URL } ,
42- { type : 'link' , text : 'Blog' , href : BLOG_URL } ,
101+ // The desktop "Resources" dropdown becomes an expandable section on mobile (SideNavigation
102+ // has no popups), matching the "Get started" section pattern below.
103+ {
104+ type : 'section' ,
105+ text : 'Resources' ,
106+ defaultExpanded : true ,
107+ items : [
108+ { type : 'link' , text : 'Case studies' , href : `${ BLOG_URL } /case-studies/` } ,
109+ { type : 'link' , text : 'Benchmarks' , href : `${ BLOG_URL } /benchmarks/` } ,
110+ { type : 'link' , text : 'Blog' , href : BLOG_URL } ,
111+ ] ,
112+ } ,
43113 { type : 'link' , text : 'GitHub' , href : dstackGithubUrl , external : true , externalIconAriaLabel } ,
44114 {
45115 type : 'section' ,
@@ -118,12 +188,15 @@ export function SiteNavigation({
118188 < span > dstack</ span >
119189 </ button >
120190 < nav className = "site-menu" aria-label = "Global" >
121- < SpaceBetween direction = "horizontal" size = "s " alignItems = "center" >
191+ < SpaceBetween direction = "horizontal" size = "l " alignItems = "center" >
122192 { audienceNavItems . map ( item => (
123193 < a key = { item . label } className = "site-menu-link" href = { item . href } >
124194 { item . label }
125195 </ a >
126196 ) ) }
197+ { /* Resources dropdown (Case studies / Benchmarks / Blog), styled to read like the
198+ plain text menu links above; opens on hover. */ }
199+ < ResourcesHoverMenu />
127200 < Button
128201 href = { dstackGithubUrl }
129202 target = "_blank"
0 commit comments