@vvedantb/ui 0.1.0 → 0.1.2

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 (74) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +3 -2
  3. package/src/ai-elements/artifact.tsx +99 -99
  4. package/src/ai-elements/attachments.tsx +366 -366
  5. package/src/ai-elements/chain-of-thought.tsx +212 -212
  6. package/src/ai-elements/code-block.tsx +478 -478
  7. package/src/ai-elements/conversation.tsx +157 -157
  8. package/src/ai-elements/image.tsx +25 -25
  9. package/src/ai-elements/inline-citation.tsx +252 -252
  10. package/src/ai-elements/message.tsx +269 -269
  11. package/src/ai-elements/model-selector.tsx +211 -211
  12. package/src/ai-elements/prompt-input.tsx +1018 -1018
  13. package/src/ai-elements/queue.tsx +251 -251
  14. package/src/ai-elements/reasoning.tsx +178 -178
  15. package/src/ai-elements/shimmer.tsx +56 -56
  16. package/src/ai-elements/sources.tsx +77 -77
  17. package/src/ai-elements/speech-input.tsx +290 -290
  18. package/src/ai-elements/suggestion.tsx +191 -191
  19. package/src/css.d.ts +2 -2
  20. package/src/index.ts +96 -96
  21. package/src/motion/presets.ts +35 -35
  22. package/src/motion/presets.vibot.ts +105 -105
  23. package/src/ui/_menu-classes.ts +30 -30
  24. package/src/ui/_menu-classes.vibot.ts +33 -33
  25. package/src/ui/accordion.tsx +81 -81
  26. package/src/ui/accordion.verve.tsx +56 -56
  27. package/src/ui/badge.tsx +34 -34
  28. package/src/ui/breadcrumb.tsx +83 -83
  29. package/src/ui/button-group.tsx +84 -84
  30. package/src/ui/button.tsx +57 -57
  31. package/src/ui/card.tsx +27 -27
  32. package/src/ui/carousel.tsx +211 -211
  33. package/src/ui/checkbox.tsx +27 -27
  34. package/src/ui/clear-input.tsx +170 -170
  35. package/src/ui/clear-input.vibot.tsx +76 -76
  36. package/src/ui/clearInputDissolve.ts +214 -214
  37. package/src/ui/collapsible.tsx +7 -7
  38. package/src/ui/command.tsx +148 -148
  39. package/src/ui/context-menu.tsx +173 -173
  40. package/src/ui/dialog.tsx +149 -149
  41. package/src/ui/dropdown-menu.tsx +178 -178
  42. package/src/ui/hover-card.tsx +31 -31
  43. package/src/ui/input-group.tsx +162 -162
  44. package/src/ui/input-group.verve.tsx +150 -150
  45. package/src/ui/input.tsx +21 -21
  46. package/src/ui/kbd.tsx +18 -18
  47. package/src/ui/label.tsx +20 -20
  48. package/src/ui/labeled-switch-row.tsx +53 -53
  49. package/src/ui/modalTransition.ts +85 -85
  50. package/src/ui/pagination.tsx +47 -47
  51. package/src/ui/popover.tsx +33 -33
  52. package/src/ui/progress.tsx +25 -25
  53. package/src/ui/radix-select.tsx +202 -202
  54. package/src/ui/scroll-area.tsx +55 -55
  55. package/src/ui/scroll-area.verve.tsx +43 -43
  56. package/src/ui/search-input.tsx +29 -29
  57. package/src/ui/select.tsx +163 -163
  58. package/src/ui/separator.tsx +28 -28
  59. package/src/ui/sheet.tsx +87 -87
  60. package/src/ui/skeleton.tsx +18 -18
  61. package/src/ui/sonner.css +16 -16
  62. package/src/ui/sonner.tsx +47 -47
  63. package/src/ui/spinner.tsx +27 -27
  64. package/src/ui/surface-classes.ts +55 -55
  65. package/src/ui/switch.tsx +26 -26
  66. package/src/ui/table.tsx +123 -123
  67. package/src/ui/tabs.tsx +140 -140
  68. package/src/ui/tabsSliding.ts +56 -56
  69. package/src/ui/textarea.tsx +21 -21
  70. package/src/ui/time-picker.tsx +170 -170
  71. package/src/ui/tooltip.tsx +25 -25
  72. package/src/ui/visually-hidden.tsx +3 -3
  73. package/src/utils/cn.ts +16 -16
  74. package/src/utils/ref.ts +10 -10
@@ -1,478 +1,478 @@
1
- "use client";
2
- import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
3
- import type {
4
- BundledLanguage,
5
- BundledTheme,
6
- HighlighterGeneric,
7
- ThemedToken,
8
- } from "shiki";
9
- import { Button } from "../ui/button";
10
- import {
11
- Select,
12
- SelectContent,
13
- SelectItem,
14
- SelectTrigger,
15
- SelectValue,
16
- } from "../ui/radix-select";
17
- import { cn } from "../utils/cn";
18
- import { flatPanelSubtleClass } from "../ui/surface-classes";
19
- import { IconCheck, IconCopy } from "@tabler/icons-react";
20
- import { createContext, useContext, useEffect, useRef, useState } from "react";
21
- import { bundledLanguages, createHighlighter } from "shiki";
22
- const isBundledLanguage = (lang: string): lang is BundledLanguage =>
23
- lang in bundledLanguages;
24
- // Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline
25
- // biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
26
- // eslint-disable-next-line no-bitwise -- shiki bitflag check
27
- const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;
28
- // biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
29
- // eslint-disable-next-line no-bitwise -- shiki bitflag check
30
- // oxlint-disable-next-line eslint(no-bitwise)
31
- const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;
32
- const isUnderline = (fontStyle: number | undefined) =>
33
- // biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
34
- // oxlint-disable-next-line eslint(no-bitwise)
35
- fontStyle && fontStyle & 4;
36
- // Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint
37
- interface KeyedToken {
38
- token: ThemedToken;
39
- key: string;
40
- }
41
- interface KeyedLine {
42
- tokens: KeyedToken[];
43
- key: string;
44
- }
45
- const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>
46
- lines.map((line, lineIdx) => ({
47
- key: `line-${lineIdx}`,
48
- tokens: line.map((token, tokenIdx) => ({
49
- key: `line-${lineIdx}-${tokenIdx}`,
50
- token,
51
- })),
52
- }));
53
- // Token rendering component
54
- const TokenSpan = ({ token }: { token: ThemedToken }) => (
55
- <span
56
- className="dark:bg-(--shiki-dark-bg)! dark:text-(--shiki-dark)!"
57
- style={
58
- {
59
- backgroundColor: token.bgColor,
60
- color: token.color,
61
- fontStyle: isItalic(token.fontStyle) ? "italic" : undefined,
62
- fontWeight: isBold(token.fontStyle) ? "bold" : undefined,
63
- textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined,
64
- ...token.htmlStyle,
65
- } as CSSProperties
66
- }
67
- >
68
- {token.content}
69
- </span>
70
- );
71
- // Line rendering component
72
- const LineSpan = ({
73
- keyedLine,
74
- showLineNumbers,
75
- }: {
76
- keyedLine: KeyedLine;
77
- showLineNumbers: boolean;
78
- }) => (
79
- <span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}>
80
- {keyedLine.tokens.length === 0
81
- ? "\n"
82
- : keyedLine.tokens.map(({ token, key }) => (
83
- <TokenSpan key={key} token={token} />
84
- ))}
85
- </span>
86
- );
87
- // Types
88
- type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
89
- code: string;
90
- language: string;
91
- showLineNumbers?: boolean;
92
- };
93
- interface TokenizedCode {
94
- tokens: ThemedToken[][];
95
- fg: string;
96
- bg: string;
97
- }
98
- interface CodeBlockContextType {
99
- code: string;
100
- }
101
- // Context
102
- const CodeBlockContext = createContext<CodeBlockContextType>({
103
- code: "",
104
- });
105
- // Highlighter cache (singleton per language)
106
- const highlighterCache = new Map<
107
- string,
108
- Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
109
- >();
110
- // Token cache
111
- const tokensCache = new Map<string, TokenizedCode>();
112
- // Subscribers for async token updates
113
- const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
114
- const getTokensCacheKey = (code: string, language: BundledLanguage) => {
115
- const start = code.slice(0, 100);
116
- const end = code.length > 100 ? code.slice(-100) : "";
117
- return `${language}:${code.length}:${start}:${end}`;
118
- };
119
- const getHighlighter = (
120
- language: BundledLanguage,
121
- ): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
122
- const cached = highlighterCache.get(language);
123
- if (cached) {
124
- return cached;
125
- }
126
- const highlighterPromise = createHighlighter({
127
- langs: [language],
128
- themes: ["github-light", "github-dark"],
129
- });
130
- highlighterCache.set(language, highlighterPromise);
131
- return highlighterPromise;
132
- };
133
- // Create raw tokens for immediate display while highlighting loads
134
- const createRawTokens = (code: string): TokenizedCode => ({
135
- bg: "transparent",
136
- fg: "inherit",
137
- tokens: code.split("\n").map((line) =>
138
- line === ""
139
- ? []
140
- : [
141
- {
142
- color: "inherit",
143
- content: line,
144
- } as ThemedToken,
145
- ],
146
- ),
147
- });
148
- // Synchronous highlight with callback for async results
149
- export const highlightCode = (
150
- code: string,
151
- language: string,
152
- // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
153
- callback?: (result: TokenizedCode) => void,
154
- ): TokenizedCode | null => {
155
- if (!isBundledLanguage(language)) return null;
156
- const tokensCacheKey = getTokensCacheKey(code, language);
157
- // Return cached result if available
158
- const cached = tokensCache.get(tokensCacheKey);
159
- if (cached) {
160
- return cached;
161
- }
162
- // Subscribe callback if provided
163
- if (callback) {
164
- if (!subscribers.has(tokensCacheKey)) {
165
- subscribers.set(tokensCacheKey, new Set());
166
- }
167
- subscribers.get(tokensCacheKey)?.add(callback);
168
- }
169
- // Start highlighting in background - fire-and-forget async pattern
170
- getHighlighter(language)
171
- // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
172
- .then((highlighter) => {
173
- const availableLangs = highlighter.getLoadedLanguages();
174
- const langToUse = availableLangs.includes(language) ? language : "text";
175
- const result = highlighter.codeToTokens(code, {
176
- lang: langToUse,
177
- themes: {
178
- dark: "github-dark",
179
- light: "github-light",
180
- },
181
- });
182
- const tokenized: TokenizedCode = {
183
- bg: result.bg ?? "transparent",
184
- fg: result.fg ?? "inherit",
185
- tokens: result.tokens,
186
- };
187
- // Cache the result
188
- tokensCache.set(tokensCacheKey, tokenized);
189
- // Notify all subscribers
190
- const subs = subscribers.get(tokensCacheKey);
191
- if (subs) {
192
- for (const sub of subs) {
193
- sub(tokenized);
194
- }
195
- subscribers.delete(tokensCacheKey);
196
- }
197
- })
198
- // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)
199
- .catch((error) => {
200
- console.error("Failed to highlight code:", error);
201
- subscribers.delete(tokensCacheKey);
202
- });
203
- return null;
204
- };
205
- // Line number styles using CSS counters
206
- const LINE_NUMBER_CLASSES = cn(
207
- "block",
208
- "before:content-[counter(line)]",
209
- "before:inline-block",
210
- "before:[counter-increment:line]",
211
- "before:w-8",
212
- "before:mr-4",
213
- "before:text-right",
214
- "before:text-muted/50",
215
- "before:font-mono",
216
- "before:select-none",
217
- );
218
- function CodeBlockBody({
219
- tokenized,
220
- showLineNumbers,
221
- className,
222
- }: {
223
- tokenized: TokenizedCode;
224
- showLineNumbers: boolean;
225
- className?: string;
226
- }) {
227
- const preStyle = {
228
- backgroundColor: tokenized.bg,
229
- color: tokenized.fg,
230
- };
231
- const keyedLines = addKeysToTokens(tokenized.tokens);
232
- return (
233
- <pre
234
- className={cn(
235
- "dark:bg-(--shiki-dark-bg)! dark:text-(--shiki-dark)! m-0 p-4 text-sm",
236
- className,
237
- )}
238
- style={preStyle}
239
- >
240
- <code
241
- className={cn(
242
- "font-mono text-sm",
243
- showLineNumbers && "[counter-increment:line_0] [counter-reset:line]",
244
- )}
245
- >
246
- {keyedLines.map((keyedLine) => (
247
- <LineSpan
248
- key={keyedLine.key}
249
- keyedLine={keyedLine}
250
- showLineNumbers={showLineNumbers}
251
- />
252
- ))}
253
- </code>
254
- </pre>
255
- );
256
- }
257
- export const CodeBlockContainer = ({
258
- className,
259
- language,
260
- style,
261
- ...props
262
- }: HTMLAttributes<HTMLDivElement> & {
263
- language: string;
264
- }) => (
265
- <div
266
- className={cn(
267
- "relative w-full overflow-hidden rounded-lg bg-surface-secondary/60 text-foreground",
268
- className,
269
- )}
270
- data-language={language}
271
- style={{
272
- containIntrinsicSize: "auto 200px",
273
- contentVisibility: "auto",
274
- ...style,
275
- }}
276
- {...props}
277
- />
278
- );
279
- export const CodeBlockHeader = ({
280
- children,
281
- className,
282
- ...props
283
- }: HTMLAttributes<HTMLDivElement>) => (
284
- <div
285
- className={cn(
286
- cn(
287
- flatPanelSubtleClass,
288
- "flex items-center justify-between px-3 py-2 text-muted text-xs",
289
- ),
290
- className,
291
- )}
292
- {...props}
293
- >
294
- {children}
295
- </div>
296
- );
297
- export const CodeBlockTitle = ({
298
- children,
299
- className,
300
- ...props
301
- }: HTMLAttributes<HTMLDivElement>) => (
302
- <div className={cn("flex items-center gap-2", className)} {...props}>
303
- {children}
304
- </div>
305
- );
306
- export const CodeBlockFilename = ({
307
- children,
308
- className,
309
- ...props
310
- }: HTMLAttributes<HTMLSpanElement>) => (
311
- <span className={cn("font-mono", className)} {...props}>
312
- {children}
313
- </span>
314
- );
315
- export const CodeBlockActions = ({
316
- children,
317
- className,
318
- ...props
319
- }: HTMLAttributes<HTMLDivElement>) => (
320
- <div
321
- className={cn("-my-1 -mr-1 flex items-center gap-2", className)}
322
- {...props}
323
- >
324
- {children}
325
- </div>
326
- );
327
- export const CodeBlockContent = ({
328
- code,
329
- language,
330
- showLineNumbers = false,
331
- }: {
332
- code: string;
333
- language: string;
334
- showLineNumbers?: boolean;
335
- }) => {
336
- // Memoized raw tokens for immediate display
337
- const rawTokens = createRawTokens(code);
338
- // Try to get cached result synchronously, otherwise use raw tokens
339
- const [tokenized, setTokenized] = useState<TokenizedCode>(
340
- () => highlightCode(code, language) ?? rawTokens,
341
- );
342
- useEffect(() => {
343
- let cancelled = false;
344
- // Reset to raw tokens when code changes (shows current code, not stale tokens)
345
- setTokenized(highlightCode(code, language) ?? rawTokens);
346
- // Subscribe to async highlighting result
347
- highlightCode(code, language, (result) => {
348
- if (!cancelled) {
349
- setTokenized(result);
350
- }
351
- });
352
- return () => {
353
- cancelled = true;
354
- };
355
- }, [code, language, rawTokens]);
356
- return (
357
- <div className="relative overflow-auto">
358
- <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />
359
- </div>
360
- );
361
- };
362
- export const CodeBlock = ({
363
- code,
364
- language,
365
- showLineNumbers = false,
366
- className,
367
- children,
368
- ...props
369
- }: CodeBlockProps) => {
370
- const contextValue = { code };
371
- return (
372
- <CodeBlockContext.Provider value={contextValue}>
373
- <CodeBlockContainer className={className} language={language} {...props}>
374
- {children}
375
- <CodeBlockContent
376
- code={code}
377
- language={language}
378
- showLineNumbers={showLineNumbers}
379
- />
380
- </CodeBlockContainer>
381
- </CodeBlockContext.Provider>
382
- );
383
- };
384
- export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
385
- onCopy?: () => void;
386
- onError?: (error: Error) => void;
387
- timeout?: number;
388
- };
389
- export const CodeBlockCopyButton = ({
390
- onCopy,
391
- onError,
392
- timeout = 2000,
393
- children,
394
- className,
395
- ...props
396
- }: CodeBlockCopyButtonProps) => {
397
- const [isCopied, setIsCopied] = useState(false);
398
- const timeoutRef = useRef<number>(0);
399
- const { code } = useContext(CodeBlockContext);
400
- const copyToClipboard = async () => {
401
- if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
402
- onError?.(new Error("Clipboard API not available"));
403
- return;
404
- }
405
- try {
406
- if (!isCopied) {
407
- await navigator.clipboard.writeText(code);
408
- setIsCopied(true);
409
- onCopy?.();
410
- timeoutRef.current = window.setTimeout(
411
- () => setIsCopied(false),
412
- timeout,
413
- );
414
- }
415
- } catch (error) {
416
- onError?.(error as Error);
417
- }
418
- };
419
- useEffect(
420
- () => () => {
421
- window.clearTimeout(timeoutRef.current);
422
- },
423
- [],
424
- );
425
- const Icon = isCopied ? IconCheck : IconCopy;
426
- return (
427
- <Button
428
- className={cn("shrink-0", className)}
429
- onClick={copyToClipboard}
430
- size="icon"
431
- variant="ghost"
432
- {...props}
433
- >
434
- {children ?? <Icon size={14} />}
435
- </Button>
436
- );
437
- };
438
- export type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;
439
- export const CodeBlockLanguageSelector = (
440
- props: CodeBlockLanguageSelectorProps,
441
- ) => <Select {...props} />;
442
- export type CodeBlockLanguageSelectorTriggerProps = ComponentProps<
443
- typeof SelectTrigger
444
- >;
445
- export const CodeBlockLanguageSelectorTrigger = ({
446
- className,
447
- ...props
448
- }: CodeBlockLanguageSelectorTriggerProps) => (
449
- <SelectTrigger
450
- className={cn(
451
- "h-7 border-none bg-transparent px-2 text-xs shadow-none",
452
- className,
453
- )}
454
- size="sm"
455
- {...props}
456
- />
457
- );
458
- export type CodeBlockLanguageSelectorValueProps = ComponentProps<
459
- typeof SelectValue
460
- >;
461
- export const CodeBlockLanguageSelectorValue = (
462
- props: CodeBlockLanguageSelectorValueProps,
463
- ) => <SelectValue {...props} />;
464
- export type CodeBlockLanguageSelectorContentProps = ComponentProps<
465
- typeof SelectContent
466
- >;
467
- export const CodeBlockLanguageSelectorContent = ({
468
- align = "end",
469
- ...props
470
- }: CodeBlockLanguageSelectorContentProps) => (
471
- <SelectContent align={align} {...props} />
472
- );
473
- export type CodeBlockLanguageSelectorItemProps = ComponentProps<
474
- typeof SelectItem
475
- >;
476
- export const CodeBlockLanguageSelectorItem = (
477
- props: CodeBlockLanguageSelectorItemProps,
478
- ) => <SelectItem {...props} />;
1
+ "use client";
2
+ import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
3
+ import type {
4
+ BundledLanguage,
5
+ BundledTheme,
6
+ HighlighterGeneric,
7
+ ThemedToken,
8
+ } from "shiki";
9
+ import { Button } from "../ui/button";
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from "../ui/radix-select";
17
+ import { cn } from "../utils/cn";
18
+ import { flatPanelSubtleClass } from "../ui/surface-classes";
19
+ import { IconCheck, IconCopy } from "@tabler/icons-react";
20
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
21
+ import { bundledLanguages, createHighlighter } from "shiki";
22
+ const isBundledLanguage = (lang: string): lang is BundledLanguage =>
23
+ lang in bundledLanguages;
24
+ // Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline
25
+ // biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
26
+ // eslint-disable-next-line no-bitwise -- shiki bitflag check
27
+ const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;
28
+ // biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
29
+ // eslint-disable-next-line no-bitwise -- shiki bitflag check
30
+ // oxlint-disable-next-line eslint(no-bitwise)
31
+ const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;
32
+ const isUnderline = (fontStyle: number | undefined) =>
33
+ // biome-ignore lint/suspicious/noBitwiseOperators: shiki bitflag check
34
+ // oxlint-disable-next-line eslint(no-bitwise)
35
+ fontStyle && fontStyle & 4;
36
+ // Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint
37
+ interface KeyedToken {
38
+ token: ThemedToken;
39
+ key: string;
40
+ }
41
+ interface KeyedLine {
42
+ tokens: KeyedToken[];
43
+ key: string;
44
+ }
45
+ const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>
46
+ lines.map((line, lineIdx) => ({
47
+ key: `line-${lineIdx}`,
48
+ tokens: line.map((token, tokenIdx) => ({
49
+ key: `line-${lineIdx}-${tokenIdx}`,
50
+ token,
51
+ })),
52
+ }));
53
+ // Token rendering component
54
+ const TokenSpan = ({ token }: { token: ThemedToken }) => (
55
+ <span
56
+ className="dark:bg-(--shiki-dark-bg)! dark:text-(--shiki-dark)!"
57
+ style={
58
+ {
59
+ backgroundColor: token.bgColor,
60
+ color: token.color,
61
+ fontStyle: isItalic(token.fontStyle) ? "italic" : undefined,
62
+ fontWeight: isBold(token.fontStyle) ? "bold" : undefined,
63
+ textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined,
64
+ ...token.htmlStyle,
65
+ } as CSSProperties
66
+ }
67
+ >
68
+ {token.content}
69
+ </span>
70
+ );
71
+ // Line rendering component
72
+ const LineSpan = ({
73
+ keyedLine,
74
+ showLineNumbers,
75
+ }: {
76
+ keyedLine: KeyedLine;
77
+ showLineNumbers: boolean;
78
+ }) => (
79
+ <span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}>
80
+ {keyedLine.tokens.length === 0
81
+ ? "\n"
82
+ : keyedLine.tokens.map(({ token, key }) => (
83
+ <TokenSpan key={key} token={token} />
84
+ ))}
85
+ </span>
86
+ );
87
+ // Types
88
+ type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
89
+ code: string;
90
+ language: string;
91
+ showLineNumbers?: boolean;
92
+ };
93
+ interface TokenizedCode {
94
+ tokens: ThemedToken[][];
95
+ fg: string;
96
+ bg: string;
97
+ }
98
+ interface CodeBlockContextType {
99
+ code: string;
100
+ }
101
+ // Context
102
+ const CodeBlockContext = createContext<CodeBlockContextType>({
103
+ code: "",
104
+ });
105
+ // Highlighter cache (singleton per language)
106
+ const highlighterCache = new Map<
107
+ string,
108
+ Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
109
+ >();
110
+ // Token cache
111
+ const tokensCache = new Map<string, TokenizedCode>();
112
+ // Subscribers for async token updates
113
+ const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
114
+ const getTokensCacheKey = (code: string, language: BundledLanguage) => {
115
+ const start = code.slice(0, 100);
116
+ const end = code.length > 100 ? code.slice(-100) : "";
117
+ return `${language}:${code.length}:${start}:${end}`;
118
+ };
119
+ const getHighlighter = (
120
+ language: BundledLanguage,
121
+ ): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
122
+ const cached = highlighterCache.get(language);
123
+ if (cached) {
124
+ return cached;
125
+ }
126
+ const highlighterPromise = createHighlighter({
127
+ langs: [language],
128
+ themes: ["github-light", "github-dark"],
129
+ });
130
+ highlighterCache.set(language, highlighterPromise);
131
+ return highlighterPromise;
132
+ };
133
+ // Create raw tokens for immediate display while highlighting loads
134
+ const createRawTokens = (code: string): TokenizedCode => ({
135
+ bg: "transparent",
136
+ fg: "inherit",
137
+ tokens: code.split("\n").map((line) =>
138
+ line === ""
139
+ ? []
140
+ : [
141
+ {
142
+ color: "inherit",
143
+ content: line,
144
+ } as ThemedToken,
145
+ ],
146
+ ),
147
+ });
148
+ // Synchronous highlight with callback for async results
149
+ export const highlightCode = (
150
+ code: string,
151
+ language: string,
152
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
153
+ callback?: (result: TokenizedCode) => void,
154
+ ): TokenizedCode | null => {
155
+ if (!isBundledLanguage(language)) return null;
156
+ const tokensCacheKey = getTokensCacheKey(code, language);
157
+ // Return cached result if available
158
+ const cached = tokensCache.get(tokensCacheKey);
159
+ if (cached) {
160
+ return cached;
161
+ }
162
+ // Subscribe callback if provided
163
+ if (callback) {
164
+ if (!subscribers.has(tokensCacheKey)) {
165
+ subscribers.set(tokensCacheKey, new Set());
166
+ }
167
+ subscribers.get(tokensCacheKey)?.add(callback);
168
+ }
169
+ // Start highlighting in background - fire-and-forget async pattern
170
+ getHighlighter(language)
171
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
172
+ .then((highlighter) => {
173
+ const availableLangs = highlighter.getLoadedLanguages();
174
+ const langToUse = availableLangs.includes(language) ? language : "text";
175
+ const result = highlighter.codeToTokens(code, {
176
+ lang: langToUse,
177
+ themes: {
178
+ dark: "github-dark",
179
+ light: "github-light",
180
+ },
181
+ });
182
+ const tokenized: TokenizedCode = {
183
+ bg: result.bg ?? "transparent",
184
+ fg: result.fg ?? "inherit",
185
+ tokens: result.tokens,
186
+ };
187
+ // Cache the result
188
+ tokensCache.set(tokensCacheKey, tokenized);
189
+ // Notify all subscribers
190
+ const subs = subscribers.get(tokensCacheKey);
191
+ if (subs) {
192
+ for (const sub of subs) {
193
+ sub(tokenized);
194
+ }
195
+ subscribers.delete(tokensCacheKey);
196
+ }
197
+ })
198
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)
199
+ .catch((error) => {
200
+ console.error("Failed to highlight code:", error);
201
+ subscribers.delete(tokensCacheKey);
202
+ });
203
+ return null;
204
+ };
205
+ // Line number styles using CSS counters
206
+ const LINE_NUMBER_CLASSES = cn(
207
+ "block",
208
+ "before:content-[counter(line)]",
209
+ "before:inline-block",
210
+ "before:[counter-increment:line]",
211
+ "before:w-8",
212
+ "before:mr-4",
213
+ "before:text-right",
214
+ "before:text-muted/50",
215
+ "before:font-mono",
216
+ "before:select-none",
217
+ );
218
+ function CodeBlockBody({
219
+ tokenized,
220
+ showLineNumbers,
221
+ className,
222
+ }: {
223
+ tokenized: TokenizedCode;
224
+ showLineNumbers: boolean;
225
+ className?: string;
226
+ }) {
227
+ const preStyle = {
228
+ backgroundColor: tokenized.bg,
229
+ color: tokenized.fg,
230
+ };
231
+ const keyedLines = addKeysToTokens(tokenized.tokens);
232
+ return (
233
+ <pre
234
+ className={cn(
235
+ "dark:bg-(--shiki-dark-bg)! dark:text-(--shiki-dark)! m-0 p-4 text-sm",
236
+ className,
237
+ )}
238
+ style={preStyle}
239
+ >
240
+ <code
241
+ className={cn(
242
+ "font-mono text-sm",
243
+ showLineNumbers && "[counter-increment:line_0] [counter-reset:line]",
244
+ )}
245
+ >
246
+ {keyedLines.map((keyedLine) => (
247
+ <LineSpan
248
+ key={keyedLine.key}
249
+ keyedLine={keyedLine}
250
+ showLineNumbers={showLineNumbers}
251
+ />
252
+ ))}
253
+ </code>
254
+ </pre>
255
+ );
256
+ }
257
+ export const CodeBlockContainer = ({
258
+ className,
259
+ language,
260
+ style,
261
+ ...props
262
+ }: HTMLAttributes<HTMLDivElement> & {
263
+ language: string;
264
+ }) => (
265
+ <div
266
+ className={cn(
267
+ "relative w-full overflow-hidden rounded-lg bg-surface-secondary/60 text-foreground",
268
+ className,
269
+ )}
270
+ data-language={language}
271
+ style={{
272
+ containIntrinsicSize: "auto 200px",
273
+ contentVisibility: "auto",
274
+ ...style,
275
+ }}
276
+ {...props}
277
+ />
278
+ );
279
+ export const CodeBlockHeader = ({
280
+ children,
281
+ className,
282
+ ...props
283
+ }: HTMLAttributes<HTMLDivElement>) => (
284
+ <div
285
+ className={cn(
286
+ cn(
287
+ flatPanelSubtleClass,
288
+ "flex items-center justify-between px-3 py-2 text-muted text-xs",
289
+ ),
290
+ className,
291
+ )}
292
+ {...props}
293
+ >
294
+ {children}
295
+ </div>
296
+ );
297
+ export const CodeBlockTitle = ({
298
+ children,
299
+ className,
300
+ ...props
301
+ }: HTMLAttributes<HTMLDivElement>) => (
302
+ <div className={cn("flex items-center gap-2", className)} {...props}>
303
+ {children}
304
+ </div>
305
+ );
306
+ export const CodeBlockFilename = ({
307
+ children,
308
+ className,
309
+ ...props
310
+ }: HTMLAttributes<HTMLSpanElement>) => (
311
+ <span className={cn("font-mono", className)} {...props}>
312
+ {children}
313
+ </span>
314
+ );
315
+ export const CodeBlockActions = ({
316
+ children,
317
+ className,
318
+ ...props
319
+ }: HTMLAttributes<HTMLDivElement>) => (
320
+ <div
321
+ className={cn("-my-1 -mr-1 flex items-center gap-2", className)}
322
+ {...props}
323
+ >
324
+ {children}
325
+ </div>
326
+ );
327
+ export const CodeBlockContent = ({
328
+ code,
329
+ language,
330
+ showLineNumbers = false,
331
+ }: {
332
+ code: string;
333
+ language: string;
334
+ showLineNumbers?: boolean;
335
+ }) => {
336
+ // Memoized raw tokens for immediate display
337
+ const rawTokens = createRawTokens(code);
338
+ // Try to get cached result synchronously, otherwise use raw tokens
339
+ const [tokenized, setTokenized] = useState<TokenizedCode>(
340
+ () => highlightCode(code, language) ?? rawTokens,
341
+ );
342
+ useEffect(() => {
343
+ let cancelled = false;
344
+ // Reset to raw tokens when code changes (shows current code, not stale tokens)
345
+ setTokenized(highlightCode(code, language) ?? rawTokens);
346
+ // Subscribe to async highlighting result
347
+ highlightCode(code, language, (result) => {
348
+ if (!cancelled) {
349
+ setTokenized(result);
350
+ }
351
+ });
352
+ return () => {
353
+ cancelled = true;
354
+ };
355
+ }, [code, language, rawTokens]);
356
+ return (
357
+ <div className="relative overflow-auto">
358
+ <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />
359
+ </div>
360
+ );
361
+ };
362
+ export const CodeBlock = ({
363
+ code,
364
+ language,
365
+ showLineNumbers = false,
366
+ className,
367
+ children,
368
+ ...props
369
+ }: CodeBlockProps) => {
370
+ const contextValue = { code };
371
+ return (
372
+ <CodeBlockContext.Provider value={contextValue}>
373
+ <CodeBlockContainer className={className} language={language} {...props}>
374
+ {children}
375
+ <CodeBlockContent
376
+ code={code}
377
+ language={language}
378
+ showLineNumbers={showLineNumbers}
379
+ />
380
+ </CodeBlockContainer>
381
+ </CodeBlockContext.Provider>
382
+ );
383
+ };
384
+ export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
385
+ onCopy?: () => void;
386
+ onError?: (error: Error) => void;
387
+ timeout?: number;
388
+ };
389
+ export const CodeBlockCopyButton = ({
390
+ onCopy,
391
+ onError,
392
+ timeout = 2000,
393
+ children,
394
+ className,
395
+ ...props
396
+ }: CodeBlockCopyButtonProps) => {
397
+ const [isCopied, setIsCopied] = useState(false);
398
+ const timeoutRef = useRef<number>(0);
399
+ const { code } = useContext(CodeBlockContext);
400
+ const copyToClipboard = async () => {
401
+ if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
402
+ onError?.(new Error("Clipboard API not available"));
403
+ return;
404
+ }
405
+ try {
406
+ if (!isCopied) {
407
+ await navigator.clipboard.writeText(code);
408
+ setIsCopied(true);
409
+ onCopy?.();
410
+ timeoutRef.current = window.setTimeout(
411
+ () => setIsCopied(false),
412
+ timeout,
413
+ );
414
+ }
415
+ } catch (error) {
416
+ onError?.(error as Error);
417
+ }
418
+ };
419
+ useEffect(
420
+ () => () => {
421
+ window.clearTimeout(timeoutRef.current);
422
+ },
423
+ [],
424
+ );
425
+ const Icon = isCopied ? IconCheck : IconCopy;
426
+ return (
427
+ <Button
428
+ className={cn("shrink-0", className)}
429
+ onClick={copyToClipboard}
430
+ size="icon"
431
+ variant="ghost"
432
+ {...props}
433
+ >
434
+ {children ?? <Icon size={14} />}
435
+ </Button>
436
+ );
437
+ };
438
+ export type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;
439
+ export const CodeBlockLanguageSelector = (
440
+ props: CodeBlockLanguageSelectorProps,
441
+ ) => <Select {...props} />;
442
+ export type CodeBlockLanguageSelectorTriggerProps = ComponentProps<
443
+ typeof SelectTrigger
444
+ >;
445
+ export const CodeBlockLanguageSelectorTrigger = ({
446
+ className,
447
+ ...props
448
+ }: CodeBlockLanguageSelectorTriggerProps) => (
449
+ <SelectTrigger
450
+ className={cn(
451
+ "h-7 border-none bg-transparent px-2 text-xs shadow-none",
452
+ className,
453
+ )}
454
+ size="sm"
455
+ {...props}
456
+ />
457
+ );
458
+ export type CodeBlockLanguageSelectorValueProps = ComponentProps<
459
+ typeof SelectValue
460
+ >;
461
+ export const CodeBlockLanguageSelectorValue = (
462
+ props: CodeBlockLanguageSelectorValueProps,
463
+ ) => <SelectValue {...props} />;
464
+ export type CodeBlockLanguageSelectorContentProps = ComponentProps<
465
+ typeof SelectContent
466
+ >;
467
+ export const CodeBlockLanguageSelectorContent = ({
468
+ align = "end",
469
+ ...props
470
+ }: CodeBlockLanguageSelectorContentProps) => (
471
+ <SelectContent align={align} {...props} />
472
+ );
473
+ export type CodeBlockLanguageSelectorItemProps = ComponentProps<
474
+ typeof SelectItem
475
+ >;
476
+ export const CodeBlockLanguageSelectorItem = (
477
+ props: CodeBlockLanguageSelectorItemProps,
478
+ ) => <SelectItem {...props} />;