- 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
104 lines
2.3 KiB
Vue
104 lines
2.3 KiB
Vue
<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> |