@gugacoder/agentic-chat 0.2.0
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/components/Chat.d.ts +21 -0
- package/dist/components/Chat.js +13 -0
- package/dist/components/ErrorNote.d.ts +5 -0
- package/dist/components/ErrorNote.js +6 -0
- package/dist/components/LazyRender.d.ts +8 -0
- package/dist/components/LazyRender.js +22 -0
- package/dist/components/Markdown.d.ts +5 -0
- package/dist/components/Markdown.js +65 -0
- package/dist/components/MessageBubble.d.ts +10 -0
- package/dist/components/MessageBubble.js +39 -0
- package/dist/components/MessageInput.d.ts +19 -0
- package/dist/components/MessageInput.js +214 -0
- package/dist/components/MessageList.d.ts +12 -0
- package/dist/components/MessageList.js +68 -0
- package/dist/components/StreamingIndicator.d.ts +1 -0
- package/dist/components/StreamingIndicator.js +9 -0
- package/dist/conversations/CollapsibleGroup.d.ts +11 -0
- package/dist/conversations/CollapsibleGroup.js +9 -0
- package/dist/conversations/ConversationBar.d.ts +27 -0
- package/dist/conversations/ConversationBar.js +53 -0
- package/dist/conversations/ConversationList.d.ts +33 -0
- package/dist/conversations/ConversationList.js +48 -0
- package/dist/conversations/ConversationListItem.d.ts +20 -0
- package/dist/conversations/ConversationListItem.js +22 -0
- package/dist/conversations/DeleteDialog.d.ts +13 -0
- package/dist/conversations/DeleteDialog.js +8 -0
- package/dist/conversations/RenameDialog.d.ts +15 -0
- package/dist/conversations/RenameDialog.js +15 -0
- package/dist/conversations/index.d.ts +9 -0
- package/dist/conversations/index.js +5 -0
- package/dist/conversations/types.d.ts +21 -0
- package/dist/conversations/types.js +1 -0
- package/dist/conversations/useConversations.d.ts +19 -0
- package/dist/conversations/useConversations.js +102 -0
- package/dist/conversations/utils.d.ts +8 -0
- package/dist/conversations/utils.js +134 -0
- package/dist/display/AlertRenderer.d.ts +2 -0
- package/dist/display/AlertRenderer.js +13 -0
- package/dist/display/CarouselRenderer.d.ts +2 -0
- package/dist/display/CarouselRenderer.js +41 -0
- package/dist/display/ChartRenderer.d.ts +2 -0
- package/dist/display/ChartRenderer.js +76 -0
- package/dist/display/ChoiceButtonsRenderer.d.ts +6 -0
- package/dist/display/ChoiceButtonsRenderer.js +23 -0
- package/dist/display/CodeBlockRenderer.d.ts +2 -0
- package/dist/display/CodeBlockRenderer.js +17 -0
- package/dist/display/ComparisonTableRenderer.d.ts +2 -0
- package/dist/display/ComparisonTableRenderer.js +26 -0
- package/dist/display/DataTableRenderer.d.ts +2 -0
- package/dist/display/DataTableRenderer.js +74 -0
- package/dist/display/FileCardRenderer.d.ts +2 -0
- package/dist/display/FileCardRenderer.js +31 -0
- package/dist/display/GalleryRenderer.d.ts +2 -0
- package/dist/display/GalleryRenderer.js +11 -0
- package/dist/display/ImageViewerRenderer.d.ts +2 -0
- package/dist/display/ImageViewerRenderer.js +15 -0
- package/dist/display/LinkPreviewRenderer.d.ts +2 -0
- package/dist/display/LinkPreviewRenderer.js +20 -0
- package/dist/display/MapViewRenderer.d.ts +2 -0
- package/dist/display/MapViewRenderer.js +20 -0
- package/dist/display/MetricCardRenderer.d.ts +2 -0
- package/dist/display/MetricCardRenderer.js +12 -0
- package/dist/display/PriceHighlightRenderer.d.ts +2 -0
- package/dist/display/PriceHighlightRenderer.js +13 -0
- package/dist/display/ProductCardRenderer.d.ts +2 -0
- package/dist/display/ProductCardRenderer.js +23 -0
- package/dist/display/ProgressStepsRenderer.d.ts +2 -0
- package/dist/display/ProgressStepsRenderer.js +14 -0
- package/dist/display/SourcesListRenderer.d.ts +2 -0
- package/dist/display/SourcesListRenderer.js +5 -0
- package/dist/display/SpreadsheetRenderer.d.ts +2 -0
- package/dist/display/SpreadsheetRenderer.js +32 -0
- package/dist/display/StepTimelineRenderer.d.ts +2 -0
- package/dist/display/StepTimelineRenderer.js +21 -0
- package/dist/display/index.d.ts +21 -0
- package/dist/display/index.js +20 -0
- package/dist/display/registry.d.ts +5 -0
- package/dist/display/registry.js +50 -0
- package/dist/hooks/ChatProvider.d.ts +10 -0
- package/dist/hooks/ChatProvider.js +14 -0
- package/dist/hooks/useBackboneChat.d.ts +37 -0
- package/dist/hooks/useBackboneChat.js +121 -0
- package/dist/hooks/useIsMobile.d.ts +1 -0
- package/dist/hooks/useIsMobile.js +12 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +40 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/parts/PartRenderer.d.ts +40 -0
- package/dist/parts/PartRenderer.js +97 -0
- package/dist/parts/ReasoningBlock.d.ts +6 -0
- package/dist/parts/ReasoningBlock.js +18 -0
- package/dist/parts/ToolActivity.d.ts +11 -0
- package/dist/parts/ToolActivity.js +52 -0
- package/dist/parts/ToolResult.d.ts +7 -0
- package/dist/parts/ToolResult.js +38 -0
- package/dist/styles.css +2 -0
- package/dist/ui/alert.d.ts +12 -0
- package/dist/ui/alert.js +28 -0
- package/dist/ui/badge.d.ts +9 -0
- package/dist/ui/badge.js +20 -0
- package/dist/ui/button.d.ts +11 -0
- package/dist/ui/button.js +31 -0
- package/dist/ui/card.d.ts +8 -0
- package/dist/ui/card.js +21 -0
- package/dist/ui/collapsible.d.ts +1 -0
- package/dist/ui/collapsible.js +2 -0
- package/dist/ui/dialog.d.ts +19 -0
- package/dist/ui/dialog.js +23 -0
- package/dist/ui/dropdown-menu.d.ts +11 -0
- package/dist/ui/dropdown-menu.js +15 -0
- package/dist/ui/input.d.ts +3 -0
- package/dist/ui/input.js +6 -0
- package/dist/ui/progress.d.ts +7 -0
- package/dist/ui/progress.js +9 -0
- package/dist/ui/scroll-area.d.ts +5 -0
- package/dist/ui/scroll-area.js +12 -0
- package/dist/ui/separator.d.ts +4 -0
- package/dist/ui/separator.js +8 -0
- package/dist/ui/skeleton.d.ts +3 -0
- package/dist/ui/skeleton.js +6 -0
- package/dist/ui/table.d.ts +10 -0
- package/dist/ui/table.js +27 -0
- package/package.json +53 -0
- package/src/components/Chat.tsx +80 -0
- package/src/components/ErrorNote.tsx +32 -0
- package/src/components/LazyRender.tsx +42 -0
- package/src/components/Markdown.tsx +114 -0
- package/src/components/MessageBubble.tsx +102 -0
- package/src/components/MessageInput.tsx +421 -0
- package/src/components/MessageList.tsx +139 -0
- package/src/components/StreamingIndicator.tsx +19 -0
- package/src/conversations/CollapsibleGroup.tsx +41 -0
- package/src/conversations/ConversationBar.tsx +200 -0
- package/src/conversations/ConversationList.tsx +234 -0
- package/src/conversations/ConversationListItem.tsx +123 -0
- package/src/conversations/DeleteDialog.tsx +55 -0
- package/src/conversations/RenameDialog.tsx +74 -0
- package/src/conversations/index.ts +14 -0
- package/src/conversations/types.ts +17 -0
- package/src/conversations/useConversations.ts +148 -0
- package/src/conversations/utils.ts +159 -0
- package/src/display/AlertRenderer.tsx +27 -0
- package/src/display/CarouselRenderer.tsx +141 -0
- package/src/display/ChartRenderer.tsx +195 -0
- package/src/display/ChoiceButtonsRenderer.tsx +114 -0
- package/src/display/CodeBlockRenderer.tsx +49 -0
- package/src/display/ComparisonTableRenderer.tsx +132 -0
- package/src/display/DataTableRenderer.tsx +144 -0
- package/src/display/FileCardRenderer.tsx +55 -0
- package/src/display/GalleryRenderer.tsx +65 -0
- package/src/display/ImageViewerRenderer.tsx +114 -0
- package/src/display/LinkPreviewRenderer.tsx +74 -0
- package/src/display/MapViewRenderer.tsx +75 -0
- package/src/display/MetricCardRenderer.tsx +29 -0
- package/src/display/PriceHighlightRenderer.tsx +44 -0
- package/src/display/ProductCardRenderer.tsx +112 -0
- package/src/display/ProgressStepsRenderer.tsx +59 -0
- package/src/display/SourcesListRenderer.tsx +47 -0
- package/src/display/SpreadsheetRenderer.tsx +86 -0
- package/src/display/StepTimelineRenderer.tsx +75 -0
- package/src/display/index.ts +21 -0
- package/src/display/registry.ts +81 -0
- package/src/hooks/ChatProvider.tsx +22 -0
- package/src/hooks/useBackboneChat.ts +148 -0
- package/src/hooks/useIsMobile.ts +15 -0
- package/src/index.ts +80 -0
- package/src/lib/utils.ts +6 -0
- package/src/parts/PartRenderer.tsx +198 -0
- package/src/parts/ReasoningBlock.tsx +41 -0
- package/src/parts/ToolActivity.tsx +79 -0
- package/src/parts/ToolResult.tsx +79 -0
- package/src/styles.css +2 -0
- package/src/ui/alert.tsx +77 -0
- package/src/ui/badge.tsx +36 -0
- package/src/ui/button.tsx +54 -0
- package/src/ui/card.tsx +68 -0
- package/src/ui/collapsible.tsx +7 -0
- package/src/ui/dialog.tsx +122 -0
- package/src/ui/dropdown-menu.tsx +76 -0
- package/src/ui/input.tsx +24 -0
- package/src/ui/progress.tsx +36 -0
- package/src/ui/scroll-area.tsx +48 -0
- package/src/ui/separator.tsx +31 -0
- package/src/ui/skeleton.tsx +9 -0
- package/src/ui/table.tsx +114 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AreaChart, Area, BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, CartesianGrid, Tooltip, ResponsiveContainer, XAxis, YAxis, } from "recharts";
|
|
3
|
+
import { Card } from "../ui/card";
|
|
4
|
+
const CHART_COLORS = [
|
|
5
|
+
"var(--chart-1)",
|
|
6
|
+
"var(--chart-2)",
|
|
7
|
+
"var(--chart-3)",
|
|
8
|
+
"var(--chart-4)",
|
|
9
|
+
"var(--chart-5)",
|
|
10
|
+
];
|
|
11
|
+
const AXIS_PROPS = {
|
|
12
|
+
stroke: "var(--border)",
|
|
13
|
+
tick: { fill: "var(--muted-foreground)", fontSize: 12 },
|
|
14
|
+
};
|
|
15
|
+
const TOOLTIP_STYLE = {
|
|
16
|
+
contentStyle: {
|
|
17
|
+
background: "var(--card)",
|
|
18
|
+
border: "1px solid var(--border)",
|
|
19
|
+
color: "var(--card-foreground)",
|
|
20
|
+
borderRadius: "0.375rem",
|
|
21
|
+
fontSize: "0.75rem",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
function formatValue(value, format) {
|
|
25
|
+
if (!format)
|
|
26
|
+
return String(value);
|
|
27
|
+
const locale = format.locale ?? "pt-BR";
|
|
28
|
+
const prefix = format.prefix ?? "";
|
|
29
|
+
const suffix = format.suffix ?? "";
|
|
30
|
+
if (prefix.includes("R$") || prefix.includes("$") || prefix.includes("€")) {
|
|
31
|
+
const currencyMap = {
|
|
32
|
+
"R$": "BRL",
|
|
33
|
+
"$": "USD",
|
|
34
|
+
"€": "EUR",
|
|
35
|
+
};
|
|
36
|
+
const currency = Object.keys(currencyMap).find(k => prefix.includes(k));
|
|
37
|
+
if (currency) {
|
|
38
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency: currencyMap[currency] }).format(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (suffix.includes("%")) {
|
|
42
|
+
return new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 }).format(value / 100);
|
|
43
|
+
}
|
|
44
|
+
const formatted = new Intl.NumberFormat(locale).format(value);
|
|
45
|
+
return `${prefix}${formatted}${suffix}`;
|
|
46
|
+
}
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
function makeTooltipFormatter(format) {
|
|
49
|
+
return (value) => [
|
|
50
|
+
typeof value === "number" ? formatValue(value, format) : String(value ?? ""),
|
|
51
|
+
"",
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
export function ChartRenderer({ type, title, data, format }) {
|
|
55
|
+
const chartData = data.map((d) => ({ name: d.label, value: d.value, color: d.color }));
|
|
56
|
+
const tooltipFormatter = makeTooltipFormatter(format);
|
|
57
|
+
const sharedProps = {
|
|
58
|
+
data: chartData,
|
|
59
|
+
margin: { top: 4, right: 8, left: 8, bottom: 4 },
|
|
60
|
+
};
|
|
61
|
+
return (_jsxs(Card, { className: "p-4", children: [title && _jsx("p", { className: "text-sm font-medium text-foreground mb-4", children: title }), _jsx("div", { className: "min-h-[200px]", children: _jsx(ResponsiveContainer, { width: "100%", height: 200, children: renderChart(type, chartData, sharedProps, tooltipFormatter) }) })] }));
|
|
62
|
+
}
|
|
63
|
+
function renderChart(type, data, sharedProps, tooltipFormatter) {
|
|
64
|
+
switch (type) {
|
|
65
|
+
case "bar":
|
|
66
|
+
return (_jsxs(BarChart, { ...sharedProps, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--border)" }), _jsx(XAxis, { dataKey: "name", ...AXIS_PROPS }), _jsx(YAxis, { width: 48, ...AXIS_PROPS }), _jsx(Tooltip, { formatter: tooltipFormatter, ...TOOLTIP_STYLE }), _jsx(Bar, { dataKey: "value", radius: [4, 4, 0, 0], children: data.map((entry, index) => (_jsx(Cell, { fill: entry.color ?? CHART_COLORS[index % CHART_COLORS.length] }, index))) })] }));
|
|
67
|
+
case "line":
|
|
68
|
+
return (_jsxs(LineChart, { ...sharedProps, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--border)" }), _jsx(XAxis, { dataKey: "name", ...AXIS_PROPS }), _jsx(YAxis, { width: 48, ...AXIS_PROPS }), _jsx(Tooltip, { formatter: tooltipFormatter, ...TOOLTIP_STYLE }), _jsx(Line, { type: "monotone", dataKey: "value", stroke: CHART_COLORS[0], strokeWidth: 2, dot: { r: 4, fill: CHART_COLORS[0] } })] }));
|
|
69
|
+
case "area":
|
|
70
|
+
return (_jsxs(AreaChart, { ...sharedProps, children: [_jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "var(--border)" }), _jsx(XAxis, { dataKey: "name", ...AXIS_PROPS }), _jsx(YAxis, { width: 48, ...AXIS_PROPS }), _jsx(Tooltip, { formatter: tooltipFormatter, ...TOOLTIP_STYLE }), _jsx(Area, { type: "monotone", dataKey: "value", stroke: CHART_COLORS[0], fill: CHART_COLORS[0], fillOpacity: 0.2, strokeWidth: 2 })] }));
|
|
71
|
+
case "pie":
|
|
72
|
+
return (_jsxs(PieChart, { children: [_jsx(Tooltip, { formatter: tooltipFormatter, ...TOOLTIP_STYLE }), _jsx(Pie, { data: data, dataKey: "value", nameKey: "name", cx: "50%", cy: "50%", outerRadius: "70%", label: ({ name }) => name, labelLine: { stroke: "var(--muted-foreground)" }, children: data.map((entry, index) => (_jsx(Cell, { fill: entry.color ?? CHART_COLORS[index % CHART_COLORS.length] }, index))) })] }));
|
|
73
|
+
case "donut":
|
|
74
|
+
return (_jsxs(PieChart, { children: [_jsx(Tooltip, { formatter: tooltipFormatter, ...TOOLTIP_STYLE }), _jsx(Pie, { data: data, dataKey: "value", nameKey: "name", cx: "50%", cy: "50%", innerRadius: "40%", outerRadius: "70%", label: ({ name }) => name, labelLine: { stroke: "var(--muted-foreground)" }, children: data.map((entry, index) => (_jsx(Cell, { fill: entry.color ?? CHART_COLORS[index % CHART_COLORS.length] }, index))) })] }));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DisplayChoices } from "@gugacoder/agentic-sdk";
|
|
2
|
+
type ChoiceButtonsProps = DisplayChoices & {
|
|
3
|
+
onChoiceSelect?: (value: string) => void;
|
|
4
|
+
};
|
|
5
|
+
export declare function ChoiceButtonsRenderer({ question, choices, layout, onChoiceSelect, }: ChoiceButtonsProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Button } from "../ui/button.js";
|
|
4
|
+
import { Card } from "../ui/card.js";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
export function ChoiceButtonsRenderer({ question, choices, layout, onChoiceSelect, }) {
|
|
7
|
+
const [selected, setSelected] = useState(null);
|
|
8
|
+
function handleSelect(id) {
|
|
9
|
+
setSelected(id);
|
|
10
|
+
onChoiceSelect?.(id);
|
|
11
|
+
}
|
|
12
|
+
return (_jsxs("div", { className: "space-y-3 w-fit", children: [question && _jsx("p", { className: "text-sm font-medium text-foreground", children: question }), _jsx("div", { className: cn(layout === "buttons" && "flex flex-wrap gap-2", layout === "cards" && "grid grid-cols-1 sm:grid-cols-2 gap-2", layout === "list" && "flex flex-col gap-1"), role: "group", "aria-label": question ?? "Escolha uma opção", children: choices.map((choice) => {
|
|
13
|
+
const isSelected = selected === choice.id;
|
|
14
|
+
if (layout === "buttons") {
|
|
15
|
+
return (_jsxs(Button, { variant: "outline", size: "sm", onClick: () => handleSelect(choice.id), "aria-pressed": isSelected, className: cn(isSelected && "ring-2 ring-ring"), children: [choice.icon && (_jsx("span", { "aria-hidden": "true", children: choice.icon })), choice.label] }, choice.id));
|
|
16
|
+
}
|
|
17
|
+
if (layout === "cards") {
|
|
18
|
+
return (_jsxs(Card, { className: cn("p-3 cursor-pointer hover:bg-muted/50 transition-colors", isSelected && "ring-2 ring-ring"), onClick: () => handleSelect(choice.id), role: "button", "aria-pressed": isSelected, children: [choice.icon && (_jsx("span", { className: "text-base", "aria-hidden": "true", children: choice.icon })), _jsx("span", { className: "block font-medium text-sm text-foreground", children: choice.label }), choice.description && (_jsx("span", { className: "block text-xs text-muted-foreground mt-1", children: choice.description }))] }, choice.id));
|
|
19
|
+
}
|
|
20
|
+
// list
|
|
21
|
+
return (_jsxs("button", { className: cn("flex items-center gap-3 w-full rounded-md px-3 py-2 text-sm text-left hover:bg-muted transition-colors", isSelected && "bg-muted ring-1 ring-ring"), onClick: () => handleSelect(choice.id), "aria-pressed": isSelected, children: [_jsx("span", { className: cn("w-4 h-4 rounded-full border-2 border-input flex-shrink-0", isSelected && "border-primary bg-primary"), "aria-hidden": "true" }), _jsxs("span", { className: "flex items-center gap-2 flex-1 min-w-0", children: [choice.icon && (_jsx("span", { "aria-hidden": "true", children: choice.icon })), _jsx("span", { className: "font-medium text-foreground", children: choice.label }), choice.description && (_jsx("span", { className: "text-xs text-muted-foreground ml-auto", children: choice.description }))] })] }, choice.id));
|
|
22
|
+
}) })] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
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
|
+
export function CodeBlockRenderer({ language, code, title, lineNumbers }) {
|
|
7
|
+
const [copied, setCopied] = useState(false);
|
|
8
|
+
async function handleCopy() {
|
|
9
|
+
await navigator.clipboard.writeText(code);
|
|
10
|
+
setCopied(true);
|
|
11
|
+
setTimeout(() => setCopied(false), 2000);
|
|
12
|
+
}
|
|
13
|
+
const displayLines = lineNumbers
|
|
14
|
+
? code.split("\n").map((line, i) => (_jsxs("span", { className: "flex gap-4", children: [_jsx("span", { className: "select-none text-muted-foreground w-6 text-right shrink-0", children: i + 1 }), _jsx("span", { children: line })] }, i)))
|
|
15
|
+
: code;
|
|
16
|
+
return (_jsxs("div", { className: cn("rounded-md border border-border bg-muted/30 overflow-hidden"), children: [_jsxs("div", { className: "flex items-center justify-between px-3 py-1.5 border-b border-border", children: [_jsx("span", { className: "text-xs text-muted-foreground font-mono", children: title ?? language }), _jsxs(Button, { variant: "ghost", size: "sm", onClick: handleCopy, "aria-label": copied ? "Copiado!" : "Copiar código", className: "h-7 gap-1.5", children: [copied ? _jsx(Check, { className: "h-3 w-3" }) : _jsx(Copy, { className: "h-3 w-3" }), _jsx("span", { className: "text-xs", children: copied ? "Copiado!" : "Copiar" })] })] }), _jsx("pre", { className: "p-4 overflow-x-auto font-mono text-sm", children: lineNumbers ? (_jsx("code", { children: displayLines })) : (_jsx("code", { children: code })) })] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { CheckCircle } from "lucide-react";
|
|
4
|
+
import { ScrollArea, ScrollBar } from "../ui/scroll-area.js";
|
|
5
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../ui/table.js";
|
|
6
|
+
import { cn } from "../lib/utils.js";
|
|
7
|
+
function formatMoney(value, currency = "BRL") {
|
|
8
|
+
return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
|
|
9
|
+
}
|
|
10
|
+
export function ComparisonTableRenderer({ title, items, attributes }) {
|
|
11
|
+
const [bestIdx, setBestIdx] = useState(null);
|
|
12
|
+
// Auto-detect best value by lowest price if no manual selection
|
|
13
|
+
const lowestPriceIdx = items.reduce((acc, item, i) => {
|
|
14
|
+
if (!item.price)
|
|
15
|
+
return acc;
|
|
16
|
+
if (acc === null)
|
|
17
|
+
return i;
|
|
18
|
+
const best = items[acc]?.price;
|
|
19
|
+
return best && item.price.value < best.value ? i : acc;
|
|
20
|
+
}, null);
|
|
21
|
+
const highlightIdx = bestIdx ?? lowestPriceIdx;
|
|
22
|
+
return (_jsxs("div", { className: "space-y-2", children: [title && _jsx("h3", { className: "text-sm font-semibold text-foreground", children: title }), _jsxs(ScrollArea, { className: "w-full", children: [_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "font-semibold text-center", children: "Atributo" }), items.map((item, i) => (_jsx(TableHead, { className: cn("font-semibold text-center", i === highlightIdx && "bg-muted"), children: _jsxs("button", { className: "flex flex-col items-center gap-0.5 w-full cursor-pointer hover:opacity-80", onClick: () => setBestIdx(i === bestIdx ? null : i), title: "Marcar como melhor", children: [i === highlightIdx && (_jsx(CheckCircle, { className: "h-3.5 w-3.5 text-primary" })), _jsx("span", { className: "font-semibold", children: item.title }), item.price && (_jsx("span", { className: "text-xs text-muted-foreground font-normal", children: formatMoney(item.price.value, item.price.currency) }))] }) }, i)))] }) }), _jsx(TableBody, { children: attributes && attributes.length > 0 ? (attributes.map((attr, ri) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: attr.label }), items.map((item, ci) => {
|
|
23
|
+
const val = item[attr.key];
|
|
24
|
+
return (_jsx(TableCell, { className: cn("text-center", ci === highlightIdx && "bg-muted/50"), children: val === true ? "✓" : val === false ? "✗" : val != null ? String(val) : "—" }, ci));
|
|
25
|
+
})] }, ri)))) : (_jsxs(_Fragment, { children: [items.some((i) => i.rating) && (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: "Avalia\u00E7\u00E3o" }), items.map((item, ci) => (_jsx(TableCell, { className: cn("text-center", ci === highlightIdx && "bg-muted/50"), children: item.rating ? `${item.rating.score}/5 (${item.rating.count})` : "—" }, ci)))] })), items.some((i) => i.description) && (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: "Descri\u00E7\u00E3o" }), items.map((item, ci) => (_jsx(TableCell, { className: cn("text-center", ci === highlightIdx && "bg-muted/50"), children: item.description ?? "—" }, ci)))] }))] })) })] }), _jsx(ScrollBar, { orientation: "horizontal" })] })] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
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
|
+
function formatMoney(value, currency = "BRL") {
|
|
9
|
+
return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
|
|
10
|
+
}
|
|
11
|
+
function renderCellValue(value, type) {
|
|
12
|
+
if (value == null)
|
|
13
|
+
return "—";
|
|
14
|
+
switch (type) {
|
|
15
|
+
case "money":
|
|
16
|
+
return typeof value === "number"
|
|
17
|
+
? formatMoney(value)
|
|
18
|
+
: typeof value === "object" && value !== null && "value" in value
|
|
19
|
+
? formatMoney(value.value, value.currency)
|
|
20
|
+
: String(value);
|
|
21
|
+
case "image":
|
|
22
|
+
return typeof value === "string" ? (_jsx("img", { src: value, alt: "", className: "rounded-sm max-h-12 object-cover", loading: "lazy", decoding: "async" })) : null;
|
|
23
|
+
case "link":
|
|
24
|
+
return typeof value === "string" ? (_jsx("a", { href: value, target: "_blank", rel: "noopener noreferrer", className: "text-primary underline underline-offset-2 hover:opacity-80", children: value })) : null;
|
|
25
|
+
case "badge":
|
|
26
|
+
return _jsx(Badge, { variant: "secondary", children: String(value) });
|
|
27
|
+
default:
|
|
28
|
+
return String(value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function compareValues(a, b, type) {
|
|
32
|
+
if (a == null && b == null)
|
|
33
|
+
return 0;
|
|
34
|
+
if (a == null)
|
|
35
|
+
return 1;
|
|
36
|
+
if (b == null)
|
|
37
|
+
return -1;
|
|
38
|
+
if (type === "money") {
|
|
39
|
+
const av = typeof a === "number" ? a : typeof a === "object" && a !== null && "value" in a ? a.value : 0;
|
|
40
|
+
const bv = typeof b === "number" ? b : typeof b === "object" && b !== null && "value" in b ? b.value : 0;
|
|
41
|
+
return av - bv;
|
|
42
|
+
}
|
|
43
|
+
if (type === "number") {
|
|
44
|
+
return (Number(a) || 0) - (Number(b) || 0);
|
|
45
|
+
}
|
|
46
|
+
return String(a).localeCompare(String(b), "pt-BR");
|
|
47
|
+
}
|
|
48
|
+
export function DataTableRenderer({ title, columns, rows, sortable }) {
|
|
49
|
+
const [sortKey, setSortKey] = useState(null);
|
|
50
|
+
const [sortDir, setSortDir] = useState("asc");
|
|
51
|
+
function handleSort(key) {
|
|
52
|
+
if (!sortable)
|
|
53
|
+
return;
|
|
54
|
+
if (sortKey === key) {
|
|
55
|
+
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
setSortKey(key);
|
|
59
|
+
setSortDir("asc");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const sortedRows = sortKey
|
|
63
|
+
? [...rows].sort((a, b) => {
|
|
64
|
+
const col = columns.find((c) => c.key === sortKey);
|
|
65
|
+
const dir = sortDir === "asc" ? 1 : -1;
|
|
66
|
+
return compareValues(a[sortKey], b[sortKey], col?.type ?? "text") * dir;
|
|
67
|
+
})
|
|
68
|
+
: rows;
|
|
69
|
+
return (_jsxs("div", { className: "space-y-2", children: [title && _jsx("h3", { className: "text-sm font-semibold text-foreground", children: title }), _jsxs(ScrollArea, { className: "w-full", children: [_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: columns.map((col) => (_jsx(TableHead, { className: col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left", children: sortable ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "-ml-3 h-8 font-semibold", onClick: () => handleSort(col.key), "aria-sort": sortKey === col.key
|
|
70
|
+
? sortDir === "asc"
|
|
71
|
+
? "ascending"
|
|
72
|
+
: "descending"
|
|
73
|
+
: undefined, children: [col.label, sortKey === col.key ? (sortDir === "asc" ? (_jsx(ArrowUp, { className: "ml-1 h-3 w-3" })) : (_jsx(ArrowDown, { className: "ml-1 h-3 w-3" }))) : (_jsx(ArrowUpDown, { className: "ml-1 h-3 w-3" }))] })) : (col.label) }, col.key))) }) }), _jsx(TableBody, { children: sortedRows.map((row, ri) => (_jsx(TableRow, { children: columns.map((col) => (_jsx(TableCell, { className: col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left", children: renderCellValue(row[col.key], col.type) }, col.key))) }, ri))) })] }), _jsx(ScrollBar, { orientation: "horizontal" })] })] }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Download, File, FileCode, FileImage, FileMinus, FileText, FileVideo, Music, } from "lucide-react";
|
|
3
|
+
import { Button } from "../ui/button.js";
|
|
4
|
+
import { Card } from "../ui/card.js";
|
|
5
|
+
function getFileIcon(type) {
|
|
6
|
+
const mime = type.toLowerCase();
|
|
7
|
+
if (mime.startsWith("image/"))
|
|
8
|
+
return FileImage;
|
|
9
|
+
if (mime.startsWith("video/"))
|
|
10
|
+
return FileVideo;
|
|
11
|
+
if (mime.startsWith("audio/"))
|
|
12
|
+
return Music;
|
|
13
|
+
if (mime === "application/pdf" || mime === "text/plain" || mime.includes("document"))
|
|
14
|
+
return FileText;
|
|
15
|
+
if (mime.includes("spreadsheet") || mime.includes("csv"))
|
|
16
|
+
return FileMinus;
|
|
17
|
+
if (mime.includes("javascript") || mime.includes("typescript") || mime.includes("json") || mime.includes("html") || mime.includes("css") || mime.includes("xml"))
|
|
18
|
+
return FileCode;
|
|
19
|
+
return File;
|
|
20
|
+
}
|
|
21
|
+
function formatSize(bytes) {
|
|
22
|
+
if (bytes < 1024)
|
|
23
|
+
return `${bytes} B`;
|
|
24
|
+
if (bytes < 1024 * 1024)
|
|
25
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
26
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
27
|
+
}
|
|
28
|
+
export function FileCardRenderer({ name, type, size, url }) {
|
|
29
|
+
const Icon = getFileIcon(type);
|
|
30
|
+
return (_jsxs(Card, { className: "flex items-center gap-3 p-3", children: [_jsx("div", { className: "shrink-0 text-primary", children: _jsx(Icon, { className: "h-8 w-8" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "font-medium text-sm truncate", children: name }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [type, size !== undefined && ` · ${formatSize(size)}`] })] }), url && (_jsx(Button, { variant: "ghost", size: "icon", asChild: true, children: _jsx("a", { href: url, download: name, "aria-label": `Baixar ${name}`, children: _jsx(Download, { className: "h-4 w-4" }) }) }))] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { ZoomIn } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog.js";
|
|
6
|
+
export function GalleryRenderer({ title, images }) {
|
|
7
|
+
const [lightboxIdx, setLightboxIdx] = useState(null);
|
|
8
|
+
const activeImage = lightboxIdx !== null ? images[lightboxIdx] : null;
|
|
9
|
+
return (_jsxs("div", { className: "space-y-2", children: [title && _jsx("h3", { className: "text-sm font-medium text-foreground", children: title }), _jsx("div", { className: "grid grid-cols-2 md:grid-cols-3 gap-2", children: images.map((img, i) => (_jsxs("button", { className: "relative group cursor-pointer rounded-md overflow-hidden", onClick: () => setLightboxIdx(i), "aria-label": img.alt ?? `Imagem ${i + 1}`, children: [_jsx("img", { src: img.url, alt: img.alt ?? "", className: "w-full h-full object-cover aspect-square", loading: "lazy" }), _jsx("div", { className: "absolute inset-0 bg-background/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity", children: _jsx(ZoomIn, { className: "h-5 w-5 text-foreground" }) }), img.caption && (_jsx("p", { className: "absolute bottom-0 left-0 right-0 text-sm text-muted-foreground bg-background/80 px-2 py-1 truncate", children: img.caption }))] }, i))) }), _jsx(Dialog, { open: lightboxIdx !== null, onOpenChange: (open) => { if (!open)
|
|
10
|
+
setLightboxIdx(null); }, children: _jsxs(DialogContent, { className: cn("max-w-4xl p-0 bg-background/95"), children: [_jsx(DialogTitle, { className: "sr-only", children: activeImage?.alt ?? "Visualizador de imagem" }), activeImage && (_jsxs("div", { className: "flex flex-col", children: [_jsx("img", { src: activeImage.url, alt: activeImage.alt ?? "", className: "w-full max-h-[80vh] object-contain" }), activeImage.caption && (_jsx("p", { className: "text-sm text-muted-foreground px-4 py-3", children: activeImage.caption }))] }))] }) })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { ZoomIn, ZoomOut, RotateCcw, X } from "lucide-react";
|
|
4
|
+
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog.js";
|
|
5
|
+
import { Button } from "../ui/button.js";
|
|
6
|
+
import { cn } from "../lib/utils.js";
|
|
7
|
+
export function ImageViewerRenderer({ url, alt, caption, width, height }) {
|
|
8
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
9
|
+
const [zoom, setZoom] = useState(1);
|
|
10
|
+
function handleOpen() {
|
|
11
|
+
setZoom(1);
|
|
12
|
+
setDialogOpen(true);
|
|
13
|
+
}
|
|
14
|
+
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs("button", { className: "relative group cursor-pointer rounded-md overflow-hidden inline-block", onClick: handleOpen, "aria-label": `Ampliar imagem${alt ? `: ${alt}` : ""}`, children: [_jsx("img", { src: url, alt: alt ?? "", className: "block max-w-full rounded-md", loading: "lazy", decoding: "async", width: width, height: height }), _jsx("div", { className: "absolute inset-0 bg-background/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity", children: _jsx(ZoomIn, { className: "h-5 w-5 text-foreground" }) })] }), caption && (_jsx("p", { className: "text-xs text-muted-foreground", children: caption })), _jsx(Dialog, { open: dialogOpen, onOpenChange: setDialogOpen, children: _jsxs(DialogContent, { className: "max-w-4xl p-0 bg-background/95 overflow-hidden", children: [_jsx(DialogTitle, { className: "sr-only", children: alt ?? "Visualizador de imagem" }), _jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex items-center gap-1 p-2 border-b border-border", children: [_jsx(Button, { variant: "ghost", size: "icon", onClick: () => setZoom((z) => Math.max(0.25, z - 0.25)), "aria-label": "Reduzir zoom", disabled: zoom <= 0.25, children: _jsx(ZoomOut, { className: "h-4 w-4" }) }), _jsxs("span", { className: cn("text-xs text-muted-foreground w-12 text-center tabular-nums"), children: [Math.round(zoom * 100), "%"] }), _jsx(Button, { variant: "ghost", size: "icon", onClick: () => setZoom((z) => Math.min(4, z + 0.25)), "aria-label": "Aumentar zoom", disabled: zoom >= 4, children: _jsx(ZoomIn, { className: "h-4 w-4" }) }), _jsx(Button, { variant: "ghost", size: "icon", onClick: () => setZoom(1), "aria-label": "Resetar zoom", children: _jsx(RotateCcw, { className: "h-4 w-4" }) }), _jsx("div", { className: "flex-1" }), _jsx(Button, { variant: "ghost", size: "icon", onClick: () => setDialogOpen(false), "aria-label": "Fechar", children: _jsx(X, { className: "h-4 w-4" }) })] }), _jsx("div", { className: "overflow-auto max-h-[80vh] flex items-center justify-center p-4", children: _jsx("img", { src: url, alt: alt ?? "", style: { transform: `scale(${zoom})`, transformOrigin: "center" }, className: "max-w-full transition-transform", width: width, height: height }) }), caption && (_jsx("p", { className: "text-xs text-muted-foreground text-center p-2 border-t border-border", children: caption }))] })] }) })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Globe } from "lucide-react";
|
|
4
|
+
import { Card } from "../ui/card";
|
|
5
|
+
function getDomain(url, domainProp) {
|
|
6
|
+
if (domainProp)
|
|
7
|
+
return domainProp;
|
|
8
|
+
try {
|
|
9
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return url;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function LinkPreviewRenderer({ url, title, description, image, favicon, domain, }) {
|
|
16
|
+
const [imgError, setImgError] = useState(false);
|
|
17
|
+
const [faviconError, setFaviconError] = useState(false);
|
|
18
|
+
const displayDomain = getDomain(url, domain);
|
|
19
|
+
return (_jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", "aria-label": `Link: ${title}`, children: _jsxs(Card, { className: "overflow-hidden hover:bg-muted/50 transition-colors", children: [image && !imgError && (_jsx("div", { className: "aspect-video overflow-hidden", children: _jsx("img", { src: image, alt: title, className: "w-full h-full object-cover", loading: "lazy", decoding: "async", onError: () => setImgError(true) }) })), _jsxs("div", { className: "p-3 space-y-1", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-xs text-muted-foreground", children: [favicon && !faviconError ? (_jsx("img", { src: favicon, alt: "", className: "w-3 h-3 rounded-sm", onError: () => setFaviconError(true), "aria-hidden": "true" })) : (_jsx(Globe, { size: 12, "aria-hidden": "true", className: "shrink-0" })), _jsx("span", { children: displayDomain })] }), _jsx("p", { className: "font-medium text-foreground text-sm", children: title }), description && (_jsx("p", { className: "text-xs text-muted-foreground line-clamp-2", children: description }))] })] }) }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { MapPin } from "lucide-react";
|
|
3
|
+
import { Card } from "../ui/card.js";
|
|
4
|
+
import { Separator } from "../ui/separator.js";
|
|
5
|
+
function buildOsmUrl(pins, zoom) {
|
|
6
|
+
if (pins.length === 0) {
|
|
7
|
+
return `https://www.openstreetmap.org/export/embed.html?bbox=-180,-90,180,90&layer=mapnik`;
|
|
8
|
+
}
|
|
9
|
+
const lat = pins.reduce((acc, p) => acc + p.lat, 0) / pins.length;
|
|
10
|
+
const lng = pins.reduce((acc, p) => acc + p.lng, 0) / pins.length;
|
|
11
|
+
const firstPin = pins[0];
|
|
12
|
+
const markerParam = pins.length === 1
|
|
13
|
+
? `&mlat=${firstPin.lat}&mlon=${firstPin.lng}`
|
|
14
|
+
: "";
|
|
15
|
+
return `https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.05},${lat - 0.05},${lng + 0.05},${lat + 0.05}&layer=mapnik&zoom=${zoom}${markerParam}`;
|
|
16
|
+
}
|
|
17
|
+
export function MapViewRenderer({ title, pins, zoom }) {
|
|
18
|
+
const osmUrl = buildOsmUrl(pins, zoom);
|
|
19
|
+
return (_jsxs(Card, { className: "overflow-hidden", children: [title && (_jsx("div", { className: "px-4 py-3", children: _jsx("h3", { className: "font-medium text-sm text-foreground", children: title }) })), _jsxs("div", { className: "relative aspect-video bg-muted text-muted-foreground overflow-hidden", children: [_jsx("iframe", { src: osmUrl, className: "w-full h-full border-0", title: title ?? "Mapa OpenStreetMap", loading: "lazy", referrerPolicy: "no-referrer", sandbox: "allow-scripts allow-same-origin" }), _jsx("div", { className: "absolute inset-0 pointer-events-none flex items-center justify-center opacity-10", children: _jsx(MapPin, { className: "h-10 w-10" }) })] }), pins.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), _jsx("ul", { className: "p-3 space-y-2", "aria-label": "Locais no mapa", children: pins.map((pin, i) => (_jsxs("li", { className: "flex items-start gap-2", children: [_jsx(MapPin, { className: "h-4 w-4 shrink-0 text-primary mt-0.5", "aria-hidden": "true" }), _jsxs("span", { className: "flex flex-col min-w-0", children: [pin.label && (_jsx("span", { className: "font-medium text-sm text-foreground", children: pin.label })), pin.address && (_jsx("span", { className: "text-xs text-muted-foreground", children: pin.address })), !pin.label && !pin.address && (_jsxs("span", { className: "text-xs text-muted-foreground font-mono", children: [pin.lat.toFixed(4), ", ", pin.lng.toFixed(4)] }))] })] }, i))) })] }))] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ArrowDown, ArrowRight, ArrowUp } from "lucide-react";
|
|
3
|
+
import { Card } from "../ui/card.js";
|
|
4
|
+
const TREND_CONFIG = {
|
|
5
|
+
up: { Icon: ArrowUp, colorClass: "text-primary" },
|
|
6
|
+
down: { Icon: ArrowDown, colorClass: "text-destructive" },
|
|
7
|
+
neutral: { Icon: ArrowRight, colorClass: "text-muted-foreground" },
|
|
8
|
+
};
|
|
9
|
+
export function MetricCardRenderer({ label, value, unit, trend }) {
|
|
10
|
+
const trendConfig = trend ? TREND_CONFIG[trend.direction] : null;
|
|
11
|
+
return (_jsxs(Card, { className: "p-4 w-fit", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: label }), _jsxs("div", { className: "flex items-baseline gap-2 mt-1", children: [_jsx("span", { className: "text-2xl font-bold text-foreground", children: value }), unit && _jsx("span", { className: "text-sm text-muted-foreground", children: unit })] }), trend && trendConfig && (_jsxs("div", { className: `flex items-center gap-1 mt-1 text-sm ${trendConfig.colorClass}`, children: [_jsx(trendConfig.Icon, { size: 14, "aria-hidden": "true" }), _jsx("span", { children: trend.value })] }))] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ExternalLink } from "lucide-react";
|
|
3
|
+
import { Badge } from "../ui/badge";
|
|
4
|
+
import { Card } from "../ui/card";
|
|
5
|
+
function formatPrice(value, currency) {
|
|
6
|
+
return new Intl.NumberFormat("pt-BR", {
|
|
7
|
+
style: "currency",
|
|
8
|
+
currency,
|
|
9
|
+
}).format(value);
|
|
10
|
+
}
|
|
11
|
+
export function PriceHighlightRenderer({ value, label, context, source, badge }) {
|
|
12
|
+
return (_jsxs(Card, { className: "p-4 space-y-1 w-fit", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: label }), _jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "text-2xl font-bold text-foreground", children: formatPrice(value.value, value.currency) }), badge && (_jsx(Badge, { variant: "destructive", children: badge.label }))] }), context && _jsx("p", { className: "text-sm text-muted-foreground", children: context }), source && (_jsxs("a", { href: source.url, target: "_blank", rel: "noopener noreferrer", className: "flex items-center gap-1.5 text-xs text-primary hover:underline", children: [source.favicon && (_jsx("img", { src: source.favicon, alt: "", width: 14, height: 14, "aria-hidden": "true" })), _jsx("span", { children: source.name }), _jsx(ExternalLink, { size: 12, "aria-hidden": "true" })] }))] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Star } from "lucide-react";
|
|
4
|
+
import { Card, CardContent, CardTitle } from "../ui/card.js";
|
|
5
|
+
import { Badge } from "../ui/badge.js";
|
|
6
|
+
import { Button } from "../ui/button.js";
|
|
7
|
+
import { cn } from "../lib/utils.js";
|
|
8
|
+
function StarRating({ score, count }) {
|
|
9
|
+
const fullStars = Math.floor(score);
|
|
10
|
+
const hasHalf = score - fullStars >= 0.5;
|
|
11
|
+
const emptyStars = 5 - fullStars - (hasHalf ? 1 : 0);
|
|
12
|
+
return (_jsxs("div", { className: "flex items-center gap-0.5 text-primary", "aria-label": `${score} de 5 estrelas (${count} avaliações)`, children: [Array.from({ length: fullStars }).map((_, i) => (_jsx(Star, { size: 14, fill: "currentColor" }, `f${i}`))), hasHalf && _jsx(Star, { size: 14, fill: "none" }), Array.from({ length: emptyStars }).map((_, i) => (_jsx(Star, { size: 14, fill: "none", className: "text-muted-foreground" }, `e${i}`))), _jsxs("span", { className: "text-xs text-muted-foreground ml-1", children: ["(", count, ")"] })] }));
|
|
13
|
+
}
|
|
14
|
+
function formatMoney(value, currency = "BRL") {
|
|
15
|
+
return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
|
|
16
|
+
}
|
|
17
|
+
export function ProductCardRenderer({ title, image, price, originalPrice, rating, badges, url, description, }) {
|
|
18
|
+
const [imgError, setImgError] = useState(false);
|
|
19
|
+
const discount = price && originalPrice && originalPrice.value > price.value
|
|
20
|
+
? Math.round(((originalPrice.value - price.value) / originalPrice.value) * 100)
|
|
21
|
+
: null;
|
|
22
|
+
return (_jsxs(Card, { className: "overflow-hidden w-fit max-w-sm", children: [image && !imgError && (_jsxs("div", { className: "relative", children: [_jsx("img", { src: image, alt: title, className: cn("w-full aspect-video object-cover"), loading: "lazy", decoding: "async", onError: () => setImgError(true) }), discount !== null && (_jsxs(Badge, { variant: "destructive", className: "absolute top-2 right-2", children: ["-", discount, "%"] }))] })), _jsxs(CardContent, { className: "p-4 space-y-2", children: [badges && badges.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1", children: badges.map((b, i) => (_jsx(Badge, { variant: "secondary", children: b.label }, i))) })), _jsx(CardTitle, { className: "text-sm", children: title }), description && (_jsx("p", { className: "text-xs text-muted-foreground line-clamp-2", children: description })), rating && _jsx(StarRating, { score: rating.score, count: rating.count }), price && (_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "text-lg font-bold", children: formatMoney(price.value, price.currency) }), originalPrice && (_jsx("span", { className: "text-sm text-muted-foreground line-through", children: formatMoney(originalPrice.value, originalPrice.currency) }))] })), url && (_jsx(Button, { className: "w-full", size: "sm", asChild: true, children: _jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", children: "Ver produto" }) }))] })] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Check, Circle, Clock } from "lucide-react";
|
|
3
|
+
import { Progress } from "../ui/progress.js";
|
|
4
|
+
import { Badge } from "../ui/badge.js";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
export function ProgressStepsRenderer({ title, steps }) {
|
|
7
|
+
const completed = steps.filter((s) => s.status === "completed").length;
|
|
8
|
+
const percentage = steps.length > 0 ? Math.round((completed / steps.length) * 100) : 0;
|
|
9
|
+
return (_jsxs("div", { className: "space-y-3", children: [title && _jsx("p", { className: "font-medium text-foreground", children: title }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Progress, { value: percentage, className: "flex-1" }), _jsxs("span", { className: "text-sm text-muted-foreground tabular-nums", children: [percentage, "%"] })] }), _jsxs("p", { className: "text-sm text-muted-foreground", children: [completed, " de ", steps.length, " conclu\u00EDdos"] }), _jsx("ol", { className: "space-y-2", children: steps.map((step, index) => {
|
|
10
|
+
const isCompleted = step.status === "completed";
|
|
11
|
+
const isPending = step.status === "pending";
|
|
12
|
+
return (_jsxs("li", { className: "flex items-start gap-2", children: [_jsx("span", { "aria-hidden": "true", className: cn("mt-0.5 shrink-0", isCompleted ? "text-primary" : "text-muted-foreground"), children: isCompleted ? (_jsx(Check, { size: 16 })) : isPending ? (_jsx(Circle, { size: 16 })) : (_jsx(Clock, { size: 16 })) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: cn("text-sm", isCompleted ? "text-foreground" : "text-muted-foreground"), children: step.label }), step.description && (_jsx("p", { className: "text-xs text-muted-foreground mt-0.5", children: step.description }))] }), _jsx(Badge, { variant: "secondary", className: "shrink-0 text-xs", children: index + 1 })] }, index));
|
|
13
|
+
}) })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ExternalLink, Globe } from "lucide-react";
|
|
3
|
+
export function SourcesListRenderer({ label, sources }) {
|
|
4
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: label }), _jsx("ol", { className: "space-y-1", children: sources.map((source, index) => (_jsx("li", { children: _jsxs("a", { href: source.url, target: "_blank", rel: "noopener noreferrer", className: "flex items-start gap-2 p-2 rounded-md hover:bg-muted text-sm", children: [_jsx("span", { className: "text-xs text-muted-foreground font-mono w-5 shrink-0 pt-0.5", children: index + 1 }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [source.favicon ? (_jsx("img", { src: source.favicon, alt: "", width: 14, height: 14, className: "shrink-0", "aria-hidden": "true" })) : (_jsx(Globe, { size: 14, className: "shrink-0 text-muted-foreground", "aria-hidden": "true" })), _jsx("span", { className: "font-medium text-primary truncate", children: source.title }), _jsx(ExternalLink, { size: 12, className: "shrink-0 text-muted-foreground", "aria-hidden": "true" })] }), source.snippet && (_jsx("p", { className: "text-xs text-muted-foreground line-clamp-2 mt-0.5", children: source.snippet }))] })] }) }, index))) })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ScrollArea, ScrollBar } from "../ui/scroll-area.js";
|
|
3
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../ui/table.js";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
function formatCell(value, colIndex, moneyColumns = [], percentColumns = []) {
|
|
6
|
+
if (value === null || value === undefined)
|
|
7
|
+
return "";
|
|
8
|
+
if (typeof value === "number") {
|
|
9
|
+
if (moneyColumns.includes(colIndex)) {
|
|
10
|
+
return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(value);
|
|
11
|
+
}
|
|
12
|
+
if (percentColumns.includes(colIndex)) {
|
|
13
|
+
return new Intl.NumberFormat("pt-BR", {
|
|
14
|
+
style: "percent",
|
|
15
|
+
minimumFractionDigits: 1,
|
|
16
|
+
maximumFractionDigits: 2,
|
|
17
|
+
}).format(value / 100);
|
|
18
|
+
}
|
|
19
|
+
return new Intl.NumberFormat("pt-BR").format(value);
|
|
20
|
+
}
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
export function SpreadsheetRenderer({ title, headers, rows, format }) {
|
|
24
|
+
const moneyColumns = format?.moneyColumns ?? [];
|
|
25
|
+
const percentColumns = format?.percentColumns ?? [];
|
|
26
|
+
return (_jsxs("div", { className: "space-y-2", children: [title && _jsx("h3", { className: "text-sm font-semibold text-foreground", children: title }), _jsxs(ScrollArea, { className: "w-full", children: [_jsxs(Table, { "aria-readonly": "true", children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "text-muted-foreground font-normal text-center w-10", "aria-label": "Linha" }), headers.map((h, i) => (_jsx(TableHead, { className: "font-semibold", children: h }, i)))] }) }), _jsx(TableBody, { children: rows.map((row, ri) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "text-center text-xs text-muted-foreground select-none", children: ri + 1 }), row.map((cell, ci) => {
|
|
27
|
+
const isMoney = moneyColumns.includes(ci);
|
|
28
|
+
const isPercent = percentColumns.includes(ci);
|
|
29
|
+
const isNumber = typeof cell === "number";
|
|
30
|
+
return (_jsx(TableCell, { className: cn((isMoney || isPercent || isNumber) && "text-right font-mono text-sm"), children: formatCell(cell, ci, moneyColumns, percentColumns) }, ci));
|
|
31
|
+
})] }, ri))) })] }), _jsx(ScrollBar, { orientation: "horizontal" })] })] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { Badge } from "../ui/badge";
|
|
4
|
+
const STATUS_CIRCLE = {
|
|
5
|
+
completed: "bg-primary text-primary-foreground",
|
|
6
|
+
current: "bg-primary/20 border border-primary",
|
|
7
|
+
pending: "bg-muted text-muted-foreground",
|
|
8
|
+
};
|
|
9
|
+
const STATUS_TITLE = {
|
|
10
|
+
completed: "text-foreground",
|
|
11
|
+
current: "text-primary",
|
|
12
|
+
pending: "text-muted-foreground",
|
|
13
|
+
};
|
|
14
|
+
export function StepTimelineRenderer({ title, steps, orientation }) {
|
|
15
|
+
const isVertical = orientation !== "horizontal";
|
|
16
|
+
return (_jsxs("div", { className: cn("flex", isVertical ? "flex-col gap-0" : "flex-row items-start gap-0"), children: [title && (_jsx("p", { className: "text-sm font-medium text-foreground mb-3", children: title })), steps.map((step, index) => {
|
|
17
|
+
const isLast = index === steps.length - 1;
|
|
18
|
+
const { status } = step;
|
|
19
|
+
return (_jsxs("div", { className: cn("flex", isVertical ? "flex-row gap-3" : "flex-col items-center gap-2 flex-1"), children: [_jsxs("div", { className: cn("flex", isVertical ? "flex-col items-center" : "flex-row items-center"), children: [_jsx("div", { className: cn("w-6 h-6 rounded-full flex items-center justify-center shrink-0", STATUS_CIRCLE[status]), children: _jsx(Badge, { variant: "secondary", className: "w-5 h-5 flex items-center justify-center rounded-full p-0 text-[10px] font-semibold border-0 bg-transparent text-inherit", children: index + 1 }) }), !isLast && (_jsx("div", { className: cn(isVertical ? "w-px h-6 bg-border" : "h-px w-full bg-border flex-1") }))] }), _jsxs("div", { className: cn("pb-4 flex-1 min-w-0", isLast && "pb-0", !isVertical && "text-center"), children: [_jsx("p", { className: cn("text-sm font-medium leading-none", STATUS_TITLE[status]), children: step.title }), step.description && (_jsx("p", { className: "text-xs text-muted-foreground mt-1", children: step.description }))] })] }, index));
|
|
20
|
+
})] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { AlertRenderer } from "./AlertRenderer.js";
|
|
2
|
+
export { MetricCardRenderer } from "./MetricCardRenderer.js";
|
|
3
|
+
export { PriceHighlightRenderer } from "./PriceHighlightRenderer.js";
|
|
4
|
+
export { FileCardRenderer } from "./FileCardRenderer.js";
|
|
5
|
+
export { CodeBlockRenderer } from "./CodeBlockRenderer.js";
|
|
6
|
+
export { SourcesListRenderer } from "./SourcesListRenderer.js";
|
|
7
|
+
export { StepTimelineRenderer } from "./StepTimelineRenderer.js";
|
|
8
|
+
export { ProgressStepsRenderer } from "./ProgressStepsRenderer.js";
|
|
9
|
+
export { ChartRenderer } from "./ChartRenderer.js";
|
|
10
|
+
export { CarouselRenderer } from "./CarouselRenderer.js";
|
|
11
|
+
export { ProductCardRenderer } from "./ProductCardRenderer.js";
|
|
12
|
+
export { ComparisonTableRenderer } from "./ComparisonTableRenderer.js";
|
|
13
|
+
export { DataTableRenderer } from "./DataTableRenderer.js";
|
|
14
|
+
export { SpreadsheetRenderer } from "./SpreadsheetRenderer.js";
|
|
15
|
+
export { GalleryRenderer } from "./GalleryRenderer.js";
|
|
16
|
+
export { ImageViewerRenderer } from "./ImageViewerRenderer.js";
|
|
17
|
+
export { LinkPreviewRenderer } from "./LinkPreviewRenderer.js";
|
|
18
|
+
export { MapViewRenderer } from "./MapViewRenderer.js";
|
|
19
|
+
export { ChoiceButtonsRenderer } from "./ChoiceButtonsRenderer.js";
|
|
20
|
+
export { defaultDisplayRenderers, resolveDisplayRenderer } from "./registry.js";
|
|
21
|
+
export type { DisplayRendererMap, DisplayActionName } from "./registry.js";
|