code-block.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. "use client";
  2. import { useEffect, useState } from "react";
  3. import { createHighlighter, type Highlighter } from "shiki";
  4. import { CopyButton } from "./copy-button";
  5. const vercelDarkTheme = {
  6. name: "vercel-dark",
  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. const vercelLightTheme = {
  64. name: "vercel-light",
  65. type: "light" as const,
  66. colors: {
  67. "editor.background": "transparent",
  68. "editor.foreground": "#171717",
  69. },
  70. settings: [
  71. {
  72. scope: ["comment", "punctuation.definition.comment"],
  73. settings: { foreground: "#6b7280" },
  74. },
  75. {
  76. scope: ["string", "string.quoted", "string.template"],
  77. settings: { foreground: "#067a6e" },
  78. },
  79. {
  80. scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
  81. settings: { foreground: "#067a6e" },
  82. },
  83. {
  84. scope: ["keyword", "storage.type", "storage.modifier"],
  85. settings: { foreground: "#d6409f" },
  86. },
  87. {
  88. scope: ["keyword.operator", "keyword.control"],
  89. settings: { foreground: "#d6409f" },
  90. },
  91. {
  92. scope: ["entity.name.function", "support.function", "meta.function-call"],
  93. settings: { foreground: "#6e56cf" },
  94. },
  95. {
  96. scope: ["variable", "variable.other", "variable.parameter"],
  97. settings: { foreground: "#171717" },
  98. },
  99. {
  100. scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
  101. settings: { foreground: "#d6409f" },
  102. },
  103. {
  104. scope: ["punctuation", "meta.brace", "meta.bracket"],
  105. settings: { foreground: "#6b7280" },
  106. },
  107. {
  108. scope: ["support.type.property-name", "entity.name.tag.json", "meta.object-literal.key"],
  109. settings: { foreground: "#171717" },
  110. },
  111. {
  112. scope: ["entity.other.attribute-name"],
  113. settings: { foreground: "#067a6e" },
  114. },
  115. {
  116. scope: ["support.type.primitive", "entity.name.type.primitive"],
  117. settings: { foreground: "#067a6e" },
  118. },
  119. ],
  120. };
  121. // Preload highlighter on module load
  122. let highlighterPromise: Promise<Highlighter> | null = null;
  123. function getHighlighter() {
  124. if (!highlighterPromise) {
  125. highlighterPromise = createHighlighter({
  126. themes: [vercelLightTheme, vercelDarkTheme],
  127. langs: ["json", "tsx", "typescript"],
  128. });
  129. }
  130. return highlighterPromise;
  131. }
  132. // Start loading immediately when module is imported
  133. if (typeof window !== "undefined") {
  134. getHighlighter();
  135. }
  136. interface CodeBlockProps {
  137. code: string;
  138. lang: "json" | "tsx" | "typescript";
  139. }
  140. export function CodeBlock({ code, lang }: CodeBlockProps) {
  141. const [html, setHtml] = useState<string>("");
  142. useEffect(() => {
  143. getHighlighter().then((highlighter) => {
  144. setHtml(highlighter.codeToHtml(code, {
  145. lang,
  146. themes: {
  147. light: "vercel-light",
  148. dark: "vercel-dark",
  149. },
  150. defaultColor: false,
  151. }));
  152. });
  153. }, [code, lang]);
  154. if (!html) {
  155. return null;
  156. }
  157. return (
  158. <div className="relative group">
  159. <div className="sticky top-0 float-right z-10">
  160. <CopyButton
  161. text={code}
  162. className="opacity-0 group-hover:opacity-100 text-neutral-400"
  163. />
  164. </div>
  165. <div
  166. 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]!"
  167. dangerouslySetInnerHTML={{ __html: html }}
  168. />
  169. </div>
  170. );
  171. }