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
118 changes: 94 additions & 24 deletions apps/website/src/components/docs-navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import {
GlNav,
GlCollapsibleNav,
GlCollapsibleNavToggle,
GlNavButton,
GlNavItem,
GlNavProvider,
GlSubNav,
GlSubNavButton,
GlSubNavItem,
Expand All @@ -18,6 +22,9 @@ type DocsNavigationProps = {
entries: DocsNavigationEntry[];
};

const DESKTOP_NAV_QUERY = "(min-width: 1200px)";
const NAVBAR_TOGGLE_TARGET_ID = "documentation-navigation-toggle-target";

function formatGroupLabel(group: string) {
return group
.split("-")
Expand All @@ -26,9 +33,49 @@ function formatGroupLabel(group: string) {
}

export function DocsNavigation({ currentId, entries }: DocsNavigationProps) {
const [isOpen, setIsOpen] = useState(false);
Comment thread
NriotHrreion marked this conversation as resolved.
const [toggleTarget, setToggleTarget] = useState<HTMLElement | null>(null);
const rootEntries = entries.filter(({ id }) => !id.includes("/"));
const groupedEntries = new Map<string, DocsNavigationEntry[]>();

useEffect(() => {
const desktopQuery = window.matchMedia(DESKTOP_NAV_QUERY);
const syncOpenState = () => setIsOpen(desktopQuery.matches);

syncOpenState();
if(typeof desktopQuery.addEventListener === "function") {
desktopQuery.addEventListener("change", syncOpenState);
return () => desktopQuery.removeEventListener("change", syncOpenState);
}

desktopQuery.addListener(syncOpenState);
return () => desktopQuery.removeListener(syncOpenState);
}, []);

useEffect(() => {
if(!window.matchMedia(DESKTOP_NAV_QUERY).matches) setIsOpen(false);
}, [currentId]);
Comment thread
NriotHrreion marked this conversation as resolved.

useEffect(() => {
let targetObserver: MutationObserver | undefined;

const findToggleTarget = () => {
const target = document.getElementById(NAVBAR_TOGGLE_TARGET_ID);
if(!target) return;

setToggleTarget(target);
targetObserver?.disconnect();
};

findToggleTarget();
if(!document.getElementById(NAVBAR_TOGGLE_TARGET_ID)) {
targetObserver = new MutationObserver(findToggleTarget);
targetObserver.observe(document.body, { childList: true, subtree: true });
}

return () => targetObserver?.disconnect();
}, []);

entries.forEach((entry) => {
const [group] = entry.id.split("/");

Expand All @@ -40,29 +87,52 @@ export function DocsNavigation({ currentId, entries }: DocsNavigationProps) {
});

return (
<GlNav aria-label="Documentation navigation" className="w-full">
{rootEntries.map((entry) => (
<GlNavItem key={entry.id} selected={entry.id === currentId}>
<GlNavButton href={entry.href}>{entry.title}</GlNavButton>
</GlNavItem>
))}

{[...groupedEntries].map(([group, groupEntries]) => {
const containsCurrentPage = groupEntries.some(({ id }) => id === currentId);

return (
<GlNavItem key={group}>
<GlNavButton>{formatGroupLabel(group)}</GlNavButton>
<GlSubNav defaultOpen={containsCurrentPage}>
{groupEntries.map((entry) => (
<GlSubNavItem key={entry.id} selected={entry.id === currentId}>
<GlSubNavButton href={entry.href}>{entry.title}</GlSubNavButton>
</GlSubNavItem>
))}
</GlSubNav>
<GlNavProvider
navId="documentation-navigation"
onOpenChange={setIsOpen}
open={isOpen}>
{toggleTarget ? createPortal(
<GlCollapsibleNavToggle
className="min-[1200px]:hidden"
collapseLabel="Collapse navigation"
expandLabel="Expand navigation" />,
toggleTarget,
) : null}

<GlCollapsibleNav aria-label="Documentation navigation">
{rootEntries.map((entry) => (
<GlNavItem key={entry.id} selected={entry.id === currentId}>
<GlNavButton href={entry.href}>
{entry.title}
</GlNavButton>
</GlNavItem>
);
})}
</GlNav>
))}

{[...groupedEntries].map(([group, groupEntries]) => {
const containsCurrentPage = groupEntries.some(({ id }) => id === currentId);

return (
<GlNavItem key={`${group}:${currentId}`}>
Comment thread
NriotHrreion marked this conversation as resolved.
<GlNavButton>
{formatGroupLabel(group)}
</GlNavButton>
<GlSubNav defaultOpen={containsCurrentPage}>
Comment thread
NriotHrreion marked this conversation as resolved.
{groupEntries.map((entry) => (
<GlSubNavItem key={entry.id} selected={entry.id === currentId}>
<GlSubNavButton href={entry.href}>{entry.title}</GlSubNavButton>
</GlSubNavItem>
))}
</GlSubNav>
</GlNavItem>
);
})}

<GlNavItem className="mt-auto min-[1200px]:hidden">
<GlCollapsibleNavToggle
collapseLabel="Collapse"
expandLabel="Expand" />
</GlNavItem>
</GlCollapsibleNav>
</GlNavProvider>
);
}
9 changes: 7 additions & 2 deletions apps/website/src/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function Navbar() {
</nav>

<div className="flex w-full flex-nowrap items-center gap-2 lg:w-auto">
<span className="contents" id="documentation-navigation-toggle-target" />
<GlButton
category="tertiary"
icon={isDark ? "moon" : "sun"}
Expand All @@ -90,10 +91,14 @@ export function Navbar() {
aria-label="Search documentation"
className="min-w-0 flex-1 sm:min-w-56 lg:w-64"
placeholder="Search documents..."/>
<GlListbox value={lang} onValueChange={(value) => setLang(value as any)}>
<GlListbox
value={lang}
onValueChange={(value) => setLang(value as any)}>
{/** @todo */}
<GlListboxTrigger icon="earth">
English
<span className="max-sm:hidden">
English
</span>
</GlListboxTrigger>
<GlListboxContent>
<GlListboxGroup>
Expand Down
27 changes: 17 additions & 10 deletions apps/website/src/layouts/base-layout.astro
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
import { ClientRouter } from "astro:transitions";
import "../styles/global.css";
import { GlLink } from "gitlab-ui-react";
import { Navbar } from "../components/navbar";
Expand All @@ -25,22 +26,28 @@ const currentYear = new Date().getFullYear();
<title>{title}</title>
<script is:inline>
(() => {
try {
const storedTheme = localStorage.getItem("gitlab-ui-react-theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const isDark = storedTheme === "dark" || (storedTheme === null && prefersDark);
const applyStoredTheme = () => {
try {
const storedTheme = localStorage.getItem("gitlab-ui-react-theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const isDark = storedTheme === "dark" || (storedTheme === null && prefersDark);

document.documentElement.classList.toggle("gl-dark", isDark);
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
} catch {
// Keep the default light theme when browser storage is unavailable.
}
document.documentElement.classList.toggle("gl-dark", isDark);
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
} catch {
// Keep the default light theme when browser storage is unavailable.
}
};

document.addEventListener("astro:after-swap", applyStoredTheme);
applyStoredTheme();
})();
</script>
<ClientRouter />
</head>

<body class="flex min-h-[100dvh] flex-col">
<Navbar client:only="react" />
<Navbar client:only="react" transition:persist="site-navbar" />

<main class="min-w-0 flex-1" id="main-content">
<slot />
Expand Down
11 changes: 6 additions & 5 deletions apps/website/src/pages/docs/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,17 @@ const tableOfContents = headings.filter(({ depth }) => depth >= 2 && depth <= 3)
<div>
<div class="mx-6 sm:mx-8 lg:mx-12 xl:mx-16 2xl:mx-28">
<div class="mx-auto w-full max-w-[96rem]">
<div class="docs-layout-grid grid md:grid-cols-[14rem_minmax(0,1fr)]">
<aside class="hidden px-4 py-6 md:sticky md:top-0 md:block md:max-h-[100dvh] md:self-start md:overflow-y-auto lg:px-5 lg:py-8">
<div class="docs-layout-grid grid min-[1200px]:grid-cols-[15.75rem_minmax(0,1fr)]">
<aside class="min-[1200px]:sticky min-[1200px]:top-0 min-[1200px]:max-h-[100dvh] min-[1200px]:self-start min-[1200px]:overflow-x-hidden min-[1200px]:overflow-y-auto min-[1200px]:py-8 min-[1200px]:ps-3">
<DocsNavigation
client:media="(min-width: 768px)"
client:load
transition:persist="docs-navigation"
Comment thread
NriotHrreion marked this conversation as resolved.
currentId={entry.id}
entries={navigationEntries}
/>
</aside>

<article class="min-w-0 py-6 sm:px-6 sm:py-8 md:px-8 lg:px-10 lg:py-10 xl:px-12">
<article class="min-w-0 py-8 max-md:pt-6 sm:px-6 sm:py-8 md:px-8 lg:px-10 lg:py-10 xl:px-12">
<GlMarkdown className="docs-content mx-auto max-w-3xl">
<header class="mb-6">
<h1>{entry.data.title}</h1>
Expand Down Expand Up @@ -99,7 +100,7 @@ const tableOfContents = headings.filter(({ depth }) => depth >= 2 && depth <= 3)
<style is:global>
@media (min-width: 1360px) {
.docs-layout-grid {
grid-template-columns: 15rem minmax(0, 1fr) 14rem;
grid-template-columns: 15.75rem minmax(0, 1fr) 14rem;
}
}

Expand Down