Skip to content

Commit 1d7f97b

Browse files
committed
Make the sidebar nice
1 parent ca5f078 commit 1d7f97b

3 files changed

Lines changed: 88 additions & 26 deletions

File tree

src/app/conf/_design-system/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export declare namespace ButtonProps {
4646
extends BaseProps,
4747
React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
4848
href?: never
49-
as: "span" | "div"
49+
as: "span" | "div" | "label"
5050
className?: string
5151
}
5252
}

src/components/checkbox-tree/checkbox-tree.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function CheckboxTree({
4343

4444
return (
4545
<div
46-
className="[*:has(_:focus)>&:not(:focus-within)]:text-neu-700"
46+
className="[*:has(_:focus-visible)>&:not(:focus-within)]:text-neu-700"
4747
key={item.id}
4848
>
4949
<div

src/components/tools-and-libraries.tsx

Lines changed: 86 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import NextLink from "next/link"
44
import { Collapse, getComponents, useConfig } from "nextra-theme-docs"
55
import { evaluate } from "nextra/components"
66
import { useMounted } from "nextra/hooks"
7-
import { memo, useCallback, useMemo, useState } from "react"
7+
import { memo, useCallback, useEffect, useMemo, useState } from "react"
88

99
import { Card } from "@/components"
1010
import { CheckboxTree, type CheckboxTreeItem } from "@/components/checkbox-tree"
@@ -52,6 +52,8 @@ type CodePageProps = {
5252

5353
const TAG_PARAM_KEY = "tags"
5454

55+
type Sort = "popularity" | "alphabetical"
56+
5557
export function CodePage({ allTags, data }: CodePageProps) {
5658
const allTagsMap = useMemo(
5759
() =>
@@ -60,9 +62,7 @@ export function CodePage({ allTags, data }: CodePageProps) {
6062
[],
6163
)
6264

63-
const { activePath } = useConfig().normalizePagesResult
64-
65-
const [sort, setSort] = useState("popularity")
65+
const [sort, setSort] = useState<Sort>("popularity")
6666

6767
const sidebarState = useToolsAndLibrariesSidebarState({
6868
allTagsMap,
@@ -76,6 +76,17 @@ export function CodePage({ allTags, data }: CodePageProps) {
7676
title = `${sidebarState.selectedTagsAsString} | ${title}`
7777
}
7878

79+
useEffect(() => {
80+
document.addEventListener("keydown", e => {
81+
if (e.key === "Escape") {
82+
const toggle = document.getElementById("sidebar-toggle")
83+
if (toggle && (toggle as HTMLInputElement).checked) {
84+
toggle.click()
85+
}
86+
}
87+
})
88+
}, [])
89+
7990
return (
8091
<>
8192
<NextHead>
@@ -92,6 +103,7 @@ export function CodePage({ allTags, data }: CodePageProps) {
92103
<style>
93104
{`@media (max-width: 767px) { html:has(#sidebar-toggle:checked) { overscroll-behavior: none; } }`}
94105
</style>
106+
<SidebarOverlay />
95107
<div className="relative flex md:gap-6">
96108
<ToolsAndLibrariesSidebar
97109
searchInputValue={sidebarState.searchInputValue}
@@ -101,25 +113,8 @@ export function CodePage({ allTags, data }: CodePageProps) {
101113
updateTags={sidebarState.updateTags}
102114
/>
103115
<div className="flex-1 @container">
104-
<Breadcrumbs
105-
className="mb-2 mt-1 md:mb-6"
106-
activePath={activePath}
107-
/>
108-
<RadioGroup
109-
value={sort}
110-
onValueChange={value => setSort(value as string)}
111-
className="typography-menu flex flex-wrap gap-2 text-sm text-neu-800 dark:text-neu-600 md:flex-nowrap"
112-
>
113-
<div>Sort by:</div>
114-
<label className="-m-1 flex items-center gap-1 p-1 [&:has([data-checked])]:text-neu-900 [&:has([data-unchecked])]:cursor-pointer [&:has([data-unchecked])]:hover:bg-neu-50/50">
115-
<Radio value="popularity" />
116-
<span>Popularity</span>
117-
</label>
118-
<label className="-m-1 flex items-center gap-1 p-1 [&:has([data-checked])]:text-neu-900 [&:has([data-unchecked])]:cursor-pointer [&:has([data-unchecked])]:hover:bg-neu-50/50">
119-
<Radio value="alphabetical" />
120-
<span>Alphabetical</span>
121-
</label>
122-
</RadioGroup>
116+
<ToolsAndLibrariesHeader sort={sort} setSort={setSort} />
117+
123118
<div className="grid gap-2 py-4 @[640px]:grid-cols-2 md:gap-4 md:py-8 lg:grid-cols-2">
124119
{(sort === "alphabetical"
125120
? [...sidebarState.filteredData].sort((a, b) =>
@@ -327,7 +322,7 @@ function ToolsAndLibrariesSidebar({
327322
}: ToolsAndLibrariesSidebarProps) {
328323
const [sidebarShown, setSidebarShown] = useState(true)
329324
return (
330-
<div className="sticky top-[calc(var(--navbar-h)+1.5rem)] max-md:fixed max-md:right-0 max-md:top-[--navbar-h] max-md:z-10 max-md:border-l max-md:border-l-white max-md:bg-[rgb(var(--nextra-bg),.8)] max-md:pr-4 max-md:pt-4 max-md:backdrop-blur-[6.4px] dark:max-md:bg-white/10 md:h-[calc(100vh-var(--nextra-navbar-height)-var(--nextra-menu-height))]">
325+
<div className="sticky top-[calc(var(--navbar-h)+1.5rem)] duration-200 ease-out max-md:fixed max-md:right-0 max-md:top-[--navbar-h] max-md:z-10 max-md:transform-gpu max-md:border-l max-md:border-white max-md:bg-[rgb(var(--nextra-bg),.8)] max-md:pr-2 max-md:pt-4 max-md:backdrop-blur-[6.4px] max-md:transition-transform dark:max-md:border-white/10 md:h-[calc(100vh-var(--nextra-navbar-height)-var(--nextra-menu-height))] max-md:hover-hover:[html:has(#sidebar-toggle:checked:hover)_&]:translate-x-[calc(100%-0.5rem)] max-md:[html:has(#sidebar-toggle:not(:checked))_&:not(:hover)]:translate-x-[calc(100%+1px)] max-md:hover-hover:[html:has(#sidebar-toggle:not(:checked))_&]:translate-y-16 max-md:hover-hover:[html:has(#sidebar-toggle:not(:checked))_&]:border-t max-md:hover-hover:[html:has(#sidebar-toggle:not(:checked):hover)_&]:translate-x-2 max-md:hover-hover:[html:has(#sidebar-toggle:not(:checked):hover)_&]:shadow-md">
331326
<Collapse horizontal isOpen={sidebarShown}>
332327
<section className="nextra-scrollbar -mt-4 w-[300px] shrink-0 overflow-y-auto p-4 pb-8 md:h-[calc(100vh-var(--nextra-navbar-height)-var(--nextra-menu-height))] lg:pb-16">
333328
<div className="sticky top-0 z-10 bg-[rgb(var(--nextra-bg))] shadow-[0_8px_16px_8px_rgb(var(--nextra-bg))] before:absolute before:-top-20 before:bottom-0 before:w-full before:bg-[rgb(var(--nextra-bg))]">
@@ -605,3 +600,70 @@ function useToolsAndLibrariesSidebarState({
605600
updateTags,
606601
}
607602
}
603+
604+
function MobileSidebarToggle(props: React.HTMLAttributes<HTMLLabelElement>) {
605+
return (
606+
<Button
607+
as="label"
608+
variant="tertiary"
609+
isIconButton
610+
{...props}
611+
className={clsx(
612+
props.className,
613+
"-m-1 cursor-pointer !p-1 !ring-transparent [&:not(:hover)]:!bg-transparent [&:not(:hover)]:!text-neu-700 dark:[&:not(:hover)]:!text-neu-500",
614+
)}
615+
>
616+
<input type="checkbox" id="sidebar-toggle" className="hidden" />
617+
<ArrowBarLeft className="size-6" />
618+
<span className="sr-only">Toggle sidebar</span>
619+
</Button>
620+
)
621+
}
622+
623+
function ToolsAndLibrariesHeader({
624+
sort,
625+
setSort,
626+
}: {
627+
sort: Sort
628+
setSort: (value: Sort) => void
629+
}) {
630+
const { activePath } = useConfig().normalizePagesResult
631+
632+
return (
633+
<div className="top-[--navbar-h] flex items-center justify-between bg-[rgb(var(--nextra-bg))] max-md:sticky">
634+
<div>
635+
<Breadcrumbs className="mb-2 mt-1 md:mb-6" activePath={activePath} />
636+
<RadioGroup
637+
value={sort}
638+
onValueChange={value => setSort(value as Sort)}
639+
className="typography-menu flex flex-wrap gap-2 text-sm text-neu-800 dark:text-neu-600 md:flex-nowrap"
640+
>
641+
<div>Sort by:</div>
642+
<label className="-m-1 flex items-center gap-1 p-1 [&:has([data-checked])]:text-neu-900 [&:has([data-unchecked])]:cursor-pointer [&:has([data-unchecked])]:hover:bg-neu-50/50">
643+
<Radio value="popularity" />
644+
<span>Popularity</span>
645+
</label>
646+
<label className="-m-1 flex items-center gap-1 p-1 [&:has([data-checked])]:text-neu-900 [&:has([data-unchecked])]:cursor-pointer [&:has([data-unchecked])]:hover:bg-neu-50/50">
647+
<Radio value="alphabetical" />
648+
<span>Alphabetical</span>
649+
</label>
650+
</RadioGroup>
651+
</div>
652+
<MobileSidebarToggle className="md:hidden" />
653+
</div>
654+
)
655+
}
656+
657+
function SidebarOverlay() {
658+
return (
659+
<button
660+
tabIndex={-1}
661+
title="Close sidebar"
662+
className="pointer-events-none fixed inset-0 top-[--navbar-h] z-[9] bg-[rgb(var(--nextra-bg),.25)] opacity-0 transition-opacity focus:outline-none max-md:[html:has(#sidebar-toggle:checked)_&]:pointer-events-auto max-md:[html:has(#sidebar-toggle:checked)_&]:opacity-100"
663+
// this button can't be a label because of hovers
664+
onClick={() => {
665+
document.getElementById("sidebar-toggle")?.click()
666+
}}
667+
/>
668+
)
669+
}

0 commit comments

Comments
 (0)