fix(stats): correct breadcrumb behavior

This commit is contained in:
Adam 2026-07-13 10:52:43 -05:00
parent 49d997aec3
commit 92cc8f553a
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
4 changed files with 85 additions and 8 deletions

View file

@ -39,6 +39,7 @@ import {
uniqueComparisonPairs,
type ComparisonModelRef,
} from "../compare-cards"
import { createBreadcrumbMenuRoot } from "../breadcrumb-menu"
import {
applyThemePreference,
Footer,
@ -284,9 +285,10 @@ function ModelHero(props: {
const labModels = () =>
props.catalogData?.labs.find((lab) => lab.id === providerSlug(labId()))?.models ??
(props.catalog ? [props.catalog] : [])
const menuRoot = createBreadcrumbMenuRoot()
return (
<section id="overview" data-section="model-hero">
<nav data-component="model-hero-breadcrumb" aria-label="Data breadcrumb">
<nav ref={menuRoot} data-component="model-hero-breadcrumb" aria-label="Data breadcrumb">
<a data-slot="model-hero-crumb" href={language.route(import.meta.env.BASE_URL)}>
Data
</a>
@ -294,14 +296,14 @@ function ModelHero(props: {
<Show
when={labs().length > 0}
fallback={
<span data-slot="model-hero-crumb" data-current="true">
<span data-slot="model-hero-crumb" data-menu="true">
<span>{props.labName}</span>
<ChevronDownIcon />
</span>
}
>
<details data-component="model-hero-menu">
<summary data-slot="model-hero-crumb" data-current="true">
<summary data-slot="model-hero-crumb" data-menu="true">
<span>{props.labName}</span>
<ChevronDownIcon />
</summary>
@ -324,14 +326,19 @@ function ModelHero(props: {
<Show
when={labModels().length > 0}
fallback={
<span data-slot="model-hero-crumb" data-current="true" aria-current="page">
<span data-slot="model-hero-crumb" data-menu="true" data-current="true" aria-current="page">
<span>{modelName()}</span>
<ChevronDownIcon />
</span>
}
>
<details data-component="model-hero-menu">
<summary data-slot="model-hero-crumb" data-current="true" aria-current="page">
<summary
data-slot="model-hero-crumb"
data-menu="true"
data-current="true"
aria-current="page"
>
<span>{modelName()}</span>
<ChevronDownIcon />
</summary>

View file

@ -29,6 +29,7 @@ import { SectionHeading } from "../section-heading"
import { runStatsEffect } from "../../stats-runtime"
import { setStatsPageCacheHeaders } from "../stats-cache"
import { ComparisonCardsSection, modelRefFromCatalog, uniqueComparisonPairs } from "../compare-cards"
import { createBreadcrumbMenuRoot } from "../breadcrumb-menu"
import {
applyThemePreference,
Footer,
@ -205,8 +206,9 @@ function LabHero(props: { lab: ModelCatalogLab; labs: ModelCatalogLab[] }) {
function LabHeroBreadcrumb(props: { label: string; labs?: ModelCatalogLab[] }) {
const language = useLanguage()
const labs = () => props.labs ?? []
const menuRoot = createBreadcrumbMenuRoot()
return (
<nav data-component="lab-hero-breadcrumb" aria-label="Data breadcrumb">
<nav ref={menuRoot} data-component="lab-hero-breadcrumb" aria-label="Data breadcrumb">
<a data-slot="lab-hero-crumb" href={language.route(import.meta.env.BASE_URL)}>
Data
</a>

View file

@ -0,0 +1,52 @@
import { onCleanup, onMount } from "solid-js"
export function createBreadcrumbMenuRoot() {
let root: HTMLElement | undefined
onMount(() => {
const closeMenus = (except?: HTMLDetailsElement) => {
root?.querySelectorAll<HTMLDetailsElement>("details[open]").forEach((menu) => {
if (menu !== except) menu.open = false
})
}
const onPointerDown = (event: PointerEvent) => {
const path = event.composedPath()
root?.querySelectorAll<HTMLDetailsElement>("details[open]").forEach((menu) => {
if (!path.includes(menu)) menu.open = false
})
}
const onKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Escape") return
const menu = root?.querySelector<HTMLDetailsElement>("details[open]")
if (!menu) return
event.preventDefault()
menu.open = false
menu.querySelector<HTMLElement>("summary")?.focus()
}
const onToggle = (event: Event) => {
if (!(event.target instanceof HTMLDetailsElement) || !event.target.open) return
closeMenus(event.target)
}
const onFocusOut = () => {
queueMicrotask(() => {
if (root?.contains(document.activeElement)) return
closeMenus()
})
}
document.addEventListener("pointerdown", onPointerDown)
document.addEventListener("keydown", onKeyDown)
root?.addEventListener("toggle", onToggle, true)
root?.addEventListener("focusout", onFocusOut)
onCleanup(() => {
document.removeEventListener("pointerdown", onPointerDown)
document.removeEventListener("keydown", onKeyDown)
root?.removeEventListener("toggle", onToggle, true)
root?.removeEventListener("focusout", onFocusOut)
})
})
return (element: HTMLElement) => {
root = element
}
}

View file

@ -3113,10 +3113,18 @@
white-space: nowrap;
}
[data-page="stats"] [data-slot="model-hero-crumb"][data-current="true"] {
[data-page="stats"] a[data-slot="model-hero-crumb"] {
color: var(--stats-hero-muted);
}
[data-page="stats"] [data-slot="model-hero-crumb"][data-menu="true"] {
padding-right: 4px;
}
[data-page="stats"] [data-slot="model-hero-crumb"][data-current="true"] {
color: var(--stats-text);
}
[data-page="stats"] [data-component="model-hero-menu"] {
position: relative;
display: inline-block;
@ -3151,7 +3159,7 @@
flex: 0 0 auto;
width: 16px;
height: 16px;
color: var(--stats-faint);
color: currentColor;
}
[data-page="stats"] [data-component="model-hero-menu"][open] [data-slot="model-hero-crumb"] svg {
@ -5533,6 +5541,14 @@
white-space: nowrap;
}
[data-page="stats"] a[data-slot="compare-home-crumb"] {
color: var(--stats-hero-muted);
}
[data-page="stats"] [data-slot="compare-home-crumb"][data-current="true"] {
color: var(--stats-text);
}
[data-page="stats"] a[data-slot="compare-home-crumb"]:hover,
[data-page="stats"] a[data-slot="compare-home-crumb"]:focus-visible {
color: var(--stats-text);