| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  | import { demoFunc } from './dees-statsgrid.demo.js'; | 
					
						
							|  |  |  | import * as plugins from './00plugins.js'; | 
					
						
							| 
									
										
										
										
											2025-06-27 17:32:01 +00:00
										 |  |  | import { cssGeistFontFamily } from './00fonts.js'; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  | 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%; | 
					
						
							| 
									
										
										
										
											2025-06-27 17:32:01 +00:00
										 |  |  |         font-family: ${cssGeistFontFamily}; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       /* CSS Variables for consistent spacing and sizing */ | 
					
						
							|  |  |  |       :host { | 
					
						
							|  |  |  |         --grid-gap: 16px; | 
					
						
							|  |  |  |         --tile-padding: 24px; | 
					
						
							|  |  |  |         --header-spacing: 16px; | 
					
						
							|  |  |  |         --content-min-height: 48px; | 
					
						
							|  |  |  |         --value-font-size: 30px; | 
					
						
							|  |  |  |         --unit-font-size: 16px; | 
					
						
							|  |  |  |         --label-font-size: 13px; | 
					
						
							|  |  |  |         --title-font-size: 14px; | 
					
						
							|  |  |  |         --description-spacing: 12px; | 
					
						
							|  |  |  |         --border-radius: 8px; | 
					
						
							|  |  |  |         --transition-duration: 0.15s; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       /* Grid Layout */ | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       .grid-header { | 
					
						
							|  |  |  |         display: flex; | 
					
						
							|  |  |  |         justify-content: space-between; | 
					
						
							|  |  |  |         align-items: center; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         margin-bottom: calc(var(--grid-gap) * 1.5); | 
					
						
							|  |  |  |         min-height: 40px; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .grid-title { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         font-size: 16px; | 
					
						
							|  |  |  |         font-weight: 500; | 
					
						
							|  |  |  |         color: ${cssManager.bdTheme('#09090b', '#fafafa')}; | 
					
						
							|  |  |  |         letter-spacing: -0.01em; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .grid-actions { | 
					
						
							|  |  |  |         display: flex; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         gap: 6px; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .grid-actions dees-button { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         font-size: var(--label-font-size); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .stats-grid { | 
					
						
							|  |  |  |         display: grid; | 
					
						
							|  |  |  |         grid-template-columns: repeat(auto-fit, minmax(${unsafeCSS(250)}px, 1fr)); | 
					
						
							|  |  |  |         gap: ${unsafeCSS(16)}px; | 
					
						
							|  |  |  |         width: 100%; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       /* Tile Base Styles */ | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       .stats-tile { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         background: ${cssManager.bdTheme('#ffffff', '#09090b')}; | 
					
						
							|  |  |  |         border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 11.8%)')}; | 
					
						
							|  |  |  |         border-radius: var(--border-radius); | 
					
						
							|  |  |  |         padding: var(--tile-padding); | 
					
						
							|  |  |  |         transition: all var(--transition-duration) ease; | 
					
						
							|  |  |  |         cursor: default; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         position: relative; | 
					
						
							|  |  |  |         overflow: hidden; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         display: flex; | 
					
						
							|  |  |  |         flex-direction: column; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .stats-tile:hover { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 10.2%)')}; | 
					
						
							|  |  |  |         border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 16.8%)')}; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .stats-tile.clickable { | 
					
						
							|  |  |  |         cursor: pointer; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       .stats-tile.clickable:hover { | 
					
						
							|  |  |  |         transform: translateY(-1px); | 
					
						
							|  |  |  |         box-shadow: 0 2px 8px ${cssManager.bdTheme('rgba(0,0,0,0.04)', 'rgba(0,0,0,0.2)')}; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       /* Tile Header */ | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       .tile-header { | 
					
						
							|  |  |  |         display: flex; | 
					
						
							|  |  |  |         justify-content: space-between; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         align-items: flex-start; | 
					
						
							|  |  |  |         margin-bottom: var(--header-spacing); | 
					
						
							|  |  |  |         flex-shrink: 0; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .tile-title { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         font-size: var(--title-font-size); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         font-weight: 500; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         margin: 0; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         letter-spacing: -0.01em; | 
					
						
							|  |  |  |         line-height: 1.2; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .tile-icon { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         opacity: 0.7; | 
					
						
							|  |  |  |         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; | 
					
						
							|  |  |  |         font-size: 16px; | 
					
						
							|  |  |  |         flex-shrink: 0; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       /* Tile Content */ | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       .tile-content { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         min-height: var(--content-min-height); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         display: flex; | 
					
						
							|  |  |  |         flex-direction: column; | 
					
						
							|  |  |  |         justify-content: center; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         flex: 1; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .tile-value { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         font-size: var(--value-font-size); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         font-weight: 600; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | 
					
						
							| 
									
										
										
										
											2025-06-27 17:32:01 +00:00
										 |  |  |         line-height: 1.1; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         display: flex; | 
					
						
							|  |  |  |         align-items: baseline; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         gap: 4px; | 
					
						
							|  |  |  |         letter-spacing: -0.025em; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .tile-unit { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         font-size: var(--unit-font-size); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         font-weight: 400; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; | 
					
						
							|  |  |  |         letter-spacing: -0.01em; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .tile-description { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         font-size: var(--label-font-size); | 
					
						
							|  |  |  |         color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; | 
					
						
							|  |  |  |         margin-top: var(--description-spacing); | 
					
						
							|  |  |  |         letter-spacing: -0.01em; | 
					
						
							|  |  |  |         flex-shrink: 0; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       /* Gauge Styles */ | 
					
						
							|  |  |  |       .gauge-wrapper { | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         width: 100%; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:58:05 +00:00
										 |  |  |         display: flex; | 
					
						
							|  |  |  |         justify-content: center; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         align-items: center; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .gauge-container { | 
					
						
							|  |  |  |         width: 140px; | 
					
						
							| 
									
										
										
										
											2025-06-27 13:44:36 +00:00
										 |  |  |         height: 80px; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         position: relative; | 
					
						
							| 
									
										
										
										
											2025-06-27 13:44:36 +00:00
										 |  |  |         margin-top: -10px; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .gauge-svg { | 
					
						
							|  |  |  |         width: 100%; | 
					
						
							|  |  |  |         height: 100%; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .gauge-background { | 
					
						
							|  |  |  |         fill: none; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         stroke: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')}; | 
					
						
							|  |  |  |         stroke-width: 8; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .gauge-fill { | 
					
						
							|  |  |  |         fill: none; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         stroke-width: 8; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         stroke-linecap: round; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .gauge-text { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         fill: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | 
					
						
							| 
									
										
										
										
											2025-06-27 17:32:01 +00:00
										 |  |  |         font-family: ${cssGeistFontFamily}; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         font-size: var(--value-font-size); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         font-weight: 600; | 
					
						
							|  |  |  |         text-anchor: middle; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         letter-spacing: -0.025em; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       .gauge-unit { | 
					
						
							|  |  |  |         font-size: var(--unit-font-size); | 
					
						
							|  |  |  |         fill: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; | 
					
						
							|  |  |  |         font-weight: 400; | 
					
						
							| 
									
										
										
										
											2025-06-27 17:32:01 +00:00
										 |  |  |         font-family: ${cssGeistFontFamily}; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       /* Percentage Styles */ | 
					
						
							|  |  |  |       .percentage-wrapper { | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         width: 100%; | 
					
						
							|  |  |  |         position: relative; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       .percentage-value { | 
					
						
							|  |  |  |         font-size: var(--value-font-size); | 
					
						
							|  |  |  |         font-weight: 600; | 
					
						
							|  |  |  |         color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | 
					
						
							| 
									
										
										
										
											2025-06-27 17:32:01 +00:00
										 |  |  |         line-height: 1.1; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         letter-spacing: -0.025em; | 
					
						
							|  |  |  |         margin-bottom: 8px; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .percentage-bar { | 
					
						
							|  |  |  |         width: 100%; | 
					
						
							|  |  |  |         height: 8px; | 
					
						
							|  |  |  |         background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')}; | 
					
						
							|  |  |  |         border-radius: 4px; | 
					
						
							|  |  |  |         overflow: hidden; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       .percentage-fill { | 
					
						
							|  |  |  |         height: 100%; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         background: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | 
					
						
							|  |  |  |         transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); | 
					
						
							|  |  |  |         border-radius: 4px; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       /* Trend Styles */ | 
					
						
							|  |  |  |       .trend-container { | 
					
						
							|  |  |  |         width: 100%; | 
					
						
							|  |  |  |         display: flex; | 
					
						
							|  |  |  |         flex-direction: column; | 
					
						
							|  |  |  |         gap: 8px; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       .trend-header { | 
					
						
							|  |  |  |         display: flex; | 
					
						
							|  |  |  |         align-items: baseline; | 
					
						
							|  |  |  |         gap: 8px; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .trend-value { | 
					
						
							|  |  |  |         font-size: var(--value-font-size); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         font-weight: 600; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | 
					
						
							| 
									
										
										
										
											2025-06-27 17:32:01 +00:00
										 |  |  |         line-height: 1.1; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         letter-spacing: -0.025em; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       .trend-unit { | 
					
						
							|  |  |  |         font-size: var(--unit-font-size); | 
					
						
							|  |  |  |         font-weight: 400; | 
					
						
							|  |  |  |         color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; | 
					
						
							|  |  |  |         letter-spacing: -0.01em; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .trend-label { | 
					
						
							|  |  |  |         font-size: var(--label-font-size); | 
					
						
							|  |  |  |         font-weight: 500; | 
					
						
							|  |  |  |         color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; | 
					
						
							|  |  |  |         letter-spacing: -0.01em; | 
					
						
							|  |  |  |         margin-left: auto; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .trend-graph { | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         width: 100%; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         height: 32px; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         position: relative; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .trend-svg { | 
					
						
							|  |  |  |         width: 100%; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         height: 100%; | 
					
						
							|  |  |  |         display: block; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .trend-line { | 
					
						
							|  |  |  |         fill: none; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         stroke: ${cssManager.bdTheme('hsl(215.4 16.3% 66.9%)', 'hsl(215 20.2% 55.1%)')}; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         stroke-width: 2; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         stroke-linejoin: round; | 
					
						
							|  |  |  |         stroke-linecap: round; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       .trend-area { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         fill: ${cssManager.bdTheme('hsl(215.4 16.3% 66.9% / 0.1)', 'hsl(215 20.2% 55.1% / 0.08)')}; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       /* Text Value Styles */ | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       .text-value { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         font-size: var(--value-font-size); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:58:05 +00:00
										 |  |  |         font-weight: 600; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; | 
					
						
							| 
									
										
										
										
											2025-06-27 17:32:01 +00:00
										 |  |  |         line-height: 1.1; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         letter-spacing: -0.025em; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:58:05 +00:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       /* Context Menu */ | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       dees-contextmenu { | 
					
						
							|  |  |  |         position: fixed; | 
					
						
							|  |  |  |         z-index: 1000; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     `,
 | 
					
						
							|  |  |  |   ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   constructor() { | 
					
						
							|  |  |  |     super(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   public render(): TemplateResult { | 
					
						
							|  |  |  |     return html`
 | 
					
						
							|  |  |  |       ${this.gridActions.length > 0 ? html`
 | 
					
						
							|  |  |  |         <div class="grid-header"> | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |           <div class="grid-title"></div> | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |           <div class="grid-actions"> | 
					
						
							|  |  |  |             ${this.gridActions.map(action => html`
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |               <dees-button  | 
					
						
							|  |  |  |                 @clicked=${() => this.handleGridAction(action)} | 
					
						
							|  |  |  |                 type="outline" | 
					
						
							|  |  |  |                 size="sm" | 
					
						
							|  |  |  |               > | 
					
						
							| 
									
										
										
										
											2025-06-30 12:57:13 +00:00
										 |  |  |                 ${action.iconName ? html`<dees-icon .icon=${action.iconName} size="small"></dees-icon>` : ''} | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |                 ${action.name} | 
					
						
							|  |  |  |               </dees-button> | 
					
						
							|  |  |  |             `)}
 | 
					
						
							|  |  |  |           </div> | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |       ` : ''}
 | 
					
						
							|  |  |  |        | 
					
						
							|  |  |  |       <div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(${this.minTileWidth}px, 1fr)); gap: ${this.gap}px;"> | 
					
						
							|  |  |  |         ${this.tiles.map(tile => this.renderTile(tile))} | 
					
						
							|  |  |  |       </div> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       ${this.contextMenuVisible ? html`
 | 
					
						
							|  |  |  |         <dees-contextmenu | 
					
						
							|  |  |  |           .x=${this.contextMenuPosition.x} | 
					
						
							|  |  |  |           .y=${this.contextMenuPosition.y} | 
					
						
							| 
									
										
										
										
											2025-06-27 17:32:01 +00:00
										 |  |  |           .menuItems=${this.contextMenuActions as any} | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |           @clicked=${() => this.contextMenuVisible = false} | 
					
						
							|  |  |  |         ></dees-contextmenu> | 
					
						
							|  |  |  |       ` : ''}
 | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private renderTile(tile: IStatsTile): TemplateResult { | 
					
						
							|  |  |  |     const hasActions = tile.actions && tile.actions.length > 0; | 
					
						
							|  |  |  |     const clickable = hasActions && tile.actions.length === 1; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return html`
 | 
					
						
							|  |  |  |       <div  | 
					
						
							|  |  |  |         class="stats-tile ${clickable ? 'clickable' : ''}" | 
					
						
							|  |  |  |         @click=${clickable ? () => this.handleTileAction(tile.actions![0], tile) : undefined} | 
					
						
							|  |  |  |         @contextmenu=${hasActions ? (e: MouseEvent) => this.showContextMenu(e, tile) : undefined} | 
					
						
							|  |  |  |       > | 
					
						
							|  |  |  |         <div class="tile-header"> | 
					
						
							|  |  |  |           <h3 class="tile-title">${tile.title}</h3> | 
					
						
							|  |  |  |           ${tile.icon ? html`
 | 
					
						
							| 
									
										
										
										
											2025-06-30 12:57:13 +00:00
										 |  |  |             <dees-icon class="tile-icon" .icon=${tile.icon} size="small"></dees-icon> | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |           ` : ''}
 | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         <div class="tile-content"> | 
					
						
							|  |  |  |           ${this.renderTileContent(tile)} | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |          | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         ${tile.description && tile.type !== 'trend' ? html`
 | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |           <div class="tile-description">${tile.description}</div> | 
					
						
							|  |  |  |         ` : ''}
 | 
					
						
							|  |  |  |       </div> | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private renderTileContent(tile: IStatsTile): TemplateResult { | 
					
						
							|  |  |  |     switch (tile.type) { | 
					
						
							|  |  |  |       case 'number': | 
					
						
							|  |  |  |         return html`
 | 
					
						
							|  |  |  |           <div class="tile-value" style="${tile.color ? `color: ${tile.color}` : ''}"> | 
					
						
							|  |  |  |             <span>${tile.value}</span> | 
					
						
							|  |  |  |             ${tile.unit ? html`<span class="tile-unit">${tile.unit}</span>` : ''} | 
					
						
							|  |  |  |           </div> | 
					
						
							|  |  |  |         `;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       case 'gauge': | 
					
						
							|  |  |  |         return this.renderGauge(tile); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       case 'percentage': | 
					
						
							|  |  |  |         return this.renderPercentage(tile); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       case 'trend': | 
					
						
							|  |  |  |         return this.renderTrend(tile); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       case 'text': | 
					
						
							|  |  |  |         return html`
 | 
					
						
							|  |  |  |           <div class="text-value" style="${tile.color ? `color: ${tile.color}` : ''}"> | 
					
						
							|  |  |  |             ${tile.value} | 
					
						
							|  |  |  |           </div> | 
					
						
							|  |  |  |         `;
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       default: | 
					
						
							|  |  |  |         return html`<div class="tile-value">${tile.value}</div>`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   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; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |      | 
					
						
							|  |  |  |     // SVG dimensions and calculations
 | 
					
						
							|  |  |  |     const width = 140; | 
					
						
							| 
									
										
										
										
											2025-06-27 13:44:36 +00:00
										 |  |  |     const height = 80; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |     const strokeWidth = 8; | 
					
						
							|  |  |  |     const padding = strokeWidth / 2 + 2; | 
					
						
							| 
									
										
										
										
											2025-06-27 13:44:36 +00:00
										 |  |  |     const radius = 48; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |     const centerX = width / 2; | 
					
						
							|  |  |  |     const centerY = height - padding; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Arc path
 | 
					
						
							|  |  |  |     const startX = centerX - radius; | 
					
						
							|  |  |  |     const startY = centerY; | 
					
						
							|  |  |  |     const endX = centerX + radius; | 
					
						
							|  |  |  |     const endY = centerY; | 
					
						
							|  |  |  |     const arcPath = `M ${startX} ${startY} A ${radius} ${radius} 0 0 1 ${endX} ${endY}`; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Calculate stroke dasharray and dashoffset
 | 
					
						
							|  |  |  |     const circumference = Math.PI * radius; | 
					
						
							|  |  |  |     const strokeDashoffset = circumference - (circumference * percentage) / 100; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |     let strokeColor = tile.color || cssManager.bdTheme('hsl(215.3 25% 28.8%)', 'hsl(210 40% 78%)'); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |     if (options.thresholds) { | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       const sortedThresholds = [...options.thresholds].sort((a, b) => b.value - a.value); | 
					
						
							|  |  |  |       for (const threshold of sortedThresholds) { | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         if (value >= threshold.value) { | 
					
						
							|  |  |  |           strokeColor = threshold.color; | 
					
						
							|  |  |  |           break; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return html`
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       <div class="gauge-wrapper"> | 
					
						
							|  |  |  |         <div class="gauge-container"> | 
					
						
							|  |  |  |           <svg class="gauge-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid meet"> | 
					
						
							|  |  |  |             <!-- Background arc --> | 
					
						
							|  |  |  |             <path | 
					
						
							|  |  |  |               class="gauge-background" | 
					
						
							|  |  |  |               d="${arcPath}" | 
					
						
							|  |  |  |             /> | 
					
						
							|  |  |  |             <!-- Filled arc --> | 
					
						
							|  |  |  |             <path | 
					
						
							|  |  |  |               class="gauge-fill" | 
					
						
							|  |  |  |               d="${arcPath}" | 
					
						
							|  |  |  |               stroke="${strokeColor}" | 
					
						
							|  |  |  |               stroke-dasharray="${circumference}" | 
					
						
							|  |  |  |               stroke-dashoffset="${strokeDashoffset}" | 
					
						
							|  |  |  |             /> | 
					
						
							|  |  |  |             <!-- Value text --> | 
					
						
							| 
									
										
										
										
											2025-06-27 17:32:01 +00:00
										 |  |  |             <text class="gauge-text" x="${centerX}" y="${centerY - 8}" dominant-baseline="middle"> | 
					
						
							|  |  |  |               <tspan>${value}</tspan>${tile.unit ? html`<tspan class="gauge-unit" dx="2" dy="0">${tile.unit}</tspan>` : ''} | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |             </text> | 
					
						
							|  |  |  |           </svg> | 
					
						
							|  |  |  |         </div> | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       </div> | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   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`
 | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       <div class="percentage-wrapper"> | 
					
						
							|  |  |  |         <div class="percentage-value">${percentage}%</div> | 
					
						
							|  |  |  |         <div class="percentage-bar"> | 
					
						
							|  |  |  |           <div  | 
					
						
							|  |  |  |             class="percentage-fill"  | 
					
						
							|  |  |  |             style="width: ${percentage}%; ${tile.color ? `background: ${tile.color}` : ''}" | 
					
						
							|  |  |  |           ></div> | 
					
						
							|  |  |  |         </div> | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       </div> | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private renderTrend(tile: IStatsTile): TemplateResult { | 
					
						
							|  |  |  |     if (!tile.trendData || tile.trendData.length < 2) { | 
					
						
							|  |  |  |       return html`<div class="tile-value">${tile.value}</div>`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const data = tile.trendData; | 
					
						
							|  |  |  |     const max = Math.max(...data); | 
					
						
							|  |  |  |     const min = Math.min(...data); | 
					
						
							|  |  |  |     const range = max - min || 1; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |     const width = 300; | 
					
						
							|  |  |  |     const height = 32; | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     // Add padding to prevent clipping
 | 
					
						
							|  |  |  |     const padding = 2; | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |     const points = data.map((value, index) => { | 
					
						
							|  |  |  |       const x = (index / (data.length - 1)) * width; | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |       const y = padding + (height - 2 * padding) - ((value - min) / range) * (height - 2 * padding); | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |       return `${x},${y}`; | 
					
						
							|  |  |  |     }).join(' '); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const areaPoints = `0,${height} ${points} ${width},${height}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return html`
 | 
					
						
							|  |  |  |       <div class="trend-container"> | 
					
						
							| 
									
										
										
										
											2025-06-27 11:50:07 +00:00
										 |  |  |         <div class="trend-header"> | 
					
						
							|  |  |  |           <span class="trend-value">${tile.value}</span> | 
					
						
							|  |  |  |           ${tile.unit ? html`<span class="trend-unit">${tile.unit}</span>` : ''} | 
					
						
							|  |  |  |           ${tile.description ? html`<span class="trend-label">${tile.description}</span>` : ''} | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |         <div class="trend-graph"> | 
					
						
							|  |  |  |           <svg class="trend-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none"> | 
					
						
							|  |  |  |             <polygon class="trend-area" points="${areaPoints}" /> | 
					
						
							|  |  |  |             <polyline class="trend-line" points="${points}" /> | 
					
						
							|  |  |  |           </svg> | 
					
						
							| 
									
										
										
										
											2025-06-10 18:29:37 +00:00
										 |  |  |         </div> | 
					
						
							|  |  |  |       </div> | 
					
						
							|  |  |  |     `;
 | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   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); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |