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:
104
components/PlaybackControls.client.vue
Normal file
104
components/PlaybackControls.client.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="playback-controls">
|
||||
<!-- Shuffle -->
|
||||
<IconButton
|
||||
:icon="Shuffle"
|
||||
:active="isShuffled"
|
||||
:title="`Shuffle: ${isShuffled ? 'On' : 'Off'}`"
|
||||
@click="toggleShuffle"
|
||||
class="shuffle-btn"
|
||||
/>
|
||||
|
||||
<!-- Repeat modes -->
|
||||
<IconButton
|
||||
:icon="repeatIcon"
|
||||
:active="repeatMode !== 'none'"
|
||||
:title="repeatTitle"
|
||||
@click="cycleRepeat"
|
||||
class="repeat-btn"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { Shuffle, Repeat, Repeat1 } from 'lucide-vue-next'
|
||||
import IconButton from './IconButton.client.vue'
|
||||
|
||||
// Playback modes
|
||||
const isShuffled = useLocalStorage('shuffle', false)
|
||||
const repeatMode = useLocalStorage('repeat', 'none') // 'none', 'all', 'one'
|
||||
|
||||
const emit = defineEmits(['shuffle-changed', 'repeat-changed'])
|
||||
|
||||
const repeatIcon = computed(() => {
|
||||
switch (repeatMode.value) {
|
||||
case 'none': return Repeat
|
||||
case 'all': return Repeat
|
||||
case 'one': return Repeat1
|
||||
default: return Repeat
|
||||
}
|
||||
})
|
||||
|
||||
const repeatTitle = computed(() => {
|
||||
switch (repeatMode.value) {
|
||||
case 'none': return 'Repeat: Off'
|
||||
case 'all': return 'Repeat: All'
|
||||
case 'one': return 'Repeat: One'
|
||||
default: return 'Repeat: Off'
|
||||
}
|
||||
})
|
||||
|
||||
const toggleShuffle = () => {
|
||||
isShuffled.value = !isShuffled.value
|
||||
emit('shuffle-changed', isShuffled.value)
|
||||
}
|
||||
|
||||
const cycleRepeat = () => {
|
||||
const modes = ['none', 'all', 'one']
|
||||
const currentIndex = modes.indexOf(repeatMode.value)
|
||||
repeatMode.value = modes[(currentIndex + 1) % modes.length]
|
||||
emit('repeat-changed', repeatMode.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.playback-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shuffle-btn:hover {
|
||||
animation: wiggle 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.repeat-btn:hover {
|
||||
animation: spin 0.6s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(-5deg); }
|
||||
75% { transform: rotate(5deg); }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(180deg); }
|
||||
}
|
||||
|
||||
/* Active state glow */
|
||||
.shuffle-btn:deep(.icon-button.active),
|
||||
.repeat-btn:deep(.icon-button.active) {
|
||||
box-shadow: 0 0 20px var(--accent-primary);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.playback-controls {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user