diff --git a/landing/components/ui/HeroDemoVideo.vue b/landing/components/ui/HeroDemoVideo.vue index 3e110cba..0db9bd69 100644 --- a/landing/components/ui/HeroDemoVideo.vue +++ b/landing/components/ui/HeroDemoVideo.vue @@ -12,6 +12,7 @@ const showControls = ref(true); const isLoaded = ref(false); const hasError = ref(false); const progress = ref(0); +const loadProgress = ref(0); const hideTimer = ref | null>(null); let intObserver: IntersectionObserver | null = null; @@ -64,6 +65,13 @@ function onSeek(e: MouseEvent) { showControlsBriefly(); } +function updateLoadProgress() { + const video = videoRef.value; + if (!video || !video.duration || !video.buffered.length) return; + const bufferedEnd = video.buffered.end(video.buffered.length - 1); + loadProgress.value = Math.round((bufferedEnd / video.duration) * 100); +} + function showControlsBriefly() { showControls.value = true; if (hideTimer.value) clearTimeout(hideTimer.value); @@ -88,8 +96,10 @@ function onMouseLeave() { onMounted(() => { const video = videoRef.value; if (video) { - video.addEventListener('loadeddata', () => { isLoaded.value = true; }); + // canplay fires earlier than loadeddata — enough to show first frame + video.addEventListener('canplay', () => { isLoaded.value = true; }, { once: true }); video.addEventListener('error', () => { hasError.value = true; }); + video.addEventListener('progress', updateLoadProgress); video.addEventListener('ended', () => { isPlaying.value = false; showControls.value = true; @@ -113,6 +123,7 @@ onMounted(() => { onUnmounted(() => { if (hideTimer.value) clearTimeout(hideTimer.value); if (intObserver) { intObserver.disconnect(); intObserver = null; } + videoRef.value?.removeEventListener('progress', updateLoadProgress); }); @@ -126,7 +137,18 @@ onUnmounted(() => {
- +
+
+ + {{ loadProgress > 0 ? `${loadProgress}%` : t('hero.watchDemo') }} + +
+
+
+
@@ -141,7 +163,7 @@ onUnmounted(() => { ref="videoRef" class="hero-video__player" :class="{ 'hero-video__player--loaded': isLoaded }" - preload="metadata" + preload="auto" muted playsinline @timeupdate="onTimeUpdate" @@ -256,16 +278,58 @@ onUnmounted(() => { animation: skeletonPulse 2s ease-in-out infinite; } -.hero-video__skeleton-icon { - color: rgba(0, 240, 255, 0.4); +.hero-video__skeleton-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; z-index: 1; } +.hero-video__skeleton-spinner { + width: 40px; + height: 40px; + border-radius: 50%; + border: 2px solid rgba(0, 240, 255, 0.15); + border-top-color: rgba(0, 240, 255, 0.7); + animation: spinnerRotate 0.8s linear infinite; +} + +.hero-video__skeleton-label { + font-size: 13px; + font-weight: 600; + color: rgba(0, 240, 255, 0.6); + font-family: "JetBrains Mono", monospace; + letter-spacing: 0.05em; +} + +.hero-video__skeleton-bar { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: rgba(255, 255, 255, 0.06); + border-radius: 0 0 16px 16px; + overflow: hidden; +} + +.hero-video__skeleton-bar-fill { + height: 100%; + background: linear-gradient(90deg, #00f0ff, #ff00ff); + border-radius: 2px; + transition: width 0.3s ease; +} + @keyframes skeletonPulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 0.8; } } +@keyframes spinnerRotate { + to { transform: rotate(360deg); } +} + /* ─── Error fallback ─── */ .hero-video__error { display: flex; diff --git a/src/renderer/components/team/dialogs/SendMessageDialog.tsx b/src/renderer/components/team/dialogs/SendMessageDialog.tsx index f1c512fc..2f6ebafb 100644 --- a/src/renderer/components/team/dialogs/SendMessageDialog.tsx +++ b/src/renderer/components/team/dialogs/SendMessageDialog.tsx @@ -318,21 +318,21 @@ export const SendMessageDialog = ({ e.preventDefault(); dragCounterRef.current = 0; setIsDragOver(false); - if (!isLeadRecipient) { + if (!supportsAttachments) { const files = e.dataTransfer?.files; if (files?.length) { showFileRestrictionError(); } return; } - if (canAttach) handleDrop(e); + handleDrop(e); }, - [isLeadRecipient, canAttach, handleDrop, showFileRestrictionError] + [supportsAttachments, handleDrop, showFileRestrictionError] ); const handlePasteWrapper = useCallback( (e: React.ClipboardEvent) => { - if (!isLeadRecipient) { + if (!supportsAttachments) { const hasFiles = Array.from(e.clipboardData.items).some((item) => item.kind === 'file'); if (hasFiles) { e.preventDefault(); @@ -340,9 +340,9 @@ export const SendMessageDialog = ({ } return; } - if (canAttach) handlePaste(e); + handlePaste(e); }, - [isLeadRecipient, canAttach, handlePaste, showFileRestrictionError] + [supportsAttachments, handlePaste, showFileRestrictionError] ); return (