@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.
- package/components.css +67 -1
- package/dist/cjs/components/code/CodeBlock.d.ts +2 -1
- package/dist/cjs/components/code/CodeBlock.d.ts.map +1 -1
- package/dist/cjs/components/code/CodeBlock.js +1 -1
- package/dist/cjs/components/code/CodeBlock.js.map +3 -3
- package/dist/cjs/components/code/LanguageBadge.d.ts +9 -0
- package/dist/cjs/components/code/LanguageBadge.d.ts.map +1 -1
- package/dist/cjs/components/code/LanguageBadge.js +1 -1
- package/dist/cjs/components/code/LanguageBadge.js.map +3 -3
- package/dist/cjs/components/code/index.d.ts +5 -2
- package/dist/cjs/components/code/index.d.ts.map +1 -1
- package/dist/cjs/components/code/index.js +1 -1
- package/dist/cjs/components/code/index.js.map +3 -3
- package/dist/cjs/components/code/types.d.ts +132 -13
- package/dist/cjs/components/code/types.d.ts.map +1 -1
- package/dist/cjs/components/code/types.js +1 -1
- package/dist/cjs/components/code/types.js.map +3 -3
- package/dist/cjs/components/code/useCodeCard.d.ts +20 -0
- package/dist/cjs/components/code/useCodeCard.d.ts.map +1 -0
- package/dist/cjs/components/code/useCodeCard.js +2 -0
- package/dist/cjs/components/code/useCodeCard.js.map +7 -0
- package/dist/esm/components/code/CodeBlock.d.ts +2 -1
- package/dist/esm/components/code/CodeBlock.d.ts.map +1 -1
- package/dist/esm/components/code/CodeBlock.js +1 -1
- package/dist/esm/components/code/CodeBlock.js.map +3 -3
- package/dist/esm/components/code/LanguageBadge.d.ts +9 -0
- package/dist/esm/components/code/LanguageBadge.d.ts.map +1 -1
- package/dist/esm/components/code/LanguageBadge.js +1 -1
- package/dist/esm/components/code/LanguageBadge.js.map +3 -3
- package/dist/esm/components/code/index.d.ts +5 -2
- package/dist/esm/components/code/index.d.ts.map +1 -1
- package/dist/esm/components/code/index.js +1 -1
- package/dist/esm/components/code/index.js.map +3 -3
- package/dist/esm/components/code/types.d.ts +132 -13
- package/dist/esm/components/code/types.d.ts.map +1 -1
- package/dist/esm/components/code/types.js +1 -0
- package/dist/esm/components/code/types.js.map +4 -4
- package/dist/esm/components/code/useCodeCard.d.ts +20 -0
- package/dist/esm/components/code/useCodeCard.d.ts.map +1 -0
- package/dist/esm/components/code/useCodeCard.js +2 -0
- package/dist/esm/components/code/useCodeCard.js.map +7 -0
- package/package.json +1 -1
- package/src/components/code/CodeBlock.tsx +248 -341
- package/src/components/code/LanguageBadge.tsx +67 -20
- package/src/components/code/index.ts +6 -3
- package/src/components/code/types.ts +219 -27
- package/src/components/code/useCodeCard.ts +82 -0
- package/src/components/index.css +62 -1
- package/styles.css +57 -1
|
@@ -1,182 +1,128 @@
|
|
|
1
|
-
import React, { useState,
|
|
2
|
-
import { Box, Card, Flex, Button,
|
|
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
|
|
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
|
-
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
backgroundImage: `
|
|
45
|
-
|
|
46
|
-
backgroundSize: `${dotSize}px ${dotSize}px`,
|
|
41
|
+
// Image background
|
|
42
|
+
return {
|
|
43
|
+
backgroundImage: `url(${background})`,
|
|
44
|
+
backgroundSize: "cover",
|
|
47
45
|
backgroundPosition: "center",
|
|
48
|
-
|
|
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={
|
|
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
|
|
89
|
-
showLanguage
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
|
164
|
-
|
|
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"
|
|
171
|
-
<Flex
|
|
172
|
-
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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={
|
|
188
|
-
</
|
|
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={
|
|
151
|
+
<Box ref={contentRef} style={{ maxHeight: collapsible ? `${contentMaxHeight}px` : undefined }} className={contentClassName}>
|
|
206
152
|
<ScrollArea type="auto" scrollbars="horizontal">
|
|
207
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
<
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
<
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
{
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
}
|