Implement Lucide Vue icon system and UI improvements

- Replace emoji icons with professional SVG icons from Lucide Vue
- Add collapsible MusicControls with compact top-right collapse button
- Improve icon system with dynamic sizing and proper prop handling
- Disable SSR to prevent hydration issues with audio APIs
- Update IconButton to support both emoji strings and SVG components
- Optimize bottom positioning for expanded vs collapsed states
- Document new icon system in DESIGN_SYSTEM.md
This commit is contained in:
2025-08-03 20:01:31 -06:00
parent 25cd914be4
commit 7cb35b8c27
17 changed files with 3397 additions and 176 deletions

View File

@@ -1,124 +1,43 @@
<template>
<div class="music-player">
<!-- Aurora background -->
<div class="aurora-bg">
<div class="aurora-orb"></div>
<div class="aurora-orb"></div>
<div class="aurora-orb"></div>
<div class="aurora-orb"></div>
</div>
<div>
<!-- Aurora Background -->
<AuroraBackground
:is-playing="isPlaying"
:current-track="currentTrack"
/>
<!-- Header -->
<header class="glass-strong" style="padding: 20px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
<h1 style="font-size: 2rem; font-weight: 700; background: linear-gradient(45deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
🎵 RepoDructor
</h1>
<div style="display: flex; gap: 10px; align-items: center;">
<!-- Theme toggle -->
<button @click="toggleTheme" class="btn-icon" title="Toggle theme">
<span v-if="isDark"></span>
<span v-else>🌙</span>
</button>
<!-- Shuffle -->
<button @click="toggleShuffle" class="btn-icon" :class="{ 'active': isShuffled }" title="Shuffle">
🔀
</button>
<!-- Repeat modes -->
<button @click="cycleRepeat" class="btn-icon" title="Repeat mode">
<span v-if="repeatMode === 'none'">🔁</span>
<span v-else-if="repeatMode === 'all'">🔂</span>
<span v-else>🔂</span>
</button>
</div>
</header>
<!-- Main Container -->
<MainContainer
:current-track="currentTrack"
:is-playing="isPlaying"
@shuffle-changed="handleShuffleChanged"
@repeat-changed="handleRepeatChanged"
>
<!-- Track List -->
<TrackList
:tracks="tracks"
:display-tracks="displayTracks"
:current-track="currentTrack"
:is-playing="isPlaying"
:loading="loading"
:loading-track="loadingTrack"
@track-selected="handleTrackSelected"
/>
</MainContainer>
<!-- Now Playing -->
<div v-if="currentTrack" class="glass" style="padding: 20px; margin-bottom: 20px; text-align: center;">
<h2 style="font-size: 1.5rem; margin-bottom: 10px;">Now Playing</h2>
<p style="font-size: 1.2rem; font-weight: 600; color: var(--accent-primary);">
{{ currentTrack.name }}
</p>
<p style="color: var(--text-secondary); margin-top: 5px;">
Duration: {{ formatTime(currentTrack.duration || 0) }}
</p>
</div>
<!-- Track List -->
<div class="track-list">
<h3 style="margin-bottom: 15px; font-size: 1.3rem;">Your Music Library</h3>
<div v-if="loading" style="text-align: center; padding: 40px;">
<p>Loading your music...</p>
</div>
<div v-else-if="tracks.length === 0" style="text-align: center; padding: 40px;">
<p>No music files found. Add some music files to the /music folder!</p>
</div>
<div v-else>
<div
v-for="(track, index) in displayTracks"
:key="track.name"
@click="playTrack(track, index)"
class="track-item"
:class="{ active: currentTrack?.name === track.name }"
style="display: flex; justify-content: space-between; align-items: center;"
>
<div>
<p style="font-weight: 500;">{{ track.name }}</p>
<p style="font-size: 0.9rem; color: var(--text-secondary);">
{{ formatTime(track.duration || 0) }}
</p>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<span v-if="currentTrack?.name === track.name && !isPlaying"></span>
<span v-else-if="currentTrack?.name === track.name && isPlaying"></span>
<span v-else>🎵</span>
</div>
</div>
</div>
</div>
<!-- Player Controls -->
<div class="player-controls" v-if="currentTrack">
<!-- Previous -->
<button @click="previousTrack" class="btn-icon" title="Previous">
</button>
<!-- Play/Pause -->
<button @click="togglePlay" class="btn-icon" style="width: 60px; height: 60px; font-size: 1.5rem;" title="Play/Pause">
<span v-if="isPlaying"></span>
<span v-else></span>
</button>
<!-- Next -->
<button @click="nextTrack" class="btn-icon" title="Next">
</button>
<!-- Progress Bar -->
<div class="progress-bar" @click="seekTo" ref="progressBar">
<div class="progress-fill" :style="{ width: progressPercent + '%' }"></div>
</div>
<!-- Time -->
<div style="min-width: 100px; text-align: center; font-size: 0.9rem;">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</div>
<!-- Volume -->
<div class="volume-control">
<span>🔊</span>
<div class="volume-slider" @click="setVolume" ref="volumeSlider">
<div class="progress-fill" :style="{ width: volume * 100 + '%' }"></div>
</div>
</div>
</div>
<!-- Music Controls -->
<MusicControls
:current-track="currentTrack"
:is-playing="isPlaying"
:current-time="currentTime"
:duration="duration"
:volume="volume"
@previous="previousTrack"
@next="nextTrack"
@toggle-play="togglePlay"
@seek="seekTo"
@volume-change="setVolume"
/>
<!-- Audio Element -->
<audio
@@ -133,9 +52,20 @@
</template>
<script setup>
// Disable SSR for this page since it uses browser APIs and audio elements
definePageMeta({
ssr: false
})
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { useLocalStorage } from '@vueuse/core'
// Import components
import AuroraBackground from '~/components/AuroraBackground.client.vue'
import MainContainer from '~/components/MainContainer.client.vue'
import TrackList from '~/components/TrackList.client.vue'
import MusicControls from '~/components/MusicControls.client.vue'
// Reactive state
const tracks = ref([])
const currentTrack = ref(null)
@@ -145,8 +75,9 @@ const currentTime = ref(0)
const duration = ref(0)
const volume = ref(0.7)
const loading = ref(true)
const loadingTrack = ref(null)
// Theme
// Theme (handled by ThemeToggle component now)
const isDark = useLocalStorage('theme-dark', false)
// Playback modes
@@ -156,14 +87,8 @@ const shuffledIndices = ref([])
// Refs
const audioPlayer = ref(null)
const progressBar = ref(null)
const volumeSlider = ref(null)
// Computed
const progressPercent = computed(() => {
if (duration.value === 0) return 0
return (currentTime.value / duration.value) * 100
})
// Computed - removed progressPercent as it's not used in this component
const displayTracks = computed(() => {
if (isShuffled.value && shuffledIndices.value.length > 0) {
@@ -197,6 +122,7 @@ const generateShuffledIndices = () => {
}
const playTrack = async (track, index) => {
loadingTrack.value = track.name
currentTrack.value = track
currentTrackIndex.value = isShuffled.value
? shuffledIndices.value.indexOf(tracks.value.indexOf(track))
@@ -224,6 +150,7 @@ const playTrack = async (track, index) => {
// Auto-play when ready
audioPlayer.value.addEventListener('canplay', () => {
loadingTrack.value = null
audioPlayer.value.play()
}, { once: true })
} catch (error) {
@@ -235,6 +162,7 @@ const playTrack = async (track, index) => {
// Auto-play when ready (fallback)
audioPlayer.value.addEventListener('canplay', () => {
loadingTrack.value = null
audioPlayer.value.play()
}, { once: true })
}
@@ -301,46 +229,30 @@ const previousTrack = () => {
}, 100)
}
const toggleShuffle = () => {
isShuffled.value = !isShuffled.value
if (isShuffled.value) {
const handleShuffleChanged = (shuffled) => {
isShuffled.value = shuffled
if (shuffled) {
generateShuffledIndices()
}
}
const cycleRepeat = () => {
const modes = ['none', 'all', 'one']
const currentIndex = modes.indexOf(repeatMode.value)
repeatMode.value = modes[(currentIndex + 1) % modes.length]
const handleRepeatChanged = (mode) => {
repeatMode.value = mode
}
const toggleTheme = () => {
isDark.value = !isDark.value
const handleTrackSelected = ({ track, index }) => {
playTrack(track, index)
}
const formatTime = (seconds) => {
if (!seconds || isNaN(seconds)) return '0:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
// Removed formatTime function as it's not used in this component - it's defined in child components where needed
const seekTo = (event) => {
if (!audioPlayer.value || !progressBar.value) return
const rect = progressBar.value.getBoundingClientRect()
const percent = (event.clientX - rect.left) / rect.width
const newTime = percent * duration.value
const seekTo = (newTime) => {
if (!audioPlayer.value) return
audioPlayer.value.currentTime = newTime
}
const setVolume = (event) => {
if (!volumeSlider.value) return
const rect = volumeSlider.value.getBoundingClientRect()
const percent = (event.clientX - rect.left) / rect.width
volume.value = Math.max(0, Math.min(1, percent))
const setVolume = (newVolume) => {
volume.value = Math.max(0, Math.min(1, newVolume))
if (audioPlayer.value) {
audioPlayer.value.volume = volume.value
@@ -454,13 +366,6 @@ onUnmounted(() => {
</script>
<style scoped>
.active {
background: linear-gradient(45deg, var(--accent-primary), var(--accent-secondary)) !important;
color: white !important;
}
/* Page-specific styles */
</style>
.btn-icon.active {
background: var(--accent-primary) !important;
color: white !important;
}
</style>