code-block.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. "use client";
  2. import { useEffect, useState } from "react";
  3. import { createHighlighter, type Highlighter } from "shiki";
  4. import { CopyButton } from "./copy-button";
  5. const vercelTheme = {
  6. name: "vercel",
  7. type: "dark" as const,
  8. colors: {
  9. "editor.background": "transparent",
  10. "editor.foreground": "#EDEDED",
  11. },
  12. settings: [
  13. {
  14. scope: ["comment", "punctuation.definition.comment"],
  15. settings: { foreground: "#666666" },
  16. },
  17. {
  18. scope: ["string", "string.quoted", "string.template"],
  19. settings: { foreground: "#50E3C2" },
  20. },
  21. {
  22. scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
  23. settings: { foreground: "#50E3C2" },
  24. },
  25. {
  26. scope: ["keyword", "storage.type", "storage.modifier"],
  27. settings: { foreground: "#FF0080" },
  28. },
  29. {
  30. scope: ["keyword.operator", "keyword.control"],
  31. settings: { foreground: "#FF0080" },
  32. },
  33. {
  34. scope: ["entity.name.function", "support.function", "meta.function-call"],
  35. settings: { foreground: "#7928CA" },
  36. },
  37. {
  38. scope: ["variable", "variable.other", "variable.parameter"],
  39. settings: { foreground: "#EDEDED" },
  40. },
  41. {
  42. scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
  43. settings: { foreground: "#FF0080" },
  44. },
  45. {
  46. scope: ["punctuation", "meta.brace", "meta.bracket"],
  47. settings: { foreground: "#888888" },
  48. },
  49. {
  50. scope: ["support.type.property-name", "entity.name.tag.json", "meta.object-literal.key"],
  51. settings: { foreground: "#EDEDED" },
  52. },
  53. {
  54. scope: ["entity.other.attribute-name"],
  55. settings: { foreground: "#50E3C2" },
  56. },
  57. {
  58. scope: ["support.type.primitive", "entity.name.type.primitive"],
  59. settings: { foreground: "#50E3C2" },
  60. },
  61. ],
  62. };
  63. // Preload highlighter on module load
  64. let highlighterPromise: Promise<Highlighter> | null = null;
  65. function getHighlighter() {
  66. if (!highlighterPromise) {
  67. highlighterPromise = createHighlighter({
  68. themes: [vercelTheme],
  69. langs: ["json", "tsx", "typescript"],
  70. });
  71. }
  72. return highlighterPromise;
  73. }
  74. // Start loading immediately when module is imported
  75. if (typeof window !== "undefined") {
  76. getHighlighter();
  77. }
  78. interface CodeBlockProps {
  79. code: string;
  80. lang: "json" | "tsx" | "typescript";
  81. }
  82. export function CodeBlock({ code, lang }: CodeBlockProps) {
  83. const [html, setHtml] = useState<string>("");
  84. useEffect(() => {
  85. getHighlighter().then((highlighter) => {
  86. setHtml(highlighter.codeToHtml(code, { lang, theme: "vercel" }));
  87. });
  88. }, [code, lang]);
  89. if (!html) {
  90. return null;
  91. }
  92. return (
  93. <div className="relative group">
  94. <CopyButton
  95. text={code}
  96. className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 text-muted-foreground"
  97. />
  98. <div
  99. className="text-[11px] leading-relaxed [&_pre]:bg-transparent! [&_pre]:p-0! [&_pre]:m-0! [&_pre]:border-none! [&_pre]:rounded-none! [&_pre]:text-[11px]! [&_code]:bg-transparent! [&_code]:p-0! [&_code]:rounded-none! [&_code]:text-[11px]!"
  100. dangerouslySetInnerHTML={{ __html: html }}
  101. />
  102. </div>
  103. );
  104. }