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`
+
+ ` : ''}
+
+
+ ${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}
+ >
+
+
+
+ ${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`
+
+
+
+ `;
+ }
+
+ 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`
+
+ `;
+ }
+
+ 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';