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.
Files changed (86) hide show
  1. package/README.md +11 -47
  2. package/dev-server/app/layout.tsx +1 -1
  3. package/dev-server/next.config.js +19 -9
  4. package/dev-server/tsconfig.json +0 -3
  5. package/dist/cli.mjs +732 -123
  6. package/dist/cli.mjs.map +1 -1
  7. package/docs/app/globals.css +15 -1
  8. package/docs/components/api/api-playground.tsx +2 -2
  9. package/docs/components/docs/copy-page-menu.tsx +55 -27
  10. package/docs/components/docs/doc-header.tsx +1 -1
  11. package/docs/components/docs/doc-shell.tsx +89 -88
  12. package/docs/components/docs/doc-sidebar.tsx +6 -3
  13. package/docs/components/docs/doc-toc.tsx +1 -1
  14. package/docs/components/docs/mobile-nav.tsx +8 -16
  15. package/docs/components/docs/sidebar-scroll-area.tsx +58 -0
  16. package/docs/components/git/repo-picker.tsx +526 -0
  17. package/docs/components/mdx/agent-instructions.tsx +17 -0
  18. package/docs/components/mdx/code-block.tsx +6 -1
  19. package/docs/components/mdx/code-group.tsx +1 -1
  20. package/docs/components/mdx/iframe.tsx +62 -0
  21. package/docs/components/mdx/index.tsx +4 -0
  22. package/docs/components/mdx/tabs.tsx +5 -5
  23. package/docs/components/mdx/video.tsx +45 -12
  24. package/docs/components/third-parties.tsx +29 -0
  25. package/docs/components/ui/badge.tsx +61 -0
  26. package/docs/components/ui/breadcrumb.tsx +61 -41
  27. package/docs/components/ui/button-group.tsx +83 -0
  28. package/docs/components/ui/button.tsx +30 -55
  29. package/docs/components/ui/command.tsx +32 -4
  30. package/docs/components/ui/copy-button.tsx +12 -19
  31. package/docs/components/ui/dialog.tsx +50 -1
  32. package/docs/components/ui/input.tsx +16 -97
  33. package/docs/components/ui/kbd.tsx +98 -0
  34. package/docs/components/ui/morph-icon.tsx +79 -0
  35. package/docs/components/ui/popover.tsx +225 -30
  36. package/docs/components/ui/search.tsx +0 -9
  37. package/docs/components/ui/sheet.tsx +30 -1
  38. package/docs/components/ui/sidebar.tsx +332 -7
  39. package/docs/components/ui/site-footer.tsx +6 -4
  40. package/docs/components/ui/skeleton.tsx +11 -0
  41. package/docs/components/ui/switch.tsx +32 -0
  42. package/docs/components/ui/tabs.tsx +138 -0
  43. package/docs/lib/api-client.ts +72 -0
  44. package/docs/lib/contextual-options.ts +9 -0
  45. package/docs/lib/dashboard-session.ts +167 -0
  46. package/docs/lib/db.ts +13 -0
  47. package/docs/lib/env.ts +4 -3
  48. package/docs/lib/etag.ts +22 -0
  49. package/docs/lib/github-install.ts +33 -0
  50. package/docs/lib/project-authz.ts +46 -0
  51. package/docs/lib/routes.ts +5 -1
  52. package/docs/lib/supabase.ts +30 -6
  53. package/docs/lib/tenancy.ts +1 -0
  54. package/docs/lib/tenant-static.ts +206 -4
  55. package/docs/lib/tenants.ts +5 -1
  56. package/docs/lib/time-ago.ts +24 -0
  57. package/docs/lib/use-tab-observer.ts +71 -0
  58. package/package.json +3 -1
  59. package/packages/@repo/common/package.json +2 -2
  60. package/packages/@repo/contracts/dist/git.d.ts +28 -0
  61. package/packages/@repo/contracts/dist/git.d.ts.map +1 -0
  62. package/packages/@repo/contracts/dist/git.js +24 -0
  63. package/packages/@repo/contracts/dist/index.d.ts +1 -1
  64. package/packages/@repo/contracts/dist/index.d.ts.map +1 -1
  65. package/packages/@repo/contracts/dist/index.js +1 -1
  66. package/packages/@repo/contracts/package.json +2 -2
  67. package/packages/@repo/contracts/src/git.ts +31 -0
  68. package/packages/@repo/contracts/src/index.ts +1 -1
  69. package/packages/@repo/models/dist/docs-config.d.ts +6 -0
  70. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  71. package/packages/@repo/models/dist/docs-config.js +1 -0
  72. package/packages/@repo/models/package.json +2 -2
  73. package/packages/@repo/models/src/docs-config.ts +1 -0
  74. package/packages/@repo/prebuild/package.json +2 -2
  75. package/packages/@repo/previewing/dist/index.d.ts +3 -0
  76. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  77. package/packages/@repo/previewing/dist/index.js +48 -0
  78. package/packages/@repo/previewing/package.json +2 -2
  79. package/packages/@repo/previewing/src/index.ts +56 -0
  80. package/packages/@repo/validation/package.json +2 -2
  81. package/packages/@repo/validation/src/blodemd-docs-schema.json +1 -0
  82. package/scripts/prepare-package.mjs +14 -0
  83. package/packages/@repo/contracts/dist/api-key.d.ts +0 -30
  84. package/packages/@repo/contracts/dist/api-key.d.ts.map +0 -1
  85. package/packages/@repo/contracts/dist/api-key.js +0 -20
  86. package/packages/@repo/contracts/src/api-key.ts +0 -27
@@ -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 shadow-input 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"
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 shadow-input 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"
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
- Checkmark1Icon,
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" ? Checkmark1Icon : CopySimpleIcon;
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
- const [pageUrl, setPageUrl] = useState("");
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
- setPageUrl(
157
- new URL(contentUrl ?? window.location.href, window.location.href).href
158
- );
159
- }, [contentUrl]);
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
- // iOS Safari loses the user gesture context after any async gap (e.g. a
207
- // fetch). To keep clipboard access working, we call clipboard.write()
208
- // synchronously within the gesture and pass a Promise to ClipboardItem
209
- // so the content resolves later while the gesture context stays alive.
210
- const blobPromise = (async () => {
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
- return new Blob([markdown], { type: "text/plain" });
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 gap-2">
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={cn(
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
- className={cn(
181
- "mx-auto flex w-full min-w-0 flex-1 flex-col gap-6 py-6 text-neutral-800 lg:py-8 dark:text-neutral-300",
182
- pageMode === "wide" ? "max-w-[60rem]" : "max-w-[40rem]"
183
- )}
184
- >
185
- <div className="flex flex-col gap-2">
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
- <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
- ))}
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
- </div>
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}
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
- ) : 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
- />
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
- </div>
264
- </Link>
265
- ) : null}
266
- </nav>
267
- ) : null}
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
- <div 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">
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
- </div>
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,4rem)+1px)] z-30 ml-auto hidden h-[90svh] w-56 shrink-0 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex"
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 h-8 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
+ "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 h-8 w-4 items-center justify-center">
95
- <div className="relative size-4">
96
- <span
97
- className={cn(
98
- "absolute left-0 block h-0.5 w-4 bg-foreground transition-all duration-100",
99
- open ? "top-[0.4rem] -rotate-45" : "top-1"
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
+ };