|
|
class CustomStatCard extends HTMLElement { |
|
|
constructor() { |
|
|
super(); |
|
|
} |
|
|
|
|
|
connectedCallback() { |
|
|
this.attachShadow({ mode: 'open' }); |
|
|
|
|
|
const title = this.getAttribute('title') || 'Metric'; |
|
|
const value = this.getAttribute('value') || '0'; |
|
|
const color = this.getAttribute('color') || 'green'; |
|
|
const icon = this.getAttribute('icon') || 'activity'; |
|
|
const trend = this.getAttribute('trend') || ''; |
|
|
|
|
|
const colorMap = { |
|
|
green: '#22c55e', |
|
|
orange: '#f97316', |
|
|
gold: '#ffd700', |
|
|
red: '#ef4444', |
|
|
blue: '#3b82f6', |
|
|
purple: '#a855f7' |
|
|
}; |
|
|
|
|
|
const accentColor = colorMap[color] || colorMap.green; |
|
|
|
|
|
let trendHtml = ''; |
|
|
if (trend) { |
|
|
const isPositive = trend.includes('+') || trend === 'record'; |
|
|
const isNegative = trend.includes('-'); |
|
|
const isNeutral = trend === 'stable'; |
|
|
|
|
|
if (isPositive) { |
|
|
trendHtml = `<div class="text-xs text-neon-green mt-1 flex items-center"> |
|
|
<svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline><polyline points="17 6 23 6 23 12"></polyline></svg> |
|
|
${trend} |
|
|
</div>`; |
|
|
} else if (isNegative) { |
|
|
trendHtml = `<div class="text-xs text-neon-red mt-1 flex items-center"> |
|
|
<svg class="w-3 h-3 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 18 13.5 8.5 8.5 13.5 1 6"></polyline><polyline points="17 18 23 18 23 12"></polyline></svg> |
|
|
${trend} |
|
|
</div>`; |
|
|
} else { |
|
|
trendHtml = `<div class="text-xs text-gray-400 mt-1">${trend}</div>`; |
|
|
} |
|
|
} |
|
|
|
|
|
this.shadowRoot.innerHTML = ` |
|
|
<style> |
|
|
:host { |
|
|
display: block; |
|
|
} |
|
|
.card { |
|
|
background-color: #0a0a0a; |
|
|
border: 1px solid #1f2937; |
|
|
border-radius: 0.5rem; |
|
|
padding: 1rem; |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.card:hover { |
|
|
border-color: ${accentColor}; |
|
|
box-shadow: 0 0 20px rgba(${parseInt(accentColor.slice(1,3),16)}, ${parseInt(accentColor.slice(3,5),16)}, ${parseInt(accentColor.slice(5,7),16)}, 0.15); |
|
|
} |
|
|
.icon-bg { |
|
|
position: absolute; |
|
|
right: 8px; |
|
|
top: 8px; |
|
|
opacity: 0.1; |
|
|
transition: opacity 0.2s; |
|
|
} |
|
|
.card:hover .icon-bg { |
|
|
opacity: 0.2; |
|
|
} |
|
|
.title { |
|
|
color: #9ca3af; |
|
|
font-size: 0.65rem; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.1em; |
|
|
font-weight: 700; |
|
|
} |
|
|
.value { |
|
|
color: #fff; |
|
|
font-size: 1.25rem; |
|
|
font-weight: 700; |
|
|
margin-top: 0.5rem; |
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; |
|
|
} |
|
|
</style> |
|
|
<div class="card"> |
|
|
<div class="icon-bg"> |
|
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="${accentColor}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-feather="${icon}"></svg> |
|
|
</div> |
|
|
<div class="title">${title}</div> |
|
|
<div class="value">${value}</div> |
|
|
${trendHtml} |
|
|
</div> |
|
|
`; |
|
|
|
|
|
|
|
|
if (typeof feather !== 'undefined') { |
|
|
feather.replace(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
customElements.define('custom-stat-card', CustomStatCard); |