@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,65 +1,65 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import type { DisplayGallery } from "./sdk-types.js";
|
|
3
|
-
import { ZoomIn } from "lucide-react";
|
|
4
|
-
import { cn } from "../lib/utils.js";
|
|
5
|
-
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog.js";
|
|
6
|
-
|
|
7
|
-
export function GalleryRenderer({ title, images }: DisplayGallery) {
|
|
8
|
-
const [lightboxIdx, setLightboxIdx] = useState<number | null>(null);
|
|
9
|
-
|
|
10
|
-
const activeImage = lightboxIdx !== null ? images[lightboxIdx] : null;
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<div className="space-y-2">
|
|
14
|
-
{title && <h3 className="text-sm font-medium text-foreground">{title}</h3>}
|
|
15
|
-
|
|
16
|
-
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
|
17
|
-
{images.map((img, i) => (
|
|
18
|
-
<button
|
|
19
|
-
key={i}
|
|
20
|
-
className="relative group cursor-pointer rounded-md overflow-hidden"
|
|
21
|
-
onClick={() => setLightboxIdx(i)}
|
|
22
|
-
aria-label={img.alt ?? `Imagem ${i + 1}`}
|
|
23
|
-
>
|
|
24
|
-
<img
|
|
25
|
-
src={img.url}
|
|
26
|
-
alt={img.alt ?? ""}
|
|
27
|
-
className="w-full h-full object-cover aspect-square"
|
|
28
|
-
loading="lazy"
|
|
29
|
-
/>
|
|
30
|
-
<div className="absolute inset-0 bg-background/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity">
|
|
31
|
-
<ZoomIn className="h-5 w-5 text-foreground" />
|
|
32
|
-
</div>
|
|
33
|
-
{img.caption && (
|
|
34
|
-
<p className="absolute bottom-0 left-0 right-0 text-sm text-muted-foreground bg-background/80 px-2 py-1 truncate">
|
|
35
|
-
{img.caption}
|
|
36
|
-
</p>
|
|
37
|
-
)}
|
|
38
|
-
</button>
|
|
39
|
-
))}
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
<Dialog open={lightboxIdx !== null} onOpenChange={(open) => { if (!open) setLightboxIdx(null); }}>
|
|
43
|
-
<DialogContent className={cn("max-w-4xl p-0 bg-background/95")}>
|
|
44
|
-
<DialogTitle className="sr-only">
|
|
45
|
-
{activeImage?.alt ?? "Visualizador de imagem"}
|
|
46
|
-
</DialogTitle>
|
|
47
|
-
{activeImage && (
|
|
48
|
-
<div className="flex flex-col">
|
|
49
|
-
<img
|
|
50
|
-
src={activeImage.url}
|
|
51
|
-
alt={activeImage.alt ?? ""}
|
|
52
|
-
className="w-full max-h-[80vh] object-contain"
|
|
53
|
-
/>
|
|
54
|
-
{activeImage.caption && (
|
|
55
|
-
<p className="text-sm text-muted-foreground px-4 py-3">
|
|
56
|
-
{activeImage.caption}
|
|
57
|
-
</p>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
)}
|
|
61
|
-
</DialogContent>
|
|
62
|
-
</Dialog>
|
|
63
|
-
</div>
|
|
64
|
-
);
|
|
65
|
-
}
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { DisplayGallery } from "./sdk-types.js";
|
|
3
|
+
import { ZoomIn } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { Dialog, DialogContent, DialogTitle } from "../ui/dialog.js";
|
|
6
|
+
|
|
7
|
+
export function GalleryRenderer({ title, images }: DisplayGallery) {
|
|
8
|
+
const [lightboxIdx, setLightboxIdx] = useState<number | null>(null);
|
|
9
|
+
|
|
10
|
+
const activeImage = lightboxIdx !== null ? images[lightboxIdx] : null;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-2">
|
|
14
|
+
{title && <h3 className="text-sm font-medium text-foreground">{title}</h3>}
|
|
15
|
+
|
|
16
|
+
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
|
17
|
+
{images.map((img, i) => (
|
|
18
|
+
<button
|
|
19
|
+
key={i}
|
|
20
|
+
className="relative group cursor-pointer rounded-md overflow-hidden"
|
|
21
|
+
onClick={() => setLightboxIdx(i)}
|
|
22
|
+
aria-label={img.alt ?? `Imagem ${i + 1}`}
|
|
23
|
+
>
|
|
24
|
+
<img
|
|
25
|
+
src={img.url}
|
|
26
|
+
alt={img.alt ?? ""}
|
|
27
|
+
className="w-full h-full object-cover aspect-square"
|
|
28
|
+
loading="lazy"
|
|
29
|
+
/>
|
|
30
|
+
<div className="absolute inset-0 bg-background/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity">
|
|
31
|
+
<ZoomIn className="h-5 w-5 text-foreground" />
|
|
32
|
+
</div>
|
|
33
|
+
{img.caption && (
|
|
34
|
+
<p className="absolute bottom-0 left-0 right-0 text-sm text-muted-foreground bg-background/80 px-2 py-1 truncate">
|
|
35
|
+
{img.caption}
|
|
36
|
+
</p>
|
|
37
|
+
)}
|
|
38
|
+
</button>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<Dialog open={lightboxIdx !== null} onOpenChange={(open) => { if (!open) setLightboxIdx(null); }}>
|
|
43
|
+
<DialogContent className={cn("max-w-4xl p-0 bg-background/95")}>
|
|
44
|
+
<DialogTitle className="sr-only">
|
|
45
|
+
{activeImage?.alt ?? "Visualizador de imagem"}
|
|
46
|
+
</DialogTitle>
|
|
47
|
+
{activeImage && (
|
|
48
|
+
<div className="flex flex-col">
|
|
49
|
+
<img
|
|
50
|
+
src={activeImage.url}
|
|
51
|
+
alt={activeImage.alt ?? ""}
|
|
52
|
+
className="w-full max-h-[80vh] object-contain"
|
|
53
|
+
/>
|
|
54
|
+
{activeImage.caption && (
|
|
55
|
+
<p className="text-sm text-muted-foreground px-4 py-3">
|
|
56
|
+
{activeImage.caption}
|
|
57
|
+
</p>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
</DialogContent>
|
|
62
|
+
</Dialog>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -1,114 +1,114 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import type { DisplayImage } from "./sdk-types.js";
|
|
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
|
-
|
|
8
|
-
export function ImageViewerRenderer({ url, alt, caption, width, height }: DisplayImage) {
|
|
9
|
-
const [dialogOpen, setDialogOpen] = useState(false);
|
|
10
|
-
const [zoom, setZoom] = useState(1);
|
|
11
|
-
|
|
12
|
-
function handleOpen() {
|
|
13
|
-
setZoom(1);
|
|
14
|
-
setDialogOpen(true);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<div className="flex flex-col gap-1.5">
|
|
19
|
-
<button
|
|
20
|
-
className="relative group cursor-pointer rounded-md overflow-hidden inline-block"
|
|
21
|
-
onClick={handleOpen}
|
|
22
|
-
aria-label={`Ampliar imagem${alt ? `: ${alt}` : ""}`}
|
|
23
|
-
>
|
|
24
|
-
<img
|
|
25
|
-
src={url}
|
|
26
|
-
alt={alt ?? ""}
|
|
27
|
-
className="block max-w-full rounded-md"
|
|
28
|
-
loading="lazy"
|
|
29
|
-
decoding="async"
|
|
30
|
-
width={width}
|
|
31
|
-
height={height}
|
|
32
|
-
/>
|
|
33
|
-
<div className="absolute inset-0 bg-background/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity">
|
|
34
|
-
<ZoomIn className="h-5 w-5 text-foreground" />
|
|
35
|
-
</div>
|
|
36
|
-
</button>
|
|
37
|
-
|
|
38
|
-
{caption && (
|
|
39
|
-
<p className="text-xs text-muted-foreground">{caption}</p>
|
|
40
|
-
)}
|
|
41
|
-
|
|
42
|
-
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
43
|
-
<DialogContent className="max-w-4xl p-0 bg-background/95 overflow-hidden">
|
|
44
|
-
<DialogTitle className="sr-only">{alt ?? "Visualizador de imagem"}</DialogTitle>
|
|
45
|
-
|
|
46
|
-
<div className="flex flex-col">
|
|
47
|
-
<div className="flex items-center gap-1 p-2 border-b border-border">
|
|
48
|
-
<Button
|
|
49
|
-
variant="ghost"
|
|
50
|
-
size="icon"
|
|
51
|
-
onClick={() => setZoom((z) => Math.max(0.25, z - 0.25))}
|
|
52
|
-
aria-label="Reduzir zoom"
|
|
53
|
-
disabled={zoom <= 0.25}
|
|
54
|
-
>
|
|
55
|
-
<ZoomOut className="h-4 w-4" />
|
|
56
|
-
</Button>
|
|
57
|
-
|
|
58
|
-
<span className={cn("text-xs text-muted-foreground w-12 text-center tabular-nums")}>
|
|
59
|
-
{Math.round(zoom * 100)}%
|
|
60
|
-
</span>
|
|
61
|
-
|
|
62
|
-
<Button
|
|
63
|
-
variant="ghost"
|
|
64
|
-
size="icon"
|
|
65
|
-
onClick={() => setZoom((z) => Math.min(4, z + 0.25))}
|
|
66
|
-
aria-label="Aumentar zoom"
|
|
67
|
-
disabled={zoom >= 4}
|
|
68
|
-
>
|
|
69
|
-
<ZoomIn className="h-4 w-4" />
|
|
70
|
-
</Button>
|
|
71
|
-
|
|
72
|
-
<Button
|
|
73
|
-
variant="ghost"
|
|
74
|
-
size="icon"
|
|
75
|
-
onClick={() => setZoom(1)}
|
|
76
|
-
aria-label="Resetar zoom"
|
|
77
|
-
>
|
|
78
|
-
<RotateCcw className="h-4 w-4" />
|
|
79
|
-
</Button>
|
|
80
|
-
|
|
81
|
-
<div className="flex-1" />
|
|
82
|
-
|
|
83
|
-
<Button
|
|
84
|
-
variant="ghost"
|
|
85
|
-
size="icon"
|
|
86
|
-
onClick={() => setDialogOpen(false)}
|
|
87
|
-
aria-label="Fechar"
|
|
88
|
-
>
|
|
89
|
-
<X className="h-4 w-4" />
|
|
90
|
-
</Button>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
<div className="overflow-auto max-h-[80vh] flex items-center justify-center p-4">
|
|
94
|
-
<img
|
|
95
|
-
src={url}
|
|
96
|
-
alt={alt ?? ""}
|
|
97
|
-
style={{ transform: `scale(${zoom})`, transformOrigin: "center" }}
|
|
98
|
-
className="max-w-full transition-transform"
|
|
99
|
-
width={width}
|
|
100
|
-
height={height}
|
|
101
|
-
/>
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
{caption && (
|
|
105
|
-
<p className="text-xs text-muted-foreground text-center p-2 border-t border-border">
|
|
106
|
-
{caption}
|
|
107
|
-
</p>
|
|
108
|
-
)}
|
|
109
|
-
</div>
|
|
110
|
-
</DialogContent>
|
|
111
|
-
</Dialog>
|
|
112
|
-
</div>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { DisplayImage } from "./sdk-types.js";
|
|
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
|
+
|
|
8
|
+
export function ImageViewerRenderer({ url, alt, caption, width, height }: DisplayImage) {
|
|
9
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
10
|
+
const [zoom, setZoom] = useState(1);
|
|
11
|
+
|
|
12
|
+
function handleOpen() {
|
|
13
|
+
setZoom(1);
|
|
14
|
+
setDialogOpen(true);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex flex-col gap-1.5">
|
|
19
|
+
<button
|
|
20
|
+
className="relative group cursor-pointer rounded-md overflow-hidden inline-block"
|
|
21
|
+
onClick={handleOpen}
|
|
22
|
+
aria-label={`Ampliar imagem${alt ? `: ${alt}` : ""}`}
|
|
23
|
+
>
|
|
24
|
+
<img
|
|
25
|
+
src={url}
|
|
26
|
+
alt={alt ?? ""}
|
|
27
|
+
className="block max-w-full rounded-md"
|
|
28
|
+
loading="lazy"
|
|
29
|
+
decoding="async"
|
|
30
|
+
width={width}
|
|
31
|
+
height={height}
|
|
32
|
+
/>
|
|
33
|
+
<div className="absolute inset-0 bg-background/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity">
|
|
34
|
+
<ZoomIn className="h-5 w-5 text-foreground" />
|
|
35
|
+
</div>
|
|
36
|
+
</button>
|
|
37
|
+
|
|
38
|
+
{caption && (
|
|
39
|
+
<p className="text-xs text-muted-foreground">{caption}</p>
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
43
|
+
<DialogContent className="max-w-4xl p-0 bg-background/95 overflow-hidden">
|
|
44
|
+
<DialogTitle className="sr-only">{alt ?? "Visualizador de imagem"}</DialogTitle>
|
|
45
|
+
|
|
46
|
+
<div className="flex flex-col">
|
|
47
|
+
<div className="flex items-center gap-1 p-2 border-b border-border">
|
|
48
|
+
<Button
|
|
49
|
+
variant="ghost"
|
|
50
|
+
size="icon"
|
|
51
|
+
onClick={() => setZoom((z) => Math.max(0.25, z - 0.25))}
|
|
52
|
+
aria-label="Reduzir zoom"
|
|
53
|
+
disabled={zoom <= 0.25}
|
|
54
|
+
>
|
|
55
|
+
<ZoomOut className="h-4 w-4" />
|
|
56
|
+
</Button>
|
|
57
|
+
|
|
58
|
+
<span className={cn("text-xs text-muted-foreground w-12 text-center tabular-nums")}>
|
|
59
|
+
{Math.round(zoom * 100)}%
|
|
60
|
+
</span>
|
|
61
|
+
|
|
62
|
+
<Button
|
|
63
|
+
variant="ghost"
|
|
64
|
+
size="icon"
|
|
65
|
+
onClick={() => setZoom((z) => Math.min(4, z + 0.25))}
|
|
66
|
+
aria-label="Aumentar zoom"
|
|
67
|
+
disabled={zoom >= 4}
|
|
68
|
+
>
|
|
69
|
+
<ZoomIn className="h-4 w-4" />
|
|
70
|
+
</Button>
|
|
71
|
+
|
|
72
|
+
<Button
|
|
73
|
+
variant="ghost"
|
|
74
|
+
size="icon"
|
|
75
|
+
onClick={() => setZoom(1)}
|
|
76
|
+
aria-label="Resetar zoom"
|
|
77
|
+
>
|
|
78
|
+
<RotateCcw className="h-4 w-4" />
|
|
79
|
+
</Button>
|
|
80
|
+
|
|
81
|
+
<div className="flex-1" />
|
|
82
|
+
|
|
83
|
+
<Button
|
|
84
|
+
variant="ghost"
|
|
85
|
+
size="icon"
|
|
86
|
+
onClick={() => setDialogOpen(false)}
|
|
87
|
+
aria-label="Fechar"
|
|
88
|
+
>
|
|
89
|
+
<X className="h-4 w-4" />
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div className="overflow-auto max-h-[80vh] flex items-center justify-center p-4">
|
|
94
|
+
<img
|
|
95
|
+
src={url}
|
|
96
|
+
alt={alt ?? ""}
|
|
97
|
+
style={{ transform: `scale(${zoom})`, transformOrigin: "center" }}
|
|
98
|
+
className="max-w-full transition-transform"
|
|
99
|
+
width={width}
|
|
100
|
+
height={height}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{caption && (
|
|
105
|
+
<p className="text-xs text-muted-foreground text-center p-2 border-t border-border">
|
|
106
|
+
{caption}
|
|
107
|
+
</p>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</DialogContent>
|
|
111
|
+
</Dialog>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import type { DisplayLink } from "./sdk-types.js";
|
|
3
|
-
import { Globe } from "lucide-react";
|
|
4
|
-
import { Card } from "../ui/card";
|
|
5
|
-
|
|
6
|
-
function getDomain(url: string, domainProp?: string): string {
|
|
7
|
-
if (domainProp) return domainProp;
|
|
8
|
-
try {
|
|
9
|
-
return new URL(url).hostname.replace(/^www\./, "");
|
|
10
|
-
} catch {
|
|
11
|
-
return url;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function LinkPreviewRenderer({
|
|
16
|
-
url,
|
|
17
|
-
title,
|
|
18
|
-
description,
|
|
19
|
-
image,
|
|
20
|
-
favicon,
|
|
21
|
-
domain,
|
|
22
|
-
}: DisplayLink) {
|
|
23
|
-
const [imgError, setImgError] = useState(false);
|
|
24
|
-
const [faviconError, setFaviconError] = useState(false);
|
|
25
|
-
|
|
26
|
-
const displayDomain = getDomain(url, domain);
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<a
|
|
30
|
-
href={url}
|
|
31
|
-
target="_blank"
|
|
32
|
-
rel="noopener noreferrer"
|
|
33
|
-
aria-label={`Link: ${title}`}
|
|
34
|
-
>
|
|
35
|
-
<Card className="overflow-hidden hover:bg-muted/50 transition-colors">
|
|
36
|
-
{image && !imgError && (
|
|
37
|
-
<div className="aspect-video overflow-hidden">
|
|
38
|
-
<img
|
|
39
|
-
src={image}
|
|
40
|
-
alt={title}
|
|
41
|
-
className="w-full h-full object-cover"
|
|
42
|
-
loading="lazy"
|
|
43
|
-
decoding="async"
|
|
44
|
-
onError={() => setImgError(true)}
|
|
45
|
-
/>
|
|
46
|
-
</div>
|
|
47
|
-
)}
|
|
48
|
-
|
|
49
|
-
<div className="p-3 space-y-1">
|
|
50
|
-
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
51
|
-
{favicon && !faviconError ? (
|
|
52
|
-
<img
|
|
53
|
-
src={favicon}
|
|
54
|
-
alt=""
|
|
55
|
-
className="w-3 h-3 rounded-sm"
|
|
56
|
-
onError={() => setFaviconError(true)}
|
|
57
|
-
aria-hidden="true"
|
|
58
|
-
/>
|
|
59
|
-
) : (
|
|
60
|
-
<Globe size={12} aria-hidden="true" className="shrink-0" />
|
|
61
|
-
)}
|
|
62
|
-
<span>{displayDomain}</span>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
<p className="font-medium text-foreground text-sm">{title}</p>
|
|
66
|
-
|
|
67
|
-
{description && (
|
|
68
|
-
<p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
|
|
69
|
-
)}
|
|
70
|
-
</div>
|
|
71
|
-
</Card>
|
|
72
|
-
</a>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { DisplayLink } from "./sdk-types.js";
|
|
3
|
+
import { Globe } from "lucide-react";
|
|
4
|
+
import { Card } from "../ui/card";
|
|
5
|
+
|
|
6
|
+
function getDomain(url: string, domainProp?: string): string {
|
|
7
|
+
if (domainProp) return domainProp;
|
|
8
|
+
try {
|
|
9
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
10
|
+
} catch {
|
|
11
|
+
return url;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function LinkPreviewRenderer({
|
|
16
|
+
url,
|
|
17
|
+
title,
|
|
18
|
+
description,
|
|
19
|
+
image,
|
|
20
|
+
favicon,
|
|
21
|
+
domain,
|
|
22
|
+
}: DisplayLink) {
|
|
23
|
+
const [imgError, setImgError] = useState(false);
|
|
24
|
+
const [faviconError, setFaviconError] = useState(false);
|
|
25
|
+
|
|
26
|
+
const displayDomain = getDomain(url, domain);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<a
|
|
30
|
+
href={url}
|
|
31
|
+
target="_blank"
|
|
32
|
+
rel="noopener noreferrer"
|
|
33
|
+
aria-label={`Link: ${title}`}
|
|
34
|
+
>
|
|
35
|
+
<Card className="overflow-hidden hover:bg-muted/50 transition-colors">
|
|
36
|
+
{image && !imgError && (
|
|
37
|
+
<div className="aspect-video overflow-hidden">
|
|
38
|
+
<img
|
|
39
|
+
src={image}
|
|
40
|
+
alt={title}
|
|
41
|
+
className="w-full h-full object-cover"
|
|
42
|
+
loading="lazy"
|
|
43
|
+
decoding="async"
|
|
44
|
+
onError={() => setImgError(true)}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<div className="p-3 space-y-1">
|
|
50
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
51
|
+
{favicon && !faviconError ? (
|
|
52
|
+
<img
|
|
53
|
+
src={favicon}
|
|
54
|
+
alt=""
|
|
55
|
+
className="w-3 h-3 rounded-sm"
|
|
56
|
+
onError={() => setFaviconError(true)}
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
/>
|
|
59
|
+
) : (
|
|
60
|
+
<Globe size={12} aria-hidden="true" className="shrink-0" />
|
|
61
|
+
)}
|
|
62
|
+
<span>{displayDomain}</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<p className="font-medium text-foreground text-sm">{title}</p>
|
|
66
|
+
|
|
67
|
+
{description && (
|
|
68
|
+
<p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</Card>
|
|
72
|
+
</a>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
import type { DisplayMap } from "./sdk-types.js";
|
|
2
|
-
import { MapPin } from "lucide-react";
|
|
3
|
-
import { Card } from "../ui/card.js";
|
|
4
|
-
import { Separator } from "../ui/separator.js";
|
|
5
|
-
|
|
6
|
-
function buildOsmUrl(pins: DisplayMap["pins"], zoom: number): string {
|
|
7
|
-
if (pins.length === 0) {
|
|
8
|
-
return `https://www.openstreetmap.org/export/embed.html?bbox=-180,-90,180,90&layer=mapnik`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const lat = pins.reduce((acc, p) => acc + p.lat, 0) / pins.length;
|
|
12
|
-
const lng = pins.reduce((acc, p) => acc + p.lng, 0) / pins.length;
|
|
13
|
-
|
|
14
|
-
const firstPin = pins[0]!;
|
|
15
|
-
const markerParam =
|
|
16
|
-
pins.length === 1
|
|
17
|
-
? `&mlat=${firstPin.lat}&mlon=${firstPin.lng}`
|
|
18
|
-
: "";
|
|
19
|
-
|
|
20
|
-
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}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function MapViewRenderer({ title, pins, zoom }: DisplayMap) {
|
|
24
|
-
const osmUrl = buildOsmUrl(pins, zoom);
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<Card className="overflow-hidden">
|
|
28
|
-
{title && (
|
|
29
|
-
<div className="px-4 py-3">
|
|
30
|
-
<h3 className="font-medium text-sm text-foreground">{title}</h3>
|
|
31
|
-
</div>
|
|
32
|
-
)}
|
|
33
|
-
|
|
34
|
-
<div className="relative aspect-video bg-muted text-muted-foreground overflow-hidden">
|
|
35
|
-
<iframe
|
|
36
|
-
src={osmUrl}
|
|
37
|
-
className="w-full h-full border-0"
|
|
38
|
-
title={title ?? "Mapa OpenStreetMap"}
|
|
39
|
-
loading="lazy"
|
|
40
|
-
referrerPolicy="no-referrer"
|
|
41
|
-
sandbox="allow-scripts allow-same-origin"
|
|
42
|
-
/>
|
|
43
|
-
<div className="absolute inset-0 pointer-events-none flex items-center justify-center opacity-10">
|
|
44
|
-
<MapPin className="h-10 w-10" />
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
{pins.length > 0 && (
|
|
49
|
-
<>
|
|
50
|
-
<Separator />
|
|
51
|
-
<ul className="p-3 space-y-2" aria-label="Locais no mapa">
|
|
52
|
-
{pins.map((pin, i) => (
|
|
53
|
-
<li key={i} className="flex items-start gap-2">
|
|
54
|
-
<MapPin className="h-4 w-4 shrink-0 text-primary mt-0.5" aria-hidden="true" />
|
|
55
|
-
<span className="flex flex-col min-w-0">
|
|
56
|
-
{pin.label && (
|
|
57
|
-
<span className="font-medium text-sm text-foreground">{pin.label}</span>
|
|
58
|
-
)}
|
|
59
|
-
{pin.address && (
|
|
60
|
-
<span className="text-xs text-muted-foreground">{pin.address}</span>
|
|
61
|
-
)}
|
|
62
|
-
{!pin.label && !pin.address && (
|
|
63
|
-
<span className="text-xs text-muted-foreground font-mono">
|
|
64
|
-
{pin.lat.toFixed(4)}, {pin.lng.toFixed(4)}
|
|
65
|
-
</span>
|
|
66
|
-
)}
|
|
67
|
-
</span>
|
|
68
|
-
</li>
|
|
69
|
-
))}
|
|
70
|
-
</ul>
|
|
71
|
-
</>
|
|
72
|
-
)}
|
|
73
|
-
</Card>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
1
|
+
import type { DisplayMap } from "./sdk-types.js";
|
|
2
|
+
import { MapPin } from "lucide-react";
|
|
3
|
+
import { Card } from "../ui/card.js";
|
|
4
|
+
import { Separator } from "../ui/separator.js";
|
|
5
|
+
|
|
6
|
+
function buildOsmUrl(pins: DisplayMap["pins"], zoom: number): string {
|
|
7
|
+
if (pins.length === 0) {
|
|
8
|
+
return `https://www.openstreetmap.org/export/embed.html?bbox=-180,-90,180,90&layer=mapnik`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const lat = pins.reduce((acc, p) => acc + p.lat, 0) / pins.length;
|
|
12
|
+
const lng = pins.reduce((acc, p) => acc + p.lng, 0) / pins.length;
|
|
13
|
+
|
|
14
|
+
const firstPin = pins[0]!;
|
|
15
|
+
const markerParam =
|
|
16
|
+
pins.length === 1
|
|
17
|
+
? `&mlat=${firstPin.lat}&mlon=${firstPin.lng}`
|
|
18
|
+
: "";
|
|
19
|
+
|
|
20
|
+
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}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function MapViewRenderer({ title, pins, zoom }: DisplayMap) {
|
|
24
|
+
const osmUrl = buildOsmUrl(pins, zoom);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Card className="overflow-hidden">
|
|
28
|
+
{title && (
|
|
29
|
+
<div className="px-4 py-3">
|
|
30
|
+
<h3 className="font-medium text-sm text-foreground">{title}</h3>
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
|
|
34
|
+
<div className="relative aspect-video bg-muted text-muted-foreground overflow-hidden">
|
|
35
|
+
<iframe
|
|
36
|
+
src={osmUrl}
|
|
37
|
+
className="w-full h-full border-0"
|
|
38
|
+
title={title ?? "Mapa OpenStreetMap"}
|
|
39
|
+
loading="lazy"
|
|
40
|
+
referrerPolicy="no-referrer"
|
|
41
|
+
sandbox="allow-scripts allow-same-origin"
|
|
42
|
+
/>
|
|
43
|
+
<div className="absolute inset-0 pointer-events-none flex items-center justify-center opacity-10">
|
|
44
|
+
<MapPin className="h-10 w-10" />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{pins.length > 0 && (
|
|
49
|
+
<>
|
|
50
|
+
<Separator />
|
|
51
|
+
<ul className="p-3 space-y-2" aria-label="Locais no mapa">
|
|
52
|
+
{pins.map((pin, i) => (
|
|
53
|
+
<li key={i} className="flex items-start gap-2">
|
|
54
|
+
<MapPin className="h-4 w-4 shrink-0 text-primary mt-0.5" aria-hidden="true" />
|
|
55
|
+
<span className="flex flex-col min-w-0">
|
|
56
|
+
{pin.label && (
|
|
57
|
+
<span className="font-medium text-sm text-foreground">{pin.label}</span>
|
|
58
|
+
)}
|
|
59
|
+
{pin.address && (
|
|
60
|
+
<span className="text-xs text-muted-foreground">{pin.address}</span>
|
|
61
|
+
)}
|
|
62
|
+
{!pin.label && !pin.address && (
|
|
63
|
+
<span className="text-xs text-muted-foreground font-mono">
|
|
64
|
+
{pin.lat.toFixed(4)}, {pin.lng.toFixed(4)}
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
|
+
</span>
|
|
68
|
+
</li>
|
|
69
|
+
))}
|
|
70
|
+
</ul>
|
|
71
|
+
</>
|
|
72
|
+
)}
|
|
73
|
+
</Card>
|
|
74
|
+
);
|
|
75
|
+
}
|