Mockups for Designs
All checks were successful
Build And Push Image / docker (push) Successful in 1m55s
All checks were successful
Build And Push Image / docker (push) Successful in 1m55s
This commit is contained in:
215
design-mockups/components/core/NeumorphicCard.vue
Normal file
215
design-mockups/components/core/NeumorphicCard.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'neumorphic-card',
|
||||
sizeClasses,
|
||||
elevationClasses,
|
||||
{
|
||||
'cursor-pointer': clickable,
|
||||
'transition-all duration-300': animated
|
||||
}
|
||||
]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div v-if="$slots.header || title" class="neumorphic-card__header">
|
||||
<slot name="header">
|
||||
<h3 v-if="title" class="neumorphic-card__title">{{ title }}</h3>
|
||||
<p v-if="subtitle" class="neumorphic-card__subtitle">{{ subtitle }}</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="neumorphic-card__content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.footer" class="neumorphic-card__footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
elevation?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
clickable?: boolean;
|
||||
animated?: boolean;
|
||||
pressed?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'md',
|
||||
elevation: 'md',
|
||||
clickable: false,
|
||||
animated: true,
|
||||
pressed: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [event: MouseEvent];
|
||||
}>();
|
||||
|
||||
const sizeClasses = computed(() => {
|
||||
const sizes = {
|
||||
sm: 'p-4',
|
||||
md: 'p-6',
|
||||
lg: 'p-8',
|
||||
xl: 'p-10'
|
||||
};
|
||||
return sizes[props.size];
|
||||
});
|
||||
|
||||
const elevationClasses = computed(() => {
|
||||
if (props.pressed) return 'neumorphic-pressed';
|
||||
|
||||
const elevations = {
|
||||
none: '',
|
||||
sm: 'neumorphic-elevation-sm',
|
||||
md: 'neumorphic-elevation-md',
|
||||
lg: 'neumorphic-elevation-lg',
|
||||
xl: 'neumorphic-elevation-xl'
|
||||
};
|
||||
return elevations[props.elevation];
|
||||
});
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (props.clickable) {
|
||||
emit('click', event);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/neumorphic-system.scss';
|
||||
|
||||
.neumorphic-card {
|
||||
background: linear-gradient(145deg, #ffffff, #f0f0f0);
|
||||
border-radius: $radius-xl;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Base elevation styles
|
||||
&.neumorphic-elevation-sm {
|
||||
box-shadow: $shadow-soft-sm;
|
||||
}
|
||||
|
||||
&.neumorphic-elevation-md {
|
||||
box-shadow: $shadow-soft-md;
|
||||
}
|
||||
|
||||
&.neumorphic-elevation-lg {
|
||||
box-shadow: $shadow-soft-lg;
|
||||
}
|
||||
|
||||
&.neumorphic-elevation-xl {
|
||||
box-shadow: $shadow-soft-xl;
|
||||
}
|
||||
|
||||
&.neumorphic-pressed {
|
||||
box-shadow: $shadow-inset-md;
|
||||
background: linear-gradient(145deg, #e6e6e6, #ffffff);
|
||||
}
|
||||
|
||||
// Hover effects for clickable cards
|
||||
&.cursor-pointer {
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: $shadow-soft-lg;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: $shadow-inset-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// Header section
|
||||
&__header {
|
||||
padding-bottom: $space-4;
|
||||
border-bottom: 1px solid rgba($neutral-300, 0.5);
|
||||
margin-bottom: $space-4;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-family: $font-heading;
|
||||
font-size: $text-xl;
|
||||
font-weight: $font-semibold;
|
||||
color: $neutral-800;
|
||||
margin: 0;
|
||||
line-height: $leading-tight;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-family: $font-body;
|
||||
font-size: $text-sm;
|
||||
color: $neutral-600;
|
||||
margin-top: $space-2;
|
||||
line-height: $leading-normal;
|
||||
}
|
||||
|
||||
// Content section
|
||||
&__content {
|
||||
font-family: $font-body;
|
||||
color: $neutral-700;
|
||||
line-height: $leading-relaxed;
|
||||
}
|
||||
|
||||
// Footer section
|
||||
&__footer {
|
||||
padding-top: $space-4;
|
||||
border-top: 1px solid rgba($neutral-300, 0.5);
|
||||
margin-top: $space-4;
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
@include dark-mode {
|
||||
background: linear-gradient(145deg, $neutral-800, $neutral-700);
|
||||
|
||||
&.neumorphic-elevation-sm {
|
||||
box-shadow: $shadow-dark-soft-sm;
|
||||
}
|
||||
|
||||
&.neumorphic-elevation-md {
|
||||
box-shadow: $shadow-dark-soft-md;
|
||||
}
|
||||
|
||||
&.neumorphic-elevation-lg {
|
||||
box-shadow: $shadow-dark-soft-lg;
|
||||
}
|
||||
|
||||
&.neumorphic-pressed {
|
||||
box-shadow: $shadow-dark-inset-md;
|
||||
background: linear-gradient(145deg, $neutral-900, $neutral-800);
|
||||
}
|
||||
|
||||
.neumorphic-card__title {
|
||||
color: $neutral-100;
|
||||
}
|
||||
|
||||
.neumorphic-card__subtitle {
|
||||
color: $neutral-400;
|
||||
}
|
||||
|
||||
.neumorphic-card__content {
|
||||
color: $neutral-300;
|
||||
}
|
||||
|
||||
.neumorphic-card__header,
|
||||
.neumorphic-card__footer {
|
||||
border-color: rgba($neutral-600, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@include responsive('sm') {
|
||||
&.p-4 { padding: $space-5; }
|
||||
&.p-6 { padding: $space-8; }
|
||||
&.p-8 { padding: $space-10; }
|
||||
&.p-10 { padding: $space-12; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
315
design-mockups/components/core/ProfessionalButton.vue
Normal file
315
design-mockups/components/core/ProfessionalButton.vue
Normal file
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<button
|
||||
:class="[
|
||||
'professional-button',
|
||||
variantClasses,
|
||||
sizeClasses,
|
||||
{
|
||||
'professional-button--loading': loading,
|
||||
'professional-button--disabled': disabled || loading,
|
||||
'professional-button--block': block,
|
||||
'professional-button--icon-only': !$slots.default && icon
|
||||
}
|
||||
]"
|
||||
:disabled="disabled || loading"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span v-if="loading" class="professional-button__spinner">
|
||||
<svg class="animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<span v-if="icon && !loading" class="professional-button__icon">
|
||||
<component :is="icon" />
|
||||
</span>
|
||||
|
||||
<span v-if="$slots.default" class="professional-button__content">
|
||||
<slot></slot>
|
||||
</span>
|
||||
|
||||
<span v-if="endIcon && !loading" class="professional-button__end-icon">
|
||||
<component :is="endIcon" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
interface Props {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
block?: boolean;
|
||||
icon?: Component;
|
||||
endIcon?: Component;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
loading: false,
|
||||
disabled: false,
|
||||
block: false
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [event: MouseEvent];
|
||||
}>();
|
||||
|
||||
const variantClasses = computed(() => {
|
||||
const variants = {
|
||||
primary: 'professional-button--primary',
|
||||
secondary: 'professional-button--secondary',
|
||||
outline: 'professional-button--outline',
|
||||
ghost: 'professional-button--ghost',
|
||||
danger: 'professional-button--danger'
|
||||
};
|
||||
return variants[props.variant];
|
||||
});
|
||||
|
||||
const sizeClasses = computed(() => {
|
||||
const sizes = {
|
||||
sm: 'professional-button--sm',
|
||||
md: 'professional-button--md',
|
||||
lg: 'professional-button--lg'
|
||||
};
|
||||
return sizes[props.size];
|
||||
});
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (!props.disabled && !props.loading) {
|
||||
emit('click', event);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/neumorphic-system.scss';
|
||||
|
||||
.professional-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-center: center;
|
||||
gap: $space-2;
|
||||
border: none;
|
||||
border-radius: $radius-lg;
|
||||
font-family: $font-body;
|
||||
font-weight: $font-medium;
|
||||
cursor: pointer;
|
||||
transition: all $transition-base $ease-in-out-soft;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Size variants
|
||||
&--sm {
|
||||
padding: $space-2 $space-4;
|
||||
font-size: $text-sm;
|
||||
min-height: $button-height-sm;
|
||||
|
||||
&.professional-button--icon-only {
|
||||
width: $button-height-sm;
|
||||
padding: $space-2;
|
||||
}
|
||||
}
|
||||
|
||||
&--md {
|
||||
padding: $space-3 $space-5;
|
||||
font-size: $text-base;
|
||||
min-height: $button-height-md;
|
||||
|
||||
&.professional-button--icon-only {
|
||||
width: $button-height-md;
|
||||
padding: $space-3;
|
||||
}
|
||||
}
|
||||
|
||||
&--lg {
|
||||
padding: $space-4 $space-6;
|
||||
font-size: $text-lg;
|
||||
min-height: $button-height-lg;
|
||||
|
||||
&.professional-button--icon-only {
|
||||
width: $button-height-lg;
|
||||
padding: $space-4;
|
||||
}
|
||||
}
|
||||
|
||||
// Variant styles
|
||||
&--primary {
|
||||
background: linear-gradient(135deg, $primary-500, $primary-600);
|
||||
color: white;
|
||||
box-shadow: $shadow-soft-sm;
|
||||
|
||||
&:hover:not(.professional-button--disabled) {
|
||||
background: linear-gradient(135deg, $primary-600, $primary-700);
|
||||
box-shadow: $shadow-soft-md;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active:not(.professional-button--disabled) {
|
||||
box-shadow: $shadow-inset-sm;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: linear-gradient(145deg, #ffffff, #f0f0f0);
|
||||
color: $neutral-800;
|
||||
box-shadow: $shadow-soft-sm;
|
||||
|
||||
&:hover:not(.professional-button--disabled) {
|
||||
box-shadow: $shadow-soft-md;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active:not(.professional-button--disabled) {
|
||||
box-shadow: $shadow-inset-sm;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
&--outline {
|
||||
background: transparent;
|
||||
color: $primary-600;
|
||||
border: 2px solid $primary-200;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover:not(.professional-button--disabled) {
|
||||
background: rgba($primary-500, 0.05);
|
||||
border-color: $primary-300;
|
||||
}
|
||||
|
||||
&:active:not(.professional-button--disabled) {
|
||||
background: rgba($primary-500, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&--ghost {
|
||||
background: transparent;
|
||||
color: $neutral-700;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover:not(.professional-button--disabled) {
|
||||
background: rgba($neutral-500, 0.08);
|
||||
}
|
||||
|
||||
&:active:not(.professional-button--disabled) {
|
||||
background: rgba($neutral-500, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: linear-gradient(135deg, $error-500, #dc2626);
|
||||
color: white;
|
||||
box-shadow: $shadow-soft-sm;
|
||||
|
||||
&:hover:not(.professional-button--disabled) {
|
||||
background: linear-gradient(135deg, #dc2626, #b91c1c);
|
||||
box-shadow: $shadow-soft-md;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active:not(.professional-button--disabled) {
|
||||
box-shadow: $shadow-inset-sm;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// States
|
||||
&--block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--loading {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
// Icons
|
||||
&__spinner {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: inherit;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon,
|
||||
&__end-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Focus state
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba($primary-500, 0.2);
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
@include dark-mode {
|
||||
&--secondary {
|
||||
background: linear-gradient(145deg, $neutral-700, $neutral-800);
|
||||
color: $neutral-100;
|
||||
|
||||
&:hover:not(.professional-button--disabled) {
|
||||
background: linear-gradient(145deg, $neutral-600, $neutral-700);
|
||||
}
|
||||
}
|
||||
|
||||
&--ghost {
|
||||
color: $neutral-300;
|
||||
|
||||
&:hover:not(.professional-button--disabled) {
|
||||
background: rgba($neutral-400, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&--outline {
|
||||
color: $primary-400;
|
||||
border-color: $primary-800;
|
||||
|
||||
&:hover:not(.professional-button--disabled) {
|
||||
background: rgba($primary-400, 0.1);
|
||||
border-color: $primary-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animation
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
320
design-mockups/components/core/StatCard.vue
Normal file
320
design-mockups/components/core/StatCard.vue
Normal file
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<NeumorphicCard :elevation="elevation" :animated="animated" class="stat-card">
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__header">
|
||||
<div class="stat-card__icon-wrapper" :class="`stat-card__icon-wrapper--${variant}`">
|
||||
<component :is="icon" v-if="icon" class="stat-card__icon" />
|
||||
<div v-else class="stat-card__icon-placeholder">
|
||||
<svg fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card__trend" v-if="trend">
|
||||
<span :class="['stat-card__trend-badge', trendClass]">
|
||||
<svg v-if="trend > 0" class="stat-card__trend-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
<svg v-else class="stat-card__trend-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6" />
|
||||
</svg>
|
||||
{{ Math.abs(trend) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card__body">
|
||||
<h3 class="stat-card__label">{{ label }}</h3>
|
||||
<div class="stat-card__value-wrapper">
|
||||
<span v-if="prefix" class="stat-card__prefix">{{ prefix }}</span>
|
||||
<span class="stat-card__value">{{ formattedValue }}</span>
|
||||
<span v-if="suffix" class="stat-card__suffix">{{ suffix }}</span>
|
||||
</div>
|
||||
<p v-if="description" class="stat-card__description">{{ description }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="showProgress" class="stat-card__progress">
|
||||
<div class="stat-card__progress-bar">
|
||||
<div
|
||||
class="stat-card__progress-fill"
|
||||
:class="`stat-card__progress-fill--${variant}`"
|
||||
:style="{ width: `${progress}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<span class="stat-card__progress-text">{{ progress }}% of target</span>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.footer" class="stat-card__footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</NeumorphicCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
import NeumorphicCard from './NeumorphicCard.vue';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
value: number | string;
|
||||
trend?: number;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
description?: string;
|
||||
variant?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||
icon?: Component;
|
||||
elevation?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
animated?: boolean;
|
||||
showProgress?: boolean;
|
||||
progress?: number;
|
||||
format?: 'number' | 'currency' | 'percentage';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: 'primary',
|
||||
elevation: 'md',
|
||||
animated: true,
|
||||
showProgress: false,
|
||||
progress: 0,
|
||||
format: 'number'
|
||||
});
|
||||
|
||||
const formattedValue = computed(() => {
|
||||
const val = props.value;
|
||||
if (typeof val === 'string') return val;
|
||||
|
||||
switch (props.format) {
|
||||
case 'currency':
|
||||
return val.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 });
|
||||
case 'percentage':
|
||||
return val.toFixed(1);
|
||||
default:
|
||||
return val.toLocaleString('en-US');
|
||||
}
|
||||
});
|
||||
|
||||
const trendClass = computed(() => {
|
||||
if (!props.trend) return '';
|
||||
return props.trend > 0 ? 'stat-card__trend-badge--up' : 'stat-card__trend-badge--down';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/neumorphic-system.scss';
|
||||
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-4;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__icon-wrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $radius-lg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: $shadow-soft-sm;
|
||||
transition: all $transition-base;
|
||||
|
||||
&--primary {
|
||||
background: linear-gradient(135deg, rgba($primary-500, 0.1), rgba($primary-600, 0.15));
|
||||
color: $primary-600;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: linear-gradient(135deg, rgba($success-500, 0.1), rgba($success-500, 0.15));
|
||||
color: $success-500;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: linear-gradient(135deg, rgba($warning-500, 0.1), rgba($warning-500, 0.15));
|
||||
color: $warning-500;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: linear-gradient(135deg, rgba($error-500, 0.1), rgba($error-500, 0.15));
|
||||
color: $error-500;
|
||||
}
|
||||
|
||||
&--info {
|
||||
background: linear-gradient(135deg, rgba($info-500, 0.1), rgba($info-500, 0.15));
|
||||
color: $info-500;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon,
|
||||
&__icon-placeholder {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&__trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__trend-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $space-1;
|
||||
padding: $space-1 $space-2;
|
||||
border-radius: $radius-full;
|
||||
font-size: $text-sm;
|
||||
font-weight: $font-semibold;
|
||||
|
||||
&--up {
|
||||
background: linear-gradient(135deg, rgba($success-500, 0.1), rgba($success-500, 0.15));
|
||||
color: $success-500;
|
||||
}
|
||||
|
||||
&--down {
|
||||
background: linear-gradient(135deg, rgba($error-500, 0.1), rgba($error-500, 0.15));
|
||||
color: $error-500;
|
||||
}
|
||||
}
|
||||
|
||||
&__trend-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: $text-sm;
|
||||
font-weight: $font-medium;
|
||||
color: $neutral-600;
|
||||
margin-bottom: $space-2;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
&__value-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: $space-1;
|
||||
margin-bottom: $space-2;
|
||||
}
|
||||
|
||||
&__prefix,
|
||||
&__suffix {
|
||||
font-size: $text-lg;
|
||||
color: $neutral-500;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-family: $font-heading;
|
||||
font-size: $text-3xl;
|
||||
font-weight: $font-bold;
|
||||
color: $neutral-800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: $text-sm;
|
||||
color: $neutral-600;
|
||||
line-height: $leading-normal;
|
||||
}
|
||||
|
||||
&__progress {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
&__progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: rgba($neutral-300, 0.3);
|
||||
border-radius: $radius-full;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-inset-sm;
|
||||
margin-bottom: $space-2;
|
||||
}
|
||||
|
||||
&__progress-fill {
|
||||
height: 100%;
|
||||
border-radius: $radius-full;
|
||||
transition: width $transition-slow $ease-out-soft;
|
||||
|
||||
&--primary {
|
||||
background: linear-gradient(90deg, $primary-500, $primary-600);
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: linear-gradient(90deg, $success-500, #059669);
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: linear-gradient(90deg, $warning-500, #D97706);
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: linear-gradient(90deg, $error-500, #DC2626);
|
||||
}
|
||||
|
||||
&--info {
|
||||
background: linear-gradient(90deg, $info-500, #2563EB);
|
||||
}
|
||||
}
|
||||
|
||||
&__progress-text {
|
||||
font-size: $text-xs;
|
||||
color: $neutral-500;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding-top: $space-3;
|
||||
border-top: 1px solid rgba($neutral-300, 0.3);
|
||||
}
|
||||
|
||||
// Hover effect
|
||||
&:hover {
|
||||
.stat-card__icon-wrapper {
|
||||
transform: scale(1.05);
|
||||
box-shadow: $shadow-soft-md;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode
|
||||
@include dark-mode {
|
||||
.stat-card__label {
|
||||
color: $neutral-400;
|
||||
}
|
||||
|
||||
.stat-card__value {
|
||||
color: $neutral-100;
|
||||
}
|
||||
|
||||
.stat-card__description {
|
||||
color: $neutral-400;
|
||||
}
|
||||
|
||||
.stat-card__prefix,
|
||||
.stat-card__suffix {
|
||||
color: $neutral-500;
|
||||
}
|
||||
|
||||
.stat-card__progress-bar {
|
||||
background: rgba($neutral-600, 0.3);
|
||||
}
|
||||
|
||||
.stat-card__progress-text {
|
||||
color: $neutral-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user