@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.
- 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 +250 -341
- package/src/components/code/LanguageBadge.tsx +65 -18
- 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,409 +1,293 @@
|
|
|
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"
|
|
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={
|
|
188
|
-
</
|
|
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={
|
|
153
|
+
<Box ref={contentRef} style={{ maxHeight: collapsible ? `${contentMaxHeight}px` : undefined }} className={contentClassName}>
|
|
206
154
|
<ScrollArea type="auto" scrollbars="horizontal">
|
|
207
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
349
|
-
|
|
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
|
-
<
|
|
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>
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
<
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
{
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
}
|