@codrstudio/openclaude-chat 0.1.0 → 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/StreamingIndicator.js +5 -5
- package/dist/display/DisplayReactRenderer.js +12 -12
- package/dist/display/react-sandbox/bootstrap.js +150 -150
- package/dist/styles.css +1 -2
- package/package.json +64 -61
- package/src/components/Chat.tsx +107 -107
- package/src/components/ErrorNote.tsx +35 -35
- package/src/components/LazyRender.tsx +42 -42
- package/src/components/Markdown.tsx +114 -114
- package/src/components/MessageBubble.tsx +107 -107
- package/src/components/MessageInput.tsx +421 -421
- package/src/components/MessageList.tsx +153 -153
- package/src/components/StreamingIndicator.tsx +19 -19
- package/src/display/AlertRenderer.tsx +23 -23
- package/src/display/CarouselRenderer.tsx +141 -141
- package/src/display/ChartRenderer.tsx +195 -195
- package/src/display/ChoiceButtonsRenderer.tsx +114 -114
- package/src/display/CodeBlockRenderer.tsx +49 -49
- package/src/display/ComparisonTableRenderer.tsx +132 -132
- package/src/display/DataTableRenderer.tsx +144 -144
- package/src/display/DisplayReactRenderer.tsx +269 -269
- package/src/display/FileCardRenderer.tsx +55 -55
- package/src/display/GalleryRenderer.tsx +65 -65
- package/src/display/ImageViewerRenderer.tsx +114 -114
- package/src/display/LinkPreviewRenderer.tsx +74 -74
- package/src/display/MapViewRenderer.tsx +75 -75
- package/src/display/MetricCardRenderer.tsx +29 -29
- package/src/display/PriceHighlightRenderer.tsx +62 -62
- package/src/display/ProductCardRenderer.tsx +112 -112
- package/src/display/ProgressStepsRenderer.tsx +59 -59
- package/src/display/SourcesListRenderer.tsx +47 -47
- package/src/display/SpreadsheetRenderer.tsx +86 -86
- package/src/display/StepTimelineRenderer.tsx +75 -75
- package/src/display/index.ts +21 -21
- package/src/display/react-sandbox/bootstrap.ts +155 -155
- package/src/display/registry.ts +84 -84
- package/src/display/sdk-types.ts +217 -217
- package/src/hooks/ChatProvider.tsx +21 -21
- package/src/hooks/useIsMobile.ts +15 -15
- package/src/hooks/useOpenClaudeChat.ts +476 -476
- package/src/index.ts +76 -76
- package/src/lib/utils.ts +6 -6
- package/src/parts/PartErrorBoundary.tsx +51 -51
- package/src/parts/PartRenderer.tsx +145 -145
- package/src/parts/ReasoningBlock.tsx +41 -41
- package/src/parts/ToolActivity.tsx +78 -78
- package/src/parts/ToolResult.tsx +79 -79
- package/src/styles.css +2 -2
- package/src/types.ts +41 -41
- package/src/ui/alert.tsx +77 -77
- package/src/ui/badge.tsx +36 -36
- package/src/ui/button.tsx +54 -54
- package/src/ui/card.tsx +68 -68
- package/src/ui/collapsible.tsx +7 -7
- package/src/ui/dialog.tsx +122 -122
- package/src/ui/dropdown-menu.tsx +76 -76
- package/src/ui/input.tsx +24 -24
- package/src/ui/progress.tsx +36 -36
- package/src/ui/scroll-area.tsx +48 -48
- package/src/ui/separator.tsx +31 -31
- package/src/ui/skeleton.tsx +9 -9
- package/src/ui/table.tsx +114 -114
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import type { DisplayMetric } from "./sdk-types.js";
|
|
2
|
-
import { ArrowDown, ArrowRight, ArrowUp } from "lucide-react";
|
|
3
|
-
import { Card } from "../ui/card.js";
|
|
4
|
-
|
|
5
|
-
const TREND_CONFIG = {
|
|
6
|
-
up: { Icon: ArrowUp, colorClass: "text-primary" },
|
|
7
|
-
down: { Icon: ArrowDown, colorClass: "text-destructive" },
|
|
8
|
-
neutral: { Icon: ArrowRight, colorClass: "text-muted-foreground" },
|
|
9
|
-
} as const;
|
|
10
|
-
|
|
11
|
-
export function MetricCardRenderer({ label, value, unit, trend }: DisplayMetric) {
|
|
12
|
-
const trendConfig = trend ? TREND_CONFIG[trend.direction] : null;
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<Card className="p-4 w-fit">
|
|
16
|
-
<p className="text-sm text-muted-foreground">{label}</p>
|
|
17
|
-
<div className="flex items-baseline gap-2 mt-1">
|
|
18
|
-
<span className="text-2xl font-bold text-foreground">{value}</span>
|
|
19
|
-
{unit && <span className="text-sm text-muted-foreground">{unit}</span>}
|
|
20
|
-
</div>
|
|
21
|
-
{trend && trendConfig && (
|
|
22
|
-
<div className={`flex items-center gap-1 mt-1 text-sm ${trendConfig.colorClass}`}>
|
|
23
|
-
<trendConfig.Icon size={14} aria-hidden="true" />
|
|
24
|
-
<span>{trend.value}</span>
|
|
25
|
-
</div>
|
|
26
|
-
)}
|
|
27
|
-
</Card>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
1
|
+
import type { DisplayMetric } from "./sdk-types.js";
|
|
2
|
+
import { ArrowDown, ArrowRight, ArrowUp } from "lucide-react";
|
|
3
|
+
import { Card } from "../ui/card.js";
|
|
4
|
+
|
|
5
|
+
const TREND_CONFIG = {
|
|
6
|
+
up: { Icon: ArrowUp, colorClass: "text-primary" },
|
|
7
|
+
down: { Icon: ArrowDown, colorClass: "text-destructive" },
|
|
8
|
+
neutral: { Icon: ArrowRight, colorClass: "text-muted-foreground" },
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export function MetricCardRenderer({ label, value, unit, trend }: DisplayMetric) {
|
|
12
|
+
const trendConfig = trend ? TREND_CONFIG[trend.direction] : null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Card className="p-4 w-fit">
|
|
16
|
+
<p className="text-sm text-muted-foreground">{label}</p>
|
|
17
|
+
<div className="flex items-baseline gap-2 mt-1">
|
|
18
|
+
<span className="text-2xl font-bold text-foreground">{value}</span>
|
|
19
|
+
{unit && <span className="text-sm text-muted-foreground">{unit}</span>}
|
|
20
|
+
</div>
|
|
21
|
+
{trend && trendConfig && (
|
|
22
|
+
<div className={`flex items-center gap-1 mt-1 text-sm ${trendConfig.colorClass}`}>
|
|
23
|
+
<trendConfig.Icon size={14} aria-hidden="true" />
|
|
24
|
+
<span>{trend.value}</span>
|
|
25
|
+
</div>
|
|
26
|
+
)}
|
|
27
|
+
</Card>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import type { DisplayPrice } from "./sdk-types.js";
|
|
2
|
-
import { ExternalLink } from "lucide-react";
|
|
3
|
-
import { Badge } from "../ui/badge";
|
|
4
|
-
import { Card } from "../ui/card";
|
|
5
|
-
|
|
6
|
-
function formatPrice(value: number, currency?: string): string {
|
|
7
|
-
// Fallbacks defensivos: se o modelo nao enviar currency ou enviar um valor
|
|
8
|
-
// invalido (ex: string vazia), caimos num formato numerico simples em vez de
|
|
9
|
-
// crashar a arvore React com "Currency code is required".
|
|
10
|
-
try {
|
|
11
|
-
return new Intl.NumberFormat("pt-BR", {
|
|
12
|
-
style: "currency",
|
|
13
|
-
currency: currency || "BRL",
|
|
14
|
-
}).format(value);
|
|
15
|
-
} catch {
|
|
16
|
-
return new Intl.NumberFormat("pt-BR").format(value);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function PriceHighlightRenderer({ value, label, context, source, badge }: DisplayPrice) {
|
|
21
|
-
// Tambem aceita value como numero flat (modelo pode nao seguir schema exato).
|
|
22
|
-
const amount =
|
|
23
|
-
typeof value === "number"
|
|
24
|
-
? value
|
|
25
|
-
: typeof value === "object" && value !== null && "value" in value
|
|
26
|
-
? (value as { value: number }).value
|
|
27
|
-
: 0;
|
|
28
|
-
const currency =
|
|
29
|
-
typeof value === "object" && value !== null && "currency" in value
|
|
30
|
-
? (value as { currency?: string }).currency
|
|
31
|
-
: undefined;
|
|
32
|
-
return (
|
|
33
|
-
<Card className="p-4 space-y-1 w-fit">
|
|
34
|
-
<p className="text-sm text-muted-foreground">{label}</p>
|
|
35
|
-
<div className="flex items-baseline gap-2">
|
|
36
|
-
<span className="text-2xl font-bold text-foreground">
|
|
37
|
-
{formatPrice(amount, currency)}
|
|
38
|
-
</span>
|
|
39
|
-
{badge && (
|
|
40
|
-
<Badge variant="destructive">
|
|
41
|
-
{badge.label}
|
|
42
|
-
</Badge>
|
|
43
|
-
)}
|
|
44
|
-
</div>
|
|
45
|
-
{context && <p className="text-sm text-muted-foreground">{context}</p>}
|
|
46
|
-
{source && (
|
|
47
|
-
<a
|
|
48
|
-
href={source.url}
|
|
49
|
-
target="_blank"
|
|
50
|
-
rel="noopener noreferrer"
|
|
51
|
-
className="flex items-center gap-1.5 text-xs text-primary hover:underline"
|
|
52
|
-
>
|
|
53
|
-
{source.favicon && (
|
|
54
|
-
<img src={source.favicon} alt="" width={14} height={14} aria-hidden="true" />
|
|
55
|
-
)}
|
|
56
|
-
<span>{source.name}</span>
|
|
57
|
-
<ExternalLink size={12} aria-hidden="true" />
|
|
58
|
-
</a>
|
|
59
|
-
)}
|
|
60
|
-
</Card>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
1
|
+
import type { DisplayPrice } from "./sdk-types.js";
|
|
2
|
+
import { ExternalLink } from "lucide-react";
|
|
3
|
+
import { Badge } from "../ui/badge";
|
|
4
|
+
import { Card } from "../ui/card";
|
|
5
|
+
|
|
6
|
+
function formatPrice(value: number, currency?: string): string {
|
|
7
|
+
// Fallbacks defensivos: se o modelo nao enviar currency ou enviar um valor
|
|
8
|
+
// invalido (ex: string vazia), caimos num formato numerico simples em vez de
|
|
9
|
+
// crashar a arvore React com "Currency code is required".
|
|
10
|
+
try {
|
|
11
|
+
return new Intl.NumberFormat("pt-BR", {
|
|
12
|
+
style: "currency",
|
|
13
|
+
currency: currency || "BRL",
|
|
14
|
+
}).format(value);
|
|
15
|
+
} catch {
|
|
16
|
+
return new Intl.NumberFormat("pt-BR").format(value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function PriceHighlightRenderer({ value, label, context, source, badge }: DisplayPrice) {
|
|
21
|
+
// Tambem aceita value como numero flat (modelo pode nao seguir schema exato).
|
|
22
|
+
const amount =
|
|
23
|
+
typeof value === "number"
|
|
24
|
+
? value
|
|
25
|
+
: typeof value === "object" && value !== null && "value" in value
|
|
26
|
+
? (value as { value: number }).value
|
|
27
|
+
: 0;
|
|
28
|
+
const currency =
|
|
29
|
+
typeof value === "object" && value !== null && "currency" in value
|
|
30
|
+
? (value as { currency?: string }).currency
|
|
31
|
+
: undefined;
|
|
32
|
+
return (
|
|
33
|
+
<Card className="p-4 space-y-1 w-fit">
|
|
34
|
+
<p className="text-sm text-muted-foreground">{label}</p>
|
|
35
|
+
<div className="flex items-baseline gap-2">
|
|
36
|
+
<span className="text-2xl font-bold text-foreground">
|
|
37
|
+
{formatPrice(amount, currency)}
|
|
38
|
+
</span>
|
|
39
|
+
{badge && (
|
|
40
|
+
<Badge variant="destructive">
|
|
41
|
+
{badge.label}
|
|
42
|
+
</Badge>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
{context && <p className="text-sm text-muted-foreground">{context}</p>}
|
|
46
|
+
{source && (
|
|
47
|
+
<a
|
|
48
|
+
href={source.url}
|
|
49
|
+
target="_blank"
|
|
50
|
+
rel="noopener noreferrer"
|
|
51
|
+
className="flex items-center gap-1.5 text-xs text-primary hover:underline"
|
|
52
|
+
>
|
|
53
|
+
{source.favicon && (
|
|
54
|
+
<img src={source.favicon} alt="" width={14} height={14} aria-hidden="true" />
|
|
55
|
+
)}
|
|
56
|
+
<span>{source.name}</span>
|
|
57
|
+
<ExternalLink size={12} aria-hidden="true" />
|
|
58
|
+
</a>
|
|
59
|
+
)}
|
|
60
|
+
</Card>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import type { DisplayProduct } from "./sdk-types.js";
|
|
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
|
-
|
|
9
|
-
function StarRating({ score, count }: { score: number; count: number }) {
|
|
10
|
-
const fullStars = Math.floor(score);
|
|
11
|
-
const hasHalf = score - fullStars >= 0.5;
|
|
12
|
-
const emptyStars = 5 - fullStars - (hasHalf ? 1 : 0);
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<div
|
|
16
|
-
className="flex items-center gap-0.5 text-primary"
|
|
17
|
-
aria-label={`${score} de 5 estrelas (${count} avaliações)`}
|
|
18
|
-
>
|
|
19
|
-
{Array.from({ length: fullStars }).map((_, i) => (
|
|
20
|
-
<Star key={`f${i}`} size={14} fill="currentColor" />
|
|
21
|
-
))}
|
|
22
|
-
{hasHalf && <Star size={14} fill="none" />}
|
|
23
|
-
{Array.from({ length: emptyStars }).map((_, i) => (
|
|
24
|
-
<Star key={`e${i}`} size={14} fill="none" className="text-muted-foreground" />
|
|
25
|
-
))}
|
|
26
|
-
<span className="text-xs text-muted-foreground ml-1">({count})</span>
|
|
27
|
-
</div>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function formatMoney(value: number, currency = "BRL") {
|
|
32
|
-
return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function ProductCardRenderer({
|
|
36
|
-
title,
|
|
37
|
-
image,
|
|
38
|
-
price,
|
|
39
|
-
originalPrice,
|
|
40
|
-
rating,
|
|
41
|
-
badges,
|
|
42
|
-
url,
|
|
43
|
-
description,
|
|
44
|
-
}: DisplayProduct) {
|
|
45
|
-
const [imgError, setImgError] = useState(false);
|
|
46
|
-
|
|
47
|
-
const discount =
|
|
48
|
-
price && originalPrice && originalPrice.value > price.value
|
|
49
|
-
? Math.round(((originalPrice.value - price.value) / originalPrice.value) * 100)
|
|
50
|
-
: null;
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<Card className="overflow-hidden w-fit max-w-sm">
|
|
54
|
-
{image && !imgError && (
|
|
55
|
-
<div className="relative">
|
|
56
|
-
<img
|
|
57
|
-
src={image}
|
|
58
|
-
alt={title}
|
|
59
|
-
className={cn("w-full aspect-video object-cover")}
|
|
60
|
-
loading="lazy"
|
|
61
|
-
decoding="async"
|
|
62
|
-
onError={() => setImgError(true)}
|
|
63
|
-
/>
|
|
64
|
-
{discount !== null && (
|
|
65
|
-
<Badge variant="destructive" className="absolute top-2 right-2">
|
|
66
|
-
-{discount}%
|
|
67
|
-
</Badge>
|
|
68
|
-
)}
|
|
69
|
-
</div>
|
|
70
|
-
)}
|
|
71
|
-
|
|
72
|
-
<CardContent className="p-4 space-y-2">
|
|
73
|
-
{badges && badges.length > 0 && (
|
|
74
|
-
<div className="flex flex-wrap gap-1">
|
|
75
|
-
{badges.map((b, i) => (
|
|
76
|
-
<Badge key={i} variant="secondary">
|
|
77
|
-
{b.label}
|
|
78
|
-
</Badge>
|
|
79
|
-
))}
|
|
80
|
-
</div>
|
|
81
|
-
)}
|
|
82
|
-
|
|
83
|
-
<CardTitle className="text-sm">{title}</CardTitle>
|
|
84
|
-
|
|
85
|
-
{description && (
|
|
86
|
-
<p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
|
|
87
|
-
)}
|
|
88
|
-
|
|
89
|
-
{rating && <StarRating score={rating.score} count={rating.count} />}
|
|
90
|
-
|
|
91
|
-
{price && (
|
|
92
|
-
<div className="flex items-baseline gap-2">
|
|
93
|
-
<span className="text-lg font-bold">{formatMoney(price.value, price.currency)}</span>
|
|
94
|
-
{originalPrice && (
|
|
95
|
-
<span className="text-sm text-muted-foreground line-through">
|
|
96
|
-
{formatMoney(originalPrice.value, originalPrice.currency)}
|
|
97
|
-
</span>
|
|
98
|
-
)}
|
|
99
|
-
</div>
|
|
100
|
-
)}
|
|
101
|
-
|
|
102
|
-
{url && (
|
|
103
|
-
<Button className="w-full" size="sm" asChild>
|
|
104
|
-
<a href={url} target="_blank" rel="noopener noreferrer">
|
|
105
|
-
Ver produto
|
|
106
|
-
</a>
|
|
107
|
-
</Button>
|
|
108
|
-
)}
|
|
109
|
-
</CardContent>
|
|
110
|
-
</Card>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { DisplayProduct } from "./sdk-types.js";
|
|
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
|
+
|
|
9
|
+
function StarRating({ score, count }: { score: number; count: number }) {
|
|
10
|
+
const fullStars = Math.floor(score);
|
|
11
|
+
const hasHalf = score - fullStars >= 0.5;
|
|
12
|
+
const emptyStars = 5 - fullStars - (hasHalf ? 1 : 0);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className="flex items-center gap-0.5 text-primary"
|
|
17
|
+
aria-label={`${score} de 5 estrelas (${count} avaliações)`}
|
|
18
|
+
>
|
|
19
|
+
{Array.from({ length: fullStars }).map((_, i) => (
|
|
20
|
+
<Star key={`f${i}`} size={14} fill="currentColor" />
|
|
21
|
+
))}
|
|
22
|
+
{hasHalf && <Star size={14} fill="none" />}
|
|
23
|
+
{Array.from({ length: emptyStars }).map((_, i) => (
|
|
24
|
+
<Star key={`e${i}`} size={14} fill="none" className="text-muted-foreground" />
|
|
25
|
+
))}
|
|
26
|
+
<span className="text-xs text-muted-foreground ml-1">({count})</span>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatMoney(value: number, currency = "BRL") {
|
|
32
|
+
return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ProductCardRenderer({
|
|
36
|
+
title,
|
|
37
|
+
image,
|
|
38
|
+
price,
|
|
39
|
+
originalPrice,
|
|
40
|
+
rating,
|
|
41
|
+
badges,
|
|
42
|
+
url,
|
|
43
|
+
description,
|
|
44
|
+
}: DisplayProduct) {
|
|
45
|
+
const [imgError, setImgError] = useState(false);
|
|
46
|
+
|
|
47
|
+
const discount =
|
|
48
|
+
price && originalPrice && originalPrice.value > price.value
|
|
49
|
+
? Math.round(((originalPrice.value - price.value) / originalPrice.value) * 100)
|
|
50
|
+
: null;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Card className="overflow-hidden w-fit max-w-sm">
|
|
54
|
+
{image && !imgError && (
|
|
55
|
+
<div className="relative">
|
|
56
|
+
<img
|
|
57
|
+
src={image}
|
|
58
|
+
alt={title}
|
|
59
|
+
className={cn("w-full aspect-video object-cover")}
|
|
60
|
+
loading="lazy"
|
|
61
|
+
decoding="async"
|
|
62
|
+
onError={() => setImgError(true)}
|
|
63
|
+
/>
|
|
64
|
+
{discount !== null && (
|
|
65
|
+
<Badge variant="destructive" className="absolute top-2 right-2">
|
|
66
|
+
-{discount}%
|
|
67
|
+
</Badge>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
<CardContent className="p-4 space-y-2">
|
|
73
|
+
{badges && badges.length > 0 && (
|
|
74
|
+
<div className="flex flex-wrap gap-1">
|
|
75
|
+
{badges.map((b, i) => (
|
|
76
|
+
<Badge key={i} variant="secondary">
|
|
77
|
+
{b.label}
|
|
78
|
+
</Badge>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
<CardTitle className="text-sm">{title}</CardTitle>
|
|
84
|
+
|
|
85
|
+
{description && (
|
|
86
|
+
<p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{rating && <StarRating score={rating.score} count={rating.count} />}
|
|
90
|
+
|
|
91
|
+
{price && (
|
|
92
|
+
<div className="flex items-baseline gap-2">
|
|
93
|
+
<span className="text-lg font-bold">{formatMoney(price.value, price.currency)}</span>
|
|
94
|
+
{originalPrice && (
|
|
95
|
+
<span className="text-sm text-muted-foreground line-through">
|
|
96
|
+
{formatMoney(originalPrice.value, originalPrice.currency)}
|
|
97
|
+
</span>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{url && (
|
|
103
|
+
<Button className="w-full" size="sm" asChild>
|
|
104
|
+
<a href={url} target="_blank" rel="noopener noreferrer">
|
|
105
|
+
Ver produto
|
|
106
|
+
</a>
|
|
107
|
+
</Button>
|
|
108
|
+
)}
|
|
109
|
+
</CardContent>
|
|
110
|
+
</Card>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import type { DisplayProgress } from "./sdk-types.js";
|
|
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
|
-
|
|
7
|
-
export function ProgressStepsRenderer({ title, steps }: DisplayProgress) {
|
|
8
|
-
const completed = steps.filter((s) => s.status === "completed").length;
|
|
9
|
-
const percentage = steps.length > 0 ? Math.round((completed / steps.length) * 100) : 0;
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<div className="space-y-3">
|
|
13
|
-
{title && <p className="font-medium text-foreground">{title}</p>}
|
|
14
|
-
<div className="flex items-center gap-3">
|
|
15
|
-
<Progress value={percentage} className="flex-1" />
|
|
16
|
-
<span className="text-sm text-muted-foreground tabular-nums">{percentage}%</span>
|
|
17
|
-
</div>
|
|
18
|
-
<p className="text-sm text-muted-foreground">
|
|
19
|
-
{completed} de {steps.length} concluídos
|
|
20
|
-
</p>
|
|
21
|
-
<ol className="space-y-2">
|
|
22
|
-
{steps.map((step, index) => {
|
|
23
|
-
const isCompleted = step.status === "completed";
|
|
24
|
-
const isPending = step.status === "pending";
|
|
25
|
-
return (
|
|
26
|
-
<li key={index} className="flex items-start gap-2">
|
|
27
|
-
<span
|
|
28
|
-
aria-hidden="true"
|
|
29
|
-
className={cn(
|
|
30
|
-
"mt-0.5 shrink-0",
|
|
31
|
-
isCompleted ? "text-primary" : "text-muted-foreground"
|
|
32
|
-
)}
|
|
33
|
-
>
|
|
34
|
-
{isCompleted ? (
|
|
35
|
-
<Check size={16} />
|
|
36
|
-
) : isPending ? (
|
|
37
|
-
<Circle size={16} />
|
|
38
|
-
) : (
|
|
39
|
-
<Clock size={16} />
|
|
40
|
-
)}
|
|
41
|
-
</span>
|
|
42
|
-
<div className="flex-1 min-w-0">
|
|
43
|
-
<p className={cn("text-sm", isCompleted ? "text-foreground" : "text-muted-foreground")}>
|
|
44
|
-
{step.label}
|
|
45
|
-
</p>
|
|
46
|
-
{step.description && (
|
|
47
|
-
<p className="text-xs text-muted-foreground mt-0.5">{step.description}</p>
|
|
48
|
-
)}
|
|
49
|
-
</div>
|
|
50
|
-
<Badge variant="secondary" className="shrink-0 text-xs">
|
|
51
|
-
{index + 1}
|
|
52
|
-
</Badge>
|
|
53
|
-
</li>
|
|
54
|
-
);
|
|
55
|
-
})}
|
|
56
|
-
</ol>
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
1
|
+
import type { DisplayProgress } from "./sdk-types.js";
|
|
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
|
+
|
|
7
|
+
export function ProgressStepsRenderer({ title, steps }: DisplayProgress) {
|
|
8
|
+
const completed = steps.filter((s) => s.status === "completed").length;
|
|
9
|
+
const percentage = steps.length > 0 ? Math.round((completed / steps.length) * 100) : 0;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-3">
|
|
13
|
+
{title && <p className="font-medium text-foreground">{title}</p>}
|
|
14
|
+
<div className="flex items-center gap-3">
|
|
15
|
+
<Progress value={percentage} className="flex-1" />
|
|
16
|
+
<span className="text-sm text-muted-foreground tabular-nums">{percentage}%</span>
|
|
17
|
+
</div>
|
|
18
|
+
<p className="text-sm text-muted-foreground">
|
|
19
|
+
{completed} de {steps.length} concluídos
|
|
20
|
+
</p>
|
|
21
|
+
<ol className="space-y-2">
|
|
22
|
+
{steps.map((step, index) => {
|
|
23
|
+
const isCompleted = step.status === "completed";
|
|
24
|
+
const isPending = step.status === "pending";
|
|
25
|
+
return (
|
|
26
|
+
<li key={index} className="flex items-start gap-2">
|
|
27
|
+
<span
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
className={cn(
|
|
30
|
+
"mt-0.5 shrink-0",
|
|
31
|
+
isCompleted ? "text-primary" : "text-muted-foreground"
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
{isCompleted ? (
|
|
35
|
+
<Check size={16} />
|
|
36
|
+
) : isPending ? (
|
|
37
|
+
<Circle size={16} />
|
|
38
|
+
) : (
|
|
39
|
+
<Clock size={16} />
|
|
40
|
+
)}
|
|
41
|
+
</span>
|
|
42
|
+
<div className="flex-1 min-w-0">
|
|
43
|
+
<p className={cn("text-sm", isCompleted ? "text-foreground" : "text-muted-foreground")}>
|
|
44
|
+
{step.label}
|
|
45
|
+
</p>
|
|
46
|
+
{step.description && (
|
|
47
|
+
<p className="text-xs text-muted-foreground mt-0.5">{step.description}</p>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
<Badge variant="secondary" className="shrink-0 text-xs">
|
|
51
|
+
{index + 1}
|
|
52
|
+
</Badge>
|
|
53
|
+
</li>
|
|
54
|
+
);
|
|
55
|
+
})}
|
|
56
|
+
</ol>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import type { DisplaySources } from "./sdk-types.js";
|
|
2
|
-
import { ExternalLink, Globe } from "lucide-react";
|
|
3
|
-
|
|
4
|
-
export function SourcesListRenderer({ label, sources }: DisplaySources) {
|
|
5
|
-
return (
|
|
6
|
-
<div className="space-y-2">
|
|
7
|
-
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{label}</p>
|
|
8
|
-
<ol className="space-y-1">
|
|
9
|
-
{sources.map((source, index) => (
|
|
10
|
-
<li key={index}>
|
|
11
|
-
<a
|
|
12
|
-
href={source.url}
|
|
13
|
-
target="_blank"
|
|
14
|
-
rel="noopener noreferrer"
|
|
15
|
-
className="flex items-start gap-2 p-2 rounded-md hover:bg-muted text-sm"
|
|
16
|
-
>
|
|
17
|
-
<span className="text-xs text-muted-foreground font-mono w-5 shrink-0 pt-0.5">
|
|
18
|
-
{index + 1}
|
|
19
|
-
</span>
|
|
20
|
-
<div className="min-w-0 flex-1">
|
|
21
|
-
<div className="flex items-center gap-1.5">
|
|
22
|
-
{source.favicon ? (
|
|
23
|
-
<img
|
|
24
|
-
src={source.favicon}
|
|
25
|
-
alt=""
|
|
26
|
-
width={14}
|
|
27
|
-
height={14}
|
|
28
|
-
className="shrink-0"
|
|
29
|
-
aria-hidden="true"
|
|
30
|
-
/>
|
|
31
|
-
) : (
|
|
32
|
-
<Globe size={14} className="shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
33
|
-
)}
|
|
34
|
-
<span className="font-medium text-primary truncate">{source.title}</span>
|
|
35
|
-
<ExternalLink size={12} className="shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
36
|
-
</div>
|
|
37
|
-
{source.snippet && (
|
|
38
|
-
<p className="text-xs text-muted-foreground line-clamp-2 mt-0.5">{source.snippet}</p>
|
|
39
|
-
)}
|
|
40
|
-
</div>
|
|
41
|
-
</a>
|
|
42
|
-
</li>
|
|
43
|
-
))}
|
|
44
|
-
</ol>
|
|
45
|
-
</div>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
1
|
+
import type { DisplaySources } from "./sdk-types.js";
|
|
2
|
+
import { ExternalLink, Globe } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export function SourcesListRenderer({ label, sources }: DisplaySources) {
|
|
5
|
+
return (
|
|
6
|
+
<div className="space-y-2">
|
|
7
|
+
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{label}</p>
|
|
8
|
+
<ol className="space-y-1">
|
|
9
|
+
{sources.map((source, index) => (
|
|
10
|
+
<li key={index}>
|
|
11
|
+
<a
|
|
12
|
+
href={source.url}
|
|
13
|
+
target="_blank"
|
|
14
|
+
rel="noopener noreferrer"
|
|
15
|
+
className="flex items-start gap-2 p-2 rounded-md hover:bg-muted text-sm"
|
|
16
|
+
>
|
|
17
|
+
<span className="text-xs text-muted-foreground font-mono w-5 shrink-0 pt-0.5">
|
|
18
|
+
{index + 1}
|
|
19
|
+
</span>
|
|
20
|
+
<div className="min-w-0 flex-1">
|
|
21
|
+
<div className="flex items-center gap-1.5">
|
|
22
|
+
{source.favicon ? (
|
|
23
|
+
<img
|
|
24
|
+
src={source.favicon}
|
|
25
|
+
alt=""
|
|
26
|
+
width={14}
|
|
27
|
+
height={14}
|
|
28
|
+
className="shrink-0"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
/>
|
|
31
|
+
) : (
|
|
32
|
+
<Globe size={14} className="shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
33
|
+
)}
|
|
34
|
+
<span className="font-medium text-primary truncate">{source.title}</span>
|
|
35
|
+
<ExternalLink size={12} className="shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
36
|
+
</div>
|
|
37
|
+
{source.snippet && (
|
|
38
|
+
<p className="text-xs text-muted-foreground line-clamp-2 mt-0.5">{source.snippet}</p>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
</a>
|
|
42
|
+
</li>
|
|
43
|
+
))}
|
|
44
|
+
</ol>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|