@kushagradhawan/kookie-blocks 0.1.4 → 0.1.6
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 +62 -18
- package/dist/cjs/components/code/CodeBlock.d.ts +1 -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/PreviewSection.js +1 -1
- package/dist/cjs/components/code/PreviewSection.js.map +2 -2
- package/dist/esm/components/code/CodeBlock.d.ts +1 -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/PreviewSection.js +1 -1
- package/dist/esm/components/code/PreviewSection.js.map +2 -2
- package/package.json +2 -2
- package/src/components/code/CodeBlock.tsx +395 -84
- package/src/components/code/PreviewSection.tsx +3 -3
- package/src/components/index.css +57 -18
- package/styles.css +52 -18
|
@@ -1,65 +1,396 @@
|
|
|
1
|
-
import React, { type ReactNode } from "react";
|
|
2
|
-
import { Card, Flex } from "@kushagradhawan/kookie-ui";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { PreviewSection } from "./PreviewSection";
|
|
1
|
+
import React, { useState, useRef, useEffect, useCallback, memo, type ReactNode } from "react";
|
|
2
|
+
import { Box, Card, Flex, Button, Code, Theme } from "@kushagradhawan/kookie-ui";
|
|
3
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
4
|
+
import { Copy01Icon, Tick01Icon, ArrowDown01Icon } from "@hugeicons/core-free-icons";
|
|
5
|
+
import { codeToHtml } from "shiki";
|
|
7
6
|
import type { CodeBlockProps } from "./types";
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
const COLLAPSED_HEIGHT = 360;
|
|
9
|
+
const DEFAULT_LIGHT_THEME = "one-light";
|
|
10
|
+
const DEFAULT_DARK_THEME = "one-dark-pro";
|
|
11
|
+
|
|
12
|
+
// ============================================
|
|
13
|
+
// Preview Section
|
|
14
|
+
// ============================================
|
|
15
|
+
|
|
16
|
+
interface PreviewSectionProps {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
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
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function PreviewSection({ children, background = "none", backgroundProps = {} }: PreviewSectionProps) {
|
|
30
|
+
const { dotSize = 24, color = "var(--gray-10)", backgroundColor = "var(--gray-2)", height, width = "100%", radius = "3" } = backgroundProps;
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
);
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
|
|
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`,
|
|
47
|
+
backgroundPosition: "center",
|
|
48
|
+
backgroundColor,
|
|
49
|
+
width,
|
|
50
|
+
...(height && { height }),
|
|
51
|
+
};
|
|
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
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Card size="1" variant="soft">
|
|
74
|
+
<Flex justify="center" align="center" py="4" style={imageStyle}>
|
|
75
|
+
<Theme fontFamily="sans">{children}</Theme>
|
|
76
|
+
</Flex>
|
|
77
|
+
</Card>
|
|
78
|
+
);
|
|
31
79
|
}
|
|
32
80
|
|
|
33
|
-
|
|
34
|
-
|
|
81
|
+
// ============================================
|
|
82
|
+
// Code Section (for runtime highlighting)
|
|
83
|
+
// ============================================
|
|
35
84
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
85
|
+
interface CodeSectionProps {
|
|
86
|
+
code: string;
|
|
87
|
+
language: string;
|
|
88
|
+
showCopy?: boolean;
|
|
89
|
+
showLanguage?: boolean;
|
|
90
|
+
lightTheme?: string;
|
|
91
|
+
darkTheme?: string;
|
|
92
|
+
}
|
|
39
93
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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);
|
|
44
108
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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);
|
|
48
132
|
}
|
|
133
|
+
}, [highlighted]);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
return () => {
|
|
137
|
+
if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
|
|
138
|
+
};
|
|
139
|
+
}, []);
|
|
49
140
|
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
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
|
|
53
150
|
}
|
|
54
|
-
}
|
|
151
|
+
}, [code]);
|
|
55
152
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
153
|
+
const displayLanguage = language === "text" ? "plaintext" : language;
|
|
154
|
+
|
|
155
|
+
const handleToggle = useCallback(() => {
|
|
156
|
+
setIsExpanded((prev) => !prev);
|
|
157
|
+
}, []);
|
|
158
|
+
|
|
159
|
+
const contentStyle: React.CSSProperties = {
|
|
160
|
+
maxHeight: isExpanded ? `${contentHeight}px` : `${COLLAPSED_HEIGHT}px`,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const chevronStyle: React.CSSProperties = {
|
|
164
|
+
transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<Box position="relative">
|
|
169
|
+
<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
|
|
180
|
+
size="2"
|
|
181
|
+
variant="ghost"
|
|
182
|
+
color="gray"
|
|
183
|
+
onClick={handleToggle}
|
|
184
|
+
tooltip={isExpanded ? "Collapse" : "Expand"}
|
|
185
|
+
aria-label={isExpanded ? "Collapse code" : "Expand code"}
|
|
186
|
+
>
|
|
187
|
+
<HugeiconsIcon icon={ArrowDown01Icon} style={chevronStyle} className="code-chevron" />
|
|
188
|
+
</Button>
|
|
189
|
+
)}
|
|
190
|
+
{showCopy && (
|
|
191
|
+
<Button
|
|
192
|
+
size="2"
|
|
193
|
+
variant="ghost"
|
|
194
|
+
color="gray"
|
|
195
|
+
onClick={handleCopy}
|
|
196
|
+
tooltip={copied ? "Copied!" : "Copy"}
|
|
197
|
+
aria-label={copied ? "Copied!" : "Copy code"}
|
|
198
|
+
>
|
|
199
|
+
<HugeiconsIcon icon={copied ? Tick01Icon : Copy01Icon} /> Copy
|
|
200
|
+
</Button>
|
|
201
|
+
)}
|
|
202
|
+
</Flex>
|
|
203
|
+
</Flex>
|
|
204
|
+
|
|
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
|
+
)}
|
|
213
|
+
</Box>
|
|
214
|
+
|
|
215
|
+
{shouldShowToggle && !isExpanded && <Box className="code-scroll-shadow visible" />}
|
|
216
|
+
</Flex>
|
|
217
|
+
</Card>
|
|
218
|
+
</Box>
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ============================================
|
|
223
|
+
// Children Code Section (for pre-highlighted MDX)
|
|
224
|
+
// ============================================
|
|
225
|
+
|
|
226
|
+
interface ChildrenCodeSectionProps {
|
|
227
|
+
children: ReactNode;
|
|
228
|
+
showCopy?: boolean;
|
|
229
|
+
showLanguage?: boolean;
|
|
230
|
+
}
|
|
59
231
|
|
|
60
|
-
|
|
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);
|
|
61
245
|
}
|
|
62
246
|
|
|
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;
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
return findLanguage(props.children);
|
|
265
|
+
}
|
|
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]);
|
|
310
|
+
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
return () => {
|
|
313
|
+
if (resetTimeoutRef.current) clearTimeout(resetTimeoutRef.current);
|
|
314
|
+
};
|
|
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]);
|
|
328
|
+
|
|
329
|
+
const contentStyle: React.CSSProperties = {
|
|
330
|
+
maxHeight: isExpanded ? `${contentHeight}px` : `${COLLAPSED_HEIGHT}px`,
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const handleToggle = useCallback(() => {
|
|
334
|
+
setIsExpanded((prev) => !prev);
|
|
335
|
+
}, []);
|
|
336
|
+
|
|
337
|
+
const chevronStyle: React.CSSProperties = {
|
|
338
|
+
transform: isExpanded ? "rotate(180deg)" : "rotate(0deg)",
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
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>
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ============================================
|
|
391
|
+
// Main CodeBlock Component
|
|
392
|
+
// ============================================
|
|
393
|
+
|
|
63
394
|
export function CodeBlock({
|
|
64
395
|
children,
|
|
65
396
|
code,
|
|
@@ -72,48 +403,28 @@ export function CodeBlock({
|
|
|
72
403
|
background,
|
|
73
404
|
backgroundProps,
|
|
74
405
|
}: CodeBlockProps) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let displayLanguage = language;
|
|
78
|
-
|
|
79
|
-
// If children are provided (pre-highlighted from MDX), extract code and language
|
|
80
|
-
if (children && !code) {
|
|
81
|
-
displayCode = extractCodeFromChildren(children);
|
|
82
|
-
displayLanguage = language || extractLanguageFromChildren(children);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Default language
|
|
86
|
-
if (!displayLanguage) {
|
|
87
|
-
displayLanguage = "text";
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// If no code to display, render nothing
|
|
91
|
-
if (!displayCode && !children) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
406
|
+
const hasCode = code || (children && React.Children.count(children) > 0);
|
|
407
|
+
const displayLanguage = language || extractLanguageFromChildren(children) || "text";
|
|
94
408
|
|
|
95
409
|
return (
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<Card size="2" variant="soft">
|
|
104
|
-
<Flex gap="2" direction="column">
|
|
105
|
-
<Flex align="start" justify="between">
|
|
106
|
-
{showLanguage && <LanguageBadge language={displayLanguage} />}
|
|
107
|
-
{showCopy && displayCode && <CopyButton code={displayCode} />}
|
|
108
|
-
</Flex>
|
|
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
|
+
)}
|
|
109
417
|
|
|
110
|
-
|
|
111
|
-
|
|
418
|
+
{code && (
|
|
419
|
+
<CodeSection code={code} language={displayLanguage} showCopy={showCopy} showLanguage={showLanguage} lightTheme={lightTheme} darkTheme={darkTheme} />
|
|
420
|
+
)}
|
|
112
421
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
422
|
+
{children && !code && (
|
|
423
|
+
<ChildrenCodeSection showCopy={showCopy} showLanguage={showLanguage}>
|
|
424
|
+
{children}
|
|
425
|
+
</ChildrenCodeSection>
|
|
426
|
+
)}
|
|
427
|
+
</Flex>
|
|
428
|
+
</Box>
|
|
118
429
|
);
|
|
119
430
|
}
|
|
@@ -15,7 +15,7 @@ interface PreviewSectionProps {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function PreviewSection({ children, background = "none", backgroundProps = {} }: PreviewSectionProps) {
|
|
18
|
-
const { dotSize = 24, color = "var(--gray-10)", backgroundColor = "var(--gray-2)", height
|
|
18
|
+
const { dotSize = 24, color = "var(--gray-10)", backgroundColor = "var(--gray-2)", height, width = "100%", radius = "3" } = backgroundProps;
|
|
19
19
|
|
|
20
20
|
// Render with no background (default card styling)
|
|
21
21
|
if (background === "none") {
|
|
@@ -36,8 +36,8 @@ export function PreviewSection({ children, background = "none", backgroundProps
|
|
|
36
36
|
backgroundSize: `${dotSize}px ${dotSize}px`,
|
|
37
37
|
backgroundPosition: "center",
|
|
38
38
|
backgroundColor,
|
|
39
|
-
height,
|
|
40
39
|
width,
|
|
40
|
+
...(height && { height }),
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
return (
|
|
@@ -56,8 +56,8 @@ export function PreviewSection({ children, background = "none", backgroundProps
|
|
|
56
56
|
backgroundPosition: "center",
|
|
57
57
|
backgroundRepeat: "no-repeat",
|
|
58
58
|
borderRadius: `var(--radius-${radius})`,
|
|
59
|
-
height,
|
|
60
59
|
width,
|
|
60
|
+
...(height && { height }),
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
return (
|
package/src/components/index.css
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
/* Components CSS aggregator for Kookie Blocks
|
|
1
|
+
/* Components CSS aggregator for Kookie Blocks */
|
|
2
2
|
|
|
3
3
|
/* ============================================
|
|
4
4
|
Code Block Component Styles
|
|
5
5
|
============================================ */
|
|
6
6
|
|
|
7
|
-
/*
|
|
8
|
-
.code-block
|
|
9
|
-
min-width: 0;
|
|
10
|
-
max-width: 100%;
|
|
7
|
+
/* Main docs code block wrapper */
|
|
8
|
+
.docs-code-block {
|
|
11
9
|
width: 100%;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Code content area - handles expand/collapse */
|
|
13
|
+
.code-content {
|
|
12
14
|
overflow: hidden;
|
|
15
|
+
transition: max-height 0.3s ease-in-out;
|
|
16
|
+
position: relative;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
/* Code block content (the pre part, below the header) */
|
|
@@ -52,14 +56,17 @@
|
|
|
52
56
|
height: 0 !important;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
.code-
|
|
57
|
-
|
|
59
|
+
/* Pre elements inside code content */
|
|
60
|
+
.code-content pre,
|
|
61
|
+
.code-block-content pre {
|
|
62
|
+
overflow-x: visible;
|
|
58
63
|
max-width: 100%;
|
|
59
64
|
min-width: 0;
|
|
60
65
|
margin: 0;
|
|
61
66
|
width: 100%;
|
|
62
67
|
box-sizing: border-box;
|
|
68
|
+
scrollbar-width: none;
|
|
69
|
+
-ms-overflow-style: none;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
/* Ensure Shiki-generated pre elements fill width */
|
|
@@ -69,8 +76,8 @@
|
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
/* Code elements inside pre */
|
|
72
|
-
.code-
|
|
73
|
-
.code-block-
|
|
79
|
+
.code-content pre code,
|
|
80
|
+
.code-block-content pre code {
|
|
74
81
|
font-family: var(--font-mono);
|
|
75
82
|
font-size: var(--font-size-2);
|
|
76
83
|
line-height: 1.75;
|
|
@@ -82,16 +89,16 @@
|
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
/* Shiki line spans */
|
|
85
|
-
.code-
|
|
86
|
-
.code-block-
|
|
92
|
+
.code-content pre code .line,
|
|
93
|
+
.code-block-content pre code .line {
|
|
87
94
|
display: flex;
|
|
88
95
|
align-items: center;
|
|
89
96
|
gap: 0;
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
/* Line numbers */
|
|
93
|
-
.code-
|
|
94
|
-
.code-block-
|
|
100
|
+
.code-content pre code .line::before,
|
|
101
|
+
.code-block-content pre code .line::before {
|
|
95
102
|
counter-increment: line;
|
|
96
103
|
content: counter(line);
|
|
97
104
|
display: inline-block;
|
|
@@ -104,8 +111,37 @@
|
|
|
104
111
|
margin-right: var(--space-4);
|
|
105
112
|
}
|
|
106
113
|
|
|
114
|
+
/* Scroll shadow overlay for collapsed state */
|
|
115
|
+
.code-scroll-shadow {
|
|
116
|
+
position: absolute;
|
|
117
|
+
bottom: 0;
|
|
118
|
+
left: 0;
|
|
119
|
+
right: 0;
|
|
120
|
+
height: 80px;
|
|
121
|
+
background: linear-gradient(to top, var(--color-panel-solid), transparent);
|
|
122
|
+
pointer-events: none;
|
|
123
|
+
opacity: 0;
|
|
124
|
+
transition: opacity 0.2s ease-in-out;
|
|
125
|
+
z-index: 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.code-scroll-shadow.visible {
|
|
129
|
+
opacity: 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Action buttons styling */
|
|
133
|
+
.code-action-buttons {
|
|
134
|
+
flex-shrink: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Chevron rotation animation */
|
|
138
|
+
.code-chevron {
|
|
139
|
+
transition: transform 0.2s ease-in-out;
|
|
140
|
+
}
|
|
141
|
+
|
|
107
142
|
/* Default to light theme for all tokens (codeToHtml with defaultColor: false) */
|
|
108
|
-
.code-block-
|
|
143
|
+
.code-block-content pre.shiki span,
|
|
144
|
+
.code-content pre.shiki span {
|
|
109
145
|
color: var(--shiki-light) !important;
|
|
110
146
|
font-style: var(--shiki-light-font-style);
|
|
111
147
|
font-weight: var(--shiki-light-font-weight);
|
|
@@ -113,9 +149,12 @@
|
|
|
113
149
|
}
|
|
114
150
|
|
|
115
151
|
/* Override with dark theme colors when inside a dark context */
|
|
116
|
-
.dark .code-block-
|
|
117
|
-
.dark-theme .code-block-
|
|
118
|
-
[data-appearance="dark"] .code-block-
|
|
152
|
+
.dark .code-block-content pre.shiki span,
|
|
153
|
+
.dark-theme .code-block-content pre.shiki span,
|
|
154
|
+
[data-appearance="dark"] .code-block-content pre.shiki span,
|
|
155
|
+
.dark .code-content pre.shiki span,
|
|
156
|
+
.dark-theme .code-content pre.shiki span,
|
|
157
|
+
[data-appearance="dark"] .code-content pre.shiki span {
|
|
119
158
|
color: var(--shiki-dark) !important;
|
|
120
159
|
font-style: var(--shiki-dark-font-style);
|
|
121
160
|
font-weight: var(--shiki-dark-font-weight);
|