@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.
Files changed (126) hide show
  1. package/dist/assets/ui.css +443 -247
  2. package/dist/blocks/components/index.d.ts +0 -2
  3. package/dist/blocks/components/index.d.ts.map +1 -1
  4. package/dist/codeblock.cjs +187 -184
  5. package/dist/codeblock.cjs.map +1 -1
  6. package/dist/codeblock.js +183 -180
  7. package/dist/codeblock.js.map +1 -1
  8. package/dist/components/Box/Box.module.scss.cjs +73 -0
  9. package/dist/components/Box/Box.module.scss.cjs.map +1 -1
  10. package/dist/components/Box/Box.module.scss.js +73 -0
  11. package/dist/components/Box/Box.module.scss.js.map +1 -1
  12. package/dist/components/ButtonGroup/ButtonGroup.module.scss.cjs +6 -0
  13. package/dist/components/ButtonGroup/ButtonGroup.module.scss.cjs.map +1 -1
  14. package/dist/components/ButtonGroup/ButtonGroup.module.scss.js +6 -0
  15. package/dist/components/ButtonGroup/ButtonGroup.module.scss.js.map +1 -1
  16. package/dist/components/CodeBlock/CodeBlock.module.scss.cjs +20 -23
  17. package/dist/components/CodeBlock/CodeBlock.module.scss.cjs.map +1 -1
  18. package/dist/components/CodeBlock/CodeBlock.module.scss.js +20 -23
  19. package/dist/components/CodeBlock/CodeBlock.module.scss.js.map +1 -1
  20. package/dist/components/CodeBlock/index.d.ts +11 -7
  21. package/dist/components/CodeBlock/index.d.ts.map +1 -1
  22. package/dist/components/Combobox/Combobox.module.scss.cjs +15 -15
  23. package/dist/components/Combobox/Combobox.module.scss.js +15 -15
  24. package/dist/components/DataTable/DataTable.module.scss.cjs +84 -0
  25. package/dist/components/DataTable/DataTable.module.scss.cjs.map +1 -0
  26. package/dist/components/DataTable/DataTable.module.scss.js +84 -0
  27. package/dist/components/DataTable/DataTable.module.scss.js.map +1 -0
  28. package/dist/components/DataTable/index.cjs +383 -0
  29. package/dist/components/DataTable/index.cjs.map +1 -0
  30. package/dist/components/DataTable/index.d.ts +78 -0
  31. package/dist/components/DataTable/index.d.ts.map +1 -0
  32. package/dist/components/DataTable/index.js +366 -0
  33. package/dist/components/DataTable/index.js.map +1 -0
  34. package/dist/components/Drawer/Drawer.module.scss.cjs +9 -0
  35. package/dist/components/Drawer/Drawer.module.scss.cjs.map +1 -1
  36. package/dist/components/Drawer/Drawer.module.scss.js +9 -0
  37. package/dist/components/Drawer/Drawer.module.scss.js.map +1 -1
  38. package/dist/components/Image/Image.module.scss.cjs +12 -0
  39. package/dist/components/Image/Image.module.scss.cjs.map +1 -1
  40. package/dist/components/Image/Image.module.scss.js +12 -0
  41. package/dist/components/Image/Image.module.scss.js.map +1 -1
  42. package/dist/components/Link/Link.module.scss.cjs +3 -0
  43. package/dist/components/Link/Link.module.scss.cjs.map +1 -1
  44. package/dist/components/Link/Link.module.scss.js +3 -0
  45. package/dist/components/Link/Link.module.scss.js.map +1 -1
  46. package/dist/components/List/List.module.scss.cjs +5 -0
  47. package/dist/components/List/List.module.scss.cjs.map +1 -1
  48. package/dist/components/List/List.module.scss.js +5 -0
  49. package/dist/components/List/List.module.scss.js.map +1 -1
  50. package/dist/components/Loading/Loading.module.scss.cjs +5 -0
  51. package/dist/components/Loading/Loading.module.scss.cjs.map +1 -1
  52. package/dist/components/Loading/Loading.module.scss.js +5 -0
  53. package/dist/components/Loading/Loading.module.scss.js.map +1 -1
  54. package/dist/components/Markdown/Markdown.module.scss.cjs +1 -1
  55. package/dist/components/Markdown/Markdown.module.scss.js +1 -1
  56. package/dist/components/Message/Message.module.scss.cjs +22 -16
  57. package/dist/components/Message/Message.module.scss.cjs.map +1 -1
  58. package/dist/components/Message/Message.module.scss.js +22 -16
  59. package/dist/components/Message/Message.module.scss.js.map +1 -1
  60. package/dist/components/Message/index.cjs +5 -3
  61. package/dist/components/Message/index.cjs.map +1 -1
  62. package/dist/components/Message/index.d.ts +5 -1
  63. package/dist/components/Message/index.d.ts.map +1 -1
  64. package/dist/components/Message/index.js +5 -3
  65. package/dist/components/Message/index.js.map +1 -1
  66. package/dist/components/Skeleton/Skeleton.module.scss.cjs +14 -0
  67. package/dist/components/Skeleton/Skeleton.module.scss.cjs.map +1 -1
  68. package/dist/components/Skeleton/Skeleton.module.scss.js +14 -0
  69. package/dist/components/Skeleton/Skeleton.module.scss.js.map +1 -1
  70. package/dist/components/Stack/Stack.module.scss.cjs +14 -0
  71. package/dist/components/Stack/Stack.module.scss.cjs.map +1 -1
  72. package/dist/components/Stack/Stack.module.scss.js +14 -0
  73. package/dist/components/Stack/Stack.module.scss.js.map +1 -1
  74. package/dist/components/Table/Table.module.scss.cjs +21 -36
  75. package/dist/components/Table/Table.module.scss.cjs.map +1 -1
  76. package/dist/components/Table/Table.module.scss.js +21 -36
  77. package/dist/components/Table/Table.module.scss.js.map +1 -1
  78. package/dist/components/Table/index.d.ts +35 -55
  79. package/dist/components/Table/index.d.ts.map +1 -1
  80. package/dist/components/Text/Text.module.scss.cjs +14 -0
  81. package/dist/components/Text/Text.module.scss.cjs.map +1 -1
  82. package/dist/components/Text/Text.module.scss.js +14 -0
  83. package/dist/components/Text/Text.module.scss.js.map +1 -1
  84. package/dist/components/Textarea/Textarea.module.scss.cjs +4 -0
  85. package/dist/components/Textarea/Textarea.module.scss.cjs.map +1 -1
  86. package/dist/components/Textarea/Textarea.module.scss.js +4 -0
  87. package/dist/components/Textarea/Textarea.module.scss.js.map +1 -1
  88. package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs +5 -0
  89. package/dist/components/ToggleGroup/ToggleGroup.module.scss.cjs.map +1 -1
  90. package/dist/components/ToggleGroup/ToggleGroup.module.scss.js +5 -0
  91. package/dist/components/ToggleGroup/ToggleGroup.module.scss.js.map +1 -1
  92. package/dist/index.cjs +119 -117
  93. package/dist/index.cjs.map +1 -1
  94. package/dist/index.d.ts +2 -1
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +3 -1
  97. package/dist/index.js.map +1 -1
  98. package/dist/table.cjs +44 -262
  99. package/dist/table.cjs.map +1 -1
  100. package/dist/table.js +47 -248
  101. package/dist/table.js.map +1 -1
  102. package/fragments.json +1 -1
  103. package/package.json +110 -118
  104. package/src/blocks/components/index.ts +0 -3
  105. package/src/components/CodeBlock/CodeBlock.module.scss +16 -34
  106. package/src/components/CodeBlock/index.tsx +351 -345
  107. package/src/components/Combobox/Combobox.module.scss +13 -9
  108. package/src/components/ConversationList/ConversationList.fragment.tsx +96 -129
  109. package/src/components/DataTable/DataTable.fragment.tsx +754 -0
  110. package/src/components/DataTable/DataTable.module.scss +300 -0
  111. package/src/components/DataTable/DataTable.test.tsx +224 -0
  112. package/src/components/DataTable/index.tsx +533 -0
  113. package/src/components/Message/Message.fragment.tsx +34 -0
  114. package/src/components/Message/Message.module.scss +11 -0
  115. package/src/components/Message/index.tsx +12 -3
  116. package/src/components/Table/Table.fragment.tsx +190 -175
  117. package/src/components/Table/Table.module.scss +15 -88
  118. package/src/components/Table/Table.test.tsx +184 -94
  119. package/src/components/Table/index.tsx +105 -374
  120. package/src/index.ts +15 -4
  121. package/src/tokens/_computed.scss +7 -6
  122. package/src/tokens/_density.scss +87 -47
  123. package/src/tokens/_variables.scss +46 -31
  124. package/dist/blocks/components/DataTable.d.ts +0 -19
  125. package/dist/blocks/components/DataTable.d.ts.map +0 -1
  126. package/src/blocks/components/DataTable.tsx +0 -124
@@ -1,12 +1,14 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import * as React from 'react';
4
- import { useState, useCallback, useEffect, useMemo } from 'react';
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: ((code: string, options: { lang: string; theme: string }) => Promise<string>) | null = null;
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('shiki');
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 '../Tabs';
25
- import { Button } from '../Button';
26
- import styles from './CodeBlock.module.scss';
27
- import '../../styles/globals.scss';
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
- | 'tsx'
31
- | 'typescript'
32
- | 'javascript'
33
- | 'jsx'
34
- | 'bash'
35
- | 'shell'
36
- | 'css'
37
- | 'scss'
38
- | 'sass'
39
- | 'json'
40
- | 'html'
41
- | 'xml'
42
- | 'markdown'
43
- | 'md'
44
- | 'yaml'
45
- | 'yml'
46
- | 'python'
47
- | 'py'
48
- | 'ruby'
49
- | 'go'
50
- | 'rust'
51
- | 'java'
52
- | 'kotlin'
53
- | 'swift'
54
- | 'c'
55
- | 'cpp'
56
- | 'csharp'
57
- | 'php'
58
- | 'sql'
59
- | 'graphql'
60
- | 'diff'
61
- | 'plaintext';
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
- | 'synthwave-84'
66
- | 'github-dark'
67
- | 'github-light'
68
- | 'one-dark-pro'
69
- | 'dracula'
70
- | 'nord'
71
- | 'monokai'
72
- | 'vitesse-dark'
73
- | 'vitesse-light'
74
- | 'min-dark'
75
- | 'min-light';
76
-
77
- export type CodeBlockCopyPlacement = 'auto' | 'header' | 'overlay';
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, '&amp;')
202
- .replace(/</g, '&lt;')
203
- .replace(/>/g, '&gt;')
204
- .replace(/"/g, '&quot;')
205
- .replace(/'/g, '&#039;');
205
+ .replace(/&/g, "&amp;")
206
+ .replace(/</g, "&lt;")
207
+ .replace(/>/g, "&gt;")
208
+ .replace(/"/g, "&quot;")
209
+ .replace(/'/g, "&#039;");
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('\n');
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() === '') continue;
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('\n');
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('\n');
263
+ .join("\n");
262
264
  }
263
265
 
264
266
  function trimTrailingWhitespace(str: string): string {
265
267
  return str
266
- .split('\n')
267
- .map((line) => line.replace(/[ \t]+$/g, ''))
268
- .join('\n');
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: '"' | '\'' | '`' | null = null;
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 === '\\' && !escaped) {
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 === '\'' || char === '`') {
295
+ if (char === '"' || char === "'" || char === "`") {
294
296
  quote = char;
295
297
  continue;
296
298
  }
297
299
 
298
- if (char === '{') braceDepth += 1;
299
- else if (char === '}') braceDepth = Math.max(0, braceDepth - 1);
300
- else if (char === '[') bracketDepth += 1;
301
- else if (char === ']') bracketDepth = Math.max(0, bracketDepth - 1);
302
- else if (char === '(') parenDepth += 1;
303
- else if (char === ')') parenDepth = Math.max(0, parenDepth - 1);
304
- else if (char === '>' && braceDepth === 0 && bracketDepth === 0 && parenDepth === 0) {
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: '"' | '\'' | '`' | null = null;
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 === '\\' && !escaped) {
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 === '\'' || char === '`') {
337
+ if (char === '"' || char === "'" || char === "`") {
336
338
  quote = char;
337
339
  current += char;
338
340
  continue;
339
341
  }
340
342
 
341
- if (char === '{') braceDepth += 1;
342
- else if (char === '}') braceDepth = Math.max(0, braceDepth - 1);
343
- else if (char === '[') bracketDepth += 1;
344
- else if (char === ']') bracketDepth = Math.max(0, bracketDepth - 1);
345
- else if (char === '(') parenDepth += 1;
346
- else if (char === ')') parenDepth = Math.max(0, parenDepth - 1);
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
- || trimmed.startsWith('</')
376
- || trimmed.startsWith('<!')
377
- || trimmed.startsWith('<?')
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('=') && !attrsSource.includes('{...')) return line;
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('\n');
410
+ ].join("\n");
409
411
  }
410
412
 
411
413
  function formatLongJsxTags(code: string): string {
412
414
  return code
413
- .split('\n')
414
- .flatMap((line) => formatLongJsxTagLine(line).split('\n'))
415
- .join('\n');
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 === 'number') {
439
+ if (typeof item === "number") {
438
440
  lines.add(item);
439
- } else if (typeof item === 'string') {
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('\n');
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 = ['line'];
495
- if (isHighlighted) lineClasses.push('highlighted');
496
- if (isAdded) lineClasses.push('diff-added');
497
- if (isRemoved) lineClasses.push('diff-removed');
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 ? '+' : isRemoved ? '-' : ' ';
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
- function CodeBlock(
523
- {
524
- code,
525
- language = 'tsx',
526
- theme = 'one-dark-pro',
527
- showCopy = true,
528
- title,
529
- filename,
530
- caption,
531
- showLineNumbers = false,
532
- startLineNumber = 1,
533
- highlightLines,
534
- addedLines,
535
- removedLines,
536
- wordWrap = false,
537
- maxHeight,
538
- collapsible = false,
539
- defaultCollapsed = false,
540
- collapsedLines = 5,
541
- compact = false,
542
- persistentCopy = false,
543
- copyPlacement = 'auto',
544
- className,
545
- ...htmlProps
546
- },
547
- ref
548
- ) {
549
- const [copied, setCopied] = useState(false);
550
- const [highlightedHtml, setHighlightedHtml] = useState<string>('');
551
- const [isLoading, setIsLoading] = useState(true);
552
- const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
553
-
554
- const trimmedCode = useMemo(() => normalizeCode(code), [code]);
555
- const codeLines = trimmedCode.split('\n');
556
- const totalLines = codeLines.length;
557
- const shouldShowCollapse = collapsible && totalLines > collapsedLines;
558
-
559
- // Compute visible code when collapsed
560
- const visibleCode = shouldShowCollapse && isCollapsed
561
- ? codeLines.slice(0, collapsedLines).join('\n')
562
- : trimmedCode;
563
-
564
- const highlightSet = useMemo(() => parseLineSpec(highlightLines), [highlightLines]);
565
- const addedSet = useMemo(() => parseLineSpec(addedLines), [addedLines]);
566
- const removedSet = useMemo(() => parseLineSpec(removedLines), [removedLines]);
567
- const hasDiff = addedSet.size > 0 || removedSet.size > 0;
568
- const resolvedCopyPlacement = copyPlacement === 'auto'
569
- ? (filename ? 'header' : 'overlay')
570
- : copyPlacement;
571
- const shouldShowHeaderCopy = showCopy && !persistentCopy && resolvedCopyPlacement === 'header';
572
- const shouldShowOverlayCopy = showCopy && !persistentCopy && resolvedCopyPlacement === 'overlay';
573
- const shouldRenderHeader = Boolean(filename) || shouldShowHeaderCopy;
574
-
575
- // Apply syntax highlighting
576
- useEffect(() => {
577
- let cancelled = false;
578
- setIsLoading(true);
579
-
580
- loadShikiDeps();
581
-
582
- if (_shikiFailed || !_codeToHtml) {
583
- if (_shikiFailed && process.env.NODE_ENV === 'development') {
584
- console.warn(
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
- _codeToHtml(visibleCode, {
598
- lang: language,
599
- theme,
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
- .then((html) => {
602
- if (!cancelled) {
603
- const processed = processShikiHtml(html, {
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
- }, [trimmedCode]);
644
-
645
- const toggleCollapsed = useCallback(() => {
646
- setIsCollapsed((prev) => !prev);
647
- }, []);
648
-
649
- const classNames = [
650
- styles.container,
651
- showLineNumbers && styles.withLineNumbers,
652
- hasDiff && styles.withDiff,
653
- wordWrap && styles.wordWrap,
654
- compact && styles.compact,
655
- className,
656
- ]
657
- .filter(Boolean)
658
- .join(' ');
659
-
660
- const wrapperClasses = [
661
- styles.wrapper,
662
- persistentCopy && styles.persistentCopyWrapper,
663
- shouldShowOverlayCopy && styles.withCopyOverlay,
664
- ].filter(Boolean).join(' ');
665
-
666
- const codeContainerStyle: React.CSSProperties = maxHeight
667
- ? { maxHeight, overflow: 'auto' }
668
- : {};
669
-
670
- return (
671
- <div ref={ref} {...htmlProps} className={classNames} data-theme="dark">
672
- {title && <div className={styles.title}>{title}</div>}
673
- <div className={wrapperClasses}>
674
- {shouldRenderHeader && (
675
- <div className={styles.header}>
676
- <span className={styles.filename}>{filename ?? ''}</span>
677
- {shouldShowHeaderCopy && (
678
- <button
679
- type="button"
680
- onClick={handleCopy}
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
- aria-label={copied ? 'Copied!' : 'Copy code'}
718
- icon={copied ? true : false}
682
+ className={`${styles.copyButton} ${copied ? styles.copied : ""}`}
683
+ aria-label={copied ? "Copied!" : "Copy code"}
719
684
  >
720
- {copied ? <CheckIcon className={styles.icon} /> : <CopyIcon className={styles.icon} />}
721
- </Button>
722
- </div>
723
- )}
724
- {shouldShowCollapse && (
725
- <button
726
- type="button"
727
- onClick={toggleCollapsed}
728
- className={styles.collapseButton}
729
- aria-expanded={!isCollapsed}
730
- aria-label={isCollapsed ? 'Expand code' : 'Collapse code'}
731
- >
732
- {isCollapsed ? (
733
- <>
734
- <ChevronDownIcon className={styles.icon} />
735
- <span>Show {totalLines - collapsedLines} more lines</span>
736
- </>
737
- ) : (
738
- <>
739
- <ChevronUpIcon className={styles.icon} />
740
- <span>Show less</span>
741
- </>
742
- )}
743
- </button>
744
- )}
745
- </div>
746
- {caption && <div className={styles.caption}>{caption}</div>}
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?: 'underline' | 'pills';
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 = 'auto',
796
+ copyPlacement = "auto",
793
797
  showLineNumbers = false,
794
798
  theme,
795
- tabsVariant = 'pills',
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
  ))}