@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.
- package/content/index.ts +26 -0
- package/dist/util/index.js +6 -0
- package/dist/util/index.mjs +6 -1
- package/docs/_registry/index.ts +426 -0
- package/docs/_registry/layout/docs-min.tsx +197 -0
- package/docs/_registry/layout/page-min.tsx +128 -0
- package/docs/components/accordion.tsx +118 -0
- package/docs/components/banner.tsx +144 -0
- package/docs/components/callout.tsx +112 -0
- package/docs/components/card.tsx +52 -0
- package/docs/components/codeblock.tsx +258 -0
- package/docs/components/dialog/search-algolia.tsx +132 -0
- package/docs/components/dialog/search-default.tsx +131 -0
- package/docs/components/dialog/search-orama.tsx +143 -0
- package/docs/components/dialog/search.tsx +529 -0
- package/docs/components/dynamic-codeblock.tsx +129 -0
- package/docs/components/files.tsx +81 -0
- package/docs/components/github-info.tsx +107 -0
- package/docs/components/heading.tsx +33 -0
- package/docs/components/image-zoom.css +77 -0
- package/docs/components/image-zoom.tsx +58 -0
- package/docs/components/index.ts +7 -0
- package/docs/components/inline-toc.tsx +48 -0
- package/docs/components/sidebar/base.tsx +451 -0
- package/docs/components/sidebar/link-item.tsx +65 -0
- package/docs/components/sidebar/page-tree.tsx +113 -0
- package/docs/components/sidebar/tabs/dropdown.tsx +109 -0
- package/docs/components/sidebar/tabs/index.tsx +89 -0
- package/docs/components/steps.tsx +9 -0
- package/docs/components/tabs.tsx +203 -0
- package/docs/components/toc/clerk.tsx +173 -0
- package/docs/components/toc/default.tsx +57 -0
- package/docs/components/toc/index.tsx +136 -0
- package/docs/components/type-table.tsx +174 -0
- package/docs/components/ui/accordion.tsx +88 -0
- package/docs/components/ui/button.tsx +28 -0
- package/docs/components/ui/collapsible.tsx +42 -0
- package/docs/components/ui/navigation-menu.tsx +83 -0
- package/docs/components/ui/popover.tsx +32 -0
- package/docs/components/ui/scroll-area.tsx +59 -0
- package/docs/components/ui/tabs.tsx +145 -0
- package/docs/contexts/i18n.tsx +56 -0
- package/docs/contexts/search.tsx +165 -0
- package/docs/contexts/tree.tsx +65 -0
- package/docs/css/black.css +39 -0
- package/docs/css/catppuccin.css +49 -0
- package/docs/css/colors/index.css +51 -0
- package/docs/css/dusk.css +47 -0
- package/docs/css/layouts/docs.css +1 -0
- package/docs/css/layouts/home.css +1 -0
- package/docs/css/layouts/notebook.css +1 -0
- package/docs/css/neutral.css +7 -0
- package/docs/css/ocean.css +48 -0
- package/docs/css/preset.css +305 -0
- package/docs/css/purple.css +39 -0
- package/docs/css/shadcn.css +36 -0
- package/docs/css/shiki.css +90 -0
- package/docs/css/solar.css +75 -0
- package/docs/css/style.css +9 -0
- package/docs/css/vitepress.css +77 -0
- package/docs/i18n.tsx +30 -0
- package/docs/icons.tsx +354 -0
- package/docs/layouts/docs/client.tsx +129 -0
- package/docs/layouts/docs/index.tsx +321 -0
- package/docs/layouts/docs/page/client.tsx +376 -0
- package/docs/layouts/docs/page/index.tsx +251 -0
- package/docs/layouts/docs/sidebar.tsx +265 -0
- package/docs/layouts/home/client.tsx +375 -0
- package/docs/layouts/home/index.tsx +51 -0
- package/docs/layouts/home/navbar.tsx +55 -0
- package/docs/layouts/notebook/client.tsx +281 -0
- package/docs/layouts/notebook/index.tsx +461 -0
- package/docs/layouts/notebook/page/client.tsx +375 -0
- package/docs/layouts/notebook/page/index.tsx +251 -0
- package/docs/layouts/notebook/sidebar.tsx +248 -0
- package/docs/layouts/shared/index.tsx +89 -0
- package/docs/layouts/shared/language-toggle.tsx +66 -0
- package/docs/layouts/shared/link-item.tsx +119 -0
- package/docs/layouts/shared/search-toggle.tsx +78 -0
- package/docs/layouts/shared/theme-toggle.tsx +86 -0
- package/docs/mdx.server.tsx +37 -0
- package/docs/mdx.tsx +97 -0
- package/docs/og.tsx +101 -0
- package/docs/page.tsx +85 -0
- package/docs/provider/base.tsx +173 -0
- package/docs/provider/next.tsx +23 -0
- package/docs/provider/react-router.tsx +23 -0
- package/docs/provider/tanstack.tsx +23 -0
- package/docs/provider/waku.tsx +23 -0
- package/docs/source.ts +3 -0
- package/docs/theme/typography/LICENSE +21 -0
- package/docs/theme/typography/index.ts +201 -0
- package/docs/theme/typography/styles.ts +449 -0
- package/docs/utils/cn.ts +1 -0
- package/docs/utils/is-active.ts +23 -0
- package/docs/utils/merge-refs.ts +15 -0
- package/docs/utils/use-copy-button.ts +39 -0
- package/docs/utils/use-footer-items.ts +27 -0
- package/docs/utils/use-is-scroll-top.ts +21 -0
- 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
|
+
}
|