veslx 0.0.1

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 (83) hide show
  1. package/README.md +3 -0
  2. package/bin/lib/import-config.ts +13 -0
  3. package/bin/lib/init.ts +31 -0
  4. package/bin/lib/serve.ts +35 -0
  5. package/bin/lib/start.ts +40 -0
  6. package/bin/lib/stop.ts +24 -0
  7. package/bin/vesl.ts +41 -0
  8. package/components.json +20 -0
  9. package/eslint.config.js +23 -0
  10. package/index.html +17 -0
  11. package/package.json +89 -0
  12. package/plugin/README.md +21 -0
  13. package/plugin/package.json +26 -0
  14. package/plugin/src/cli.ts +30 -0
  15. package/plugin/src/client.tsx +224 -0
  16. package/plugin/src/lib.ts +268 -0
  17. package/plugin/src/plugin.ts +109 -0
  18. package/postcss.config.js +5 -0
  19. package/public/logo_dark.png +0 -0
  20. package/public/logo_light.png +0 -0
  21. package/src/App.tsx +21 -0
  22. package/src/components/front-matter.tsx +53 -0
  23. package/src/components/gallery/components/figure-caption.tsx +15 -0
  24. package/src/components/gallery/components/figure-header.tsx +20 -0
  25. package/src/components/gallery/components/lightbox.tsx +106 -0
  26. package/src/components/gallery/components/loading-image.tsx +48 -0
  27. package/src/components/gallery/hooks/use-gallery-images.ts +103 -0
  28. package/src/components/gallery/hooks/use-lightbox.ts +40 -0
  29. package/src/components/gallery/index.tsx +134 -0
  30. package/src/components/gallery/lib/render-math-in-text.tsx +47 -0
  31. package/src/components/header.tsx +68 -0
  32. package/src/components/index.ts +5 -0
  33. package/src/components/loading.tsx +16 -0
  34. package/src/components/mdx-components.tsx +163 -0
  35. package/src/components/mode-toggle.tsx +44 -0
  36. package/src/components/page-error.tsx +59 -0
  37. package/src/components/parameter-badge.tsx +78 -0
  38. package/src/components/parameter-table.tsx +420 -0
  39. package/src/components/post-list.tsx +148 -0
  40. package/src/components/running-bar.tsx +21 -0
  41. package/src/components/runtime-mdx.tsx +82 -0
  42. package/src/components/slide.tsx +11 -0
  43. package/src/components/theme-provider.tsx +6 -0
  44. package/src/components/ui/badge.tsx +36 -0
  45. package/src/components/ui/breadcrumb.tsx +115 -0
  46. package/src/components/ui/button.tsx +56 -0
  47. package/src/components/ui/card.tsx +79 -0
  48. package/src/components/ui/carousel.tsx +260 -0
  49. package/src/components/ui/dropdown-menu.tsx +198 -0
  50. package/src/components/ui/input.tsx +22 -0
  51. package/src/components/ui/kbd.tsx +22 -0
  52. package/src/components/ui/select.tsx +158 -0
  53. package/src/components/ui/separator.tsx +29 -0
  54. package/src/components/ui/shadcn-io/code-block/index.tsx +620 -0
  55. package/src/components/ui/shadcn-io/code-block/server.tsx +63 -0
  56. package/src/components/ui/sheet.tsx +140 -0
  57. package/src/components/ui/sidebar.tsx +771 -0
  58. package/src/components/ui/skeleton.tsx +15 -0
  59. package/src/components/ui/spinner.tsx +16 -0
  60. package/src/components/ui/tooltip.tsx +28 -0
  61. package/src/components/welcome.tsx +21 -0
  62. package/src/hooks/use-key-bindings.ts +72 -0
  63. package/src/hooks/use-mobile.tsx +19 -0
  64. package/src/index.css +279 -0
  65. package/src/lib/constants.ts +10 -0
  66. package/src/lib/format-date.tsx +6 -0
  67. package/src/lib/format-file-size.ts +10 -0
  68. package/src/lib/parameter-utils.ts +134 -0
  69. package/src/lib/utils.ts +6 -0
  70. package/src/main.tsx +10 -0
  71. package/src/pages/home.tsx +39 -0
  72. package/src/pages/post.tsx +65 -0
  73. package/src/pages/slides.tsx +173 -0
  74. package/tailwind.config.js +136 -0
  75. package/test-content/.vesl.json +49 -0
  76. package/test-content/README.md +33 -0
  77. package/test-content/test-post/README.mdx +7 -0
  78. package/test-content/test-slides/SLIDES.mdx +8 -0
  79. package/tsconfig.app.json +32 -0
  80. package/tsconfig.json +15 -0
  81. package/tsconfig.node.json +25 -0
  82. package/vesl.config.ts +4 -0
  83. package/vite.config.ts +54 -0
@@ -0,0 +1,620 @@
1
+ 'use client';
2
+
3
+ import {
4
+ type IconType,
5
+ SiAstro,
6
+ SiBiome,
7
+ SiBower,
8
+ SiBun,
9
+ SiC,
10
+ SiCircleci,
11
+ SiCoffeescript,
12
+ SiCplusplus,
13
+ SiCss,
14
+ SiCssmodules,
15
+ SiDart,
16
+ SiDocker,
17
+ SiDocusaurus,
18
+ SiDotenv,
19
+ SiEditorconfig,
20
+ SiEslint,
21
+ SiGatsby,
22
+ SiGitignoredotio,
23
+ SiGnubash,
24
+ SiGo,
25
+ SiGraphql,
26
+ SiGrunt,
27
+ SiGulp,
28
+ SiHandlebarsdotjs,
29
+ SiHtml5,
30
+ SiJavascript,
31
+ SiJest,
32
+ SiJson,
33
+ SiLess,
34
+ SiMarkdown,
35
+ SiMdx,
36
+ SiMintlify,
37
+ SiMocha,
38
+ SiMysql,
39
+ SiNextdotjs,
40
+ SiPerl,
41
+ SiPhp,
42
+ SiPostcss,
43
+ SiPrettier,
44
+ SiPrisma,
45
+ SiPug,
46
+ SiPython,
47
+ SiR,
48
+ SiReact,
49
+ SiReadme,
50
+ SiRedis,
51
+ SiRemix,
52
+ SiRive,
53
+ SiRollupdotjs,
54
+ SiRuby,
55
+ SiSanity,
56
+ SiSass,
57
+ SiScala,
58
+ SiSentry,
59
+ SiShadcnui,
60
+ SiStorybook,
61
+ SiStylelint,
62
+ SiSublimetext,
63
+ SiSvelte,
64
+ SiSvg,
65
+ SiSwift,
66
+ SiTailwindcss,
67
+ SiToml,
68
+ SiTypescript,
69
+ SiVercel,
70
+ SiVite,
71
+ SiVuedotjs,
72
+ SiWebassembly,
73
+ } from '@icons-pack/react-simple-icons';
74
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
75
+ import { CheckIcon, CopyIcon } from 'lucide-react';
76
+ import type {
77
+ ComponentProps,
78
+ HTMLAttributes,
79
+ ReactElement,
80
+ ReactNode,
81
+ } from 'react';
82
+ import {
83
+ cloneElement,
84
+ createContext,
85
+ useContext,
86
+ useEffect,
87
+ useState,
88
+ } from 'react';
89
+ import { Button } from '@/components/ui/button';
90
+ import {
91
+ Select,
92
+ SelectContent,
93
+ SelectItem,
94
+ SelectTrigger,
95
+ SelectValue,
96
+ } from '@/components/ui/select';
97
+ import { cn } from '@/lib/utils';
98
+
99
+ export type BundledLanguage = string;
100
+
101
+ const filenameIconMap = {
102
+ '.env': SiDotenv,
103
+ '*.astro': SiAstro,
104
+ 'biome.json': SiBiome,
105
+ '.bowerrc': SiBower,
106
+ 'bun.lockb': SiBun,
107
+ '*.c': SiC,
108
+ '*.cpp': SiCplusplus,
109
+ '.circleci/config.yml': SiCircleci,
110
+ '*.coffee': SiCoffeescript,
111
+ '*.module.css': SiCssmodules,
112
+ '*.css': SiCss,
113
+ '*.dart': SiDart,
114
+ Dockerfile: SiDocker,
115
+ 'docusaurus.config.js': SiDocusaurus,
116
+ '.editorconfig': SiEditorconfig,
117
+ '.eslintrc': SiEslint,
118
+ 'eslint.config.*': SiEslint,
119
+ 'gatsby-config.*': SiGatsby,
120
+ '.gitignore': SiGitignoredotio,
121
+ '*.go': SiGo,
122
+ '*.graphql': SiGraphql,
123
+ '*.sh': SiGnubash,
124
+ 'Gruntfile.*': SiGrunt,
125
+ 'gulpfile.*': SiGulp,
126
+ '*.hbs': SiHandlebarsdotjs,
127
+ '*.html': SiHtml5,
128
+ '*.js': SiJavascript,
129
+ '*.json': SiJson,
130
+ '*.test.js': SiJest,
131
+ '*.less': SiLess,
132
+ '*.md': SiMarkdown,
133
+ '*.mdx': SiMdx,
134
+ 'mintlify.json': SiMintlify,
135
+ 'mocha.opts': SiMocha,
136
+ '*.mustache': SiHandlebarsdotjs,
137
+ '*.sql': SiMysql,
138
+ 'next.config.*': SiNextdotjs,
139
+ '*.pl': SiPerl,
140
+ '*.php': SiPhp,
141
+ 'postcss.config.*': SiPostcss,
142
+ 'prettier.config.*': SiPrettier,
143
+ '*.prisma': SiPrisma,
144
+ '*.pug': SiPug,
145
+ '*.py': SiPython,
146
+ '*.r': SiR,
147
+ '*.rb': SiRuby,
148
+ '*.jsx': SiReact,
149
+ '*.tsx': SiReact,
150
+ 'readme.md': SiReadme,
151
+ '*.rdb': SiRedis,
152
+ 'remix.config.*': SiRemix,
153
+ '*.riv': SiRive,
154
+ 'rollup.config.*': SiRollupdotjs,
155
+ 'sanity.config.*': SiSanity,
156
+ '*.sass': SiSass,
157
+ '*.scss': SiSass,
158
+ '*.sc': SiScala,
159
+ '*.scala': SiScala,
160
+ 'sentry.client.config.*': SiSentry,
161
+ 'components.json': SiShadcnui,
162
+ 'storybook.config.*': SiStorybook,
163
+ 'stylelint.config.*': SiStylelint,
164
+ '.sublime-settings': SiSublimetext,
165
+ '*.svelte': SiSvelte,
166
+ '*.svg': SiSvg,
167
+ '*.swift': SiSwift,
168
+ 'tailwind.config.*': SiTailwindcss,
169
+ '*.toml': SiToml,
170
+ '*.ts': SiTypescript,
171
+ 'vercel.json': SiVercel,
172
+ 'vite.config.*': SiVite,
173
+ '*.vue': SiVuedotjs,
174
+ '*.wasm': SiWebassembly,
175
+ };
176
+
177
+ const lineNumberClassNames = cn(
178
+ '[&_code]:[counter-reset:line]',
179
+ '[&_code]:[counter-increment:line_0]',
180
+ '[&_.line]:before:content-[counter(line)]',
181
+ '[&_.line]:before:inline-block',
182
+ '[&_.line]:before:[counter-increment:line]',
183
+ '[&_.line]:before:w-4',
184
+ '[&_.line]:before:mr-4',
185
+ '[&_.line]:before:text-[13px]',
186
+ '[&_.line]:before:text-right',
187
+ '[&_.line]:before:text-muted-foreground/50',
188
+ '[&_.line]:before:font-mono',
189
+ '[&_.line]:before:select-none'
190
+ );
191
+
192
+ const darkModeClassNames = cn(
193
+ 'dark:[&_.shiki]:!text-[var(--shiki-dark)]',
194
+ 'dark:[&_.shiki]:!bg-[var(--shiki-dark-bg)]',
195
+ 'dark:[&_.shiki]:![font-style:var(--shiki-dark-font-style)]',
196
+ 'dark:[&_.shiki]:![font-weight:var(--shiki-dark-font-weight)]',
197
+ 'dark:[&_.shiki]:![text-decoration:var(--shiki-dark-text-decoration)]',
198
+ 'dark:[&_.shiki_span]:!text-[var(--shiki-dark)]',
199
+ 'dark:[&_.shiki_span]:![font-style:var(--shiki-dark-font-style)]',
200
+ 'dark:[&_.shiki_span]:![font-weight:var(--shiki-dark-font-weight)]',
201
+ 'dark:[&_.shiki_span]:![text-decoration:var(--shiki-dark-text-decoration)]'
202
+ );
203
+
204
+ const lineHighlightClassNames = cn(
205
+ '[&_.line.highlighted]:bg-blue-50',
206
+ '[&_.line.highlighted]:after:bg-blue-500',
207
+ '[&_.line.highlighted]:after:absolute',
208
+ '[&_.line.highlighted]:after:left-0',
209
+ '[&_.line.highlighted]:after:top-0',
210
+ '[&_.line.highlighted]:after:bottom-0',
211
+ '[&_.line.highlighted]:after:w-0.5',
212
+ 'dark:[&_.line.highlighted]:!bg-blue-500/10'
213
+ );
214
+
215
+ const lineDiffClassNames = cn(
216
+ '[&_.line.diff]:after:absolute',
217
+ '[&_.line.diff]:after:left-0',
218
+ '[&_.line.diff]:after:top-0',
219
+ '[&_.line.diff]:after:bottom-0',
220
+ '[&_.line.diff]:after:w-0.5',
221
+ '[&_.line.diff.add]:bg-emerald-50',
222
+ '[&_.line.diff.add]:after:bg-emerald-500',
223
+ '[&_.line.diff.remove]:bg-rose-50',
224
+ '[&_.line.diff.remove]:after:bg-rose-500',
225
+ 'dark:[&_.line.diff.add]:!bg-emerald-500/10',
226
+ 'dark:[&_.line.diff.remove]:!bg-rose-500/10'
227
+ );
228
+
229
+ const lineFocusedClassNames = cn(
230
+ '[&_code:has(.focused)_.line]:blur-[2px]',
231
+ '[&_code:has(.focused)_.line.focused]:blur-none'
232
+ );
233
+
234
+ const wordHighlightClassNames = cn(
235
+ '[&_.highlighted-word]:bg-blue-50',
236
+ 'dark:[&_.highlighted-word]:!bg-blue-500/10'
237
+ );
238
+
239
+ const codeBlockClassName = cn(
240
+ 'mt-0 bg-background text-sm',
241
+ '[&_pre]:py-4',
242
+ '[&_.shiki]:!bg-[var(--shiki-bg)]',
243
+ '[&_code]:w-full',
244
+ '[&_code]:grid',
245
+ '[&_code]:overflow-x-auto',
246
+ '[&_code]:bg-transparent',
247
+ '[&_.line]:px-4',
248
+ '[&_.line]:w-full',
249
+ '[&_.line]:relative'
250
+ );
251
+
252
+
253
+ type CodeBlockData = {
254
+ language: string;
255
+ filename: string;
256
+ code: string;
257
+ };
258
+
259
+ type CodeBlockContextType = {
260
+ value: string | undefined;
261
+ onValueChange: ((value: string) => void) | undefined;
262
+ data: CodeBlockData[];
263
+ };
264
+
265
+ const CodeBlockContext = createContext<CodeBlockContextType>({
266
+ value: undefined,
267
+ onValueChange: undefined,
268
+ data: [],
269
+ });
270
+
271
+ export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
272
+ defaultValue?: string;
273
+ value?: string;
274
+ onValueChange?: (value: string) => void;
275
+ data: CodeBlockData[];
276
+ };
277
+
278
+ export const CodeBlock = ({
279
+ value: controlledValue,
280
+ onValueChange: controlledOnValueChange,
281
+ defaultValue,
282
+ className,
283
+ data,
284
+ ...props
285
+ }: CodeBlockProps) => {
286
+ const [value, onValueChange] = useControllableState({
287
+ defaultProp: defaultValue ?? '',
288
+ prop: controlledValue,
289
+ onChange: controlledOnValueChange,
290
+ });
291
+
292
+ return (
293
+ <CodeBlockContext.Provider value={{ value, onValueChange, data }}>
294
+ <div
295
+ className={cn('size-full overflow-hidden rounded-md border', className)}
296
+ {...props}
297
+ />
298
+ </CodeBlockContext.Provider>
299
+ );
300
+ };
301
+
302
+ export type CodeBlockHeaderProps = HTMLAttributes<HTMLDivElement>;
303
+
304
+ export const CodeBlockHeader = ({
305
+ className,
306
+ ...props
307
+ }: CodeBlockHeaderProps) => (
308
+ <div
309
+ className={cn(
310
+ 'flex flex-row items-center border-b bg-secondary p-1',
311
+ className
312
+ )}
313
+ {...props}
314
+ />
315
+ );
316
+
317
+ export type CodeBlockFilesProps = Omit<
318
+ HTMLAttributes<HTMLDivElement>,
319
+ 'children'
320
+ > & {
321
+ children: (item: CodeBlockData) => ReactNode;
322
+ };
323
+
324
+ export const CodeBlockFiles = ({
325
+ className,
326
+ children,
327
+ ...props
328
+ }: CodeBlockFilesProps) => {
329
+ const { data } = useContext(CodeBlockContext);
330
+
331
+ return (
332
+ <div
333
+ className={cn('flex grow flex-row items-center gap-2', className)}
334
+ {...props}
335
+ >
336
+ {data.map(children)}
337
+ </div>
338
+ );
339
+ };
340
+
341
+ export type CodeBlockFilenameProps = HTMLAttributes<HTMLDivElement> & {
342
+ icon?: IconType;
343
+ value?: string;
344
+ };
345
+
346
+ export const CodeBlockFilename = ({
347
+ className,
348
+ icon,
349
+ value,
350
+ children,
351
+ ...props
352
+ }: CodeBlockFilenameProps) => {
353
+ const { value: activeValue } = useContext(CodeBlockContext);
354
+ const defaultIcon = Object.entries(filenameIconMap).find(([pattern]) => {
355
+ const regex = new RegExp(
356
+ `^${pattern.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*')}$`
357
+ );
358
+ return regex.test(children as string);
359
+ })?.[1];
360
+ const Icon = icon ?? defaultIcon;
361
+
362
+ if (value !== activeValue) {
363
+ return null;
364
+ }
365
+
366
+ return (
367
+ <div
368
+ className="flex items-center gap-2 bg-secondary px-4 py-1.5 text-muted-foreground text-xs"
369
+ {...props}
370
+ >
371
+ {Icon && <Icon className="h-4 w-4 shrink-0" />}
372
+ <span className="flex-1 truncate">{children}</span>
373
+ </div>
374
+ );
375
+ };
376
+
377
+ export type CodeBlockSelectProps = ComponentProps<typeof Select>;
378
+
379
+ export const CodeBlockSelect = (props: CodeBlockSelectProps) => {
380
+ const { value, onValueChange } = useContext(CodeBlockContext);
381
+
382
+ return <Select onValueChange={onValueChange} value={value} {...props} />;
383
+ };
384
+
385
+ export type CodeBlockSelectTriggerProps = ComponentProps<typeof SelectTrigger>;
386
+
387
+ export const CodeBlockSelectTrigger = ({
388
+ className,
389
+ ...props
390
+ }: CodeBlockSelectTriggerProps) => (
391
+ <SelectTrigger
392
+ className={cn(
393
+ 'w-fit border-none text-muted-foreground text-xs shadow-none',
394
+ className
395
+ )}
396
+ {...props}
397
+ />
398
+ );
399
+
400
+ export type CodeBlockSelectValueProps = ComponentProps<typeof SelectValue>;
401
+
402
+ export const CodeBlockSelectValue = (props: CodeBlockSelectValueProps) => (
403
+ <SelectValue {...props} />
404
+ );
405
+
406
+ export type CodeBlockSelectContentProps = Omit<
407
+ ComponentProps<typeof SelectContent>,
408
+ 'children'
409
+ > & {
410
+ children: (item: CodeBlockData) => ReactNode;
411
+ };
412
+
413
+ export const CodeBlockSelectContent = ({
414
+ children,
415
+ ...props
416
+ }: CodeBlockSelectContentProps) => {
417
+ const { data } = useContext(CodeBlockContext);
418
+
419
+ return <SelectContent {...props}>{data.map(children)}</SelectContent>;
420
+ };
421
+
422
+ export type CodeBlockSelectItemProps = ComponentProps<typeof SelectItem>;
423
+
424
+ export const CodeBlockSelectItem = ({
425
+ className,
426
+ ...props
427
+ }: CodeBlockSelectItemProps) => (
428
+ <SelectItem className={cn('text-sm', className)} {...props} />
429
+ );
430
+
431
+ export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
432
+ onCopy?: () => void;
433
+ onError?: (error: Error) => void;
434
+ timeout?: number;
435
+ };
436
+
437
+ export const CodeBlockCopyButton = ({
438
+ asChild,
439
+ onCopy,
440
+ onError,
441
+ timeout = 2000,
442
+ children,
443
+ className,
444
+ ...props
445
+ }: CodeBlockCopyButtonProps) => {
446
+ const [isCopied, setIsCopied] = useState(false);
447
+ const { data, value } = useContext(CodeBlockContext);
448
+ const code = data.find((item) => item.language === value)?.code;
449
+
450
+ const copyToClipboard = () => {
451
+ if (
452
+ typeof window === 'undefined' ||
453
+ !navigator.clipboard.writeText ||
454
+ !code
455
+ ) {
456
+ return;
457
+ }
458
+
459
+ navigator.clipboard.writeText(code).then(() => {
460
+ setIsCopied(true);
461
+ onCopy?.();
462
+
463
+ setTimeout(() => setIsCopied(false), timeout);
464
+ }, onError);
465
+ };
466
+
467
+ if (asChild) {
468
+ return cloneElement(children as ReactElement, {
469
+ // @ts-expect-error - we know this is a button
470
+ onClick: copyToClipboard,
471
+ });
472
+ }
473
+
474
+ const Icon = isCopied ? CheckIcon : CopyIcon;
475
+
476
+ return (
477
+ <Button
478
+ className={cn('shrink-0', className)}
479
+ onClick={copyToClipboard}
480
+ size="icon"
481
+ variant="ghost"
482
+ {...props}
483
+ >
484
+ {children ?? <Icon className="text-muted-foreground" size={14} />}
485
+ </Button>
486
+ );
487
+ };
488
+
489
+ type CodeBlockFallbackProps = HTMLAttributes<HTMLDivElement>;
490
+
491
+ const CodeBlockFallback = ({ children, ...props }: CodeBlockFallbackProps) => (
492
+ <div {...props}>
493
+ <pre className="w-full">
494
+ <code>
495
+ {children
496
+ ?.toString()
497
+ .split('\n')
498
+ .map((line, i) => (
499
+ <span className="line" key={i}>
500
+ {line}
501
+ </span>
502
+ ))}
503
+ </code>
504
+ </pre>
505
+ </div>
506
+ );
507
+
508
+ export type CodeBlockBodyProps = Omit<
509
+ HTMLAttributes<HTMLDivElement>,
510
+ 'children'
511
+ > & {
512
+ children: (item: CodeBlockData) => ReactNode;
513
+ };
514
+
515
+ export const CodeBlockBody = ({ children, ...props }: CodeBlockBodyProps) => {
516
+ const { data } = useContext(CodeBlockContext);
517
+
518
+ return <div {...props}>{data.map(children)}</div>;
519
+ };
520
+
521
+ export type CodeBlockItemProps = HTMLAttributes<HTMLDivElement> & {
522
+ value: string;
523
+ lineNumbers?: boolean;
524
+ };
525
+
526
+ export const CodeBlockItem = ({
527
+ children,
528
+ lineNumbers = true,
529
+ className,
530
+ value,
531
+ ...props
532
+ }: CodeBlockItemProps) => {
533
+ const { value: activeValue } = useContext(CodeBlockContext);
534
+
535
+ if (value !== activeValue) {
536
+ return null;
537
+ }
538
+
539
+ return (
540
+ <div
541
+ className={cn(
542
+ codeBlockClassName,
543
+ lineHighlightClassNames,
544
+ lineDiffClassNames,
545
+ lineFocusedClassNames,
546
+ wordHighlightClassNames,
547
+ darkModeClassNames,
548
+ lineNumbers && lineNumberClassNames,
549
+ className
550
+ )}
551
+ {...props}
552
+ >
553
+ {children}
554
+ </div>
555
+ );
556
+ };
557
+
558
+ export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & {
559
+ themes?: {
560
+ light: string;
561
+ dark: string;
562
+ };
563
+ language?: BundledLanguage;
564
+ syntaxHighlighting?: boolean;
565
+ children: string;
566
+ };
567
+
568
+ export const CodeBlockContent = ({
569
+ children,
570
+ themes = {
571
+ light: 'vitesse-light',
572
+ dark: 'vitesse-dark',
573
+ },
574
+ language = 'typescript',
575
+ syntaxHighlighting = true,
576
+ ...props
577
+ }: CodeBlockContentProps) => {
578
+ const [highlightedCode, setHighlightedCode] = useState<string>('');
579
+ const [isLoading, setIsLoading] = useState(syntaxHighlighting);
580
+
581
+ useEffect(() => {
582
+ if (!syntaxHighlighting) {
583
+ setIsLoading(false);
584
+ return;
585
+ }
586
+
587
+ const loadHighlightedCode = async () => {
588
+ try {
589
+ const { codeToHtml } = await import('shiki');
590
+
591
+ const html = await codeToHtml(children, {
592
+ lang: language,
593
+ themes: {
594
+ light: themes.light,
595
+ dark: themes.dark,
596
+ },
597
+ });
598
+
599
+ setHighlightedCode(html);
600
+ setIsLoading(false);
601
+ } catch (error) {
602
+ console.error(`Failed to highlight code for language "${language}":`, error);
603
+ setIsLoading(false);
604
+ }
605
+ };
606
+
607
+ loadHighlightedCode();
608
+ }, [children, language, themes, syntaxHighlighting]);
609
+
610
+ if (!syntaxHighlighting || isLoading) {
611
+ return <CodeBlockFallback {...props}>{children}</CodeBlockFallback>;
612
+ }
613
+
614
+ return (
615
+ <div
616
+ dangerouslySetInnerHTML={{ __html: highlightedCode }}
617
+ {...props}
618
+ />
619
+ );
620
+ };
@@ -0,0 +1,63 @@
1
+ import {
2
+ transformerNotationDiff,
3
+ transformerNotationErrorLevel,
4
+ transformerNotationFocus,
5
+ transformerNotationHighlight,
6
+ transformerNotationWordHighlight,
7
+ } from '@shikijs/transformers';
8
+ import type { HTMLAttributes } from 'react';
9
+ import {
10
+ type BundledLanguage,
11
+ type CodeOptionsMultipleThemes,
12
+ codeToHtml,
13
+ } from 'shiki';
14
+
15
+ export type CodeBlockContentProps = HTMLAttributes<HTMLDivElement> & {
16
+ themes?: CodeOptionsMultipleThemes['themes'];
17
+ language?: BundledLanguage;
18
+ children: string;
19
+ syntaxHighlighting?: boolean;
20
+ };
21
+
22
+ export const CodeBlockContent = async ({
23
+ children,
24
+ themes,
25
+ language,
26
+ syntaxHighlighting = true,
27
+ ...props
28
+ }: CodeBlockContentProps) => {
29
+ const html = syntaxHighlighting
30
+ ? await codeToHtml(children as string, {
31
+ lang: language ?? 'typescript',
32
+ themes: themes ?? {
33
+ light: 'vitesse-light',
34
+ dark: 'vitesse-dark',
35
+ },
36
+ transformers: [
37
+ transformerNotationDiff({
38
+ matchAlgorithm: 'v3',
39
+ }),
40
+ transformerNotationHighlight({
41
+ matchAlgorithm: 'v3',
42
+ }),
43
+ transformerNotationWordHighlight({
44
+ matchAlgorithm: 'v3',
45
+ }),
46
+ transformerNotationFocus({
47
+ matchAlgorithm: 'v3',
48
+ }),
49
+ transformerNotationErrorLevel({
50
+ matchAlgorithm: 'v3',
51
+ }),
52
+ ],
53
+ })
54
+ : children;
55
+
56
+ return (
57
+ <div
58
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: "Kinda how Shiki works"
59
+ dangerouslySetInnerHTML={{ __html: html }}
60
+ {...props}
61
+ />
62
+ );
63
+ };