xertica-ui 1.0.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/App.tsx +182 -0
- package/README.md +330 -0
- package/assets/xertica-logo.svg +38 -0
- package/assets/xertica-x-logo.svg +21 -0
- package/bin/cli.ts +193 -0
- package/components/AssistenteXertica.tsx +2003 -0
- package/components/AudioPlayer.tsx +203 -0
- package/components/CodeBlock.tsx +242 -0
- package/components/DocumentEditor.tsx +504 -0
- package/components/ForgotPasswordPage.tsx +170 -0
- package/components/FormattedDocument.tsx +87 -0
- package/components/HomeContent.tsx +123 -0
- package/components/HomePage.tsx +70 -0
- package/components/LanguageSelector.tsx +54 -0
- package/components/LoginPage.tsx +199 -0
- package/components/MarkdownMessage.tsx +62 -0
- package/components/ModernChatInput.tsx +502 -0
- package/components/PodcastPlayer.tsx +409 -0
- package/components/ResetPasswordPage.tsx +234 -0
- package/components/Sidebar.tsx +489 -0
- package/components/TemplateContent.tsx +629 -0
- package/components/TemplatePage.tsx +70 -0
- package/components/ThemeToggle.tsx +65 -0
- package/components/VerifyEmailPage.tsx +187 -0
- package/components/XerticaLogo.tsx +69 -0
- package/components/XerticaOrbe.tsx +1339 -0
- package/components/XerticaXLogo.tsx +53 -0
- package/components/examples/DrawingMapExample.tsx +530 -0
- package/components/examples/FilterableMapExample.tsx +380 -0
- package/components/examples/LocationPickerExample.tsx +330 -0
- package/components/examples/MapExamples.tsx +280 -0
- package/components/examples/MapShowcase.tsx +446 -0
- package/components/examples/RouteMapExamples.tsx +329 -0
- package/components/examples/SimpleFilterableMap.tsx +192 -0
- package/components/examples/index.ts +52 -0
- package/components/figma/ImageWithFallback.tsx +27 -0
- package/components/index.ts +44 -0
- package/components/media/AudioPlayer.tsx +278 -0
- package/components/media/FloatingMediaWrapper.tsx +166 -0
- package/components/media/VideoPlayer.tsx +285 -0
- package/components/ui/accordion.tsx +66 -0
- package/components/ui/alert-dialog.tsx +159 -0
- package/components/ui/alert.tsx +91 -0
- package/components/ui/aspect-ratio.tsx +11 -0
- package/components/ui/avatar.tsx +65 -0
- package/components/ui/badge.tsx +55 -0
- package/components/ui/breadcrumb.tsx +109 -0
- package/components/ui/button.tsx +78 -0
- package/components/ui/calendar.tsx +235 -0
- package/components/ui/card.tsx +92 -0
- package/components/ui/carousel.tsx +241 -0
- package/components/ui/chart.tsx +353 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/collapsible.tsx +33 -0
- package/components/ui/command.tsx +177 -0
- package/components/ui/context-menu.tsx +252 -0
- package/components/ui/dialog.tsx +138 -0
- package/components/ui/drawer.tsx +134 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/empty.tsx +90 -0
- package/components/ui/file-upload.tsx +152 -0
- package/components/ui/form.tsx +195 -0
- package/components/ui/google-maps-loader.tsx +379 -0
- package/components/ui/hover-card.tsx +44 -0
- package/components/ui/index.ts +242 -0
- package/components/ui/input-otp.tsx +77 -0
- package/components/ui/input.tsx +38 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/map-config.ts +12 -0
- package/components/ui/map-layers.tsx +129 -0
- package/components/ui/map.exports.ts +31 -0
- package/components/ui/map.tsx +412 -0
- package/components/ui/menubar.tsx +276 -0
- package/components/ui/navigation-menu.tsx +162 -0
- package/components/ui/notification-badge.tsx +61 -0
- package/components/ui/page-header.tsx +229 -0
- package/components/ui/pagination.tsx +127 -0
- package/components/ui/popover.tsx +48 -0
- package/components/ui/progress.tsx +31 -0
- package/components/ui/radio-group.tsx +56 -0
- package/components/ui/rating.tsx +102 -0
- package/components/ui/resizable.tsx +405 -0
- package/components/ui/route-map.tsx +246 -0
- package/components/ui/scroll-area.tsx +58 -0
- package/components/ui/search.tsx +70 -0
- package/components/ui/select.tsx +176 -0
- package/components/ui/separator.tsx +28 -0
- package/components/ui/sheet.tsx +138 -0
- package/components/ui/sidebar.tsx +726 -0
- package/components/ui/simple-map.tsx +92 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/slider.tsx +58 -0
- package/components/ui/sonner.tsx +77 -0
- package/components/ui/stats-card.tsx +84 -0
- package/components/ui/stepper.tsx +126 -0
- package/components/ui/switch.tsx +34 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/tabs.tsx +66 -0
- package/components/ui/textarea.tsx +26 -0
- package/components/ui/timeline.tsx +140 -0
- package/components/ui/toggle-group.tsx +71 -0
- package/components/ui/toggle.tsx +46 -0
- package/components/ui/tooltip.tsx +61 -0
- package/components/ui/tree-view.tsx +123 -0
- package/components/ui/use-mobile.ts +24 -0
- package/components/ui/utils.ts +6 -0
- package/components/ui/xertica-assistant.tsx +1420 -0
- package/contexts/ApiKeyContext.tsx +123 -0
- package/contexts/AssistenteContext.tsx +118 -0
- package/contexts/BrandColorsContext.tsx +551 -0
- package/contexts/LanguageContext.tsx +36 -0
- package/contexts/ThemeContext.tsx +85 -0
- package/dist/cli.js +20922 -0
- package/eslint.config.js +41 -0
- package/guidelines/Guidelines.md +61 -0
- package/hooks/useTheme.ts +4 -0
- package/imports/Podcast.tsx +389 -0
- package/imports/XerticaAi.tsx +46 -0
- package/imports/XerticaX.tsx +20 -0
- package/imports/svg-aueiaqngck.ts +11 -0
- package/imports/svg-v9krss1ozd.ts +16 -0
- package/imports/svg-vhrdofe3qe.ts +5 -0
- package/index.css +4448 -0
- package/index.html +14 -0
- package/main.tsx +10 -0
- package/package.json +119 -0
- package/postcss.config.js +6 -0
- package/routes.tsx +33 -0
- package/styles/globals.css +15 -0
- package/styles/xertica/app-overrides/chat.css +61 -0
- package/styles/xertica/app-overrides/scrollbar.css +33 -0
- package/styles/xertica/base.css +70 -0
- package/styles/xertica/integrations/google-maps.css +76 -0
- package/styles/xertica/integrations/sonner.css +73 -0
- package/styles/xertica/theme-map.css +88 -0
- package/styles/xertica/tokens.css +190 -0
- package/tsconfig.json +31 -0
- package/tsconfig.node.json +10 -0
- package/utils/gemini.ts +140 -0
- package/vite-env.d.ts +12 -0
- package/vite.config.ts +36 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
PlayCircle,
|
|
4
|
+
PauseCircle,
|
|
5
|
+
Volume2,
|
|
6
|
+
RotateCcw,
|
|
7
|
+
Gauge,
|
|
8
|
+
Info,
|
|
9
|
+
RefreshCw,
|
|
10
|
+
Download,
|
|
11
|
+
X,
|
|
12
|
+
ExternalLink,
|
|
13
|
+
VolumeX,
|
|
14
|
+
Radio
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
import { Button } from './ui/button';
|
|
17
|
+
import { Slider } from './ui/slider';
|
|
18
|
+
import {
|
|
19
|
+
Tooltip,
|
|
20
|
+
TooltipContent,
|
|
21
|
+
TooltipProvider,
|
|
22
|
+
TooltipTrigger
|
|
23
|
+
} from './ui/tooltip';
|
|
24
|
+
import { cn } from './ui/utils';
|
|
25
|
+
|
|
26
|
+
interface AudioPlayerProps {
|
|
27
|
+
isOpen: boolean;
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
sidebarExpanded: boolean;
|
|
30
|
+
title?: string;
|
|
31
|
+
subtitle?: string;
|
|
32
|
+
duration?: number; // in seconds
|
|
33
|
+
currentTime?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function AudioPlayer({
|
|
37
|
+
isOpen,
|
|
38
|
+
onClose,
|
|
39
|
+
sidebarExpanded,
|
|
40
|
+
title = "Processo 50002396220258210104",
|
|
41
|
+
subtitle = "Podcast atualizado até Evento 26",
|
|
42
|
+
duration = 1200, // 20:00
|
|
43
|
+
currentTime = 0
|
|
44
|
+
}: AudioPlayerProps) {
|
|
45
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
46
|
+
const [current, setCurrent] = useState(currentTime);
|
|
47
|
+
const [volume, setVolume] = useState([80]);
|
|
48
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
49
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (isOpen) {
|
|
53
|
+
setIsVisible(true);
|
|
54
|
+
} else {
|
|
55
|
+
const timer = setTimeout(() => setIsVisible(false), 300);
|
|
56
|
+
return () => clearTimeout(timer);
|
|
57
|
+
}
|
|
58
|
+
}, [isOpen]);
|
|
59
|
+
|
|
60
|
+
if (!isVisible) return null;
|
|
61
|
+
|
|
62
|
+
const formatTime = (seconds: number) => {
|
|
63
|
+
const mins = Math.floor(seconds / 60);
|
|
64
|
+
const secs = Math.floor(seconds % 60);
|
|
65
|
+
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const togglePlay = () => setIsPlaying(!isPlaying);
|
|
69
|
+
const toggleMute = () => setIsMuted(!isMuted);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div
|
|
73
|
+
className={cn(
|
|
74
|
+
"fixed bottom-0 right-0 z-50 border-t border-border backdrop-blur-md transition-all duration-300 ease-in-out",
|
|
75
|
+
"bg-card/95 text-card-foreground shadow-[var(--elevation-sm)]",
|
|
76
|
+
sidebarExpanded ? "left-0 md:left-64" : "left-0 md:left-20",
|
|
77
|
+
isOpen ? "translate-y-0 opacity-100" : "translate-y-full opacity-0"
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<div className="h-[72px] px-4 md:px-6 flex items-center justify-between gap-4">
|
|
81
|
+
|
|
82
|
+
{/* Left: Info */}
|
|
83
|
+
<div className="flex flex-col min-w-0 w-[180px] lg:w-[240px] shrink-0">
|
|
84
|
+
<div className="flex items-center gap-2 mb-0.5">
|
|
85
|
+
<Radio className="w-3 h-3 text-[var(--chart-4)] animate-pulse" />
|
|
86
|
+
<h4 className="font-medium text-sm md:text-base truncate" title={title}>
|
|
87
|
+
{title}
|
|
88
|
+
</h4>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
91
|
+
<span className="truncate">{subtitle}</span>
|
|
92
|
+
<ExternalLink className="w-3 h-3 cursor-pointer hover:text-foreground ml-1 opacity-70 hover:opacity-100" />
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Center: Controls */}
|
|
97
|
+
<div className="flex-1 flex items-center justify-center gap-4 max-w-3xl">
|
|
98
|
+
<button
|
|
99
|
+
onClick={togglePlay}
|
|
100
|
+
className="text-muted-foreground hover:text-foreground transition-colors focus:outline-none hover:scale-105 active:scale-95 transform duration-100"
|
|
101
|
+
>
|
|
102
|
+
{isPlaying ? (
|
|
103
|
+
<PauseCircle className="w-10 h-10" strokeWidth={1.5} />
|
|
104
|
+
) : (
|
|
105
|
+
<PlayCircle className="w-10 h-10" strokeWidth={1.5} />
|
|
106
|
+
)}
|
|
107
|
+
</button>
|
|
108
|
+
|
|
109
|
+
<div className="flex-1 flex items-center gap-3">
|
|
110
|
+
<span className="text-xs text-muted-foreground font-mono shrink-0 w-10 text-right">
|
|
111
|
+
{formatTime(current)}
|
|
112
|
+
</span>
|
|
113
|
+
<Slider
|
|
114
|
+
defaultValue={[0]}
|
|
115
|
+
max={duration}
|
|
116
|
+
value={[current]}
|
|
117
|
+
onValueChange={(val) => setCurrent(val[0])}
|
|
118
|
+
className="cursor-pointer"
|
|
119
|
+
/>
|
|
120
|
+
<span className="text-xs text-muted-foreground font-mono shrink-0 w-10">
|
|
121
|
+
{formatTime(duration)}
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Right: Actions */}
|
|
127
|
+
<div className="flex items-center gap-1 md:gap-3 shrink-0">
|
|
128
|
+
{/* Volume */}
|
|
129
|
+
<div className="hidden lg:flex items-center gap-2 w-28 mr-2 group">
|
|
130
|
+
<button onClick={toggleMute} className="text-muted-foreground hover:text-foreground">
|
|
131
|
+
{isMuted || volume[0] === 0 ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
|
|
132
|
+
</button>
|
|
133
|
+
<Slider
|
|
134
|
+
defaultValue={[80]}
|
|
135
|
+
max={100}
|
|
136
|
+
value={volume}
|
|
137
|
+
onValueChange={setVolume}
|
|
138
|
+
className="w-full opacity-60 group-hover:opacity-100 transition-opacity"
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div className="flex items-center gap-1">
|
|
143
|
+
<TooltipProvider>
|
|
144
|
+
<Tooltip>
|
|
145
|
+
<TooltipTrigger asChild>
|
|
146
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
147
|
+
<RotateCcw className="w-4 h-4" />
|
|
148
|
+
</Button>
|
|
149
|
+
</TooltipTrigger>
|
|
150
|
+
<TooltipContent>Reiniciar</TooltipContent>
|
|
151
|
+
</Tooltip>
|
|
152
|
+
</TooltipProvider>
|
|
153
|
+
|
|
154
|
+
<TooltipProvider>
|
|
155
|
+
<Tooltip>
|
|
156
|
+
<TooltipTrigger asChild>
|
|
157
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
158
|
+
<Gauge className="w-4 h-4" />
|
|
159
|
+
</Button>
|
|
160
|
+
</TooltipTrigger>
|
|
161
|
+
<TooltipContent>Velocidade (1.0x)</TooltipContent>
|
|
162
|
+
</Tooltip>
|
|
163
|
+
</TooltipProvider>
|
|
164
|
+
|
|
165
|
+
<TooltipProvider>
|
|
166
|
+
<Tooltip>
|
|
167
|
+
<TooltipTrigger asChild>
|
|
168
|
+
<Button variant="ghost" size="icon" className="text-[var(--chart-3)] hover:text-[var(--chart-3)]/80 hover:bg-accent rounded-full h-9 w-9">
|
|
169
|
+
<Info className="w-4 h-4 fill-current" />
|
|
170
|
+
</Button>
|
|
171
|
+
</TooltipTrigger>
|
|
172
|
+
<TooltipContent className="max-w-[300px] bg-popover text-popover-foreground">
|
|
173
|
+
<p>Identificamos a entrada de novos eventos neste processo desde a última atualização deste podcast.</p>
|
|
174
|
+
</TooltipContent>
|
|
175
|
+
</Tooltip>
|
|
176
|
+
</TooltipProvider>
|
|
177
|
+
|
|
178
|
+
<TooltipProvider>
|
|
179
|
+
<Tooltip>
|
|
180
|
+
<TooltipTrigger asChild>
|
|
181
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
182
|
+
<RefreshCw className="w-4 h-4" />
|
|
183
|
+
</Button>
|
|
184
|
+
</TooltipTrigger>
|
|
185
|
+
<TooltipContent>Atualizar Versão</TooltipContent>
|
|
186
|
+
</Tooltip>
|
|
187
|
+
</TooltipProvider>
|
|
188
|
+
|
|
189
|
+
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
190
|
+
<Download className="w-4 h-4" />
|
|
191
|
+
</Button>
|
|
192
|
+
|
|
193
|
+
<div className="w-px h-8 bg-border mx-1" />
|
|
194
|
+
|
|
195
|
+
<Button onClick={onClose} variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground hover:bg-accent rounded-full h-9 w-9">
|
|
196
|
+
<X className="w-5 h-5" />
|
|
197
|
+
</Button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Check, Copy } from 'lucide-react';
|
|
3
|
+
import { Button } from './ui/button';
|
|
4
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
5
|
+
|
|
6
|
+
// Elegant custom theme inspired by Xertica.ai design system
|
|
7
|
+
const elegantTheme = {
|
|
8
|
+
'code[class*="language-"]': {
|
|
9
|
+
color: 'var(--foreground)',
|
|
10
|
+
fontFamily: 'var(--font-mono)',
|
|
11
|
+
fontSize: '0.875rem',
|
|
12
|
+
textAlign: 'left' as const,
|
|
13
|
+
whiteSpace: 'pre' as const,
|
|
14
|
+
wordSpacing: 'normal',
|
|
15
|
+
wordBreak: 'normal',
|
|
16
|
+
lineHeight: '1.6',
|
|
17
|
+
tabSize: 2,
|
|
18
|
+
hyphens: 'none' as const,
|
|
19
|
+
},
|
|
20
|
+
'pre[class*="language-"]': {
|
|
21
|
+
color: 'var(--foreground)',
|
|
22
|
+
fontFamily: 'var(--font-mono)',
|
|
23
|
+
fontSize: '0.875rem',
|
|
24
|
+
textAlign: 'left' as const,
|
|
25
|
+
whiteSpace: 'pre' as const,
|
|
26
|
+
wordSpacing: 'normal',
|
|
27
|
+
wordBreak: 'normal',
|
|
28
|
+
lineHeight: '1.6',
|
|
29
|
+
tabSize: 2,
|
|
30
|
+
hyphens: 'none' as const,
|
|
31
|
+
padding: '1rem',
|
|
32
|
+
margin: 0,
|
|
33
|
+
overflow: 'auto',
|
|
34
|
+
background: 'transparent',
|
|
35
|
+
},
|
|
36
|
+
'comment': {
|
|
37
|
+
color: 'var(--muted-foreground)',
|
|
38
|
+
fontStyle: 'italic',
|
|
39
|
+
},
|
|
40
|
+
'prolog': {
|
|
41
|
+
color: 'var(--muted-foreground)',
|
|
42
|
+
},
|
|
43
|
+
'doctype': {
|
|
44
|
+
color: 'var(--muted-foreground)',
|
|
45
|
+
},
|
|
46
|
+
'cdata': {
|
|
47
|
+
color: 'var(--muted-foreground)',
|
|
48
|
+
},
|
|
49
|
+
'punctuation': {
|
|
50
|
+
color: 'var(--muted-foreground)',
|
|
51
|
+
},
|
|
52
|
+
'property': {
|
|
53
|
+
color: 'var(--chart-4)',
|
|
54
|
+
},
|
|
55
|
+
'tag': {
|
|
56
|
+
color: 'var(--chart-1)',
|
|
57
|
+
},
|
|
58
|
+
'boolean': {
|
|
59
|
+
color: 'var(--chart-5)',
|
|
60
|
+
},
|
|
61
|
+
'number': {
|
|
62
|
+
color: 'var(--chart-5)',
|
|
63
|
+
},
|
|
64
|
+
'constant': {
|
|
65
|
+
color: 'var(--chart-5)',
|
|
66
|
+
},
|
|
67
|
+
'symbol': {
|
|
68
|
+
color: 'var(--chart-5)',
|
|
69
|
+
},
|
|
70
|
+
'deleted': {
|
|
71
|
+
color: 'var(--chart-5)',
|
|
72
|
+
},
|
|
73
|
+
'selector': {
|
|
74
|
+
color: 'var(--chart-2)',
|
|
75
|
+
},
|
|
76
|
+
'attr-name': {
|
|
77
|
+
color: 'var(--chart-4)',
|
|
78
|
+
},
|
|
79
|
+
'string': {
|
|
80
|
+
color: 'var(--chart-2)',
|
|
81
|
+
},
|
|
82
|
+
'char': {
|
|
83
|
+
color: 'var(--chart-2)',
|
|
84
|
+
},
|
|
85
|
+
'builtin': {
|
|
86
|
+
color: 'var(--chart-4)',
|
|
87
|
+
},
|
|
88
|
+
'inserted': {
|
|
89
|
+
color: 'var(--chart-2)',
|
|
90
|
+
},
|
|
91
|
+
'operator': {
|
|
92
|
+
color: 'var(--primary)',
|
|
93
|
+
},
|
|
94
|
+
'entity': {
|
|
95
|
+
color: 'var(--chart-4)',
|
|
96
|
+
},
|
|
97
|
+
'url': {
|
|
98
|
+
color: 'var(--chart-1)',
|
|
99
|
+
},
|
|
100
|
+
'.language-css .token.string': {
|
|
101
|
+
color: 'var(--chart-2)',
|
|
102
|
+
},
|
|
103
|
+
'.style .token.string': {
|
|
104
|
+
color: 'var(--chart-2)',
|
|
105
|
+
},
|
|
106
|
+
'atrule': {
|
|
107
|
+
color: 'var(--chart-3)',
|
|
108
|
+
},
|
|
109
|
+
'attr-value': {
|
|
110
|
+
color: 'var(--chart-2)',
|
|
111
|
+
},
|
|
112
|
+
'keyword': {
|
|
113
|
+
color: 'var(--chart-3)',
|
|
114
|
+
},
|
|
115
|
+
'function': {
|
|
116
|
+
color: 'var(--chart-1)',
|
|
117
|
+
},
|
|
118
|
+
'class-name': {
|
|
119
|
+
color: 'var(--chart-4)',
|
|
120
|
+
},
|
|
121
|
+
'regex': {
|
|
122
|
+
color: 'var(--chart-5)',
|
|
123
|
+
},
|
|
124
|
+
'important': {
|
|
125
|
+
color: 'var(--chart-5)',
|
|
126
|
+
fontWeight: 'bold',
|
|
127
|
+
},
|
|
128
|
+
'variable': {
|
|
129
|
+
color: 'var(--chart-5)',
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
interface CodeBlockProps {
|
|
134
|
+
code: string;
|
|
135
|
+
language?: 'typescript' | 'tsx' | 'css' | 'bash' | 'jsx';
|
|
136
|
+
filename?: string;
|
|
137
|
+
showLineNumbers?: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const CodeBlock = ({
|
|
141
|
+
code,
|
|
142
|
+
language = 'tsx',
|
|
143
|
+
filename,
|
|
144
|
+
showLineNumbers = false
|
|
145
|
+
}: CodeBlockProps) => {
|
|
146
|
+
const [copied, setCopied] = useState(false);
|
|
147
|
+
|
|
148
|
+
const handleCopy = async () => {
|
|
149
|
+
try {
|
|
150
|
+
// Try modern Clipboard API first (with permission check)
|
|
151
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
152
|
+
try {
|
|
153
|
+
await navigator.clipboard.writeText(code);
|
|
154
|
+
} catch (clipboardError: any) {
|
|
155
|
+
// If clipboard API fails due to permissions, fall back to execCommand
|
|
156
|
+
if (clipboardError.name === 'NotAllowedError' || clipboardError.message.includes('permissions policy')) {
|
|
157
|
+
throw new Error('Clipboard permission denied, falling back');
|
|
158
|
+
}
|
|
159
|
+
throw clipboardError;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
throw new Error('Clipboard API not available');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setCopied(true);
|
|
166
|
+
setTimeout(() => setCopied(false), 2000);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
// Fallback for browsers that don't support Clipboard API or when permissions are denied
|
|
169
|
+
try {
|
|
170
|
+
const textArea = document.createElement('textarea');
|
|
171
|
+
textArea.value = code;
|
|
172
|
+
textArea.style.position = 'fixed';
|
|
173
|
+
textArea.style.left = '-999999px';
|
|
174
|
+
textArea.style.top = '-999999px';
|
|
175
|
+
document.body.appendChild(textArea);
|
|
176
|
+
textArea.focus();
|
|
177
|
+
textArea.select();
|
|
178
|
+
|
|
179
|
+
const successful = document.execCommand('copy');
|
|
180
|
+
document.body.removeChild(textArea);
|
|
181
|
+
|
|
182
|
+
if (successful) {
|
|
183
|
+
setCopied(true);
|
|
184
|
+
setTimeout(() => setCopied(false), 2000);
|
|
185
|
+
} else {
|
|
186
|
+
console.error('Failed to copy text: execCommand returned false');
|
|
187
|
+
}
|
|
188
|
+
} catch (fallbackErr) {
|
|
189
|
+
console.error('All copy methods failed:', fallbackErr);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Ensure code is a string
|
|
195
|
+
const codeString = typeof code === 'string' ? code : String(code || '');
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="relative group rounded-[var(--radius)] border border-border overflow-hidden bg-muted/30">
|
|
199
|
+
{filename && (
|
|
200
|
+
<div className="flex items-center justify-between px-4 py-2 bg-muted border-b border-border">
|
|
201
|
+
<span className="text-xs text-muted-foreground font-mono">{filename}</span>
|
|
202
|
+
<span className="text-xs text-muted-foreground uppercase">{language}</span>
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
<div className="relative">
|
|
207
|
+
<Button
|
|
208
|
+
variant="ghost"
|
|
209
|
+
size="icon"
|
|
210
|
+
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 bg-background/80 hover:bg-background"
|
|
211
|
+
onClick={handleCopy}
|
|
212
|
+
>
|
|
213
|
+
{copied ? (
|
|
214
|
+
<Check className="w-4 h-4 text-[var(--toast-success-icon)]" />
|
|
215
|
+
) : (
|
|
216
|
+
<Copy className="w-4 h-4" />
|
|
217
|
+
)}
|
|
218
|
+
</Button>
|
|
219
|
+
|
|
220
|
+
<div className="overflow-x-auto w-full max-w-full">
|
|
221
|
+
<SyntaxHighlighter
|
|
222
|
+
language={language}
|
|
223
|
+
style={elegantTheme}
|
|
224
|
+
showLineNumbers={showLineNumbers}
|
|
225
|
+
wrapLines={true}
|
|
226
|
+
customStyle={{
|
|
227
|
+
margin: 0,
|
|
228
|
+
padding: '1rem',
|
|
229
|
+
background: 'transparent',
|
|
230
|
+
fontSize: 'inherit',
|
|
231
|
+
}}
|
|
232
|
+
codeTagProps={{
|
|
233
|
+
className: "text-[length:inherit]"
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
{codeString}
|
|
237
|
+
</SyntaxHighlighter>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
};
|