@kushagradhawan/kookie-blocks 0.1.7 → 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 -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 +250 -341
  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 +62 -1
  49. package/styles.css +57 -1
@@ -1,409 +1,293 @@
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"
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">
153
+ <Box ref={contentRef} style={{ maxHeight: collapsible ? `${contentMaxHeight}px` : undefined }} className={contentClassName}>
206
154
  <ScrollArea type="auto" scrollbars="horizontal">
207
- {highlighted ? (
208
- <Box dangerouslySetInnerHTML={{ __html: highlighted }} />
209
- ) : (
210
- <pre>
211
- <Code size="3">{code}</Code>
212
- </pre>
213
- )}
155
+ {isLoading ? <CodeSkeleton /> : children}
214
156
  </ScrollArea>
215
157
  </Box>
216
158
 
217
- {shouldShowToggle && !isExpanded && <Box className="code-scroll-shadow visible" />}
159
+ {showToggle && !isExpanded && <Box className="code-scroll-shadow visible" />}
218
160
  </Flex>
219
161
  </Card>
220
162
  </Box>
221
163
  );
222
164
  });
223
165
 
224
- // ============================================
225
- // Children Code Section (for pre-highlighted MDX)
226
- // ============================================
227
-
228
- interface ChildrenCodeSectionProps {
229
- children: ReactNode;
230
- showCopy?: boolean;
231
- 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;
232
176
  }
233
177
 
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
- }
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]);
248
209
 
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;
210
+ useEffect(() => {
211
+ let cancelled = false;
212
+ setIsLoading(true);
213
+
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);
273
227
  }
274
- } else {
275
- return findLanguage(props.children);
276
228
  }
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]);
229
+ });
321
230
 
322
- useEffect(() => {
323
231
  return () => {
324
- if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
232
+ cancelled = true;
325
233
  };
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]);
234
+ }, [code, shikiOptions]);
339
235
 
340
- const contentStyle: React.CSSProperties = {
341
- maxHeight: isExpanded ? `${contentHeight}px` : `${COLLAPSED_HEIGHT}px`,
342
- };
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
+ });
343
252
 
344
- const handleToggle = useCallback(() => {
345
- setIsExpanded((prev) => !prev);
346
- }, []);
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
+ }
347
262
 
348
- const chevronStyle: React.CSSProperties = {
349
- transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
350
- };
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);
351
274
 
352
275
  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>
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>
400
288
  );
401
289
  });
402
290
 
403
- // ============================================
404
- // Main CodeBlock Component
405
- // ============================================
406
-
407
291
  export function CodeBlock({
408
292
  children,
409
293
  code,
@@ -411,33 +295,58 @@ export function CodeBlock({
411
295
  preview,
412
296
  showCopy = true,
413
297
  showLanguage = true,
414
- lightTheme,
415
- darkTheme,
298
+ showLineNumbers = true,
299
+ shikiConfig,
416
300
  background,
417
301
  backgroundProps,
302
+ collapsible = true,
303
+ collapsedHeight = DEFAULT_COLLAPSED_HEIGHT,
304
+ file,
418
305
  }: CodeBlockProps) {
419
- const hasCode = code || (children && React.Children.count(children) > 0);
420
306
  const displayLanguage = language || extractLanguageFromChildren(children) || "text";
421
307
 
422
308
  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>
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>
442
347
  );
443
348
  }
349
+
350
+ export function useCodeBlockContext() {
351
+ return useContext(CodeBlockContext);
352
+ }