@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,258 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Check, Clipboard } from '@icons';
|
|
3
|
+
import {
|
|
4
|
+
type ComponentProps,
|
|
5
|
+
createContext,
|
|
6
|
+
type HTMLAttributes,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
type RefObject,
|
|
9
|
+
use,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
} from 'react';
|
|
13
|
+
import { cn } from '@/utils/cn';
|
|
14
|
+
import { useCopyButton } from '@/utils/use-copy-button';
|
|
15
|
+
import { buttonVariants } from '@/components/ui/button';
|
|
16
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
17
|
+
import { mergeRefs } from '@/utils/merge-refs';
|
|
18
|
+
|
|
19
|
+
export interface CodeBlockProps extends ComponentProps<'figure'> {
|
|
20
|
+
/**
|
|
21
|
+
* Icon of code block
|
|
22
|
+
*
|
|
23
|
+
* When passed as a string, it assumes the value is the HTML of icon
|
|
24
|
+
*/
|
|
25
|
+
icon?: ReactNode;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Allow to copy code with copy button
|
|
29
|
+
*
|
|
30
|
+
* @defaultValue true
|
|
31
|
+
*/
|
|
32
|
+
allowCopy?: boolean;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Keep original background color generated by Shiki or Rehype Code
|
|
36
|
+
*
|
|
37
|
+
* @defaultValue false
|
|
38
|
+
*/
|
|
39
|
+
keepBackground?: boolean;
|
|
40
|
+
|
|
41
|
+
viewportProps?: HTMLAttributes<HTMLElement>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* show line numbers
|
|
45
|
+
*/
|
|
46
|
+
'data-line-numbers'?: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @defaultValue 1
|
|
50
|
+
*/
|
|
51
|
+
'data-line-numbers-start'?: number;
|
|
52
|
+
|
|
53
|
+
Actions?: (props: { className?: string; children?: ReactNode }) => ReactNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TabsContext = createContext<{
|
|
57
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
58
|
+
nested: boolean;
|
|
59
|
+
} | null>(null);
|
|
60
|
+
|
|
61
|
+
export function Pre(props: ComponentProps<'pre'>) {
|
|
62
|
+
return (
|
|
63
|
+
<pre
|
|
64
|
+
{...props}
|
|
65
|
+
className={cn('min-w-full w-max *:flex *:flex-col', props.className)}
|
|
66
|
+
>
|
|
67
|
+
{props.children}
|
|
68
|
+
</pre>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function CodeBlock({
|
|
73
|
+
ref,
|
|
74
|
+
title,
|
|
75
|
+
allowCopy = true,
|
|
76
|
+
keepBackground = false,
|
|
77
|
+
icon,
|
|
78
|
+
viewportProps = {},
|
|
79
|
+
children,
|
|
80
|
+
Actions = (props) => (
|
|
81
|
+
<div {...props} className={cn('empty:hidden', props.className)} />
|
|
82
|
+
),
|
|
83
|
+
...props
|
|
84
|
+
}: CodeBlockProps) {
|
|
85
|
+
const inTab = use(TabsContext) !== null;
|
|
86
|
+
const areaRef = useRef<HTMLDivElement>(null);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<figure
|
|
90
|
+
ref={ref}
|
|
91
|
+
dir="ltr"
|
|
92
|
+
{...props}
|
|
93
|
+
tabIndex={-1}
|
|
94
|
+
className={cn(
|
|
95
|
+
inTab
|
|
96
|
+
? 'bg-fd-secondary -mx-px -mb-px last:rounded-b-xl'
|
|
97
|
+
: 'my-4 bg-fd-card rounded-xl',
|
|
98
|
+
keepBackground && 'bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)',
|
|
99
|
+
|
|
100
|
+
'shiki relative border shadow-sm not-prose overflow-hidden text-sm',
|
|
101
|
+
props.className,
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
104
|
+
{title ? (
|
|
105
|
+
<div className="flex text-fd-muted-foreground items-center gap-2 h-9.5 border-b px-4">
|
|
106
|
+
{typeof icon === 'string' ? (
|
|
107
|
+
<div
|
|
108
|
+
className="[&_svg]:size-3.5"
|
|
109
|
+
dangerouslySetInnerHTML={{
|
|
110
|
+
__html: icon,
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
) : (
|
|
114
|
+
icon
|
|
115
|
+
)}
|
|
116
|
+
<figcaption className="flex-1 truncate">{title}</figcaption>
|
|
117
|
+
{Actions({
|
|
118
|
+
className: '-me-2',
|
|
119
|
+
children: allowCopy && <CopyButton containerRef={areaRef} />,
|
|
120
|
+
})}
|
|
121
|
+
</div>
|
|
122
|
+
) : (
|
|
123
|
+
Actions({
|
|
124
|
+
className:
|
|
125
|
+
'absolute top-2 right-2 z-2 backdrop-blur-lg rounded-lg text-fd-muted-foreground',
|
|
126
|
+
children: allowCopy && <CopyButton containerRef={areaRef} />,
|
|
127
|
+
})
|
|
128
|
+
)}
|
|
129
|
+
<div
|
|
130
|
+
ref={areaRef}
|
|
131
|
+
{...viewportProps}
|
|
132
|
+
role="region"
|
|
133
|
+
tabIndex={0}
|
|
134
|
+
className={cn(
|
|
135
|
+
'text-[0.8125rem] py-3.5 overflow-auto max-h-[600px] fd-scroll-container focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-fd-ring',
|
|
136
|
+
viewportProps.className,
|
|
137
|
+
)}
|
|
138
|
+
style={
|
|
139
|
+
{
|
|
140
|
+
// space for toolbar
|
|
141
|
+
'--padding-right': !title ? 'calc(var(--spacing) * 8)' : undefined,
|
|
142
|
+
counterSet: props['data-line-numbers']
|
|
143
|
+
? `line ${Number(props['data-line-numbers-start'] ?? 1) - 1}`
|
|
144
|
+
: undefined,
|
|
145
|
+
...viewportProps.style,
|
|
146
|
+
} as object
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
{children}
|
|
150
|
+
</div>
|
|
151
|
+
</figure>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function CopyButton({
|
|
156
|
+
className,
|
|
157
|
+
containerRef,
|
|
158
|
+
...props
|
|
159
|
+
}: ComponentProps<'button'> & {
|
|
160
|
+
containerRef: RefObject<HTMLElement | null>;
|
|
161
|
+
}) {
|
|
162
|
+
const [checked, onClick] = useCopyButton(() => {
|
|
163
|
+
const pre = containerRef.current?.getElementsByTagName('pre').item(0);
|
|
164
|
+
if (!pre) return;
|
|
165
|
+
|
|
166
|
+
const clone = pre.cloneNode(true) as HTMLElement;
|
|
167
|
+
clone.querySelectorAll('.nd-copy-ignore').forEach((node) => {
|
|
168
|
+
node.replaceWith('\n');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
void navigator.clipboard.writeText(clone.textContent ?? '');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
data-checked={checked || undefined}
|
|
178
|
+
className={cn(
|
|
179
|
+
buttonVariants({
|
|
180
|
+
className:
|
|
181
|
+
'hover:text-fd-accent-foreground data-checked:text-fd-accent-foreground',
|
|
182
|
+
size: 'icon-xs',
|
|
183
|
+
}),
|
|
184
|
+
className,
|
|
185
|
+
)}
|
|
186
|
+
aria-label={checked ? 'Copied Text' : 'Copy Text'}
|
|
187
|
+
onClick={onClick}
|
|
188
|
+
{...props}
|
|
189
|
+
>
|
|
190
|
+
{checked ? <Check /> : <Clipboard />}
|
|
191
|
+
</button>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function CodeBlockTabs({ ref, ...props }: ComponentProps<typeof Tabs>) {
|
|
196
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
197
|
+
const nested = use(TabsContext) !== null;
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<Tabs
|
|
201
|
+
ref={mergeRefs(containerRef, ref)}
|
|
202
|
+
{...props}
|
|
203
|
+
className={cn(
|
|
204
|
+
'bg-fd-card rounded-xl border',
|
|
205
|
+
!nested && 'my-4',
|
|
206
|
+
props.className,
|
|
207
|
+
)}
|
|
208
|
+
>
|
|
209
|
+
<TabsContext
|
|
210
|
+
value={useMemo(
|
|
211
|
+
() => ({
|
|
212
|
+
containerRef,
|
|
213
|
+
nested,
|
|
214
|
+
}),
|
|
215
|
+
[nested],
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
{props.children}
|
|
219
|
+
</TabsContext>
|
|
220
|
+
</Tabs>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function CodeBlockTabsList(props: ComponentProps<typeof TabsList>) {
|
|
225
|
+
return (
|
|
226
|
+
<TabsList
|
|
227
|
+
{...props}
|
|
228
|
+
className={cn(
|
|
229
|
+
'flex flex-row px-2 overflow-x-auto text-fd-muted-foreground',
|
|
230
|
+
props.className,
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
{props.children}
|
|
234
|
+
</TabsList>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function CodeBlockTabsTrigger({
|
|
239
|
+
children,
|
|
240
|
+
...props
|
|
241
|
+
}: ComponentProps<typeof TabsTrigger>) {
|
|
242
|
+
return (
|
|
243
|
+
<TabsTrigger
|
|
244
|
+
{...props}
|
|
245
|
+
className={cn(
|
|
246
|
+
'relative group inline-flex text-sm font-medium text-nowrap items-center transition-colors gap-2 px-2 py-1.5 hover:text-fd-accent-foreground data-[state=active]:text-fd-primary [&_svg]:size-3.5',
|
|
247
|
+
props.className,
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
<div className="absolute inset-x-2 bottom-0 h-px group-data-[state=active]:bg-fd-primary" />
|
|
251
|
+
{children}
|
|
252
|
+
</TabsTrigger>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function CodeBlockTab(props: ComponentProps<typeof TabsContent>) {
|
|
257
|
+
return <TabsContent {...props} />;
|
|
258
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AlgoliaOptions,
|
|
5
|
+
useDocsSearch,
|
|
6
|
+
} from '@hanzo/docs-core/search/client';
|
|
7
|
+
import { type ReactNode, useMemo, useState } from 'react';
|
|
8
|
+
import { useOnChange } from '@hanzo/docs-core/utils/use-on-change';
|
|
9
|
+
import {
|
|
10
|
+
SearchDialog,
|
|
11
|
+
SearchDialogClose,
|
|
12
|
+
SearchDialogContent,
|
|
13
|
+
SearchDialogFooter,
|
|
14
|
+
SearchDialogHeader,
|
|
15
|
+
SearchDialogIcon,
|
|
16
|
+
SearchDialogInput,
|
|
17
|
+
SearchDialogList,
|
|
18
|
+
SearchDialogOverlay,
|
|
19
|
+
type SharedProps,
|
|
20
|
+
TagsList,
|
|
21
|
+
TagsListItem,
|
|
22
|
+
} from './search';
|
|
23
|
+
import type { SortedResult } from '@hanzo/docs-core/search';
|
|
24
|
+
import type { SearchLink, TagItem } from '@/contexts/search';
|
|
25
|
+
import { useI18n } from '@/contexts/i18n';
|
|
26
|
+
|
|
27
|
+
export interface AlgoliaSearchDialogProps extends SharedProps {
|
|
28
|
+
searchOptions: AlgoliaOptions;
|
|
29
|
+
links?: SearchLink[];
|
|
30
|
+
|
|
31
|
+
footer?: ReactNode;
|
|
32
|
+
|
|
33
|
+
defaultTag?: string;
|
|
34
|
+
tags?: TagItem[];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Add the "Powered by Algolia" label, this is useful for free tier users
|
|
38
|
+
*
|
|
39
|
+
* @defaultValue false
|
|
40
|
+
*/
|
|
41
|
+
showAlgolia?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Allow to clear tag filters
|
|
45
|
+
*
|
|
46
|
+
* @defaultValue false
|
|
47
|
+
*/
|
|
48
|
+
allowClear?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function AlgoliaSearchDialog({
|
|
52
|
+
searchOptions,
|
|
53
|
+
tags = [],
|
|
54
|
+
defaultTag,
|
|
55
|
+
showAlgolia = false,
|
|
56
|
+
allowClear = false,
|
|
57
|
+
links = [],
|
|
58
|
+
footer,
|
|
59
|
+
...props
|
|
60
|
+
}: AlgoliaSearchDialogProps) {
|
|
61
|
+
const [tag, setTag] = useState(defaultTag);
|
|
62
|
+
const { locale } = useI18n();
|
|
63
|
+
const { search, setSearch, query } = useDocsSearch({
|
|
64
|
+
type: 'algolia',
|
|
65
|
+
tag,
|
|
66
|
+
locale,
|
|
67
|
+
...searchOptions,
|
|
68
|
+
});
|
|
69
|
+
const defaultItems = useMemo<SortedResult[] | null>(() => {
|
|
70
|
+
if (links.length === 0) return null;
|
|
71
|
+
return links.map(([name, link]) => ({
|
|
72
|
+
type: 'page',
|
|
73
|
+
id: name,
|
|
74
|
+
content: name,
|
|
75
|
+
url: link,
|
|
76
|
+
}));
|
|
77
|
+
}, [links]);
|
|
78
|
+
|
|
79
|
+
useOnChange(defaultTag, (v) => {
|
|
80
|
+
setTag(v);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const label = showAlgolia && <AlgoliaTitle />;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<SearchDialog
|
|
87
|
+
search={search}
|
|
88
|
+
onSearchChange={setSearch}
|
|
89
|
+
isLoading={query.isLoading}
|
|
90
|
+
{...props}
|
|
91
|
+
>
|
|
92
|
+
<SearchDialogOverlay />
|
|
93
|
+
<SearchDialogContent>
|
|
94
|
+
<SearchDialogHeader>
|
|
95
|
+
<SearchDialogIcon />
|
|
96
|
+
<SearchDialogInput />
|
|
97
|
+
<SearchDialogClose />
|
|
98
|
+
</SearchDialogHeader>
|
|
99
|
+
<SearchDialogList
|
|
100
|
+
items={query.data !== 'empty' ? query.data : defaultItems}
|
|
101
|
+
/>
|
|
102
|
+
</SearchDialogContent>
|
|
103
|
+
<SearchDialogFooter>
|
|
104
|
+
{tags.length > 0 ? (
|
|
105
|
+
<TagsList tag={tag} onTagChange={setTag} allowClear={allowClear}>
|
|
106
|
+
{tags.map((tag) => (
|
|
107
|
+
<TagsListItem key={tag.value} value={tag.value}>
|
|
108
|
+
{tag.name}
|
|
109
|
+
</TagsListItem>
|
|
110
|
+
))}
|
|
111
|
+
{label}
|
|
112
|
+
</TagsList>
|
|
113
|
+
) : (
|
|
114
|
+
label
|
|
115
|
+
)}
|
|
116
|
+
{footer}
|
|
117
|
+
</SearchDialogFooter>
|
|
118
|
+
</SearchDialog>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function AlgoliaTitle() {
|
|
123
|
+
return (
|
|
124
|
+
<a
|
|
125
|
+
href="https://algolia.com"
|
|
126
|
+
rel="noreferrer noopener"
|
|
127
|
+
className="ms-auto text-xs text-fd-muted-foreground"
|
|
128
|
+
>
|
|
129
|
+
Search powered by Algolia
|
|
130
|
+
</a>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useDocsSearch } from '@hanzo/docs-core/search/client';
|
|
4
|
+
import { type ReactNode, useMemo, useState } from 'react';
|
|
5
|
+
import { useOnChange } from '@hanzo/docs-core/utils/use-on-change';
|
|
6
|
+
import { useI18n } from '@/contexts/i18n';
|
|
7
|
+
import {
|
|
8
|
+
SearchDialog,
|
|
9
|
+
SearchDialogClose,
|
|
10
|
+
SearchDialogContent,
|
|
11
|
+
SearchDialogFooter,
|
|
12
|
+
SearchDialogHeader,
|
|
13
|
+
SearchDialogIcon,
|
|
14
|
+
SearchDialogInput,
|
|
15
|
+
SearchDialogList,
|
|
16
|
+
SearchDialogOverlay,
|
|
17
|
+
type SharedProps,
|
|
18
|
+
TagsList,
|
|
19
|
+
TagsListItem,
|
|
20
|
+
} from './search';
|
|
21
|
+
import type { SortedResult } from '@hanzo/docs-core/search';
|
|
22
|
+
import type { SearchLink, TagItem } from '@/contexts/search';
|
|
23
|
+
|
|
24
|
+
export interface DefaultSearchDialogProps extends SharedProps {
|
|
25
|
+
links?: SearchLink[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @defaultValue 'fetch'
|
|
29
|
+
*/
|
|
30
|
+
type?: 'fetch' | 'static';
|
|
31
|
+
|
|
32
|
+
defaultTag?: string;
|
|
33
|
+
tags?: TagItem[];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Search API URL
|
|
37
|
+
*/
|
|
38
|
+
api?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The debounced delay for performing a search.
|
|
42
|
+
*/
|
|
43
|
+
delayMs?: number;
|
|
44
|
+
|
|
45
|
+
footer?: ReactNode;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Allow to clear tag filters
|
|
49
|
+
*
|
|
50
|
+
* @defaultValue false
|
|
51
|
+
*/
|
|
52
|
+
allowClear?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function DefaultSearchDialog({
|
|
56
|
+
defaultTag,
|
|
57
|
+
tags = [],
|
|
58
|
+
api,
|
|
59
|
+
delayMs,
|
|
60
|
+
type = 'fetch',
|
|
61
|
+
allowClear = false,
|
|
62
|
+
links = [],
|
|
63
|
+
footer,
|
|
64
|
+
...props
|
|
65
|
+
}: DefaultSearchDialogProps) {
|
|
66
|
+
const { locale } = useI18n();
|
|
67
|
+
const [tag, setTag] = useState(defaultTag);
|
|
68
|
+
const { search, setSearch, query } = useDocsSearch(
|
|
69
|
+
type === 'fetch'
|
|
70
|
+
? {
|
|
71
|
+
type: 'fetch',
|
|
72
|
+
api,
|
|
73
|
+
locale,
|
|
74
|
+
tag,
|
|
75
|
+
delayMs,
|
|
76
|
+
}
|
|
77
|
+
: {
|
|
78
|
+
type: 'static',
|
|
79
|
+
from: api,
|
|
80
|
+
locale,
|
|
81
|
+
tag,
|
|
82
|
+
delayMs,
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
const defaultItems = useMemo<SortedResult[] | null>(() => {
|
|
86
|
+
if (links.length === 0) return null;
|
|
87
|
+
return links.map(([name, link]) => ({
|
|
88
|
+
type: 'page',
|
|
89
|
+
id: name,
|
|
90
|
+
content: name,
|
|
91
|
+
url: link,
|
|
92
|
+
}));
|
|
93
|
+
}, [links]);
|
|
94
|
+
|
|
95
|
+
useOnChange(defaultTag, (v) => {
|
|
96
|
+
setTag(v);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<SearchDialog
|
|
101
|
+
search={search}
|
|
102
|
+
onSearchChange={setSearch}
|
|
103
|
+
isLoading={query.isLoading}
|
|
104
|
+
{...props}
|
|
105
|
+
>
|
|
106
|
+
<SearchDialogOverlay />
|
|
107
|
+
<SearchDialogContent>
|
|
108
|
+
<SearchDialogHeader>
|
|
109
|
+
<SearchDialogIcon />
|
|
110
|
+
<SearchDialogInput />
|
|
111
|
+
<SearchDialogClose />
|
|
112
|
+
</SearchDialogHeader>
|
|
113
|
+
<SearchDialogList
|
|
114
|
+
items={query.data !== 'empty' ? query.data : defaultItems}
|
|
115
|
+
/>
|
|
116
|
+
</SearchDialogContent>
|
|
117
|
+
<SearchDialogFooter>
|
|
118
|
+
{tags.length > 0 && (
|
|
119
|
+
<TagsList tag={tag} onTagChange={setTag} allowClear={allowClear}>
|
|
120
|
+
{tags.map((tag) => (
|
|
121
|
+
<TagsListItem key={tag.value} value={tag.value}>
|
|
122
|
+
{tag.name}
|
|
123
|
+
</TagsListItem>
|
|
124
|
+
))}
|
|
125
|
+
</TagsList>
|
|
126
|
+
)}
|
|
127
|
+
{footer}
|
|
128
|
+
</SearchDialogFooter>
|
|
129
|
+
</SearchDialog>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type OramaCloudOptions,
|
|
5
|
+
useDocsSearch,
|
|
6
|
+
} from '@hanzo/docs-core/search/client';
|
|
7
|
+
import { type ReactNode, useMemo, useState } from 'react';
|
|
8
|
+
import { useOnChange } from '@hanzo/docs-core/utils/use-on-change';
|
|
9
|
+
import {
|
|
10
|
+
SearchDialog,
|
|
11
|
+
SearchDialogClose,
|
|
12
|
+
SearchDialogContent,
|
|
13
|
+
SearchDialogFooter,
|
|
14
|
+
SearchDialogHeader,
|
|
15
|
+
SearchDialogIcon,
|
|
16
|
+
SearchDialogInput,
|
|
17
|
+
SearchDialogList,
|
|
18
|
+
SearchDialogOverlay,
|
|
19
|
+
type SharedProps,
|
|
20
|
+
TagsList,
|
|
21
|
+
TagsListItem,
|
|
22
|
+
} from './search';
|
|
23
|
+
import type { SortedResult } from '@hanzo/docs-core/search';
|
|
24
|
+
import type { SearchLink, TagItem } from '@/contexts/search';
|
|
25
|
+
import { useI18n } from '@/contexts/i18n';
|
|
26
|
+
|
|
27
|
+
export interface OramaSearchDialogProps extends SharedProps {
|
|
28
|
+
links?: SearchLink[];
|
|
29
|
+
client: OramaCloudOptions['client'];
|
|
30
|
+
searchOptions?: OramaCloudOptions['params'];
|
|
31
|
+
index?: OramaCloudOptions['index'];
|
|
32
|
+
|
|
33
|
+
footer?: ReactNode;
|
|
34
|
+
|
|
35
|
+
defaultTag?: string;
|
|
36
|
+
tags?: TagItem[];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Add the "Powered by Orama" label
|
|
40
|
+
*
|
|
41
|
+
* @defaultValue true
|
|
42
|
+
*/
|
|
43
|
+
showOrama?: boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Allow to clear tag filters
|
|
47
|
+
*
|
|
48
|
+
* @defaultValue false
|
|
49
|
+
*/
|
|
50
|
+
allowClear?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Orama Cloud integration
|
|
55
|
+
*/
|
|
56
|
+
export default function OramaSearchDialog({
|
|
57
|
+
client,
|
|
58
|
+
searchOptions,
|
|
59
|
+
tags = [],
|
|
60
|
+
defaultTag,
|
|
61
|
+
showOrama = true,
|
|
62
|
+
allowClear = false,
|
|
63
|
+
index,
|
|
64
|
+
footer,
|
|
65
|
+
links = [],
|
|
66
|
+
...props
|
|
67
|
+
}: OramaSearchDialogProps) {
|
|
68
|
+
const { locale } = useI18n();
|
|
69
|
+
const [tag, setTag] = useState(defaultTag);
|
|
70
|
+
const { search, setSearch, query } = useDocsSearch({
|
|
71
|
+
type: 'orama-cloud',
|
|
72
|
+
client,
|
|
73
|
+
index,
|
|
74
|
+
params: searchOptions,
|
|
75
|
+
locale,
|
|
76
|
+
tag,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const defaultItems = useMemo<SortedResult[] | null>(() => {
|
|
80
|
+
if (links.length === 0) return null;
|
|
81
|
+
|
|
82
|
+
return links.map(([name, link]) => ({
|
|
83
|
+
type: 'page',
|
|
84
|
+
id: name,
|
|
85
|
+
content: name,
|
|
86
|
+
url: link,
|
|
87
|
+
}));
|
|
88
|
+
}, [links]);
|
|
89
|
+
|
|
90
|
+
useOnChange(defaultTag, (v) => {
|
|
91
|
+
setTag(v);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const label = showOrama && <Label />;
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<SearchDialog
|
|
98
|
+
search={search}
|
|
99
|
+
onSearchChange={setSearch}
|
|
100
|
+
isLoading={query.isLoading}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
<SearchDialogOverlay />
|
|
104
|
+
<SearchDialogContent>
|
|
105
|
+
<SearchDialogHeader>
|
|
106
|
+
<SearchDialogIcon />
|
|
107
|
+
<SearchDialogInput />
|
|
108
|
+
<SearchDialogClose />
|
|
109
|
+
</SearchDialogHeader>
|
|
110
|
+
<SearchDialogList
|
|
111
|
+
items={query.data !== 'empty' ? query.data : defaultItems}
|
|
112
|
+
/>
|
|
113
|
+
<SearchDialogFooter>
|
|
114
|
+
{tags.length > 0 ? (
|
|
115
|
+
<TagsList tag={tag} onTagChange={setTag} allowClear={allowClear}>
|
|
116
|
+
{tags.map((tag) => (
|
|
117
|
+
<TagsListItem key={tag.value} value={tag.value}>
|
|
118
|
+
{tag.name}
|
|
119
|
+
</TagsListItem>
|
|
120
|
+
))}
|
|
121
|
+
{label}
|
|
122
|
+
</TagsList>
|
|
123
|
+
) : (
|
|
124
|
+
label
|
|
125
|
+
)}
|
|
126
|
+
{footer}
|
|
127
|
+
</SearchDialogFooter>
|
|
128
|
+
</SearchDialogContent>
|
|
129
|
+
</SearchDialog>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Label() {
|
|
134
|
+
return (
|
|
135
|
+
<a
|
|
136
|
+
href="https://orama.com"
|
|
137
|
+
rel="noreferrer noopener"
|
|
138
|
+
className="ms-auto text-xs text-fd-muted-foreground"
|
|
139
|
+
>
|
|
140
|
+
Search powered by Orama
|
|
141
|
+
</a>
|
|
142
|
+
);
|
|
143
|
+
}
|