@fragments-sdk/ui 0.9.5 → 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 +196 -199
- package/dist/codeblock.cjs +183 -184
- package/dist/codeblock.cjs.map +1 -1
- package/dist/codeblock.js +179 -180
- package/dist/codeblock.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 +6 -6
- 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/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/fragments.json +1 -1
- package/package.json +2 -2
- package/src/components/CodeBlock/CodeBlock.module.scss +16 -34
- package/src/components/CodeBlock/index.tsx +345 -347
- package/src/components/Combobox/Combobox.module.scss +13 -9
- package/src/components/ConversationList/ConversationList.fragment.tsx +96 -129
- 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/tokens/_computed.scss +7 -6
- package/src/tokens/_density.scss +87 -47
- package/src/tokens/_variables.scss +46 -31
|
@@ -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 */
|
|
@@ -200,11 +202,11 @@ function ChevronUpIcon({ className }: { className?: string }) {
|
|
|
200
202
|
|
|
201
203
|
function escapeHtml(str: string): string {
|
|
202
204
|
return str
|
|
203
|
-
.replace(/&/g,
|
|
204
|
-
.replace(/</g,
|
|
205
|
-
.replace(/>/g,
|
|
206
|
-
.replace(/"/g,
|
|
207
|
-
.replace(/'/g,
|
|
205
|
+
.replace(/&/g, "&")
|
|
206
|
+
.replace(/</g, "<")
|
|
207
|
+
.replace(/>/g, ">")
|
|
208
|
+
.replace(/"/g, """)
|
|
209
|
+
.replace(/'/g, "'");
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
/**
|
|
@@ -212,12 +214,12 @@ function escapeHtml(str: string): string {
|
|
|
212
214
|
* This handles template literals that have extra indentation from code formatting.
|
|
213
215
|
*/
|
|
214
216
|
function dedent(str: string): string {
|
|
215
|
-
const lines = str.split(
|
|
217
|
+
const lines = str.split("\n");
|
|
216
218
|
|
|
217
219
|
// Find the minimum indentation (ignoring empty lines)
|
|
218
220
|
let minIndent = Infinity;
|
|
219
221
|
for (const line of lines) {
|
|
220
|
-
if (line.trim() ===
|
|
222
|
+
if (line.trim() === "") continue;
|
|
221
223
|
const match = line.match(/^(\s*)/);
|
|
222
224
|
if (match) {
|
|
223
225
|
minIndent = Math.min(minIndent, match[1].length);
|
|
@@ -230,16 +232,14 @@ function dedent(str: string): string {
|
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
// Remove the common indentation from all lines
|
|
233
|
-
return lines
|
|
234
|
-
.map(line => line.slice(minIndent))
|
|
235
|
-
.join('\n');
|
|
235
|
+
return lines.map((line) => line.slice(minIndent)).join("\n");
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
/**
|
|
239
239
|
* Normalize indentation while handling JSX where first line is already at column 0.
|
|
240
240
|
*/
|
|
241
241
|
function normalizeIndentation(str: string): string {
|
|
242
|
-
const lines = str.split(
|
|
242
|
+
const lines = str.split("\n");
|
|
243
243
|
if (lines.length <= 1) return str;
|
|
244
244
|
|
|
245
245
|
let minIndent = Infinity;
|
|
@@ -260,18 +260,18 @@ function normalizeIndentation(str: string): string {
|
|
|
260
260
|
|
|
261
261
|
return lines
|
|
262
262
|
.map((line) => line.slice(Math.min(minIndent, line.match(/^(\s*)/)?.[1].length ?? 0)))
|
|
263
|
-
.join(
|
|
263
|
+
.join("\n");
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
function trimTrailingWhitespace(str: string): string {
|
|
267
267
|
return str
|
|
268
|
-
.split(
|
|
269
|
-
.map((line) => line.replace(/[ \t]+$/g,
|
|
270
|
-
.join(
|
|
268
|
+
.split("\n")
|
|
269
|
+
.map((line) => line.replace(/[ \t]+$/g, ""))
|
|
270
|
+
.join("\n");
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
function findTagEnd(line: string): number {
|
|
274
|
-
let quote: '"' | '
|
|
274
|
+
let quote: '"' | "'" | "`" | null = null;
|
|
275
275
|
let escaped = false;
|
|
276
276
|
let braceDepth = 0;
|
|
277
277
|
let bracketDepth = 0;
|
|
@@ -281,7 +281,7 @@ function findTagEnd(line: string): number {
|
|
|
281
281
|
const char = line[i];
|
|
282
282
|
|
|
283
283
|
if (quote) {
|
|
284
|
-
if (char ===
|
|
284
|
+
if (char === "\\" && !escaped) {
|
|
285
285
|
escaped = true;
|
|
286
286
|
continue;
|
|
287
287
|
}
|
|
@@ -292,18 +292,18 @@ function findTagEnd(line: string): number {
|
|
|
292
292
|
continue;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
if (char === '"' || char === '
|
|
295
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
296
296
|
quote = char;
|
|
297
297
|
continue;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
if (char ===
|
|
301
|
-
else if (char ===
|
|
302
|
-
else if (char ===
|
|
303
|
-
else if (char ===
|
|
304
|
-
else if (char ===
|
|
305
|
-
else if (char ===
|
|
306
|
-
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) {
|
|
307
307
|
return i;
|
|
308
308
|
}
|
|
309
309
|
}
|
|
@@ -313,8 +313,8 @@ function findTagEnd(line: string): number {
|
|
|
313
313
|
|
|
314
314
|
function splitJsxAttributes(attrs: string): string[] {
|
|
315
315
|
const parts: string[] = [];
|
|
316
|
-
let current =
|
|
317
|
-
let quote: '"' | '
|
|
316
|
+
let current = "";
|
|
317
|
+
let quote: '"' | "'" | "`" | null = null;
|
|
318
318
|
let escaped = false;
|
|
319
319
|
let braceDepth = 0;
|
|
320
320
|
let bracketDepth = 0;
|
|
@@ -323,7 +323,7 @@ function splitJsxAttributes(attrs: string): string[] {
|
|
|
323
323
|
for (const char of attrs) {
|
|
324
324
|
if (quote) {
|
|
325
325
|
current += char;
|
|
326
|
-
if (char ===
|
|
326
|
+
if (char === "\\" && !escaped) {
|
|
327
327
|
escaped = true;
|
|
328
328
|
continue;
|
|
329
329
|
}
|
|
@@ -334,23 +334,23 @@ function splitJsxAttributes(attrs: string): string[] {
|
|
|
334
334
|
continue;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
if (char === '"' || char === '
|
|
337
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
338
338
|
quote = char;
|
|
339
339
|
current += char;
|
|
340
340
|
continue;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
if (char ===
|
|
344
|
-
else if (char ===
|
|
345
|
-
else if (char ===
|
|
346
|
-
else if (char ===
|
|
347
|
-
else if (char ===
|
|
348
|
-
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);
|
|
349
349
|
|
|
350
350
|
if (/\s/.test(char) && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) {
|
|
351
351
|
if (current.trim().length > 0) {
|
|
352
352
|
parts.push(current.trim());
|
|
353
|
-
current =
|
|
353
|
+
current = "";
|
|
354
354
|
}
|
|
355
355
|
continue;
|
|
356
356
|
}
|
|
@@ -369,14 +369,14 @@ function formatLongJsxTagLine(line: string): string {
|
|
|
369
369
|
const maxInlineLength = 110;
|
|
370
370
|
if (line.length <= maxInlineLength) return line;
|
|
371
371
|
|
|
372
|
-
const indent = line.match(/^(\s*)/)?.[1] ??
|
|
372
|
+
const indent = line.match(/^(\s*)/)?.[1] ?? "";
|
|
373
373
|
const trimmed = line.trimStart();
|
|
374
374
|
|
|
375
375
|
if (
|
|
376
|
-
!trimmed.startsWith(
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
376
|
+
!trimmed.startsWith("<") ||
|
|
377
|
+
trimmed.startsWith("</") ||
|
|
378
|
+
trimmed.startsWith("<!") ||
|
|
379
|
+
trimmed.startsWith("<?")
|
|
380
380
|
) {
|
|
381
381
|
return line;
|
|
382
382
|
}
|
|
@@ -386,7 +386,7 @@ function formatLongJsxTagLine(line: string): string {
|
|
|
386
386
|
if (trimmed.slice(tagEnd + 1).trim().length > 0) return line;
|
|
387
387
|
|
|
388
388
|
const rawTagBody = trimmed.slice(1, tagEnd).trim();
|
|
389
|
-
const isSelfClosing = rawTagBody.endsWith(
|
|
389
|
+
const isSelfClosing = rawTagBody.endsWith("/");
|
|
390
390
|
const tagBody = isSelfClosing ? rawTagBody.slice(0, -1).trimEnd() : rawTagBody;
|
|
391
391
|
const firstSpace = tagBody.search(/\s/);
|
|
392
392
|
if (firstSpace === -1) return line;
|
|
@@ -395,31 +395,31 @@ function formatLongJsxTagLine(line: string): string {
|
|
|
395
395
|
if (!/^[A-Za-z][\w.:-]*$/.test(tagName)) return line;
|
|
396
396
|
|
|
397
397
|
const attrsSource = tagBody.slice(firstSpace).trim();
|
|
398
|
-
if (!attrsSource.includes(
|
|
398
|
+
if (!attrsSource.includes("=") && !attrsSource.includes("{...")) return line;
|
|
399
399
|
|
|
400
400
|
const attrs = splitJsxAttributes(attrsSource);
|
|
401
401
|
if (attrs.length === 0) return line;
|
|
402
402
|
|
|
403
403
|
const attrIndent = `${indent} `;
|
|
404
|
-
const close = isSelfClosing ?
|
|
404
|
+
const close = isSelfClosing ? "/>" : ">";
|
|
405
405
|
|
|
406
406
|
return [
|
|
407
407
|
`${indent}<${tagName}`,
|
|
408
408
|
...attrs.map((attr) => `${attrIndent}${attr}`),
|
|
409
409
|
`${indent}${close}`,
|
|
410
|
-
].join(
|
|
410
|
+
].join("\n");
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
function formatLongJsxTags(code: string): string {
|
|
414
414
|
return code
|
|
415
|
-
.split(
|
|
416
|
-
.flatMap((line) => formatLongJsxTagLine(line).split(
|
|
417
|
-
.join(
|
|
415
|
+
.split("\n")
|
|
416
|
+
.flatMap((line) => formatLongJsxTagLine(line).split("\n"))
|
|
417
|
+
.join("\n");
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
function normalizeCode(code: string): string {
|
|
421
421
|
const trimmed = code.trim();
|
|
422
|
-
if (trimmed.length === 0) return
|
|
422
|
+
if (trimmed.length === 0) return "";
|
|
423
423
|
|
|
424
424
|
const normalized = normalizeIndentation(trimmed);
|
|
425
425
|
const dedented = dedent(normalized);
|
|
@@ -436,9 +436,9 @@ function parseLineSpec(spec?: (number | string)[]): Set<number> {
|
|
|
436
436
|
if (!spec) return lines;
|
|
437
437
|
|
|
438
438
|
for (const item of spec) {
|
|
439
|
-
if (typeof item ===
|
|
439
|
+
if (typeof item === "number") {
|
|
440
440
|
lines.add(item);
|
|
441
|
-
} else if (typeof item ===
|
|
441
|
+
} else if (typeof item === "string") {
|
|
442
442
|
const rangeMatch = item.match(/^(\d+)-(\d+)$/);
|
|
443
443
|
if (rangeMatch) {
|
|
444
444
|
const start = parseInt(rangeMatch[1], 10);
|
|
@@ -483,7 +483,7 @@ function processShikiHtml(html: string, options: ProcessOptions): string {
|
|
|
483
483
|
if (!codeMatch) return html;
|
|
484
484
|
|
|
485
485
|
const codeContent = codeMatch[1];
|
|
486
|
-
const lines = codeContent.split(
|
|
486
|
+
const lines = codeContent.split("\n");
|
|
487
487
|
|
|
488
488
|
// Process each line
|
|
489
489
|
const processedLines = lines.map((line, index) => {
|
|
@@ -493,265 +493,263 @@ function processShikiHtml(html: string, options: ProcessOptions): string {
|
|
|
493
493
|
const isAdded = addedLines.has(lineNum);
|
|
494
494
|
const isRemoved = removedLines.has(lineNum);
|
|
495
495
|
|
|
496
|
-
const lineClasses = [
|
|
497
|
-
if (isHighlighted) lineClasses.push(
|
|
498
|
-
if (isAdded) lineClasses.push(
|
|
499
|
-
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");
|
|
500
500
|
|
|
501
|
-
const lineClass = lineClasses.join(
|
|
502
|
-
const diffMarker = isAdded ?
|
|
501
|
+
const lineClass = lineClasses.join(" ");
|
|
502
|
+
const diffMarker = isAdded ? "+" : isRemoved ? "-" : " ";
|
|
503
503
|
|
|
504
504
|
if (showLineNumbers || hasDiff) {
|
|
505
505
|
const lineNumHtml = showLineNumbers
|
|
506
506
|
? `<span class="line-number">${displayLineNum}</span>`
|
|
507
|
-
:
|
|
508
|
-
const diffMarkerHtml = hasDiff
|
|
509
|
-
? `<span class="diff-marker">${diffMarker}</span>`
|
|
510
|
-
: '';
|
|
507
|
+
: "";
|
|
508
|
+
const diffMarkerHtml = hasDiff ? `<span class="diff-marker">${diffMarker}</span>` : "";
|
|
511
509
|
return `<span class="${lineClass}">${lineNumHtml}${diffMarkerHtml}${line}</span>`;
|
|
512
510
|
}
|
|
513
511
|
return `<span class="${lineClass}">${line}</span>`;
|
|
514
512
|
});
|
|
515
513
|
|
|
516
514
|
// Reconstruct the HTML
|
|
517
|
-
return html.replace(
|
|
518
|
-
/<code[^>]*>[\s\S]*?<\/code>/,
|
|
519
|
-
`<code>${processedLines.join('\n')}</code>`
|
|
520
|
-
);
|
|
515
|
+
return html.replace(/<code[^>]*>[\s\S]*?<\/code>/, `<code>${processedLines.join("\n")}</code>`);
|
|
521
516
|
}
|
|
522
517
|
|
|
523
|
-
const CodeBlockBase = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
console.warn(
|
|
588
|
-
'[@fragments-sdk/ui] CodeBlock: shiki is not installed. ' +
|
|
589
|
-
'Install it with: npm install shiki'
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
// Fallback to plain text without syntax highlighting
|
|
593
|
-
setHighlightedHtml(
|
|
594
|
-
`<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"
|
|
595
582
|
);
|
|
596
|
-
setIsLoading(false);
|
|
597
|
-
return;
|
|
598
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
|
+
}
|
|
599
589
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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
|
+
}
|
|
603
606
|
})
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
showLineNumbers,
|
|
608
|
-
startLineNumber,
|
|
609
|
-
highlightLines: highlightSet,
|
|
610
|
-
addedLines: addedSet,
|
|
611
|
-
removedLines: removedSet,
|
|
612
|
-
});
|
|
613
|
-
setHighlightedHtml(processed);
|
|
614
|
-
setIsLoading(false);
|
|
615
|
-
}
|
|
616
|
-
})
|
|
617
|
-
.catch((err) => {
|
|
618
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
619
|
-
console.error('Syntax highlighting failed:', err);
|
|
620
|
-
}
|
|
621
|
-
if (!cancelled) {
|
|
622
|
-
// Fallback to plain text
|
|
623
|
-
setHighlightedHtml(
|
|
624
|
-
`<pre class="shiki"><code>${escapeHtml(visibleCode)}</code></pre>`
|
|
625
|
-
);
|
|
626
|
-
setIsLoading(false);
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
return () => {
|
|
631
|
-
cancelled = true;
|
|
632
|
-
};
|
|
633
|
-
}, [visibleCode, language, theme, showLineNumbers, startLineNumber, highlightSet, addedSet, removedSet]);
|
|
634
|
-
|
|
635
|
-
const handleCopy = useCallback(async () => {
|
|
636
|
-
try {
|
|
637
|
-
// Always copy the full code, even when collapsed
|
|
638
|
-
await navigator.clipboard.writeText(trimmedCode);
|
|
639
|
-
setCopied(true);
|
|
640
|
-
setTimeout(() => setCopied(false), 2000);
|
|
641
|
-
onCopy?.();
|
|
642
|
-
} catch (err) {
|
|
643
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
644
|
-
console.error('Failed to copy:', err);
|
|
607
|
+
.catch((err) => {
|
|
608
|
+
if (process.env.NODE_ENV !== "production") {
|
|
609
|
+
console.error("Syntax highlighting failed:", err);
|
|
645
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);
|
|
646
642
|
}
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
className={`${styles.copyButton} ${copied ? styles.copied : ''}`}
|
|
686
|
-
aria-label={copied ? 'Copied!' : 'Copy code'}
|
|
687
|
-
>
|
|
688
|
-
{copied ? <CheckIcon className={styles.icon} /> : <CopyIcon className={styles.icon} />}
|
|
689
|
-
</button>
|
|
690
|
-
)}
|
|
691
|
-
</div>
|
|
692
|
-
)}
|
|
693
|
-
{shouldShowOverlayCopy && (
|
|
694
|
-
<button
|
|
695
|
-
type="button"
|
|
696
|
-
onClick={handleCopy}
|
|
697
|
-
className={`${styles.copyButton} ${styles.copyOverlay} ${copied ? styles.copied : ''}`}
|
|
698
|
-
aria-label={copied ? 'Copied!' : 'Copy code'}
|
|
699
|
-
>
|
|
700
|
-
{copied ? <CheckIcon className={styles.icon} /> : <CopyIcon className={styles.icon} />}
|
|
701
|
-
</button>
|
|
702
|
-
)}
|
|
703
|
-
{isLoading ? (
|
|
704
|
-
<div className={styles.loading} style={codeContainerStyle}>
|
|
705
|
-
<pre>
|
|
706
|
-
<code>{visibleCode}</code>
|
|
707
|
-
</pre>
|
|
708
|
-
</div>
|
|
709
|
-
) : (
|
|
710
|
-
<div
|
|
711
|
-
className={styles.codeContainer}
|
|
712
|
-
style={codeContainerStyle}
|
|
713
|
-
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
|
714
|
-
/>
|
|
715
|
-
)}
|
|
716
|
-
{persistentCopy && (
|
|
717
|
-
<div className={styles.persistentCopy}>
|
|
718
|
-
<Button
|
|
719
|
-
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"
|
|
720
681
|
onClick={handleCopy}
|
|
721
|
-
|
|
722
|
-
|
|
682
|
+
className={`${styles.copyButton} ${copied ? styles.copied : ""}`}
|
|
683
|
+
aria-label={copied ? "Copied!" : "Copy code"}
|
|
723
684
|
>
|
|
724
|
-
{copied ?
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
</
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
+
)}
|
|
751
748
|
</div>
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
);
|
|
749
|
+
{caption && <div className={styles.caption}>{caption}</div>}
|
|
750
|
+
</div>
|
|
751
|
+
);
|
|
752
|
+
});
|
|
755
753
|
|
|
756
754
|
// ============================================
|
|
757
755
|
// Tabbed Code Block
|
|
@@ -780,7 +778,7 @@ export interface TabbedCodeBlockProps {
|
|
|
780
778
|
/** Syntax highlighting theme (applies to all tabs) */
|
|
781
779
|
theme?: CodeBlockTheme;
|
|
782
780
|
/** Tab list visual style */
|
|
783
|
-
tabsVariant?:
|
|
781
|
+
tabsVariant?: "underline" | "pills";
|
|
784
782
|
/** Enable word wrapping for long lines */
|
|
785
783
|
wordWrap?: boolean;
|
|
786
784
|
/** Maximum height in pixels (enables scrolling) */
|
|
@@ -795,16 +793,16 @@ function TabbedCodeBlock({
|
|
|
795
793
|
tabs,
|
|
796
794
|
defaultTab,
|
|
797
795
|
showCopy = true,
|
|
798
|
-
copyPlacement =
|
|
796
|
+
copyPlacement = "auto",
|
|
799
797
|
showLineNumbers = false,
|
|
800
798
|
theme,
|
|
801
|
-
tabsVariant =
|
|
799
|
+
tabsVariant = "pills",
|
|
802
800
|
wordWrap,
|
|
803
801
|
maxHeight,
|
|
804
802
|
className,
|
|
805
803
|
onCopy,
|
|
806
804
|
}: TabbedCodeBlockProps) {
|
|
807
|
-
const defaultValue = defaultTab || tabs[0]?.label ||
|
|
805
|
+
const defaultValue = defaultTab || tabs[0]?.label || "";
|
|
808
806
|
|
|
809
807
|
return (
|
|
810
808
|
<div className={className}>
|