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,278 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { Play, Pause, Volume2, VolumeX, Maximize2 } from 'lucide-react';
|
|
3
|
+
import { Slider } from '../ui/slider';
|
|
4
|
+
import { Button } from '../ui/button';
|
|
5
|
+
import { FloatingMediaWrapper } from './FloatingMediaWrapper';
|
|
6
|
+
import { cn } from '../ui/utils';
|
|
7
|
+
|
|
8
|
+
export interface AudioPlayerProps {
|
|
9
|
+
src: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
artist?: string;
|
|
12
|
+
coverArt?: string;
|
|
13
|
+
autoPlay?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function AudioPlayer({
|
|
18
|
+
src,
|
|
19
|
+
title = "Audio",
|
|
20
|
+
artist,
|
|
21
|
+
autoPlay = false,
|
|
22
|
+
className
|
|
23
|
+
}: AudioPlayerProps) {
|
|
24
|
+
const audioRef = useRef<HTMLAudioElement>(null);
|
|
25
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
26
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
27
|
+
const [progress, setProgress] = useState(0);
|
|
28
|
+
const [volume, setVolume] = useState(1);
|
|
29
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
30
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
31
|
+
const [duration, setDuration] = useState(0);
|
|
32
|
+
const [isFloating, setIsFloating] = useState(false);
|
|
33
|
+
const [isManualFloating, setIsManualFloating] = useState(false);
|
|
34
|
+
|
|
35
|
+
// Wrapper for setIsFloating to handle scroll-on-restore and sync state
|
|
36
|
+
const handleSetFloating = (floating: boolean) => {
|
|
37
|
+
// Capture precise time before switching
|
|
38
|
+
if (audioRef.current) {
|
|
39
|
+
setCurrentTime(audioRef.current.currentTime);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setIsFloating(floating);
|
|
43
|
+
|
|
44
|
+
if (!floating) {
|
|
45
|
+
setIsManualFloating(false);
|
|
46
|
+
if (containerRef.current) {
|
|
47
|
+
containerRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Restore state when switching modes (remounting audio element)
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const audio = audioRef.current;
|
|
55
|
+
if (audio) {
|
|
56
|
+
// If we are switching modes, restore time and play state
|
|
57
|
+
if (Math.abs(audio.currentTime - currentTime) > 0.5) {
|
|
58
|
+
audio.currentTime = currentTime;
|
|
59
|
+
}
|
|
60
|
+
if (isPlaying) {
|
|
61
|
+
const playPromise = audio.play();
|
|
62
|
+
if (playPromise !== undefined) {
|
|
63
|
+
playPromise.catch(error => {
|
|
64
|
+
// Auto-play might be blocked or interrupted
|
|
65
|
+
console.log("Playback interrupted during switch:", error);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// We depend on isFloating to trigger this check when the DOM node is recreated
|
|
71
|
+
// But we also need to attach listeners to the new node
|
|
72
|
+
}, [isFloating]);
|
|
73
|
+
|
|
74
|
+
// Auto-float on scroll logic
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const container = containerRef.current;
|
|
77
|
+
if (!container) return;
|
|
78
|
+
|
|
79
|
+
const observer = new IntersectionObserver(
|
|
80
|
+
([entry]) => {
|
|
81
|
+
if (isPlaying && !entry.isIntersecting && !isFloating) {
|
|
82
|
+
// Sync time before floating
|
|
83
|
+
if (audioRef.current) setCurrentTime(audioRef.current.currentTime);
|
|
84
|
+
setIsFloating(true);
|
|
85
|
+
}
|
|
86
|
+
else if (entry.isIntersecting && isFloating && !isManualFloating) {
|
|
87
|
+
// Sync time before docking
|
|
88
|
+
if (audioRef.current) setCurrentTime(audioRef.current.currentTime);
|
|
89
|
+
handleSetFloating(false);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{ threshold: 0.2 }
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
observer.observe(container);
|
|
96
|
+
return () => observer.disconnect();
|
|
97
|
+
}, [isPlaying, isFloating, isManualFloating]);
|
|
98
|
+
|
|
99
|
+
// Main Audio Event Listeners
|
|
100
|
+
// We need to re-run this effect when isFloating changes because audioRef.current changes
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const audio = audioRef.current;
|
|
103
|
+
if (!audio) return;
|
|
104
|
+
|
|
105
|
+
const updateTime = () => {
|
|
106
|
+
setCurrentTime(audio.currentTime);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const updateDuration = () => setDuration(audio.duration);
|
|
110
|
+
const onPlay = () => setIsPlaying(true);
|
|
111
|
+
const onPause = () => setIsPlaying(false);
|
|
112
|
+
|
|
113
|
+
audio.addEventListener('timeupdate', updateTime);
|
|
114
|
+
audio.addEventListener('loadedmetadata', updateDuration);
|
|
115
|
+
audio.addEventListener('play', onPlay);
|
|
116
|
+
audio.addEventListener('pause', onPause);
|
|
117
|
+
|
|
118
|
+
// Initial volume set
|
|
119
|
+
audio.volume = volume;
|
|
120
|
+
audio.muted = isMuted;
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
audio.removeEventListener('timeupdate', updateTime);
|
|
124
|
+
audio.removeEventListener('loadedmetadata', updateDuration);
|
|
125
|
+
audio.removeEventListener('play', onPlay);
|
|
126
|
+
audio.removeEventListener('pause', onPause);
|
|
127
|
+
};
|
|
128
|
+
}, [isFloating]); // Depend on isFloating to re-attach listeners to new element
|
|
129
|
+
|
|
130
|
+
const togglePlay = () => {
|
|
131
|
+
if (audioRef.current) {
|
|
132
|
+
if (isPlaying) {
|
|
133
|
+
audioRef.current.pause();
|
|
134
|
+
} else {
|
|
135
|
+
audioRef.current.play();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleSeek = (value: number[]) => {
|
|
141
|
+
if (audioRef.current) {
|
|
142
|
+
audioRef.current.currentTime = value[0];
|
|
143
|
+
setCurrentTime(value[0]);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handleVolumeChange = (value: number[]) => {
|
|
148
|
+
const newVolume = value[0];
|
|
149
|
+
if (audioRef.current) {
|
|
150
|
+
audioRef.current.volume = newVolume;
|
|
151
|
+
setVolume(newVolume);
|
|
152
|
+
setIsMuted(newVolume === 0);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const formatTime = (time: number) => {
|
|
157
|
+
const minutes = Math.floor(time / 60);
|
|
158
|
+
const seconds = Math.floor(time % 60);
|
|
159
|
+
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const PlayerControls = ({ isCompact = false }) => (
|
|
163
|
+
<div className={cn("flex items-center gap-3 w-full", isCompact ? "px-3 py-1" : "px-4 py-3")}>
|
|
164
|
+
<Button
|
|
165
|
+
onClick={togglePlay}
|
|
166
|
+
size="icon"
|
|
167
|
+
variant="outline"
|
|
168
|
+
className={cn(
|
|
169
|
+
"shrink-0 rounded-full border-primary/20 text-primary hover:bg-primary/5 hover:text-primary hover:border-primary/50 transition-colors",
|
|
170
|
+
isCompact ? "h-8 w-8" : "h-10 w-10"
|
|
171
|
+
)}
|
|
172
|
+
>
|
|
173
|
+
{isPlaying ? <Pause className="w-4 h-4 fill-current" /> : <Play className="w-4 h-4 fill-current ml-0.5" />}
|
|
174
|
+
</Button>
|
|
175
|
+
|
|
176
|
+
<div className="flex-1 min-w-0 flex flex-col justify-center gap-1.5">
|
|
177
|
+
<div className="flex items-center justify-between text-xs leading-none">
|
|
178
|
+
<div className="flex items-center gap-2 truncate">
|
|
179
|
+
<span className="font-medium text-foreground truncate">{title}</span>
|
|
180
|
+
{artist && !isCompact && <span className="text-muted-foreground hidden sm:inline-block border-l pl-2 border-border truncate">{artist}</span>}
|
|
181
|
+
</div>
|
|
182
|
+
<div className="text-muted-foreground font-medium tabular-nums shrink-0 text-[10px] sm:text-xs">
|
|
183
|
+
{formatTime(currentTime)} / {formatTime(duration || 0)}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<Slider
|
|
188
|
+
value={[currentTime]}
|
|
189
|
+
max={duration || 100}
|
|
190
|
+
step={1}
|
|
191
|
+
onValueChange={handleSeek}
|
|
192
|
+
className="w-full"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
197
|
+
{!isCompact && (
|
|
198
|
+
<div className="hidden sm:flex items-center gap-2 group/volume mr-2">
|
|
199
|
+
<Button
|
|
200
|
+
variant="ghost"
|
|
201
|
+
size="icon"
|
|
202
|
+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
203
|
+
onClick={() => {
|
|
204
|
+
const newMuted = !isMuted;
|
|
205
|
+
setIsMuted(newMuted);
|
|
206
|
+
if (audioRef.current) audioRef.current.muted = newMuted;
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
{isMuted ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
|
|
210
|
+
</Button>
|
|
211
|
+
<div className="w-0 overflow-hidden group-hover/volume:w-20 transition-all duration-300">
|
|
212
|
+
<Slider
|
|
213
|
+
value={[isMuted ? 0 : volume]}
|
|
214
|
+
max={1}
|
|
215
|
+
step={0.01}
|
|
216
|
+
onValueChange={handleVolumeChange}
|
|
217
|
+
className="w-20"
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
|
|
223
|
+
{!isCompact && (
|
|
224
|
+
<div className="w-px h-6 bg-border mx-1 hidden sm:block" />
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{!isCompact && (
|
|
228
|
+
<Button
|
|
229
|
+
variant="ghost"
|
|
230
|
+
size="icon"
|
|
231
|
+
className="h-8 w-8 text-muted-foreground hover:text-foreground"
|
|
232
|
+
onClick={() => {
|
|
233
|
+
setIsManualFloating(true);
|
|
234
|
+
handleSetFloating(true);
|
|
235
|
+
}}
|
|
236
|
+
title="Modo Flutuante"
|
|
237
|
+
>
|
|
238
|
+
<Maximize2 className="w-4 h-4" />
|
|
239
|
+
</Button>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<div ref={containerRef} className={className}>
|
|
247
|
+
<FloatingMediaWrapper
|
|
248
|
+
isFloating={isFloating}
|
|
249
|
+
setIsFloating={handleSetFloating}
|
|
250
|
+
onCloseMedia={() => {
|
|
251
|
+
setIsPlaying(false);
|
|
252
|
+
setIsFloating(false);
|
|
253
|
+
if (audioRef.current) audioRef.current.pause();
|
|
254
|
+
}}
|
|
255
|
+
title={title}
|
|
256
|
+
aspectRatio={320/110}
|
|
257
|
+
minHeight={110}
|
|
258
|
+
minWidth={320}
|
|
259
|
+
className="w-full"
|
|
260
|
+
>
|
|
261
|
+
<div className={cn(
|
|
262
|
+
"bg-card w-full overflow-hidden flex flex-col justify-center",
|
|
263
|
+
isFloating
|
|
264
|
+
? "h-full bg-transparent text-primary-foreground [&_.text-muted-foreground]:text-primary-foreground/80 [&_.text-foreground]:text-primary-foreground [&_.text-primary]:text-primary-foreground [&_.border-primary\\/20]:border-primary-foreground/30 [&_.hover\\:bg-primary\\/5]:hover:bg-primary-foreground/10 [&_[data-slot=slider-track]]:bg-primary-foreground/20 [&_[data-slot=slider-range]]:bg-primary-foreground [&_[data-slot=slider-thumb]]:border-primary-foreground"
|
|
265
|
+
: "border rounded-md shadow-sm"
|
|
266
|
+
)}>
|
|
267
|
+
<audio
|
|
268
|
+
ref={audioRef}
|
|
269
|
+
src={src}
|
|
270
|
+
autoPlay={autoPlay}
|
|
271
|
+
className="hidden"
|
|
272
|
+
/>
|
|
273
|
+
<PlayerControls isCompact={isFloating} />
|
|
274
|
+
</div>
|
|
275
|
+
</FloatingMediaWrapper>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
import { Resizable } from 're-resizable';
|
|
5
|
+
import { X, GripHorizontal, Maximize2, Minimize2 } from 'lucide-react';
|
|
6
|
+
import { Button } from '../ui/button';
|
|
7
|
+
import { cn } from '../ui/utils';
|
|
8
|
+
|
|
9
|
+
interface FloatingMediaWrapperProps {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
isFloating: boolean;
|
|
12
|
+
setIsFloating: (floating: boolean) => void;
|
|
13
|
+
title?: string;
|
|
14
|
+
onClose?: () => void;
|
|
15
|
+
aspectRatio?: number;
|
|
16
|
+
className?: string;
|
|
17
|
+
minWidth?: number;
|
|
18
|
+
minHeight?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function FloatingMediaWrapper({
|
|
22
|
+
children,
|
|
23
|
+
isFloating,
|
|
24
|
+
setIsFloating,
|
|
25
|
+
title,
|
|
26
|
+
onClose,
|
|
27
|
+
aspectRatio = 16 / 9,
|
|
28
|
+
className,
|
|
29
|
+
minWidth = 320,
|
|
30
|
+
minHeight = 180,
|
|
31
|
+
}: FloatingMediaWrapperProps) {
|
|
32
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setIsMounted(true);
|
|
36
|
+
return () => setIsMounted(false);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
// Placeholder when content is floating
|
|
40
|
+
if (isFloating) {
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<div
|
|
44
|
+
className={cn(
|
|
45
|
+
"w-full rounded-[var(--radius-card)] bg-muted/20 border border-dashed border-muted-foreground/20 flex items-center justify-center flex-col gap-2 p-8 text-muted-foreground transition-all",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
style={{ aspectRatio }}
|
|
49
|
+
>
|
|
50
|
+
<Maximize2 className="w-8 h-8 opacity-50" />
|
|
51
|
+
<p className="text-sm font-medium">Reproduzindo em janela flutuante</p>
|
|
52
|
+
<Button variant="outline" size="sm" onClick={() => setIsFloating(false)}>
|
|
53
|
+
Restaurar para a página
|
|
54
|
+
</Button>
|
|
55
|
+
</div>
|
|
56
|
+
{isMounted && createPortal(
|
|
57
|
+
<FloatingContainer
|
|
58
|
+
onClose={() => setIsFloating(false)}
|
|
59
|
+
onCloseMedia={onClose}
|
|
60
|
+
title={title}
|
|
61
|
+
aspectRatio={aspectRatio}
|
|
62
|
+
minWidth={minWidth}
|
|
63
|
+
minHeight={minHeight}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</FloatingContainer>,
|
|
67
|
+
document.body
|
|
68
|
+
)}
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={cn("relative w-full overflow-hidden rounded-[var(--radius-card)]", className)}>
|
|
75
|
+
{children}
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface FloatingContainerProps {
|
|
81
|
+
children: React.ReactNode;
|
|
82
|
+
onClose: () => void;
|
|
83
|
+
onCloseMedia?: () => void;
|
|
84
|
+
title?: string;
|
|
85
|
+
aspectRatio: number;
|
|
86
|
+
minWidth: number;
|
|
87
|
+
minHeight: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function FloatingContainer({
|
|
91
|
+
children,
|
|
92
|
+
onClose,
|
|
93
|
+
onCloseMedia,
|
|
94
|
+
title,
|
|
95
|
+
aspectRatio,
|
|
96
|
+
minWidth,
|
|
97
|
+
minHeight
|
|
98
|
+
}: FloatingContainerProps) {
|
|
99
|
+
// Initial position: bottom right with some margin
|
|
100
|
+
const initialX = typeof window !== 'undefined' ? window.innerWidth - minWidth - 24 : 0;
|
|
101
|
+
const initialY = typeof window !== 'undefined' ? window.innerHeight - minHeight - 24 : 0;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="fixed inset-0 pointer-events-none z-50 overflow-hidden">
|
|
105
|
+
<motion.div
|
|
106
|
+
drag
|
|
107
|
+
dragMomentum={false}
|
|
108
|
+
dragElastic={0.05}
|
|
109
|
+
initial={{ x: initialX, y: initialY, scale: 0.9, opacity: 0 }}
|
|
110
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
111
|
+
exit={{ scale: 0.9, opacity: 0 }}
|
|
112
|
+
className="pointer-events-auto absolute"
|
|
113
|
+
>
|
|
114
|
+
<Resizable
|
|
115
|
+
defaultSize={{
|
|
116
|
+
width: minWidth,
|
|
117
|
+
height: minWidth / aspectRatio,
|
|
118
|
+
}}
|
|
119
|
+
minWidth={minWidth}
|
|
120
|
+
minHeight={minHeight}
|
|
121
|
+
lockAspectRatio={aspectRatio}
|
|
122
|
+
enable={{
|
|
123
|
+
top: true, right: true, bottom: true, left: true,
|
|
124
|
+
topRight: true, bottomRight: true, bottomLeft: true, topLeft: true
|
|
125
|
+
}}
|
|
126
|
+
className="shadow-2xl rounded-[var(--radius-card)] overflow-hidden border border-primary/20 bg-primary/80 backdrop-blur-md text-primary-foreground"
|
|
127
|
+
>
|
|
128
|
+
<div className="flex flex-col h-full w-full">
|
|
129
|
+
{/* Drag Handle & Header */}
|
|
130
|
+
<div className="h-8 bg-primary-foreground/10 backdrop-blur-sm border-b border-primary-foreground/10 flex items-center justify-between px-2 cursor-move active:cursor-grabbing group z-50">
|
|
131
|
+
<div className="flex items-center gap-2 text-xs font-medium text-primary-foreground/90 truncate flex-1">
|
|
132
|
+
<GripHorizontal className="w-4 h-4" />
|
|
133
|
+
<span className="truncate max-w-[150px]">{title || 'Media Player'}</span>
|
|
134
|
+
</div>
|
|
135
|
+
<div className="flex items-center gap-1">
|
|
136
|
+
<Button
|
|
137
|
+
variant="ghost"
|
|
138
|
+
size="icon"
|
|
139
|
+
className="h-6 w-6 text-primary-foreground/80 hover:bg-primary-foreground/20 hover:text-primary-foreground"
|
|
140
|
+
onClick={onClose}
|
|
141
|
+
title="Restaurar"
|
|
142
|
+
>
|
|
143
|
+
<Minimize2 className="w-3 h-3" />
|
|
144
|
+
</Button>
|
|
145
|
+
<Button
|
|
146
|
+
variant="ghost"
|
|
147
|
+
size="icon"
|
|
148
|
+
className="h-6 w-6 text-primary-foreground/80 hover:bg-destructive/20 hover:text-destructive"
|
|
149
|
+
onClick={onCloseMedia || onClose}
|
|
150
|
+
title="Fechar"
|
|
151
|
+
>
|
|
152
|
+
<X className="w-3 h-3" />
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Content */}
|
|
158
|
+
<div className="flex-1 relative bg-black overflow-hidden group">
|
|
159
|
+
{children}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</Resizable>
|
|
163
|
+
</motion.div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|