diff --git a/ts_web/elements/dees-statsgrid.demo.ts b/ts_web/elements/dees-statsgrid.demo.ts new file mode 100644 index 0000000..67ce39a --- /dev/null +++ b/ts_web/elements/dees-statsgrid.demo.ts @@ -0,0 +1,389 @@ +import { html, cssManager } from '@design.estate/dees-element'; +import type { IStatsTile } from './dees-statsgrid.js'; + +export const demoFunc = () => { + // Demo data with different tile types + const demoTiles: IStatsTile[] = [ + { + id: 'revenue', + title: 'Total Revenue', + value: 125420, + unit: '$', + type: 'number', + icon: 'faDollarSign', + description: '+12.5% from last month', + color: '#22c55e', + actions: [ + { + name: 'View Details', + iconName: 'faChartLine', + action: async () => { + console.log('Viewing revenue details for tile:', 'revenue'); + console.log('Current value:', 125420); + alert(`Revenue Details: $125,420 (+12.5%)`); + } + }, + { + name: 'Export Data', + iconName: 'faFileExport', + action: async () => { + console.log('Exporting revenue data'); + alert('Revenue data exported to CSV'); + } + } + ] + }, + { + id: 'users', + title: 'Active Users', + value: 3847, + type: 'number', + icon: 'faUsers', + description: '324 new this week', + actions: [ + { + name: 'View User List', + iconName: 'faList', + action: async () => { + console.log('Viewing user list'); + } + } + ] + }, + { + id: 'cpu', + title: 'CPU Usage', + value: 73, + type: 'gauge', + icon: 'faMicrochip', + gaugeOptions: { + min: 0, + max: 100, + thresholds: [ + { value: 0, color: '#22c55e' }, + { value: 60, color: '#f59e0b' }, + { value: 80, color: '#ef4444' } + ] + } + }, + { + id: 'storage', + title: 'Storage Used', + value: 65, + type: 'percentage', + icon: 'faHardDrive', + description: '650 GB of 1 TB', + color: '#3b82f6' + }, + { + id: 'memory', + title: 'Memory Usage', + value: 45, + type: 'gauge', + icon: 'faMemory', + gaugeOptions: { + min: 0, + max: 100, + thresholds: [ + { value: 0, color: '#22c55e' }, + { value: 70, color: '#f59e0b' }, + { value: 90, color: '#ef4444' } + ] + } + }, + { + id: 'requests', + title: 'API Requests', + value: '1.2k', + unit: '/min', + type: 'trend', + icon: 'faServer', + trendData: [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 92] + }, + { + id: 'uptime', + title: 'System Uptime', + value: '99.95%', + type: 'text', + icon: 'faCheckCircle', + color: '#22c55e', + description: 'Last 30 days' + }, + { + id: 'latency', + title: 'Response Time', + value: 142, + unit: 'ms', + type: 'trend', + icon: 'faClock', + trendData: [150, 145, 148, 142, 138, 140, 135, 145, 142], + description: 'P95 latency' + }, + { + id: 'errors', + title: 'Error Rate', + value: 0.03, + unit: '%', + type: 'number', + icon: 'faExclamationTriangle', + color: '#ef4444', + actions: [ + { + name: 'View Error Logs', + iconName: 'faFileAlt', + action: async () => { + console.log('Viewing error logs'); + } + } + ] + } + ]; + + // Grid actions for the demo + const gridActions = [ + { + name: 'Refresh', + iconName: 'faSync', + action: async () => { + console.log('Refreshing stats...'); + // Simulate refresh animation + const grid = document.querySelector('dees-statsgrid'); + if (grid) { + grid.style.opacity = '0.5'; + setTimeout(() => { + grid.style.opacity = '1'; + }, 500); + } + } + }, + { + name: 'Export Report', + iconName: 'faFileExport', + action: async () => { + console.log('Exporting stats report...'); + } + }, + { + name: 'Settings', + iconName: 'faCog', + action: async () => { + console.log('Opening settings...'); + } + } + ]; + + return html` + + +
+ + +
+

Full Featured Stats Grid

+

+ A comprehensive dashboard with various tile types, actions, and real-time updates. +

+ +
+ +
+

Compact Grid (Smaller Tiles)

+

+ Same data displayed with smaller minimum tile width for more compact layouts. +

+ +
+ +
+

Simple Metrics (No Actions)

+

+ Clean display without interactive elements for pure visualization. +

+ +
+ +
+

Performance Monitoring

+

+ Real-time performance metrics with gauge visualizations and thresholds. +

+ { + console.log('Starting auto refresh...'); + } + } + ]} + .minTileWidth=${280} + .gap=${20} + > +
+ + +
+ `; +}; \ No newline at end of file diff --git a/ts_web/elements/dees-statsgrid.ts b/ts_web/elements/dees-statsgrid.ts new file mode 100644 index 0000000..f1efd40 --- /dev/null +++ b/ts_web/elements/dees-statsgrid.ts @@ -0,0 +1,492 @@ +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: flex-start; + margin-bottom: 12px; + } + + .tile-title { + font-size: 14px; + font-weight: 500; + color: ${cssManager.bdTheme('#666', '#aaa')}; + margin: 0; + } + + .tile-icon { + opacity: 0.6; + } + + .tile-content { + min-height: 60px; + display: flex; + flex-direction: column; + justify-content: center; + } + + .tile-value { + font-size: 32px; + font-weight: 600; + color: ${cssManager.bdTheme('#333', '#fff')}; + line-height: 1.2; + display: flex; + align-items: baseline; + gap: 6px; + } + + .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: 120px; + position: relative; + } + + .gauge-svg { + width: 100%; + height: 100%; + } + + .gauge-background { + fill: none; + stroke: ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; + stroke-width: 8; + } + + .gauge-fill { + fill: none; + stroke-width: 8; + stroke-linecap: round; + transition: stroke-dashoffset 0.5s ease; + } + + .gauge-text { + fill: ${cssManager.bdTheme('#333', '#fff')}; + font-size: 24px; + font-weight: 600; + text-anchor: middle; + alignment-baseline: 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: 60px; + position: relative; + } + + .trend-svg { + width: 100%; + height: 100%; + } + + .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: 18px; + font-weight: 500; + color: ${cssManager.bdTheme('#333', '#fff')}; + } + + 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 = 251.2; // Circumference of circle with r=40 + 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 = 60; + 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); + } +} \ No newline at end of file diff --git a/ts_web/elements/index.ts b/ts_web/elements/index.ts index dac170b..7cc34f6 100644 --- a/ts_web/elements/index.ts +++ b/ts_web/elements/index.ts @@ -41,6 +41,7 @@ export * from './dees-simple-appdash.js'; export * from './dees-simple-login.js'; export * from './dees-speechbubble.js'; export * from './dees-spinner.js'; +export * from './dees-statsgrid.js'; export * from './dees-stepper.js'; export * from './dees-table.js'; export * from './dees-terminal.js';