Visitar URL original
feat(site): simplify the docs header menu and move the V1 notice into the header by iamdavidhill · Pull Request #63399 · anomalyco/opencode · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions services/site/src/component/site/css-variants.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"data-menu-align": "edge",
"data-wide-align": null,
"data-nav-numbers": "1",
"data-docs-nav": "split",
"data-docs-nav": "combined",
"data-section-menu": "list",
"data-docs-variants": "typography-4 lists-3 blockquote-2 callout-3 tabs-6 code-6 codeTitle-6 codeLines-6 codeDiff-6 terminal-6 fileTree-6 steps-2 cards-3 tables-5 badges-4 details-2 media-2 api-7 config-6 cli-6 pagination-4 breadcrumbs-3 search-4 switchers-2 editLink-4 feedback-5 inlineCode-6",
"data-status-bars": "4",
Expand All @@ -41,7 +41,7 @@
["[data-component=\"hero-install\"][data-mode=\"row\"]", "data-style", "inline"],
["[data-component=\"hero\"][data-design~=\"hero\"]", "data-title-size", "fitted"],
["[data-component=\"page-title\"]", "data-sticky", null],
["[data-component=\"docs-version-notice\"]", "data-variant", "frosted top"],
["[data-component=\"docs-version-notice\"]", "data-variant", "inverted"],
["[data-component=\"products\"]", "data-tone", "color"],
["[data-component=\"badges\"]", "data-style", "sans"],
["[data-component=\"badges\"]", "data-layout", "tabs"],
Expand Down
19 changes: 2 additions & 17 deletions services/site/src/component/site/docs-lockup.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
position: relative;
display: inline-flex;
align-items: center;
gap: 6px;
/* Label and chevron are one button, so the chevron sits close, as in the product navs. */
gap: 4px;

/* OpenTUI Mono's caps sit 0.325px below its line box's centre; this puts them on the logo's centre. */
padding: 0 6px 0.65px;
Expand Down Expand Up @@ -439,22 +440,6 @@
}
}

/* The section word also opens the OpenCode product menu on hover (packages/nav-menus), so hovered, open and pressed it
takes the one name fill every OpenCode nav shares (--nav-name-hover), never a darker one; the chevron keeps its own
states for the sections menu. */
[data-site] [data-component="docs-lockup"] [data-slot="word"][data-nav-product] {
@media (hover: hover) and (pointer: fine) {
&:hover {
background: var(--nav-name-hover);
}
}

&:active,
&[aria-expanded="true"] {
background: var(--nav-name-hover);
}
}

/* The logo before the lockup stays a plain link (no fill); its keyboard ring, drawn around the mark
(site.css), is 2px like the segments'. */
[data-site]
Expand Down
59 changes: 22 additions & 37 deletions services/site/src/component/site/docs-lockup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ type Item = {
/**
* Settings › Docs switcher › lockup: the docs section word after the site's logo, like the Data and
* Console headers (public/nav-demo/switcher.js). One button opens a menu of the docs sections, where
* "Docs" has a submenu of versions. On an older version, "/ V1" after the word shows it and switches
* back. The menus are always in the page, hidden while closed, so pages that never hydrate (the docs)
* "Docs" has a submenu of versions. On the V1 docs the word reads "V1 docs" and the same menu marks V1
* under "Docs". The menus are always in the page, hidden while closed, so pages that never hydrate (the docs)
* can open them from a delegated listener (src/docs/client.ts).
*/
export function DocsLockup(props: {
Expand All @@ -63,10 +63,18 @@ export function DocsLockup(props: {
}
}) {
const i18n = useI18n()
// On the V1 docs the product name says so: "V1 docs", with the same chevron and menu as the latest docs.
const v1 = () => props.version?.current === "v1"
const name = (label: string) =>
label === "Docs"
? docsSectionName(i18n, label)
: i18n.t("docs.lockup.sectionDocs", { section: docsSectionName(i18n, label) })
v1()
? i18n.t("docs.lockup.sectionDocs", { section: "V1" })
: label === "Docs"
? docsSectionName(i18n, label)
: i18n.t("docs.lockup.sectionDocs", { section: docsSectionName(i18n, label) })
// The sections menu names each section's docs: "CLI docs", "API docs". The main docs are about OpenCode
// itself, so they're "OpenCode docs" rather than "Docs docs".
const menuName = (label: string) =>
i18n.t("docs.lockup.sectionDocs", { section: label === "Docs" ? "OpenCode" : docsSectionName(i18n, label) })
// V1 is a version, not a section: "Docs" has a submenu of versions, V2 (these docs) and V1 (the old
// docs site). The /docs demo can pass its own (?version= switches in place).
const sections = () => docsProducts.filter((product) => product.label !== "V1")
Expand Down Expand Up @@ -115,8 +123,8 @@ export function DocsLockup(props: {
triggerLabel={(label) => i18n.t("docs.lockup.switchLabel", { name: name(label) })}
items={sections().map((product) => ({
id: product.label,
label: docsSectionName(i18n, product.label),
ariaLabel: name(product.label),
label: menuName(product.label),
ariaLabel: menuName(product.label),
href: props.href?.(product.label) ?? product.href,
sub:
product.label !== "Docs"
Expand All @@ -132,30 +140,8 @@ export function DocsLockup(props: {
}))}
current={props.current}
onSelect={props.onSelect}
split={{ label: i18n.t("docs.lockup.switch"), onSelect: props.onSelect }}
split={{ label: i18n.t("docs.lockup.switch"), onSelect: props.onSelect, href: v1() ? v1DocsUrl() : undefined }}
/>
{/* Only the main docs have versions, and only an older one shows here. */}
<Show when={props.current === "Docs" && props.version?.current !== "latest" && props.version}>
{(version) => (
<>
<span data-slot="separator" aria-hidden="true">
|
</span>
<Dropdown
id={`docs-version-menu${suffix}`}
variant="version"
menuLabel={i18n.t("docs.version.label")}
trigger={(id) => docsVersions.find((item) => item.id === id)!.label}
triggerLabel={(id) =>
i18n.t("docs.version.switch", { version: docsVersions.find((item) => item.id === id)!.label })
}
items={versionItems(version())}
current={version().current}
onSelect={(id) => version().onSelect(id as DocsVersion)}
/>
</>
)}
</Show>
</div>
)
if (props.inline) return lockup()
Expand Down Expand Up @@ -186,8 +172,11 @@ function Dropdown(props: {
items: Item[]
current: string
onSelect?: (id: string) => void
/** Settings › Docs nav › split: the word links to the section's home; `label` names the chevron. */
split?: { label: string; onSelect?: (id: string) => void }
/**
* Settings › Docs nav › split: the word links to the section's home (or `href`, the V1 docs' home);
* `label` names the chevron.
*/
split?: { label: string; onSelect?: (id: string) => void; href?: string }
}) {
const i18n = useI18n()
const [open, setOpen] = createSignal(false)
Expand Down Expand Up @@ -323,11 +312,7 @@ function Dropdown(props: {
<a
data-slot="word"
data-design="docsNav sectionMenuStyle menuActive menuColor menuAlign docsSubmenu"
href={props.items[currentIndex()].href}
data-nav-product="docs"
data-nav-product-chevron="none"
aria-haspopup="menu"
aria-expanded="false"
href={split().href ?? props.items[currentIndex()].href}
aria-label={i18n.t("docs.lockup.home", { name: props.trigger(props.items[currentIndex()].id) })}
onClick={(event) => {
close()
Expand Down
27 changes: 24 additions & 3 deletions services/site/src/component/site/docs-version-notice.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import "./docs-version-notice.css"
import { useI18n } from "~/context/i18n"
import { Show } from "solid-js"
import { siteSettings } from "./settings"

/** On the V1 docs: a bar at the top of the content column, with a way back to the latest docs. */
export function DocsVersionNotice(props: { latest: string; onLatest?: () => void }) {
/**
* On the V1 docs: a bar with a way back to the latest docs, at the top of the content column, or in the
* logo row (`placement="header"`, the docs), where the sentence shortens with "…" before the link does.
* On phones the header placement is a bar on the screen's bottom edge saying only "V1 docs" (docs.css).
*/
export function DocsVersionNotice(props: { latest: string; onLatest?: () => void; placement?: "header" }) {
const i18n = useI18n()
return (
<aside
data-component="docs-version-notice"
data-variant={siteSettings.docsVersionNotice}
data-design="docsVersionNotice"
data-placement={props.placement}
aria-label={i18n.t("docs.version.label")}
>
<span data-slot="badge">V1</span>
<Show when={props.placement}>
<span data-slot="short">{i18n.t("docs.version.v1Docs")}</span>
</Show>
<p>
{i18n.t("docs.version.notice")}{" "}
<span data-slot="sentence">{i18n.t("docs.version.notice")}</span>{" "}
<a
href={props.latest}
onClick={(event) => {
Expand All @@ -24,8 +33,20 @@ export function DocsVersionNotice(props: { latest: string; onLatest?: () => void
}}
>
{i18n.t("docs.version.switchToLatest")}
<Show when={props.placement}>
<LinkChevron />
</Show>
</a>
</p>
</aside>
)
}

/** The 16px chevron after the logo row's links, toward the reading direction (mirrored in RTL by CSS). */
export function LinkChevron() {
return (
<svg data-slot="link-chevron" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M6 4L10 8L6 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" />
</svg>
)
}
4 changes: 2 additions & 2 deletions services/site/src/component/site/settings-defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,8 @@
"consoleCopyColumns": "2 columns",
"docsSwitcher": "lockup",
"docsSubmenu": "flyout",
"docsNav": "split",
"docsVersionNotice": "frosted top",
"docsNav": "combined",
"docsVersionNotice": "inverted",
"terminalInstall": "button",
"carouselPreset": "four across",
"carousel": {
Expand Down
4 changes: 2 additions & 2 deletions services/site/src/component/site/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1051,8 +1051,8 @@ function createSchema() {
consoleCopyColumns: z.enum(sectionColumns).catch("2 columns"),
docsSwitcher: z.enum(docsSwitchers).catch("lockup"),
docsSubmenu: z.enum(docsSubmenus).catch("flyout"),
docsNav: z.enum(docsNavs).catch("split"),
docsVersionNotice: z.enum(docsVersionNotices).catch("frosted top"),
docsNav: z.enum(docsNavs).catch("combined"),
docsVersionNotice: z.enum(docsVersionNotices).catch("inverted"),
terminalInstall: z.enum(terminalInstalls).catch("button"),
carouselPreset: z.enum(carouselPresetNames).catch("four across"),
carousel: z
Expand Down
32 changes: 30 additions & 2 deletions services/site/src/docs/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ function enter(menu: HTMLElement) {
menu.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 140, easing: "ease-out" })
}

// The versions flyout appears softly beside "Docs": a 140ms fade with a 3px nudge from the row it opens
// from, on the strong ease-out, transform and opacity only. Only its first showing in an open menu
// animates; coming back to "Docs" shows it at once, and it hides at once. Reduced motion shows it at once.
let flown = false
function flyIn(submenu: HTMLElement) {
if (flown || matchMedia("(prefers-reduced-motion: reduce)").matches) return
flown = true
const from = `translateX(${html.dir === "rtl" ? 3 : -3}px)`
submenu.animate(
[
{ opacity: 0, transform: from },
{ opacity: 1, transform: "translateX(0)" },
],
{ duration: 140, easing: "cubic-bezier(0.23, 1, 0.32, 1)" },
)
}

// The docs lockup (component/site/docs-lockup.tsx): the chevron opens the sections menu, where "Docs"
// has its versions in a flyout beside it, or nested under it on phones and touch screens. An older
// version adds a second menu, of the versions, so each dropdown in it opens on its own.
Expand Down Expand Up @@ -177,6 +194,7 @@ function openLockup(root: HTMLElement, focus?: "first" | "last" | "current") {
}

function closeLockup(refocus?: boolean) {
flown = false
if (!lockup) return
const root = lockup
lockup = undefined
Expand All @@ -185,7 +203,10 @@ function closeLockup(refocus?: boolean) {
$$("[aria-haspopup=menu]:not([role=menuitem], [data-nav-product])", root).forEach((button) =>
button.setAttribute("aria-expanded", "false"),
)
if (refocus) ($("[data-slot=toggle]", root) ?? $("[data-slot=trigger]", root))?.focus()
if (refocus)
$$("[data-slot=toggle], [data-slot=trigger]", root)
.find((button) => button.offsetParent)
?.focus()
}

function toggleSub(root: HTMLElement, open: boolean, focus?: boolean) {
Expand All @@ -196,7 +217,7 @@ function toggleSub(root: HTMLElement, open: boolean, focus?: boolean) {
parent.toggleAttribute("data-open", open)
const submenu = $("[data-slot=submenu]", parent)!
submenu.hidden = !open
if (opening && menuOf(root).dataset.submenu === "flyout") enter(submenu)
if (opening && menuOf(root).dataset.submenu === "flyout") flyIn(submenu)
$$("[aria-expanded]", parent).forEach((item) => item.setAttribute("aria-expanded", String(open)))
if (open && focus)
($('[aria-current="page"]', $("[data-slot=submenu]", parent)!) ?? $("[data-slot=submenu] a", parent))?.focus()
Expand All @@ -210,6 +231,13 @@ on("click", (event) => {
return lockup === root ? closeLockup() : openLockup(root)
const expand = target.closest("[data-slot=expand]")
if (expand) return toggleSub(root, expand.getAttribute("aria-expanded") !== "true")
// Nested (phones, touch and narrow windows): tapping "Docs" opens and closes its versions in place,
// like its "›", instead of leaving for the docs home, which V2 still links. The flyout keeps the link.
const parent = target.closest("[data-slot=parent] > [role=menuitem]")
if (parent && menuOf(root).dataset.submenu === "nested") {
event.preventDefault()
return toggleSub(root, parent.getAttribute("aria-expanded") !== "true")
}
if (target.closest("a")) closeLockup()
})

Expand Down
10 changes: 6 additions & 4 deletions services/site/src/docs/layouts/DocsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function DocsLayout(props: {
const contents = props.headings
.filter((heading) => heading.depth === 2 || heading.depth === 3)
.map((heading) => ({ id: heading.slug, title: heading.text, level: heading.depth as 2 | 3 }))
// The lockup's versions: this page in each (the versions menu, and "| V1" on V1 pages).
// The lockup's versions: this page in each (the versions menu under "Docs"; V1 pages read "V1 docs").
const version = () =>
props.versions && {
current: props.set.id,
Expand Down Expand Up @@ -187,6 +187,11 @@ export function DocsLayout(props: {
</div>
</div>
</aside>
{/* V1: the notice sits in the logo row, after the sidebar's label so it can start past a long one
(docs.css); phones show it as a bar on the screen's bottom edge. */}
<Show when={props.set.id === "v1" && props.versions}>
{(versions) => <DocsVersionNotice latest={versions().latest} placement="header" />}
</Show>
{/* Mobile layout: the Contents rail folded into a bar stuck under the
section row from load. It names the section being read (client.ts swaps the label as you scroll)
and keeps "Contents" in its accessible name. Hidden beside the rail on wide screens. */}
Expand All @@ -213,9 +218,6 @@ export function DocsLayout(props: {

<main data-component="content" id="content" tabindex={-1}>
<div data-slot="page">
<Show when={props.set.id === "v1" && props.versions}>
{(versions) => <DocsVersionNotice latest={versions().latest} />}
</Show>
<article
data-component="docs-article"
data-section="article"
Expand Down
Loading
Loading