import { demoFunc } from './dees-statsgrid.demo.js'; import * as plugins from './00plugins.js'; import { customElement, html, DeesElement, property, state, css, unsafeCSS, cssManager, } from '@design.estate/dees-element'; import type { TemplateResult } from '@design.estate/dees-element'; import './dees-icon.js'; import './dees-contextmenu.js'; import './dees-button.js'; declare global { interface HTMLElementTagNameMap { 'dees-statsgrid': DeesStatsGrid; } } export interface IStatsTile { id: string; title: string; value: number | string; unit?: string; type: 'number' | 'gauge' | 'percentage' | 'trend' | 'text'; // For gauge type gaugeOptions?: { min: number; max: number; thresholds?: Array<{value: number; color: string}>; }; // For trend type trendData?: number[]; // Visual customization color?: string; icon?: string; description?: string; // Tile-specific actions actions?: plugins.tsclass.website.IMenuItem[]; } @customElement('dees-statsgrid') export class DeesStatsGrid extends DeesElement { public static demo = demoFunc; @property({ type: Array }) public tiles: IStatsTile[] = []; @property({ type: Number }) public minTileWidth: number = 250; @property({ type: Number }) public gap: number = 16; @property({ type: Array }) public gridActions: plugins.tsclass.website.IMenuItem[] = []; @state() private contextMenuVisible = false; @state() private contextMenuPosition = { x: 0, y: 0 }; @state() private contextMenuActions: plugins.tsclass.website.IMenuItem[] = []; public static styles = [ cssManager.defaultStyles, css` :host { display: block; width: 100%; } .grid-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: ${unsafeCSS(16)}px; min-height: 32px; } .grid-title { font-size: 18px; font-weight: 600; color: ${cssManager.bdTheme('#333', '#fff')}; } .grid-actions { display: flex; gap: 8px; } .grid-actions dees-button { font-size: 14px; min-width: auto; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(${unsafeCSS(250)}px, 1fr)); gap: ${unsafeCSS(16)}px; width: 100%; } .stats-tile { background: ${cssManager.bdTheme('#fff', '#1a1a1a')}; border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; border-radius: 12px; padding: 20px; transition: all 0.3s ease; cursor: pointer; position: relative; overflow: hidden; } .stats-tile:hover { transform: translateY(-2px); box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; border-color: ${cssManager.bdTheme('#d0d0d0', '#3a3a3a')}; } .stats-tile.clickable { cursor: pointer; } .tile-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; width: 100%; } .tile-title { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('#666', '#aaa')}; margin: 0; } .tile-icon { opacity: 0.6; } .tile-content { height: 90px; display: flex; flex-direction: column; justify-content: center; align-items: center; position: relative; } .tile-value { font-size: 32px; font-weight: 600; color: ${cssManager.bdTheme('#333', '#fff')}; line-height: 1.2; display: flex; align-items: baseline; justify-content: center; gap: 6px; width: 100%; } .tile-unit { font-size: 18px; font-weight: 400; color: ${cssManager.bdTheme('#666', '#aaa')}; } .tile-description { font-size: 12px; color: ${cssManager.bdTheme('#888', '#777')}; margin-top: 8px; } .gauge-container { width: 100%; height: 80px; position: relative; display: flex; align-items: center; justify-content: center; } .gauge-svg { width: 100%; height: 100%; } .gauge-background { fill: none; stroke: ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; stroke-width: 6; } .gauge-fill { fill: none; stroke-width: 6; stroke-linecap: round; transition: stroke-dashoffset 0.5s ease; } .gauge-text { fill: ${cssManager.bdTheme('#333', '#fff')}; font-size: 18px; font-weight: 600; text-anchor: middle; } .percentage-container { width: 100%; height: 24px; background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')}; border-radius: 12px; overflow: hidden; position: relative; } .percentage-fill { height: 100%; background: ${cssManager.bdTheme('#0084ff', '#0066cc')}; transition: width 0.5s ease; border-radius: 12px; } .percentage-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 12px; font-weight: 600; color: ${cssManager.bdTheme('#333', '#fff')}; } .trend-container { width: 100%; height: 100%; position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 4px; } .trend-svg { width: 100%; height: 40px; flex-shrink: 0; } .trend-line { fill: none; stroke: ${cssManager.bdTheme('#0084ff', '#0066cc')}; stroke-width: 2; } .trend-area { fill: ${cssManager.bdTheme('rgba(0, 132, 255, 0.1)', 'rgba(0, 102, 204, 0.2)')}; } .text-value { font-size: 32px; font-weight: 600; color: ${cssManager.bdTheme('#333', '#fff')}; } .trend-value { font-size: 32px; font-weight: 600; color: ${cssManager.bdTheme('#333', '#fff')}; display: flex; align-items: baseline; gap: 6px; } .trend-value .tile-unit { font-size: 18px; } dees-contextmenu { position: fixed; z-index: 1000; } `, ]; constructor() { super(); } public render(): TemplateResult { return html` ${this.gridActions.length > 0 ? html`
Statistics
${this.gridActions.map(action => html` this.handleGridAction(action)}> ${action.iconName ? html`` : ''} ${action.name} `)}
` : ''}
${this.tiles.map(tile => this.renderTile(tile))}
${this.contextMenuVisible ? html` this.contextMenuVisible = false} > ` : ''} `; } private renderTile(tile: IStatsTile): TemplateResult { const hasActions = tile.actions && tile.actions.length > 0; const clickable = hasActions && tile.actions.length === 1; return html`
this.handleTileAction(tile.actions![0], tile) : undefined} @contextmenu=${hasActions ? (e: MouseEvent) => this.showContextMenu(e, tile) : undefined} >

${tile.title}

${tile.icon ? html` ` : ''}
${this.renderTileContent(tile)}
${tile.description ? html`
${tile.description}
` : ''}
`; } private renderTileContent(tile: IStatsTile): TemplateResult { switch (tile.type) { case 'number': return html`
${tile.value} ${tile.unit ? html`${tile.unit}` : ''}
`; case 'gauge': return this.renderGauge(tile); case 'percentage': return this.renderPercentage(tile); case 'trend': return this.renderTrend(tile); case 'text': return html`
${tile.value}
`; default: return html`
${tile.value}
`; } } private renderGauge(tile: IStatsTile): TemplateResult { const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value); const options = tile.gaugeOptions || { min: 0, max: 100 }; const percentage = ((value - options.min) / (options.max - options.min)) * 100; const strokeDasharray = 188.5; // Circumference of circle with r=30 const strokeDashoffset = strokeDasharray - (strokeDasharray * percentage) / 100; let strokeColor = tile.color || cssManager.bdTheme('#0084ff', '#0066cc'); if (options.thresholds) { for (const threshold of options.thresholds.reverse()) { if (value >= threshold.value) { strokeColor = threshold.color; break; } } } return html`
${value}${tile.unit || ''}
`; } private renderPercentage(tile: IStatsTile): TemplateResult { const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value); const percentage = Math.min(100, Math.max(0, value)); return html`
${percentage}%
`; } private renderTrend(tile: IStatsTile): TemplateResult { if (!tile.trendData || tile.trendData.length < 2) { return html`
${tile.value}
`; } const data = tile.trendData; const max = Math.max(...data); const min = Math.min(...data); const range = max - min || 1; const width = 200; const height = 40; const points = data.map((value, index) => { const x = (index / (data.length - 1)) * width; const y = height - ((value - min) / range) * height; return `${x},${y}`; }).join(' '); const areaPoints = `0,${height} ${points} ${width},${height}`; return html`
${tile.value} ${tile.unit ? html`${tile.unit}` : ''}
`; } private async handleGridAction(action: plugins.tsclass.website.IMenuItem) { if (action.action) { await action.action(); } } private async handleTileAction(action: plugins.tsclass.website.IMenuItem, _tile: IStatsTile) { if (action.action) { await action.action(); } // Note: tile data is available through closure when defining actions } private showContextMenu(event: MouseEvent, tile: IStatsTile) { if (!tile.actions || tile.actions.length === 0) return; event.preventDefault(); this.contextMenuPosition = { x: event.clientX, y: event.clientY }; this.contextMenuActions = tile.actions; this.contextMenuVisible = true; // Close context menu on click outside const closeHandler = () => { this.contextMenuVisible = false; document.removeEventListener('click', closeHandler); }; setTimeout(() => { document.addEventListener('click', closeHandler); }, 100); } }