@codrstudio/openclaude-chat 0.1.0 → 0.1.9

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 (62) hide show
  1. package/dist/components/StreamingIndicator.js +5 -5
  2. package/dist/display/DisplayReactRenderer.js +12 -12
  3. package/dist/display/react-sandbox/bootstrap.js +150 -150
  4. package/dist/styles.css +1 -2
  5. package/package.json +64 -61
  6. package/src/components/Chat.tsx +107 -107
  7. package/src/components/ErrorNote.tsx +35 -35
  8. package/src/components/LazyRender.tsx +42 -42
  9. package/src/components/Markdown.tsx +114 -114
  10. package/src/components/MessageBubble.tsx +107 -107
  11. package/src/components/MessageInput.tsx +421 -421
  12. package/src/components/MessageList.tsx +153 -153
  13. package/src/components/StreamingIndicator.tsx +19 -19
  14. package/src/display/AlertRenderer.tsx +23 -23
  15. package/src/display/CarouselRenderer.tsx +141 -141
  16. package/src/display/ChartRenderer.tsx +195 -195
  17. package/src/display/ChoiceButtonsRenderer.tsx +114 -114
  18. package/src/display/CodeBlockRenderer.tsx +49 -49
  19. package/src/display/ComparisonTableRenderer.tsx +132 -132
  20. package/src/display/DataTableRenderer.tsx +144 -144
  21. package/src/display/DisplayReactRenderer.tsx +269 -269
  22. package/src/display/FileCardRenderer.tsx +55 -55
  23. package/src/display/GalleryRenderer.tsx +65 -65
  24. package/src/display/ImageViewerRenderer.tsx +114 -114
  25. package/src/display/LinkPreviewRenderer.tsx +74 -74
  26. package/src/display/MapViewRenderer.tsx +75 -75
  27. package/src/display/MetricCardRenderer.tsx +29 -29
  28. package/src/display/PriceHighlightRenderer.tsx +62 -62
  29. package/src/display/ProductCardRenderer.tsx +112 -112
  30. package/src/display/ProgressStepsRenderer.tsx +59 -59
  31. package/src/display/SourcesListRenderer.tsx +47 -47
  32. package/src/display/SpreadsheetRenderer.tsx +86 -86
  33. package/src/display/StepTimelineRenderer.tsx +75 -75
  34. package/src/display/index.ts +21 -21
  35. package/src/display/react-sandbox/bootstrap.ts +155 -155
  36. package/src/display/registry.ts +84 -84
  37. package/src/display/sdk-types.ts +217 -217
  38. package/src/hooks/ChatProvider.tsx +21 -21
  39. package/src/hooks/useIsMobile.ts +15 -15
  40. package/src/hooks/useOpenClaudeChat.ts +476 -476
  41. package/src/index.ts +76 -76
  42. package/src/lib/utils.ts +6 -6
  43. package/src/parts/PartErrorBoundary.tsx +51 -51
  44. package/src/parts/PartRenderer.tsx +145 -145
  45. package/src/parts/ReasoningBlock.tsx +41 -41
  46. package/src/parts/ToolActivity.tsx +78 -78
  47. package/src/parts/ToolResult.tsx +79 -79
  48. package/src/styles.css +2 -2
  49. package/src/types.ts +41 -41
  50. package/src/ui/alert.tsx +77 -77
  51. package/src/ui/badge.tsx +36 -36
  52. package/src/ui/button.tsx +54 -54
  53. package/src/ui/card.tsx +68 -68
  54. package/src/ui/collapsible.tsx +7 -7
  55. package/src/ui/dialog.tsx +122 -122
  56. package/src/ui/dropdown-menu.tsx +76 -76
  57. package/src/ui/input.tsx +24 -24
  58. package/src/ui/progress.tsx +36 -36
  59. package/src/ui/scroll-area.tsx +48 -48
  60. package/src/ui/separator.tsx +31 -31
  61. package/src/ui/skeleton.tsx +9 -9
  62. package/src/ui/table.tsx +114 -114
@@ -1,49 +1,49 @@
1
- import type { DisplayCode } from "./sdk-types.js";
2
- import { Check, Copy } from "lucide-react";
3
- import { useState } from "react";
4
- import { Button } from "../ui/button.js";
5
- import { cn } from "../lib/utils.js";
6
-
7
- export function CodeBlockRenderer({ language, code, title, lineNumbers }: DisplayCode) {
8
- const [copied, setCopied] = useState(false);
9
-
10
- async function handleCopy() {
11
- await navigator.clipboard.writeText(code);
12
- setCopied(true);
13
- setTimeout(() => setCopied(false), 2000);
14
- }
15
-
16
- const displayLines = lineNumbers
17
- ? code.split("\n").map((line, i) => (
18
- <span key={i} className="flex gap-4">
19
- <span className="select-none text-muted-foreground w-6 text-right shrink-0">{i + 1}</span>
20
- <span>{line}</span>
21
- </span>
22
- ))
23
- : code;
24
-
25
- return (
26
- <div className={cn("rounded-md border border-border bg-muted/30 overflow-hidden")}>
27
- <div className="flex items-center justify-between px-3 py-1.5 border-b border-border">
28
- <span className="text-xs text-muted-foreground font-mono">{title ?? language}</span>
29
- <Button
30
- variant="ghost"
31
- size="sm"
32
- onClick={handleCopy}
33
- aria-label={copied ? "Copiado!" : "Copiar código"}
34
- className="h-7 gap-1.5"
35
- >
36
- {copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
37
- <span className="text-xs">{copied ? "Copiado!" : "Copiar"}</span>
38
- </Button>
39
- </div>
40
- <pre className="p-4 overflow-x-auto font-mono text-sm">
41
- {lineNumbers ? (
42
- <code>{displayLines}</code>
43
- ) : (
44
- <code>{code}</code>
45
- )}
46
- </pre>
47
- </div>
48
- );
49
- }
1
+ import type { DisplayCode } from "./sdk-types.js";
2
+ import { Check, Copy } from "lucide-react";
3
+ import { useState } from "react";
4
+ import { Button } from "../ui/button.js";
5
+ import { cn } from "../lib/utils.js";
6
+
7
+ export function CodeBlockRenderer({ language, code, title, lineNumbers }: DisplayCode) {
8
+ const [copied, setCopied] = useState(false);
9
+
10
+ async function handleCopy() {
11
+ await navigator.clipboard.writeText(code);
12
+ setCopied(true);
13
+ setTimeout(() => setCopied(false), 2000);
14
+ }
15
+
16
+ const displayLines = lineNumbers
17
+ ? code.split("\n").map((line, i) => (
18
+ <span key={i} className="flex gap-4">
19
+ <span className="select-none text-muted-foreground w-6 text-right shrink-0">{i + 1}</span>
20
+ <span>{line}</span>
21
+ </span>
22
+ ))
23
+ : code;
24
+
25
+ return (
26
+ <div className={cn("rounded-md border border-border bg-muted/30 overflow-hidden")}>
27
+ <div className="flex items-center justify-between px-3 py-1.5 border-b border-border">
28
+ <span className="text-xs text-muted-foreground font-mono">{title ?? language}</span>
29
+ <Button
30
+ variant="ghost"
31
+ size="sm"
32
+ onClick={handleCopy}
33
+ aria-label={copied ? "Copiado!" : "Copiar código"}
34
+ className="h-7 gap-1.5"
35
+ >
36
+ {copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
37
+ <span className="text-xs">{copied ? "Copiado!" : "Copiar"}</span>
38
+ </Button>
39
+ </div>
40
+ <pre className="p-4 overflow-x-auto font-mono text-sm">
41
+ {lineNumbers ? (
42
+ <code>{displayLines}</code>
43
+ ) : (
44
+ <code>{code}</code>
45
+ )}
46
+ </pre>
47
+ </div>
48
+ );
49
+ }
@@ -1,132 +1,132 @@
1
- import { useState } from "react";
2
- import type { DisplayComparison } from "./sdk-types.js";
3
- import { CheckCircle } from "lucide-react";
4
- import { ScrollArea, ScrollBar } from "../ui/scroll-area.js";
5
- import {
6
- Table,
7
- TableBody,
8
- TableCell,
9
- TableHead,
10
- TableHeader,
11
- TableRow,
12
- } from "../ui/table.js";
13
- import { cn } from "../lib/utils.js";
14
-
15
- function formatMoney(value: number, currency = "BRL") {
16
- return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
17
- }
18
-
19
- export function ComparisonTableRenderer({ title, items, attributes }: DisplayComparison) {
20
- const [bestIdx, setBestIdx] = useState<number | null>(null);
21
-
22
- // Auto-detect best value by lowest price if no manual selection
23
- const lowestPriceIdx = items.reduce<number | null>((acc, item, i) => {
24
- if (!item.price) return acc;
25
- if (acc === null) return i;
26
- const best = items[acc]?.price;
27
- return best && item.price.value < best.value ? i : acc;
28
- }, null);
29
-
30
- const highlightIdx = bestIdx ?? lowestPriceIdx;
31
-
32
- return (
33
- <div className="space-y-2">
34
- {title && <h3 className="text-sm font-semibold text-foreground">{title}</h3>}
35
-
36
- <ScrollArea className="w-full">
37
- <Table>
38
- <TableHeader>
39
- <TableRow>
40
- <TableHead className="font-semibold text-center">Atributo</TableHead>
41
- {items.map((item, i) => (
42
- <TableHead
43
- key={i}
44
- className={cn(
45
- "font-semibold text-center",
46
- i === highlightIdx && "bg-muted"
47
- )}
48
- >
49
- <button
50
- className="flex flex-col items-center gap-0.5 w-full cursor-pointer hover:opacity-80"
51
- onClick={() => setBestIdx(i === bestIdx ? null : i)}
52
- title="Marcar como melhor"
53
- >
54
- {i === highlightIdx && (
55
- <CheckCircle className="h-3.5 w-3.5 text-primary" />
56
- )}
57
- <span className="font-semibold">{item.title}</span>
58
- {item.price && (
59
- <span className="text-xs text-muted-foreground font-normal">
60
- {formatMoney(item.price.value, item.price.currency)}
61
- </span>
62
- )}
63
- </button>
64
- </TableHead>
65
- ))}
66
- </TableRow>
67
- </TableHeader>
68
-
69
- <TableBody>
70
- {attributes && attributes.length > 0 ? (
71
- attributes.map((attr, ri) => (
72
- <TableRow key={ri}>
73
- <TableCell className="font-medium">{attr.label}</TableCell>
74
- {items.map((item, ci) => {
75
- const val = (item as Record<string, unknown>)[attr.key];
76
- return (
77
- <TableCell
78
- key={ci}
79
- className={cn(
80
- "text-center",
81
- ci === highlightIdx && "bg-muted/50"
82
- )}
83
- >
84
- {val === true ? "✓" : val === false ? "✗" : val != null ? String(val) : "—"}
85
- </TableCell>
86
- );
87
- })}
88
- </TableRow>
89
- ))
90
- ) : (
91
- <>
92
- {items.some((i) => i.rating) && (
93
- <TableRow>
94
- <TableCell className="font-medium">Avaliação</TableCell>
95
- {items.map((item, ci) => (
96
- <TableCell
97
- key={ci}
98
- className={cn(
99
- "text-center",
100
- ci === highlightIdx && "bg-muted/50"
101
- )}
102
- >
103
- {item.rating ? `${item.rating.score}/5 (${item.rating.count})` : "—"}
104
- </TableCell>
105
- ))}
106
- </TableRow>
107
- )}
108
- {items.some((i) => i.description) && (
109
- <TableRow>
110
- <TableCell className="font-medium">Descrição</TableCell>
111
- {items.map((item, ci) => (
112
- <TableCell
113
- key={ci}
114
- className={cn(
115
- "text-center",
116
- ci === highlightIdx && "bg-muted/50"
117
- )}
118
- >
119
- {item.description ?? "—"}
120
- </TableCell>
121
- ))}
122
- </TableRow>
123
- )}
124
- </>
125
- )}
126
- </TableBody>
127
- </Table>
128
- <ScrollBar orientation="horizontal" />
129
- </ScrollArea>
130
- </div>
131
- );
132
- }
1
+ import { useState } from "react";
2
+ import type { DisplayComparison } from "./sdk-types.js";
3
+ import { CheckCircle } from "lucide-react";
4
+ import { ScrollArea, ScrollBar } from "../ui/scroll-area.js";
5
+ import {
6
+ Table,
7
+ TableBody,
8
+ TableCell,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ } from "../ui/table.js";
13
+ import { cn } from "../lib/utils.js";
14
+
15
+ function formatMoney(value: number, currency = "BRL") {
16
+ return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
17
+ }
18
+
19
+ export function ComparisonTableRenderer({ title, items, attributes }: DisplayComparison) {
20
+ const [bestIdx, setBestIdx] = useState<number | null>(null);
21
+
22
+ // Auto-detect best value by lowest price if no manual selection
23
+ const lowestPriceIdx = items.reduce<number | null>((acc, item, i) => {
24
+ if (!item.price) return acc;
25
+ if (acc === null) return i;
26
+ const best = items[acc]?.price;
27
+ return best && item.price.value < best.value ? i : acc;
28
+ }, null);
29
+
30
+ const highlightIdx = bestIdx ?? lowestPriceIdx;
31
+
32
+ return (
33
+ <div className="space-y-2">
34
+ {title && <h3 className="text-sm font-semibold text-foreground">{title}</h3>}
35
+
36
+ <ScrollArea className="w-full">
37
+ <Table>
38
+ <TableHeader>
39
+ <TableRow>
40
+ <TableHead className="font-semibold text-center">Atributo</TableHead>
41
+ {items.map((item, i) => (
42
+ <TableHead
43
+ key={i}
44
+ className={cn(
45
+ "font-semibold text-center",
46
+ i === highlightIdx && "bg-muted"
47
+ )}
48
+ >
49
+ <button
50
+ className="flex flex-col items-center gap-0.5 w-full cursor-pointer hover:opacity-80"
51
+ onClick={() => setBestIdx(i === bestIdx ? null : i)}
52
+ title="Marcar como melhor"
53
+ >
54
+ {i === highlightIdx && (
55
+ <CheckCircle className="h-3.5 w-3.5 text-primary" />
56
+ )}
57
+ <span className="font-semibold">{item.title}</span>
58
+ {item.price && (
59
+ <span className="text-xs text-muted-foreground font-normal">
60
+ {formatMoney(item.price.value, item.price.currency)}
61
+ </span>
62
+ )}
63
+ </button>
64
+ </TableHead>
65
+ ))}
66
+ </TableRow>
67
+ </TableHeader>
68
+
69
+ <TableBody>
70
+ {attributes && attributes.length > 0 ? (
71
+ attributes.map((attr, ri) => (
72
+ <TableRow key={ri}>
73
+ <TableCell className="font-medium">{attr.label}</TableCell>
74
+ {items.map((item, ci) => {
75
+ const val = (item as Record<string, unknown>)[attr.key];
76
+ return (
77
+ <TableCell
78
+ key={ci}
79
+ className={cn(
80
+ "text-center",
81
+ ci === highlightIdx && "bg-muted/50"
82
+ )}
83
+ >
84
+ {val === true ? "✓" : val === false ? "✗" : val != null ? String(val) : "—"}
85
+ </TableCell>
86
+ );
87
+ })}
88
+ </TableRow>
89
+ ))
90
+ ) : (
91
+ <>
92
+ {items.some((i) => i.rating) && (
93
+ <TableRow>
94
+ <TableCell className="font-medium">Avaliação</TableCell>
95
+ {items.map((item, ci) => (
96
+ <TableCell
97
+ key={ci}
98
+ className={cn(
99
+ "text-center",
100
+ ci === highlightIdx && "bg-muted/50"
101
+ )}
102
+ >
103
+ {item.rating ? `${item.rating.score}/5 (${item.rating.count})` : "—"}
104
+ </TableCell>
105
+ ))}
106
+ </TableRow>
107
+ )}
108
+ {items.some((i) => i.description) && (
109
+ <TableRow>
110
+ <TableCell className="font-medium">Descrição</TableCell>
111
+ {items.map((item, ci) => (
112
+ <TableCell
113
+ key={ci}
114
+ className={cn(
115
+ "text-center",
116
+ ci === highlightIdx && "bg-muted/50"
117
+ )}
118
+ >
119
+ {item.description ?? "—"}
120
+ </TableCell>
121
+ ))}
122
+ </TableRow>
123
+ )}
124
+ </>
125
+ )}
126
+ </TableBody>
127
+ </Table>
128
+ <ScrollBar orientation="horizontal" />
129
+ </ScrollArea>
130
+ </div>
131
+ );
132
+ }
@@ -1,144 +1,144 @@
1
- import { useState } from "react";
2
- import type { DisplayTable } from "./sdk-types.js";
3
- import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
4
- import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "../ui/table.js";
5
- import { ScrollArea, ScrollBar } from "../ui/scroll-area.js";
6
- import { Button } from "../ui/button.js";
7
- import { Badge } from "../ui/badge.js";
8
-
9
- type SortDir = "asc" | "desc";
10
-
11
- function formatMoney(value: number, currency = "BRL") {
12
- return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
13
- }
14
-
15
- function renderCellValue(value: unknown, type: string): React.ReactNode {
16
- if (value == null) return "—";
17
- switch (type) {
18
- case "money":
19
- return typeof value === "number"
20
- ? formatMoney(value)
21
- : typeof value === "object" && value !== null && "value" in value
22
- ? formatMoney((value as { value: number; currency?: string }).value, (value as { currency?: string }).currency)
23
- : String(value);
24
- case "image":
25
- return typeof value === "string" ? (
26
- <img src={value} alt="" className="rounded-sm max-h-12 object-cover" loading="lazy" decoding="async" />
27
- ) : null;
28
- case "link":
29
- return typeof value === "string" ? (
30
- <a href={value} target="_blank" rel="noopener noreferrer" className="text-primary underline underline-offset-2 hover:opacity-80">
31
- {value}
32
- </a>
33
- ) : null;
34
- case "badge":
35
- return <Badge variant="secondary">{String(value)}</Badge>;
36
- default:
37
- return String(value);
38
- }
39
- }
40
-
41
- function compareValues(a: unknown, b: unknown, type: string): number {
42
- if (a == null && b == null) return 0;
43
- if (a == null) return 1;
44
- if (b == null) return -1;
45
-
46
- if (type === "money") {
47
- const av = typeof a === "number" ? a : typeof a === "object" && a !== null && "value" in a ? (a as { value: number }).value : 0;
48
- const bv = typeof b === "number" ? b : typeof b === "object" && b !== null && "value" in b ? (b as { value: number }).value : 0;
49
- return av - bv;
50
- }
51
- if (type === "number") {
52
- return (Number(a) || 0) - (Number(b) || 0);
53
- }
54
- return String(a).localeCompare(String(b), "pt-BR");
55
- }
56
-
57
- export function DataTableRenderer({ title, columns, rows, sortable }: DisplayTable) {
58
- const [sortKey, setSortKey] = useState<string | null>(null);
59
- const [sortDir, setSortDir] = useState<SortDir>("asc");
60
-
61
- function handleSort(key: string) {
62
- if (!sortable) return;
63
- if (sortKey === key) {
64
- setSortDir((d) => (d === "asc" ? "desc" : "asc"));
65
- } else {
66
- setSortKey(key);
67
- setSortDir("asc");
68
- }
69
- }
70
-
71
- const sortedRows = sortKey
72
- ? [...rows].sort((a, b) => {
73
- const col = columns.find((c) => c.key === sortKey);
74
- const dir = sortDir === "asc" ? 1 : -1;
75
- return compareValues(a[sortKey], b[sortKey], col?.type ?? "text") * dir;
76
- })
77
- : rows;
78
-
79
- return (
80
- <div className="space-y-2">
81
- {title && <h3 className="text-sm font-semibold text-foreground">{title}</h3>}
82
-
83
- <ScrollArea className="w-full">
84
- <Table>
85
- <TableHeader>
86
- <TableRow>
87
- {columns.map((col) => (
88
- <TableHead
89
- key={col.key}
90
- className={col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}
91
- >
92
- {sortable ? (
93
- <Button
94
- variant="ghost"
95
- size="sm"
96
- className="-ml-3 h-8 font-semibold"
97
- onClick={() => handleSort(col.key)}
98
- aria-sort={
99
- sortKey === col.key
100
- ? sortDir === "asc"
101
- ? "ascending"
102
- : "descending"
103
- : undefined
104
- }
105
- >
106
- {col.label}
107
- {sortKey === col.key ? (
108
- sortDir === "asc" ? (
109
- <ArrowUp className="ml-1 h-3 w-3" />
110
- ) : (
111
- <ArrowDown className="ml-1 h-3 w-3" />
112
- )
113
- ) : (
114
- <ArrowUpDown className="ml-1 h-3 w-3" />
115
- )}
116
- </Button>
117
- ) : (
118
- col.label
119
- )}
120
- </TableHead>
121
- ))}
122
- </TableRow>
123
- </TableHeader>
124
-
125
- <TableBody>
126
- {sortedRows.map((row, ri) => (
127
- <TableRow key={ri}>
128
- {columns.map((col) => (
129
- <TableCell
130
- key={col.key}
131
- className={col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}
132
- >
133
- {renderCellValue(row[col.key], col.type)}
134
- </TableCell>
135
- ))}
136
- </TableRow>
137
- ))}
138
- </TableBody>
139
- </Table>
140
- <ScrollBar orientation="horizontal" />
141
- </ScrollArea>
142
- </div>
143
- );
144
- }
1
+ import { useState } from "react";
2
+ import type { DisplayTable } from "./sdk-types.js";
3
+ import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
4
+ import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "../ui/table.js";
5
+ import { ScrollArea, ScrollBar } from "../ui/scroll-area.js";
6
+ import { Button } from "../ui/button.js";
7
+ import { Badge } from "../ui/badge.js";
8
+
9
+ type SortDir = "asc" | "desc";
10
+
11
+ function formatMoney(value: number, currency = "BRL") {
12
+ return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
13
+ }
14
+
15
+ function renderCellValue(value: unknown, type: string): React.ReactNode {
16
+ if (value == null) return "—";
17
+ switch (type) {
18
+ case "money":
19
+ return typeof value === "number"
20
+ ? formatMoney(value)
21
+ : typeof value === "object" && value !== null && "value" in value
22
+ ? formatMoney((value as { value: number; currency?: string }).value, (value as { currency?: string }).currency)
23
+ : String(value);
24
+ case "image":
25
+ return typeof value === "string" ? (
26
+ <img src={value} alt="" className="rounded-sm max-h-12 object-cover" loading="lazy" decoding="async" />
27
+ ) : null;
28
+ case "link":
29
+ return typeof value === "string" ? (
30
+ <a href={value} target="_blank" rel="noopener noreferrer" className="text-primary underline underline-offset-2 hover:opacity-80">
31
+ {value}
32
+ </a>
33
+ ) : null;
34
+ case "badge":
35
+ return <Badge variant="secondary">{String(value)}</Badge>;
36
+ default:
37
+ return String(value);
38
+ }
39
+ }
40
+
41
+ function compareValues(a: unknown, b: unknown, type: string): number {
42
+ if (a == null && b == null) return 0;
43
+ if (a == null) return 1;
44
+ if (b == null) return -1;
45
+
46
+ if (type === "money") {
47
+ const av = typeof a === "number" ? a : typeof a === "object" && a !== null && "value" in a ? (a as { value: number }).value : 0;
48
+ const bv = typeof b === "number" ? b : typeof b === "object" && b !== null && "value" in b ? (b as { value: number }).value : 0;
49
+ return av - bv;
50
+ }
51
+ if (type === "number") {
52
+ return (Number(a) || 0) - (Number(b) || 0);
53
+ }
54
+ return String(a).localeCompare(String(b), "pt-BR");
55
+ }
56
+
57
+ export function DataTableRenderer({ title, columns, rows, sortable }: DisplayTable) {
58
+ const [sortKey, setSortKey] = useState<string | null>(null);
59
+ const [sortDir, setSortDir] = useState<SortDir>("asc");
60
+
61
+ function handleSort(key: string) {
62
+ if (!sortable) return;
63
+ if (sortKey === key) {
64
+ setSortDir((d) => (d === "asc" ? "desc" : "asc"));
65
+ } else {
66
+ setSortKey(key);
67
+ setSortDir("asc");
68
+ }
69
+ }
70
+
71
+ const sortedRows = sortKey
72
+ ? [...rows].sort((a, b) => {
73
+ const col = columns.find((c) => c.key === sortKey);
74
+ const dir = sortDir === "asc" ? 1 : -1;
75
+ return compareValues(a[sortKey], b[sortKey], col?.type ?? "text") * dir;
76
+ })
77
+ : rows;
78
+
79
+ return (
80
+ <div className="space-y-2">
81
+ {title && <h3 className="text-sm font-semibold text-foreground">{title}</h3>}
82
+
83
+ <ScrollArea className="w-full">
84
+ <Table>
85
+ <TableHeader>
86
+ <TableRow>
87
+ {columns.map((col) => (
88
+ <TableHead
89
+ key={col.key}
90
+ className={col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}
91
+ >
92
+ {sortable ? (
93
+ <Button
94
+ variant="ghost"
95
+ size="sm"
96
+ className="-ml-3 h-8 font-semibold"
97
+ onClick={() => handleSort(col.key)}
98
+ aria-sort={
99
+ sortKey === col.key
100
+ ? sortDir === "asc"
101
+ ? "ascending"
102
+ : "descending"
103
+ : undefined
104
+ }
105
+ >
106
+ {col.label}
107
+ {sortKey === col.key ? (
108
+ sortDir === "asc" ? (
109
+ <ArrowUp className="ml-1 h-3 w-3" />
110
+ ) : (
111
+ <ArrowDown className="ml-1 h-3 w-3" />
112
+ )
113
+ ) : (
114
+ <ArrowUpDown className="ml-1 h-3 w-3" />
115
+ )}
116
+ </Button>
117
+ ) : (
118
+ col.label
119
+ )}
120
+ </TableHead>
121
+ ))}
122
+ </TableRow>
123
+ </TableHeader>
124
+
125
+ <TableBody>
126
+ {sortedRows.map((row, ri) => (
127
+ <TableRow key={ri}>
128
+ {columns.map((col) => (
129
+ <TableCell
130
+ key={col.key}
131
+ className={col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}
132
+ >
133
+ {renderCellValue(row[col.key], col.type)}
134
+ </TableCell>
135
+ ))}
136
+ </TableRow>
137
+ ))}
138
+ </TableBody>
139
+ </Table>
140
+ <ScrollBar orientation="horizontal" />
141
+ </ScrollArea>
142
+ </div>
143
+ );
144
+ }