@hanzo/ui 5.3.26 → 5.3.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/content/index.ts +26 -0
  2. package/dist/util/index.js +6 -0
  3. package/dist/util/index.mjs +6 -1
  4. package/docs/_registry/index.ts +426 -0
  5. package/docs/_registry/layout/docs-min.tsx +197 -0
  6. package/docs/_registry/layout/page-min.tsx +128 -0
  7. package/docs/components/accordion.tsx +118 -0
  8. package/docs/components/banner.tsx +144 -0
  9. package/docs/components/callout.tsx +112 -0
  10. package/docs/components/card.tsx +52 -0
  11. package/docs/components/codeblock.tsx +258 -0
  12. package/docs/components/dialog/search-algolia.tsx +132 -0
  13. package/docs/components/dialog/search-default.tsx +131 -0
  14. package/docs/components/dialog/search-orama.tsx +143 -0
  15. package/docs/components/dialog/search.tsx +529 -0
  16. package/docs/components/dynamic-codeblock.tsx +129 -0
  17. package/docs/components/files.tsx +81 -0
  18. package/docs/components/github-info.tsx +107 -0
  19. package/docs/components/heading.tsx +33 -0
  20. package/docs/components/image-zoom.css +77 -0
  21. package/docs/components/image-zoom.tsx +58 -0
  22. package/docs/components/index.ts +7 -0
  23. package/docs/components/inline-toc.tsx +48 -0
  24. package/docs/components/sidebar/base.tsx +451 -0
  25. package/docs/components/sidebar/link-item.tsx +65 -0
  26. package/docs/components/sidebar/page-tree.tsx +113 -0
  27. package/docs/components/sidebar/tabs/dropdown.tsx +109 -0
  28. package/docs/components/sidebar/tabs/index.tsx +89 -0
  29. package/docs/components/steps.tsx +9 -0
  30. package/docs/components/tabs.tsx +203 -0
  31. package/docs/components/toc/clerk.tsx +173 -0
  32. package/docs/components/toc/default.tsx +57 -0
  33. package/docs/components/toc/index.tsx +136 -0
  34. package/docs/components/type-table.tsx +174 -0
  35. package/docs/components/ui/accordion.tsx +88 -0
  36. package/docs/components/ui/button.tsx +28 -0
  37. package/docs/components/ui/collapsible.tsx +42 -0
  38. package/docs/components/ui/navigation-menu.tsx +83 -0
  39. package/docs/components/ui/popover.tsx +32 -0
  40. package/docs/components/ui/scroll-area.tsx +59 -0
  41. package/docs/components/ui/tabs.tsx +145 -0
  42. package/docs/contexts/i18n.tsx +56 -0
  43. package/docs/contexts/search.tsx +165 -0
  44. package/docs/contexts/tree.tsx +65 -0
  45. package/docs/css/black.css +39 -0
  46. package/docs/css/catppuccin.css +49 -0
  47. package/docs/css/colors/index.css +51 -0
  48. package/docs/css/dusk.css +47 -0
  49. package/docs/css/layouts/docs.css +1 -0
  50. package/docs/css/layouts/home.css +1 -0
  51. package/docs/css/layouts/notebook.css +1 -0
  52. package/docs/css/neutral.css +7 -0
  53. package/docs/css/ocean.css +48 -0
  54. package/docs/css/preset.css +305 -0
  55. package/docs/css/purple.css +39 -0
  56. package/docs/css/shadcn.css +36 -0
  57. package/docs/css/shiki.css +90 -0
  58. package/docs/css/solar.css +75 -0
  59. package/docs/css/style.css +9 -0
  60. package/docs/css/vitepress.css +77 -0
  61. package/docs/i18n.tsx +30 -0
  62. package/docs/icons.tsx +354 -0
  63. package/docs/layouts/docs/client.tsx +129 -0
  64. package/docs/layouts/docs/index.tsx +321 -0
  65. package/docs/layouts/docs/page/client.tsx +376 -0
  66. package/docs/layouts/docs/page/index.tsx +251 -0
  67. package/docs/layouts/docs/sidebar.tsx +265 -0
  68. package/docs/layouts/home/client.tsx +375 -0
  69. package/docs/layouts/home/index.tsx +51 -0
  70. package/docs/layouts/home/navbar.tsx +55 -0
  71. package/docs/layouts/notebook/client.tsx +281 -0
  72. package/docs/layouts/notebook/index.tsx +461 -0
  73. package/docs/layouts/notebook/page/client.tsx +375 -0
  74. package/docs/layouts/notebook/page/index.tsx +251 -0
  75. package/docs/layouts/notebook/sidebar.tsx +248 -0
  76. package/docs/layouts/shared/index.tsx +89 -0
  77. package/docs/layouts/shared/language-toggle.tsx +66 -0
  78. package/docs/layouts/shared/link-item.tsx +119 -0
  79. package/docs/layouts/shared/search-toggle.tsx +78 -0
  80. package/docs/layouts/shared/theme-toggle.tsx +86 -0
  81. package/docs/mdx.server.tsx +37 -0
  82. package/docs/mdx.tsx +97 -0
  83. package/docs/og.tsx +101 -0
  84. package/docs/page.tsx +85 -0
  85. package/docs/provider/base.tsx +173 -0
  86. package/docs/provider/next.tsx +23 -0
  87. package/docs/provider/react-router.tsx +23 -0
  88. package/docs/provider/tanstack.tsx +23 -0
  89. package/docs/provider/waku.tsx +23 -0
  90. package/docs/source.ts +3 -0
  91. package/docs/theme/typography/LICENSE +21 -0
  92. package/docs/theme/typography/index.ts +201 -0
  93. package/docs/theme/typography/styles.ts +449 -0
  94. package/docs/utils/cn.ts +1 -0
  95. package/docs/utils/is-active.ts +23 -0
  96. package/docs/utils/merge-refs.ts +15 -0
  97. package/docs/utils/use-copy-button.ts +39 -0
  98. package/docs/utils/use-footer-items.ts +27 -0
  99. package/docs/utils/use-is-scroll-top.ts +21 -0
  100. package/package.json +4 -2
@@ -0,0 +1,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
+ }