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:
138
components/IconButton.client.vue
Normal file
138
components/IconButton.client.vue
Normal 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>
|
||||
Reference in New Issue
Block a user