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

@@ -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>