-
Notifications
You must be signed in to change notification settings - Fork 60
Expand file tree
/
Copy pathmenu.tsx
More file actions
220 lines (202 loc) · 6.12 KB
/
menu.tsx
File metadata and controls
220 lines (202 loc) · 6.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import * as React from "react";
import { Link } from "react-router";
import classNames from "classnames";
import iconsHref from "~/icons.svg";
import type { MenuDoc } from "~/modules/gh-docs/.server/docs";
import { useNavigation } from "~/hooks/use-navigation";
import { useDelayedValue } from "~/hooks/use-delayed-value";
import { useHeaderData } from "../docs-header/use-header-data";
export function Menu({
menu,
changelogHref,
}: {
menu?: MenuDoc[];
changelogHref?: string;
}) {
// github might be down but the menu but the doc could be cached in memory, so
// prevent the whole page from blowing up and still render the doc
if (menu === undefined) {
return (
<div className="bold text-gray-300 dark:text-gray-400">
Failed to load menu
</div>
);
}
return (
<nav>
{changelogHref ? (
<HeaderMenuLink to={changelogHref}>Changelog</HeaderMenuLink>
) : null}
{menu.map((category) => (
<div key={category.attrs.title}>
<MenuCategory category={category} />
</div>
))}
</nav>
);
}
function MenuCategory({ category }: { category: MenuDoc }) {
let { refParam } = useHeaderData();
let prefix = refParam ? `/${refParam}/` : "/";
return (
<MenuCategoryDetails className="group" slug={category.slug}>
<MenuSummary>
{category.attrs.title}
<svg aria-hidden className="hidden h-5 w-5 group-open:block">
<use href={`${iconsHref}#chevron-d`} />
</svg>
<svg aria-hidden className="h-5 w-5 group-open:hidden">
<use href={`${iconsHref}#chevron-r`} />
</svg>
</MenuSummary>
<div className="mb-2">
{category.children.sort(sortDocs).map((doc, index) => (
<React.Fragment key={index}>
{doc.children.length > 0 ? (
<div className="mb-2 ml-2">
<MenuHeading label={doc.attrs.title} />
{doc.children.sort(sortDocs).map((doc, index) => (
<MenuLink key={index} to={prefix + doc.slug!}>
{doc.attrs.title} {doc.attrs.new && "🆕"}
</MenuLink>
))}
</div>
) : (
<MenuLink key={index} to={prefix + doc.slug!}>
{doc.attrs.title} {doc.attrs.new && "🆕"}
</MenuLink>
)}
</React.Fragment>
))}
</div>
</MenuCategoryDetails>
);
}
function MenuHeading({ label }: { label: string }) {
return (
<div className="pb-2 pt-2 text-xs font-bold uppercase tracking-wider">
{label}
</div>
);
}
type MenuCategoryDetailsType = {
className?: string;
slug?: string;
children: React.ReactNode;
};
function MenuCategoryDetails({
className,
slug,
children,
}: MenuCategoryDetailsType) {
let { isActive } = useNavigation(slug);
// By default only the active path is open
const [isOpen, setIsOpen] = React.useState(true);
// Auto open the details element, necessary when navigating from the index page
React.useEffect(() => {
if (isActive) {
setIsOpen(true);
}
}, [isActive]);
return (
<details
className={classNames(className, "relative flex flex-col")}
open={isOpen}
onToggle={(e) => {
// Synchronize the DOM's state with React state to prevent the
// details element from being closed after navigation and re-evaluation
// of useIsActivePath
setIsOpen(e.currentTarget.open);
}}
>
{children}
</details>
);
}
let sortDocs = (a: MenuDoc, b: MenuDoc) =>
(a.attrs.order || Infinity) - (b.attrs.order || Infinity);
// This components attempts to keep all of the styles as similar as possible
function MenuSummary({ children }: { children: React.ReactNode }) {
const sharedClassName =
// -mx-4 so there's some nice padding on the hover but the text still lines up
"-mx-4 rounded-md px-4 py-3 transition-colors duration-100";
return (
<summary
className={classNames(
sharedClassName,
"_no-triangle block cursor-pointer select-none",
"outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-red-brand dark:focus-visible:ring-gray-100",
"hover:bg-gray-50 active:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-800 dark:active:bg-gray-700",
)}
>
<div className="flex h-5 w-full items-center justify-between font-bold">
{children}
</div>
</summary>
);
}
function HeaderMenuLink({
to,
children,
}: {
to: string;
children: React.ReactNode;
}) {
return (
<LinkWithSpinner
to={to}
className={(isActive) =>
classNames(
"relative -mx-4 flex items-center justify-between rounded-md px-4 py-3 font-bold",
isActive
? "bg-gray-50 font-semibold text-red-brand dark:bg-gray-800"
: "hover:bg-gray-50 active:text-red-brand dark:hover:bg-gray-800 dark:active:text-red-brand",
)
}
>
{children}
</LinkWithSpinner>
);
}
function MenuLink({ to, children }: { to: string; children: React.ReactNode }) {
return (
<LinkWithSpinner
to={to}
className={(isActive) =>
classNames(
"relative -mx-2 flex items-center justify-between rounded-md px-4 py-1.5 lg:text-sm",
isActive
? "bg-gray-50 font-semibold text-red-brand dark:bg-gray-800"
: "text-gray-400 hover:text-gray-800 active:text-red-brand dark:text-gray-400 dark:hover:text-gray-50 dark:active:text-red-brand",
)
}
>
{children}
</LinkWithSpinner>
);
}
function LinkWithSpinner({
to,
children,
className,
}: {
to: string;
children: React.ReactNode;
className: (isActive: boolean) => string;
}) {
let { isActive, isPending } = useNavigation(to);
let slowNav = useDelayedValue(isPending);
return (
<Link prefetch="intent" to={to} className={className(isActive)}>
{children}
{slowNav && !isActive && (
<svg
aria-hidden
className="absolute -left-1 h-4 w-4 animate-spin lg:-left-2"
>
<use href={`${iconsHref}#arrow-path`} />
</svg>
)}
</Link>
);
}