@kushagradhawan/kookie-blocks 0.1.7 → 0.1.9

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 (49) hide show
  1. package/components.css +67 -1
  2. package/dist/cjs/components/code/CodeBlock.d.ts +2 -1
  3. package/dist/cjs/components/code/CodeBlock.d.ts.map +1 -1
  4. package/dist/cjs/components/code/CodeBlock.js +1 -1
  5. package/dist/cjs/components/code/CodeBlock.js.map +3 -3
  6. package/dist/cjs/components/code/LanguageBadge.d.ts +9 -0
  7. package/dist/cjs/components/code/LanguageBadge.d.ts.map +1 -1
  8. package/dist/cjs/components/code/LanguageBadge.js +1 -1
  9. package/dist/cjs/components/code/LanguageBadge.js.map +3 -3
  10. package/dist/cjs/components/code/index.d.ts +5 -2
  11. package/dist/cjs/components/code/index.d.ts.map +1 -1
  12. package/dist/cjs/components/code/index.js +1 -1
  13. package/dist/cjs/components/code/index.js.map +3 -3
  14. package/dist/cjs/components/code/types.d.ts +132 -13
  15. package/dist/cjs/components/code/types.d.ts.map +1 -1
  16. package/dist/cjs/components/code/types.js +1 -1
  17. package/dist/cjs/components/code/types.js.map +3 -3
  18. package/dist/cjs/components/code/useCodeCard.d.ts +20 -0
  19. package/dist/cjs/components/code/useCodeCard.d.ts.map +1 -0
  20. package/dist/cjs/components/code/useCodeCard.js +2 -0
  21. package/dist/cjs/components/code/useCodeCard.js.map +7 -0
  22. package/dist/esm/components/code/CodeBlock.d.ts +2 -1
  23. package/dist/esm/components/code/CodeBlock.d.ts.map +1 -1
  24. package/dist/esm/components/code/CodeBlock.js +1 -1
  25. package/dist/esm/components/code/CodeBlock.js.map +3 -3
  26. package/dist/esm/components/code/LanguageBadge.d.ts +9 -0
  27. package/dist/esm/components/code/LanguageBadge.d.ts.map +1 -1
  28. package/dist/esm/components/code/LanguageBadge.js +1 -1
  29. package/dist/esm/components/code/LanguageBadge.js.map +3 -3
  30. package/dist/esm/components/code/index.d.ts +5 -2
  31. package/dist/esm/components/code/index.d.ts.map +1 -1
  32. package/dist/esm/components/code/index.js +1 -1
  33. package/dist/esm/components/code/index.js.map +3 -3
  34. package/dist/esm/components/code/types.d.ts +132 -13
  35. package/dist/esm/components/code/types.d.ts.map +1 -1
  36. package/dist/esm/components/code/types.js +1 -0
  37. package/dist/esm/components/code/types.js.map +4 -4
  38. package/dist/esm/components/code/useCodeCard.d.ts +20 -0
  39. package/dist/esm/components/code/useCodeCard.d.ts.map +1 -0
  40. package/dist/esm/components/code/useCodeCard.js +2 -0
  41. package/dist/esm/components/code/useCodeCard.js.map +7 -0
  42. package/package.json +1 -1
  43. package/src/components/code/CodeBlock.tsx +248 -341
  44. package/src/components/code/LanguageBadge.tsx +67 -20
  45. package/src/components/code/index.ts +6 -3
  46. package/src/components/code/types.ts +219 -27
  47. package/src/components/code/useCodeCard.ts +82 -0
  48. package/src/components/index.css +62 -1
  49. package/styles.css +57 -1
@@ -1,182 +1,128 @@
1
- import React, { useState, useRef, useEffect, useCallback, memo, type ReactNode } from "react";
2
- import { Box, Card, Flex, Button, Code, Theme, ScrollArea } from "@kushagradhawan/kookie-ui";
1
+ import React, { useState, useEffect, useMemo, memo, createContext, useContext, type ReactNode } from "react";
2
+ import { Box, Card, Code, Flex, Button, Text, Theme, ScrollArea, IconButton } from "@kushagradhawan/kookie-ui";
3
3
  import { HugeiconsIcon } from "@hugeicons/react";
4
4
  import { Copy01Icon, Tick01Icon, ArrowDown01Icon } from "@hugeicons/core-free-icons";
5
- import { codeToHtml } from "shiki";
6
- import type { CodeBlockProps } from "./types";
5
+ import { codeToHtml, type BundledLanguage, type BundledTheme } from "shiki";
6
+ import type { CodeBlockProps, ShikiConfig, PreviewBackgroundProps } from "./types";
7
+ import { extractTextFromChildren, extractLanguageFromChildren } from "./types";
8
+ import { useCodeCard } from "./useCodeCard";
9
+ import { LanguageBadge } from "./LanguageBadge";
7
10
 
8
- const COLLAPSED_HEIGHT = 360;
11
+ const CodeBlockContext = createContext<boolean>(false);
12
+
13
+ const DEFAULT_COLLAPSED_HEIGHT = 360;
9
14
  const DEFAULT_LIGHT_THEME = "one-light";
10
15
  const DEFAULT_DARK_THEME = "one-dark-pro";
11
16
 
12
- // ============================================
13
- // Preview Section
14
- // ============================================
15
-
16
17
  interface PreviewSectionProps {
17
18
  children: ReactNode;
18
19
  background?: "none" | "dots" | string;
19
- backgroundProps?: {
20
- dotSize?: number;
21
- color?: string;
22
- backgroundColor?: string;
23
- height?: string;
24
- width?: string;
25
- radius?: string;
26
- };
20
+ backgroundProps?: PreviewBackgroundProps;
27
21
  }
28
22
 
29
23
  function PreviewSection({ children, background = "none", backgroundProps = {} }: PreviewSectionProps) {
30
24
  const { dotSize = 24, color = "var(--gray-10)", backgroundColor = "var(--gray-2)", height, width = "100%", radius = "3" } = backgroundProps;
31
25
 
32
- if (background === "none") {
33
- return (
34
- <Card size="1" variant="soft">
35
- <Flex justify="center" align="center" py="4">
36
- <Theme fontFamily="sans">{children}</Theme>
37
- </Flex>
38
- </Card>
39
- );
40
- }
26
+ const backgroundStyle = useMemo((): React.CSSProperties | undefined => {
27
+ if (background === "none") return undefined;
28
+
29
+ if (background === "dots") {
30
+ return {
31
+ backgroundImage: `radial-gradient(circle, ${color} 1px, transparent 1px)`,
32
+ borderRadius: `var(--radius-${radius})`,
33
+ backgroundSize: `${dotSize}px ${dotSize}px`,
34
+ backgroundPosition: "center",
35
+ backgroundColor,
36
+ width,
37
+ ...(height && { height }),
38
+ };
39
+ }
41
40
 
42
- if (background === "dots") {
43
- const dotsStyle: React.CSSProperties = {
44
- backgroundImage: `radial-gradient(circle, ${color} 1px, transparent 1px)`,
45
- borderRadius: `var(--radius-${radius})`,
46
- backgroundSize: `${dotSize}px ${dotSize}px`,
41
+ // Image background
42
+ return {
43
+ backgroundImage: `url(${background})`,
44
+ backgroundSize: "cover",
47
45
  backgroundPosition: "center",
48
- backgroundColor,
46
+ backgroundRepeat: "no-repeat",
47
+ borderRadius: `var(--radius-${radius})`,
49
48
  width,
50
49
  ...(height && { height }),
51
50
  };
52
-
53
- return (
54
- <Card size="1" variant="soft">
55
- <Flex justify="center" align="center" py="4" style={dotsStyle}>
56
- <Theme fontFamily="sans">{children}</Theme>
57
- </Flex>
58
- </Card>
59
- );
60
- }
61
-
62
- const imageStyle: React.CSSProperties = {
63
- backgroundImage: `url(${background})`,
64
- backgroundSize: "cover",
65
- backgroundPosition: "center",
66
- backgroundRepeat: "no-repeat",
67
- borderRadius: `var(--radius-${radius})`,
68
- width,
69
- ...(height && { height }),
70
- };
51
+ }, [background, color, backgroundColor, dotSize, height, width, radius]);
71
52
 
72
53
  return (
73
54
  <Card size="1" variant="soft">
74
- <Flex justify="center" align="center" py="4" style={imageStyle}>
55
+ <Flex justify="center" align="center" py="4" style={backgroundStyle}>
75
56
  <Theme fontFamily="sans">{children}</Theme>
76
57
  </Flex>
77
58
  </Card>
78
59
  );
79
60
  }
80
61
 
81
- // ============================================
82
- // Code Section (for runtime highlighting)
83
- // ============================================
84
-
85
- interface CodeSectionProps {
62
+ interface CodeCardProps {
86
63
  code: string;
87
64
  language: string;
88
- showCopy?: boolean;
89
- showLanguage?: boolean;
90
- lightTheme?: string;
91
- darkTheme?: string;
65
+ showCopy: boolean;
66
+ showLanguage: boolean;
67
+ showLineNumbers: boolean;
68
+ collapsible: boolean;
69
+ collapsedHeight: number;
70
+ file?: string;
71
+ isLoading?: boolean;
72
+ children: ReactNode;
92
73
  }
93
74
 
94
- const CodeSection = memo(function CodeSection({
95
- code,
96
- language,
97
- showCopy = true,
98
- showLanguage = true,
99
- lightTheme = DEFAULT_LIGHT_THEME,
100
- darkTheme = DEFAULT_DARK_THEME,
101
- }: CodeSectionProps) {
102
- const [highlighted, setHighlighted] = useState<string | null>(null);
103
- const [isExpanded, setIsExpanded] = useState(false);
104
- const [contentHeight, setContentHeight] = useState(COLLAPSED_HEIGHT);
105
- const [copied, setCopied] = useState(false);
106
- const contentRef = useRef<HTMLDivElement>(null);
107
- const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
108
-
109
- const shouldShowToggle = contentHeight > COLLAPSED_HEIGHT;
110
-
111
- useEffect(() => {
112
- let cancelled = false;
113
- codeToHtml(code, {
114
- lang: language,
115
- themes: { light: lightTheme, dark: darkTheme },
116
- defaultColor: false,
117
- })
118
- .then((html) => {
119
- if (!cancelled) setHighlighted(html);
120
- })
121
- .catch(() => {
122
- if (!cancelled) setHighlighted(null);
123
- });
124
- return () => {
125
- cancelled = true;
126
- };
127
- }, [code, language, lightTheme, darkTheme]);
128
-
129
- useEffect(() => {
130
- if (contentRef.current) {
131
- setContentHeight(contentRef.current.scrollHeight);
132
- }
133
- }, [highlighted]);
134
-
135
- useEffect(() => {
136
- return () => {
137
- if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
138
- };
139
- }, []);
140
-
141
- const handleCopy = useCallback(async () => {
142
- if (!code.trim()) return;
143
- try {
144
- await navigator.clipboard.writeText(code);
145
- setCopied(true);
146
- if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
147
- resetTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
148
- } catch {
149
- // Silently fail
150
- }
151
- }, [code]);
152
-
153
- const displayLanguage = language === "text" ? "plaintext" : language;
75
+ function CodeSkeleton() {
76
+ // Generate varied line widths for visual interest
77
+ const lineWidths = ["85%", "70%", "90%", "60%", "75%", "80%"];
154
78
 
155
- const handleToggle = useCallback(() => {
156
- setIsExpanded((prev) => !prev);
157
- }, []);
79
+ return (
80
+ <Box className="code-skeleton">
81
+ {lineWidths.map((width, index) => (
82
+ <Box key={index} className="code-skeleton-line" style={{ width }} />
83
+ ))}
84
+ </Box>
85
+ );
86
+ }
158
87
 
159
- const contentStyle: React.CSSProperties = {
160
- maxHeight: isExpanded ? `${contentHeight}px` : `${COLLAPSED_HEIGHT}px`,
161
- };
88
+ const CodeCard = memo(function CodeCard({
89
+ code,
90
+ language,
91
+ showCopy,
92
+ showLanguage,
93
+ showLineNumbers,
94
+ collapsible,
95
+ collapsedHeight,
96
+ file,
97
+ isLoading = false,
98
+ children,
99
+ }: CodeCardProps) {
100
+ const { isExpanded, shouldShowToggle, copied, contentRef, contentMaxHeight, handleToggle, handleCopy } = useCodeCard({
101
+ code,
102
+ collapsedHeight,
103
+ });
162
104
 
163
- const chevronStyle: React.CSSProperties = {
164
- transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
165
- };
105
+ const showToggle = collapsible && shouldShowToggle;
106
+ const chevronRotation = isExpanded ? "rotate(180deg)" : "rotate(0deg)";
107
+ const contentClassName = showLineNumbers ? "code-content" : "code-content hide-line-numbers";
166
108
 
167
109
  return (
168
110
  <Box position="relative">
169
111
  <Card size="1" variant="soft">
170
- <Flex direction="column" gap="3">
171
- <Flex gap="2" justify="between" align="start">
172
- {showLanguage && (
173
- <Code size="1" color="gray" highContrast>
174
- {displayLanguage}
175
- </Code>
176
- )}
177
- <Flex align="center" gap="2" className="code-action-buttons">
178
- {shouldShowToggle && (
179
- <Button
112
+ <Flex direction="column">
113
+ <Flex justify="between" align="start" gap="2">
114
+ <Flex align="center" gap="2">
115
+ {showLanguage && <LanguageBadge language={language} />}
116
+ {file && (
117
+ <Text size="1" color="gray" highContrast>
118
+ {file}
119
+ </Text>
120
+ )}
121
+ </Flex>
122
+
123
+ <Flex align="center" className="code-action-buttons">
124
+ {showToggle && (
125
+ <IconButton
180
126
  size="2"
181
127
  variant="ghost"
182
128
  color="gray"
@@ -184,8 +130,8 @@ const CodeSection = memo(function CodeSection({
184
130
  tooltip={isExpanded ? "Collapse" : "Expand"}
185
131
  aria-label={isExpanded ? "Collapse code" : "Expand code"}
186
132
  >
187
- <HugeiconsIcon icon={ArrowDown01Icon} style={chevronStyle} className="code-chevron" />
188
- </Button>
133
+ <HugeiconsIcon icon={ArrowDown01Icon} style={{ transform: chevronRotation }} className="code-chevron" strokeWidth={1.75} />
134
+ </IconButton>
189
135
  )}
190
136
  {showCopy && (
191
137
  <Button
@@ -196,214 +142,150 @@ const CodeSection = memo(function CodeSection({
196
142
  tooltip={copied ? "Copied!" : "Copy"}
197
143
  aria-label={copied ? "Copied!" : "Copy code"}
198
144
  >
199
- <HugeiconsIcon icon={copied ? Tick01Icon : Copy01Icon} /> Copy
145
+ <HugeiconsIcon icon={copied ? Tick01Icon : Copy01Icon} strokeWidth={1.75} /> Copy
200
146
  </Button>
201
147
  )}
202
148
  </Flex>
203
149
  </Flex>
204
150
 
205
- <Box ref={contentRef} style={contentStyle} className="code-content">
151
+ <Box ref={contentRef} style={{ maxHeight: collapsible ? `${contentMaxHeight}px` : undefined }} className={contentClassName}>
206
152
  <ScrollArea type="auto" scrollbars="horizontal">
207
- {highlighted ? (
208
- <Box dangerouslySetInnerHTML={{ __html: highlighted }} />
209
- ) : (
210
- <pre>
211
- <Code size="3">{code}</Code>
212
- </pre>
213
- )}
153
+ {isLoading ? <CodeSkeleton /> : children}
214
154
  </ScrollArea>
215
155
  </Box>
216
156
 
217
- {shouldShowToggle && !isExpanded && <Box className="code-scroll-shadow visible" />}
157
+ {showToggle && !isExpanded && <Box className="code-scroll-shadow visible" />}
218
158
  </Flex>
219
159
  </Card>
220
160
  </Box>
221
161
  );
222
162
  });
223
163
 
224
- // ============================================
225
- // Children Code Section (for pre-highlighted MDX)
226
- // ============================================
227
-
228
- interface ChildrenCodeSectionProps {
229
- children: ReactNode;
230
- showCopy?: boolean;
231
- showLanguage?: boolean;
164
+ interface RuntimeCodeSectionProps {
165
+ code: string;
166
+ language: string;
167
+ showCopy: boolean;
168
+ showLanguage: boolean;
169
+ showLineNumbers: boolean;
170
+ collapsible: boolean;
171
+ collapsedHeight: number;
172
+ file?: string;
173
+ shikiConfig?: ShikiConfig;
232
174
  }
233
175
 
234
- function extractCodeFromChildren(children?: ReactNode): string {
235
- const extractText = (node: any): string => {
236
- if (typeof node === "string") return node;
237
- if (typeof node === "number") return String(node);
238
- if (!node) return "";
239
- if (Array.isArray(node)) return node.map(extractText).join("");
240
- if (typeof node === "object" && "props" in node) {
241
- const props = node.props;
242
- if (props?.children) return extractText(props.children);
243
- }
244
- return "";
245
- };
246
- return extractText(children);
247
- }
176
+ const RuntimeCodeSection = memo(function RuntimeCodeSection({
177
+ code,
178
+ language,
179
+ showCopy,
180
+ showLanguage,
181
+ showLineNumbers,
182
+ collapsible,
183
+ collapsedHeight,
184
+ file,
185
+ shikiConfig,
186
+ }: RuntimeCodeSectionProps) {
187
+ const [highlighted, setHighlighted] = useState<string | null>(null);
188
+ const [isLoading, setIsLoading] = useState(true);
189
+
190
+ // Memoize Shiki config to prevent unnecessary re-highlights
191
+ const shikiOptions = useMemo(() => {
192
+ const lightTheme = shikiConfig?.themes?.light || DEFAULT_LIGHT_THEME;
193
+ const darkTheme = shikiConfig?.themes?.dark || DEFAULT_DARK_THEME;
194
+
195
+ return {
196
+ lang: language as BundledLanguage,
197
+ themes: {
198
+ light: lightTheme as BundledTheme,
199
+ dark: darkTheme as BundledTheme,
200
+ },
201
+ defaultColor: false as const,
202
+ langAlias: shikiConfig?.langAlias,
203
+ transformers: shikiConfig?.transformers,
204
+ meta: shikiConfig?.meta ? { __raw: shikiConfig.meta } : undefined,
205
+ };
206
+ }, [language, shikiConfig?.themes?.light, shikiConfig?.themes?.dark, shikiConfig?.langAlias, shikiConfig?.transformers, shikiConfig?.meta]);
248
207
 
249
- function extractLanguageFromChildren(children?: ReactNode): string {
250
- const findLanguage = (node: any): string | null => {
251
- if (!node) return null;
252
- if (typeof node === "object" && "props" in node) {
253
- const props = node.props;
254
-
255
- // Check data-language attribute (rehype-pretty-code)
256
- if (props?.["data-language"]) {
257
- return props["data-language"];
258
- }
259
-
260
- // Check className for language-xxx
261
- const className = props?.className || props?.class || "";
262
- if (typeof className === "string") {
263
- const match = className.match(/language-([\w-]+)/i);
264
- if (match) return match[1];
265
- }
266
-
267
- // Recursively check children
268
- if (props?.children) {
269
- if (Array.isArray(props.children)) {
270
- for (const child of props.children) {
271
- const lang = findLanguage(child);
272
- if (lang) return lang;
208
+ useEffect(() => {
209
+ let cancelled = false;
210
+ setIsLoading(true);
211
+
212
+ codeToHtml(code, shikiOptions)
213
+ .then((html) => {
214
+ if (!cancelled) {
215
+ setHighlighted(html);
216
+ setIsLoading(false);
217
+ }
218
+ })
219
+ .catch((error) => {
220
+ if (!cancelled) {
221
+ setHighlighted(null);
222
+ setIsLoading(false);
223
+ if (process.env.NODE_ENV === "development") {
224
+ console.error("[CodeBlock] Shiki highlighting failed:", error);
273
225
  }
274
- } else {
275
- return findLanguage(props.children);
276
226
  }
277
- }
278
- }
279
- return null;
280
- };
281
- return findLanguage(children) || "text";
282
- }
283
-
284
- function formatLanguageLabel(lang: string): string {
285
- const aliasMap: Record<string, string> = {
286
- tsx: "TSX",
287
- ts: "TS",
288
- jsx: "JSX",
289
- js: "JS",
290
- javascript: "JS",
291
- typescript: "TS",
292
- css: "CSS",
293
- html: "HTML",
294
- json: "JSON",
295
- bash: "SH",
296
- sh: "SH",
297
- shell: "SH",
298
- text: "plaintext",
299
- };
300
- return aliasMap[lang.toLowerCase()] || lang.toLowerCase();
301
- }
302
-
303
- const ChildrenCodeSection = memo(function ChildrenCodeSection({ children, showCopy = true, showLanguage = true }: ChildrenCodeSectionProps) {
304
- const [isExpanded, setIsExpanded] = useState(false);
305
- const [contentHeight, setContentHeight] = useState(COLLAPSED_HEIGHT);
306
- const [copied, setCopied] = useState(false);
307
- const contentRef = useRef<HTMLDivElement>(null);
308
- const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
309
-
310
- const code = extractCodeFromChildren(children);
311
- const language = extractLanguageFromChildren(children);
312
- const displayLanguage = formatLanguageLabel(language);
313
-
314
- const shouldShowToggle = contentHeight > COLLAPSED_HEIGHT;
315
-
316
- useEffect(() => {
317
- if (contentRef.current) {
318
- setContentHeight(contentRef.current.scrollHeight);
319
- }
320
- }, [children]);
227
+ });
321
228
 
322
- useEffect(() => {
323
229
  return () => {
324
- if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
230
+ cancelled = true;
325
231
  };
326
- }, []);
327
-
328
- const handleCopy = useCallback(async () => {
329
- if (!code.trim()) return;
330
- try {
331
- await navigator.clipboard.writeText(code);
332
- setCopied(true);
333
- if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
334
- resetTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
335
- } catch {
336
- // Silently fail
337
- }
338
- }, [code]);
232
+ }, [code, shikiOptions]);
339
233
 
340
- const contentStyle: React.CSSProperties = {
341
- maxHeight: isExpanded ? `${contentHeight}px` : `${COLLAPSED_HEIGHT}px`,
342
- };
234
+ return (
235
+ <CodeCard
236
+ code={code}
237
+ language={language}
238
+ showCopy={showCopy}
239
+ showLanguage={showLanguage}
240
+ showLineNumbers={showLineNumbers}
241
+ collapsible={collapsible}
242
+ collapsedHeight={collapsedHeight}
243
+ file={file}
244
+ isLoading={isLoading}
245
+ >
246
+ {highlighted ? <Box dangerouslySetInnerHTML={{ __html: highlighted }} /> : null}
247
+ </CodeCard>
248
+ );
249
+ });
343
250
 
344
- const handleToggle = useCallback(() => {
345
- setIsExpanded((prev) => !prev);
346
- }, []);
251
+ interface ChildrenCodeSectionProps {
252
+ children: ReactNode;
253
+ showCopy: boolean;
254
+ showLanguage: boolean;
255
+ showLineNumbers: boolean;
256
+ collapsible: boolean;
257
+ collapsedHeight: number;
258
+ file?: string;
259
+ }
347
260
 
348
- const chevronStyle: React.CSSProperties = {
349
- transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
350
- };
261
+ const ChildrenCodeSection = memo(function ChildrenCodeSection({
262
+ children,
263
+ showCopy,
264
+ showLanguage,
265
+ showLineNumbers,
266
+ collapsible,
267
+ collapsedHeight,
268
+ file,
269
+ }: ChildrenCodeSectionProps) {
270
+ const code = extractTextFromChildren(children);
271
+ const language = extractLanguageFromChildren(children);
351
272
 
352
273
  return (
353
- <Box position="relative">
354
- <Card size="1" variant="soft">
355
- <Flex direction="column" gap="3">
356
- <Flex gap="2" justify="between" align="start">
357
- {showLanguage && (
358
- <Code size="1" color="gray" highContrast>
359
- {displayLanguage}
360
- </Code>
361
- )}
362
- <Flex align="center" gap="2" className="code-action-buttons">
363
- {shouldShowToggle && (
364
- <Button
365
- size="2"
366
- variant="ghost"
367
- color="gray"
368
- onClick={handleToggle}
369
- tooltip={isExpanded ? "Collapse" : "Expand"}
370
- aria-label={isExpanded ? "Collapse code" : "Expand code"}
371
- >
372
- <HugeiconsIcon icon={ArrowDown01Icon} style={chevronStyle} className="code-chevron" />
373
- </Button>
374
- )}
375
- {showCopy && (
376
- <Button
377
- size="2"
378
- variant="ghost"
379
- color="gray"
380
- onClick={handleCopy}
381
- tooltip={copied ? "Copied!" : "Copy"}
382
- aria-label={copied ? "Copied!" : "Copy code"}
383
- >
384
- <HugeiconsIcon icon={copied ? Tick01Icon : Copy01Icon} /> Copy
385
- </Button>
386
- )}
387
- </Flex>
388
- </Flex>
389
-
390
- <Box ref={contentRef} style={contentStyle} className="code-content">
391
- <ScrollArea type="auto" scrollbars="horizontal">
392
- {children}
393
- </ScrollArea>
394
- </Box>
395
-
396
- {shouldShowToggle && !isExpanded && <Box className="code-scroll-shadow visible" />}
397
- </Flex>
398
- </Card>
399
- </Box>
274
+ <CodeCard
275
+ code={code}
276
+ language={language}
277
+ showCopy={showCopy}
278
+ showLanguage={showLanguage}
279
+ showLineNumbers={showLineNumbers}
280
+ collapsible={collapsible}
281
+ collapsedHeight={collapsedHeight}
282
+ file={file}
283
+ >
284
+ {children}
285
+ </CodeCard>
400
286
  );
401
287
  });
402
288
 
403
- // ============================================
404
- // Main CodeBlock Component
405
- // ============================================
406
-
407
289
  export function CodeBlock({
408
290
  children,
409
291
  code,
@@ -411,33 +293,58 @@ export function CodeBlock({
411
293
  preview,
412
294
  showCopy = true,
413
295
  showLanguage = true,
414
- lightTheme,
415
- darkTheme,
296
+ showLineNumbers = true,
297
+ shikiConfig,
416
298
  background,
417
299
  backgroundProps,
300
+ collapsible = true,
301
+ collapsedHeight = DEFAULT_COLLAPSED_HEIGHT,
302
+ file,
418
303
  }: CodeBlockProps) {
419
- const hasCode = code || (children && React.Children.count(children) > 0);
420
304
  const displayLanguage = language || extractLanguageFromChildren(children) || "text";
421
305
 
422
306
  return (
423
- <Box className="docs-code-block" mt="6" mb="8">
424
- <Flex direction="column" gap="2">
425
- {preview && (
426
- <PreviewSection background={background} backgroundProps={backgroundProps}>
427
- {preview}
428
- </PreviewSection>
429
- )}
430
-
431
- {code && (
432
- <CodeSection code={code} language={displayLanguage} showCopy={showCopy} showLanguage={showLanguage} lightTheme={lightTheme} darkTheme={darkTheme} />
433
- )}
434
-
435
- {children && !code && (
436
- <ChildrenCodeSection showCopy={showCopy} showLanguage={showLanguage}>
437
- {children}
438
- </ChildrenCodeSection>
439
- )}
440
- </Flex>
441
- </Box>
307
+ <CodeBlockContext.Provider value={true}>
308
+ <Box className="docs-code-block" mt="6" mb="8">
309
+ <Flex direction="column" gap="2">
310
+ {preview && (
311
+ <PreviewSection background={background} backgroundProps={backgroundProps}>
312
+ {preview}
313
+ </PreviewSection>
314
+ )}
315
+
316
+ {code && (
317
+ <RuntimeCodeSection
318
+ code={code}
319
+ language={displayLanguage}
320
+ showCopy={showCopy}
321
+ showLanguage={showLanguage}
322
+ showLineNumbers={showLineNumbers}
323
+ collapsible={collapsible}
324
+ collapsedHeight={collapsedHeight}
325
+ file={file}
326
+ shikiConfig={shikiConfig}
327
+ />
328
+ )}
329
+
330
+ {children && !code && (
331
+ <ChildrenCodeSection
332
+ showCopy={showCopy}
333
+ showLanguage={showLanguage}
334
+ showLineNumbers={showLineNumbers}
335
+ collapsible={collapsible}
336
+ collapsedHeight={collapsedHeight}
337
+ file={file}
338
+ >
339
+ {children}
340
+ </ChildrenCodeSection>
341
+ )}
342
+ </Flex>
343
+ </Box>
344
+ </CodeBlockContext.Provider>
442
345
  );
443
346
  }
347
+
348
+ export function useCodeBlockContext() {
349
+ return useContext(CodeBlockContext);
350
+ }