code-block.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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: [
  23. "constant.numeric",
  24. "constant.language.boolean",
  25. "constant.language.null",
  26. ],
  27. settings: { foreground: "#50E3C2" },
  28. },
  29. {
  30. scope: ["keyword", "storage.type", "storage.modifier"],
  31. settings: { foreground: "#FF0080" },
  32. },
  33. {
  34. scope: ["keyword.operator", "keyword.control"],
  35. settings: { foreground: "#FF0080" },
  36. },
  37. {
  38. scope: ["entity.name.function", "support.function", "meta.function-call"],
  39. settings: { foreground: "#7928CA" },
  40. },
  41. {
  42. scope: ["variable", "variable.other", "variable.parameter"],
  43. settings: { foreground: "#EDEDED" },
  44. },
  45. {
  46. scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
  47. settings: { foreground: "#FF0080" },
  48. },
  49. {
  50. scope: ["punctuation", "meta.brace", "meta.bracket"],
  51. settings: { foreground: "#888888" },
  52. },
  53. {
  54. scope: [
  55. "support.type.property-name",
  56. "entity.name.tag.json",
  57. "meta.object-literal.key",
  58. ],
  59. settings: { foreground: "#EDEDED" },
  60. },
  61. {
  62. scope: ["entity.other.attribute-name"],
  63. settings: { foreground: "#50E3C2" },
  64. },
  65. {
  66. scope: ["support.type.primitive", "entity.name.type.primitive"],
  67. settings: { foreground: "#50E3C2" },
  68. },
  69. ],
  70. };
  71. const vercelLightTheme = {
  72. name: "vercel-light",
  73. type: "light" as const,
  74. colors: {
  75. "editor.background": "transparent",
  76. "editor.foreground": "#171717",
  77. },
  78. settings: [
  79. {
  80. scope: ["comment", "punctuation.definition.comment"],
  81. settings: { foreground: "#6b7280" },
  82. },
  83. {
  84. scope: ["string", "string.quoted", "string.template"],
  85. settings: { foreground: "#067a6e" },
  86. },
  87. {
  88. scope: [
  89. "constant.numeric",
  90. "constant.language.boolean",
  91. "constant.language.null",
  92. ],
  93. settings: { foreground: "#067a6e" },
  94. },
  95. {
  96. scope: ["keyword", "storage.type", "storage.modifier"],
  97. settings: { foreground: "#d6409f" },
  98. },
  99. {
  100. scope: ["keyword.operator", "keyword.control"],
  101. settings: { foreground: "#d6409f" },
  102. },
  103. {
  104. scope: ["entity.name.function", "support.function", "meta.function-call"],
  105. settings: { foreground: "#6e56cf" },
  106. },
  107. {
  108. scope: ["variable", "variable.other", "variable.parameter"],
  109. settings: { foreground: "#171717" },
  110. },
  111. {
  112. scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
  113. settings: { foreground: "#d6409f" },
  114. },
  115. {
  116. scope: ["punctuation", "meta.brace", "meta.bracket"],
  117. settings: { foreground: "#6b7280" },
  118. },
  119. {
  120. scope: [
  121. "support.type.property-name",
  122. "entity.name.tag.json",
  123. "meta.object-literal.key",
  124. ],
  125. settings: { foreground: "#171717" },
  126. },
  127. {
  128. scope: ["entity.other.attribute-name"],
  129. settings: { foreground: "#067a6e" },
  130. },
  131. {
  132. scope: ["support.type.primitive", "entity.name.type.primitive"],
  133. settings: { foreground: "#067a6e" },
  134. },
  135. ],
  136. };
  137. // Preload highlighter on module load
  138. let highlighterPromise: Promise<Highlighter> | null = null;
  139. function getHighlighter() {
  140. if (!highlighterPromise) {
  141. highlighterPromise = createHighlighter({
  142. themes: [vercelLightTheme, vercelDarkTheme],
  143. langs: ["json", "tsx", "typescript", "yaml"],
  144. });
  145. }
  146. return highlighterPromise;
  147. }
  148. // Start loading immediately when module is imported
  149. if (typeof window !== "undefined") {
  150. getHighlighter();
  151. }
  152. interface CodeBlockProps {
  153. code: string;
  154. lang: "json" | "tsx" | "typescript" | "yaml";
  155. fillHeight?: boolean;
  156. hideCopyButton?: boolean;
  157. }
  158. export function CodeBlock({
  159. code,
  160. lang,
  161. fillHeight,
  162. hideCopyButton,
  163. }: CodeBlockProps) {
  164. const [html, setHtml] = useState<string>("");
  165. useEffect(() => {
  166. getHighlighter().then((highlighter) => {
  167. setHtml(
  168. highlighter.codeToHtml(code, {
  169. lang,
  170. themes: {
  171. light: "vercel-light",
  172. dark: "vercel-dark",
  173. },
  174. defaultColor: false,
  175. }),
  176. );
  177. });
  178. }, [code, lang]);
  179. if (!html) {
  180. return fillHeight ? <div className="p-3" /> : null;
  181. }
  182. return (
  183. <div className={`relative group ${fillHeight ? "p-3" : ""}`}>
  184. {!hideCopyButton && (
  185. <div className="float-right sticky top-3 z-10 ml-2">
  186. <CopyButton
  187. text={code}
  188. className="opacity-0 group-hover:opacity-100 text-neutral-400"
  189. />
  190. </div>
  191. )}
  192. <div
  193. className="text-[13px] leading-relaxed [&_pre]:bg-transparent! [&_pre]:p-0! [&_pre]:m-0! [&_pre]:border-none! [&_pre]:rounded-none! [&_pre]:text-[13px]! [&_pre]:overflow-visible! [&_code]:bg-transparent! [&_code]:p-0! [&_code]:rounded-none! [&_code]:text-[13px]!"
  194. dangerouslySetInnerHTML={{ __html: html }}
  195. />
  196. </div>
  197. );
  198. }