Mockups for Designs
All checks were successful
Build And Push Image / docker (push) Successful in 1m55s

This commit is contained in:
2025-09-03 21:04:44 +02:00
parent e75de8b9f4
commit 4d24315103
12 changed files with 5425 additions and 0 deletions

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

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

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