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,138 @@
<template>
<BaseButton
variant="icon"
:active="active"
:disabled="disabled"
:title="title"
@click="$emit('click', $event)"
:class="[
'icon-button',
{
'large': size === 'large',
'small': size === 'small'
}
]"
>
<component v-if="typeof icon === 'object' || typeof icon === 'function'" :is="icon" class="icon" :size="iconSize" />
<span v-else class="icon">{{ icon }}</span>
<div v-if="badge" class="badge">{{ badge }}</div>
</BaseButton>
</template>
<script setup>
import { computed } from 'vue'
import BaseButton from './BaseButton.client.vue'
const props = defineProps({
icon: {
type: [String, Object, Function],
required: true
},
active: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
size: {
type: String,
default: 'normal',
validator: (value) => ['small', 'normal', 'large'].includes(value)
},
badge: {
type: [String, Number],
default: null
}
})
defineEmits(['click'])
const iconSize = computed(() => {
switch (props.size) {
case 'small': return 18
case 'large': return 28
default: return 20
}
})
</script>
<style scoped>
.icon-button {
position: relative;
}
.icon-button.small {
width: 36px;
height: 36px;
font-size: 0.9rem;
}
.icon-button.large {
width: 60px;
height: 60px;
font-size: 1.5rem;
}
.icon {
display: block;
transition: transform 0.2s ease;
}
.icon-button:hover .icon {
transform: scale(1.1);
}
.badge {
position: absolute;
top: -4px;
right: -4px;
background: var(--accent-primary);
color: white;
border-radius: 50%;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 600;
z-index: 1;
}
/* Special animations for specific icons */
.icon-button[title*="Shuffle"]:hover .icon {
animation: shuffle 0.5s ease-in-out;
}
.icon-button[title*="Repeat"]:hover .icon {
animation: rotate 0.5s ease-in-out;
}
.icon-button[title*="Play"]:hover .icon,
.icon-button[title*="Pause"]:hover .icon {
animation: pulse-icon 0.3s ease-in-out;
}
@keyframes shuffle {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes pulse-icon {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
</style>