mirror of
https://github.com/strands-agents/harness-sdk.git
synced 2026-10-02 02:44:48 +08:00
fix(site): show full product sidebar in mobile nav and fix dark LinkCard hover (#4780)
Co-authored-by: Timothy Moreton <tmoreton@amazon.com>
This commit is contained in:
co-authored by
Timothy Moreton
parent
e0c95eeead
commit
47ad14bacb
@@ -0,0 +1,127 @@
|
||||
---
|
||||
/**
|
||||
* A product's full docs sidebar for the mobile menu. Mirrors the desktop
|
||||
* sidebar shape: first-level bands render as open headings, deeper subgroups
|
||||
* collapse behind a native <details> toggle.
|
||||
*/
|
||||
import type { ProductNavItem } from '../util/product-nav';
|
||||
|
||||
interface Props {
|
||||
items: ProductNavItem[];
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
const { items, depth = 0 } = Astro.props;
|
||||
---
|
||||
|
||||
<ul class="mnav" role="list">
|
||||
{items.map((item) =>
|
||||
item.type === 'link' ? (
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class="mnav__link"
|
||||
{...(item.external ? { target: '_blank', rel: 'noopener' } : {})}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
) : depth === 0 ? (
|
||||
<li class="mnav__band">
|
||||
<span class="mnav__band-label">{item.label}</span>
|
||||
<Astro.self items={item.items} depth={depth + 1} />
|
||||
</li>
|
||||
) : (
|
||||
<li>
|
||||
<details class="mnav__group">
|
||||
<summary class="mnav__link mnav__summary">{item.label}</summary>
|
||||
<Astro.self items={item.items} depth={depth + 1} />
|
||||
</details>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
.mnav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mnav .mnav {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mnav__group > .mnav {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.mnav__band-label {
|
||||
display: block;
|
||||
font-family: 'Space Grotesk', system-ui, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--nav-fg);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.mnav__band + .mnav__band,
|
||||
li + .mnav__band {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.mnav__link {
|
||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
color: var(--strands-grey-400);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mnav__link:hover {
|
||||
color: var(--nav-fg);
|
||||
}
|
||||
|
||||
.mnav__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mnav__summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mnav__summary::after {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 4px;
|
||||
border-right: 1.5px solid currentColor;
|
||||
border-bottom: 1.5px solid currentColor;
|
||||
transform: rotate(45deg);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.mnav__group[open] > .mnav__summary::after {
|
||||
transform: rotate(-135deg);
|
||||
}
|
||||
|
||||
/* Language-scoped rows (Quickstart / API reference) follow the header language toggle, as in
|
||||
the docs sidebar. Default (attr unset) = TypeScript. */
|
||||
:global(html:not([data-lang-pref='python'])) li:has(> a[href*='/quickstart/python']),
|
||||
:global(html:not([data-lang-pref='python'])) li:has(> a[href*='/api/python']),
|
||||
:global(html[data-lang-pref='python']) li:has(> a[href*='/quickstart/typescript']),
|
||||
:global(html[data-lang-pref='python']) li:has(> a[href*='/api/typescript']) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -11,22 +11,27 @@ import LanguageToggle from './LanguageToggle.astro';
|
||||
import { getStarCount } from '../util/github';
|
||||
import { products } from '../config/navbar';
|
||||
import { pathWithBase } from '../util/links';
|
||||
import { getProductNav } from '../util/product-nav';
|
||||
import MobileNavTree from './MobileNavTree.astro';
|
||||
|
||||
const stars = await getStarCount();
|
||||
|
||||
// The "Projects" dropdown is organized around products (Strands harness, SDK,
|
||||
// Shell, Evals) from navigation.yml, so the dropdown and the docs stay
|
||||
// aligned. Each product renders as one entry linking to its hub; `pages` drive
|
||||
// the mobile accordion. Labs (experimental projects) has its own docs hub, so
|
||||
// aligned. Each product renders as one entry linking to its hub; its full
|
||||
// docs sidebar (`nav`) drives the mobile accordion. Labs (experimental projects) has its own docs hub, so
|
||||
// it's appended here linking to that page.
|
||||
const docsCategories = [
|
||||
...products.map((p) => ({
|
||||
title: p.slug,
|
||||
description: p.description,
|
||||
href: p.href,
|
||||
accent: p.accent,
|
||||
pages: p.pages,
|
||||
})),
|
||||
...(await Promise.all(
|
||||
products.map(async (p) => ({
|
||||
title: p.slug,
|
||||
description: p.description,
|
||||
href: p.href,
|
||||
accent: p.accent,
|
||||
pages: p.pages,
|
||||
nav: await getProductNav(p.href),
|
||||
})),
|
||||
)),
|
||||
{
|
||||
title: '/labs',
|
||||
description: 'Experimental projects pushing the boundaries of what agents can do.',
|
||||
@@ -144,7 +149,7 @@ const learnCategories = [
|
||||
</button>
|
||||
<div class="mobile-menu__section-body">
|
||||
{section.categories.map((cat) =>
|
||||
cat.pages ? (
|
||||
cat.nav?.length ? (
|
||||
<div class="mobile-menu__cat" data-mobile-cat>
|
||||
<div class="mobile-menu__cat-head">
|
||||
{cat.href ? (
|
||||
@@ -181,22 +186,9 @@ const learnCategories = [
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ul class="mobile-menu__pages" role="list">
|
||||
{cat.pages.map((page) => (
|
||||
<li>
|
||||
<a
|
||||
href={page.href}
|
||||
class="mobile-menu__page"
|
||||
{...(page.langAware ? { 'data-lang-href': '' } : {})}
|
||||
{...(page.external && page.href.startsWith('http')
|
||||
? { target: '_blank', rel: 'noopener' }
|
||||
: {})}
|
||||
>
|
||||
{page.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div class="mobile-menu__pages">
|
||||
<MobileNavTree items={cat.nav} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
@@ -924,19 +916,6 @@ const learnCategories = [
|
||||
.mobile-menu__cat.is-open .mobile-menu__pages {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mobile-menu__page {
|
||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
color: var(--strands-grey-400);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mobile-menu__page:hover {
|
||||
color: var(--nav-fg);
|
||||
}
|
||||
}
|
||||
/* ---------- Spacebar frog easter egg ---------- */
|
||||
/* Each spacebar press hops the frog one step forward (the hop arc + forward
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
--sl-color-gray-4: #6f6d6d;
|
||||
--sl-color-gray-5: var(--strands-charcoal-2);
|
||||
--sl-color-gray-6: var(--strands-charcoal-1);
|
||||
--sl-color-gray-7: var(--strands-charcoal-1);
|
||||
--sl-color-black: var(--strands-black);
|
||||
--sl-color-bg: var(--strands-black);
|
||||
--sl-color-hairline-shade: var(--strands-charcoal-1);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import path from 'node:path'
|
||||
import { getCollection } from 'astro:content'
|
||||
import { loadSidebarFromConfig, type StarlightSidebarItem } from '../sidebar'
|
||||
import { pathWithBase } from './links'
|
||||
|
||||
/** A page link or a labeled group of them, mirroring a product's docs sidebar. */
|
||||
export type ProductNavItem =
|
||||
| { type: 'link'; label: string; href: string; external?: boolean }
|
||||
| { type: 'group'; label: string; items: ProductNavItem[] }
|
||||
|
||||
// Built once per build: the nav renders on every page but the tree is static.
|
||||
let cached: Promise<StarlightSidebarItem[]> | undefined
|
||||
let labelsCached: Promise<Map<string, string>> | undefined
|
||||
|
||||
function getSidebar(): Promise<StarlightSidebarItem[]> {
|
||||
cached ??= Promise.resolve(
|
||||
loadSidebarFromConfig(path.resolve('./src/config/navigation.yml'), path.resolve('./src/content'))
|
||||
)
|
||||
return cached
|
||||
}
|
||||
|
||||
/** Docs id -> sidebar label, resolved the same way Starlight does (sidebar.label, then title). */
|
||||
function getLabels(): Promise<Map<string, string>> {
|
||||
labelsCached ??= getCollection('docs').then(
|
||||
(docs) => new Map(docs.map((doc) => [doc.id, doc.data.sidebar?.label ?? doc.data.title]))
|
||||
)
|
||||
return labelsCached
|
||||
}
|
||||
|
||||
function toNavItem(item: StarlightSidebarItem, labels: Map<string, string>): ProductNavItem | null {
|
||||
if ('items' in item) {
|
||||
const items = item.items.map((child) => toNavItem(child, labels)).filter((c) => c !== null)
|
||||
return items.length > 0 ? { type: 'group', label: item.label, items } : null
|
||||
}
|
||||
if ('link' in item) {
|
||||
return { type: 'link', label: item.label, href: item.link, external: /^https?:\/\//.test(item.link) }
|
||||
}
|
||||
const label = item.label ?? labels.get(item.slug)
|
||||
if (!label) return null
|
||||
return { type: 'link', label, href: pathWithBase(`/${item.slug}/`) }
|
||||
}
|
||||
|
||||
function hasSlug(item: StarlightSidebarItem, slug: string): boolean {
|
||||
if ('items' in item) return item.items.some((child) => hasSlug(child, slug))
|
||||
return 'slug' in item && item.slug === slug
|
||||
}
|
||||
|
||||
/**
|
||||
* The full sidebar tree of the product whose hub page is `hubHref`, for the
|
||||
* mobile menu. Matched by hub page rather than label because product and
|
||||
* sidebar group labels differ (e.g. "Evals" vs "Evals SDK").
|
||||
*/
|
||||
export async function getProductNav(hubHref: string): Promise<ProductNavItem[]> {
|
||||
const [sidebar, labels] = await Promise.all([getSidebar(), getLabels()])
|
||||
const hubSlug = hubHref.replace(pathWithBase('/'), '').replace(/\/$/, '')
|
||||
const group = sidebar.find((item) => 'items' in item && hasSlug(item, hubSlug))
|
||||
if (!group || !('items' in group)) return []
|
||||
return group.items.map((item) => toNavItem(item, labels)).filter((i) => i !== null)
|
||||
}
|
||||
Reference in New Issue
Block a user