code-block.tsx 2.9 KB

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