Spaces:
Running
Running
| class MetricCard extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.attachShadow({ mode: 'open' }); | |
| } | |
| static get observedAttributes() { | |
| return ['title', 'value', 'icon', 'color', 'trend', 'desc']; | |
| } | |
| connectedCallback() { | |
| this.render(); | |
| // Listen for updates to simulate "live" data | |
| document.addEventListener('update-metrics', () => { | |
| if(Math.random() > 0.5) { | |
| this.perturbValue(); | |
| } | |
| }); | |
| } | |
| attributeChangedCallback(name, oldValue, newValue) { | |
| if (oldValue !== newValue) { | |
| this.render(); | |
| } | |
| } | |
| perturbValue() { | |
| const valueDisplay = this.shadowRoot.getElementById('metric-value'); | |
| const trendDisplay = this.shadowRoot.getElementById('metric-trend'); | |
| // Slight random fluctuation visual only | |
| const original = valueDisplay.innerText; | |
| valueDisplay.style.opacity = '0.7'; | |
| setTimeout(() => { | |
| valueDisplay.style.opacity = '1'; | |
| // Sometimes change trend sign | |
| if(Math.random() > 0.8) { | |
| const isPos = trendDisplay.innerText.includes('+'); | |
| trendDisplay.innerText = isPos ? trendDisplay.innerText.replace('+', '-') : trendDisplay.innerText.replace('-', '+'); | |
| trendDisplay.className = isPos ? 'text-xs text-red-400' : 'text-xs text-ai-green'; | |
| } | |
| }, 200); | |
| } | |
| render() { | |
| const title = this.getAttribute('title') || 'Metric'; | |
| const value = this.getAttribute('value') || '0'; | |
| const icon = this.getAttribute('icon') || 'activity'; | |
| const color = this.getAttribute('color') || 'text-white'; | |
| const trend = this.getAttribute('trend') || '0%'; | |
| const desc = this.getAttribute('desc') || ''; | |
| this.shadowRoot.innerHTML = ` | |
| <style> | |
| :host { | |
| display: block; | |
| } | |
| .card { | |
| background-color: #1e293b; | |
| border: 1px solid #334155; | |
| border-radius: 0.75rem; | |
| padding: 1.5rem; | |
| transition: transform 0.2s, box-shadow 0.2s; | |
| height: 100%; | |
| } | |
| .card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.3); | |
| border-color: #10b981; | |
| } | |
| .icon-box { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 8px; | |
| background-color: rgba(16, 185, 129, 0.1); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-bottom: 1rem; | |
| } | |
| </style> | |
| <div class="card"> | |
| <div class="flex justify-between items-start"> | |
| <div> | |
| <p class="text-slate-400 text-sm font-medium mb-1">${title}</p> | |
| <h3 id="metric-value" class="text-3xl font-bold text-white transition-opacity duration-200">${value}</h3> | |
| </div> | |
| <div class="icon-box"> | |
| <i data-feather="${icon}" class="${color}"></i> | |
| </div> | |
| </div> | |
| <div class="mt-4 flex items-center justify-between"> | |
| <span id="metric-trend" class="text-xs text-ai-green font-bold bg-green-900/30 px-2 py-1 rounded"> | |
| ${trend} | |
| </span> | |
| <span class="text-xs text-slate-500">${desc}</span> | |
| </div> | |
| </div> | |
| `; | |
| // Initialize icon | |
| if (window.feather) { | |
| window.feather.replace(); | |
| } | |
| } | |
| } | |
| customElements.define('metric-card', MetricCard); |