aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
661 lines (658 loc) • 23.7 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { useRef, useState, useCallback, useEffect } from 'react';
import { GlassCore } from '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import '../../primitives/motion/MotionFramer.js';
import { cn } from '../../lib/utilsComprehensive.js';
import { useMedia } from './GlassMediaProvider.js';
const formatTime = seconds => {
if (isNaN(seconds)) return "0:00";
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, "0")}`;
};
const Waveform = ({
audioData,
currentTime,
duration,
onSeek
}) => {
const canvasRef = useRef(null);
const [isHovering, setIsHovering] = useState(false);
const [hoverTime, setHoverTime] = useState(0);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas || !audioData.length) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const {
width,
height
} = canvas;
ctx.clearRect(0, 0, width, height);
// Draw waveform
const barWidth = width / audioData.length;
const progress = currentTime / duration;
audioData.forEach((amplitude, index) => {
const barHeight = amplitude * height * 0.8;
const x = index * barWidth;
const y = (height - barHeight) / 2;
// Color based on progress
const isPlayed = index / audioData.length < progress;
ctx.fillStyle = isPlayed ? "var(--glass-color-primary)" : "var(--glass-gray-200)";
ctx.fillRect(x, y, barWidth - 1, barHeight);
});
// Progress indicator
const progressX = progress * width;
ctx.strokeStyle = "#1d4ed8";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(progressX, 0);
ctx.lineTo(progressX, height);
ctx.stroke();
// Hover indicator
if (isHovering) {
const hoverX = hoverTime / duration * width;
ctx.strokeStyle = "#64748b";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(hoverX, 0);
ctx.lineTo(hoverX, height);
ctx.stroke();
}
}, [audioData, currentTime, duration, isHovering, hoverTime]);
const handleMouseMove = e => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = x / rect.width;
const time = percentage * duration;
setHoverTime(time);
};
const handleClick = e => {
const canvas = canvasRef.current;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = x / rect.width;
const time = percentage * duration;
onSeek(time);
};
return jsxs("div", {
"data-glass-component": true,
className: 'relative',
children: [jsx("canvas", {
ref: canvasRef,
width: 400,
height: 80,
className: 'glass-w-full h-20 cursor-pointer',
onMouseMove: handleMouseMove,
onMouseEnter: () => setIsHovering(true),
onMouseLeave: () => setIsHovering(false),
onClick: handleClick
}), isHovering && jsx("div", {
className: 'absolute bottom-24 glass-surface-dark text-primary glass-text-xs glass-px-2 glass-py-1 glass-radius pointer-events-none',
style: {
left: `${hoverTime / duration * 100}%`,
transform: "translateX(-50%)"
},
children: formatTime(hoverTime)
})]
});
};
const AudioVisualizer = ({
type,
audioContext,
analyzer,
isPlaying
}) => {
const canvasRef = useRef(null);
const animationRef = useRef();
const drawBars = useCallback((ctx, dataArray) => {
const {
width,
height
} = ctx.canvas;
ctx.clearRect(0, 0, width, height);
const barWidth = width / dataArray.length * 2.5;
let x = 0;
for (let i = 0; i < dataArray.length; i++) {
const barHeight = dataArray[i] / 255 * height;
const r = barHeight + 25 * (i / dataArray.length);
const g = 250 * (i / dataArray.length);
const b = 50;
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fillRect(x, height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}, []);
const drawWave = useCallback((ctx, dataArray) => {
const {
width,
height
} = ctx.canvas;
ctx.clearRect(0, 0, width, height);
ctx.lineWidth = 2;
ctx.strokeStyle = "var(--glass-color-primary)";
ctx.beginPath();
const sliceWidth = width / dataArray.length;
let x = 0;
for (let i = 0; i < dataArray.length; i++) {
const v = dataArray[i] / 128.0;
const y = v * height / 2;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
x += sliceWidth;
}
ctx.lineTo(width, height / 2);
ctx.stroke();
}, []);
const drawCircular = useCallback((ctx, dataArray) => {
const {
width,
height
} = ctx.canvas;
ctx.clearRect(0, 0, width, height);
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 4;
ctx.strokeStyle = "var(--glass-color-primary)";
ctx.lineWidth = 2;
for (let i = 0; i < dataArray.length; i++) {
const angle = i / dataArray.length * 2 * Math.PI;
const amplitude = dataArray[i] / 255 * radius;
const x1 = centerX + Math.cos(angle) * radius;
const y1 = centerY + Math.sin(angle) * radius;
const x2 = centerX + Math.cos(angle) * (radius + amplitude);
const y2 = centerY + Math.sin(angle) * (radius + amplitude);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
}, []);
const animate = useCallback(() => {
if (!analyzer || !isPlaying) return;
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const dataArray = new Uint8Array(analyzer.frequencyBinCount);
analyzer.getByteFrequencyData(dataArray);
switch (type) {
case "bars":
drawBars(ctx, dataArray);
break;
case "wave":
drawWave(ctx, dataArray);
break;
case "circular":
drawCircular(ctx, dataArray);
break;
}
animationRef.current = requestAnimationFrame(animate);
}, [analyzer, isPlaying, type, drawBars, drawWave, drawCircular]);
useEffect(() => {
if (isPlaying) {
animate();
} else if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isPlaying, animate]);
return jsx("canvas", {
ref: canvasRef,
width: 200,
height: 100,
className: 'glass-w-full h-24'
});
};
const PlaylistPanel = ({
playlist,
currentMediaId,
onMediaSelect
}) => {
return jsxs("div", {
className: "glass-surface-subtle glass-p-4 glass-radius-lg",
children: [jsx("h3", {
className: 'font-semibold glass-text-secondary mb-3',
children: "Playlist"
}), jsx("div", {
className: 'space-y-2 glass-max-h-64 overflow-y-auto',
children: playlist.map((media, index) => jsxs("button", {
onClick: () => onMediaSelect(media),
className: cn("flex items-center gap-3 w-full p-3 rounded-lg text-left transition-colors glass-focus glass-touch-target glass-contrast-guard", media.id === currentMediaId ? "bg-blue-100 border border-blue-200" : "bg-white hover:bg-gray-50 border border-gray-200"),
children: [jsx("div", {
className: "glass-text-lg",
children: index + 1
}), jsxs("div", {
className: "glass-flex-1 glass-min-w-0",
children: [jsx("div", {
className: 'font-medium glass-text-secondary truncate',
children: media.title || `Track ${index + 1}`
}), media.description && jsx("div", {
className: 'glass-text-sm glass-text-secondary truncate',
children: media.description
}), jsx("div", {
className: "glass-text-xs glass-text-secondary",
children: media.duration ? formatTime(media.duration) : "--:--"
})]
}), media.id === currentMediaId && jsx("div", {
className: 'text-primary',
children: "\u25B6"
})]
}, media.id))
})]
});
};
const TranscriptPanel = ({
transcript,
currentTime,
searchQuery,
onSearchChange,
onTranscriptClick
}) => {
const [highlightedResults, setHighlightedResults] = useState([]);
useEffect(() => {
if (searchQuery) {
const results = transcript.filter(entry => entry.text.toLowerCase().includes(searchQuery.toLowerCase()));
setHighlightedResults(results);
} else {
setHighlightedResults([]);
}
}, [searchQuery, transcript]);
const highlightText = (text, query) => {
if (!query) return text;
const regex = new RegExp(`(${query})`, "gi");
return text.split(regex).map((part, index) => part.toLowerCase() === query.toLowerCase() ? jsx("mark", {
className: "glass-surface-subtle glass-radius glass-px-1",
children: part
}, index) : part);
};
return jsxs("div", {
className: "glass-surface-subtle glass-p-4 glass-radius-lg glass-h-full glass-flex glass-flex-col",
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between mb-4',
children: [jsx("h3", {
className: 'font-semibold glass-text-secondary',
children: "Transcript"
}), jsxs("div", {
className: "glass-text-sm glass-text-secondary",
children: [transcript.length, " entries"]
})]
}), jsxs("div", {
className: 'mb-4',
children: [jsx("input", {
type: "text",
placeholder: "Search transcript...",
value: searchQuery,
onChange: e => onSearchChange(e.target.value),
className: 'glass-w-full glass-px-3 glass-py-2 glass-border glass-border-subtle glass-radius-lg focus:outline-none focus:ring-2 focus:ring-blue-500 glass-focus glass-touch-target glass-contrast-guard'
}), searchQuery && jsxs("div", {
className: 'glass-text-sm glass-text-secondary mt-2',
children: [highlightedResults.length, " results found"]
})]
}), jsx("div", {
className: 'glass-flex-1 overflow-y-auto space-y-2',
children: (searchQuery ? highlightedResults : transcript).map(entry => {
const isActive = currentTime >= entry.startTime && currentTime <= entry.endTime;
return jsxs("button", {
onClick: () => onTranscriptClick(entry),
className: cn("flex flex-col items-start gap-2 w-full p-3 rounded-lg text-left transition-colors glass-focus glass-touch-target glass-contrast-guard", isActive ? "bg-blue-100 border border-blue-200" : "bg-white hover:bg-gray-50"),
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between glass-w-full",
children: [jsx("span", {
className: 'glass-text-xs glass-text-secondary font-mono',
children: formatTime(entry.startTime)
}), entry.speaker && jsx("span", {
className: "glass-text-xs glass-surface-subtle glass-text-secondary glass-px-2 glass-py-1 glass-radius",
children: entry.speaker
})]
}), jsx("div", {
className: 'glass-text-sm glass-text-secondary leading-relaxed',
children: searchQuery ? highlightText(entry.text, searchQuery) : entry.text
})]
}, entry.id);
})
})]
});
};
const GlassAdvancedAudioPlayer = ({
mediaFile,
className,
variant = "full",
showTranscript = false,
showPlaylist = false,
showWaveform = false,
showLyrics = false,
autoplay = false,
loop = false,
preload = "metadata",
visualizerType = "bars",
onTimeUpdate,
onEnded,
onError,
playlist = []
}) => {
const {
playbackState,
setPlaybackState,
play,
pause,
seekTo,
setVolume,
setPlaybackRate,
toggleMute,
transcripts,
generateTranscript,
trackView
} = useMedia();
const audioRef = useRef(null);
const audioContextRef = useRef();
const analyzerRef = useRef();
const [transcriptSearch, setTranscriptSearch] = useState("");
const [hasTranscript, setHasTranscript] = useState(false);
const [waveformData, setWaveformData] = useState([]);
const [currentPlaylistIndex, setCurrentPlaylistIndex] = useState(0);
const isPlaying = playbackState?.mediaId === mediaFile.id && playbackState.isPlaying;
const currentTime = playbackState?.mediaId === mediaFile.id ? playbackState.currentTime : 0;
const duration = playbackState?.mediaId === mediaFile.id ? playbackState.duration : mediaFile.duration || 0;
const volume = playbackState?.volume || 1;
const playbackRate = playbackState?.playbackRate || 1;
const isMuted = playbackState?.isMuted || false;
// Lazily create audio context when user initiates playback
const ensureAudioContext = useCallback(() => {
if (visualizerType === "none" || !audioRef.current || audioContextRef.current) return;
try {
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
analyzerRef.current = audioContextRef.current.createAnalyser();
analyzerRef.current.fftSize = 256;
const source = audioContextRef.current.createMediaElementSource(audioRef.current);
source.connect(analyzerRef.current);
analyzerRef.current.connect(audioContextRef.current.destination);
} catch (error) {
console.warn("Could not initialize audio context:", error);
}
}, [visualizerType]);
// Initialize transcript
useEffect(() => {
if (!transcripts[mediaFile.id] && showTranscript) {
generateTranscript(mediaFile.id).then(() => {
setHasTranscript(true);
});
} else if (transcripts[mediaFile.id]) {
setHasTranscript(true);
}
}, [mediaFile.id, showTranscript, generateTranscript, transcripts]);
// Generate mock waveform data
useEffect(() => {
if (showWaveform) {
const mockData = Array.from({
length: 100
}, () => Math.random() * 0.8 + 0.2);
setWaveformData(mockData);
}
}, [showWaveform]);
// Audio event handlers
const handleTimeUpdate = () => {
if (audioRef.current) {
const currentTime = audioRef.current.currentTime;
if (playbackState && playbackState.mediaId === mediaFile.id) {
setPlaybackState({
...playbackState,
currentTime
});
}
onTimeUpdate?.(currentTime);
}
};
const handleLoadedMetadata = () => {
if (audioRef.current) {
const duration = audioRef.current.duration;
if (playbackState && playbackState.mediaId === mediaFile.id) {
setPlaybackState({
...playbackState,
duration,
isLoading: false
});
}
}
};
const handleEnded = () => {
if (playlist.length > 0 && currentPlaylistIndex < playlist.length - 1) {
// Auto-play next track
const nextIndex = currentPlaylistIndex + 1;
setCurrentPlaylistIndex(nextIndex);
handleMediaSelect(playlist[nextIndex]);
} else {
if (playbackState && playbackState.mediaId === mediaFile.id) {
setPlaybackState({
...playbackState,
isPlaying: false,
currentTime: 0
});
}
trackView(mediaFile.id, duration);
onEnded?.();
}
};
const handlePlayPause = () => {
if (isPlaying) {
pause();
if (audioRef.current) {
audioRef.current.pause();
}
} else {
play(mediaFile.id);
// Create AudioContext and wiring as part of user gesture
ensureAudioContext();
if (audioRef.current) {
audioRef.current.play();
}
}
};
const handleSeek = time => {
seekTo(time);
if (audioRef.current) {
audioRef.current.currentTime = time;
}
};
const handleVolumeChange = newVolume => {
setVolume(newVolume);
if (audioRef.current) {
audioRef.current.volume = newVolume;
}
};
const handleMuteToggle = () => {
toggleMute();
if (audioRef.current) {
audioRef.current.muted = !isMuted;
}
};
const handleMediaSelect = media => {
play(media.id);
const index = playlist.findIndex(item => item.id === media.id);
if (index !== -1) {
setCurrentPlaylistIndex(index);
}
};
const handleTranscriptClick = entry => {
handleSeek(entry.startTime);
};
const renderCompactPlayer = () => jsxs("div", {
className: "glass-flex glass-items-center glass-gap-4 glass-p-4",
children: [jsx("button", {
onClick: handlePlayPause,
className: 'w-12 h-12 glass-flex glass-items-center glass-justify-center glass-surface-blue hover:glass-surface-blue text-primary glass-radius-full transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: isPlaying ? "⏸️" : "▶️"
}), jsxs("div", {
className: "glass-flex-1",
children: [jsx("div", {
className: 'font-medium glass-text-secondary truncate',
children: mediaFile.title || "Untitled Track"
}), jsx("div", {
className: 'glass-text-sm glass-text-secondary truncate',
children: mediaFile.description || "No description"
})]
}), jsxs("div", {
className: 'glass-text-sm glass-text-secondary font-mono',
children: [formatTime(currentTime), " / ", formatTime(duration)]
})]
});
const renderFullPlayer = () => jsxs("div", {
className: 'glass-p-6 space-y-6',
children: [jsxs("div", {
className: 'text-center',
children: [jsx("h2", {
className: 'glass-text-xl font-semibold glass-text-secondary',
children: mediaFile.title || "Untitled Track"
}), mediaFile.description && jsx("p", {
className: 'glass-text-secondary mt-1',
children: mediaFile.description
})]
}), showWaveform ? jsx(Waveform, {
audioData: waveformData,
currentTime: currentTime,
duration: duration,
onSeek: handleSeek
}) : visualizerType !== "none" && jsx(AudioVisualizer, {
type: visualizerType,
audioContext: audioContextRef.current,
analyzer: analyzerRef.current,
isPlaying: isPlaying
}), jsxs("div", {
className: 'space-y-2',
children: [jsxs("div", {
className: 'glass-flex glass-justify-between glass-text-sm glass-text-secondary font-mono',
children: [jsx("span", {
children: formatTime(currentTime)
}), jsx("span", {
children: formatTime(duration)
})]
}), jsx("div", {
className: 'glass-w-full h-2 glass-surface-subtle glass-radius-full cursor-pointer',
children: jsx("div", {
className: "glass-h-full glass-surface-blue glass-radius-full",
style: {
width: `${currentTime / duration * 100}%`
}
})
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-justify-center glass-gap-6",
children: [jsx("button", {
onClick: () => handleSeek(Math.max(0, currentTime - 10)),
className: 'w-10 h-10 glass-flex glass-items-center glass-justify-center hover:glass-surface-subtle glass-radius-full transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: "\u23EA"
}), jsx("button", {
onClick: handlePlayPause,
className: 'w-16 h-16 glass-flex glass-items-center glass-justify-center glass-surface-blue hover:glass-surface-blue text-primary glass-radius-full transition-colors glass-text-xl glass-focus glass-touch-target glass-contrast-guard',
children: isPlaying ? "⏸️" : "▶️"
}), jsx("button", {
onClick: () => handleSeek(Math.min(duration, currentTime + 10)),
className: 'w-10 h-10 glass-flex glass-items-center glass-justify-center hover:glass-surface-subtle glass-radius-full transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: "\u23E9"
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-3",
children: [jsx("button", {
onClick: handleMuteToggle,
className: 'w-8 h-8 glass-flex glass-items-center glass-justify-center hover:glass-surface-subtle glass-radius transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: isMuted ? "🔇" : "🔊"
}), jsx("input", {
type: "range",
min: "0",
max: "1",
step: "0.01",
value: isMuted ? 0 : volume,
onChange: e => handleVolumeChange(Number(e.target.value)),
className: 'w-20 glass-focus glass-touch-target glass-contrast-guard'
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-3",
children: [jsx("span", {
className: "glass-text-sm glass-text-secondary",
children: "Speed:"
}), jsxs("select", {
value: playbackRate,
onChange: e => setPlaybackRate(Number(e.target.value)),
className: "glass-text-sm glass-border glass-border-subtle glass-radius glass-px-2 glass-py-1 glass-focus glass-touch-target glass-contrast-guard",
children: [jsx("option", {
value: 0.5,
children: "0.5x"
}), jsx("option", {
value: 0.75,
children: "0.75x"
}), jsx("option", {
value: 1,
children: "1x"
}), jsx("option", {
value: 1.25,
children: "1.25x"
}), jsx("option", {
value: 1.5,
children: "1.5x"
}), jsx("option", {
value: 2,
children: "2x"
})]
})]
})]
})]
});
return jsx(GlassCore, {
className: cn("overflow-hidden", className),
children: jsxs("div", {
className: "glass-flex",
children: [jsxs("div", {
className: "glass-flex-1",
children: [jsx("audio", {
ref: audioRef,
src: mediaFile.src,
autoPlay: autoplay,
loop: loop,
preload: preload,
onTimeUpdate: handleTimeUpdate,
onLoadedMetadata: handleLoadedMetadata,
onEnded: handleEnded,
onError: () => onError?.("Audio playback error occurred")
}), variant === "compact" ? renderCompactPlayer() : renderFullPlayer()]
}), jsxs("div", {
className: "glass-flex",
children: [showPlaylist && playlist.length > 0 && jsx("div", {
className: 'w-80 glass-border-l glass-border-subtle',
children: jsx(PlaylistPanel, {
playlist: playlist,
currentMediaId: mediaFile.id,
onMediaSelect: handleMediaSelect
})
}), showTranscript && hasTranscript && transcripts[mediaFile.id] && jsx("div", {
className: 'w-96 glass-border-l glass-border-subtle h-96 overflow-hidden',
children: jsx(TranscriptPanel, {
transcript: transcripts[mediaFile.id],
currentTime: currentTime,
searchQuery: transcriptSearch,
onSearchChange: setTranscriptSearch,
onTranscriptClick: handleTranscriptClick
})
})]
})]
})
});
};
export { GlassAdvancedAudioPlayer };
//# sourceMappingURL=GlassAdvancedAudioPlayer.js.map