@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,248 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import * as Base from '@/components/sidebar/base';
|
|
3
|
+
import { cn } from '@/utils/cn';
|
|
4
|
+
import { type ComponentProps, use, useRef } from 'react';
|
|
5
|
+
import { cva } from 'class-variance-authority';
|
|
6
|
+
import { LayoutContext } from './client';
|
|
7
|
+
import { createPageTreeRenderer } from '@/components/sidebar/page-tree';
|
|
8
|
+
import { createLinkItemRenderer } from '@/components/sidebar/link-item';
|
|
9
|
+
import { mergeRefs } from '@/utils/merge-refs';
|
|
10
|
+
|
|
11
|
+
const itemVariants = cva(
|
|
12
|
+
'relative flex flex-row items-center gap-2 rounded-lg p-2 text-start text-fd-muted-foreground wrap-anywhere [&_svg]:size-4 [&_svg]:shrink-0',
|
|
13
|
+
{
|
|
14
|
+
variants: {
|
|
15
|
+
variant: {
|
|
16
|
+
link: 'transition-colors hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none data-[active=true]:bg-fd-primary/10 data-[active=true]:text-fd-primary data-[active=true]:hover:transition-colors',
|
|
17
|
+
button:
|
|
18
|
+
'transition-colors hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none',
|
|
19
|
+
},
|
|
20
|
+
highlight: {
|
|
21
|
+
true: "data-[active=true]:before:content-[''] data-[active=true]:before:bg-fd-primary data-[active=true]:before:absolute data-[active=true]:before:w-px data-[active=true]:before:inset-y-2.5 data-[active=true]:before:start-2.5",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function getItemOffset(depth: number) {
|
|
28
|
+
return `calc(${2 + 3 * depth} * var(--spacing))`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const {
|
|
32
|
+
SidebarProvider: Sidebar,
|
|
33
|
+
SidebarFolder,
|
|
34
|
+
SidebarCollapseTrigger,
|
|
35
|
+
SidebarViewport,
|
|
36
|
+
SidebarTrigger,
|
|
37
|
+
} = Base;
|
|
38
|
+
|
|
39
|
+
export function SidebarContent({
|
|
40
|
+
ref: refProp,
|
|
41
|
+
className,
|
|
42
|
+
children,
|
|
43
|
+
...props
|
|
44
|
+
}: ComponentProps<'aside'>) {
|
|
45
|
+
const { navMode } = use(LayoutContext)!;
|
|
46
|
+
const ref = useRef<HTMLElement>(null);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Base.SidebarContent>
|
|
50
|
+
{({ collapsed, hovered, ref: asideRef, ...rest }) => (
|
|
51
|
+
<div
|
|
52
|
+
data-sidebar-placeholder=""
|
|
53
|
+
className={cn(
|
|
54
|
+
'sticky z-20 [grid-area:sidebar] pointer-events-none *:pointer-events-auto md:layout:[--fd-sidebar-width:268px] max-md:hidden',
|
|
55
|
+
navMode === 'auto'
|
|
56
|
+
? 'top-(--fd-docs-row-1) h-[calc(var(--fd-docs-height)-var(--fd-docs-row-1))]'
|
|
57
|
+
: 'top-(--fd-docs-row-2) h-[calc(var(--fd-docs-height)-var(--fd-docs-row-2))]',
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
{collapsed && (
|
|
61
|
+
<div className="absolute start-0 inset-y-0 w-4" {...rest} />
|
|
62
|
+
)}
|
|
63
|
+
<aside
|
|
64
|
+
id="nd-sidebar"
|
|
65
|
+
ref={mergeRefs(ref, refProp, asideRef)}
|
|
66
|
+
data-collapsed={collapsed}
|
|
67
|
+
data-hovered={collapsed && hovered}
|
|
68
|
+
className={cn(
|
|
69
|
+
'absolute flex flex-col w-full start-0 inset-y-0 items-end text-sm duration-250 *:w-(--fd-sidebar-width)',
|
|
70
|
+
navMode === 'auto' && 'bg-fd-card border-e',
|
|
71
|
+
collapsed && [
|
|
72
|
+
'inset-y-2 rounded-xl bg-fd-card transition-transform border w-(--fd-sidebar-width)',
|
|
73
|
+
hovered
|
|
74
|
+
? 'shadow-lg translate-x-2 rtl:-translate-x-2'
|
|
75
|
+
: '-translate-x-(--fd-sidebar-width) rtl:translate-x-full',
|
|
76
|
+
],
|
|
77
|
+
ref.current &&
|
|
78
|
+
(ref.current.getAttribute('data-collapsed') === 'true') !==
|
|
79
|
+
collapsed &&
|
|
80
|
+
'transition-[width,inset-block,translate,background-color]',
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
{...rest}
|
|
85
|
+
>
|
|
86
|
+
{children}
|
|
87
|
+
</aside>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</Base.SidebarContent>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function SidebarDrawer({
|
|
95
|
+
children,
|
|
96
|
+
className,
|
|
97
|
+
...props
|
|
98
|
+
}: ComponentProps<typeof Base.SidebarDrawerContent>) {
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
<Base.SidebarDrawerOverlay className="fixed z-40 inset-0 backdrop-blur-xs data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out" />
|
|
102
|
+
<Base.SidebarDrawerContent
|
|
103
|
+
className={cn(
|
|
104
|
+
'fixed text-[0.9375rem] flex flex-col shadow-lg border-s end-0 inset-y-0 w-[85%] max-w-[380px] z-40 bg-fd-background data-[state=open]:animate-fd-sidebar-in data-[state=closed]:animate-fd-sidebar-out',
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
{children}
|
|
110
|
+
</Base.SidebarDrawerContent>
|
|
111
|
+
</>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function SidebarSeparator({
|
|
116
|
+
className,
|
|
117
|
+
style,
|
|
118
|
+
children,
|
|
119
|
+
...props
|
|
120
|
+
}: ComponentProps<'p'>) {
|
|
121
|
+
const depth = Base.useFolderDepth();
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Base.SidebarSeparator
|
|
125
|
+
className={cn('[&_svg]:size-4 [&_svg]:shrink-0', className)}
|
|
126
|
+
style={{
|
|
127
|
+
paddingInlineStart: getItemOffset(depth),
|
|
128
|
+
...style,
|
|
129
|
+
}}
|
|
130
|
+
{...props}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</Base.SidebarSeparator>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function SidebarItem({
|
|
138
|
+
className,
|
|
139
|
+
style,
|
|
140
|
+
children,
|
|
141
|
+
...props
|
|
142
|
+
}: ComponentProps<typeof Base.SidebarItem>) {
|
|
143
|
+
const depth = Base.useFolderDepth();
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Base.SidebarItem
|
|
147
|
+
className={cn(
|
|
148
|
+
itemVariants({ variant: 'link', highlight: depth >= 1 }),
|
|
149
|
+
className,
|
|
150
|
+
)}
|
|
151
|
+
style={{
|
|
152
|
+
paddingInlineStart: getItemOffset(depth),
|
|
153
|
+
...style,
|
|
154
|
+
}}
|
|
155
|
+
{...props}
|
|
156
|
+
>
|
|
157
|
+
{children}
|
|
158
|
+
</Base.SidebarItem>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function SidebarFolderTrigger({
|
|
163
|
+
className,
|
|
164
|
+
style,
|
|
165
|
+
...props
|
|
166
|
+
}: ComponentProps<typeof Base.SidebarFolderTrigger>) {
|
|
167
|
+
const { depth, collapsible } = Base.useFolder()!;
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Base.SidebarFolderTrigger
|
|
171
|
+
className={cn(
|
|
172
|
+
itemVariants({ variant: collapsible ? 'button' : null }),
|
|
173
|
+
'w-full',
|
|
174
|
+
className,
|
|
175
|
+
)}
|
|
176
|
+
style={{
|
|
177
|
+
paddingInlineStart: getItemOffset(depth - 1),
|
|
178
|
+
...style,
|
|
179
|
+
}}
|
|
180
|
+
{...props}
|
|
181
|
+
>
|
|
182
|
+
{props.children}
|
|
183
|
+
</Base.SidebarFolderTrigger>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function SidebarFolderLink({
|
|
188
|
+
className,
|
|
189
|
+
style,
|
|
190
|
+
...props
|
|
191
|
+
}: ComponentProps<typeof Base.SidebarFolderLink>) {
|
|
192
|
+
const depth = Base.useFolderDepth();
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<Base.SidebarFolderLink
|
|
196
|
+
className={cn(
|
|
197
|
+
itemVariants({ variant: 'link', highlight: depth > 1 }),
|
|
198
|
+
'w-full',
|
|
199
|
+
className,
|
|
200
|
+
)}
|
|
201
|
+
style={{
|
|
202
|
+
paddingInlineStart: getItemOffset(depth - 1),
|
|
203
|
+
...style,
|
|
204
|
+
}}
|
|
205
|
+
{...props}
|
|
206
|
+
>
|
|
207
|
+
{props.children}
|
|
208
|
+
</Base.SidebarFolderLink>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function SidebarFolderContent({
|
|
213
|
+
className,
|
|
214
|
+
children,
|
|
215
|
+
...props
|
|
216
|
+
}: ComponentProps<typeof Base.SidebarFolderContent>) {
|
|
217
|
+
const depth = Base.useFolderDepth();
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<Base.SidebarFolderContent
|
|
221
|
+
className={cn(
|
|
222
|
+
'relative',
|
|
223
|
+
depth === 1 &&
|
|
224
|
+
"before:content-[''] before:absolute before:w-px before:inset-y-1 before:bg-fd-border before:start-2.5",
|
|
225
|
+
className,
|
|
226
|
+
)}
|
|
227
|
+
{...props}
|
|
228
|
+
>
|
|
229
|
+
{children}
|
|
230
|
+
</Base.SidebarFolderContent>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
export const SidebarPageTree = createPageTreeRenderer({
|
|
234
|
+
SidebarFolder,
|
|
235
|
+
SidebarFolderContent,
|
|
236
|
+
SidebarFolderLink,
|
|
237
|
+
SidebarFolderTrigger,
|
|
238
|
+
SidebarItem,
|
|
239
|
+
SidebarSeparator,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
export const SidebarLinkItem = createLinkItemRenderer({
|
|
243
|
+
SidebarFolder,
|
|
244
|
+
SidebarFolderContent,
|
|
245
|
+
SidebarFolderLink,
|
|
246
|
+
SidebarFolderTrigger,
|
|
247
|
+
SidebarItem,
|
|
248
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { I18nConfig } from '@hanzo/docs-core/i18n';
|
|
3
|
+
import type { LinkItemType } from './link-item';
|
|
4
|
+
|
|
5
|
+
export interface NavOptions {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
component: ReactNode;
|
|
8
|
+
|
|
9
|
+
title?: ReactNode;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Redirect url of title
|
|
13
|
+
* @defaultValue '/'
|
|
14
|
+
*/
|
|
15
|
+
url?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Use transparent background
|
|
19
|
+
*
|
|
20
|
+
* @defaultValue none
|
|
21
|
+
*/
|
|
22
|
+
transparentMode?: 'always' | 'top' | 'none';
|
|
23
|
+
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BaseLayoutProps {
|
|
28
|
+
themeSwitch?: {
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
component?: ReactNode;
|
|
31
|
+
mode?: 'light-dark' | 'light-dark-system';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
searchToggle?: Partial<{
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
components: Partial<{
|
|
37
|
+
sm: ReactNode;
|
|
38
|
+
lg: ReactNode;
|
|
39
|
+
}>;
|
|
40
|
+
}>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* I18n options
|
|
44
|
+
*
|
|
45
|
+
* @defaultValue false
|
|
46
|
+
*/
|
|
47
|
+
i18n?: boolean | I18nConfig;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* GitHub url
|
|
51
|
+
*/
|
|
52
|
+
githubUrl?: string;
|
|
53
|
+
|
|
54
|
+
links?: LinkItemType[];
|
|
55
|
+
/**
|
|
56
|
+
* Replace or disable navbar
|
|
57
|
+
*/
|
|
58
|
+
nav?: Partial<NavOptions>;
|
|
59
|
+
|
|
60
|
+
children?: ReactNode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get link items with shortcuts
|
|
65
|
+
*/
|
|
66
|
+
export function resolveLinkItems({
|
|
67
|
+
links = [],
|
|
68
|
+
githubUrl,
|
|
69
|
+
}: Pick<BaseLayoutProps, 'links' | 'githubUrl'>): LinkItemType[] {
|
|
70
|
+
const result = [...links];
|
|
71
|
+
|
|
72
|
+
if (githubUrl)
|
|
73
|
+
result.push({
|
|
74
|
+
type: 'icon',
|
|
75
|
+
url: githubUrl,
|
|
76
|
+
text: 'Github',
|
|
77
|
+
label: 'GitHub',
|
|
78
|
+
icon: (
|
|
79
|
+
<svg role="img" viewBox="0 0 24 24" fill="currentColor">
|
|
80
|
+
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
|
81
|
+
</svg>
|
|
82
|
+
),
|
|
83
|
+
external: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type * from './link-item';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { useI18n } from '@/contexts/i18n';
|
|
4
|
+
import {
|
|
5
|
+
Popover,
|
|
6
|
+
PopoverContent,
|
|
7
|
+
PopoverTrigger,
|
|
8
|
+
} from '@/components/ui/popover';
|
|
9
|
+
import { cn } from '@/utils/cn';
|
|
10
|
+
import { buttonVariants } from '@/components/ui/button';
|
|
11
|
+
|
|
12
|
+
export type LanguageSelectProps = ComponentProps<'button'>;
|
|
13
|
+
|
|
14
|
+
export function LanguageToggle(props: LanguageSelectProps): React.ReactElement {
|
|
15
|
+
const context = useI18n();
|
|
16
|
+
if (!context.locales) throw new Error('Missing `<I18nProvider />`');
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Popover>
|
|
20
|
+
<PopoverTrigger
|
|
21
|
+
aria-label={context.text.chooseLanguage}
|
|
22
|
+
{...props}
|
|
23
|
+
className={cn(
|
|
24
|
+
buttonVariants({
|
|
25
|
+
color: 'ghost',
|
|
26
|
+
className: 'gap-1.5 p-1.5',
|
|
27
|
+
}),
|
|
28
|
+
props.className,
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
{props.children}
|
|
32
|
+
</PopoverTrigger>
|
|
33
|
+
<PopoverContent className="flex flex-col overflow-x-hidden p-0">
|
|
34
|
+
<p className="mb-1 p-2 text-xs font-medium text-fd-muted-foreground">
|
|
35
|
+
{context.text.chooseLanguage}
|
|
36
|
+
</p>
|
|
37
|
+
{context.locales.map((item) => (
|
|
38
|
+
<button
|
|
39
|
+
key={item.locale}
|
|
40
|
+
type="button"
|
|
41
|
+
className={cn(
|
|
42
|
+
'p-2 text-start text-sm',
|
|
43
|
+
item.locale === context.locale
|
|
44
|
+
? 'bg-fd-primary/10 font-medium text-fd-primary'
|
|
45
|
+
: 'hover:bg-fd-accent hover:text-fd-accent-foreground',
|
|
46
|
+
)}
|
|
47
|
+
onClick={() => {
|
|
48
|
+
context.onChange?.(item.locale);
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
{item.name}
|
|
52
|
+
</button>
|
|
53
|
+
))}
|
|
54
|
+
</PopoverContent>
|
|
55
|
+
</Popover>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function LanguageToggleText(props: ComponentProps<'span'>) {
|
|
60
|
+
const context = useI18n();
|
|
61
|
+
const text = context.locales?.find(
|
|
62
|
+
(item) => item.locale === context.locale,
|
|
63
|
+
)?.name;
|
|
64
|
+
|
|
65
|
+
return <span {...props}>{text}</span>;
|
|
66
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
3
|
+
import { usePathname } from '@hanzo/docs-core/framework';
|
|
4
|
+
import { isActive } from '@/utils/is-active';
|
|
5
|
+
import Link from '@hanzo/docs-core/link';
|
|
6
|
+
|
|
7
|
+
interface Filterable {
|
|
8
|
+
/**
|
|
9
|
+
* Restrict where the item is displayed
|
|
10
|
+
*
|
|
11
|
+
* @defaultValue 'all'
|
|
12
|
+
*/
|
|
13
|
+
on?: 'menu' | 'nav' | 'all';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface WithHref {
|
|
17
|
+
url: string;
|
|
18
|
+
/**
|
|
19
|
+
* When the item is marked as active
|
|
20
|
+
*
|
|
21
|
+
* @defaultValue 'url'
|
|
22
|
+
*/
|
|
23
|
+
active?: 'url' | 'nested-url' | 'none';
|
|
24
|
+
external?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MainItemType extends WithHref, Filterable {
|
|
28
|
+
type?: 'main';
|
|
29
|
+
icon?: ReactNode;
|
|
30
|
+
text: ReactNode;
|
|
31
|
+
description?: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IconItemType extends WithHref, Filterable {
|
|
35
|
+
type: 'icon';
|
|
36
|
+
/**
|
|
37
|
+
* `aria-label` of icon button
|
|
38
|
+
*/
|
|
39
|
+
label?: string;
|
|
40
|
+
icon: ReactNode;
|
|
41
|
+
text: ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* @defaultValue true
|
|
44
|
+
*/
|
|
45
|
+
secondary?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ButtonItemType extends WithHref, Filterable {
|
|
49
|
+
type: 'button';
|
|
50
|
+
icon?: ReactNode;
|
|
51
|
+
text: ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* @defaultValue false
|
|
54
|
+
*/
|
|
55
|
+
secondary?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface MenuItemType extends Partial<WithHref>, Filterable {
|
|
59
|
+
type: 'menu';
|
|
60
|
+
icon?: ReactNode;
|
|
61
|
+
text: ReactNode;
|
|
62
|
+
|
|
63
|
+
items: (
|
|
64
|
+
| (MainItemType & {
|
|
65
|
+
/**
|
|
66
|
+
* Options when displayed on navigation menu
|
|
67
|
+
*/
|
|
68
|
+
menu?: ComponentProps<'a'> & {
|
|
69
|
+
banner?: ReactNode;
|
|
70
|
+
};
|
|
71
|
+
})
|
|
72
|
+
| CustomItemType
|
|
73
|
+
)[];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @defaultValue false
|
|
77
|
+
*/
|
|
78
|
+
secondary?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CustomItemType extends Filterable {
|
|
82
|
+
type: 'custom';
|
|
83
|
+
/**
|
|
84
|
+
* @defaultValue false
|
|
85
|
+
*/
|
|
86
|
+
secondary?: boolean;
|
|
87
|
+
children: ReactNode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type LinkItemType =
|
|
91
|
+
| MainItemType
|
|
92
|
+
| IconItemType
|
|
93
|
+
| ButtonItemType
|
|
94
|
+
| MenuItemType
|
|
95
|
+
| CustomItemType;
|
|
96
|
+
|
|
97
|
+
export function LinkItem({
|
|
98
|
+
ref,
|
|
99
|
+
item,
|
|
100
|
+
...props
|
|
101
|
+
}: Omit<ComponentProps<'a'>, 'href'> & { item: WithHref }) {
|
|
102
|
+
const pathname = usePathname();
|
|
103
|
+
const activeType = item.active ?? 'url';
|
|
104
|
+
const active =
|
|
105
|
+
activeType !== 'none' &&
|
|
106
|
+
isActive(item.url, pathname, activeType === 'nested-url');
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Link
|
|
110
|
+
ref={ref}
|
|
111
|
+
href={item.url}
|
|
112
|
+
external={item.external}
|
|
113
|
+
{...props}
|
|
114
|
+
data-active={active}
|
|
115
|
+
>
|
|
116
|
+
{props.children}
|
|
117
|
+
</Link>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { Search } from '@icons';
|
|
4
|
+
import { useSearchContext } from '@/contexts/search';
|
|
5
|
+
import { useI18n } from '@/contexts/i18n';
|
|
6
|
+
import { cn } from '@/utils/cn';
|
|
7
|
+
import { type ButtonProps, buttonVariants } from '@/components/ui/button';
|
|
8
|
+
|
|
9
|
+
interface SearchToggleProps
|
|
10
|
+
extends Omit<ComponentProps<'button'>, 'color'>, ButtonProps {
|
|
11
|
+
hideIfDisabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function SearchToggle({
|
|
15
|
+
hideIfDisabled,
|
|
16
|
+
size = 'icon-sm',
|
|
17
|
+
color = 'ghost',
|
|
18
|
+
...props
|
|
19
|
+
}: SearchToggleProps) {
|
|
20
|
+
const { setOpenSearch, enabled } = useSearchContext();
|
|
21
|
+
if (hideIfDisabled && !enabled) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
className={cn(
|
|
27
|
+
buttonVariants({
|
|
28
|
+
size,
|
|
29
|
+
color,
|
|
30
|
+
}),
|
|
31
|
+
props.className,
|
|
32
|
+
)}
|
|
33
|
+
data-search=""
|
|
34
|
+
aria-label="Open Search"
|
|
35
|
+
onClick={() => {
|
|
36
|
+
setOpenSearch(true);
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<Search />
|
|
40
|
+
</button>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function LargeSearchToggle({
|
|
45
|
+
hideIfDisabled,
|
|
46
|
+
...props
|
|
47
|
+
}: ComponentProps<'button'> & {
|
|
48
|
+
hideIfDisabled?: boolean;
|
|
49
|
+
}) {
|
|
50
|
+
const { enabled, hotKey, setOpenSearch } = useSearchContext();
|
|
51
|
+
const { text } = useI18n();
|
|
52
|
+
if (hideIfDisabled && !enabled) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
data-search-full=""
|
|
58
|
+
{...props}
|
|
59
|
+
className={cn(
|
|
60
|
+
'inline-flex items-center gap-2 rounded-lg border bg-fd-secondary/50 p-1.5 ps-2 text-sm text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground',
|
|
61
|
+
props.className,
|
|
62
|
+
)}
|
|
63
|
+
onClick={() => {
|
|
64
|
+
setOpenSearch(true);
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
<Search className="size-4" />
|
|
68
|
+
{text.search}
|
|
69
|
+
<div className="ms-auto inline-flex gap-0.5">
|
|
70
|
+
{hotKey.map((k, i) => (
|
|
71
|
+
<kbd key={i} className="rounded-md border bg-fd-background px-1.5">
|
|
72
|
+
{k.display}
|
|
73
|
+
</kbd>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { cva } from 'class-variance-authority';
|
|
3
|
+
import { Airplay, Moon, Sun } from '@icons';
|
|
4
|
+
import { useTheme } from 'next-themes';
|
|
5
|
+
import { ComponentProps, useEffect, useState } from 'react';
|
|
6
|
+
import { cn } from '@/utils/cn';
|
|
7
|
+
|
|
8
|
+
const itemVariants = cva(
|
|
9
|
+
'size-6.5 rounded-full p-1.5 text-fd-muted-foreground',
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
active: {
|
|
13
|
+
true: 'bg-fd-accent text-fd-accent-foreground',
|
|
14
|
+
false: 'text-fd-muted-foreground',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const full = [
|
|
21
|
+
['light', Sun] as const,
|
|
22
|
+
['dark', Moon] as const,
|
|
23
|
+
['system', Airplay] as const,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function ThemeToggle({
|
|
27
|
+
className,
|
|
28
|
+
mode = 'light-dark',
|
|
29
|
+
...props
|
|
30
|
+
}: ComponentProps<'div'> & {
|
|
31
|
+
mode?: 'light-dark' | 'light-dark-system';
|
|
32
|
+
}) {
|
|
33
|
+
const { setTheme, theme, resolvedTheme } = useTheme();
|
|
34
|
+
const [mounted, setMounted] = useState(false);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setMounted(true);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
const container = cn(
|
|
41
|
+
'inline-flex items-center rounded-full border p-1',
|
|
42
|
+
className,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (mode === 'light-dark') {
|
|
46
|
+
const value = mounted ? resolvedTheme : null;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
className={container}
|
|
51
|
+
aria-label={`Toggle Theme`}
|
|
52
|
+
onClick={() => setTheme(value === 'light' ? 'dark' : 'light')}
|
|
53
|
+
data-theme-toggle=""
|
|
54
|
+
>
|
|
55
|
+
{full.map(([key, Icon]) => {
|
|
56
|
+
if (key === 'system') return;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Icon
|
|
60
|
+
key={key}
|
|
61
|
+
fill="currentColor"
|
|
62
|
+
className={cn(itemVariants({ active: value === key }))}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
})}
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const value = mounted ? theme : null;
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className={container} data-theme-toggle="" {...props}>
|
|
74
|
+
{full.map(([key, Icon]) => (
|
|
75
|
+
<button
|
|
76
|
+
key={key}
|
|
77
|
+
aria-label={key}
|
|
78
|
+
className={cn(itemVariants({ active: value === key }))}
|
|
79
|
+
onClick={() => setTheme(key)}
|
|
80
|
+
>
|
|
81
|
+
<Icon className="size-full" fill="currentColor" />
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|