@kushagradhawan/kookie-blocks 0.1.6 → 0.1.8

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 -64
  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 +252 -330
  44. package/src/components/code/LanguageBadge.tsx +65 -18
  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 +64 -62
  49. package/styles.css +60 -59
@@ -1,396 +1,293 @@
1
- import React, { useState, useRef, useEffect, useCallback, memo, type ReactNode } from "react";
2
- import { Box, Card, Flex, Button, Code, Theme } 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"
129
+ highContrast
183
130
  onClick={handleToggle}
184
131
  tooltip={isExpanded ? "Collapse" : "Expand"}
185
132
  aria-label={isExpanded ? "Collapse code" : "Expand code"}
186
133
  >
187
- <HugeiconsIcon icon={ArrowDown01Icon} style={chevronStyle} className="code-chevron" />
188
- </Button>
134
+ <HugeiconsIcon icon={ArrowDown01Icon} style={{ transform: chevronRotation }} className="code-chevron" strokeWidth={1.75} />
135
+ </IconButton>
189
136
  )}
190
137
  {showCopy && (
191
138
  <Button
192
139
  size="2"
193
140
  variant="ghost"
194
141
  color="gray"
142
+ highContrast
195
143
  onClick={handleCopy}
196
144
  tooltip={copied ? "Copied!" : "Copy"}
197
145
  aria-label={copied ? "Copied!" : "Copy code"}
198
146
  >
199
- <HugeiconsIcon icon={copied ? Tick01Icon : Copy01Icon} /> Copy
147
+ <HugeiconsIcon icon={copied ? Tick01Icon : Copy01Icon} strokeWidth={1.75} /> Copy
200
148
  </Button>
201
149
  )}
202
150
  </Flex>
203
151
  </Flex>
204
152
 
205
- <Box ref={contentRef} style={contentStyle} className="code-content">
206
- {highlighted ? (
207
- <Box className="code-block-content" width="100%" style={{ minWidth: 0 }} dangerouslySetInnerHTML={{ __html: highlighted }} />
208
- ) : (
209
- <pre className="code-block-content">
210
- <Code size="3">{code}</Code>
211
- </pre>
212
- )}
153
+ <Box ref={contentRef} style={{ maxHeight: collapsible ? `${contentMaxHeight}px` : undefined }} className={contentClassName}>
154
+ <ScrollArea type="auto" scrollbars="horizontal">
155
+ {isLoading ? <CodeSkeleton /> : children}
156
+ </ScrollArea>
213
157
  </Box>
214
158
 
215
- {shouldShowToggle && !isExpanded && <Box className="code-scroll-shadow visible" />}
159
+ {showToggle && !isExpanded && <Box className="code-scroll-shadow visible" />}
216
160
  </Flex>
217
161
  </Card>
218
162
  </Box>
219
163
  );
220
164
  });
221
165
 
222
- // ============================================
223
- // Children Code Section (for pre-highlighted MDX)
224
- // ============================================
225
-
226
- interface ChildrenCodeSectionProps {
227
- children: ReactNode;
228
- showCopy?: boolean;
229
- showLanguage?: boolean;
166
+ interface RuntimeCodeSectionProps {
167
+ code: string;
168
+ language: string;
169
+ showCopy: boolean;
170
+ showLanguage: boolean;
171
+ showLineNumbers: boolean;
172
+ collapsible: boolean;
173
+ collapsedHeight: number;
174
+ file?: string;
175
+ shikiConfig?: ShikiConfig;
230
176
  }
231
177
 
232
- function extractCodeFromChildren(children?: ReactNode): string {
233
- const extractText = (node: any): string => {
234
- if (typeof node === "string") return node;
235
- if (typeof node === "number") return String(node);
236
- if (!node) return "";
237
- if (Array.isArray(node)) return node.map(extractText).join("");
238
- if (typeof node === "object" && "props" in node) {
239
- const props = node.props;
240
- if (props?.children) return extractText(props.children);
241
- }
242
- return "";
243
- };
244
- return extractText(children);
245
- }
178
+ const RuntimeCodeSection = memo(function RuntimeCodeSection({
179
+ code,
180
+ language,
181
+ showCopy,
182
+ showLanguage,
183
+ showLineNumbers,
184
+ collapsible,
185
+ collapsedHeight,
186
+ file,
187
+ shikiConfig,
188
+ }: RuntimeCodeSectionProps) {
189
+ const [highlighted, setHighlighted] = useState<string | null>(null);
190
+ const [isLoading, setIsLoading] = useState(true);
191
+
192
+ // Memoize Shiki config to prevent unnecessary re-highlights
193
+ const shikiOptions = useMemo(() => {
194
+ const lightTheme = shikiConfig?.themes?.light || DEFAULT_LIGHT_THEME;
195
+ const darkTheme = shikiConfig?.themes?.dark || DEFAULT_DARK_THEME;
196
+
197
+ return {
198
+ lang: language as BundledLanguage,
199
+ themes: {
200
+ light: lightTheme as BundledTheme,
201
+ dark: darkTheme as BundledTheme,
202
+ },
203
+ defaultColor: false as const,
204
+ langAlias: shikiConfig?.langAlias,
205
+ transformers: shikiConfig?.transformers,
206
+ meta: shikiConfig?.meta ? { __raw: shikiConfig.meta } : undefined,
207
+ };
208
+ }, [language, shikiConfig?.themes?.light, shikiConfig?.themes?.dark, shikiConfig?.langAlias, shikiConfig?.transformers, shikiConfig?.meta]);
209
+
210
+ useEffect(() => {
211
+ let cancelled = false;
212
+ setIsLoading(true);
246
213
 
247
- function extractLanguageFromChildren(children?: ReactNode): string {
248
- const findLanguage = (node: any): string | null => {
249
- if (!node) return null;
250
- if (typeof node === "object" && "props" in node) {
251
- const props = node.props;
252
- const className = props?.className || props?.class || "";
253
- if (typeof className === "string") {
254
- const match = className.match(/language-([\w-]+)/i);
255
- if (match) return match[1];
256
- }
257
- if (props?.children) {
258
- if (Array.isArray(props.children)) {
259
- for (const child of props.children) {
260
- const lang = findLanguage(child);
261
- if (lang) return lang;
214
+ codeToHtml(code, shikiOptions)
215
+ .then((html) => {
216
+ if (!cancelled) {
217
+ setHighlighted(html);
218
+ setIsLoading(false);
219
+ }
220
+ })
221
+ .catch((error) => {
222
+ if (!cancelled) {
223
+ setHighlighted(null);
224
+ setIsLoading(false);
225
+ if (process.env.NODE_ENV === "development") {
226
+ console.error("[CodeBlock] Shiki highlighting failed:", error);
262
227
  }
263
- } else {
264
- return findLanguage(props.children);
265
228
  }
266
- }
267
- }
268
- return null;
269
- };
270
- return findLanguage(children) || "text";
271
- }
272
-
273
- function formatLanguageLabel(lang: string): string {
274
- const aliasMap: Record<string, string> = {
275
- tsx: "TSX",
276
- ts: "TS",
277
- jsx: "JSX",
278
- js: "JS",
279
- javascript: "JS",
280
- typescript: "TS",
281
- css: "CSS",
282
- html: "HTML",
283
- json: "JSON",
284
- bash: "SH",
285
- sh: "SH",
286
- shell: "SH",
287
- text: "plaintext",
288
- };
289
- return aliasMap[lang.toLowerCase()] || lang.toLowerCase();
290
- }
291
-
292
- const ChildrenCodeSection = memo(function ChildrenCodeSection({ children, showCopy = true, showLanguage = true }: ChildrenCodeSectionProps) {
293
- const [isExpanded, setIsExpanded] = useState(false);
294
- const [contentHeight, setContentHeight] = useState(COLLAPSED_HEIGHT);
295
- const [copied, setCopied] = useState(false);
296
- const contentRef = useRef<HTMLDivElement>(null);
297
- const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
298
-
299
- const code = extractCodeFromChildren(children);
300
- const language = extractLanguageFromChildren(children);
301
- const displayLanguage = formatLanguageLabel(language);
302
-
303
- const shouldShowToggle = contentHeight > COLLAPSED_HEIGHT;
304
-
305
- useEffect(() => {
306
- if (contentRef.current) {
307
- setContentHeight(contentRef.current.scrollHeight);
308
- }
309
- }, [children]);
229
+ });
310
230
 
311
- useEffect(() => {
312
231
  return () => {
313
- if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
232
+ cancelled = true;
314
233
  };
315
- }, []);
316
-
317
- const handleCopy = useCallback(async () => {
318
- if (!code.trim()) return;
319
- try {
320
- await navigator.clipboard.writeText(code);
321
- setCopied(true);
322
- if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
323
- resetTimeoutRef.current = setTimeout(() => setCopied(false), 2000);
324
- } catch {
325
- // Silently fail
326
- }
327
- }, [code]);
234
+ }, [code, shikiOptions]);
328
235
 
329
- const contentStyle: React.CSSProperties = {
330
- maxHeight: isExpanded ? `${contentHeight}px` : `${COLLAPSED_HEIGHT}px`,
331
- };
236
+ return (
237
+ <CodeCard
238
+ code={code}
239
+ language={language}
240
+ showCopy={showCopy}
241
+ showLanguage={showLanguage}
242
+ showLineNumbers={showLineNumbers}
243
+ collapsible={collapsible}
244
+ collapsedHeight={collapsedHeight}
245
+ file={file}
246
+ isLoading={isLoading}
247
+ >
248
+ {highlighted ? <Box dangerouslySetInnerHTML={{ __html: highlighted }} /> : null}
249
+ </CodeCard>
250
+ );
251
+ });
332
252
 
333
- const handleToggle = useCallback(() => {
334
- setIsExpanded((prev) => !prev);
335
- }, []);
253
+ interface ChildrenCodeSectionProps {
254
+ children: ReactNode;
255
+ showCopy: boolean;
256
+ showLanguage: boolean;
257
+ showLineNumbers: boolean;
258
+ collapsible: boolean;
259
+ collapsedHeight: number;
260
+ file?: string;
261
+ }
336
262
 
337
- const chevronStyle: React.CSSProperties = {
338
- transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
339
- };
263
+ const ChildrenCodeSection = memo(function ChildrenCodeSection({
264
+ children,
265
+ showCopy,
266
+ showLanguage,
267
+ showLineNumbers,
268
+ collapsible,
269
+ collapsedHeight,
270
+ file,
271
+ }: ChildrenCodeSectionProps) {
272
+ const code = extractTextFromChildren(children);
273
+ const language = extractLanguageFromChildren(children);
340
274
 
341
275
  return (
342
- <Box position="relative">
343
- <Card size="1" variant="soft">
344
- <Flex direction="column" gap="3">
345
- <Flex gap="2" justify="between" align="start">
346
- {showLanguage && (
347
- <Code size="1" color="gray" highContrast>
348
- {displayLanguage}
349
- </Code>
350
- )}
351
- <Flex align="center" gap="2" className="code-action-buttons">
352
- {shouldShowToggle && (
353
- <Button
354
- size="2"
355
- variant="ghost"
356
- color="gray"
357
- onClick={handleToggle}
358
- tooltip={isExpanded ? "Collapse" : "Expand"}
359
- aria-label={isExpanded ? "Collapse code" : "Expand code"}
360
- >
361
- <HugeiconsIcon icon={ArrowDown01Icon} style={chevronStyle} className="code-chevron" />
362
- </Button>
363
- )}
364
- {showCopy && (
365
- <Button
366
- size="2"
367
- variant="ghost"
368
- color="gray"
369
- onClick={handleCopy}
370
- tooltip={copied ? "Copied!" : "Copy"}
371
- aria-label={copied ? "Copied!" : "Copy code"}
372
- >
373
- <HugeiconsIcon icon={copied ? Tick01Icon : Copy01Icon} /> Copy
374
- </Button>
375
- )}
376
- </Flex>
377
- </Flex>
378
-
379
- <Box ref={contentRef} style={contentStyle} className="code-content">
380
- <div className="code-block-content">{children}</div>
381
- </Box>
382
-
383
- {shouldShowToggle && !isExpanded && <Box className="code-scroll-shadow visible" />}
384
- </Flex>
385
- </Card>
386
- </Box>
276
+ <CodeCard
277
+ code={code}
278
+ language={language}
279
+ showCopy={showCopy}
280
+ showLanguage={showLanguage}
281
+ showLineNumbers={showLineNumbers}
282
+ collapsible={collapsible}
283
+ collapsedHeight={collapsedHeight}
284
+ file={file}
285
+ >
286
+ {children}
287
+ </CodeCard>
387
288
  );
388
289
  });
389
290
 
390
- // ============================================
391
- // Main CodeBlock Component
392
- // ============================================
393
-
394
291
  export function CodeBlock({
395
292
  children,
396
293
  code,
@@ -398,33 +295,58 @@ export function CodeBlock({
398
295
  preview,
399
296
  showCopy = true,
400
297
  showLanguage = true,
401
- lightTheme,
402
- darkTheme,
298
+ showLineNumbers = true,
299
+ shikiConfig,
403
300
  background,
404
301
  backgroundProps,
302
+ collapsible = true,
303
+ collapsedHeight = DEFAULT_COLLAPSED_HEIGHT,
304
+ file,
405
305
  }: CodeBlockProps) {
406
- const hasCode = code || (children && React.Children.count(children) > 0);
407
306
  const displayLanguage = language || extractLanguageFromChildren(children) || "text";
408
307
 
409
308
  return (
410
- <Box className="docs-code-block" mt="6" mb="8">
411
- <Flex direction="column" gap="2">
412
- {preview && (
413
- <PreviewSection background={background} backgroundProps={backgroundProps}>
414
- {preview}
415
- </PreviewSection>
416
- )}
417
-
418
- {code && (
419
- <CodeSection code={code} language={displayLanguage} showCopy={showCopy} showLanguage={showLanguage} lightTheme={lightTheme} darkTheme={darkTheme} />
420
- )}
421
-
422
- {children && !code && (
423
- <ChildrenCodeSection showCopy={showCopy} showLanguage={showLanguage}>
424
- {children}
425
- </ChildrenCodeSection>
426
- )}
427
- </Flex>
428
- </Box>
309
+ <CodeBlockContext.Provider value={true}>
310
+ <Box className="docs-code-block" mt="6" mb="8">
311
+ <Flex direction="column" gap="2">
312
+ {preview && (
313
+ <PreviewSection background={background} backgroundProps={backgroundProps}>
314
+ {preview}
315
+ </PreviewSection>
316
+ )}
317
+
318
+ {code && (
319
+ <RuntimeCodeSection
320
+ code={code}
321
+ language={displayLanguage}
322
+ showCopy={showCopy}
323
+ showLanguage={showLanguage}
324
+ showLineNumbers={showLineNumbers}
325
+ collapsible={collapsible}
326
+ collapsedHeight={collapsedHeight}
327
+ file={file}
328
+ shikiConfig={shikiConfig}
329
+ />
330
+ )}
331
+
332
+ {children && !code && (
333
+ <ChildrenCodeSection
334
+ showCopy={showCopy}
335
+ showLanguage={showLanguage}
336
+ showLineNumbers={showLineNumbers}
337
+ collapsible={collapsible}
338
+ collapsedHeight={collapsedHeight}
339
+ file={file}
340
+ >
341
+ {children}
342
+ </ChildrenCodeSection>
343
+ )}
344
+ </Flex>
345
+ </Box>
346
+ </CodeBlockContext.Provider>
429
347
  );
430
348
  }
349
+
350
+ export function useCodeBlockContext() {
351
+ return useContext(CodeBlockContext);
352
+ }