monacousa-portal/components/ui/GlassCard.vue

246 lines
4.7 KiB
Vue

<template>
<div
v-motion
:initial="animated ? animationConfig.initial : {}"
:enter="animated ? animationConfig.enter : {}"
:hovered="hoverable ? { scale: 1.02 } : {}"
:delay="delay"
class="glass-card"
:class="[
`glass-card--${variant}`,
`glass-card--${size}`,
{
'glass-card--clickable': clickable,
'glass-card--elevated': elevated
}
]"
>
<div v-if="hasHeader" class="glass-card__header">
<slot name="header">
<h3 v-if="title" class="glass-card__title">{{ title }}</h3>
<p v-if="subtitle" class="glass-card__subtitle">{{ subtitle }}</p>
</slot>
</div>
<div class="glass-card__body">
<slot />
</div>
<div v-if="hasFooter" class="glass-card__footer">
<slot name="footer" />
</div>
<div v-if="gradient" class="glass-card__gradient"></div>
</div>
</template>
<script setup lang="ts">
import { computed, useSlots } from 'vue'
interface Props {
title?: string
subtitle?: string
variant?: 'light' | 'dark' | 'colored' | 'gradient'
size?: 'sm' | 'md' | 'lg' | 'xl'
clickable?: boolean
hoverable?: boolean
elevated?: boolean
gradient?: boolean
animated?: boolean
delay?: number
}
const props = withDefaults(defineProps<Props>(), {
variant: 'light',
size: 'md',
clickable: false,
hoverable: true,
elevated: true,
gradient: false,
animated: true,
delay: 0
})
const slots = useSlots()
const hasHeader = computed(() => !!slots.header || props.title || props.subtitle)
const hasFooter = computed(() => !!slots.footer)
const animationConfig = {
initial: {
opacity: 0,
y: 20,
scale: 0.95
},
enter: {
opacity: 1,
y: 0,
scale: 1,
transition: {
type: 'spring',
stiffness: 200,
damping: 20
}
}
}
</script>
<style scoped lang="scss">
.glass-card {
position: relative;
border-radius: 20px;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
// Variants - Updated to bolt.ai style with reduced blur
&--light {
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #27272a;
}
&--dark {
background: rgba(0, 0, 0, 0.05);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
border: 1px solid rgba(0, 0, 0, 0.1);
color: #27272a;
}
&--colored {
background: rgba(254, 242, 242, 0.6);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
border: 1px solid rgba(220, 38, 38, 0.1);
color: #27272a;
}
&--gradient {
background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%);
border: 1px solid rgba(0, 0, 0, 0.05);
color: #27272a;
}
// Sizes
&--sm {
.glass-card__body {
padding: 1rem;
}
.glass-card__header {
padding: 1rem 1rem 0.5rem;
}
.glass-card__footer {
padding: 0.5rem 1rem 1rem;
}
}
&--md {
.glass-card__body {
padding: 1.5rem;
}
.glass-card__header {
padding: 1.5rem 1.5rem 0.75rem;
}
.glass-card__footer {
padding: 0.75rem 1.5rem 1.5rem;
}
}
&--lg {
.glass-card__body {
padding: 2rem;
}
.glass-card__header {
padding: 2rem 2rem 1rem;
}
.glass-card__footer {
padding: 1rem 2rem 2rem;
}
}
&--xl {
.glass-card__body {
padding: 2.5rem;
}
.glass-card__header {
padding: 2.5rem 2.5rem 1.25rem;
}
.glass-card__footer {
padding: 1.25rem 2.5rem 2.5rem;
}
}
// States
&--clickable {
cursor: pointer;
&:active {
transform: scale(0.98);
}
}
&--elevated {
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
&:hover {
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.12);
}
}
&:hover {
transform: translateY(-2px);
}
// Header
&__header {
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
&__title {
font-size: 1.25rem;
font-weight: 600;
margin: 0;
color: #27272a;
}
&__subtitle {
font-size: 0.875rem;
margin: 0.25rem 0 0;
opacity: 0.8;
}
// Body
&__body {
position: relative;
z-index: 1;
}
// Footer
&__footer {
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
// Gradient overlay
&__gradient {
position: absolute;
top: 0;
right: 0;
width: 50%;
height: 100%;
background: linear-gradient(90deg,
transparent 0%,
rgba(220, 38, 38, 0.05) 100%);
pointer-events: none;
}
}
// Dark mode support
@media (prefers-color-scheme: dark) {
.glass-card--light {
background: rgba(30, 30, 30, 0.7);
border-color: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
}
</style>