egaki 0.6.0 → 0.7.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 (271) hide show
  1. package/CHANGELOG.md +145 -9
  2. package/dist/cli/cache-utils.d.ts +33 -0
  3. package/dist/cli/cache-utils.d.ts.map +1 -0
  4. package/dist/cli/cache-utils.js +157 -0
  5. package/dist/cli/cached-generate.d.ts +105 -0
  6. package/dist/cli/cached-generate.d.ts.map +1 -0
  7. package/dist/cli/cached-generate.js +208 -0
  8. package/dist/cli/cached-generate.test.d.ts +11 -0
  9. package/dist/cli/cached-generate.test.d.ts.map +1 -0
  10. package/dist/cli/cached-generate.test.js +431 -0
  11. package/dist/cli/cartesia-provider.d.ts +16 -0
  12. package/dist/cli/cartesia-provider.d.ts.map +1 -0
  13. package/dist/cli/cartesia-provider.js +184 -0
  14. package/dist/cli/chatgpt-auth.d.ts.map +1 -0
  15. package/dist/cli/cli.d.ts +4 -0
  16. package/dist/cli/cli.d.ts.map +1 -0
  17. package/dist/{cli.js → cli/cli.js} +626 -600
  18. package/dist/cli/clone-generate.d.ts +26 -0
  19. package/dist/cli/clone-generate.d.ts.map +1 -0
  20. package/dist/cli/clone-generate.js +148 -0
  21. package/dist/cli/credentials.d.ts.map +1 -0
  22. package/dist/{credentials.js → cli/credentials.js} +20 -0
  23. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  24. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  25. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  26. package/dist/cli/generate.d.ts +140 -0
  27. package/dist/cli/generate.d.ts.map +1 -0
  28. package/dist/cli/generate.js +604 -0
  29. package/dist/cli/generate.test.d.ts +2 -0
  30. package/dist/cli/generate.test.d.ts.map +1 -0
  31. package/dist/cli/generate.test.js +131 -0
  32. package/dist/cli/login.d.ts.map +1 -0
  33. package/dist/cli/main.d.ts +3 -0
  34. package/dist/cli/main.d.ts.map +1 -0
  35. package/dist/cli/main.js +7 -0
  36. package/dist/{model-catalog.d.ts → cli/model-catalog.d.ts} +6 -0
  37. package/dist/cli/model-catalog.d.ts.map +1 -0
  38. package/dist/{model-catalog.js → cli/model-catalog.js} +714 -4
  39. package/dist/{models.d.ts → cli/models.d.ts} +12 -5
  40. package/dist/cli/models.d.ts.map +1 -0
  41. package/dist/{models.js → cli/models.js} +58 -57
  42. package/dist/cli/open-browser.d.ts.map +1 -0
  43. package/dist/cli/openai-speech-provider.d.ts +3 -0
  44. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.js +28 -0
  46. package/dist/cli/separation-generate.d.ts +34 -0
  47. package/dist/cli/separation-generate.d.ts.map +1 -0
  48. package/dist/cli/separation-generate.js +117 -0
  49. package/dist/cli/speech-catalog.d.ts +35 -0
  50. package/dist/cli/speech-catalog.d.ts.map +1 -0
  51. package/dist/cli/speech-catalog.js +195 -0
  52. package/dist/cli/speech-generate.d.ts +86 -0
  53. package/dist/cli/speech-generate.d.ts.map +1 -0
  54. package/dist/cli/speech-generate.js +156 -0
  55. package/dist/cli/speech-models.d.ts +8 -0
  56. package/dist/cli/speech-models.d.ts.map +1 -0
  57. package/dist/cli/speech-models.js +109 -0
  58. package/dist/cli/speech-provider.test.d.ts +2 -0
  59. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  60. package/dist/cli/speech-provider.test.js +197 -0
  61. package/dist/cli/subscription.d.ts.map +1 -0
  62. package/dist/cli/transcription-catalog.d.ts +35 -0
  63. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.js +192 -0
  65. package/dist/cli/transcription-generate.d.ts +62 -0
  66. package/dist/cli/transcription-generate.d.ts.map +1 -0
  67. package/dist/cli/transcription-generate.js +144 -0
  68. package/dist/cli/transcription-models.d.ts +9 -0
  69. package/dist/cli/transcription-models.d.ts.map +1 -0
  70. package/dist/cli/transcription-models.js +100 -0
  71. package/dist/cli/transcription-providers.d.ts +22 -0
  72. package/dist/cli/transcription-providers.d.ts.map +1 -0
  73. package/dist/cli/transcription-providers.js +290 -0
  74. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  75. package/dist/cli/xai-auth.d.ts.map +1 -0
  76. package/dist/{xai-auth.js → cli/xai-auth.js} +1 -1
  77. package/dist/vite/angled-screen.d.ts +57 -0
  78. package/dist/vite/angled-screen.d.ts.map +1 -0
  79. package/dist/vite/angled-screen.js +115 -0
  80. package/dist/vite/app.d.ts +51 -0
  81. package/dist/vite/app.d.ts.map +1 -0
  82. package/dist/vite/app.js +150 -0
  83. package/dist/vite/bands-shader.d.ts +47 -0
  84. package/dist/vite/bands-shader.d.ts.map +1 -0
  85. package/dist/vite/bands-shader.js +267 -0
  86. package/dist/vite/code-block.d.ts +107 -0
  87. package/dist/vite/code-block.d.ts.map +1 -0
  88. package/dist/vite/code-block.js +641 -0
  89. package/dist/vite/components.d.ts +36 -0
  90. package/dist/vite/components.d.ts.map +1 -0
  91. package/dist/vite/components.js +113 -0
  92. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  93. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  94. package/dist/vite/dispersion-rings-shader.js +282 -0
  95. package/dist/vite/easing-curves.d.ts +129 -0
  96. package/dist/vite/easing-curves.d.ts.map +1 -0
  97. package/dist/vite/easing-curves.js +584 -0
  98. package/dist/vite/easing-curves.test.d.ts +2 -0
  99. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  100. package/dist/vite/easing-curves.test.js +200 -0
  101. package/dist/vite/generated-media-client.d.ts +35 -0
  102. package/dist/vite/generated-media-client.d.ts.map +1 -0
  103. package/dist/vite/generated-media-client.js +76 -0
  104. package/dist/vite/keyframes.d.ts +129 -0
  105. package/dist/vite/keyframes.d.ts.map +1 -0
  106. package/dist/vite/keyframes.js +194 -0
  107. package/dist/vite/layout-editor.d.ts +45 -0
  108. package/dist/vite/layout-editor.d.ts.map +1 -0
  109. package/dist/vite/layout-editor.js +587 -0
  110. package/dist/vite/layout-transition.d.ts +136 -0
  111. package/dist/vite/layout-transition.d.ts.map +1 -0
  112. package/dist/vite/layout-transition.js +581 -0
  113. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  114. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  115. package/dist/vite/liquid-gradient-shader.js +413 -0
  116. package/dist/vite/mdx-client.d.ts +15 -0
  117. package/dist/vite/mdx-client.d.ts.map +1 -0
  118. package/dist/vite/mdx-client.js +265 -0
  119. package/dist/vite/mdx-parse.d.ts +102 -0
  120. package/dist/vite/mdx-parse.d.ts.map +1 -0
  121. package/dist/vite/mdx-parse.js +251 -0
  122. package/dist/vite/mdx-provided-components.d.ts +18 -0
  123. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  124. package/dist/vite/mdx-provided-components.js +14 -0
  125. package/dist/vite/mdx-video.d.ts +327 -0
  126. package/dist/vite/mdx-video.d.ts.map +1 -0
  127. package/dist/vite/mdx-video.js +596 -0
  128. package/dist/vite/mdx-video.test.d.ts +9 -0
  129. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  130. package/dist/vite/mdx-video.test.js +1945 -0
  131. package/dist/vite/media-components.d.ts +45 -0
  132. package/dist/vite/media-components.d.ts.map +1 -0
  133. package/dist/vite/media-components.js +355 -0
  134. package/dist/vite/media-duration-store.d.ts +66 -0
  135. package/dist/vite/media-duration-store.d.ts.map +1 -0
  136. package/dist/vite/media-duration-store.js +204 -0
  137. package/dist/vite/player-page.d.ts +35 -0
  138. package/dist/vite/player-page.d.ts.map +1 -0
  139. package/dist/vite/player-page.js +720 -0
  140. package/dist/vite/render-client.d.ts +18 -0
  141. package/dist/vite/render-client.d.ts.map +1 -0
  142. package/dist/vite/render-client.js +67 -0
  143. package/dist/vite/sdk.d.ts +221 -0
  144. package/dist/vite/sdk.d.ts.map +1 -0
  145. package/dist/vite/sdk.js +355 -0
  146. package/dist/vite/server-components.d.ts +55 -0
  147. package/dist/vite/server-components.d.ts.map +1 -0
  148. package/dist/vite/server-components.js +156 -0
  149. package/dist/vite/server-mdx.d.ts +74 -0
  150. package/dist/vite/server-mdx.d.ts.map +1 -0
  151. package/dist/vite/server-mdx.js +259 -0
  152. package/dist/vite/shader-renderer.d.ts +48 -0
  153. package/dist/vite/shader-renderer.d.ts.map +1 -0
  154. package/dist/vite/shader-renderer.js +411 -0
  155. package/dist/vite/store-hooks.d.ts +12 -0
  156. package/dist/vite/store-hooks.d.ts.map +1 -0
  157. package/dist/vite/store-hooks.js +93 -0
  158. package/dist/vite/store.d.ts +75 -0
  159. package/dist/vite/store.d.ts.map +1 -0
  160. package/dist/vite/store.js +52 -0
  161. package/dist/vite/store.test.d.ts +6 -0
  162. package/dist/vite/store.test.d.ts.map +1 -0
  163. package/dist/vite/store.test.js +172 -0
  164. package/dist/vite/tweakpane-hook.d.ts +94 -0
  165. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  166. package/dist/vite/tweakpane-hook.js +450 -0
  167. package/dist/vite/vite-plugin.d.ts +28 -0
  168. package/dist/vite/vite-plugin.d.ts.map +1 -0
  169. package/dist/vite/vite-plugin.js +539 -0
  170. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  171. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  172. package/dist/vite/wave-gradient-shader.js +207 -0
  173. package/package.json +101 -24
  174. package/skills/egaki/SKILL.md +162 -1
  175. package/src/cli/cache-utils.ts +169 -0
  176. package/src/cli/cached-generate.test.ts +531 -0
  177. package/src/cli/cached-generate.ts +348 -0
  178. package/src/cli/cartesia-provider.ts +249 -0
  179. package/src/{cli.ts → cli/cli.ts} +836 -884
  180. package/src/cli/clone-generate.ts +202 -0
  181. package/src/{credentials.ts → cli/credentials.ts} +20 -0
  182. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  183. package/src/cli/generate.test.ts +166 -0
  184. package/src/cli/generate.ts +912 -0
  185. package/src/cli/main.ts +8 -0
  186. package/src/{model-catalog.ts → cli/model-catalog.ts} +787 -58
  187. package/src/{models.ts → cli/models.ts} +65 -86
  188. package/src/cli/openai-speech-provider.ts +34 -0
  189. package/src/cli/separation-generate.ts +187 -0
  190. package/src/cli/speech-catalog.ts +253 -0
  191. package/src/cli/speech-generate.ts +275 -0
  192. package/src/cli/speech-models.ts +146 -0
  193. package/src/cli/speech-provider.test.ts +226 -0
  194. package/src/cli/transcription-catalog.ts +250 -0
  195. package/src/cli/transcription-generate.ts +214 -0
  196. package/src/cli/transcription-models.ts +136 -0
  197. package/src/cli/transcription-providers.ts +427 -0
  198. package/src/{xai-auth.ts → cli/xai-auth.ts} +1 -1
  199. package/src/vite/angled-screen.tsx +214 -0
  200. package/src/vite/app.tsx +197 -0
  201. package/src/vite/bands-shader.tsx +319 -0
  202. package/src/vite/code-block.tsx +939 -0
  203. package/src/vite/components.tsx +278 -0
  204. package/src/vite/dispersion-rings-shader.tsx +331 -0
  205. package/src/vite/easing-curves.test.ts +232 -0
  206. package/src/vite/easing-curves.ts +743 -0
  207. package/src/vite/generated-media-client.tsx +121 -0
  208. package/src/vite/keyframes.tsx +347 -0
  209. package/src/vite/layout-editor.tsx +709 -0
  210. package/src/vite/layout-transition.tsx +706 -0
  211. package/src/vite/liquid-gradient-shader.tsx +474 -0
  212. package/src/vite/mdx-client.tsx +420 -0
  213. package/src/vite/mdx-parse.ts +343 -0
  214. package/src/vite/mdx-provided-components.ts +19 -0
  215. package/src/vite/mdx-video.test.tsx +2219 -0
  216. package/src/vite/mdx-video.tsx +976 -0
  217. package/src/vite/media-components.tsx +473 -0
  218. package/src/vite/media-duration-store.ts +225 -0
  219. package/src/vite/motion-timing.ts +194 -0
  220. package/src/vite/player-page.tsx +1107 -0
  221. package/src/vite/render-client.ts +84 -0
  222. package/src/vite/sdk.ts +598 -0
  223. package/src/vite/server-components.tsx +241 -0
  224. package/src/vite/server-mdx.ts +285 -0
  225. package/src/vite/shader-renderer.tsx +535 -0
  226. package/src/vite/store-hooks.ts +117 -0
  227. package/src/vite/store.test.ts +202 -0
  228. package/src/vite/store.ts +115 -0
  229. package/src/vite/styles.css +21 -0
  230. package/src/vite/tweakpane-hook.tsx +593 -0
  231. package/src/vite/virtual-modules.d.ts +30 -0
  232. package/src/vite/vite-plugin.ts +585 -0
  233. package/src/vite/wave-gradient-shader.tsx +253 -0
  234. package/README.md +0 -370
  235. package/dist/antigravity-auth.d.ts +0 -15
  236. package/dist/antigravity-auth.d.ts.map +0 -1
  237. package/dist/antigravity-auth.js +0 -544
  238. package/dist/antigravity-image.d.ts +0 -11
  239. package/dist/antigravity-image.d.ts.map +0 -1
  240. package/dist/antigravity-image.js +0 -285
  241. package/dist/chatgpt-auth.d.ts.map +0 -1
  242. package/dist/cli.d.ts +0 -3
  243. package/dist/cli.d.ts.map +0 -1
  244. package/dist/credentials.d.ts.map +0 -1
  245. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  246. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  247. package/dist/gateway-model-candidates.generated.js +0 -64
  248. package/dist/login.d.ts.map +0 -1
  249. package/dist/model-catalog.d.ts.map +0 -1
  250. package/dist/models.d.ts.map +0 -1
  251. package/dist/open-browser.d.ts.map +0 -1
  252. package/dist/subscription.d.ts.map +0 -1
  253. package/dist/video-model-catalog.d.ts.map +0 -1
  254. package/dist/xai-auth.d.ts.map +0 -1
  255. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  256. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  257. /package/dist/{credentials.d.ts → cli/credentials.d.ts} +0 -0
  258. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  259. /package/dist/{login.js → cli/login.js} +0 -0
  260. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  261. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  262. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  263. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  264. /package/dist/{video-model-catalog.d.ts → cli/video-model-catalog.d.ts} +0 -0
  265. /package/dist/{video-model-catalog.js → cli/video-model-catalog.js} +0 -0
  266. /package/dist/{xai-auth.d.ts → cli/xai-auth.d.ts} +0 -0
  267. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  268. /package/src/{login.ts → cli/login.ts} +0 -0
  269. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  270. /package/src/{subscription.ts → cli/subscription.ts} +0 -0
  271. /package/src/{video-model-catalog.ts → cli/video-model-catalog.ts} +0 -0
@@ -0,0 +1,939 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * CodeBlock — ray.so-faithful syntax-highlighted code block for video.
5
+ *
6
+ * Uses shiki (TextMate grammars + WASM) for real syntax highlighting,
7
+ * with a CSS variables theme that maps token scopes to --ray-* vars.
8
+ * Theme colors are injected as inline CSS custom properties. The HTML
9
+ * from shiki's codeToHtml() is rendered via dangerouslySetInnerHTML.
10
+ *
11
+ * Per-theme frame styles are ported from ray.so's frame components:
12
+ * - Vercel: gridlines + corner brackets, no window chrome
13
+ * - Stripe: dashed gridlines + skewed stripe shape
14
+ * - OpenAI: clean rounded window, navy background
15
+ * - Supabase/Cloudflare: filename header, custom borders
16
+ * - Default: macOS traffic lights + title bar (non-partner themes)
17
+ *
18
+ * Source: https://github.com/raycast/ray-so
19
+ *
20
+ * Key source files:
21
+ * - Themes & syntax colors: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/store/themes.ts
22
+ * - CSS variables theme: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/util/theme-css-variables.ts
23
+ * - Shiki highlighter init: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/code.tsx
24
+ * - Highlighted code render: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/components/HighlightedCode.tsx
25
+ * - Editor + token CSS: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/components/Editor.module.css
26
+ * - Frame switch: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/components/Frame.tsx
27
+ * - Default frame: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/components/frames/DefaultFrame.tsx
28
+ * - Vercel frame: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/components/frames/VercelFrame.tsx
29
+ * - Stripe frame: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/components/frames/StripeFrame.tsx
30
+ * - OpenAI frame: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/components/frames/OpenAIFrame.tsx
31
+ * - Supabase frame: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/components/frames/SupabaseFrame.tsx
32
+ * - Cloudflare frame: https://github.com/raycast/ray-so/blob/main/app/(navigation)/(code)/components/frames/CloudflareFrame.tsx
33
+ */
34
+
35
+ import { createContext, useContext, useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react'
36
+ import { interpolate, spring, useCurrentFrame, useVideoConfig } from 'remotion'
37
+ import { useTweakpane } from './tweakpane-hook.tsx'
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Nesting guard
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export const CodeBlockContext = createContext(false)
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // CSS variables theme — exact port from ray.so's theme-css-variables.ts
47
+ // Maps TextMate scopes to --ray-* CSS variables.
48
+ // ---------------------------------------------------------------------------
49
+
50
+ function createCssVariablesTheme() {
51
+ const prefix = '--ray-'
52
+ const v = (name: string) => `var(${prefix}${name})`
53
+
54
+ return {
55
+ name: 'css-variables',
56
+ type: 'dark' as const,
57
+ colors: {
58
+ 'editor.foreground': v('foreground'),
59
+ 'editor.background': v('background'),
60
+ },
61
+ tokenColors: [
62
+ { scope: ['keyword.operator.accessor', 'meta.group.braces.round.function.arguments', 'meta.template.expression', 'markup.fenced_code meta.embedded.block'], settings: { foreground: v('foreground') } },
63
+ { scope: 'emphasis', settings: { fontStyle: 'italic' } },
64
+ { scope: ['strong', 'markup.heading.markdown', 'markup.bold.markdown'], settings: { fontStyle: 'bold' } },
65
+ { scope: ['markup.italic.markdown'], settings: { fontStyle: 'italic' } },
66
+ { scope: 'meta.link.inline.markdown', settings: { fontStyle: 'underline', foreground: v('token-link') } },
67
+ { scope: ['string', 'markup.fenced_code', 'markup.inline', 'string.quoted.docstring.multi.python'], settings: { foreground: v('token-string') } },
68
+ { scope: ['comment', 'string.quoted.docstring.multi', 'meta.diff.header.from-file', 'meta.diff.header.to-file'], settings: { foreground: v('token-comment') } },
69
+ { scope: ['constant.numeric', 'constant.language', 'constant.other.placeholder', 'constant.character.format.placeholder', 'variable.language.this', 'variable.other.object', 'variable.other.class', 'variable.other.constant', 'meta.property-name', 'meta.property-value', 'support'], settings: { foreground: v('token-constant') } },
70
+ { scope: ['keyword', 'storage.modifier', 'storage.type', 'storage.control.clojure', 'entity.name.function.clojure', 'entity.name.tag.yaml', 'support.function.node', 'support.type.property-name.json', 'punctuation.separator.key-value', 'punctuation.definition.template-expression'], settings: { foreground: v('token-keyword') } },
71
+ { scope: 'variable.parameter.function', settings: { foreground: v('token-parameter') } },
72
+ { scope: ['support.function', 'entity.name.type', 'entity.other.inherited-class', 'meta.function-call', 'meta.instance.constructor', 'entity.other.attribute-name', 'entity.name.function', 'constant.keyword.clojure'], settings: { foreground: v('token-function') } },
73
+ { scope: ['entity.name.tag', 'string.quoted', 'string.regexp', 'string.interpolated', 'string.template', 'string.unquoted.plain.out.yaml', 'keyword.other.template'], settings: { foreground: v('token-string-expression') } },
74
+ { scope: ['punctuation.definition.arguments', 'punctuation.definition.dict', 'punctuation.separator', 'meta.function-call.arguments'], settings: { foreground: v('token-punctuation') } },
75
+ { scope: ['markup.underline.link', 'punctuation.definition.metadata.markdown'], settings: { foreground: v('token-link') } },
76
+ { scope: ['beginning.punctuation.definition.list.markdown'], settings: { foreground: v('token-string') } },
77
+ { scope: ['punctuation.definition.string.begin.markdown', 'punctuation.definition.string.end.markdown', 'string.other.link.title.markdown', 'string.other.link.description.markdown'], settings: { foreground: v('token-keyword') } },
78
+ { scope: ['constant.numeric.decimal', 'constant.language.boolean', 'meta.var.exp.ts'], settings: { foreground: v('token-number') } },
79
+ { scope: ['meta.objectliteral'], settings: { foreground: v('token-object-literal') } },
80
+ { scope: ['support.variable.property'], settings: { foreground: v('token-property') } },
81
+ { scope: ['punctuation.definition.deleted.diff', 'markup.deleted.diff'], settings: { foreground: v('token-diff-deleted') } },
82
+ { scope: ['punctuation.definition.inserted.diff', 'markup.inserted.diff'], settings: { foreground: v('token-diff-inserted') } },
83
+ ],
84
+ }
85
+ }
86
+
87
+ const CSS_VARS_THEME = createCssVariablesTheme()
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Shiki highlighter — lazy singleton
91
+ // ---------------------------------------------------------------------------
92
+
93
+ type ShikiHighlighter = {
94
+ codeToHtml: (code: string, options: any) => string
95
+ getLoadedLanguages: () => string[]
96
+ loadLanguage: (lang: any) => Promise<void>
97
+ }
98
+
99
+ let highlighterPromise: Promise<ShikiHighlighter> | null = null
100
+ let highlighterInstance: ShikiHighlighter | null = null
101
+
102
+ async function getHighlighter(): Promise<ShikiHighlighter> {
103
+ if (highlighterInstance) return highlighterInstance
104
+ if (highlighterPromise) return highlighterPromise
105
+
106
+ highlighterPromise = (async () => {
107
+ const { createHighlighter } = await import('shiki')
108
+ const h = await createHighlighter({
109
+ themes: [CSS_VARS_THEME as any],
110
+ langs: ['typescript', 'tsx', 'javascript', 'jsx', 'python', 'bash', 'json', 'css', 'html', 'markdown', 'yaml', 'rust', 'go', 'sql', 'diff', 'swift', 'ruby', 'java', 'c', 'cpp'],
111
+ })
112
+ highlighterInstance = h
113
+ return h
114
+ })()
115
+
116
+ return highlighterPromise
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Syntax colors type + CSS variable conversion (from ray.so themes.ts)
121
+ // ---------------------------------------------------------------------------
122
+
123
+ type SyntaxColors = {
124
+ foreground: string
125
+ constant?: string
126
+ string?: string
127
+ comment?: string
128
+ keyword?: string
129
+ parameter?: string
130
+ function?: string
131
+ stringExpression?: string
132
+ punctuation?: string
133
+ link?: string
134
+ number?: string
135
+ property?: string
136
+ objectLiteral?: string
137
+ highlight?: string
138
+ highlightBorder?: string
139
+ highlightHover?: string
140
+ diffDeleted?: string
141
+ diffInserted?: string
142
+ }
143
+
144
+ function syntaxToCssVars(s: SyntaxColors): CSSProperties {
145
+ return {
146
+ '--ray-foreground': s.foreground,
147
+ '--ray-token-constant': s.constant,
148
+ '--ray-token-string': s.string,
149
+ '--ray-token-comment': s.comment,
150
+ '--ray-token-keyword': s.keyword,
151
+ '--ray-token-parameter': s.parameter,
152
+ '--ray-token-function': s.function,
153
+ '--ray-token-string-expression': s.stringExpression,
154
+ '--ray-token-punctuation': s.punctuation,
155
+ '--ray-token-link': s.link,
156
+ '--ray-token-number': s.number,
157
+ '--ray-token-property': s.property,
158
+ '--ray-token-object-literal': s.objectLiteral,
159
+ '--ray-highlight': s.highlight,
160
+ '--ray-highlight-border': s.highlightBorder,
161
+ '--ray-highlight-hover': s.highlightHover,
162
+ '--ray-token-diff-deleted': s.diffDeleted,
163
+ '--ray-token-diff-inserted': s.diffInserted,
164
+ } as CSSProperties
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Frame type — determines the visual wrapper around the code
169
+ // ---------------------------------------------------------------------------
170
+
171
+ type FrameType =
172
+ | 'default' // macOS traffic lights + title
173
+ | 'vercel' // gridlines + corner brackets
174
+ | 'stripe' // dashed gridlines + skewed stripe
175
+ | 'openai' // clean rounded window
176
+ | 'supabase' // filename header
177
+ | 'cloudflare' // gridlines + filename header
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Theme type
181
+ // ---------------------------------------------------------------------------
182
+
183
+ /** Font stack used for code rendering. Uses the preferred theme font with
184
+ * safe system monospace fallbacks. ray.so uses per-theme web fonts (Geist
185
+ * Mono, Source Code Pro, etc.) which are loaded via Next.js font optimization.
186
+ * In egaki video we can't rely on web fonts being installed, so the stack
187
+ * falls through to SF Mono / Menlo / Consolas which are available on every
188
+ * major OS and render digits correctly in all browsers. */
189
+ const CODE_FONT = '"SF Mono", "Menlo", "Consolas", "Liberation Mono", monospace'
190
+ const CODE_FONT_WEIGHT = 400
191
+
192
+ export type CodeBlockTheme = {
193
+ id: string
194
+ name: string
195
+ background: { from: string; to: string }
196
+ frame: FrameType
197
+ /** Decorative — the preferred font name from ray.so. Not used for rendering
198
+ * since web fonts aren't guaranteed; kept for documentation only. */
199
+ font?: string
200
+ /** Per-frame visual overrides */
201
+ frameColors?: {
202
+ windowBg?: string
203
+ windowBgLight?: string
204
+ border?: string
205
+ borderLight?: string
206
+ headerBg?: string
207
+ headerBgLight?: string
208
+ gridline?: string
209
+ gridlineLight?: string
210
+ lineNumber?: string
211
+ lineNumberLight?: string
212
+ frameBg?: string
213
+ frameBgLight?: string
214
+ }
215
+ syntax: {
216
+ light?: SyntaxColors
217
+ dark?: SyntaxColors
218
+ }
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Themes — curated from ray.so
223
+ // ---------------------------------------------------------------------------
224
+
225
+ export const CODE_THEMES: Record<string, CodeBlockTheme> = {
226
+ vercel: {
227
+ id: 'vercel',
228
+ name: 'Vercel',
229
+ background: { from: '#232323', to: '#1F1F1F' },
230
+ frame: 'vercel',
231
+ font: 'geist-mono',
232
+ frameColors: {
233
+ frameBg: '#000000',
234
+ frameBgLight: '#ffffff',
235
+ gridline: '#1a1a1a',
236
+ gridlineLight: '#ebebeb',
237
+ },
238
+ syntax: {
239
+ light: { foreground: 'hsla(0, 0%, 9%, 1)', constant: 'oklch(53.18% 0.2399 256.99)', string: 'oklch(51.75% 0.1453 147.65)', comment: 'hsla(0, 0%, 40%, 1)', keyword: 'oklch(53.5% 0.2058 2.84)', parameter: 'oklch(52.79% 0.1496 54.65)', function: 'oklch(47.18% 0.2579 304)', stringExpression: 'oklch(51.75% 0.1453 147.65)', punctuation: 'hsla(0, 0%, 9%, 1)', number: '#111111', property: 'oklch(53.18% 0.2399 256.99)', highlight: 'oklch(94.58% 0.0293 249.85)', highlightHover: 'oklch(94.58% 0.0293 249.85 / 30%)', highlightBorder: 'oklch(53.18% 0.2399 256.99)' },
240
+ dark: { foreground: 'hsla(0, 0%, 93%, 1)', constant: 'oklch(71.7% 0.1648 250.79)', string: 'oklch(73.1% 0.2158 148.29)', comment: 'hsla(0, 0%, 63%, 1)', keyword: 'oklch(69.36% 0.2223 3.91)', parameter: 'oklch(77.21% 0.1991 64.28)', function: 'oklch(69.87% 0.2037 309.51)', stringExpression: 'oklch(73.1% 0.2158 148.29)', punctuation: 'hsla(0, 0%, 93%, 1)', number: '#ffffff', property: 'oklch(71.7% 0.1648 250.79)', highlight: 'oklch(30.86% 0.1022 255.21)', highlightHover: 'oklch(30.86% 0.1022 255.21 / 30%)', highlightBorder: 'oklch(71.7% 0.1648 250.79)' },
241
+ },
242
+ },
243
+ stripe: {
244
+ id: 'stripe',
245
+ name: 'Stripe',
246
+ background: { from: '#0a2540', to: '#0a2540' },
247
+ frame: 'stripe',
248
+ font: 'source-code-pro',
249
+ frameColors: {
250
+ frameBg: '#0a2540',
251
+ windowBg: '#0c2e4e',
252
+ border: '#0f395e',
253
+ gridline: 'rgba(255, 255, 255, 0.1)',
254
+ lineNumber: '#55718d',
255
+ },
256
+ syntax: {
257
+ dark: { foreground: '#FFFFFF', constant: '#FFFFFF', string: '#ffa956', comment: '#a9bcce', keyword: '#8095ff', parameter: '#FF6B35', function: '#00d4ff', stringExpression: '#ffa956', punctuation: '#FFFFFF', number: '#ffa956', property: '#1abdc0', objectLiteral: '#1abdc0', highlight: 'rgba(255, 107, 53, 0.15)', highlightBorder: '#FF6B35', highlightHover: 'rgba(255, 107, 53, 0.08)' },
258
+ },
259
+ },
260
+ openai: {
261
+ id: 'openai',
262
+ name: 'OpenAI',
263
+ background: { from: '#000', to: '#000' },
264
+ frame: 'openai',
265
+ font: 'soehne-mono',
266
+ frameColors: {
267
+ frameBg: '#121a29',
268
+ frameBgLight: 'linear-gradient(238deg, #f1f0f4 0%, #f8f8fd 100%)',
269
+ windowBg: '#232b41',
270
+ windowBgLight: '#fff',
271
+ border: 'rgba(255, 255, 255, 0.1)',
272
+ borderLight: 'rgba(0, 0, 0, 0.1)',
273
+ lineNumber: 'rgba(255, 255, 255, 0.2)',
274
+ lineNumberLight: 'hsla(240, 12%, 71%, 1)',
275
+ },
276
+ syntax: {
277
+ light: { foreground: '#171717', constant: '#DF3079', string: '#171717', comment: 'hsla(240, 12%, 71%, 1)', keyword: '#2E95D3', parameter: '#ededed', function: '#00A67D', stringExpression: '#00a67d', punctuation: '#171717', number: '#e9950c', property: '#F22C3D' },
278
+ dark: { foreground: '#fff', constant: '#df3079', string: '#fff', comment: 'rgba(255,255,255,0.4)', keyword: '#2E95D3', parameter: '#fff', function: '#00A67D', stringExpression: '#00A67D', punctuation: '#fff', number: '#e9950c', property: '#F22C3D', highlight: 'rgba(255, 255, 255, 0.05)', highlightHover: 'rgba(255, 255, 255, 0.03)' },
279
+ },
280
+ },
281
+ supabase: {
282
+ id: 'supabase',
283
+ name: 'Supabase',
284
+ background: { from: '#121212', to: '#121212' },
285
+ frame: 'supabase',
286
+ font: 'jetbrains-mono',
287
+ frameColors: {
288
+ frameBg: '#121212',
289
+ frameBgLight: '#fcfcfc',
290
+ windowBg: '#171717',
291
+ windowBgLight: '#f8f8f8',
292
+ border: '#292929',
293
+ borderLight: '#dfdfdf',
294
+ headerBg: '#1f1f1f',
295
+ headerBgLight: '#fcfcfc',
296
+ },
297
+ syntax: {
298
+ light: { foreground: '#525252', constant: '#15593b', string: '#f1a10d', comment: '#7e7e7e', keyword: '#6b35dc', parameter: '#525252', function: '#15593b', stringExpression: '#f1a10d', punctuation: '#a0a0a0', number: '#525252', property: '#15593b', highlight: 'oklch(0.88 0.22 153.28 / 0.12)', highlightHover: 'oklch(0.88 0.22 153.28 / 0.06)', highlightBorder: '#009a55' },
299
+ dark: { foreground: '#ffffff', constant: '#3ecf8e', string: '#ffcda1', comment: '#7e7e7e', keyword: '#bda4ff', parameter: '#ffffff', function: '#3ecf8e', stringExpression: '#ffcda1', punctuation: '#ffffff', number: '#ededed', property: '#3ecf8e', highlight: '#232323', highlightHover: '#1D1D1D', highlightBorder: '#383838' },
300
+ },
301
+ },
302
+ cloudflare: {
303
+ id: 'cloudflare',
304
+ name: 'Cloudflare',
305
+ background: { from: '#0C0C0C', to: '#0C0C0C' },
306
+ frame: 'cloudflare',
307
+ font: 'ibm-plex-mono',
308
+ frameColors: {
309
+ frameBg: '#0c0c0c',
310
+ frameBgLight: '#f5f5f5',
311
+ windowBg: '#0c0c0c',
312
+ windowBgLight: '#ffffff',
313
+ border: 'transparent',
314
+ borderLight: 'transparent',
315
+ headerBg: '#0f0f0f',
316
+ headerBgLight: '#fafafa',
317
+ gridline: '#262626',
318
+ gridlineLight: '#e5e5e5',
319
+ },
320
+ syntax: {
321
+ light: { foreground: '#521000', constant: '#5a11cc', string: '#0876d9', comment: '#52100080', keyword: '#d94008', parameter: '#c77700', function: '#7612cc', stringExpression: '#0876d9', punctuation: '#52100080', number: '#5a11cc', property: '#5a11cc', highlight: 'rgba(255, 80, 10, 0.1)', highlightHover: 'rgba(255, 80, 10, 0.05)', highlightBorder: '#FF500A' },
322
+ dark: { foreground: '#E8E8E8', constant: '#79b8ff', string: '#0A95FF', comment: '#888888', keyword: '#FF7F4D', parameter: '#FFB366', function: '#B084FF', stringExpression: '#0A95FF', punctuation: '#AAAAAA', number: '#79b8ff', property: '#79b8ff', highlight: 'rgba(255, 80, 10, 0.15)', highlightHover: 'rgba(255, 80, 10, 0.08)', highlightBorder: '#FF500A' },
323
+ },
324
+ },
325
+ gemini: {
326
+ id: 'gemini', name: 'Gemini', background: { from: '#16181d', to: '#16181d' }, frame: 'default',
327
+ syntax: { dark: { foreground: '#abb2bf', constant: '#56b6c2', string: '#98c379', comment: '#5c6370', keyword: '#5c9dc7', parameter: '#d19a66', function: '#98c379', stringExpression: '#98c379', punctuation: '#abb2bf', number: '#56b6c2', property: '#56b6c2', highlight: 'rgba(92, 157, 199, 0.15)', highlightHover: 'rgba(92, 157, 199, 0.1)', highlightBorder: '#5c9dc7' } },
328
+ },
329
+ clerk: {
330
+ id: 'clerk', name: 'Clerk', background: { from: '#000000', to: '#000000' }, frame: 'default', font: 'geist-mono',
331
+ syntax: { dark: { foreground: '#ffffff', constant: '#86ef9b', string: '#5de3ff', comment: '#9394a1', keyword: '#bab1ff', parameter: '#86ef9b', function: '#bab1ff', stringExpression: '#5de3ff', punctuation: '#b7b8c2', number: '#86ef9b', property: '#86ef9b', highlight: '#5de3ff1a', highlightHover: '#5de3ff0d', highlightBorder: '#00000000' } },
332
+ },
333
+ prisma: {
334
+ id: 'prisma', name: 'Prisma', background: { from: '#000', to: '#000' }, frame: 'default',
335
+ syntax: { dark: { foreground: '#ffffff', constant: '#7F9CF5', string: '#71E8DF', comment: '#718096', keyword: '#71E8DF', parameter: '#71E8DF', function: '#7F9CF5', stringExpression: '#71E8DF', punctuation: '#FFFFFF', number: '#71E8DF', property: '#71E8DF', highlight: '#71e8de2e', highlightHover: '#71e8de1b', highlightBorder: '#71E8DF' } },
336
+ },
337
+ elevenlabs: {
338
+ id: 'elevenlabs', name: 'ElevenLabs', background: { from: '#000', to: '#000' }, frame: 'default', font: 'roboto-mono',
339
+ syntax: { dark: { foreground: '#fff', constant: '#8F8FFF', string: '#a1ffe0', comment: 'hsla(0, 0%, 63%, 1)', keyword: '#fff9b2', parameter: '#8F8FFF', function: '#ff8080', stringExpression: '#A1FFE0', punctuation: '#fff', number: '#8F8FFF', property: '#8F8FFF', highlight: 'hsla(240, 100%, 78%, 0.09)', highlightHover: 'hsla(240, 100%, 78%, 0.05)', highlightBorder: 'hsla(240, 100%, 78%, 0.45)' } },
340
+ },
341
+ triggerdev: {
342
+ id: 'triggerdev', name: 'Trigger.dev', background: { from: '#121317', to: '#121317' }, frame: 'default', font: 'geist-mono',
343
+ syntax: { dark: { foreground: '#CCCBFF', constant: '#9C9AF2', string: '#AFEC73', comment: '#5F6570', keyword: '#E888F8', parameter: '#CCCBFF', function: '#9684FF', stringExpression: '#AFEC73', punctuation: '#878C99', number: '#b5cea8', property: '#CCCBFF' } },
344
+ },
345
+ nuxt: {
346
+ id: 'nuxt', name: 'Nuxt', background: { from: '#292D3E', to: '#292D3E' }, frame: 'default', font: 'geist-mono',
347
+ syntax: { dark: { foreground: '#babed8', constant: '#BABED8', string: '#C3E88D', comment: '#676E95', keyword: '#C793EA', parameter: '#babed8', function: '#82AAFF', stringExpression: '#f07178', punctuation: '#89DDFF', number: '#F78C6C', property: '#f07178', highlight: 'rgba(113, 124, 180, 0.31)', highlightHover: 'rgba(113, 124, 180, 0.2)', highlightBorder: '#80CBC4' } },
348
+ },
349
+ tailwind: {
350
+ id: 'tailwind', name: 'Tailwind', background: { from: '#36B6F0', to: '#36B6F0' }, frame: 'default', font: 'fira-code',
351
+ syntax: { dark: { foreground: '#fff', constant: '#C1B2F9', string: '#C1B2F9', comment: 'rgba(255,255,255,0.4)', keyword: '#C1B2F9', punctuation: 'rgba(255,255,255,0.6)', number: '#C1B2F9', property: '#C1B2F9', function: '#fff', highlight: 'rgba(193,178,249,0.12)', highlightBorder: '#C1B2F9' } },
352
+ },
353
+ resend: {
354
+ id: 'resend', name: 'Resend', background: { from: '#B1B1B1', to: '#181818' }, frame: 'default', font: 'commit-mono',
355
+ syntax: { dark: { foreground: '#ffffff', constant: '#a7a7a7', string: '#a7a7a7', comment: '#666666', keyword: '#a7a7a7', function: '#ffffff', punctuation: '#a7a7a7', number: '#ffffff', property: '#a7a7a7' } },
356
+ },
357
+ auth0: {
358
+ id: 'auth0', name: 'Auth0', background: { from: '#171717', to: '#24173A' }, frame: 'default',
359
+ syntax: { dark: { foreground: '#F1F1F1', constant: '#99A7F1', string: '#98D2B2', comment: '#808080', keyword: '#B59DF8', parameter: '#F1F1F1', function: '#8B66F4', stringExpression: '#98D2B2', punctuation: '#FDE66F', number: '#B59DF8', property: '#B9C3F5', highlight: 'rgba(82, 139, 255, 0.24)', highlightHover: 'rgba(82, 139, 255, 0.12)', highlightBorder: '#528BFF' } },
360
+ },
361
+ noir: {
362
+ id: 'noir', name: 'Noir', background: { from: '#B1B1B1', to: '#181818' }, frame: 'default',
363
+ syntax: {
364
+ light: { foreground: '#111111', constant: '#666666', keyword: '#666666', function: '#111111', punctuation: '#666666', string: '#666666', comment: '#999999', number: '#111111', property: '#666666' },
365
+ dark: { foreground: '#ffffff', constant: '#a7a7a7', keyword: '#a7a7a7', function: '#ffffff', punctuation: '#a7a7a7', string: '#a7a7a7', comment: '#666666', number: '#ffffff', property: '#a7a7a7' },
366
+ },
367
+ },
368
+ mono: {
369
+ id: 'mono', name: 'Mono', background: { from: '#333', to: '#181818' }, frame: 'default',
370
+ syntax: { dark: { foreground: '#ffffff', constant: '#a7a7a7', keyword: '#a7a7a7', function: '#ffffff', punctuation: '#a7a7a7', string: '#a7a7a7', comment: '#666666', number: '#ffffff', property: '#a7a7a7' } },
371
+ },
372
+ bitmap: {
373
+ id: 'bitmap', name: 'Bitmap', background: { from: '#881616', to: '#F1393F' }, frame: 'default',
374
+ syntax: { dark: { foreground: '#FEFDFD', constant: '#E42B37', string: '#E42B37', comment: '#996B6D', keyword: '#EB6F6F', parameter: '#C88E8E', function: '#E42B37', stringExpression: '#EBB99D', punctuation: '#EB6F6F', number: '#E42B37', property: '#E42B37', highlight: 'hsla(355, 76%, 63%, 0.25)', highlightBorder: '#E42B37', highlightHover: 'hsla(355, 76%, 63%, 0.16)' } },
375
+ },
376
+ ice: {
377
+ id: 'ice', name: 'Ice', background: { from: '#fff', to: '#80deea' }, frame: 'default',
378
+ syntax: {
379
+ light: { foreground: '#1C1B29', constant: '#00B0E9', string: '#6ABAD8', comment: '#BDC0C1', keyword: '#81909D', parameter: '#1E3C78', function: '#1E3C78', stringExpression: '#7BBCD8', punctuation: '#1E3C78', number: '#00B0E9', property: '#00B0E9', highlight: 'rgba(0,167,219,0.1)', highlightBorder: '#00B0E9', highlightHover: 'rgba(0,167,219,0.05)' },
380
+ dark: { foreground: '#FFFFFF', constant: '#92DEF6', string: '#92DEF6', comment: '#5C6A70', keyword: '#BFC4C9', parameter: '#778CB6', function: '#778CB6', stringExpression: '#89C3DC', punctuation: '#778CB6', number: '#00B0E9', property: '#00B0E9', highlight: 'rgba(146,222,246,0.14)', highlightBorder: '#92DEF6', highlightHover: 'rgba(146,222,246,0.09)' },
381
+ },
382
+ },
383
+ sand: { id: 'sand', name: 'Sand', background: { from: '#EED5B6', to: '#AF8856' }, frame: 'default', syntax: { dark: { foreground: '#FFFFFF', constant: '#C2B181', string: '#C2B181', comment: '#837E77', keyword: '#D3B48C', parameter: '#F4A361', function: '#F4A361', stringExpression: '#EED5B8', punctuation: '#F4A361', number: '#C2B181', property: '#C2B181', highlight: 'rgba(244,163,97,0.14)', highlightBorder: '#F4A361', highlightHover: 'rgba(244,163,97,0.09)' } } },
384
+ forest: { id: 'forest', name: 'Forest', background: { from: '#506853', to: '#213223' }, frame: 'default', syntax: { dark: { foreground: '#FFFFFF', constant: '#6B8F71', string: '#C9C8BC', comment: '#555E56', keyword: '#AAB4A3', parameter: '#6B8F71', function: '#87B882', stringExpression: '#CCBD6E', punctuation: '#AAB4A3', number: '#AAB4A3', property: '#C9C7BC', highlight: 'rgba(170,180,163,0.14)', highlightBorder: '#6B8F71', highlightHover: 'rgba(170,180,163,0.09)' } } },
385
+ breeze: { id: 'breeze', name: 'Breeze', background: { from: '#CF2F98', to: '#6A3DEC' }, frame: 'default', syntax: { dark: { foreground: '#FFFFFF', constant: '#49E8F2', string: '#E9AEFE', comment: '#8A757D', keyword: '#6599FF', parameter: '#F8518D', function: '#F8518D', stringExpression: '#E9AEFE', punctuation: '#F8518D', number: '#55E7B2', property: '#49E8F2' } } },
386
+ candy: { id: 'candy', name: 'Candy', background: { from: '#A58EFB', to: '#E9BFF8' }, frame: 'default', syntax: { dark: { foreground: '#FFFFFF', constant: '#1AC8FF', string: '#DFD473', comment: '#807796', keyword: '#FF659C', parameter: '#1AC8FF', function: '#73DFA5', stringExpression: '#DFD473', punctuation: '#FF659C', number: '#7A7FFD', property: '#1AC8FF' } } },
387
+ crimson: { id: 'crimson', name: 'Crimson', background: { from: '#FF6363', to: '#733434' }, frame: 'default', syntax: { dark: { foreground: '#FEFDFD', constant: '#D15510', string: '#EBB99D', comment: '#895E60', keyword: '#EB6F6F', parameter: '#C88E8E', function: '#C88E8E', stringExpression: '#EBB99D', punctuation: '#EB6F6F', number: '#FDA97A', property: '#D15510' } } },
388
+ falcon: { id: 'falcon', name: 'Falcon', background: { from: '#BDE3EC', to: '#363654' }, frame: 'default', syntax: { dark: { foreground: '#FFFFFF', constant: '#799DB1', string: '#6A8697', comment: '#6D7E88', keyword: '#9AB6B2', parameter: '#6D88BB', function: '#6D88BB', stringExpression: '#789083', punctuation: '#9AB6B2', number: '#BD9C9C', property: '#799DB1' } } },
389
+ meadow: { id: 'meadow', name: 'Meadow', background: { from: '#59D499', to: '#A0872D' }, frame: 'default', syntax: { dark: { foreground: '#FFFFFF', constant: '#E4B165', string: '#E9EB9D', comment: '#708B6C', keyword: '#6DD79F', parameter: '#B3D767', function: '#B3D767', stringExpression: '#E9EB9D', punctuation: '#6DD79F', number: '#46B114', property: '#E4B165' } } },
390
+ midnight: { id: 'midnight', name: 'Midnight', background: { from: '#4CC8C8', to: '#202033' }, frame: 'default', syntax: { dark: { foreground: '#FFFFFF', constant: '#9681C2', string: '#6D86A4', comment: '#4A4C56', keyword: '#7DA9AB', parameter: '#51D0F8', function: '#51D0F8', stringExpression: '#6D86A4', punctuation: '#7DA9AB', number: '#75D2B1', property: '#9681C2' } } },
391
+ raindrop: { id: 'raindrop', name: 'Raindrop', background: { from: '#8EC7FB', to: '#1C55AA' }, frame: 'default', syntax: { dark: { foreground: '#E4F2FF', constant: '#008BB7', string: '#9DD8EB', comment: '#6C808B', keyword: '#2ED9FF', parameter: '#1AD6B5', function: '#1AD6B5', stringExpression: '#9DD8EB', punctuation: '#2ED9FF', number: '#9984EE', property: '#008BB7' } } },
392
+ sunset: { id: 'sunset', name: 'Sunset', background: { from: '#FFCF73', to: '#FF7A2F' }, frame: 'default', syntax: { dark: { foreground: '#FFFFFF', constant: '#E978A1', string: '#F9D38C', comment: '#878572', keyword: '#FFAF65', parameter: '#E2D66B', function: '#E2D66B', stringExpression: '#F9D38C', punctuation: '#FFAF65', number: '#E7CF55', property: '#E978A1' } } },
393
+ }
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // Language detection from filename or className
397
+ // ---------------------------------------------------------------------------
398
+
399
+ const EXT_TO_LANG: Record<string, string> = {
400
+ ts: 'tsx', tsx: 'tsx', js: 'javascript', jsx: 'jsx', py: 'python',
401
+ sh: 'bash', bash: 'bash', json: 'json', css: 'css', html: 'html',
402
+ md: 'markdown', yml: 'yaml', yaml: 'yaml', rs: 'rust', go: 'go',
403
+ sql: 'sql', diff: 'diff', swift: 'swift', rb: 'ruby', java: 'java',
404
+ c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', typescript: 'tsx',
405
+ javascript: 'javascript', python: 'python', ruby: 'ruby',
406
+ }
407
+
408
+ function detectLanguage(title?: string, language?: string): string {
409
+ if (language) {
410
+ return EXT_TO_LANG[language.toLowerCase()] || language.toLowerCase()
411
+ }
412
+ if (title) {
413
+ const ext = title.split('.').pop()?.toLowerCase()
414
+ if (ext && EXT_TO_LANG[ext]) return EXT_TO_LANG[ext]
415
+ }
416
+ return 'tsx'
417
+ }
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // useHighlightedHtml — async shiki highlighting with state
421
+ // ---------------------------------------------------------------------------
422
+
423
+ function useHighlightedHtml(code: string, lang: string, highlightLines: number[]): string {
424
+ const [html, setHtml] = useState('')
425
+
426
+ useEffect(() => {
427
+ let cancelled = false
428
+
429
+ void getHighlighter().then((h) => {
430
+ if (cancelled) return
431
+
432
+ // Load language on demand if not already loaded
433
+ const loaded = h.getLoadedLanguages()
434
+ const langToUse = loaded.includes(lang) ? lang : 'tsx'
435
+
436
+ const result = h.codeToHtml(code, {
437
+ lang: langToUse,
438
+ theme: 'css-variables',
439
+ transformers: [{
440
+ line(node: any, line: number) {
441
+ node.properties = node.properties || {}
442
+ node.properties['data-line'] = line
443
+ node.properties.style = (node.properties.style || '') + ';padding:0 16px;margin:0 -16px;display:inline-block;width:calc(100% + 32px);'
444
+ if (highlightLines.includes(line)) {
445
+ node.properties.style += 'background-color:var(--ray-highlight, rgba(255,255,255,0.06));'
446
+ node.properties['class'] = ((node.properties['class'] || '') + ' highlighted-line').trim()
447
+ }
448
+ },
449
+ }],
450
+ })
451
+ if (!cancelled) setHtml(result)
452
+ })
453
+
454
+ return () => { cancelled = true }
455
+ }, [code, lang, highlightLines.join(',')])
456
+
457
+ return html
458
+ }
459
+
460
+ // ---------------------------------------------------------------------------
461
+ // Frame sub-components
462
+ // ---------------------------------------------------------------------------
463
+
464
+ const FONT_SANS = '-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif'
465
+
466
+ /** Vercel: gridlines extending beyond the code + corner brackets */
467
+ /** Gridlines that extend beyond the code window to the edges of the
468
+ * nearest overflow:hidden ancestor (the CodeBlock outer container).
469
+ * Uses large fixed offsets instead of vw/vh so they stay contained
470
+ * when multiple CodeBlocks sit in a grid. */
471
+ function VercelGridlines({ color }: { color: string }) {
472
+ const line = { position: 'absolute' as const, background: color }
473
+ return (
474
+ <>
475
+ <span style={{ ...line, top: 0, left: -9999, width: 99999, height: 1 }} />
476
+ <span style={{ ...line, bottom: 0, left: -9999, width: 99999, height: 1 }} />
477
+ <span style={{ ...line, top: -9999, left: 0, width: 1, height: 99999 }} />
478
+ <span style={{ ...line, top: -9999, right: 0, width: 1, height: 99999 }} />
479
+ </>
480
+ )
481
+ }
482
+
483
+ function VercelBracket({ position, color }: { position: 'top-left' | 'bottom-right'; color: string }) {
484
+ const isTopLeft = position === 'top-left'
485
+ return (
486
+ <span style={{
487
+ position: 'absolute',
488
+ ...(isTopLeft ? { top: -12, left: -12 } : { bottom: -12, right: -12 }),
489
+ width: 25,
490
+ height: 25,
491
+ display: 'flex',
492
+ alignItems: 'center',
493
+ justifyContent: 'center',
494
+ }}>
495
+ {/* Horizontal bar */}
496
+ <span style={{ position: 'absolute', top: 12, width: '100%', height: 1, background: color }} />
497
+ {/* Vertical bar */}
498
+ <span style={{ position: 'absolute', left: 12, width: 1, height: '100%', background: color }} />
499
+ </span>
500
+ )
501
+ }
502
+
503
+ /** Stripe: dashed vertical gridlines + skewed stripe shape */
504
+ function StripeBackground({ gridlineColor, windowWidth }: { gridlineColor: string; windowWidth: number }) {
505
+ const positions = [0, 25, 50, 75, 100]
506
+ const lineStyle = (pct: number, solid?: boolean): CSSProperties => ({
507
+ position: 'absolute',
508
+ top: 0,
509
+ left: `${pct}%`,
510
+ width: 1,
511
+ height: '100%',
512
+ borderLeft: `1px ${solid ? 'solid' : 'dashed'} ${gridlineColor}`,
513
+ pointerEvents: 'none',
514
+ })
515
+ return (
516
+ <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none' }}>
517
+ {/* Gridlines */}
518
+ <div style={{ position: 'relative', width: windowWidth, height: '100%', margin: '0 auto' }}>
519
+ {positions.map((pct) => (
520
+ <div key={pct} style={lineStyle(pct, pct === 0 || pct === 100)} />
521
+ ))}
522
+ </div>
523
+ {/* Skewed stripe */}
524
+ <div style={{
525
+ position: 'absolute',
526
+ bottom: 0,
527
+ left: 0,
528
+ width: '100%',
529
+ height: '40%',
530
+ background: 'hsla(213.69, 52%, 97.828%)',
531
+ transform: 'skewY(-6deg)',
532
+ transformOrigin: '100% 0',
533
+ }}>
534
+ <div style={{ position: 'relative', width: windowWidth, height: '100%', margin: '0 auto' }}>
535
+ {positions.map((pct) => (
536
+ <div key={pct} style={{ ...lineStyle(pct, pct === 0 || pct === 100), borderColor: 'rgba(66, 71, 112, 0.15)' }} />
537
+ ))}
538
+ {/* Colored layer bars */}
539
+ <div style={{ position: 'absolute', bottom: 65, left: '75%', width: 500, height: 50 }}>
540
+ <div style={{ position: 'absolute', width: '100%', height: '100%', background: 'rgb(17, 239, 227)' }} />
541
+ <div style={{ position: 'absolute', width: '100%', height: 32, background: 'rgb(153, 102, 255)', transform: 'translate(50px, 50px)' }} />
542
+ <div style={{ position: 'absolute', width: '100%', height: 18, background: 'hsla(221.1, 99.822%, 44.876%)', transform: 'translate(50px, 32px)' }} />
543
+ </div>
544
+ </div>
545
+ </div>
546
+ </div>
547
+ )
548
+ }
549
+
550
+ /** Filename header for Supabase/Cloudflare style */
551
+ function FilenameHeader({ title, border, bg, titleColor, fontFamily }: { title: string; border: string; bg: string; titleColor: string; fontFamily: string }) {
552
+ return (
553
+ <div style={{
554
+ display: 'flex',
555
+ height: 40,
556
+ alignItems: 'center',
557
+ justifyContent: 'space-between',
558
+ padding: '0 16px',
559
+ borderBottom: `1px solid ${border}`,
560
+ background: bg,
561
+ fontFamily,
562
+ fontSize: 14,
563
+ color: titleColor,
564
+ }}>
565
+ <span>{title}</span>
566
+ </div>
567
+ )
568
+ }
569
+
570
+ /** Default frame: macOS traffic lights + title. Matches ray.so DefaultFrame
571
+ * header: 24px grid with 60px | 1fr | 60px columns, 9px dot gap, no border. */
572
+ function DefaultChrome({ title, titleColor }: { title?: string; titleColor: string }) {
573
+ return (
574
+ <div style={{
575
+ display: 'grid',
576
+ height: 24,
577
+ alignItems: 'center',
578
+ padding: '0 16px',
579
+ gap: 12,
580
+ gridTemplateColumns: '60px 1fr 60px',
581
+ }}>
582
+ <div style={{ display: 'flex', gap: 9 }}>
583
+ {['a', 'b', 'c'].map((k) => (
584
+ <div key={k} style={{ width: 14, height: 14, borderRadius: 7, backgroundColor: 'rgba(128,128,128,0.2)' }} />
585
+ ))}
586
+ </div>
587
+ <div style={{ textAlign: 'center', color: titleColor, fontSize: 12, fontWeight: 500, letterSpacing: '0.32px', lineHeight: '12px', fontFamily: FONT_SANS, overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
588
+ {title || ''}
589
+ </div>
590
+ <div />
591
+ </div>
592
+ )
593
+ }
594
+
595
+ // ---------------------------------------------------------------------------
596
+ // Editor — code display via dangerouslySetInnerHTML
597
+ // ---------------------------------------------------------------------------
598
+
599
+ function CodeEditor({ html, showLineNumbers, lineCount, cssVars, lineNumColor, fontSize, fontFamily, fontWeight }: {
600
+ html: string
601
+ showLineNumbers: boolean
602
+ lineCount: number
603
+ cssVars: CSSProperties
604
+ lineNumColor: string
605
+ fontSize: number
606
+ fontFamily: string
607
+ fontWeight: number
608
+ }) {
609
+ // Line number gutter widths computed from fontSize so they scale correctly.
610
+ // A monospace digit is ~0.6em wide; we size the gutter to fit the widest
611
+ // line number plus comfortable spacing.
612
+ // A monospace character is roughly 0.6em wide. Size the gutter to fit the
613
+ // widest line number plus comfortable spacing, all relative to fontSize.
614
+ const ch = Math.ceil(fontSize * 0.62)
615
+ const gutterDigits = lineCount > 99 ? 3 : 2
616
+ const gutterWidth = ch * (gutterDigits + 1) // extra ch for breathing room
617
+ const gutterGap = Math.round(fontSize * 0.8) // space between number and code
618
+ const gutterTotal = gutterWidth + gutterGap
619
+
620
+ const lineNumberCss = showLineNumbers ? `
621
+ .shiki code .line { padding-left: ${gutterTotal}px !important; }
622
+ .shiki code .line::before {
623
+ content: attr(data-line);
624
+ display: inline-block;
625
+ width: ${gutterWidth}px;
626
+ margin-right: ${gutterGap}px;
627
+ margin-left: -${gutterTotal}px;
628
+ color: ${lineNumColor};
629
+ text-align: right;
630
+ }
631
+ ` : ''
632
+
633
+ const editorCss = `
634
+ .shiki { margin: 0; background-color: transparent !important; font-family: inherit; white-space: pre-wrap; }
635
+ .shiki code { font-family: inherit; }
636
+ .shiki code .line { transition: width 0.2s, padding 0.2s, margin 0.2s; }
637
+ .highlighted-line { position: relative; }
638
+ .highlighted-line::after {
639
+ content: "";
640
+ position: absolute;
641
+ top: 0;
642
+ bottom: 0;
643
+ left: -1px;
644
+ width: 2px;
645
+ background-color: var(--ray-highlight-border, var(--ray-token-keyword));
646
+ }
647
+ ${lineNumberCss}
648
+ `
649
+
650
+ return (
651
+ <div style={{
652
+ padding: 16,
653
+ fontFamily,
654
+ fontWeight,
655
+ fontSize,
656
+ lineHeight: 1.7,
657
+ letterSpacing: '0.1px',
658
+ tabSize: 2,
659
+ fontVariantLigatures: 'none',
660
+ ...cssVars,
661
+ }}>
662
+ <style dangerouslySetInnerHTML={{ __html: editorCss }} />
663
+ <div dangerouslySetInnerHTML={{ __html: html }} />
664
+ </div>
665
+ )
666
+ }
667
+
668
+ // ---------------------------------------------------------------------------
669
+ // CodeBlock component
670
+ // ---------------------------------------------------------------------------
671
+
672
+ export interface CodeBlockProps {
673
+ children?: ReactNode
674
+ theme?: string
675
+ darkMode?: boolean
676
+ title?: string
677
+ language?: string
678
+ showLineNumbers?: boolean
679
+ highlightLines?: number[]
680
+ padding?: number
681
+ fontSize?: number
682
+ width?: number | string
683
+ height?: number | string
684
+ showBackground?: boolean
685
+ staggerFrames?: number
686
+ }
687
+
688
+ export function CodeBlock({
689
+ children,
690
+ theme: themeId = 'vercel',
691
+ darkMode = true,
692
+ title,
693
+ language,
694
+ showLineNumbers = false,
695
+ highlightLines = [],
696
+ padding = 32,
697
+ fontSize = 15,
698
+ width = 780,
699
+ height,
700
+ showBackground = true,
701
+ staggerFrames = 0,
702
+ }: CodeBlockProps) {
703
+ const frame = useCurrentFrame()
704
+ const { fps } = useVideoConfig()
705
+
706
+ const tp = useTweakpane('CodeBlock', {
707
+ theme: { value: themeId, options: Object.keys(CODE_THEMES).sort() },
708
+ darkMode,
709
+ showLineNumbers,
710
+ showBackground,
711
+ fontSize: { value: fontSize, min: 8, max: 32, step: 1 },
712
+ padding: { value: padding, min: 0, max: 80, step: 4 },
713
+ })
714
+
715
+ const resolvedThemeId = tp.theme
716
+ const themeData = (CODE_THEMES[resolvedThemeId] ?? CODE_THEMES.vercel)!
717
+ const fc = themeData.frameColors || {}
718
+
719
+ // Resolve dark/light mode
720
+ const hasDark = !!themeData.syntax.dark
721
+ const hasLight = !!themeData.syntax.light
722
+ const effectiveDark = hasDark && !hasLight ? true : hasLight && !hasDark ? false : tp.darkMode
723
+ const colors = (effectiveDark ? themeData.syntax.dark : themeData.syntax.light)!
724
+ const cssVars = syntaxToCssVars(colors)
725
+
726
+ // Extract code and highlight
727
+ const code = extractCodeString(children)
728
+ const lines = code.split('\n')
729
+ const lang = detectLanguage(title, language)
730
+ const html = useHighlightedHtml(code, lang, highlightLines)
731
+
732
+ // Entrance animation
733
+ const windowScale = staggerFrames > 0
734
+ ? spring({ frame, fps, config: { damping: 20, stiffness: 80 } })
735
+ : 1
736
+ const windowOpacity = staggerFrames > 0
737
+ ? interpolate(frame, [0, 10], [0, 1], { extrapolateRight: 'clamp' })
738
+ : 1
739
+
740
+ // Frame-specific colors
741
+ const frameBg = effectiveDark ? (fc.frameBg || 'transparent') : (fc.frameBgLight || 'transparent')
742
+ const windowBg = effectiveDark ? (fc.windowBg || 'rgba(0, 0, 0, 0.88)') : (fc.windowBgLight || 'rgba(255, 255, 255, 0.95)')
743
+ const borderColor = effectiveDark ? (fc.border || 'rgba(255,255,255,0.08)') : (fc.borderLight || 'rgba(0,0,0,0.08)')
744
+ const lineNumColor = effectiveDark ? (fc.lineNumber || 'rgba(255,255,255,0.2)') : (fc.lineNumberLight || 'rgba(0,0,0,0.2)')
745
+ const gridlineColor = effectiveDark ? (fc.gridline || '#1a1a1a') : (fc.gridlineLight || '#ebebeb')
746
+ const headerBg = effectiveDark ? (fc.headerBg || windowBg) : (fc.headerBgLight || windowBg)
747
+ const titleColor = effectiveDark ? 'rgba(255,255,255,0.5)' : 'rgba(0,0,0,0.5)'
748
+
749
+ // Background: either theme gradient or frame-specific solid
750
+ const outerBg = tp.showBackground
751
+ ? (themeData.frame === 'default'
752
+ ? `linear-gradient(140deg, ${themeData.background.from}, ${themeData.background.to})`
753
+ : frameBg)
754
+ : 'transparent'
755
+
756
+ const outerWidth = typeof width === 'number' ? width + tp.padding * 2 : width
757
+ const outerHeight = height ? (typeof height === 'number' ? height + tp.padding * 2 : height) : undefined
758
+
759
+ const editorEl = (
760
+ <CodeEditor
761
+ html={html}
762
+ showLineNumbers={tp.showLineNumbers}
763
+ lineCount={lines.length}
764
+ cssVars={cssVars}
765
+ lineNumColor={lineNumColor}
766
+ fontSize={tp.fontSize}
767
+ fontFamily={CODE_FONT}
768
+ fontWeight={CODE_FONT_WEIGHT}
769
+ />
770
+ )
771
+
772
+ function renderFrame() {
773
+ switch (themeData.frame) {
774
+ case 'vercel':
775
+ return (
776
+ <div style={{ position: 'relative' }}>
777
+ <VercelGridlines color={gridlineColor} />
778
+ <VercelBracket position="top-left" color="#515356" />
779
+ <VercelBracket position="bottom-right" color="#515356" />
780
+ {editorEl}
781
+ </div>
782
+ )
783
+
784
+ case 'stripe':
785
+ return (
786
+ <>
787
+ {tp.showBackground && (
788
+ <StripeBackground gridlineColor={fc.gridline || 'rgba(255,255,255,0.1)'} windowWidth={typeof width === 'number' ? width : 780} />
789
+ )}
790
+ <div style={{
791
+ position: 'relative',
792
+ zIndex: 1,
793
+ border: `1px solid ${borderColor}`,
794
+ borderRadius: 8,
795
+ background: windowBg,
796
+ boxShadow: 'rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px',
797
+ }}>
798
+ {editorEl}
799
+ </div>
800
+ </>
801
+ )
802
+
803
+ case 'openai':
804
+ return (
805
+ <div style={{
806
+ position: 'relative',
807
+ zIndex: 1,
808
+ border: `0.5px solid ${borderColor}`,
809
+ borderRadius: 8,
810
+ background: windowBg,
811
+ boxShadow: '0px 100px 89px 0px rgba(0,0,0,0.07), 0px 41.778px 37.182px 0px rgba(0,0,0,0.05), 0px 22.336px 19.879px 0px rgba(0,0,0,0.04), 0px 12.522px 11.144px 0px rgba(0,0,0,0.04), 0px 6.65px 5.919px 0px rgba(0,0,0,0.03), 0px 2.767px 2.463px 0px rgba(0,0,0,0.02)',
812
+ }}>
813
+ {editorEl}
814
+ </div>
815
+ )
816
+
817
+ case 'supabase':
818
+ return (
819
+ <div style={{
820
+ border: `1px solid ${borderColor}`,
821
+ borderRadius: 6,
822
+ background: windowBg,
823
+ overflow: 'hidden',
824
+ }}>
825
+ {title && <FilenameHeader title={title} border={borderColor} bg={headerBg} titleColor={effectiveDark ? '#fafafa' : '#171717'} fontFamily={CODE_FONT} />}
826
+ {editorEl}
827
+ </div>
828
+ )
829
+
830
+ case 'cloudflare':
831
+ return (
832
+ <div style={{ position: 'relative' }}>
833
+ <VercelGridlines color={gridlineColor} />
834
+ <div style={{ position: 'relative', background: windowBg }}>
835
+ {title && <FilenameHeader title={title} border={gridlineColor} bg={headerBg} titleColor={effectiveDark ? '#ededed' : 'oklch(14.5% 0 0)'} fontFamily={CODE_FONT} />}
836
+ {editorEl}
837
+ </div>
838
+ </div>
839
+ )
840
+
841
+ case 'default':
842
+ default: {
843
+ // ray.so uses box-shadow borders (not CSS border) + multi-layer shadow
844
+ const shadowBorder = effectiveDark
845
+ ? '0 0 0 1px rgba(255,255,255,0.06), 0 0 0 1.5px rgba(0,0,0,0.4)'
846
+ : '0 0 0 1px rgba(0,0,0,0.04), 0 0 0 1.5px rgba(0,0,0,0.08)'
847
+ const dropShadow = tp.showBackground
848
+ ? ', 0 2.8px 2.2px rgba(0,0,0,0.034), 0 6.7px 5.3px rgba(0,0,0,0.048), 0 12.5px 10px rgba(0,0,0,0.06), 0 22.3px 17.9px rgba(0,0,0,0.072), 0 41.8px 33.4px rgba(0,0,0,0.086), 0 100px 80px rgba(0,0,0,0.12)'
849
+ : ''
850
+ return (
851
+ <div style={{
852
+ display: 'flex',
853
+ flexDirection: 'column',
854
+ minHeight: 100,
855
+ paddingTop: 10,
856
+ borderRadius: 18,
857
+ background: windowBg,
858
+ overflow: 'hidden',
859
+ boxShadow: shadowBorder + dropShadow,
860
+ }}>
861
+ <DefaultChrome title={title} titleColor={titleColor} />
862
+ {editorEl}
863
+ </div>
864
+ )
865
+ }
866
+ }
867
+ }
868
+
869
+ return (
870
+ <CodeBlockContext.Provider value={true}>
871
+ <div style={{
872
+ display: 'flex',
873
+ alignItems: 'center',
874
+ justifyContent: 'center',
875
+ padding: tp.padding,
876
+ background: outerBg,
877
+ borderRadius: tp.showBackground ? (themeData.frame === 'default' ? 18 : 0) : 0,
878
+ width: outerWidth,
879
+ height: outerHeight,
880
+ transform: `scale(${windowScale})`,
881
+ opacity: windowOpacity,
882
+ overflow: 'hidden',
883
+ position: 'relative',
884
+ }}>
885
+ {renderFrame()}
886
+ </div>
887
+ </CodeBlockContext.Provider>
888
+ )
889
+ }
890
+
891
+ // ---------------------------------------------------------------------------
892
+ // Helper: extract code string from React children
893
+ // ---------------------------------------------------------------------------
894
+
895
+ function extractCodeString(children: ReactNode): string {
896
+ if (typeof children === 'string') return children.trimEnd()
897
+ if (children == null) return ''
898
+ if (typeof children === 'object' && 'props' in (children as any)) {
899
+ const el = children as any
900
+ if (el.props?.children != null) return extractCodeString(el.props.children)
901
+ }
902
+ if (Array.isArray(children)) return children.map(extractCodeString).join('')
903
+ return String(children).trimEnd()
904
+ }
905
+
906
+ // ---------------------------------------------------------------------------
907
+ // MDX code block wrapper
908
+ // ---------------------------------------------------------------------------
909
+
910
+ export function MdxCodeBlockWrapper({
911
+ children,
912
+ ...rest
913
+ }: { children?: ReactNode } & Record<string, any>) {
914
+ const isNested = useContext(CodeBlockContext)
915
+ if (isNested) return <pre {...rest}>{children}</pre>
916
+
917
+ let language: string | undefined
918
+ if (children && typeof children === 'object' && 'props' in (children as any)) {
919
+ const className = (children as any).props?.className
920
+ if (typeof className === 'string') {
921
+ const match = className.match(/language-(\w+)/)
922
+ if (match) language = match[1]
923
+ }
924
+ }
925
+
926
+ return (
927
+ <div style={{ width: '100%', maxWidth: '80%', display: 'flex', justifyContent: 'center' }}>
928
+ <CodeBlock
929
+ title={language || undefined}
930
+ language={language}
931
+ width="100%"
932
+ showBackground={false}
933
+ showLineNumbers
934
+ >
935
+ {children}
936
+ </CodeBlock>
937
+ </div>
938
+ )
939
+ }