@fragments-sdk/ui 0.9.4 → 0.9.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/dist/assets/ui.css +443 -247
- package/dist/blocks/components/index.d.ts +0 -2
- package/dist/blocks/components/index.d.ts.map +1 -1
- package/dist/codeblock.cjs +187 -184
- package/dist/codeblock.cjs.map +1 -1
- package/dist/codeblock.js +183 -180
- package/dist/codeblock.js.map +1 -1
- package/dist/components/Box/Box.module.scss.cjs +73 -0
- package/dist/components/Box/Box.module.scss.cjs.map +1 -1
- package/dist/components/Box/Box.module.scss.js +73 -0
- package/dist/components/Box/Box.module.scss.js.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.cjs +6 -0
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.cjs.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.js +6 -0
- package/dist/components/ButtonGroup/ButtonGroup.module.scss.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +20 -23
- package/dist/components/CodeBlock/CodeBlock.module.scss.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.module.scss.js +20 -23
- package/dist/components/CodeBlock/CodeBlock.module.scss.js.map +1 -1
- package/dist/components/CodeBlock/index.d.ts +11 -7
- package/dist/components/CodeBlock/index.d.ts.map +1 -1
- package/dist/components/Combobox/Combobox.module.scss.cjs +15 -15
- package/dist/components/Combobox/Combobox.module.scss.js +15 -15
- package/dist/components/DataTable/DataTable.module.scss.cjs +84 -0
- package/dist/components/DataTable/DataTable.module.scss.cjs.map +1 -0
- package/dist/components/DataTable/DataTable.module.scss.js +84 -0
- package/dist/components/DataTable/DataTable.module.scss.js.map +1 -0
- package/dist/components/DataTable/index.cjs +383 -0
- package/dist/components/DataTable/index.cjs.map +1 -0
- package/dist/components/DataTable/index.d.ts +78 -0
- package/dist/components/DataTable/index.d.ts.map +1 -0
- package/dist/components/DataTable/index.js +366 -0
- package/dist/components/DataTable/index.js.map +1 -0
- package/dist/components/Drawer/Drawer.module.scss.cjs +9 -0
- package/dist/components/Drawer/Drawer.module.scss.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.module.scss.js +9 -0
- package/dist/components/Drawer/Drawer.module.scss.js.map +1 -1
- package/dist/components/Image/Image.module.scss.cjs +12 -0
- package/dist/components/Image/Image.module.scss.cjs.map +1 -1
- package/dist/components/Image/Image.module.scss.js +12 -0
- package/dist/components/Image/Image.module.scss.js.map +1 -1
- package/dist/components/Link/Link.module.scss.cjs +3 -0
- package/dist/components/Link/Link.module.scss.cjs.map +1 -1
- package/dist/components/Link/Link.module.scss.js +3 -0
- package/dist/components/Link/Link.module.scss.js.map +1 -1
- package/dist/components/List/List.module.scss.cjs +5 -0
- package/dist/components/List/List.module.scss.cjs.map +1 -1
- package/dist/components/List/List.module.scss.js +5 -0
- package/dist/components/List/List.module.scss.js.map +1 -1
- package/dist/components/Loading/Loading.module.scss.cjs +5 -0
- package/dist/components/Loading/Loading.module.scss.cjs.map +1 -1
- package/dist/components/Loading/Loading.module.scss.js +5 -0
- package/dist/components/Loading/Loading.module.scss.js.map +1 -1
- package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
- package/dist/components/Markdown/Markdown.module.scss.js +1 -1
- package/dist/components/Message/Message.module.scss.cjs +22 -16
- package/dist/components/Message/Message.module.scss.cjs.map +1 -1
- package/dist/components/Message/Message.module.scss.js +22 -16
- package/dist/components/Message/Message.module.scss.js.map +1 -1
- package/dist/components/Message/index.cjs +5 -3
- package/dist/components/Message/index.cjs.map +1 -1
- package/dist/components/Message/index.d.ts +5 -1
- package/dist/components/Message/index.d.ts.map +1 -1
- package/dist/components/Message/index.js +5 -3
- package/dist/components/Message/index.js.map +1 -1
- package/dist/components/Skeleton/Skeleton.module.scss.cjs +14 -0
- package/dist/components/Skeleton/Skeleton.module.scss.cjs.map +1 -1
- package/dist/components/Skeleton/Skeleton.module.scss.js +14 -0
- package/dist/components/Skeleton/Skeleton.module.scss.js.map +1 -1
- package/dist/components/Stack/Stack.module.scss.cjs +14 -0
- package/dist/components/Stack/Stack.module.scss.cjs.map +1 -1
- package/dist/components/Stack/Stack.module.scss.js +14 -0
- package/dist/components/Stack/Stack.module.scss.js.map +1 -1
- package/dist/components/Table/Table.module.scss.cjs +21 -36
- package/dist/components/Table/Table.module.scss.cjs.map +1 -1
- package/dist/components/Table/Table.module.scss.js +21 -36
- package/dist/components/Table/Table.module.scss.js.map +1 -1
- package/dist/components/Table/index.d.ts +35 -55
- package/dist/components/Table/index.d.ts.map +1 -1
- package/dist/components/Text/Text.module.scss.cjs +14 -0
- package/dist/components/Text/Text.module.scss.cjs.map +1 -1
- package/dist/components/Text/Text.module.scss.js +14 -0
- package/dist/components/Text/Text.module.scss.js.map +1 -1
- package/dist/components/Textarea/Textarea.module.scss.cjs +4 -0
- package/dist/components/Textarea/Textarea.module.scss.cjs.map +1 -1
- package/dist/components/Textarea/Textarea.module.scss.js +4 -0
- package/dist/components/Textarea/Textarea.module.scss.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs +5 -0
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.js +5 -0
- package/dist/components/ToggleGroup/ToggleGroup.module.scss.js.map +1 -1
- package/dist/index.cjs +119 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/table.cjs +44 -262
- package/dist/table.cjs.map +1 -1
- package/dist/table.js +47 -248
- package/dist/table.js.map +1 -1
- package/fragments.json +1 -1
- package/package.json +110 -118
- package/src/blocks/components/index.ts +0 -3
- package/src/components/CodeBlock/CodeBlock.module.scss +16 -34
- package/src/components/CodeBlock/index.tsx +351 -345
- package/src/components/Combobox/Combobox.module.scss +13 -9
- package/src/components/ConversationList/ConversationList.fragment.tsx +96 -129
- package/src/components/DataTable/DataTable.fragment.tsx +754 -0
- package/src/components/DataTable/DataTable.module.scss +300 -0
- package/src/components/DataTable/DataTable.test.tsx +224 -0
- package/src/components/DataTable/index.tsx +533 -0
- package/src/components/Message/Message.fragment.tsx +34 -0
- package/src/components/Message/Message.module.scss +11 -0
- package/src/components/Message/index.tsx +12 -3
- package/src/components/Table/Table.fragment.tsx +190 -175
- package/src/components/Table/Table.module.scss +15 -88
- package/src/components/Table/Table.test.tsx +184 -94
- package/src/components/Table/index.tsx +105 -374
- package/src/index.ts +15 -4
- package/src/tokens/_computed.scss +7 -6
- package/src/tokens/_density.scss +87 -47
- package/src/tokens/_variables.scss +46 -31
- package/dist/blocks/components/DataTable.d.ts +0 -19
- package/dist/blocks/components/DataTable.d.ts.map +0 -1
- package/src/blocks/components/DataTable.tsx +0 -124
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import * as React from
|
|
4
|
-
import { useState, useCallback, useEffect, useMemo } from
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
5
5
|
// ============================================
|
|
6
6
|
// Lazy-loaded dependency (shiki)
|
|
7
7
|
// ============================================
|
|
8
8
|
|
|
9
|
-
let _codeToHtml:
|
|
9
|
+
let _codeToHtml:
|
|
10
|
+
| ((code: string, options: { lang: string; theme: string }) => Promise<string>)
|
|
11
|
+
| null = null;
|
|
10
12
|
let _shikiLoaded = false;
|
|
11
13
|
let _shikiFailed = false;
|
|
12
14
|
|
|
@@ -15,66 +17,66 @@ function loadShikiDeps() {
|
|
|
15
17
|
_shikiLoaded = true;
|
|
16
18
|
try {
|
|
17
19
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
|
-
const shiki = require(
|
|
20
|
+
const shiki = require("shiki");
|
|
19
21
|
_codeToHtml = shiki.codeToHtml;
|
|
20
22
|
} catch {
|
|
21
23
|
_shikiFailed = true;
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
|
-
import { TabsRoot, TabsList, Tab, TabsPanel } from
|
|
25
|
-
import { Button } from
|
|
26
|
-
import styles from
|
|
27
|
-
import
|
|
26
|
+
import { TabsRoot, TabsList, Tab, TabsPanel } from "../Tabs";
|
|
27
|
+
import { Button } from "../Button";
|
|
28
|
+
import styles from "./CodeBlock.module.scss";
|
|
29
|
+
import "../../styles/globals.scss";
|
|
28
30
|
|
|
29
31
|
export type CodeBlockLanguage =
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
32
|
+
| "tsx"
|
|
33
|
+
| "typescript"
|
|
34
|
+
| "javascript"
|
|
35
|
+
| "jsx"
|
|
36
|
+
| "bash"
|
|
37
|
+
| "shell"
|
|
38
|
+
| "css"
|
|
39
|
+
| "scss"
|
|
40
|
+
| "sass"
|
|
41
|
+
| "json"
|
|
42
|
+
| "html"
|
|
43
|
+
| "xml"
|
|
44
|
+
| "markdown"
|
|
45
|
+
| "md"
|
|
46
|
+
| "yaml"
|
|
47
|
+
| "yml"
|
|
48
|
+
| "python"
|
|
49
|
+
| "py"
|
|
50
|
+
| "ruby"
|
|
51
|
+
| "go"
|
|
52
|
+
| "rust"
|
|
53
|
+
| "java"
|
|
54
|
+
| "kotlin"
|
|
55
|
+
| "swift"
|
|
56
|
+
| "c"
|
|
57
|
+
| "cpp"
|
|
58
|
+
| "csharp"
|
|
59
|
+
| "php"
|
|
60
|
+
| "sql"
|
|
61
|
+
| "graphql"
|
|
62
|
+
| "diff"
|
|
63
|
+
| "plaintext";
|
|
62
64
|
|
|
63
65
|
/** Available syntax highlighting themes */
|
|
64
66
|
export type CodeBlockTheme =
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
77
|
-
export type CodeBlockCopyPlacement =
|
|
67
|
+
| "synthwave-84"
|
|
68
|
+
| "github-dark"
|
|
69
|
+
| "github-light"
|
|
70
|
+
| "one-dark-pro"
|
|
71
|
+
| "dracula"
|
|
72
|
+
| "nord"
|
|
73
|
+
| "monokai"
|
|
74
|
+
| "vitesse-dark"
|
|
75
|
+
| "vitesse-light"
|
|
76
|
+
| "min-dark"
|
|
77
|
+
| "min-light";
|
|
78
|
+
|
|
79
|
+
export type CodeBlockCopyPlacement = "auto" | "header" | "overlay";
|
|
78
80
|
|
|
79
81
|
export interface CodeBlockProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
80
82
|
/** Code string to display */
|
|
@@ -117,6 +119,8 @@ export interface CodeBlockProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
117
119
|
persistentCopy?: boolean;
|
|
118
120
|
/** Placement of copy button when not using persistent copy */
|
|
119
121
|
copyPlacement?: CodeBlockCopyPlacement;
|
|
122
|
+
/** Callback fired when the copy button is clicked and copy succeeds */
|
|
123
|
+
onCopy?: () => void;
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
function CopyIcon({ className }: { className?: string }) {
|
|
@@ -198,11 +202,11 @@ function ChevronUpIcon({ className }: { className?: string }) {
|
|
|
198
202
|
|
|
199
203
|
function escapeHtml(str: string): string {
|
|
200
204
|
return str
|
|
201
|
-
.replace(/&/g,
|
|
202
|
-
.replace(/</g,
|
|
203
|
-
.replace(/>/g,
|
|
204
|
-
.replace(/"/g,
|
|
205
|
-
.replace(/'/g,
|
|
205
|
+
.replace(/&/g, "&")
|
|
206
|
+
.replace(/</g, "<")
|
|
207
|
+
.replace(/>/g, ">")
|
|
208
|
+
.replace(/"/g, """)
|
|
209
|
+
.replace(/'/g, "'");
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
/**
|
|
@@ -210,12 +214,12 @@ function escapeHtml(str: string): string {
|
|
|
210
214
|
* This handles template literals that have extra indentation from code formatting.
|
|
211
215
|
*/
|
|
212
216
|
function dedent(str: string): string {
|
|
213
|
-
const lines = str.split(
|
|
217
|
+
const lines = str.split("\n");
|
|
214
218
|
|
|
215
219
|
// Find the minimum indentation (ignoring empty lines)
|
|
216
220
|
let minIndent = Infinity;
|
|
217
221
|
for (const line of lines) {
|
|
218
|
-
if (line.trim() ===
|
|
222
|
+
if (line.trim() === "") continue;
|
|
219
223
|
const match = line.match(/^(\s*)/);
|
|
220
224
|
if (match) {
|
|
221
225
|
minIndent = Math.min(minIndent, match[1].length);
|
|
@@ -228,16 +232,14 @@ function dedent(str: string): string {
|
|
|
228
232
|
}
|
|
229
233
|
|
|
230
234
|
// Remove the common indentation from all lines
|
|
231
|
-
return lines
|
|
232
|
-
.map(line => line.slice(minIndent))
|
|
233
|
-
.join('\n');
|
|
235
|
+
return lines.map((line) => line.slice(minIndent)).join("\n");
|
|
234
236
|
}
|
|
235
237
|
|
|
236
238
|
/**
|
|
237
239
|
* Normalize indentation while handling JSX where first line is already at column 0.
|
|
238
240
|
*/
|
|
239
241
|
function normalizeIndentation(str: string): string {
|
|
240
|
-
const lines = str.split(
|
|
242
|
+
const lines = str.split("\n");
|
|
241
243
|
if (lines.length <= 1) return str;
|
|
242
244
|
|
|
243
245
|
let minIndent = Infinity;
|
|
@@ -258,18 +260,18 @@ function normalizeIndentation(str: string): string {
|
|
|
258
260
|
|
|
259
261
|
return lines
|
|
260
262
|
.map((line) => line.slice(Math.min(minIndent, line.match(/^(\s*)/)?.[1].length ?? 0)))
|
|
261
|
-
.join(
|
|
263
|
+
.join("\n");
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
function trimTrailingWhitespace(str: string): string {
|
|
265
267
|
return str
|
|
266
|
-
.split(
|
|
267
|
-
.map((line) => line.replace(/[ \t]+$/g,
|
|
268
|
-
.join(
|
|
268
|
+
.split("\n")
|
|
269
|
+
.map((line) => line.replace(/[ \t]+$/g, ""))
|
|
270
|
+
.join("\n");
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
function findTagEnd(line: string): number {
|
|
272
|
-
let quote: '"' | '
|
|
274
|
+
let quote: '"' | "'" | "`" | null = null;
|
|
273
275
|
let escaped = false;
|
|
274
276
|
let braceDepth = 0;
|
|
275
277
|
let bracketDepth = 0;
|
|
@@ -279,7 +281,7 @@ function findTagEnd(line: string): number {
|
|
|
279
281
|
const char = line[i];
|
|
280
282
|
|
|
281
283
|
if (quote) {
|
|
282
|
-
if (char ===
|
|
284
|
+
if (char === "\\" && !escaped) {
|
|
283
285
|
escaped = true;
|
|
284
286
|
continue;
|
|
285
287
|
}
|
|
@@ -290,18 +292,18 @@ function findTagEnd(line: string): number {
|
|
|
290
292
|
continue;
|
|
291
293
|
}
|
|
292
294
|
|
|
293
|
-
if (char === '"' || char === '
|
|
295
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
294
296
|
quote = char;
|
|
295
297
|
continue;
|
|
296
298
|
}
|
|
297
299
|
|
|
298
|
-
if (char ===
|
|
299
|
-
else if (char ===
|
|
300
|
-
else if (char ===
|
|
301
|
-
else if (char ===
|
|
302
|
-
else if (char ===
|
|
303
|
-
else if (char ===
|
|
304
|
-
else if (char ===
|
|
300
|
+
if (char === "{") braceDepth += 1;
|
|
301
|
+
else if (char === "}") braceDepth = Math.max(0, braceDepth - 1);
|
|
302
|
+
else if (char === "[") bracketDepth += 1;
|
|
303
|
+
else if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
304
|
+
else if (char === "(") parenDepth += 1;
|
|
305
|
+
else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
306
|
+
else if (char === ">" && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) {
|
|
305
307
|
return i;
|
|
306
308
|
}
|
|
307
309
|
}
|
|
@@ -311,8 +313,8 @@ function findTagEnd(line: string): number {
|
|
|
311
313
|
|
|
312
314
|
function splitJsxAttributes(attrs: string): string[] {
|
|
313
315
|
const parts: string[] = [];
|
|
314
|
-
let current =
|
|
315
|
-
let quote: '"' | '
|
|
316
|
+
let current = "";
|
|
317
|
+
let quote: '"' | "'" | "`" | null = null;
|
|
316
318
|
let escaped = false;
|
|
317
319
|
let braceDepth = 0;
|
|
318
320
|
let bracketDepth = 0;
|
|
@@ -321,7 +323,7 @@ function splitJsxAttributes(attrs: string): string[] {
|
|
|
321
323
|
for (const char of attrs) {
|
|
322
324
|
if (quote) {
|
|
323
325
|
current += char;
|
|
324
|
-
if (char ===
|
|
326
|
+
if (char === "\\" && !escaped) {
|
|
325
327
|
escaped = true;
|
|
326
328
|
continue;
|
|
327
329
|
}
|
|
@@ -332,23 +334,23 @@ function splitJsxAttributes(attrs: string): string[] {
|
|
|
332
334
|
continue;
|
|
333
335
|
}
|
|
334
336
|
|
|
335
|
-
if (char === '"' || char === '
|
|
337
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
336
338
|
quote = char;
|
|
337
339
|
current += char;
|
|
338
340
|
continue;
|
|
339
341
|
}
|
|
340
342
|
|
|
341
|
-
if (char ===
|
|
342
|
-
else if (char ===
|
|
343
|
-
else if (char ===
|
|
344
|
-
else if (char ===
|
|
345
|
-
else if (char ===
|
|
346
|
-
else if (char ===
|
|
343
|
+
if (char === "{") braceDepth += 1;
|
|
344
|
+
else if (char === "}") braceDepth = Math.max(0, braceDepth - 1);
|
|
345
|
+
else if (char === "[") bracketDepth += 1;
|
|
346
|
+
else if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1);
|
|
347
|
+
else if (char === "(") parenDepth += 1;
|
|
348
|
+
else if (char === ")") parenDepth = Math.max(0, parenDepth - 1);
|
|
347
349
|
|
|
348
350
|
if (/\s/.test(char) && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) {
|
|
349
351
|
if (current.trim().length > 0) {
|
|
350
352
|
parts.push(current.trim());
|
|
351
|
-
current =
|
|
353
|
+
current = "";
|
|
352
354
|
}
|
|
353
355
|
continue;
|
|
354
356
|
}
|
|
@@ -367,14 +369,14 @@ function formatLongJsxTagLine(line: string): string {
|
|
|
367
369
|
const maxInlineLength = 110;
|
|
368
370
|
if (line.length <= maxInlineLength) return line;
|
|
369
371
|
|
|
370
|
-
const indent = line.match(/^(\s*)/)?.[1] ??
|
|
372
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? "";
|
|
371
373
|
const trimmed = line.trimStart();
|
|
372
374
|
|
|
373
375
|
if (
|
|
374
|
-
!trimmed.startsWith(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
376
|
+
!trimmed.startsWith("<") ||
|
|
377
|
+
trimmed.startsWith("</") ||
|
|
378
|
+
trimmed.startsWith("<!") ||
|
|
379
|
+
trimmed.startsWith("<?")
|
|
378
380
|
) {
|
|
379
381
|
return line;
|
|
380
382
|
}
|
|
@@ -384,7 +386,7 @@ function formatLongJsxTagLine(line: string): string {
|
|
|
384
386
|
if (trimmed.slice(tagEnd + 1).trim().length > 0) return line;
|
|
385
387
|
|
|
386
388
|
const rawTagBody = trimmed.slice(1, tagEnd).trim();
|
|
387
|
-
const isSelfClosing = rawTagBody.endsWith(
|
|
389
|
+
const isSelfClosing = rawTagBody.endsWith("/");
|
|
388
390
|
const tagBody = isSelfClosing ? rawTagBody.slice(0, -1).trimEnd() : rawTagBody;
|
|
389
391
|
const firstSpace = tagBody.search(/\s/);
|
|
390
392
|
if (firstSpace === -1) return line;
|
|
@@ -393,31 +395,31 @@ function formatLongJsxTagLine(line: string): string {
|
|
|
393
395
|
if (!/^[A-Za-z][\w.:-]*$/.test(tagName)) return line;
|
|
394
396
|
|
|
395
397
|
const attrsSource = tagBody.slice(firstSpace).trim();
|
|
396
|
-
if (!attrsSource.includes(
|
|
398
|
+
if (!attrsSource.includes("=") && !attrsSource.includes("{...")) return line;
|
|
397
399
|
|
|
398
400
|
const attrs = splitJsxAttributes(attrsSource);
|
|
399
401
|
if (attrs.length === 0) return line;
|
|
400
402
|
|
|
401
403
|
const attrIndent = `${indent} `;
|
|
402
|
-
const close = isSelfClosing ?
|
|
404
|
+
const close = isSelfClosing ? "/>" : ">";
|
|
403
405
|
|
|
404
406
|
return [
|
|
405
407
|
`${indent}<${tagName}`,
|
|
406
408
|
...attrs.map((attr) => `${attrIndent}${attr}`),
|
|
407
409
|
`${indent}${close}`,
|
|
408
|
-
].join(
|
|
410
|
+
].join("\n");
|
|
409
411
|
}
|
|
410
412
|
|
|
411
413
|
function formatLongJsxTags(code: string): string {
|
|
412
414
|
return code
|
|
413
|
-
.split(
|
|
414
|
-
.flatMap((line) => formatLongJsxTagLine(line).split(
|
|
415
|
-
.join(
|
|
415
|
+
.split("\n")
|
|
416
|
+
.flatMap((line) => formatLongJsxTagLine(line).split("\n"))
|
|
417
|
+
.join("\n");
|
|
416
418
|
}
|
|
417
419
|
|
|
418
420
|
function normalizeCode(code: string): string {
|
|
419
421
|
const trimmed = code.trim();
|
|
420
|
-
if (trimmed.length === 0) return
|
|
422
|
+
if (trimmed.length === 0) return "";
|
|
421
423
|
|
|
422
424
|
const normalized = normalizeIndentation(trimmed);
|
|
423
425
|
const dedented = dedent(normalized);
|
|
@@ -434,9 +436,9 @@ function parseLineSpec(spec?: (number | string)[]): Set<number> {
|
|
|
434
436
|
if (!spec) return lines;
|
|
435
437
|
|
|
436
438
|
for (const item of spec) {
|
|
437
|
-
if (typeof item ===
|
|
439
|
+
if (typeof item === "number") {
|
|
438
440
|
lines.add(item);
|
|
439
|
-
} else if (typeof item ===
|
|
441
|
+
} else if (typeof item === "string") {
|
|
440
442
|
const rangeMatch = item.match(/^(\d+)-(\d+)$/);
|
|
441
443
|
if (rangeMatch) {
|
|
442
444
|
const start = parseInt(rangeMatch[1], 10);
|
|
@@ -481,7 +483,7 @@ function processShikiHtml(html: string, options: ProcessOptions): string {
|
|
|
481
483
|
if (!codeMatch) return html;
|
|
482
484
|
|
|
483
485
|
const codeContent = codeMatch[1];
|
|
484
|
-
const lines = codeContent.split(
|
|
486
|
+
const lines = codeContent.split("\n");
|
|
485
487
|
|
|
486
488
|
// Process each line
|
|
487
489
|
const processedLines = lines.map((line, index) => {
|
|
@@ -491,263 +493,263 @@ function processShikiHtml(html: string, options: ProcessOptions): string {
|
|
|
491
493
|
const isAdded = addedLines.has(lineNum);
|
|
492
494
|
const isRemoved = removedLines.has(lineNum);
|
|
493
495
|
|
|
494
|
-
const lineClasses = [
|
|
495
|
-
if (isHighlighted) lineClasses.push(
|
|
496
|
-
if (isAdded) lineClasses.push(
|
|
497
|
-
if (isRemoved) lineClasses.push(
|
|
496
|
+
const lineClasses = ["line"];
|
|
497
|
+
if (isHighlighted) lineClasses.push("highlighted");
|
|
498
|
+
if (isAdded) lineClasses.push("diff-added");
|
|
499
|
+
if (isRemoved) lineClasses.push("diff-removed");
|
|
498
500
|
|
|
499
|
-
const lineClass = lineClasses.join(
|
|
500
|
-
const diffMarker = isAdded ?
|
|
501
|
+
const lineClass = lineClasses.join(" ");
|
|
502
|
+
const diffMarker = isAdded ? "+" : isRemoved ? "-" : " ";
|
|
501
503
|
|
|
502
504
|
if (showLineNumbers || hasDiff) {
|
|
503
505
|
const lineNumHtml = showLineNumbers
|
|
504
506
|
? `<span class="line-number">${displayLineNum}</span>`
|
|
505
|
-
:
|
|
506
|
-
const diffMarkerHtml = hasDiff
|
|
507
|
-
? `<span class="diff-marker">${diffMarker}</span>`
|
|
508
|
-
: '';
|
|
507
|
+
: "";
|
|
508
|
+
const diffMarkerHtml = hasDiff ? `<span class="diff-marker">${diffMarker}</span>` : "";
|
|
509
509
|
return `<span class="${lineClass}">${lineNumHtml}${diffMarkerHtml}${line}</span>`;
|
|
510
510
|
}
|
|
511
511
|
return `<span class="${lineClass}">${line}</span>`;
|
|
512
512
|
});
|
|
513
513
|
|
|
514
514
|
// Reconstruct the HTML
|
|
515
|
-
return html.replace(
|
|
516
|
-
/<code[^>]*>[\s\S]*?<\/code>/,
|
|
517
|
-
`<code>${processedLines.join('\n')}</code>`
|
|
518
|
-
);
|
|
515
|
+
return html.replace(/<code[^>]*>[\s\S]*?<\/code>/, `<code>${processedLines.join("\n")}</code>`);
|
|
519
516
|
}
|
|
520
517
|
|
|
521
|
-
const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
'[@fragments-sdk/ui] CodeBlock: shiki is not installed. ' +
|
|
586
|
-
'Install it with: npm install shiki'
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
// Fallback to plain text without syntax highlighting
|
|
590
|
-
setHighlightedHtml(
|
|
591
|
-
`<pre class="shiki"><code>${escapeHtml(visibleCode)}</code></pre>`
|
|
518
|
+
const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(function CodeBlock(
|
|
519
|
+
{
|
|
520
|
+
code,
|
|
521
|
+
language = "tsx",
|
|
522
|
+
theme = "one-dark-pro",
|
|
523
|
+
showCopy = true,
|
|
524
|
+
title,
|
|
525
|
+
filename,
|
|
526
|
+
caption,
|
|
527
|
+
showLineNumbers = false,
|
|
528
|
+
startLineNumber = 1,
|
|
529
|
+
highlightLines,
|
|
530
|
+
addedLines,
|
|
531
|
+
removedLines,
|
|
532
|
+
wordWrap = false,
|
|
533
|
+
maxHeight,
|
|
534
|
+
collapsible = false,
|
|
535
|
+
defaultCollapsed = false,
|
|
536
|
+
collapsedLines = 5,
|
|
537
|
+
compact = false,
|
|
538
|
+
persistentCopy = false,
|
|
539
|
+
copyPlacement = "auto",
|
|
540
|
+
onCopy,
|
|
541
|
+
className,
|
|
542
|
+
...htmlProps
|
|
543
|
+
},
|
|
544
|
+
ref
|
|
545
|
+
) {
|
|
546
|
+
const [copied, setCopied] = useState(false);
|
|
547
|
+
const [highlightedHtml, setHighlightedHtml] = useState<string>("");
|
|
548
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
549
|
+
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
|
550
|
+
|
|
551
|
+
const trimmedCode = useMemo(() => normalizeCode(code), [code]);
|
|
552
|
+
const codeLines = trimmedCode.split("\n");
|
|
553
|
+
const totalLines = codeLines.length;
|
|
554
|
+
const shouldShowCollapse = collapsible && totalLines > collapsedLines;
|
|
555
|
+
|
|
556
|
+
// Compute visible code when collapsed
|
|
557
|
+
const visibleCode =
|
|
558
|
+
shouldShowCollapse && isCollapsed ? codeLines.slice(0, collapsedLines).join("\n") : trimmedCode;
|
|
559
|
+
|
|
560
|
+
const highlightSet = useMemo(() => parseLineSpec(highlightLines), [highlightLines]);
|
|
561
|
+
const addedSet = useMemo(() => parseLineSpec(addedLines), [addedLines]);
|
|
562
|
+
const removedSet = useMemo(() => parseLineSpec(removedLines), [removedLines]);
|
|
563
|
+
const hasDiff = addedSet.size > 0 || removedSet.size > 0;
|
|
564
|
+
const resolvedCopyPlacement =
|
|
565
|
+
copyPlacement === "auto" ? (filename ? "header" : "overlay") : copyPlacement;
|
|
566
|
+
const shouldShowHeaderCopy = showCopy && !persistentCopy && resolvedCopyPlacement === "header";
|
|
567
|
+
const shouldShowOverlayCopy = showCopy && !persistentCopy && resolvedCopyPlacement === "overlay";
|
|
568
|
+
const shouldRenderHeader = Boolean(filename) || shouldShowHeaderCopy;
|
|
569
|
+
|
|
570
|
+
// Apply syntax highlighting
|
|
571
|
+
useEffect(() => {
|
|
572
|
+
let cancelled = false;
|
|
573
|
+
setIsLoading(true);
|
|
574
|
+
|
|
575
|
+
loadShikiDeps();
|
|
576
|
+
|
|
577
|
+
if (_shikiFailed || !_codeToHtml) {
|
|
578
|
+
if (_shikiFailed && process.env.NODE_ENV === "development") {
|
|
579
|
+
console.warn(
|
|
580
|
+
"[@fragments-sdk/ui] CodeBlock: shiki is not installed. " +
|
|
581
|
+
"Install it with: npm install shiki"
|
|
592
582
|
);
|
|
593
|
-
setIsLoading(false);
|
|
594
|
-
return;
|
|
595
583
|
}
|
|
584
|
+
// Fallback to plain text without syntax highlighting
|
|
585
|
+
setHighlightedHtml(`<pre class="shiki"><code>${escapeHtml(visibleCode)}</code></pre>`);
|
|
586
|
+
setIsLoading(false);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
596
589
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
590
|
+
_codeToHtml(visibleCode, {
|
|
591
|
+
lang: language,
|
|
592
|
+
theme,
|
|
593
|
+
})
|
|
594
|
+
.then((html) => {
|
|
595
|
+
if (!cancelled) {
|
|
596
|
+
const processed = processShikiHtml(html, {
|
|
597
|
+
showLineNumbers,
|
|
598
|
+
startLineNumber,
|
|
599
|
+
highlightLines: highlightSet,
|
|
600
|
+
addedLines: addedSet,
|
|
601
|
+
removedLines: removedSet,
|
|
602
|
+
});
|
|
603
|
+
setHighlightedHtml(processed);
|
|
604
|
+
setIsLoading(false);
|
|
605
|
+
}
|
|
600
606
|
})
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
showLineNumbers,
|
|
605
|
-
startLineNumber,
|
|
606
|
-
highlightLines: highlightSet,
|
|
607
|
-
addedLines: addedSet,
|
|
608
|
-
removedLines: removedSet,
|
|
609
|
-
});
|
|
610
|
-
setHighlightedHtml(processed);
|
|
611
|
-
setIsLoading(false);
|
|
612
|
-
}
|
|
613
|
-
})
|
|
614
|
-
.catch((err) => {
|
|
615
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
616
|
-
console.error('Syntax highlighting failed:', err);
|
|
617
|
-
}
|
|
618
|
-
if (!cancelled) {
|
|
619
|
-
// Fallback to plain text
|
|
620
|
-
setHighlightedHtml(
|
|
621
|
-
`<pre class="shiki"><code>${escapeHtml(visibleCode)}</code></pre>`
|
|
622
|
-
);
|
|
623
|
-
setIsLoading(false);
|
|
624
|
-
}
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
return () => {
|
|
628
|
-
cancelled = true;
|
|
629
|
-
};
|
|
630
|
-
}, [visibleCode, language, theme, showLineNumbers, startLineNumber, highlightSet, addedSet, removedSet]);
|
|
631
|
-
|
|
632
|
-
const handleCopy = useCallback(async () => {
|
|
633
|
-
try {
|
|
634
|
-
// Always copy the full code, even when collapsed
|
|
635
|
-
await navigator.clipboard.writeText(trimmedCode);
|
|
636
|
-
setCopied(true);
|
|
637
|
-
setTimeout(() => setCopied(false), 2000);
|
|
638
|
-
} catch (err) {
|
|
639
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
640
|
-
console.error('Failed to copy:', err);
|
|
607
|
+
.catch((err) => {
|
|
608
|
+
if (process.env.NODE_ENV !== "production") {
|
|
609
|
+
console.error("Syntax highlighting failed:", err);
|
|
641
610
|
}
|
|
611
|
+
if (!cancelled) {
|
|
612
|
+
// Fallback to plain text
|
|
613
|
+
setHighlightedHtml(`<pre class="shiki"><code>${escapeHtml(visibleCode)}</code></pre>`);
|
|
614
|
+
setIsLoading(false);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
return () => {
|
|
619
|
+
cancelled = true;
|
|
620
|
+
};
|
|
621
|
+
}, [
|
|
622
|
+
visibleCode,
|
|
623
|
+
language,
|
|
624
|
+
theme,
|
|
625
|
+
showLineNumbers,
|
|
626
|
+
startLineNumber,
|
|
627
|
+
highlightSet,
|
|
628
|
+
addedSet,
|
|
629
|
+
removedSet,
|
|
630
|
+
]);
|
|
631
|
+
|
|
632
|
+
const handleCopy = useCallback(async () => {
|
|
633
|
+
try {
|
|
634
|
+
// Always copy the full code, even when collapsed
|
|
635
|
+
await navigator.clipboard.writeText(trimmedCode);
|
|
636
|
+
setCopied(true);
|
|
637
|
+
setTimeout(() => setCopied(false), 2000);
|
|
638
|
+
onCopy?.();
|
|
639
|
+
} catch (err) {
|
|
640
|
+
if (process.env.NODE_ENV !== "production") {
|
|
641
|
+
console.error("Failed to copy:", err);
|
|
642
642
|
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
className={`${styles.copyButton} ${copied ? styles.copied : ''}`}
|
|
682
|
-
aria-label={copied ? 'Copied!' : 'Copy code'}
|
|
683
|
-
>
|
|
684
|
-
{copied ? <CheckIcon className={styles.icon} /> : <CopyIcon className={styles.icon} />}
|
|
685
|
-
</button>
|
|
686
|
-
)}
|
|
687
|
-
</div>
|
|
688
|
-
)}
|
|
689
|
-
{shouldShowOverlayCopy && (
|
|
690
|
-
<button
|
|
691
|
-
type="button"
|
|
692
|
-
onClick={handleCopy}
|
|
693
|
-
className={`${styles.copyButton} ${styles.copyOverlay} ${copied ? styles.copied : ''}`}
|
|
694
|
-
aria-label={copied ? 'Copied!' : 'Copy code'}
|
|
695
|
-
>
|
|
696
|
-
{copied ? <CheckIcon className={styles.icon} /> : <CopyIcon className={styles.icon} />}
|
|
697
|
-
</button>
|
|
698
|
-
)}
|
|
699
|
-
{isLoading ? (
|
|
700
|
-
<div className={styles.loading} style={codeContainerStyle}>
|
|
701
|
-
<pre>
|
|
702
|
-
<code>{visibleCode}</code>
|
|
703
|
-
</pre>
|
|
704
|
-
</div>
|
|
705
|
-
) : (
|
|
706
|
-
<div
|
|
707
|
-
className={styles.codeContainer}
|
|
708
|
-
style={codeContainerStyle}
|
|
709
|
-
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
|
710
|
-
/>
|
|
711
|
-
)}
|
|
712
|
-
{persistentCopy && (
|
|
713
|
-
<div className={styles.persistentCopy}>
|
|
714
|
-
<Button
|
|
715
|
-
size="sm"
|
|
643
|
+
}
|
|
644
|
+
}, [trimmedCode, onCopy]);
|
|
645
|
+
|
|
646
|
+
const toggleCollapsed = useCallback(() => {
|
|
647
|
+
setIsCollapsed((prev) => !prev);
|
|
648
|
+
}, []);
|
|
649
|
+
|
|
650
|
+
const classNames = [
|
|
651
|
+
styles.container,
|
|
652
|
+
showLineNumbers && styles.withLineNumbers,
|
|
653
|
+
hasDiff && styles.withDiff,
|
|
654
|
+
wordWrap && styles.wordWrap,
|
|
655
|
+
compact && styles.compact,
|
|
656
|
+
className,
|
|
657
|
+
]
|
|
658
|
+
.filter(Boolean)
|
|
659
|
+
.join(" ");
|
|
660
|
+
|
|
661
|
+
const wrapperClasses = [
|
|
662
|
+
styles.wrapper,
|
|
663
|
+
persistentCopy && styles.persistentCopyWrapper,
|
|
664
|
+
shouldShowOverlayCopy && styles.withCopyOverlay,
|
|
665
|
+
]
|
|
666
|
+
.filter(Boolean)
|
|
667
|
+
.join(" ");
|
|
668
|
+
|
|
669
|
+
const codeContainerStyle: React.CSSProperties = maxHeight ? { maxHeight, overflow: "auto" } : {};
|
|
670
|
+
|
|
671
|
+
return (
|
|
672
|
+
<div ref={ref} {...htmlProps} className={classNames} data-theme="dark">
|
|
673
|
+
{title && <div className={styles.title}>{title}</div>}
|
|
674
|
+
<div className={wrapperClasses}>
|
|
675
|
+
{shouldRenderHeader && (
|
|
676
|
+
<div className={styles.header}>
|
|
677
|
+
<span className={styles.filename}>{filename ?? ""}</span>
|
|
678
|
+
{shouldShowHeaderCopy && (
|
|
679
|
+
<button
|
|
680
|
+
type="button"
|
|
716
681
|
onClick={handleCopy}
|
|
717
|
-
|
|
718
|
-
|
|
682
|
+
className={`${styles.copyButton} ${copied ? styles.copied : ""}`}
|
|
683
|
+
aria-label={copied ? "Copied!" : "Copy code"}
|
|
719
684
|
>
|
|
720
|
-
{copied ?
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
</
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
685
|
+
{copied ? (
|
|
686
|
+
<CheckIcon className={styles.icon} />
|
|
687
|
+
) : (
|
|
688
|
+
<CopyIcon className={styles.icon} />
|
|
689
|
+
)}
|
|
690
|
+
</button>
|
|
691
|
+
)}
|
|
692
|
+
</div>
|
|
693
|
+
)}
|
|
694
|
+
{shouldShowOverlayCopy && (
|
|
695
|
+
<button
|
|
696
|
+
type="button"
|
|
697
|
+
onClick={handleCopy}
|
|
698
|
+
className={`${styles.copyButton} ${styles.copyOverlay} ${copied ? styles.copied : ""}`}
|
|
699
|
+
aria-label={copied ? "Copied!" : "Copy code"}
|
|
700
|
+
>
|
|
701
|
+
{copied ? <CheckIcon className={styles.icon} /> : <CopyIcon className={styles.icon} />}
|
|
702
|
+
</button>
|
|
703
|
+
)}
|
|
704
|
+
{isLoading ? (
|
|
705
|
+
<div className={styles.loading} style={codeContainerStyle}>
|
|
706
|
+
<pre>
|
|
707
|
+
<code>{visibleCode}</code>
|
|
708
|
+
</pre>
|
|
709
|
+
</div>
|
|
710
|
+
) : (
|
|
711
|
+
<div
|
|
712
|
+
className={styles.codeContainer}
|
|
713
|
+
style={codeContainerStyle}
|
|
714
|
+
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
|
715
|
+
/>
|
|
716
|
+
)}
|
|
717
|
+
{persistentCopy && (
|
|
718
|
+
<button
|
|
719
|
+
type="button"
|
|
720
|
+
onClick={handleCopy}
|
|
721
|
+
className={`${styles.persistentCopy} ${styles.copyButton} ${styles.copyOverlay} ${copied ? styles.copied : ""}`}
|
|
722
|
+
aria-label={copied ? "Copied!" : "Copy code"}
|
|
723
|
+
>
|
|
724
|
+
{copied ? <CheckIcon className={styles.icon} /> : <CopyIcon className={styles.icon} />}
|
|
725
|
+
</button>
|
|
726
|
+
)}
|
|
727
|
+
{shouldShowCollapse && (
|
|
728
|
+
<button
|
|
729
|
+
type="button"
|
|
730
|
+
onClick={toggleCollapsed}
|
|
731
|
+
className={styles.collapseButton}
|
|
732
|
+
aria-expanded={!isCollapsed}
|
|
733
|
+
aria-label={isCollapsed ? "Expand code" : "Collapse code"}
|
|
734
|
+
>
|
|
735
|
+
{isCollapsed ? (
|
|
736
|
+
<>
|
|
737
|
+
<ChevronDownIcon className={styles.icon} />
|
|
738
|
+
<span>Show {totalLines - collapsedLines} more lines</span>
|
|
739
|
+
</>
|
|
740
|
+
) : (
|
|
741
|
+
<>
|
|
742
|
+
<ChevronUpIcon className={styles.icon} />
|
|
743
|
+
<span>Show less</span>
|
|
744
|
+
</>
|
|
745
|
+
)}
|
|
746
|
+
</button>
|
|
747
|
+
)}
|
|
747
748
|
</div>
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
);
|
|
749
|
+
{caption && <div className={styles.caption}>{caption}</div>}
|
|
750
|
+
</div>
|
|
751
|
+
);
|
|
752
|
+
});
|
|
751
753
|
|
|
752
754
|
// ============================================
|
|
753
755
|
// Tabbed Code Block
|
|
@@ -776,28 +778,31 @@ export interface TabbedCodeBlockProps {
|
|
|
776
778
|
/** Syntax highlighting theme (applies to all tabs) */
|
|
777
779
|
theme?: CodeBlockTheme;
|
|
778
780
|
/** Tab list visual style */
|
|
779
|
-
tabsVariant?:
|
|
781
|
+
tabsVariant?: "underline" | "pills";
|
|
780
782
|
/** Enable word wrapping for long lines */
|
|
781
783
|
wordWrap?: boolean;
|
|
782
784
|
/** Maximum height in pixels (enables scrolling) */
|
|
783
785
|
maxHeight?: number;
|
|
784
786
|
/** Additional class name */
|
|
785
787
|
className?: string;
|
|
788
|
+
/** Callback fired when a tab's copy button is clicked. Receives the tab label. */
|
|
789
|
+
onCopy?: (tabLabel: string) => void;
|
|
786
790
|
}
|
|
787
791
|
|
|
788
792
|
function TabbedCodeBlock({
|
|
789
793
|
tabs,
|
|
790
794
|
defaultTab,
|
|
791
795
|
showCopy = true,
|
|
792
|
-
copyPlacement =
|
|
796
|
+
copyPlacement = "auto",
|
|
793
797
|
showLineNumbers = false,
|
|
794
798
|
theme,
|
|
795
|
-
tabsVariant =
|
|
799
|
+
tabsVariant = "pills",
|
|
796
800
|
wordWrap,
|
|
797
801
|
maxHeight,
|
|
798
802
|
className,
|
|
803
|
+
onCopy,
|
|
799
804
|
}: TabbedCodeBlockProps) {
|
|
800
|
-
const defaultValue = defaultTab || tabs[0]?.label ||
|
|
805
|
+
const defaultValue = defaultTab || tabs[0]?.label || "";
|
|
801
806
|
|
|
802
807
|
return (
|
|
803
808
|
<div className={className}>
|
|
@@ -820,6 +825,7 @@ function TabbedCodeBlock({
|
|
|
820
825
|
showLineNumbers={showLineNumbers}
|
|
821
826
|
wordWrap={wordWrap}
|
|
822
827
|
maxHeight={maxHeight}
|
|
828
|
+
onCopy={onCopy ? () => onCopy(tab.label) : undefined}
|
|
823
829
|
/>
|
|
824
830
|
</TabsPanel>
|
|
825
831
|
))}
|