@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,321 @@
1
+ import type * as PageTree from '@hanzo/docs-core/page-tree';
2
+ import {
3
+ type ComponentProps,
4
+ type HTMLAttributes,
5
+ type ReactNode,
6
+ useMemo,
7
+ } from 'react';
8
+ import { Languages, Sidebar as SidebarIcon } from '@icons';
9
+ import { cn } from '@/utils/cn';
10
+ import { buttonVariants } from '@/components/ui/button';
11
+ import {
12
+ Sidebar,
13
+ SidebarCollapseTrigger,
14
+ SidebarContent,
15
+ SidebarDrawer,
16
+ SidebarLinkItem,
17
+ SidebarPageTree,
18
+ SidebarTrigger,
19
+ SidebarViewport,
20
+ } from './sidebar';
21
+ import { type BaseLayoutProps, resolveLinkItems } from '@/layouts/shared';
22
+ import { LinkItem } from '@/layouts/shared/link-item';
23
+ import {
24
+ LanguageToggle,
25
+ LanguageToggleText,
26
+ } from '@/layouts/shared/language-toggle';
27
+ import {
28
+ LayoutBody,
29
+ LayoutContextProvider,
30
+ LayoutHeader,
31
+ LayoutTabs,
32
+ } from './client';
33
+ import { TreeContextProvider } from '@/contexts/tree';
34
+ import { ThemeToggle } from '../shared/theme-toggle';
35
+ import Link from '@hanzo/docs-core/link';
36
+ import {
37
+ LargeSearchToggle,
38
+ SearchToggle,
39
+ } from '@/layouts/shared/search-toggle';
40
+ import {
41
+ getSidebarTabs,
42
+ type GetSidebarTabsOptions,
43
+ } from '@/components/sidebar/tabs';
44
+ import type { SidebarPageTreeComponents } from '@/components/sidebar/page-tree';
45
+ import {
46
+ SidebarTabsDropdown,
47
+ type SidebarTabWithProps,
48
+ } from '@/components/sidebar/tabs/dropdown';
49
+
50
+ export interface DocsLayoutProps extends BaseLayoutProps {
51
+ tree: PageTree.Root;
52
+
53
+ sidebar?: SidebarOptions;
54
+
55
+ tabMode?: 'top' | 'auto';
56
+
57
+ /**
58
+ * Props for the `div` container
59
+ */
60
+ containerProps?: HTMLAttributes<HTMLDivElement>;
61
+ }
62
+
63
+ interface SidebarOptions
64
+ extends
65
+ ComponentProps<'aside'>,
66
+ Pick<ComponentProps<typeof Sidebar>, 'defaultOpenLevel' | 'prefetch'> {
67
+ enabled?: boolean;
68
+ component?: ReactNode;
69
+ components?: Partial<SidebarPageTreeComponents>;
70
+
71
+ /**
72
+ * Root Toggle options
73
+ */
74
+ tabs?: SidebarTabWithProps[] | GetSidebarTabsOptions | false;
75
+
76
+ banner?: ReactNode;
77
+ footer?: ReactNode;
78
+
79
+ /**
80
+ * Support collapsing the sidebar on desktop mode
81
+ *
82
+ * @defaultValue true
83
+ */
84
+ collapsible?: boolean;
85
+ }
86
+
87
+ export function DocsLayout({
88
+ nav: { transparentMode, ...nav } = {},
89
+ sidebar: {
90
+ tabs: sidebarTabs,
91
+ enabled: sidebarEnabled = true,
92
+ defaultOpenLevel,
93
+ prefetch,
94
+ ...sidebarProps
95
+ } = {},
96
+ searchToggle = {},
97
+ themeSwitch = {},
98
+ tabMode = 'auto',
99
+ i18n = false,
100
+ children,
101
+ tree,
102
+ ...props
103
+ }: DocsLayoutProps) {
104
+ const tabs = useMemo(() => {
105
+ if (Array.isArray(sidebarTabs)) {
106
+ return sidebarTabs;
107
+ }
108
+ if (typeof sidebarTabs === 'object') {
109
+ return getSidebarTabs(tree, sidebarTabs);
110
+ }
111
+ if (sidebarTabs !== false) {
112
+ return getSidebarTabs(tree);
113
+ }
114
+ return [];
115
+ }, [tree, sidebarTabs]);
116
+ const links = resolveLinkItems(props);
117
+
118
+ function sidebar() {
119
+ const {
120
+ footer,
121
+ banner,
122
+ collapsible = true,
123
+ component,
124
+ components,
125
+ ...rest
126
+ } = sidebarProps;
127
+ if (component) return component;
128
+
129
+ const iconLinks = links.filter((item) => item.type === 'icon');
130
+ const viewport = (
131
+ <SidebarViewport>
132
+ {links
133
+ .filter((v) => v.type !== 'icon')
134
+ .map((item, i, list) => (
135
+ <SidebarLinkItem
136
+ key={i}
137
+ item={item}
138
+ className={cn(i === list.length - 1 && 'mb-4')}
139
+ />
140
+ ))}
141
+ <SidebarPageTree {...components} />
142
+ </SidebarViewport>
143
+ );
144
+
145
+ return (
146
+ <>
147
+ <SidebarContent {...rest}>
148
+ <div className="flex flex-col gap-3 p-4 pb-2">
149
+ <div className="flex">
150
+ <Link
151
+ href={nav.url ?? '/'}
152
+ className="inline-flex text-[0.9375rem] items-center gap-2.5 font-medium me-auto"
153
+ >
154
+ {nav.title}
155
+ </Link>
156
+ {nav.children}
157
+ {collapsible && (
158
+ <SidebarCollapseTrigger
159
+ className={cn(
160
+ buttonVariants({
161
+ color: 'ghost',
162
+ size: 'icon-sm',
163
+ className: 'mb-auto text-fd-muted-foreground',
164
+ }),
165
+ )}
166
+ >
167
+ <SidebarIcon />
168
+ </SidebarCollapseTrigger>
169
+ )}
170
+ </div>
171
+ {searchToggle.enabled !== false &&
172
+ (searchToggle.components?.lg ?? (
173
+ <LargeSearchToggle hideIfDisabled />
174
+ ))}
175
+ {tabs.length > 0 && tabMode === 'auto' && (
176
+ <SidebarTabsDropdown options={tabs} />
177
+ )}
178
+ {banner}
179
+ </div>
180
+ {viewport}
181
+ {(i18n ||
182
+ iconLinks.length > 0 ||
183
+ themeSwitch?.enabled !== false ||
184
+ footer) && (
185
+ <div className="flex flex-col border-t p-4 pt-2 empty:hidden">
186
+ <div className="flex text-fd-muted-foreground items-center empty:hidden">
187
+ {i18n && (
188
+ <LanguageToggle>
189
+ <Languages className="size-4.5" />
190
+ </LanguageToggle>
191
+ )}
192
+ {iconLinks.map((item, i) => (
193
+ <LinkItem
194
+ key={i}
195
+ item={item}
196
+ className={cn(
197
+ buttonVariants({ size: 'icon-sm', color: 'ghost' }),
198
+ )}
199
+ aria-label={item.label}
200
+ >
201
+ {item.icon}
202
+ </LinkItem>
203
+ ))}
204
+ {themeSwitch.enabled !== false &&
205
+ (themeSwitch.component ?? (
206
+ <ThemeToggle
207
+ className="ms-auto p-0"
208
+ mode={themeSwitch.mode}
209
+ />
210
+ ))}
211
+ </div>
212
+ {footer}
213
+ </div>
214
+ )}
215
+ </SidebarContent>
216
+ <SidebarDrawer>
217
+ <div className="flex flex-col gap-3 p-4 pb-2">
218
+ <div className="flex text-fd-muted-foreground items-center gap-1.5">
219
+ <div className="flex flex-1">
220
+ {iconLinks.map((item, i) => (
221
+ <LinkItem
222
+ key={i}
223
+ item={item}
224
+ className={cn(
225
+ buttonVariants({
226
+ size: 'icon-sm',
227
+ color: 'ghost',
228
+ className: 'p-2',
229
+ }),
230
+ )}
231
+ aria-label={item.label}
232
+ >
233
+ {item.icon}
234
+ </LinkItem>
235
+ ))}
236
+ </div>
237
+ {i18n && (
238
+ <LanguageToggle>
239
+ <Languages className="size-4.5" />
240
+ <LanguageToggleText />
241
+ </LanguageToggle>
242
+ )}
243
+ {themeSwitch.enabled !== false &&
244
+ (themeSwitch.component ?? (
245
+ <ThemeToggle className="p-0" mode={themeSwitch.mode} />
246
+ ))}
247
+ <SidebarTrigger
248
+ className={cn(
249
+ buttonVariants({
250
+ color: 'ghost',
251
+ size: 'icon-sm',
252
+ className: 'p-2',
253
+ }),
254
+ )}
255
+ >
256
+ <SidebarIcon />
257
+ </SidebarTrigger>
258
+ </div>
259
+ {tabs.length > 0 && <SidebarTabsDropdown options={tabs} />}
260
+ {banner}
261
+ </div>
262
+ {viewport}
263
+ <div className="flex flex-col border-t p-4 pt-2 empty:hidden">
264
+ {footer}
265
+ </div>
266
+ </SidebarDrawer>
267
+ </>
268
+ );
269
+ }
270
+
271
+ return (
272
+ <TreeContextProvider tree={tree}>
273
+ <LayoutContextProvider navTransparentMode={transparentMode}>
274
+ <Sidebar defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
275
+ <LayoutBody {...props.containerProps}>
276
+ {nav.enabled !== false &&
277
+ (nav.component ?? (
278
+ <LayoutHeader
279
+ id="nd-subnav"
280
+ className="[grid-area:header] sticky top-(--fd-docs-row-1) z-30 flex items-center ps-4 pe-2.5 border-b transition-colors backdrop-blur-sm h-(--fd-header-height) md:hidden max-md:layout:[--fd-header-height:--spacing(14)] data-[transparent=false]:bg-fd-background/80"
281
+ >
282
+ <Link
283
+ href={nav.url ?? '/'}
284
+ className="inline-flex items-center gap-2.5 font-semibold"
285
+ >
286
+ {nav.title}
287
+ </Link>
288
+ <div className="flex-1">{nav.children}</div>
289
+ {searchToggle.enabled !== false &&
290
+ (searchToggle.components?.sm ?? (
291
+ <SearchToggle className="p-2" hideIfDisabled />
292
+ ))}
293
+ {sidebarEnabled && (
294
+ <SidebarTrigger
295
+ className={cn(
296
+ buttonVariants({
297
+ color: 'ghost',
298
+ size: 'icon-sm',
299
+ className: 'p-2',
300
+ }),
301
+ )}
302
+ >
303
+ <SidebarIcon />
304
+ </SidebarTrigger>
305
+ )}
306
+ </LayoutHeader>
307
+ ))}
308
+ {sidebarEnabled && sidebar()}
309
+ {tabMode === 'top' && tabs.length > 0 && (
310
+ <LayoutTabs
311
+ options={tabs}
312
+ className="z-10 bg-fd-background border-b px-6 pt-3 xl:px-8 max-md:hidden"
313
+ />
314
+ )}
315
+ {children}
316
+ </LayoutBody>
317
+ </Sidebar>
318
+ </LayoutContextProvider>
319
+ </TreeContextProvider>
320
+ );
321
+ }
@@ -0,0 +1,376 @@
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
+
267
+ const { previous, next } = useMemo(() => {
268
+ if (items) return items;
269
+
270
+ const idx = footerList.findIndex((item) =>
271
+ isActive(item.url, pathname, false),
272
+ );
273
+
274
+ if (idx === -1) return {};
275
+ return {
276
+ previous: footerList[idx - 1],
277
+ next: footerList[idx + 1],
278
+ };
279
+ }, [footerList, items, pathname]);
280
+
281
+ return (
282
+ <div
283
+ {...props}
284
+ className={cn(
285
+ '@container grid gap-4',
286
+ previous && next ? 'grid-cols-2' : 'grid-cols-1',
287
+ props.className,
288
+ )}
289
+ >
290
+ {previous ? <FooterItem item={previous} index={0} /> : null}
291
+ {next ? <FooterItem item={next} index={1} /> : null}
292
+ </div>
293
+ );
294
+ }
295
+
296
+ function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
297
+ const { text } = useI18n();
298
+ const Icon = index === 0 ? ChevronLeft : ChevronRight;
299
+
300
+ return (
301
+ <Link
302
+ href={item.url}
303
+ className={cn(
304
+ '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',
305
+ index === 1 && 'text-end',
306
+ )}
307
+ >
308
+ <div
309
+ className={cn(
310
+ 'inline-flex items-center gap-1.5 font-medium',
311
+ index === 1 && 'flex-row-reverse',
312
+ )}
313
+ >
314
+ <Icon className="-mx-1 size-4 shrink-0 rtl:rotate-180" />
315
+ <p>{item.name}</p>
316
+ </div>
317
+ <p className="text-fd-muted-foreground truncate">
318
+ {item.description ?? (index === 0 ? text.previousPage : text.nextPage)}
319
+ </p>
320
+ </Link>
321
+ );
322
+ }
323
+
324
+ export type BreadcrumbProps = BreadcrumbOptions & ComponentProps<'div'>;
325
+
326
+ export function PageBreadcrumb({
327
+ includeRoot,
328
+ includeSeparator,
329
+ includePage,
330
+ ...props
331
+ }: BreadcrumbProps) {
332
+ const path = useTreePath();
333
+ const { root } = useTreeContext();
334
+ const items = useMemo(() => {
335
+ return getBreadcrumbItemsFromPath(root, path, {
336
+ includePage,
337
+ includeSeparator,
338
+ includeRoot,
339
+ });
340
+ }, [includePage, includeRoot, includeSeparator, path, root]);
341
+
342
+ if (items.length === 0) return null;
343
+
344
+ return (
345
+ <div
346
+ {...props}
347
+ className={cn(
348
+ 'flex items-center gap-1.5 text-sm text-fd-muted-foreground',
349
+ props.className,
350
+ )}
351
+ >
352
+ {items.map((item, i) => {
353
+ const className = cn(
354
+ 'truncate',
355
+ i === items.length - 1 && 'text-fd-primary font-medium',
356
+ );
357
+
358
+ return (
359
+ <Fragment key={i}>
360
+ {i !== 0 && <ChevronRight className="size-3.5 shrink-0" />}
361
+ {item.url ? (
362
+ <Link
363
+ href={item.url}
364
+ className={cn(className, 'transition-opacity hover:opacity-80')}
365
+ >
366
+ {item.name}
367
+ </Link>
368
+ ) : (
369
+ <span className={className}>{item.name}</span>
370
+ )}
371
+ </Fragment>
372
+ );
373
+ })}
374
+ </div>
375
+ );
376
+ }