Skip to content

Commit 5ba2af4

Browse files
Andrey Cheptsovclaude
andcommitted
Add Resources nav dropdown and unify Cloudscape popup/hover styling
- Top nav: replace the standalone Blog link with a hover-opening "Resources" dropdown (Case studies / Benchmarks / Blog); mirror it as an expandable section in the mobile SideNavigation. - Dropdown popups: larger, consistent group-header/item label sizing, label/description spacing, and top/bottom breathing room that a first/last item's hover fill covers (fixes the dark-mode border being painted over). - Unify the hover tint across normal buttons, cards, and dropdown items via a single --cs-hover variable. - Footer column links now match the column heading size. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c6a7e53 commit 5ba2af4

4 files changed

Lines changed: 151 additions & 19 deletions

File tree

website/src/cloudscape-overrides.css

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ body {
7777
border-inline: 0 !important;
7878
}
7979

80-
/* 5) "Get started" dropdown popup (portaled to <body>, so targeted by stable prefix).
80+
/* 5) Dropdown menu popups (the "Get started" and "Resources" menus). Targeted by stable
81+
class prefix so the rules apply wherever the popup renders.
8182
a) Fixed width, flat (no drop-shadow), and a single uniform 0.5px border on all four
8283
sides. By default Cloudscape draws top/bottom on the wrapper (1px) and left/right on
8384
a ::after — so we set the wrapper border ourselves and drop the ::after layer. */
@@ -89,10 +90,11 @@ body {
8990
[class*='awsui_dropdown-content-wrapper']::after {
9091
border: 0 !important;
9192
}
92-
/* b) Group headers ("Products" / "Login"): same weight as item labels (600), but muted
93-
color so the groups read as quiet labels. */
93+
/* b) Group headers ("Products" / "Login"): same weight (600) and size (16px) as the item
94+
labels, but muted color so the groups read as quiet labels. */
9495
[class*='awsui_header_16mm3'] {
9596
font-weight: 600 !important;
97+
font-size: 16px !important;
9698
color: var(--cs-muted) !important;
9799
padding-inline: 16px !important;
98100
}
@@ -103,15 +105,41 @@ body {
103105
}
104106
[class*='awsui_menu-item'] [class*='awsui_main-row'] {
105107
font-weight: 600;
108+
font-size: 15px; /* (Cloudscape's default popup label is 14px) */
106109
}
107110
/* The hovered item still picked up a border in dark mode (the token override didn't hold
108111
there), so force it off on the highlighted item itself (the cue is the bg tint). */
109112
[class*='awsui_item-element'][class*='awsui_highlighted'] {
110113
border-color: transparent !important;
111114
}
112-
/* Keep the description muted at all times — Cloudscape brightens it on highlight. */
115+
/* Keep the description muted at all times — Cloudscape brightens it on highlight. A hair of
116+
separation (1.5px) from the bold label above so the two lines don't read as one block. */
113117
[class*='awsui_secondary-text'] {
114118
color: var(--cs-muted) !important;
119+
margin-block-start: 1.5px;
120+
}
121+
/* A little breathing room at the top and bottom of the popup (inside the border). Placed on
122+
the first/last rows rather than on the list itself, so a hovered first/last item's
123+
background fills that space instead of leaving a thin un-highlighted strip against the
124+
border (the hover tint is painted on the item-element, which is what carries the padding). */
125+
[class*='awsui_options-list'] {
126+
padding-block: 0 !important;
127+
/* Cloudscape pulls the list 1px into the wrapper border (decrease-block-margin) to overlap
128+
its default 1px divider. With our single hairline border that just lets a hovered
129+
first/last item's fill paint over the border (most visible in dark mode) — so sit the
130+
list flush inside the border instead. */
131+
margin-block: 0 !important;
132+
}
133+
[class*='awsui_options-list'] > :first-child {
134+
padding-block-start: 6px !important;
135+
}
136+
/* Last item — flat list (ButtonDropdown without groups, e.g. the Resources menu). */
137+
[class*='awsui_options-list'] > [class*='awsui_item-element']:last-child {
138+
padding-block-end: 6px !important;
139+
}
140+
/* Last item — grouped list: the last item inside the last category (e.g. "Get started"). */
141+
[class*='awsui_options-list'] > [class*='awsui_category']:last-child [class*='awsui_item-element']:last-child {
142+
padding-block-end: 6px !important;
115143
}
116144
/* d) Push the external-link icon to the right edge of the item. It's rendered inline at
117145
the end of the label, so make the label row fill the width and flex the icon out.

website/src/cloudscape-theme.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import type { ButtonProps } from '@cloudscape-design/components/button';
1111
const TEXT = { light: '#16191f', dark: '#f2f3f3' }; // body text color
1212
const SURFACE = { light: '#ffffff', dark: '#0f141d' }; // page background — used as the label color on filled buttons
1313
// Hover states, light + theme-aware: filled (primary) buttons shift to a slightly softer
14-
// shade; outlined (normal) buttons get a faint tint instead of a solid fill.
14+
// shade; outlined (normal) buttons, cards, and dropdown items all share one faint tint
15+
// (--cs-hover, defined per theme in styles.css) so every interactive surface hovers alike.
1516
const HOVER_FILL = 'var(--cs-btn-hover)'; // filled-button hover fill (defined per theme in styles.css; shared with the split-button override)
16-
const HOVER_TINT = { light: 'rgba(22, 25, 31, 0.05)', dark: 'rgba(242, 243, 243, 0.085)' };
1717
const FONT = 'var(--font-base)'; // single source of truth: the Geist stack defined in styles.css
1818

1919
const tokens = {
@@ -84,8 +84,8 @@ const tokens = {
8484
// Normal (secondary) button: outlined, transparent by default; on hover/active a faint
8585
// tint (not a solid fill). Border + label stay the text color.
8686
colorBackgroundButtonNormalDefault: 'transparent',
87-
colorBackgroundButtonNormalHover: HOVER_TINT,
88-
colorBackgroundButtonNormalActive: HOVER_TINT,
87+
colorBackgroundButtonNormalHover: 'var(--cs-hover)',
88+
colorBackgroundButtonNormalActive: 'var(--cs-hover)',
8989
colorBorderButtonNormalDefault: TEXT,
9090
colorBorderButtonNormalHover: TEXT,
9191
colorBorderButtonNormalActive: TEXT,

website/src/components/SiteNavigation.tsx

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useRef, useState } from 'react';
22
import { useLocation, useNavigate } from 'react-router-dom';
33
import Button from '@cloudscape-design/components/button';
44
import ButtonDropdown, { ButtonDropdownProps } from '@cloudscape-design/components/button-dropdown';
@@ -11,13 +11,72 @@ import { BLOG_URL, DOCS_URL, ROUTES, docsUrl } from '../routes';
1111
const dstackGithubUrl = 'https://github.com/dstackai/dstack';
1212
const 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).
1615
const 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.
2281
const productDropdownItems: ButtonDropdownProps.Items = [
2382
{
@@ -39,7 +98,18 @@ const productDropdownItems: ButtonDropdownProps.Items = [
3998
// Items for the mobile slide-out navigation.
4099
const 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"

website/src/styles.css

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
--cs-link: #0972d3;
2424
--cs-border: #16191f;
2525
--cs-panel: #f4f4f7;
26-
--cs-hover: rgba(22, 25, 31, 0.035); /* faint, light hover tint for cards */
26+
--cs-hover: rgba(22, 25, 31, 0.05); /* shared faint hover tint: normal buttons, cards, dropdown items */
2727
--cs-btn-hover: #272d38; /* filled (primary) button hover fill — shared with the split-button override */
2828
--cs-seg-divider: rgba(255, 255, 255, 0.22); /* split-button segment divider (light line on the dark fill) */
2929
--cs-dark: #0f141d;
@@ -56,7 +56,7 @@
5656
--cs-link: #58a6ff;
5757
--cs-border: #f2f3f3;
5858
--cs-panel: #1b212d;
59-
--cs-hover: rgba(242, 243, 243, 0.06);
59+
--cs-hover: rgba(242, 243, 243, 0.085);
6060
--cs-btn-hover: #e2e5e8;
6161
--cs-seg-divider: rgba(0, 0, 0, 0.18); /* split-button segment divider (dark line on the light fill) */
6262
--hero-gradient: linear-gradient(
@@ -203,7 +203,10 @@ p {
203203
align-items: center;
204204
gap: 0.25rem;
205205
min-height: 42px;
206-
padding: 10px 16px;
206+
/* No horizontal padding: borderless text items are spaced purely by the nav's
207+
SpaceBetween gap, so the whitespace to a neighboring text item matches the whitespace
208+
to a bordered button (whose padding sits inside its border) — giving uniform gaps. */
209+
padding: 10px 0;
207210
border-radius: 21px;
208211
color: var(--cs-text);
209212
font-weight: 700;
@@ -228,6 +231,31 @@ p {
228231
border: 2px solid #879596;
229232
}
230233

234+
/* "Resources" top-nav dropdown (ResourcesHoverMenu): make the ButtonDropdown trigger read
235+
like the plain text menu links (.site-menu-link) — no border/background, bold 16px label,
236+
underline on hover — rather than the bordered "normal" button look. */
237+
.site-menu-dropdown-wrap {
238+
display: inline-flex;
239+
align-items: center;
240+
}
241+
.site-menu-dropdown [class*='awsui_button'] {
242+
border-color: transparent !important;
243+
background: transparent !important;
244+
color: var(--cs-text) !important;
245+
font-size: 16px !important; /* match the .site-menu-link items (e.g. Documentation) */
246+
font-weight: 700 !important;
247+
padding: 10px 0 !important; /* no horizontal padding — matches the text links */
248+
}
249+
.site-menu-dropdown [class*='awsui_button']:hover {
250+
background: transparent !important;
251+
text-decoration: underline;
252+
text-underline-offset: 0.2em;
253+
}
254+
/* The menu opens on hover, so the dropdown caret is redundant — hide it. */
255+
.site-menu-dropdown [class*='awsui_button'] [class*='awsui_icon'] {
256+
display: none !important;
257+
}
258+
231259
.home-hero {
232260
position: relative;
233261
overflow: visible;
@@ -998,10 +1026,13 @@ p {
9981026
text-decoration-thickness: 2px !important;
9991027
}
10001028

1001-
/* Footer column links: full-strength text, 14px, no underline until hover. */
1029+
/* Footer column links: full-strength text, same 16px as the column headings, no underline
1030+
until hover. */
10021031
.footer-links a {
10031032
color: var(--cs-text) !important;
1004-
font-size: 14px;
1033+
/* Cloudscape <Link> sets 14px via its boosted awsui_font-size-body-m class, so override
1034+
with !important to match the 16px column headings. */
1035+
font-size: 16px !important;
10051036
text-decoration-line: none !important;
10061037
}
10071038

0 commit comments

Comments
 (0)