- Persist 2FA state in sessionStorage to survive mobile page reloads - Reuse existing valid OTP on re-login to avoid rate limiting and duplicate emails - Add embedded sparkline charts to SolduriCompactCard with expand toggle - Mobile dashboard redesigned: 2 pages with enriched compact cards + cashflow type - Login UI simplified: remove gradient bg, subtitle, icon; use design tokens - Focus OTP input when session is restored from 2FA state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
741 lines
19 KiB
Vue
741 lines
19 KiB
Vue
<template>
|
|
<div class="metric-card cashflow-card">
|
|
<!-- Main values section - Split layout (Încasări | Plăți) -->
|
|
<div class="values-section">
|
|
<!-- Încasări Section -->
|
|
<div class="value-block inflows">
|
|
<div class="metric-label">Încasări</div>
|
|
<div class="metric-value text-success">
|
|
{{ formatCurrency(inflowsValue) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Divider -->
|
|
<div class="divider"></div>
|
|
|
|
<!-- Plăți Section -->
|
|
<div class="value-block outflows">
|
|
<div class="metric-label">Plăți</div>
|
|
<div class="metric-value text-error">
|
|
{{ formatCurrency(outflowsValue) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts toggle header -->
|
|
<div
|
|
v-if="hasSparklineData"
|
|
class="charts-toggle-header"
|
|
@click="toggleChartsExpanded"
|
|
>
|
|
<span>Grafice evoluție</span>
|
|
<i
|
|
class="pi pi-chevron-right"
|
|
:class="{ expanded: chartsExpanded }"
|
|
></i>
|
|
</div>
|
|
|
|
<!-- Dual sparkline charts - stacked vertical (collapsible) -->
|
|
<div v-show="chartsExpanded" class="charts-content">
|
|
<div class="sparkline-dual-container" v-if="hasSparklineData">
|
|
<!-- Grafic Încasări -->
|
|
<div class="sparkline-wrapper">
|
|
<div class="sparkline-title text-success">Încasări</div>
|
|
<div class="sparkline-chart">
|
|
<canvas ref="inflowsCanvas" class="sparkline-canvas"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grafic Plăți -->
|
|
<div class="sparkline-wrapper">
|
|
<div class="sparkline-title text-error">Plăți</div>
|
|
<div class="sparkline-chart">
|
|
<canvas ref="outflowsCanvas" class="sparkline-canvas"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cache Footer -->
|
|
<CacheFooter
|
|
:cache-hit="cacheInfo?.hit"
|
|
:response-time-ms="cacheInfo?.time"
|
|
:cache-source="cacheInfo?.source"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import {
|
|
ref,
|
|
computed,
|
|
watch,
|
|
onMounted,
|
|
onBeforeUnmount,
|
|
nextTick,
|
|
} from "vue";
|
|
import { Chart, registerables } from "chart.js";
|
|
import CacheFooter from "@/shared/components/CacheFooter.vue";
|
|
|
|
Chart.register(...registerables);
|
|
|
|
const props = defineProps({
|
|
inflowsValue: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
outflowsValue: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
inflowsTrend: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
outflowsTrend: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
inflowsSparkline: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
outflowsSparkline: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
inflowsPreviousSparkline: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
outflowsPreviousSparkline: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
sparklineLabels: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
previousSparklineLabels: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
cacheInfo: {
|
|
type: Object,
|
|
default: () => ({ hit: false, time: 0, source: null }),
|
|
},
|
|
});
|
|
|
|
// Refs pentru 2 canvas-uri separate
|
|
const inflowsCanvas = ref(null);
|
|
const outflowsCanvas = ref(null);
|
|
let inflowsChartInstance = null;
|
|
let outflowsChartInstance = null;
|
|
|
|
// Charts collapsible state
|
|
const chartsExpanded = ref(false);
|
|
|
|
const toggleChartsExpanded = () => {
|
|
chartsExpanded.value = !chartsExpanded.value;
|
|
};
|
|
|
|
// Format amount (without RON suffix)
|
|
const formatCurrency = (amount) => {
|
|
if (!amount && amount !== 0) return "0";
|
|
return new Intl.NumberFormat("ro-RO", {
|
|
style: "decimal",
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(Math.abs(amount));
|
|
};
|
|
|
|
// Check if sparkline data exists
|
|
const hasSparklineData = computed(() => {
|
|
return (
|
|
props.inflowsSparkline.length > 0 && props.outflowsSparkline.length > 0
|
|
);
|
|
});
|
|
|
|
// Initialize Încasări chart
|
|
const initializeInflowsChart = async () => {
|
|
if (!inflowsCanvas.value || props.inflowsSparkline.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Destroy existing chart
|
|
if (inflowsChartInstance) {
|
|
inflowsChartInstance.destroy();
|
|
inflowsChartInstance = null;
|
|
}
|
|
|
|
await nextTick();
|
|
|
|
// Double-check canvas is still available after nextTick
|
|
if (!inflowsCanvas.value) {
|
|
console.warn('[CashFlowMetricCard] Inflows canvas ref not available after nextTick');
|
|
return;
|
|
}
|
|
|
|
const ctx = inflowsCanvas.value.getContext("2d");
|
|
|
|
// Generate labels
|
|
const labels =
|
|
props.sparklineLabels.length > 0
|
|
? props.sparklineLabels
|
|
: props.inflowsSparkline.map((_, i) => `L${i + 1}`);
|
|
|
|
// Prepare datasets
|
|
const datasets = [
|
|
{
|
|
label: "Încasări (curent)",
|
|
data: props.inflowsSparkline,
|
|
borderColor: "#10b981",
|
|
backgroundColor: "rgba(16, 185, 129, 0.1)",
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4,
|
|
pointHoverBackgroundColor: "#10b981",
|
|
pointHoverBorderColor: "#ffffff",
|
|
pointHoverBorderWidth: 2,
|
|
},
|
|
];
|
|
|
|
// Add previous year dataset if available
|
|
if (
|
|
props.inflowsPreviousSparkline &&
|
|
props.inflowsPreviousSparkline.length > 0
|
|
) {
|
|
datasets.push({
|
|
label: "Încasări (anul precedent)",
|
|
data: props.inflowsPreviousSparkline,
|
|
borderColor: "rgba(16, 185, 129, 0.4)",
|
|
backgroundColor: "rgba(16, 185, 129, 0.05)",
|
|
borderWidth: 2,
|
|
borderDash: [5, 5],
|
|
fill: false,
|
|
tension: 0.4,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4,
|
|
pointHoverBackgroundColor: "rgba(16, 185, 129, 0.4)",
|
|
pointHoverBorderColor: "#ffffff",
|
|
pointHoverBorderWidth: 2,
|
|
});
|
|
}
|
|
|
|
// Calculate limits including both datasets
|
|
const allDataPoints = [...props.inflowsSparkline];
|
|
if (
|
|
props.inflowsPreviousSparkline &&
|
|
props.inflowsPreviousSparkline.length > 0
|
|
) {
|
|
allDataPoints.push(...props.inflowsPreviousSparkline);
|
|
}
|
|
const dataMin = Math.min(...allDataPoints);
|
|
const dataMax = Math.max(...allDataPoints);
|
|
const dataRange = dataMax - dataMin;
|
|
const dataMean =
|
|
allDataPoints.reduce((sum, val) => sum + val, 0) / allDataPoints.length;
|
|
|
|
// CORECT: Forțăm range minim SIMETRIC, apoi adăugăm padding
|
|
const minVisibleRange = dataMean * 0.25; // 25% din medie = range minim vizibil
|
|
const center = (dataMin + dataMax) / 2;
|
|
const targetRange = Math.max(dataRange, minVisibleRange);
|
|
|
|
// Calculează limite simetric față de centru
|
|
let calculatedMin = center - targetRange / 2;
|
|
let calculatedMax = center + targetRange / 2;
|
|
|
|
// Adaugă padding PESTE range-ul asigurat (nu înăuntru!)
|
|
const paddingAmount = targetRange * 0.1; // 10% padding suplimentar
|
|
|
|
// Aplică limitele finale (nu permite negative dacă toate datele sunt pozitive)
|
|
const allPositive = dataMin >= 0;
|
|
const yMin = allPositive
|
|
? Math.max(0, calculatedMin - paddingAmount)
|
|
: calculatedMin - paddingAmount;
|
|
const yMax = calculatedMax + paddingAmount;
|
|
|
|
inflowsChartInstance = new Chart(ctx, {
|
|
type: "line",
|
|
data: {
|
|
labels: labels,
|
|
datasets: datasets,
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: {
|
|
intersect: false,
|
|
mode: "index",
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: datasets.length > 1,
|
|
position: "top",
|
|
align: "end",
|
|
labels: {
|
|
boxWidth: 12,
|
|
boxHeight: 12,
|
|
padding: 8,
|
|
font: {
|
|
size: 10,
|
|
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
|
},
|
|
color: "rgba(107, 114, 128, 0.9)",
|
|
usePointStyle: true,
|
|
pointStyle: "line",
|
|
},
|
|
},
|
|
tooltip: {
|
|
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
titleColor: "#ffffff",
|
|
bodyColor: "#ffffff",
|
|
borderColor: "rgba(255, 255, 255, 0.2)",
|
|
borderWidth: 1,
|
|
cornerRadius: 6,
|
|
displayColors: true,
|
|
callbacks: {
|
|
title: (context) => context[0].label || "",
|
|
label: (context) => {
|
|
const value = context.parsed.y;
|
|
const label = context.dataset.label || "";
|
|
const formattedValue = new Intl.NumberFormat("ro-RO", {
|
|
style: "decimal",
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(value);
|
|
return `${label}: ${formattedValue}`;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
grid: {
|
|
display: false,
|
|
drawBorder: false,
|
|
},
|
|
ticks: {
|
|
color: "rgba(107, 114, 128, 0.7)",
|
|
font: {
|
|
size: 10,
|
|
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
|
},
|
|
maxRotation: 45,
|
|
minRotation: 45,
|
|
maxTicksLimit: 6,
|
|
},
|
|
border: {
|
|
display: false,
|
|
},
|
|
},
|
|
y: {
|
|
display: true,
|
|
min: yMin,
|
|
max: yMax,
|
|
grid: {
|
|
color: "rgba(107, 114, 128, 0.1)",
|
|
drawBorder: false,
|
|
},
|
|
ticks: {
|
|
color: "#10b981",
|
|
font: {
|
|
size: 11,
|
|
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
|
},
|
|
maxTicksLimit: 3,
|
|
callback: function (value) {
|
|
if (value >= 1000000) {
|
|
return (value / 1000000).toFixed(1) + "M";
|
|
} else if (value >= 1000) {
|
|
return (value / 1000).toFixed(0) + "k";
|
|
}
|
|
return value.toFixed(0);
|
|
},
|
|
},
|
|
border: {
|
|
display: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
// Initialize Plăți chart
|
|
const initializeOutflowsChart = async () => {
|
|
if (!outflowsCanvas.value || props.outflowsSparkline.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Destroy existing chart
|
|
if (outflowsChartInstance) {
|
|
outflowsChartInstance.destroy();
|
|
outflowsChartInstance = null;
|
|
}
|
|
|
|
await nextTick();
|
|
|
|
// Double-check canvas is still available after nextTick
|
|
if (!outflowsCanvas.value) {
|
|
console.warn('[CashFlowMetricCard] Outflows canvas ref not available after nextTick');
|
|
return;
|
|
}
|
|
|
|
const ctx = outflowsCanvas.value.getContext("2d");
|
|
|
|
// Generate labels
|
|
const labels =
|
|
props.sparklineLabels.length > 0
|
|
? props.sparklineLabels
|
|
: props.outflowsSparkline.map((_, i) => `L${i + 1}`);
|
|
|
|
// Prepare datasets
|
|
const datasets = [
|
|
{
|
|
label: "Plăți (curent)",
|
|
data: props.outflowsSparkline,
|
|
borderColor: "#ef4444",
|
|
backgroundColor: "rgba(239, 68, 68, 0.1)",
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4,
|
|
pointHoverBackgroundColor: "#ef4444",
|
|
pointHoverBorderColor: "#ffffff",
|
|
pointHoverBorderWidth: 2,
|
|
},
|
|
];
|
|
|
|
// Add previous year dataset if available
|
|
if (
|
|
props.outflowsPreviousSparkline &&
|
|
props.outflowsPreviousSparkline.length > 0
|
|
) {
|
|
datasets.push({
|
|
label: "Plăți (anul precedent)",
|
|
data: props.outflowsPreviousSparkline,
|
|
borderColor: "rgba(239, 68, 68, 0.4)",
|
|
backgroundColor: "rgba(239, 68, 68, 0.05)",
|
|
borderWidth: 2,
|
|
borderDash: [5, 5],
|
|
fill: false,
|
|
tension: 0.4,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4,
|
|
pointHoverBackgroundColor: "rgba(239, 68, 68, 0.4)",
|
|
pointHoverBorderColor: "#ffffff",
|
|
pointHoverBorderWidth: 2,
|
|
});
|
|
}
|
|
|
|
// Calculate limits including both datasets
|
|
const allDataPoints = [...props.outflowsSparkline];
|
|
if (
|
|
props.outflowsPreviousSparkline &&
|
|
props.outflowsPreviousSparkline.length > 0
|
|
) {
|
|
allDataPoints.push(...props.outflowsPreviousSparkline);
|
|
}
|
|
const dataMin = Math.min(...allDataPoints);
|
|
const dataMax = Math.max(...allDataPoints);
|
|
const dataRange = dataMax - dataMin;
|
|
const dataMean =
|
|
allDataPoints.reduce((sum, val) => sum + val, 0) / allDataPoints.length;
|
|
|
|
// CORECT: Forțăm range minim SIMETRIC, apoi adăugăm padding
|
|
const minVisibleRange = dataMean * 0.25; // 25% din medie = range minim vizibil
|
|
const center = (dataMin + dataMax) / 2;
|
|
const targetRange = Math.max(dataRange, minVisibleRange);
|
|
|
|
// Calculează limite simetric față de centru
|
|
let calculatedMin = center - targetRange / 2;
|
|
let calculatedMax = center + targetRange / 2;
|
|
|
|
// Adaugă padding PESTE range-ul asigurat (nu înăuntru!)
|
|
const paddingAmount = targetRange * 0.1; // 10% padding suplimentar
|
|
|
|
// Aplică limitele finale (nu permite negative dacă toate datele sunt pozitive)
|
|
const allPositive = dataMin >= 0;
|
|
const yMin = allPositive
|
|
? Math.max(0, calculatedMin - paddingAmount)
|
|
: calculatedMin - paddingAmount;
|
|
const yMax = calculatedMax + paddingAmount;
|
|
|
|
outflowsChartInstance = new Chart(ctx, {
|
|
type: "line",
|
|
data: {
|
|
labels: labels,
|
|
datasets: datasets,
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: {
|
|
intersect: false,
|
|
mode: "index",
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: datasets.length > 1,
|
|
position: "top",
|
|
align: "end",
|
|
labels: {
|
|
boxWidth: 12,
|
|
boxHeight: 12,
|
|
padding: 8,
|
|
font: {
|
|
size: 10,
|
|
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
|
},
|
|
color: "rgba(107, 114, 128, 0.9)",
|
|
usePointStyle: true,
|
|
pointStyle: "line",
|
|
},
|
|
},
|
|
tooltip: {
|
|
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
|
titleColor: "#ffffff",
|
|
bodyColor: "#ffffff",
|
|
borderColor: "rgba(255, 255, 255, 0.2)",
|
|
borderWidth: 1,
|
|
cornerRadius: 6,
|
|
displayColors: true,
|
|
callbacks: {
|
|
title: (context) => context[0].label || "",
|
|
label: (context) => {
|
|
const value = context.parsed.y;
|
|
const label = context.dataset.label || "";
|
|
const formattedValue = new Intl.NumberFormat("ro-RO", {
|
|
style: "decimal",
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(value);
|
|
return `${label}: ${formattedValue}`;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: true,
|
|
grid: {
|
|
display: false,
|
|
drawBorder: false,
|
|
},
|
|
ticks: {
|
|
color: "rgba(107, 114, 128, 0.7)",
|
|
font: {
|
|
size: 10,
|
|
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
|
},
|
|
maxRotation: 45,
|
|
minRotation: 45,
|
|
maxTicksLimit: 6,
|
|
},
|
|
border: {
|
|
display: false,
|
|
},
|
|
},
|
|
y: {
|
|
display: true,
|
|
min: yMin,
|
|
max: yMax,
|
|
grid: {
|
|
color: "rgba(107, 114, 128, 0.1)",
|
|
drawBorder: false,
|
|
},
|
|
ticks: {
|
|
color: "#ef4444",
|
|
font: {
|
|
size: 11,
|
|
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
|
},
|
|
maxTicksLimit: 3,
|
|
callback: function (value) {
|
|
if (value >= 1000000) {
|
|
return (value / 1000000).toFixed(1) + "M";
|
|
} else if (value >= 1000) {
|
|
return (value / 1000).toFixed(0) + "k";
|
|
}
|
|
return value.toFixed(0);
|
|
},
|
|
},
|
|
border: {
|
|
display: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
// Watch for data changes
|
|
watch(
|
|
() => [
|
|
props.inflowsSparkline,
|
|
props.outflowsSparkline,
|
|
props.sparklineLabels,
|
|
props.inflowsPreviousSparkline,
|
|
props.outflowsPreviousSparkline,
|
|
props.previousSparklineLabels,
|
|
],
|
|
async () => {
|
|
await Promise.all([initializeInflowsChart(), initializeOutflowsChart()]);
|
|
},
|
|
{ deep: true },
|
|
);
|
|
|
|
// Lifecycle hooks
|
|
onMounted(async () => {
|
|
await Promise.all([initializeInflowsChart(), initializeOutflowsChart()]);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
if (inflowsChartInstance) {
|
|
inflowsChartInstance.destroy();
|
|
inflowsChartInstance = null;
|
|
}
|
|
if (outflowsChartInstance) {
|
|
outflowsChartInstance.destroy();
|
|
outflowsChartInstance = null;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Component-specific: Dual-chart layout for CashFlowMetricCard */
|
|
|
|
/* Ultra-compact cashflow card */
|
|
|
|
/* Metric label and value typography */
|
|
.metric-label {
|
|
font-size: var(--text-sm);
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--font-bold);
|
|
font-family: var(--font-mono, monospace);
|
|
}
|
|
|
|
/* Split layout: Încasări | Divider | Plăți */
|
|
.values-section {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto 1fr;
|
|
gap: 2px;
|
|
align-items: start;
|
|
}
|
|
|
|
.value-block {
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.divider {
|
|
width: 1px;
|
|
height: 100%;
|
|
border-left: 1px dotted var(--color-border);
|
|
background: none;
|
|
}
|
|
|
|
/* Charts toggle header */
|
|
.charts-toggle-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: var(--space-sm) var(--space-md);
|
|
margin-top: var(--space-sm);
|
|
background: var(--surface-hover);
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--font-medium);
|
|
color: var(--color-text-secondary);
|
|
transition: background-color var(--transition-fast);
|
|
}
|
|
|
|
.charts-toggle-header:hover {
|
|
background: var(--surface-border);
|
|
}
|
|
|
|
.charts-toggle-header i {
|
|
transition: transform var(--transition-fast);
|
|
}
|
|
|
|
.charts-toggle-header i.expanded {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
/* Charts content wrapper */
|
|
.charts-content {
|
|
margin-top: var(--space-sm);
|
|
}
|
|
|
|
/* Dual sparkline container (unique to this card) */
|
|
.sparkline-dual-container {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.sparkline-wrapper {
|
|
width: 100%;
|
|
background: var(--color-bg-secondary);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: 4px;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.sparkline-chart {
|
|
width: 100%;
|
|
height: 150px;
|
|
position: relative;
|
|
}
|
|
|
|
/* Chart.js canvas sizing (required for proper rendering) */
|
|
.sparkline-canvas {
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
display: block;
|
|
}
|
|
|
|
/* Responsive: Stack vertically on mobile */
|
|
@media (max-width: 768px) {
|
|
.values-section {
|
|
grid-template-columns: 1fr;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.divider {
|
|
width: 100%;
|
|
height: 1px;
|
|
min-height: 1px;
|
|
}
|
|
|
|
.sparkline-chart {
|
|
height: 130px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.sparkline-chart {
|
|
height: 150px;
|
|
}
|
|
|
|
.sparkline-wrapper {
|
|
padding: 0.25rem;
|
|
}
|
|
}
|
|
</style>
|