blodemd 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -47
- package/dev-server/app/layout.tsx +1 -1
- package/dev-server/next.config.js +19 -9
- package/dev-server/tsconfig.json +0 -3
- package/dist/cli.mjs +732 -123
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +15 -1
- package/docs/components/api/api-playground.tsx +2 -2
- package/docs/components/docs/copy-page-menu.tsx +55 -27
- package/docs/components/docs/doc-header.tsx +1 -1
- package/docs/components/docs/doc-shell.tsx +89 -88
- package/docs/components/docs/doc-sidebar.tsx +6 -3
- package/docs/components/docs/doc-toc.tsx +1 -1
- package/docs/components/docs/mobile-nav.tsx +8 -16
- package/docs/components/docs/sidebar-scroll-area.tsx +58 -0
- package/docs/components/git/repo-picker.tsx +526 -0
- package/docs/components/mdx/agent-instructions.tsx +17 -0
- package/docs/components/mdx/code-block.tsx +6 -1
- package/docs/components/mdx/code-group.tsx +1 -1
- package/docs/components/mdx/iframe.tsx +62 -0
- package/docs/components/mdx/index.tsx +4 -0
- package/docs/components/mdx/tabs.tsx +5 -5
- package/docs/components/mdx/video.tsx +45 -12
- package/docs/components/third-parties.tsx +29 -0
- package/docs/components/ui/badge.tsx +61 -0
- package/docs/components/ui/breadcrumb.tsx +61 -41
- package/docs/components/ui/button-group.tsx +83 -0
- package/docs/components/ui/button.tsx +30 -55
- package/docs/components/ui/command.tsx +32 -4
- package/docs/components/ui/copy-button.tsx +12 -19
- package/docs/components/ui/dialog.tsx +50 -1
- package/docs/components/ui/input.tsx +16 -97
- package/docs/components/ui/kbd.tsx +98 -0
- package/docs/components/ui/morph-icon.tsx +79 -0
- package/docs/components/ui/popover.tsx +225 -30
- package/docs/components/ui/search.tsx +0 -9
- package/docs/components/ui/sheet.tsx +30 -1
- package/docs/components/ui/sidebar.tsx +332 -7
- package/docs/components/ui/site-footer.tsx +6 -4
- package/docs/components/ui/skeleton.tsx +11 -0
- package/docs/components/ui/switch.tsx +32 -0
- package/docs/components/ui/tabs.tsx +138 -0
- package/docs/lib/api-client.ts +72 -0
- package/docs/lib/contextual-options.ts +9 -0
- package/docs/lib/dashboard-session.ts +167 -0
- package/docs/lib/db.ts +13 -0
- package/docs/lib/env.ts +4 -3
- package/docs/lib/etag.ts +22 -0
- package/docs/lib/github-install.ts +33 -0
- package/docs/lib/project-authz.ts +46 -0
- package/docs/lib/routes.ts +5 -1
- package/docs/lib/supabase.ts +30 -6
- package/docs/lib/tenancy.ts +1 -0
- package/docs/lib/tenant-static.ts +206 -4
- package/docs/lib/tenants.ts +5 -1
- package/docs/lib/time-ago.ts +24 -0
- package/docs/lib/use-tab-observer.ts +71 -0
- package/package.json +3 -1
- package/packages/@repo/common/package.json +2 -2
- package/packages/@repo/contracts/dist/git.d.ts +28 -0
- package/packages/@repo/contracts/dist/git.d.ts.map +1 -0
- package/packages/@repo/contracts/dist/git.js +24 -0
- package/packages/@repo/contracts/dist/index.d.ts +1 -1
- package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
- package/packages/@repo/contracts/dist/index.js +1 -1
- package/packages/@repo/contracts/package.json +2 -2
- package/packages/@repo/contracts/src/git.ts +31 -0
- package/packages/@repo/contracts/src/index.ts +1 -1
- package/packages/@repo/models/dist/docs-config.d.ts +6 -0
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +1 -0
- package/packages/@repo/models/package.json +2 -2
- package/packages/@repo/models/src/docs-config.ts +1 -0
- package/packages/@repo/prebuild/package.json +2 -2
- package/packages/@repo/previewing/dist/index.d.ts +3 -0
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +48 -0
- package/packages/@repo/previewing/package.json +2 -2
- package/packages/@repo/previewing/src/index.ts +56 -0
- package/packages/@repo/validation/package.json +2 -2
- package/packages/@repo/validation/src/blodemd-docs-schema.json +1 -0
- package/scripts/prepare-package.mjs +14 -0
- package/packages/@repo/contracts/dist/api-key.d.ts +0 -30
- package/packages/@repo/contracts/dist/api-key.d.ts.map +0 -1
- package/packages/@repo/contracts/dist/api-key.js +0 -20
- package/packages/@repo/contracts/src/api-key.ts +0 -27
package/docs/app/globals.css
CHANGED
|
@@ -223,6 +223,11 @@
|
|
|
223
223
|
font-synthesis: none;
|
|
224
224
|
font-kerning: normal;
|
|
225
225
|
font-variant-ligatures: common-ligatures;
|
|
226
|
+
font-feature-settings: "ss01", "cv11", "calt";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
:is([data-slot="kbd"], code, pre, .font-mono) {
|
|
230
|
+
font-variant-numeric: tabular-nums;
|
|
226
231
|
}
|
|
227
232
|
|
|
228
233
|
[data-slot="layout"] {
|
|
@@ -284,6 +289,15 @@
|
|
|
284
289
|
@apply 3xl:max-w-screen-2xl mx-auto max-w-[1400px] px-4 lg:px-8;
|
|
285
290
|
}
|
|
286
291
|
|
|
292
|
+
@utility h-display {
|
|
293
|
+
line-height: 0.95;
|
|
294
|
+
letter-spacing: -0.02em;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@utility measure {
|
|
298
|
+
max-width: 65ch;
|
|
299
|
+
}
|
|
300
|
+
|
|
287
301
|
@utility no-scrollbar {
|
|
288
302
|
-ms-overflow-style: none;
|
|
289
303
|
scrollbar-width: none;
|
|
@@ -397,10 +411,10 @@
|
|
|
397
411
|
[data-line],
|
|
398
412
|
.shiki .line {
|
|
399
413
|
display: inline-block;
|
|
400
|
-
width: 100%;
|
|
401
414
|
min-height: calc(var(--spacing) * 1);
|
|
402
415
|
padding-top: calc(var(--spacing) * 0.5);
|
|
403
416
|
padding-bottom: calc(var(--spacing) * 0.5);
|
|
417
|
+
padding-right: calc(var(--spacing) * 10);
|
|
404
418
|
}
|
|
405
419
|
|
|
406
420
|
[data-line] span,
|
|
@@ -397,7 +397,7 @@ export const ApiPlayground = ({
|
|
|
397
397
|
<Field>
|
|
398
398
|
<FieldLabel htmlFor="api-server">Server</FieldLabel>
|
|
399
399
|
<select
|
|
400
|
-
className="flex h-[var(--field-height)] w-full rounded-[var(--field-radius)] border border-input bg-card px-[var(--field-padding-x)] py-[var(--field-padding-y)] font-sans text-base text-foreground
|
|
400
|
+
className="flex h-[var(--field-height)] w-full rounded-[var(--field-radius)] border border-input bg-card px-[var(--field-padding-x)] py-[var(--field-padding-y)] font-sans text-base text-foreground transition-colors hover:border-input-hover focus:border-ring focus:outline-hidden focus:ring-2 focus:ring-ring/15 focus:ring-offset-1 focus:ring-offset-background"
|
|
401
401
|
id="api-server"
|
|
402
402
|
onChange={handleServerChange}
|
|
403
403
|
value={serverIndex}
|
|
@@ -440,7 +440,7 @@ export const ApiPlayground = ({
|
|
|
440
440
|
<Field>
|
|
441
441
|
<FieldLabel htmlFor="request-body">Request body</FieldLabel>
|
|
442
442
|
<textarea
|
|
443
|
-
className="flex w-full rounded-[var(--field-radius)] border border-input bg-card px-[var(--field-padding-x)] py-[var(--field-padding-y)] font-sans text-base text-foreground
|
|
443
|
+
className="flex w-full rounded-[var(--field-radius)] border border-input bg-card px-[var(--field-padding-x)] py-[var(--field-padding-y)] font-sans text-base text-foreground transition-colors placeholder:text-placeholder-foreground hover:border-input-hover focus:border-ring focus:outline-hidden focus:ring-2 focus:ring-ring/15 focus:ring-offset-1 focus:ring-offset-background"
|
|
444
444
|
id="request-body"
|
|
445
445
|
onChange={handleBodyChange}
|
|
446
446
|
rows={6}
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { slugify } from "@repo/common";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
CheckIcon,
|
|
6
6
|
ChevronDownSmallIcon,
|
|
7
7
|
ClaudeaiIcon,
|
|
8
8
|
CopySimpleIcon,
|
|
9
9
|
OpenaiIcon,
|
|
10
10
|
} from "blode-icons-react";
|
|
11
11
|
import type React from "react";
|
|
12
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
12
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
Popover,
|
|
@@ -25,6 +25,8 @@ interface CopyPageMenuProps {
|
|
|
25
25
|
|
|
26
26
|
type CopyStatus = "copied" | "error" | "idle";
|
|
27
27
|
|
|
28
|
+
const MARKDOWN_ACCEPT = "text/markdown,text/plain;q=0.9,*/*;q=0.8";
|
|
29
|
+
|
|
28
30
|
const LEADING_H1_REGEX = /^#\s+([^\r\n]+)(?:\r?\n(?:\r?\n)?)?/;
|
|
29
31
|
|
|
30
32
|
const stripMatchingLeadingH1 = (source: string, title: string) => {
|
|
@@ -80,7 +82,7 @@ const getCopyDescription = (copyStatus: CopyStatus) => {
|
|
|
80
82
|
};
|
|
81
83
|
|
|
82
84
|
const getCopyIcon = (copyStatus: CopyStatus) =>
|
|
83
|
-
copyStatus === "copied" ?
|
|
85
|
+
copyStatus === "copied" ? CheckIcon : CopySimpleIcon;
|
|
84
86
|
|
|
85
87
|
const MenuItem = ({
|
|
86
88
|
children,
|
|
@@ -141,7 +143,15 @@ export const CopyPageMenu = ({
|
|
|
141
143
|
const [copyStatus, setCopyStatus] = useState<CopyStatus>("idle");
|
|
142
144
|
const [fetchedContent, setFetchedContent] = useState<string | null>(null);
|
|
143
145
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
144
|
-
|
|
146
|
+
|
|
147
|
+
const pageUrl = useMemo(
|
|
148
|
+
() =>
|
|
149
|
+
typeof window === "undefined"
|
|
150
|
+
? ""
|
|
151
|
+
: new URL(contentUrl ?? window.location.href, window.location.href)
|
|
152
|
+
.href,
|
|
153
|
+
[contentUrl]
|
|
154
|
+
);
|
|
145
155
|
|
|
146
156
|
useEffect(
|
|
147
157
|
() => () => {
|
|
@@ -153,10 +163,31 @@ export const CopyPageMenu = ({
|
|
|
153
163
|
);
|
|
154
164
|
|
|
155
165
|
useEffect(() => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
if (content || !contentUrl) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
|
|
172
|
+
const prefetch = async () => {
|
|
173
|
+
try {
|
|
174
|
+
const res = await fetch(contentUrl, {
|
|
175
|
+
headers: { accept: MARKDOWN_ACCEPT },
|
|
176
|
+
signal: controller.signal,
|
|
177
|
+
});
|
|
178
|
+
const text = res.ok ? await res.text() : null;
|
|
179
|
+
if (text) {
|
|
180
|
+
setFetchedContent(text);
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Ignore prefetch failures — the copy handler retries on demand
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
prefetch();
|
|
188
|
+
|
|
189
|
+
return () => controller.abort();
|
|
190
|
+
}, [content, contentUrl]);
|
|
160
191
|
|
|
161
192
|
const closeMenu = useCallback(() => {
|
|
162
193
|
setMenuOpen(false);
|
|
@@ -188,9 +219,7 @@ export const CopyPageMenu = ({
|
|
|
188
219
|
}
|
|
189
220
|
|
|
190
221
|
const response = await fetch(contentUrl, {
|
|
191
|
-
headers: {
|
|
192
|
-
accept: "text/markdown,text/plain;q=0.9,*/*;q=0.8",
|
|
193
|
-
},
|
|
222
|
+
headers: { accept: MARKDOWN_ACCEPT },
|
|
194
223
|
});
|
|
195
224
|
if (!response.ok) {
|
|
196
225
|
throw new Error(`Failed to load page markdown: ${response.status}`);
|
|
@@ -203,25 +232,24 @@ export const CopyPageMenu = ({
|
|
|
203
232
|
|
|
204
233
|
const handleCopy = useCallback(async () => {
|
|
205
234
|
try {
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
235
|
+
// Use ClipboardItem with a promise-based blob so the clipboard "slot"
|
|
236
|
+
// is claimed synchronously during the tap gesture. iOS Safari revokes
|
|
237
|
+
// user-activation if an async boundary (e.g. a network fetch) sits
|
|
238
|
+
// between the tap and the clipboard call, so the old writeText path
|
|
239
|
+
// fails when tapped before the pre-fetch finishes.
|
|
240
|
+
if (typeof ClipboardItem !== "undefined" && navigator.clipboard.write) {
|
|
241
|
+
const blobPromise = (async () => {
|
|
242
|
+
const text = await getContent();
|
|
243
|
+
const markdown = formatMarkdownForCopy(text, title);
|
|
244
|
+
return new Blob([markdown], { type: "text/plain" });
|
|
245
|
+
})();
|
|
246
|
+
const item = new ClipboardItem({ "text/plain": blobPromise });
|
|
247
|
+
await navigator.clipboard.write([item]);
|
|
248
|
+
} else {
|
|
211
249
|
const nextContent = await getContent();
|
|
212
250
|
const markdown = formatMarkdownForCopy(nextContent, title);
|
|
213
|
-
|
|
214
|
-
})();
|
|
215
|
-
|
|
216
|
-
if (typeof ClipboardItem === "undefined") {
|
|
217
|
-
const blob = await blobPromise;
|
|
218
|
-
await navigator.clipboard.writeText(await blob.text());
|
|
219
|
-
} else {
|
|
220
|
-
await navigator.clipboard.write([
|
|
221
|
-
new ClipboardItem({ "text/plain": blobPromise }),
|
|
222
|
-
]);
|
|
251
|
+
await navigator.clipboard.writeText(markdown);
|
|
223
252
|
}
|
|
224
|
-
|
|
225
253
|
setTemporaryCopyStatus("copied");
|
|
226
254
|
closeMenu();
|
|
227
255
|
} catch {
|
|
@@ -127,7 +127,7 @@ export const DocHeader = ({
|
|
|
127
127
|
return (
|
|
128
128
|
<header className="sticky top-0 z-50 w-full bg-background">
|
|
129
129
|
<div className="container-wrapper px-4 lg:px-8">
|
|
130
|
-
<div className="flex h-(--header-height) items-center
|
|
130
|
+
<div className="flex h-(--header-height) items-center">
|
|
131
131
|
<MobileNav
|
|
132
132
|
activeTabIndex={activeTabIndex}
|
|
133
133
|
basePath={basePath}
|
|
@@ -170,101 +170,102 @@ export const DocShell = ({
|
|
|
170
170
|
<main id="main-content">{content}</main>
|
|
171
171
|
) : (
|
|
172
172
|
<main
|
|
173
|
-
className=
|
|
174
|
-
"flex scroll-mt-24 items-stretch gap-1 px-4 lg:px-8",
|
|
175
|
-
!showSidebar && "mx-auto max-w-[960px]"
|
|
176
|
-
)}
|
|
173
|
+
className="flex scroll-mt-24 items-stretch pb-8 text-[1.05rem] sm:text-[15px] xl:w-full"
|
|
177
174
|
id="main-content"
|
|
175
|
+
role="application"
|
|
176
|
+
style={{ "--sidebar-width": "14rem" } as React.CSSProperties}
|
|
178
177
|
>
|
|
179
|
-
<div
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
<Breadcrumbs basePath={basePath} breadcrumbs={breadcrumbs} />
|
|
178
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
179
|
+
<div
|
|
180
|
+
className={cn(
|
|
181
|
+
"mx-auto flex w-full min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300",
|
|
182
|
+
pageMode === "wide" ? "max-w-[60rem]" : "max-w-[40rem]"
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
187
185
|
<div className="flex flex-col gap-2">
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
186
|
+
<Breadcrumbs basePath={basePath} breadcrumbs={breadcrumbs} />
|
|
187
|
+
<div className="flex flex-col gap-2">
|
|
188
|
+
<div className="flex flex-col items-start gap-3 sm:flex-row sm:justify-between">
|
|
189
|
+
<h1 className="min-w-0 scroll-m-24 text-3xl font-semibold tracking-tight sm:text-3xl">
|
|
190
|
+
{pageTitle}
|
|
191
|
+
{deprecated ? (
|
|
192
|
+
<span className="ml-3 inline-flex translate-y-[-2px] items-center rounded-md bg-yellow-100 px-2 py-0.5 align-middle text-xs font-medium text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-300">
|
|
193
|
+
Deprecated
|
|
194
|
+
</span>
|
|
195
|
+
) : null}
|
|
196
|
+
</h1>
|
|
197
|
+
{headerContextualMenu ??
|
|
198
|
+
(rawContent === undefined &&
|
|
199
|
+
markdownHref === undefined ? null : (
|
|
200
|
+
<CopyPageMenu
|
|
201
|
+
content={markdownHref ? undefined : rawContent}
|
|
202
|
+
contentUrl={markdownHref}
|
|
203
|
+
key={`copy-${currentPath}`}
|
|
204
|
+
title={pageTitle}
|
|
205
|
+
/>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
{pageDescription ? (
|
|
209
|
+
<p className="text-[1.05rem] text-muted-foreground sm:text-balance sm:text-base md:max-w-[80%]">
|
|
210
|
+
{pageDescription}
|
|
211
|
+
</p>
|
|
212
|
+
) : null}
|
|
207
213
|
</div>
|
|
208
|
-
{pageDescription ? (
|
|
209
|
-
<p className="text-[1.05rem] text-muted-foreground sm:text-balance sm:text-base md:max-w-[80%]">
|
|
210
|
-
{pageDescription}
|
|
211
|
-
</p>
|
|
212
|
-
) : null}
|
|
213
214
|
</div>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
</
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
{nextPage.title}
|
|
246
|
-
</span>
|
|
247
|
-
{nextPage.description ? (
|
|
248
|
-
<span className="hidden w-full truncate text-right text-muted-foreground lg:block lg:w-72">
|
|
249
|
-
{nextPage.description}
|
|
215
|
+
<div className="grid min-w-0 grid-cols-1 gap-4.5 leading-relaxed [&_blockquote]:border-l-3 [&_blockquote]:border-primary [&_blockquote]:pl-3.5 [&_blockquote]:text-muted-foreground [&_h2]:mt-10 [&_h2]:mb-3 [&_h2]:text-2xl [&_h2]:font-bold [&_h3]:mt-8 [&_h3]:mb-2 [&_h3]:text-[1.375rem] [&_h3]:font-semibold [&_h4]:mt-6 [&_h4]:mb-2 [&_h4]:text-base [&_h4]:font-semibold [&_ol]:list-decimal [&_ol]:pl-6 [&_table]:w-full [&_table]:border-collapse [&_table]:text-sm [&_td]:border-b [&_td]:border-border [&_td]:px-2.5 [&_td]:py-2 [&_td]:text-left [&_th]:border-b [&_th]:border-border [&_th]:px-2.5 [&_th]:py-2 [&_th]:text-left [&_ul]:list-disc [&_ul]:pl-6">
|
|
216
|
+
{content}
|
|
217
|
+
</div>
|
|
218
|
+
{!hideFooterPagination && (prevPage || nextPage) ? (
|
|
219
|
+
<nav
|
|
220
|
+
className="flex w-full rounded-2xl bg-muted/50 p-1 text-sm"
|
|
221
|
+
id="pagination"
|
|
222
|
+
>
|
|
223
|
+
{prevPage ? (
|
|
224
|
+
<Link
|
|
225
|
+
className="group flex items-center justify-between gap-1.5 pl-3 pr-6"
|
|
226
|
+
href={toDocHref(prevPage.path, basePath)}
|
|
227
|
+
>
|
|
228
|
+
<ChevronLeftIcon
|
|
229
|
+
aria-hidden="true"
|
|
230
|
+
className="size-3 text-muted-foreground/50 group-hover:text-muted-foreground"
|
|
231
|
+
/>
|
|
232
|
+
<span className="font-medium tracking-tight text-muted-foreground group-hover:text-foreground">
|
|
233
|
+
Previous
|
|
234
|
+
</span>
|
|
235
|
+
</Link>
|
|
236
|
+
) : null}
|
|
237
|
+
{nextPage ? (
|
|
238
|
+
<Link
|
|
239
|
+
className="group ml-auto flex w-full min-w-0 flex-1"
|
|
240
|
+
href={toDocHref(nextPage.path, basePath)}
|
|
241
|
+
>
|
|
242
|
+
<div className="flex flex-1 items-center justify-end rounded-xl bg-background hover:ring-1 hover:ring-border sm:h-16">
|
|
243
|
+
<div className="flex min-w-0 flex-col items-end justify-center px-5">
|
|
244
|
+
<span className="text-right font-semibold text-foreground/80">
|
|
245
|
+
{nextPage.title}
|
|
250
246
|
</span>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
<
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
247
|
+
{nextPage.description ? (
|
|
248
|
+
<span className="hidden w-full truncate text-right text-muted-foreground lg:block lg:w-72">
|
|
249
|
+
{nextPage.description}
|
|
250
|
+
</span>
|
|
251
|
+
) : null}
|
|
252
|
+
</div>
|
|
253
|
+
<div className="h-8 w-px bg-border/50" />
|
|
254
|
+
<div className="flex items-center gap-1.5 pl-5 pr-3">
|
|
255
|
+
<span className="font-medium tracking-tight text-muted-foreground group-hover:text-foreground">
|
|
256
|
+
Next
|
|
257
|
+
</span>
|
|
258
|
+
<ChevronRightIcon
|
|
259
|
+
aria-hidden="true"
|
|
260
|
+
className="size-3 text-muted-foreground/50 group-hover:text-muted-foreground"
|
|
261
|
+
/>
|
|
262
|
+
</div>
|
|
262
263
|
</div>
|
|
263
|
-
</
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
264
|
+
</Link>
|
|
265
|
+
) : null}
|
|
266
|
+
</nav>
|
|
267
|
+
) : null}
|
|
268
|
+
</div>
|
|
268
269
|
</div>
|
|
269
270
|
{hasToc ? (
|
|
270
271
|
<DocToc contextualItems={contextualTocItems} toc={toc} />
|
|
@@ -9,6 +9,8 @@ import type { NavEntry, NavPage } from "@/lib/navigation";
|
|
|
9
9
|
import { isExternalHref, toDocHref } from "@/lib/routes";
|
|
10
10
|
import { cn } from "@/lib/utils";
|
|
11
11
|
|
|
12
|
+
import { SidebarScrollArea } from "./sidebar-scroll-area";
|
|
13
|
+
|
|
12
14
|
const MENU_BUTTON_CLASS =
|
|
13
15
|
"relative flex min-h-[30px] items-center gap-2 overflow-visible rounded-md border border-transparent px-2 text-[0.8rem] font-medium transition-colors hover:bg-accent/70 hover:text-foreground";
|
|
14
16
|
|
|
@@ -81,6 +83,7 @@ const NavPageLink = ({
|
|
|
81
83
|
return (
|
|
82
84
|
<a
|
|
83
85
|
className={className}
|
|
86
|
+
data-active={isActive || undefined}
|
|
84
87
|
href={href}
|
|
85
88
|
rel="noopener noreferrer"
|
|
86
89
|
target="_blank"
|
|
@@ -91,7 +94,7 @@ const NavPageLink = ({
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
return (
|
|
94
|
-
<Link className={className} href={href}>
|
|
97
|
+
<Link className={className} data-active={isActive || undefined} href={href}>
|
|
95
98
|
{linkContent}
|
|
96
99
|
</Link>
|
|
97
100
|
);
|
|
@@ -143,7 +146,7 @@ export const DocSidebar = ({
|
|
|
143
146
|
<div className="h-9" />
|
|
144
147
|
<div className="absolute top-8 z-10 h-8 w-full shrink-0 bg-gradient-to-b from-background via-background/80 to-background/50 blur-xs" />
|
|
145
148
|
<div className="absolute top-12 right-2 bottom-0 hidden h-full w-px bg-gradient-to-b from-transparent via-border to-transparent lg:flex" />
|
|
146
|
-
<
|
|
149
|
+
<SidebarScrollArea className="no-scrollbar mx-auto flex min-h-0 w-full flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden px-2">
|
|
147
150
|
{anchors?.length ? (
|
|
148
151
|
<Section paddedTop title="Pinned">
|
|
149
152
|
<ul className="space-y-1">
|
|
@@ -205,7 +208,7 @@ export const DocSidebar = ({
|
|
|
205
208
|
);
|
|
206
209
|
})}
|
|
207
210
|
<div className="sticky -bottom-1 z-10 h-16 shrink-0 bg-gradient-to-t from-background via-background/80 to-background/50 blur-xs" />
|
|
208
|
-
</
|
|
211
|
+
</SidebarScrollArea>
|
|
209
212
|
</aside>
|
|
210
213
|
);
|
|
211
214
|
};
|
|
@@ -16,7 +16,7 @@ export const DocToc = ({
|
|
|
16
16
|
return (
|
|
17
17
|
<nav
|
|
18
18
|
aria-label="Table of contents"
|
|
19
|
-
className="sticky top-[calc(var(--header-height
|
|
19
|
+
className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[90svh] w-(--sidebar-width) flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex"
|
|
20
20
|
>
|
|
21
21
|
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
|
|
22
22
|
<div className="flex flex-col gap-2 p-4 pt-0 text-sm">
|
|
@@ -5,6 +5,7 @@ import { useCallback, useState } from "react";
|
|
|
5
5
|
import type { ReactNode } from "react";
|
|
6
6
|
|
|
7
7
|
import { Button } from "@/components/ui/button";
|
|
8
|
+
import { MorphIcon } from "@/components/ui/morph-icon";
|
|
8
9
|
import {
|
|
9
10
|
Popover,
|
|
10
11
|
PopoverContent,
|
|
@@ -86,26 +87,17 @@ export const MobileNav = ({
|
|
|
86
87
|
<PopoverTrigger asChild>
|
|
87
88
|
<Button
|
|
88
89
|
className={cn(
|
|
89
|
-
"extend-touch-target !p-0
|
|
90
|
+
"extend-touch-target !p-0 -ml-3 size-10 touch-manipulation items-center justify-start gap-2.5 hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 active:bg-transparent dark:hover:bg-transparent",
|
|
90
91
|
className
|
|
91
92
|
)}
|
|
92
93
|
variant="ghost"
|
|
93
94
|
>
|
|
94
|
-
<div className="relative flex
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)}
|
|
101
|
-
/>
|
|
102
|
-
<span
|
|
103
|
-
className={cn(
|
|
104
|
-
"absolute left-0 block h-0.5 w-4 bg-foreground transition-all duration-100",
|
|
105
|
-
open ? "top-[0.4rem] rotate-45" : "top-2.5"
|
|
106
|
-
)}
|
|
107
|
-
/>
|
|
108
|
-
</div>
|
|
95
|
+
<div className="relative flex size-10 items-center justify-center">
|
|
96
|
+
<MorphIcon
|
|
97
|
+
icon={open ? "cross" : "menu"}
|
|
98
|
+
size={16}
|
|
99
|
+
strokeWidth={2}
|
|
100
|
+
/>
|
|
109
101
|
<span className="sr-only">Toggle Menu</span>
|
|
110
102
|
</div>
|
|
111
103
|
</Button>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
let savedScrollTop = 0;
|
|
6
|
+
|
|
7
|
+
export const SidebarScrollArea = ({
|
|
8
|
+
children,
|
|
9
|
+
className,
|
|
10
|
+
}: {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}) => {
|
|
14
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
|
|
16
|
+
useLayoutEffect(() => {
|
|
17
|
+
const el = scrollRef.current;
|
|
18
|
+
if (!el) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (savedScrollTop > 0) {
|
|
23
|
+
el.scrollTop = savedScrollTop;
|
|
24
|
+
} else {
|
|
25
|
+
const active = el.querySelector<HTMLElement>("[data-active]");
|
|
26
|
+
if (active) {
|
|
27
|
+
active.scrollIntoView({ behavior: "instant", block: "center" });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const el = scrollRef.current;
|
|
34
|
+
if (!el) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let raf = 0;
|
|
39
|
+
const onScroll = () => {
|
|
40
|
+
cancelAnimationFrame(raf);
|
|
41
|
+
raf = requestAnimationFrame(() => {
|
|
42
|
+
savedScrollTop = el.scrollTop;
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
47
|
+
return () => {
|
|
48
|
+
cancelAnimationFrame(raf);
|
|
49
|
+
el.removeEventListener("scroll", onScroll);
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div ref={scrollRef} className={className}>
|
|
55
|
+
{children}
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
};
|