@hanzo/ui 5.3.26 → 5.3.29

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 (100) hide show
  1. package/content/index.ts +26 -0
  2. package/dist/util/index.js +6 -0
  3. package/dist/util/index.mjs +6 -1
  4. package/docs/_registry/index.ts +426 -0
  5. package/docs/_registry/layout/docs-min.tsx +197 -0
  6. package/docs/_registry/layout/page-min.tsx +128 -0
  7. package/docs/components/accordion.tsx +118 -0
  8. package/docs/components/banner.tsx +144 -0
  9. package/docs/components/callout.tsx +112 -0
  10. package/docs/components/card.tsx +52 -0
  11. package/docs/components/codeblock.tsx +258 -0
  12. package/docs/components/dialog/search-algolia.tsx +132 -0
  13. package/docs/components/dialog/search-default.tsx +131 -0
  14. package/docs/components/dialog/search-orama.tsx +143 -0
  15. package/docs/components/dialog/search.tsx +529 -0
  16. package/docs/components/dynamic-codeblock.tsx +129 -0
  17. package/docs/components/files.tsx +81 -0
  18. package/docs/components/github-info.tsx +107 -0
  19. package/docs/components/heading.tsx +33 -0
  20. package/docs/components/image-zoom.css +77 -0
  21. package/docs/components/image-zoom.tsx +58 -0
  22. package/docs/components/index.ts +7 -0
  23. package/docs/components/inline-toc.tsx +48 -0
  24. package/docs/components/sidebar/base.tsx +451 -0
  25. package/docs/components/sidebar/link-item.tsx +65 -0
  26. package/docs/components/sidebar/page-tree.tsx +113 -0
  27. package/docs/components/sidebar/tabs/dropdown.tsx +109 -0
  28. package/docs/components/sidebar/tabs/index.tsx +89 -0
  29. package/docs/components/steps.tsx +9 -0
  30. package/docs/components/tabs.tsx +203 -0
  31. package/docs/components/toc/clerk.tsx +173 -0
  32. package/docs/components/toc/default.tsx +57 -0
  33. package/docs/components/toc/index.tsx +136 -0
  34. package/docs/components/type-table.tsx +174 -0
  35. package/docs/components/ui/accordion.tsx +88 -0
  36. package/docs/components/ui/button.tsx +28 -0
  37. package/docs/components/ui/collapsible.tsx +42 -0
  38. package/docs/components/ui/navigation-menu.tsx +83 -0
  39. package/docs/components/ui/popover.tsx +32 -0
  40. package/docs/components/ui/scroll-area.tsx +59 -0
  41. package/docs/components/ui/tabs.tsx +145 -0
  42. package/docs/contexts/i18n.tsx +56 -0
  43. package/docs/contexts/search.tsx +165 -0
  44. package/docs/contexts/tree.tsx +65 -0
  45. package/docs/css/black.css +39 -0
  46. package/docs/css/catppuccin.css +49 -0
  47. package/docs/css/colors/index.css +51 -0
  48. package/docs/css/dusk.css +47 -0
  49. package/docs/css/layouts/docs.css +1 -0
  50. package/docs/css/layouts/home.css +1 -0
  51. package/docs/css/layouts/notebook.css +1 -0
  52. package/docs/css/neutral.css +7 -0
  53. package/docs/css/ocean.css +48 -0
  54. package/docs/css/preset.css +305 -0
  55. package/docs/css/purple.css +39 -0
  56. package/docs/css/shadcn.css +36 -0
  57. package/docs/css/shiki.css +90 -0
  58. package/docs/css/solar.css +75 -0
  59. package/docs/css/style.css +9 -0
  60. package/docs/css/vitepress.css +77 -0
  61. package/docs/i18n.tsx +30 -0
  62. package/docs/icons.tsx +354 -0
  63. package/docs/layouts/docs/client.tsx +129 -0
  64. package/docs/layouts/docs/index.tsx +321 -0
  65. package/docs/layouts/docs/page/client.tsx +376 -0
  66. package/docs/layouts/docs/page/index.tsx +251 -0
  67. package/docs/layouts/docs/sidebar.tsx +265 -0
  68. package/docs/layouts/home/client.tsx +375 -0
  69. package/docs/layouts/home/index.tsx +51 -0
  70. package/docs/layouts/home/navbar.tsx +55 -0
  71. package/docs/layouts/notebook/client.tsx +281 -0
  72. package/docs/layouts/notebook/index.tsx +461 -0
  73. package/docs/layouts/notebook/page/client.tsx +375 -0
  74. package/docs/layouts/notebook/page/index.tsx +251 -0
  75. package/docs/layouts/notebook/sidebar.tsx +248 -0
  76. package/docs/layouts/shared/index.tsx +89 -0
  77. package/docs/layouts/shared/language-toggle.tsx +66 -0
  78. package/docs/layouts/shared/link-item.tsx +119 -0
  79. package/docs/layouts/shared/search-toggle.tsx +78 -0
  80. package/docs/layouts/shared/theme-toggle.tsx +86 -0
  81. package/docs/mdx.server.tsx +37 -0
  82. package/docs/mdx.tsx +97 -0
  83. package/docs/og.tsx +101 -0
  84. package/docs/page.tsx +85 -0
  85. package/docs/provider/base.tsx +173 -0
  86. package/docs/provider/next.tsx +23 -0
  87. package/docs/provider/react-router.tsx +23 -0
  88. package/docs/provider/tanstack.tsx +23 -0
  89. package/docs/provider/waku.tsx +23 -0
  90. package/docs/source.ts +3 -0
  91. package/docs/theme/typography/LICENSE +21 -0
  92. package/docs/theme/typography/index.ts +201 -0
  93. package/docs/theme/typography/styles.ts +449 -0
  94. package/docs/utils/cn.ts +1 -0
  95. package/docs/utils/is-active.ts +23 -0
  96. package/docs/utils/merge-refs.ts +15 -0
  97. package/docs/utils/use-copy-button.ts +39 -0
  98. package/docs/utils/use-footer-items.ts +27 -0
  99. package/docs/utils/use-is-scroll-top.ts +21 -0
  100. package/package.json +4 -2
@@ -0,0 +1,375 @@
1
+ 'use client';
2
+
3
+ import {
4
+ type ComponentProps,
5
+ createContext,
6
+ Fragment,
7
+ use,
8
+ useEffect,
9
+ useEffectEvent,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from 'react';
14
+ import { ChevronDown, ChevronLeft, ChevronRight } from '@icons';
15
+ import Link from '@hanzo/docs-core/link';
16
+ import { cn } from '@/utils/cn';
17
+ import { useI18n } from '@/contexts/i18n';
18
+ import { useTreeContext, useTreePath } from '@/contexts/tree';
19
+ import type * as PageTree from '@hanzo/docs-core/page-tree';
20
+ import { usePathname } from '@hanzo/docs-core/framework';
21
+ import {
22
+ type BreadcrumbOptions,
23
+ getBreadcrumbItemsFromPath,
24
+ } from '@hanzo/docs-core/breadcrumb';
25
+ import { isActive } from '@/utils/is-active';
26
+ import {
27
+ Collapsible,
28
+ CollapsibleContent,
29
+ CollapsibleTrigger,
30
+ } from '@/components/ui/collapsible';
31
+ import { useTOCItems } from '@/components/toc';
32
+ import { useActiveAnchor } from '@hanzo/docs-core/toc';
33
+ import { LayoutContext } from '../client';
34
+ import { useFooterItems } from '@/utils/use-footer-items';
35
+
36
+ const TocPopoverContext = createContext<{
37
+ open: boolean;
38
+ setOpen: (open: boolean) => void;
39
+ } | null>(null);
40
+
41
+ export function PageTOCPopover({
42
+ className,
43
+ children,
44
+ ...rest
45
+ }: ComponentProps<'div'>) {
46
+ const ref = useRef<HTMLElement>(null);
47
+ const [open, setOpen] = useState(false);
48
+ const { isNavTransparent } = use(LayoutContext)!;
49
+
50
+ const onClick = useEffectEvent((e: Event) => {
51
+ if (!open) return;
52
+
53
+ if (ref.current && !ref.current.contains(e.target as HTMLElement))
54
+ setOpen(false);
55
+ });
56
+
57
+ useEffect(() => {
58
+ window.addEventListener('click', onClick);
59
+
60
+ return () => {
61
+ window.removeEventListener('click', onClick);
62
+ };
63
+ }, []);
64
+
65
+ return (
66
+ <TocPopoverContext
67
+ value={useMemo(
68
+ () => ({
69
+ open,
70
+ setOpen,
71
+ }),
72
+ [setOpen, open],
73
+ )}
74
+ >
75
+ <Collapsible
76
+ open={open}
77
+ onOpenChange={setOpen}
78
+ data-toc-popover=""
79
+ className={cn(
80
+ 'sticky top-(--fd-docs-row-2) z-10 [grid-area:toc-popover] h-(--fd-toc-popover-height) xl:hidden max-xl:layout:[--fd-toc-popover-height:--spacing(10)]',
81
+ className,
82
+ )}
83
+ {...rest}
84
+ >
85
+ <header
86
+ ref={ref}
87
+ className={cn(
88
+ 'border-b backdrop-blur-sm transition-colors',
89
+ (!isNavTransparent || open) && 'bg-fd-background/80',
90
+ open && 'shadow-lg',
91
+ )}
92
+ >
93
+ {children}
94
+ </header>
95
+ </Collapsible>
96
+ </TocPopoverContext>
97
+ );
98
+ }
99
+
100
+ export function PageTOCPopoverTrigger({
101
+ className,
102
+ ...props
103
+ }: ComponentProps<'button'>) {
104
+ const { text } = useI18n();
105
+ const { open } = use(TocPopoverContext)!;
106
+ const items = useTOCItems();
107
+ const active = useActiveAnchor();
108
+ const selected = useMemo(
109
+ () => items.findIndex((item) => active === item.url.slice(1)),
110
+ [items, active],
111
+ );
112
+ const path = useTreePath().at(-1);
113
+ const showItem = selected !== -1 && !open;
114
+
115
+ return (
116
+ <CollapsibleTrigger
117
+ className={cn(
118
+ 'flex w-full h-10 items-center text-sm text-fd-muted-foreground gap-2.5 px-4 py-2.5 text-start focus-visible:outline-none [&_svg]:size-4 md:px-6',
119
+ className,
120
+ )}
121
+ data-toc-popover-trigger=""
122
+ {...props}
123
+ >
124
+ <ProgressCircle
125
+ value={(selected + 1) / Math.max(1, items.length)}
126
+ max={1}
127
+ className={cn('shrink-0', open && 'text-fd-primary')}
128
+ />
129
+ <span className="grid flex-1 *:my-auto *:row-start-1 *:col-start-1">
130
+ <span
131
+ className={cn(
132
+ 'truncate transition-all',
133
+ open && 'text-fd-foreground',
134
+ showItem && 'opacity-0 -translate-y-full pointer-events-none',
135
+ )}
136
+ >
137
+ {path?.name ?? text.toc}
138
+ </span>
139
+ <span
140
+ className={cn(
141
+ 'truncate transition-all',
142
+ !showItem && 'opacity-0 translate-y-full pointer-events-none',
143
+ )}
144
+ >
145
+ {items[selected]?.title}
146
+ </span>
147
+ </span>
148
+ <ChevronDown
149
+ className={cn(
150
+ 'shrink-0 transition-transform mx-0.5',
151
+ open && 'rotate-180',
152
+ )}
153
+ />
154
+ </CollapsibleTrigger>
155
+ );
156
+ }
157
+
158
+ interface ProgressCircleProps extends Omit<
159
+ React.ComponentProps<'svg'>,
160
+ 'strokeWidth'
161
+ > {
162
+ value: number;
163
+ strokeWidth?: number;
164
+ size?: number;
165
+ min?: number;
166
+ max?: number;
167
+ }
168
+
169
+ function clamp(input: number, min: number, max: number): number {
170
+ if (input < min) return min;
171
+ if (input > max) return max;
172
+ return input;
173
+ }
174
+
175
+ function ProgressCircle({
176
+ value,
177
+ strokeWidth = 2,
178
+ size = 24,
179
+ min = 0,
180
+ max = 100,
181
+ ...restSvgProps
182
+ }: ProgressCircleProps) {
183
+ const normalizedValue = clamp(value, min, max);
184
+ const radius = (size - strokeWidth) / 2;
185
+ const circumference = 2 * Math.PI * radius;
186
+ const progress = (normalizedValue / max) * circumference;
187
+ const circleProps = {
188
+ cx: size / 2,
189
+ cy: size / 2,
190
+ r: radius,
191
+ fill: 'none',
192
+ strokeWidth,
193
+ };
194
+
195
+ return (
196
+ <svg
197
+ role="progressbar"
198
+ viewBox={`0 0 ${size} ${size}`}
199
+ aria-valuenow={normalizedValue}
200
+ aria-valuemin={min}
201
+ aria-valuemax={max}
202
+ {...restSvgProps}
203
+ >
204
+ <circle {...circleProps} className="stroke-current/25" />
205
+ <circle
206
+ {...circleProps}
207
+ stroke="currentColor"
208
+ strokeDasharray={circumference}
209
+ strokeDashoffset={circumference - progress}
210
+ strokeLinecap="round"
211
+ transform={`rotate(-90 ${size / 2} ${size / 2})`}
212
+ className="transition-all"
213
+ />
214
+ </svg>
215
+ );
216
+ }
217
+
218
+ export function PageTOCPopoverContent(props: ComponentProps<'div'>) {
219
+ return (
220
+ <CollapsibleContent
221
+ data-toc-popover-content=""
222
+ {...props}
223
+ className={cn('flex flex-col px-4 max-h-[50vh] md:px-6', props.className)}
224
+ >
225
+ {props.children}
226
+ </CollapsibleContent>
227
+ );
228
+ }
229
+
230
+ export function PageLastUpdate({
231
+ date: value,
232
+ ...props
233
+ }: Omit<ComponentProps<'p'>, 'children'> & { date: Date }) {
234
+ const { text } = useI18n();
235
+ const [date, setDate] = useState('');
236
+
237
+ useEffect(() => {
238
+ // to the timezone of client
239
+ setDate(value.toLocaleDateString());
240
+ }, [value]);
241
+
242
+ return (
243
+ <p
244
+ {...props}
245
+ className={cn('text-sm text-fd-muted-foreground', props.className)}
246
+ >
247
+ {text.lastUpdate} {date}
248
+ </p>
249
+ );
250
+ }
251
+
252
+ type Item = Pick<PageTree.Item, 'name' | 'description' | 'url'>;
253
+ export interface FooterProps extends ComponentProps<'div'> {
254
+ /**
255
+ * Items including information for the next and previous page
256
+ */
257
+ items?: {
258
+ previous?: Item;
259
+ next?: Item;
260
+ };
261
+ }
262
+
263
+ export function PageFooter({ items, ...props }: FooterProps) {
264
+ const footerList = useFooterItems();
265
+ const pathname = usePathname();
266
+ const { previous, next } = useMemo(() => {
267
+ if (items) return items;
268
+
269
+ const idx = footerList.findIndex((item) =>
270
+ isActive(item.url, pathname, false),
271
+ );
272
+
273
+ if (idx === -1) return {};
274
+ return {
275
+ previous: footerList[idx - 1],
276
+ next: footerList[idx + 1],
277
+ };
278
+ }, [footerList, items, pathname]);
279
+
280
+ return (
281
+ <div
282
+ {...props}
283
+ className={cn(
284
+ '@container grid gap-4',
285
+ previous && next ? 'grid-cols-2' : 'grid-cols-1',
286
+ props.className,
287
+ )}
288
+ >
289
+ {previous ? <FooterItem item={previous} index={0} /> : null}
290
+ {next ? <FooterItem item={next} index={1} /> : null}
291
+ </div>
292
+ );
293
+ }
294
+
295
+ function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
296
+ const { text } = useI18n();
297
+ const Icon = index === 0 ? ChevronLeft : ChevronRight;
298
+
299
+ return (
300
+ <Link
301
+ href={item.url}
302
+ className={cn(
303
+ 'flex flex-col gap-2 rounded-lg border p-4 text-sm transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground @max-lg:col-span-full',
304
+ index === 1 && 'text-end',
305
+ )}
306
+ >
307
+ <div
308
+ className={cn(
309
+ 'inline-flex items-center gap-1.5 font-medium',
310
+ index === 1 && 'flex-row-reverse',
311
+ )}
312
+ >
313
+ <Icon className="-mx-1 size-4 shrink-0 rtl:rotate-180" />
314
+ <p>{item.name}</p>
315
+ </div>
316
+ <p className="text-fd-muted-foreground truncate">
317
+ {item.description ?? (index === 0 ? text.previousPage : text.nextPage)}
318
+ </p>
319
+ </Link>
320
+ );
321
+ }
322
+
323
+ export type BreadcrumbProps = BreadcrumbOptions & ComponentProps<'div'>;
324
+
325
+ export function PageBreadcrumb({
326
+ includeRoot,
327
+ includeSeparator,
328
+ includePage,
329
+ ...props
330
+ }: BreadcrumbProps) {
331
+ const path = useTreePath();
332
+ const { root } = useTreeContext();
333
+ const items = useMemo(() => {
334
+ return getBreadcrumbItemsFromPath(root, path, {
335
+ includePage,
336
+ includeSeparator,
337
+ includeRoot,
338
+ });
339
+ }, [includePage, includeRoot, includeSeparator, path, root]);
340
+
341
+ if (items.length === 0) return null;
342
+
343
+ return (
344
+ <div
345
+ {...props}
346
+ className={cn(
347
+ 'flex items-center gap-1.5 text-sm text-fd-muted-foreground',
348
+ props.className,
349
+ )}
350
+ >
351
+ {items.map((item, i) => {
352
+ const className = cn(
353
+ 'truncate',
354
+ i === items.length - 1 && 'text-fd-primary font-medium',
355
+ );
356
+
357
+ return (
358
+ <Fragment key={i}>
359
+ {i !== 0 && <ChevronRight className="size-3.5 shrink-0" />}
360
+ {item.url ? (
361
+ <Link
362
+ href={item.url}
363
+ className={cn(className, 'transition-opacity hover:opacity-80')}
364
+ >
365
+ {item.name}
366
+ </Link>
367
+ ) : (
368
+ <span className={className}>{item.name}</span>
369
+ )}
370
+ </Fragment>
371
+ );
372
+ })}
373
+ </div>
374
+ );
375
+ }
@@ -0,0 +1,251 @@
1
+ import type { ComponentProps, ReactNode } from 'react';
2
+ import { cn } from '@/utils/cn';
3
+ import { buttonVariants } from '@/components/ui/button';
4
+ import { Edit, Text } from '@icons';
5
+ import { I18nLabel } from '@/contexts/i18n';
6
+ import {
7
+ type BreadcrumbProps,
8
+ type FooterProps,
9
+ PageBreadcrumb,
10
+ PageFooter,
11
+ PageTOCPopover,
12
+ PageTOCPopoverContent,
13
+ PageTOCPopoverTrigger,
14
+ } from './client';
15
+ import type { AnchorProviderProps, TOCItemType } from '@hanzo/docs-core/toc';
16
+ import * as TocDefault from '@/components/toc/default';
17
+ import * as TocClerk from '@/components/toc/clerk';
18
+ import { TOCProvider, TOCScrollArea } from '@/components/toc';
19
+
20
+ interface BreadcrumbOptions extends BreadcrumbProps {
21
+ enabled: boolean;
22
+ component: ReactNode;
23
+ }
24
+
25
+ interface FooterOptions extends FooterProps {
26
+ enabled: boolean;
27
+ component: ReactNode;
28
+ }
29
+
30
+ export interface DocsPageProps {
31
+ toc?: TOCItemType[];
32
+ tableOfContent?: Partial<TableOfContentOptions>;
33
+ tableOfContentPopover?: Partial<TableOfContentPopoverOptions>;
34
+
35
+ /**
36
+ * Extend the page to fill all available space
37
+ *
38
+ * @defaultValue false
39
+ */
40
+ full?: boolean;
41
+
42
+ /**
43
+ * Replace or disable breadcrumb
44
+ */
45
+ breadcrumb?: Partial<BreadcrumbOptions>;
46
+
47
+ /**
48
+ * Footer navigation, you can disable it by passing `false`
49
+ */
50
+ footer?: Partial<FooterOptions>;
51
+
52
+ children?: ReactNode;
53
+ }
54
+
55
+ type TableOfContentOptions = Pick<AnchorProviderProps, 'single'> & {
56
+ /**
57
+ * Custom content in TOC container, before the main TOC
58
+ */
59
+ header?: ReactNode;
60
+
61
+ /**
62
+ * Custom content in TOC container, after the main TOC
63
+ */
64
+ footer?: ReactNode;
65
+
66
+ enabled: boolean;
67
+ component: ReactNode;
68
+
69
+ /**
70
+ * @defaultValue 'normal'
71
+ */
72
+ style?: 'normal' | 'clerk';
73
+ };
74
+
75
+ type TableOfContentPopoverOptions = Omit<TableOfContentOptions, 'single'>;
76
+
77
+ export function DocsPage({
78
+ breadcrumb: {
79
+ enabled: breadcrumbEnabled = true,
80
+ component: breadcrumb,
81
+ ...breadcrumbProps
82
+ } = {},
83
+ footer = {},
84
+ full = false,
85
+ tableOfContentPopover: {
86
+ enabled: tocPopoverEnabled,
87
+ component: tocPopover,
88
+ ...tocPopoverOptions
89
+ } = {},
90
+ tableOfContent: {
91
+ enabled: tocEnabled,
92
+ component: tocReplace,
93
+ ...tocOptions
94
+ } = {},
95
+ toc = [],
96
+ children,
97
+ }: DocsPageProps) {
98
+ // disable TOC on full mode, you can still enable it with `enabled` option.
99
+ tocEnabled ??=
100
+ !full &&
101
+ (toc.length > 0 ||
102
+ tocOptions.footer !== undefined ||
103
+ tocOptions.header !== undefined);
104
+
105
+ tocPopoverEnabled ??=
106
+ toc.length > 0 ||
107
+ tocPopoverOptions.header !== undefined ||
108
+ tocPopoverOptions.footer !== undefined;
109
+
110
+ let wrapper = (children: ReactNode) => children;
111
+
112
+ if (tocEnabled || tocPopoverEnabled) {
113
+ wrapper = (children) => (
114
+ <TOCProvider single={tocOptions.single} toc={toc}>
115
+ {children}
116
+ </TOCProvider>
117
+ );
118
+ }
119
+
120
+ return wrapper(
121
+ <>
122
+ {tocPopoverEnabled &&
123
+ (tocPopover ?? (
124
+ <PageTOCPopover>
125
+ <PageTOCPopoverTrigger />
126
+ <PageTOCPopoverContent>
127
+ {tocPopoverOptions.header}
128
+ <TOCScrollArea>
129
+ {tocPopoverOptions.style === 'clerk' ? (
130
+ <TocClerk.TOCItems />
131
+ ) : (
132
+ <TocDefault.TOCItems />
133
+ )}
134
+ </TOCScrollArea>
135
+ {tocPopoverOptions.footer}
136
+ </PageTOCPopoverContent>
137
+ </PageTOCPopover>
138
+ ))}
139
+ <article
140
+ id="nd-page"
141
+ data-full={full}
142
+ className={cn(
143
+ 'flex flex-col [grid-area:main] px-4 py-6 gap-4 md:px-6 md:pt-8 xl:px-8 xl:pt-14 *:max-w-[900px]',
144
+ full && '*:max-w-[1285px]',
145
+ )}
146
+ >
147
+ {breadcrumbEnabled &&
148
+ (breadcrumb ?? <PageBreadcrumb {...breadcrumbProps} />)}
149
+ {children}
150
+ {footer.enabled !== false &&
151
+ (footer.component ?? <PageFooter items={footer.items} />)}
152
+ </article>
153
+ {tocEnabled &&
154
+ (tocReplace ?? (
155
+ <div
156
+ id="nd-toc"
157
+ className="sticky top-(--fd-docs-row-3) [grid-area:toc] h-[calc(var(--fd-docs-height)-var(--fd-docs-row-3))] flex flex-col w-(--fd-toc-width) pt-12 pe-4 pb-2 xl:layout:[--fd-toc-width:268px] max-xl:hidden"
158
+ >
159
+ {tocOptions.header}
160
+ <h3
161
+ id="toc-title"
162
+ className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground"
163
+ >
164
+ <Text className="size-4" />
165
+ <I18nLabel label="toc" />
166
+ </h3>
167
+ <TOCScrollArea>
168
+ {tocOptions.style === 'clerk' ? (
169
+ <TocClerk.TOCItems />
170
+ ) : (
171
+ <TocDefault.TOCItems />
172
+ )}
173
+ </TOCScrollArea>
174
+ {tocOptions.footer}
175
+ </div>
176
+ ))}
177
+ </>,
178
+ );
179
+ }
180
+
181
+ export function EditOnGitHub(props: ComponentProps<'a'>) {
182
+ return (
183
+ <a
184
+ target="_blank"
185
+ rel="noreferrer noopener"
186
+ {...props}
187
+ className={cn(
188
+ buttonVariants({
189
+ color: 'secondary',
190
+ size: 'sm',
191
+ className: 'gap-1.5 not-prose',
192
+ }),
193
+ props.className,
194
+ )}
195
+ >
196
+ {props.children ?? (
197
+ <>
198
+ <Edit className="size-3.5" />
199
+ <I18nLabel label="editOnGithub" />
200
+ </>
201
+ )}
202
+ </a>
203
+ );
204
+ }
205
+
206
+ /**
207
+ * Add typography styles
208
+ */
209
+ export function DocsBody({
210
+ children,
211
+ className,
212
+ ...props
213
+ }: ComponentProps<'div'>) {
214
+ return (
215
+ <div {...props} className={cn('prose flex-1', className)}>
216
+ {children}
217
+ </div>
218
+ );
219
+ }
220
+
221
+ export function DocsDescription({
222
+ children,
223
+ className,
224
+ ...props
225
+ }: ComponentProps<'p'>) {
226
+ // Don't render if no description provided
227
+ if (children === undefined) return null;
228
+
229
+ return (
230
+ <p
231
+ {...props}
232
+ className={cn('mb-8 text-lg text-fd-muted-foreground', className)}
233
+ >
234
+ {children}
235
+ </p>
236
+ );
237
+ }
238
+
239
+ export function DocsTitle({
240
+ children,
241
+ className,
242
+ ...props
243
+ }: ComponentProps<'h1'>) {
244
+ return (
245
+ <h1 {...props} className={cn('text-[1.75em] font-semibold', className)}>
246
+ {children}
247
+ </h1>
248
+ );
249
+ }
250
+
251
+ export { PageLastUpdate, PageBreadcrumb } from './client';