@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,529 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChevronRight, Hash, Search as SearchIcon } from '@icons';
|
|
4
|
+
import {
|
|
5
|
+
type ComponentProps,
|
|
6
|
+
createContext,
|
|
7
|
+
Fragment,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
useCallback,
|
|
10
|
+
useContext,
|
|
11
|
+
useEffect,
|
|
12
|
+
useEffectEvent,
|
|
13
|
+
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
} from 'react';
|
|
17
|
+
import { I18nLabel, useI18n } from '@/contexts/i18n';
|
|
18
|
+
import { cn } from '@/utils/cn';
|
|
19
|
+
import {
|
|
20
|
+
Dialog,
|
|
21
|
+
DialogContent,
|
|
22
|
+
DialogOverlay,
|
|
23
|
+
DialogTitle,
|
|
24
|
+
} from '@radix-ui/react-dialog';
|
|
25
|
+
import type {
|
|
26
|
+
HighlightedText,
|
|
27
|
+
ReactSortedResult as BaseResultType,
|
|
28
|
+
} from '@hanzo/docs-core/search';
|
|
29
|
+
import { cva } from 'class-variance-authority';
|
|
30
|
+
import { useRouter } from '@hanzo/docs-core/framework';
|
|
31
|
+
import type { SharedProps } from '@/contexts/search';
|
|
32
|
+
import { useOnChange } from '@hanzo/docs-core/utils/use-on-change';
|
|
33
|
+
import scrollIntoView from 'scroll-into-view-if-needed';
|
|
34
|
+
import { buttonVariants } from '@/components/ui/button';
|
|
35
|
+
|
|
36
|
+
export type SearchItemType =
|
|
37
|
+
| (BaseResultType & {
|
|
38
|
+
external?: boolean;
|
|
39
|
+
})
|
|
40
|
+
| {
|
|
41
|
+
id: string;
|
|
42
|
+
type: 'action';
|
|
43
|
+
node: ReactNode;
|
|
44
|
+
onSelect: () => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// needed for backward compatible since some previous guides referenced it
|
|
48
|
+
export type { SharedProps };
|
|
49
|
+
|
|
50
|
+
export interface SearchDialogProps extends SharedProps {
|
|
51
|
+
search: string;
|
|
52
|
+
onSearchChange: (v: string) => void;
|
|
53
|
+
isLoading?: boolean;
|
|
54
|
+
|
|
55
|
+
children: ReactNode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const Context = createContext<{
|
|
59
|
+
open: boolean;
|
|
60
|
+
onOpenChange: (open: boolean) => void;
|
|
61
|
+
search: string;
|
|
62
|
+
onSearchChange: (v: string) => void;
|
|
63
|
+
|
|
64
|
+
isLoading: boolean;
|
|
65
|
+
} | null>(null);
|
|
66
|
+
|
|
67
|
+
const ListContext = createContext<{
|
|
68
|
+
active: string | null;
|
|
69
|
+
setActive: (v: string | null) => void;
|
|
70
|
+
} | null>(null);
|
|
71
|
+
|
|
72
|
+
const TagsListContext = createContext<{
|
|
73
|
+
value?: string;
|
|
74
|
+
onValueChange: (value: string | undefined) => void;
|
|
75
|
+
allowClear: boolean;
|
|
76
|
+
} | null>(null);
|
|
77
|
+
|
|
78
|
+
export function SearchDialog({
|
|
79
|
+
open,
|
|
80
|
+
onOpenChange,
|
|
81
|
+
search,
|
|
82
|
+
onSearchChange,
|
|
83
|
+
isLoading = false,
|
|
84
|
+
children,
|
|
85
|
+
}: SearchDialogProps) {
|
|
86
|
+
const [active, setActive] = useState<string | null>(null);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
90
|
+
<Context.Provider
|
|
91
|
+
value={useMemo(
|
|
92
|
+
() => ({
|
|
93
|
+
open,
|
|
94
|
+
onOpenChange,
|
|
95
|
+
search,
|
|
96
|
+
onSearchChange,
|
|
97
|
+
active,
|
|
98
|
+
setActive,
|
|
99
|
+
isLoading,
|
|
100
|
+
}),
|
|
101
|
+
[active, isLoading, onOpenChange, onSearchChange, open, search],
|
|
102
|
+
)}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</Context.Provider>
|
|
106
|
+
</Dialog>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function SearchDialogHeader(props: ComponentProps<'div'>) {
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
{...props}
|
|
114
|
+
className={cn('flex flex-row items-center gap-2 p-3', props.className)}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function SearchDialogInput(props: ComponentProps<'input'>) {
|
|
120
|
+
const { text } = useI18n();
|
|
121
|
+
const { search, onSearchChange } = useSearch();
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<input
|
|
125
|
+
{...props}
|
|
126
|
+
value={search}
|
|
127
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
128
|
+
placeholder={text.search}
|
|
129
|
+
className="w-0 flex-1 bg-transparent text-lg placeholder:text-fd-muted-foreground focus-visible:outline-none"
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function SearchDialogClose({
|
|
135
|
+
children = 'ESC',
|
|
136
|
+
className,
|
|
137
|
+
...props
|
|
138
|
+
}: ComponentProps<'button'>) {
|
|
139
|
+
const { onOpenChange } = useSearch();
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() => onOpenChange(false)}
|
|
145
|
+
className={cn(
|
|
146
|
+
buttonVariants({
|
|
147
|
+
color: 'outline',
|
|
148
|
+
size: 'sm',
|
|
149
|
+
className: 'font-mono text-fd-muted-foreground',
|
|
150
|
+
}),
|
|
151
|
+
className,
|
|
152
|
+
)}
|
|
153
|
+
{...props}
|
|
154
|
+
>
|
|
155
|
+
{children}
|
|
156
|
+
</button>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function SearchDialogFooter(props: ComponentProps<'div'>) {
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
{...props}
|
|
164
|
+
className={cn('bg-fd-secondary/50 p-3 empty:hidden', props.className)}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function SearchDialogOverlay(
|
|
170
|
+
props: ComponentProps<typeof DialogOverlay>,
|
|
171
|
+
) {
|
|
172
|
+
return (
|
|
173
|
+
<DialogOverlay
|
|
174
|
+
{...props}
|
|
175
|
+
className={cn(
|
|
176
|
+
'fixed inset-0 z-50 backdrop-blur-xs bg-fd-overlay data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out',
|
|
177
|
+
props.className,
|
|
178
|
+
)}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function SearchDialogContent({
|
|
184
|
+
children,
|
|
185
|
+
...props
|
|
186
|
+
}: ComponentProps<typeof DialogContent>) {
|
|
187
|
+
const { text } = useI18n();
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<DialogContent
|
|
191
|
+
aria-describedby={undefined}
|
|
192
|
+
{...props}
|
|
193
|
+
className={cn(
|
|
194
|
+
'fixed left-1/2 top-4 md:top-[calc(50%-250px)] z-50 w-[calc(100%-1rem)] max-w-screen-sm -translate-x-1/2 rounded-xl border bg-fd-popover text-fd-popover-foreground shadow-2xl shadow-black/50 overflow-hidden data-[state=closed]:animate-fd-dialog-out data-[state=open]:animate-fd-dialog-in',
|
|
195
|
+
'*:border-b *:has-[+:last-child[data-empty=true]]:border-b-0 *:data-[empty=true]:border-b-0 *:last:border-b-0',
|
|
196
|
+
props.className,
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
<DialogTitle className="hidden">{text.search}</DialogTitle>
|
|
200
|
+
{children}
|
|
201
|
+
</DialogContent>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function SearchDialogList({
|
|
206
|
+
items = null,
|
|
207
|
+
Empty = () => (
|
|
208
|
+
<div className="py-12 text-center text-sm text-fd-muted-foreground">
|
|
209
|
+
<I18nLabel label="searchNoResult" />
|
|
210
|
+
</div>
|
|
211
|
+
),
|
|
212
|
+
Item = (props) => <SearchDialogListItem {...props} />,
|
|
213
|
+
...props
|
|
214
|
+
}: Omit<ComponentProps<'div'>, 'children'> & {
|
|
215
|
+
items: SearchItemType[] | null | undefined;
|
|
216
|
+
/**
|
|
217
|
+
* Renderer for empty list UI
|
|
218
|
+
*/
|
|
219
|
+
Empty?: () => ReactNode;
|
|
220
|
+
/**
|
|
221
|
+
* Renderer for items
|
|
222
|
+
*/
|
|
223
|
+
Item?: (props: { item: SearchItemType; onClick: () => void }) => ReactNode;
|
|
224
|
+
}) {
|
|
225
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
226
|
+
const [active, setActive] = useState<string | null>(() =>
|
|
227
|
+
items && items.length > 0 ? items[0].id : null,
|
|
228
|
+
);
|
|
229
|
+
const { onOpenChange } = useSearch();
|
|
230
|
+
const router = useRouter();
|
|
231
|
+
|
|
232
|
+
const onOpen = (item: SearchItemType) => {
|
|
233
|
+
if (item.type === 'action') {
|
|
234
|
+
item.onSelect();
|
|
235
|
+
} else if (item.external) {
|
|
236
|
+
window.open(item.url, '_blank')?.focus();
|
|
237
|
+
} else {
|
|
238
|
+
router.push(item.url);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
onOpenChange(false);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const onKey = useEffectEvent((e: KeyboardEvent) => {
|
|
245
|
+
if (!items || e.isComposing) return;
|
|
246
|
+
|
|
247
|
+
if (e.key === 'ArrowDown' || e.key == 'ArrowUp') {
|
|
248
|
+
let idx = items.findIndex((item) => item.id === active);
|
|
249
|
+
if (idx === -1) idx = 0;
|
|
250
|
+
else if (e.key === 'ArrowDown') idx++;
|
|
251
|
+
else idx--;
|
|
252
|
+
|
|
253
|
+
setActive(items.at(idx % items.length)?.id ?? null);
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (e.key === 'Enter') {
|
|
258
|
+
const selected = items.find((item) => item.id === active);
|
|
259
|
+
|
|
260
|
+
if (selected) onOpen(selected);
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
const element = ref.current;
|
|
267
|
+
if (!element) return;
|
|
268
|
+
|
|
269
|
+
const observer = new ResizeObserver(() => {
|
|
270
|
+
const viewport = element.firstElementChild!;
|
|
271
|
+
|
|
272
|
+
element.style.setProperty(
|
|
273
|
+
'--fd-animated-height',
|
|
274
|
+
`${viewport.clientHeight}px`,
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const viewport = element.firstElementChild;
|
|
279
|
+
if (viewport) observer.observe(viewport);
|
|
280
|
+
|
|
281
|
+
window.addEventListener('keydown', onKey);
|
|
282
|
+
return () => {
|
|
283
|
+
observer.disconnect();
|
|
284
|
+
window.removeEventListener('keydown', onKey);
|
|
285
|
+
};
|
|
286
|
+
}, []);
|
|
287
|
+
|
|
288
|
+
useOnChange(items, () => {
|
|
289
|
+
if (items && items.length > 0) {
|
|
290
|
+
setActive(items[0].id);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div
|
|
296
|
+
{...props}
|
|
297
|
+
ref={ref}
|
|
298
|
+
data-empty={items === null}
|
|
299
|
+
className={cn(
|
|
300
|
+
'overflow-hidden h-(--fd-animated-height) transition-[height]',
|
|
301
|
+
props.className,
|
|
302
|
+
)}
|
|
303
|
+
>
|
|
304
|
+
<div
|
|
305
|
+
className={cn(
|
|
306
|
+
'w-full flex flex-col overflow-y-auto max-h-[460px] p-1',
|
|
307
|
+
!items && 'hidden',
|
|
308
|
+
)}
|
|
309
|
+
>
|
|
310
|
+
<ListContext.Provider
|
|
311
|
+
value={useMemo(
|
|
312
|
+
() => ({
|
|
313
|
+
active,
|
|
314
|
+
setActive,
|
|
315
|
+
}),
|
|
316
|
+
[active],
|
|
317
|
+
)}
|
|
318
|
+
>
|
|
319
|
+
{items?.length === 0 && Empty()}
|
|
320
|
+
|
|
321
|
+
{items?.map((item) => (
|
|
322
|
+
<Fragment key={item.id}>
|
|
323
|
+
{Item({ item, onClick: () => onOpen(item) })}
|
|
324
|
+
</Fragment>
|
|
325
|
+
))}
|
|
326
|
+
</ListContext.Provider>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function SearchDialogListItem({
|
|
333
|
+
item,
|
|
334
|
+
className,
|
|
335
|
+
children,
|
|
336
|
+
renderHighlights: render = renderHighlights,
|
|
337
|
+
...props
|
|
338
|
+
}: ComponentProps<'button'> & {
|
|
339
|
+
renderHighlights?: typeof renderHighlights;
|
|
340
|
+
item: SearchItemType;
|
|
341
|
+
}) {
|
|
342
|
+
const { active: activeId, setActive } = useSearchList();
|
|
343
|
+
const active = item.id === activeId;
|
|
344
|
+
|
|
345
|
+
if (item.type === 'action') {
|
|
346
|
+
children ??= item.node;
|
|
347
|
+
} else {
|
|
348
|
+
children ??= (
|
|
349
|
+
<>
|
|
350
|
+
<div className="inline-flex items-center text-fd-muted-foreground text-xs empty:hidden">
|
|
351
|
+
{item.breadcrumbs?.map((item, i) => (
|
|
352
|
+
<Fragment key={i}>
|
|
353
|
+
{i > 0 && <ChevronRight className="size-4" />}
|
|
354
|
+
{item}
|
|
355
|
+
</Fragment>
|
|
356
|
+
))}
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
{item.type !== 'page' && (
|
|
360
|
+
<div
|
|
361
|
+
role="none"
|
|
362
|
+
className="absolute start-3 inset-y-0 w-px bg-fd-border"
|
|
363
|
+
/>
|
|
364
|
+
)}
|
|
365
|
+
<p
|
|
366
|
+
className={cn(
|
|
367
|
+
'min-w-0 truncate',
|
|
368
|
+
item.type !== 'page' && 'ps-4',
|
|
369
|
+
item.type === 'page' || item.type === 'heading'
|
|
370
|
+
? 'font-medium'
|
|
371
|
+
: 'text-fd-popover-foreground/80',
|
|
372
|
+
)}
|
|
373
|
+
>
|
|
374
|
+
{item.type === 'heading' && (
|
|
375
|
+
<Hash className="inline me-1 size-4 text-fd-muted-foreground" />
|
|
376
|
+
)}
|
|
377
|
+
{item.contentWithHighlights
|
|
378
|
+
? render(item.contentWithHighlights)
|
|
379
|
+
: item.content}
|
|
380
|
+
</p>
|
|
381
|
+
</>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
ref={useCallback(
|
|
389
|
+
(element: HTMLButtonElement | null) => {
|
|
390
|
+
if (active && element) {
|
|
391
|
+
scrollIntoView(element, {
|
|
392
|
+
scrollMode: 'if-needed',
|
|
393
|
+
block: 'nearest',
|
|
394
|
+
boundary: element.parentElement,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
[active],
|
|
399
|
+
)}
|
|
400
|
+
aria-selected={active}
|
|
401
|
+
className={cn(
|
|
402
|
+
'relative select-none px-2.5 py-2 text-start text-sm rounded-lg',
|
|
403
|
+
active && 'bg-fd-accent text-fd-accent-foreground',
|
|
404
|
+
className,
|
|
405
|
+
)}
|
|
406
|
+
onPointerMove={() => setActive(item.id)}
|
|
407
|
+
{...props}
|
|
408
|
+
>
|
|
409
|
+
{children}
|
|
410
|
+
</button>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function SearchDialogIcon(props: ComponentProps<'svg'>) {
|
|
415
|
+
const { isLoading } = useSearch();
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<SearchIcon
|
|
419
|
+
{...props}
|
|
420
|
+
className={cn(
|
|
421
|
+
'size-5 text-fd-muted-foreground',
|
|
422
|
+
isLoading && 'animate-pulse duration-400',
|
|
423
|
+
props.className,
|
|
424
|
+
)}
|
|
425
|
+
/>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export interface TagsListProps extends ComponentProps<'div'> {
|
|
430
|
+
tag?: string;
|
|
431
|
+
onTagChange: (tag: string | undefined) => void;
|
|
432
|
+
allowClear?: boolean;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const itemVariants = cva(
|
|
436
|
+
'rounded-md border px-2 py-0.5 text-xs font-medium text-fd-muted-foreground transition-colors',
|
|
437
|
+
{
|
|
438
|
+
variants: {
|
|
439
|
+
active: {
|
|
440
|
+
true: 'bg-fd-accent text-fd-accent-foreground',
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
export function TagsList({
|
|
447
|
+
tag,
|
|
448
|
+
onTagChange,
|
|
449
|
+
allowClear = false,
|
|
450
|
+
...props
|
|
451
|
+
}: TagsListProps) {
|
|
452
|
+
return (
|
|
453
|
+
<div
|
|
454
|
+
{...props}
|
|
455
|
+
className={cn('flex items-center gap-1 flex-wrap', props.className)}
|
|
456
|
+
>
|
|
457
|
+
<TagsListContext.Provider
|
|
458
|
+
value={useMemo(
|
|
459
|
+
() => ({
|
|
460
|
+
value: tag,
|
|
461
|
+
onValueChange: onTagChange,
|
|
462
|
+
allowClear,
|
|
463
|
+
}),
|
|
464
|
+
[allowClear, onTagChange, tag],
|
|
465
|
+
)}
|
|
466
|
+
>
|
|
467
|
+
{props.children}
|
|
468
|
+
</TagsListContext.Provider>
|
|
469
|
+
</div>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function TagsListItem({
|
|
474
|
+
value,
|
|
475
|
+
className,
|
|
476
|
+
...props
|
|
477
|
+
}: ComponentProps<'button'> & {
|
|
478
|
+
value: string;
|
|
479
|
+
}) {
|
|
480
|
+
const { onValueChange, value: selectedValue, allowClear } = useTagsList();
|
|
481
|
+
const selected = value === selectedValue;
|
|
482
|
+
|
|
483
|
+
return (
|
|
484
|
+
<button
|
|
485
|
+
type="button"
|
|
486
|
+
data-active={selected}
|
|
487
|
+
className={cn(itemVariants({ active: selected, className }))}
|
|
488
|
+
onClick={() => {
|
|
489
|
+
onValueChange(selected && allowClear ? undefined : value);
|
|
490
|
+
}}
|
|
491
|
+
tabIndex={-1}
|
|
492
|
+
{...props}
|
|
493
|
+
>
|
|
494
|
+
{props.children}
|
|
495
|
+
</button>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function renderHighlights(highlights: HighlightedText<ReactNode>[]): ReactNode {
|
|
500
|
+
return highlights.map((node, i) => {
|
|
501
|
+
if (node.styles?.highlight) {
|
|
502
|
+
return (
|
|
503
|
+
<span key={i} className="text-fd-primary underline">
|
|
504
|
+
{node.content}
|
|
505
|
+
</span>
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return <Fragment key={i}>{node.content}</Fragment>;
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function useSearch() {
|
|
514
|
+
const ctx = useContext(Context);
|
|
515
|
+
if (!ctx) throw new Error('Missing <SearchDialog />');
|
|
516
|
+
return ctx;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function useTagsList() {
|
|
520
|
+
const ctx = useContext(TagsListContext);
|
|
521
|
+
if (!ctx) throw new Error('Missing <TagsList />');
|
|
522
|
+
return ctx;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function useSearchList() {
|
|
526
|
+
const ctx = useContext(ListContext);
|
|
527
|
+
if (!ctx) throw new Error('Missing <SearchDialogList />');
|
|
528
|
+
return ctx;
|
|
529
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { CodeBlock, type CodeBlockProps, Pre } from '@/components/codeblock';
|
|
3
|
+
import type {
|
|
4
|
+
HighlightOptions,
|
|
5
|
+
HighlightOptionsCommon,
|
|
6
|
+
HighlightOptionsThemes,
|
|
7
|
+
} from '@hanzo/docs-core/highlight';
|
|
8
|
+
import { useShiki } from '@hanzo/docs-core/highlight/client';
|
|
9
|
+
import { cn } from '@/utils/cn';
|
|
10
|
+
import {
|
|
11
|
+
type ComponentProps,
|
|
12
|
+
createContext,
|
|
13
|
+
type FC,
|
|
14
|
+
Suspense,
|
|
15
|
+
use,
|
|
16
|
+
useDeferredValue,
|
|
17
|
+
useId,
|
|
18
|
+
} from 'react';
|
|
19
|
+
|
|
20
|
+
export interface DynamicCodeblockProps {
|
|
21
|
+
lang: string;
|
|
22
|
+
code: string;
|
|
23
|
+
/**
|
|
24
|
+
* Extra props for the underlying `<CodeBlock />` component.
|
|
25
|
+
*
|
|
26
|
+
* Ignored if you defined your own `pre` component in `options.components`.
|
|
27
|
+
*/
|
|
28
|
+
codeblock?: CodeBlockProps;
|
|
29
|
+
/**
|
|
30
|
+
* Wrap in React `<Suspense />` and provide a fallback.
|
|
31
|
+
*
|
|
32
|
+
* @defaultValue true
|
|
33
|
+
*/
|
|
34
|
+
wrapInSuspense?: boolean;
|
|
35
|
+
options?: Omit<HighlightOptionsCommon, 'lang'> & HighlightOptionsThemes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const PropsContext = createContext<CodeBlockProps | undefined>(undefined);
|
|
39
|
+
|
|
40
|
+
function DefaultPre(props: ComponentProps<'pre'>) {
|
|
41
|
+
const extraProps = use(PropsContext);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<CodeBlock
|
|
45
|
+
{...props}
|
|
46
|
+
{...extraProps}
|
|
47
|
+
className={cn('my-0', props.className, extraProps?.className)}
|
|
48
|
+
>
|
|
49
|
+
<Pre>{props.children}</Pre>
|
|
50
|
+
</CodeBlock>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function DynamicCodeBlock({
|
|
55
|
+
lang,
|
|
56
|
+
code,
|
|
57
|
+
codeblock,
|
|
58
|
+
options,
|
|
59
|
+
wrapInSuspense = true,
|
|
60
|
+
}: DynamicCodeblockProps) {
|
|
61
|
+
const id = useId();
|
|
62
|
+
const shikiOptions = {
|
|
63
|
+
lang,
|
|
64
|
+
...options,
|
|
65
|
+
components: {
|
|
66
|
+
pre: DefaultPre,
|
|
67
|
+
...options?.components,
|
|
68
|
+
},
|
|
69
|
+
} satisfies HighlightOptions;
|
|
70
|
+
|
|
71
|
+
const children = (
|
|
72
|
+
<PropsContext value={codeblock}>
|
|
73
|
+
<Internal
|
|
74
|
+
id={id}
|
|
75
|
+
{...useDeferredValue({ code, options: shikiOptions })}
|
|
76
|
+
/>
|
|
77
|
+
</PropsContext>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (wrapInSuspense)
|
|
81
|
+
return (
|
|
82
|
+
<Suspense
|
|
83
|
+
fallback={
|
|
84
|
+
<Placeholder code={code} components={shikiOptions.components} />
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</Suspense>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return children;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function Placeholder({
|
|
95
|
+
code,
|
|
96
|
+
components = {},
|
|
97
|
+
}: {
|
|
98
|
+
code: string;
|
|
99
|
+
components: HighlightOptions['components'];
|
|
100
|
+
}) {
|
|
101
|
+
const { pre: Pre = 'pre', code: Code = 'code' } = components as Record<
|
|
102
|
+
string,
|
|
103
|
+
FC
|
|
104
|
+
>;
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<Pre>
|
|
108
|
+
<Code>
|
|
109
|
+
{code.split('\n').map((line, i) => (
|
|
110
|
+
<span key={i} className="line">
|
|
111
|
+
{line}
|
|
112
|
+
</span>
|
|
113
|
+
))}
|
|
114
|
+
</Code>
|
|
115
|
+
</Pre>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Internal({
|
|
120
|
+
id,
|
|
121
|
+
code,
|
|
122
|
+
options,
|
|
123
|
+
}: {
|
|
124
|
+
id: string;
|
|
125
|
+
code: string;
|
|
126
|
+
options: HighlightOptions;
|
|
127
|
+
}) {
|
|
128
|
+
return useShiki(code, options, [id, options.lang, code]);
|
|
129
|
+
}
|