tin-spa 20.14.19 → 20.14.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13854,7 +13854,9 @@ class TilesComponent {
13854
13854
  return;
13855
13855
  }
13856
13856
  if (this.isCarousel) {
13857
- this.tilesStyle = { 'grid-template-columns': '' };
13857
+ // Changed: the width goes on the CONTAINER as a custom property rather than inline on every card, so a
13858
+ // media query can narrow it on a phone. An inline style on the card would have needed !important to beat.
13859
+ this.tilesStyle = { '--tile-basis': min + 'px' };
13858
13860
  this.tileBasis = min + 'px';
13859
13861
  return;
13860
13862
  }
@@ -14257,11 +14259,11 @@ class TilesComponent {
14257
14259
  return [];
14258
14260
  }
14259
14261
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TilesComponent, deps: [{ token: DataServiceLib }, { token: MessageService }, { token: i0.ChangeDetectorRef }, { token: ApiErrorService }], target: i0.ɵɵFactoryTarget.Component }); }
14260
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TilesComponent, isStandalone: false, selector: "spa-tiles", inputs: { config: "config", lastSearch: "lastSearch", data: "data", reload: "reload" }, outputs: { tileActionSelected: "tileActionSelected", tileClick: "tileClick", tileUnClick: "tileUnClick" }, viewQueries: [{ propertyName: "tileScroller", first: true, predicate: ["tileScroller"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Tile row layout. THREE modes, and the DEFAULT is the original.\n 'auto' (default, and what every existing page gets): the original bootstrap .row/.col \u2014 tiles stretch to\n fill and shrink to a 150px floor, wrapping when they run out of room. This is byte-for-byte the markup\n that shipped before, restored on the owner's instruction 2026-08-08: the grid I briefly made the default\n forced ONE tile per row on a phone, because a 200px track floor does not fit twice in a ~380px viewport.\n 'wrap': a grid of equal tracks \u2014 opt in where uniform tile widths matter more than filling the row.\n 'carousel': one row, horizontal scroll. The Day Book opts into this.\n The tile body itself is ONE template shared by all three, so the modes can never drift apart. -->\n\n<!-- AUTO \u2014 the original layout, unchanged -->\n<div *ngIf=\"isAuto\" class=\"d-flex row align-items-center justify-content-between\">\n <ng-container *ngFor=\"let tile of tiles\">\n <!-- Changed: tile-clickable class gives pointer + hover lift only on tiles that actually respond to clicks -->\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"col\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" style=\"margin-left: 5px;margin-right: 5px; padding: 10px 16px ; min-width: 150px; margin-top: 5px;\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n</div>\n\n<!-- WRAP / CAROUSEL \u2014 opt-in only -->\n<div *ngIf=\"!isAuto\" class=\"spa-tiles-outer\">\n\n <!-- Carousel only: scroll affordances, shown solely when there is something to scroll to -->\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollLeft\" (click)=\"scrollTiles(-1)\" aria-label=\"Scroll tiles left\">\n <mat-icon>chevron_left</mat-icon>\n </button>\n\n <div #tileScroller class=\"spa-tiles\" [class.tiles-wrap]=\"!isCarousel\" [class.tiles-carousel]=\"isCarousel\" [ngStyle]=\"tilesStyle\" (scroll)=\"onTilesScroll()\">\n <ng-container *ngFor=\"let tile of tiles\">\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"tile-card\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" [style.flex]=\"tileBasis ? '0 0 ' + tileBasis : null\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n </div>\n\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollRight\" (click)=\"scrollTiles(1)\" aria-label=\"Scroll tiles right\">\n <mat-icon>chevron_right</mat-icon>\n </button>\n\n</div>\n\n<!-- The tile body, shared by all three modes. ctx() returns a CACHED context object per tile \u2014 building\n `{ $implicit: tile }` inline would hand NgTemplateOutlet a new object every change-detection pass and\n re-create every tile's view, which is the documented cause of a real livelock in this library. -->\n<ng-template #tileBody let-tile>\n\n <!-- Changed: Chart-style tile \u2014 header, prominent chart, optional value, footer -->\n <ng-container *ngIf=\"tile.chart; else standardTile\">\n <!-- Changed: Header with tile name \u2014 left-aligned for better hierarchy -->\n <div class=\"tile-chart-header\">\n <span>{{tile.alias ?? tile.name | camelToWords}}</span>\n </div>\n\n <!-- Changed: Chart fills tile \u2014 uses helper method for reliable data detection -->\n <div class=\"tile-chart\" *ngIf=\"hasTileChartData(tile)\" [style.height.px]=\"tile.chart.height ?? 120\">\n <canvas baseChart\n [type]=\"tile.chart.type\"\n [data]=\"getTileMiniChartData(tile)\"\n [options]=\"getMiniChartOptions(tile)\"\n [plugins]=\"getTileChartPlugins(tile)\">\n </canvas>\n </div>\n\n <!-- Changed: Optional value display below chart \u2014 only show primitive values, skip chart data objects -->\n <div class=\"tile-chart-value\" *ngIf=\"tile.name && isTileValuePrimitive(tile)\">\n <span class=\"tile-chart-value-text\" [style.color]=\"tile.color\">{{tile.prefix ?? ''}}{{data?.[tile.name]}}<span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span></span>\n </div>\n\n <!-- Changed: Footer with divider for chart tiles -->\n <div class=\"tile-footer\" *ngIf=\"tile.footer || tile.info\">\n <mat-divider></mat-divider>\n <div class=\"d-flex align-items-center\" style=\"gap: 4px; color: #9a9a9a; font-size: 12px; margin-top: 6px;\">\n <mat-icon *ngIf=\"tile.footerIcon\" style=\"font-size: 16px; width: 16px; height: 16px;\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" style=\"font-size: 16px; width: 16px; height: 16px; color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Changed: Standard tile \u2014 Paper Dashboard style: icon left, label+value right, optional footer -->\n <ng-template #standardTile>\n <!-- Changed: Icon-style tile \u2014 icon left, label top-right, large value below -->\n <ng-container *ngIf=\"tile.icon; else basicTile\">\n <div class=\"tile-icon-row\">\n <div class=\"tile-icon-wrap\" [style.color]=\"tile.color ?? '#2196f3'\">\n <mat-icon>{{tile.icon}}</mat-icon>\n </div>\n <div class=\"tile-icon-content\">\n <div class=\"tile-icon-label\">{{tile.alias ?? tile.name | camelToWords}}</div>\n <!-- Changed (Quiet Loading D5): while the value is genuinely unknown the slot shows a shimmer chip instead of the old lying `0`; icon, label and footer render normally so the card never resizes. On a reload the previous number stays put and simply updates. -->\n <div class=\"tile-icon-value\" [style.color]=\"tile.color\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\">\n <span *ngIf=\"tile.prefix\">{{tile.prefix}}</span><span *ngIf=\"showValueGhost(tile); else iconTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #iconTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template><span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n </div>\n <!-- Changed: Footer with divider \u2014 info tooltip or custom footer text -->\n <div class=\"tile-icon-footer\" *ngIf=\"tile.info || tile.footer\">\n <mat-divider></mat-divider>\n <div class=\"tile-icon-footer-content\">\n <mat-icon *ngIf=\"tile.footerIcon\" class=\"tile-icon-footer-icon\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" class=\"tile-icon-footer-icon\" style=\"color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Basic tile fallback \u2014 centered number display (no icon) -->\n <ng-template #basicTile>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div style=\"text-align: center;font-size: 30px;\">\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.prefix\" >{{tile.prefix}}</mat-label> &nbsp;\n <!-- Changed (Quiet Loading D5): same value ghost as the icon tile \u2014 chip while unknown, real number otherwise -->\n <mat-label style=\"font-weight:bold; text-align: center;\" [ngStyle]=\"{'color':tile.color }\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\"><span *ngIf=\"showValueGhost(tile); else basicTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #basicTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template></mat-label>&nbsp;\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.suffix\">{{tile.suffix}}</mat-label>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div class=\"d-flex justify-content-center align-items-center\" style=\"text-align: center;\">\n <mat-label style=\"padding-left:5px;padding-right:5px; text-align: center;font-size: 14px;\">{{tile.alias ?? tile.name | camelToWords}}</mat-label>\n <mat-icon *ngIf=\"tile.info\" [matTooltip]=\"tile.info\" matTooltipPosition=\"above\" style=\"font-size: 20px; color:steelblue;\">info</mat-icon>\n </div>\n </div>\n </ng-template>\n </ng-template>\n\n</ng-template>\n", styles: [".card{min-width:180px;flex:1;display:flex;flex-direction:column;align-items:center;padding:5px 10px}.tiles{gap:1;row-gap:5px}.spa-tiles-outer{display:flex;align-items:center;gap:4px}.spa-tiles{flex:1;min-width:0}.tiles-wrap{display:grid;gap:10px;align-items:stretch}.tiles-carousel{display:flex;gap:10px;overflow-x:auto;scroll-behavior:smooth;scroll-snap-type:x proximity;padding-bottom:6px}.tiles-carousel>.tile-card{scroll-snap-align:start}.tile-card{padding:10px 16px;margin:0}.tiles-nav{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:#0000000a;color:#666;cursor:pointer;transition:background-color .2s ease,color .2s ease}.tiles-nav:hover{background:#2196f31f;color:#2196f3}.tiles-nav mat-icon{font-size:20px;width:20px;height:20px}.col{transition:all .2s ease}.tile-clickable{cursor:pointer}.tile-clickable:hover{transform:translateY(-2px);box-shadow:0 4px 10px #00000021;background-color:#2196f312}.selected-tile{background-color:#e0e0e0;box-shadow:0 4px 8px #0003;transform:translateY(-2px);border:2px solid #3f51b5}.selected-tile mat-label{font-weight:700}.selected-tile:hover{background-color:#e0e0e0}.tile-chart-header{display:flex;justify-content:flex-start;font-size:11px;font-weight:500;color:#999;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}.tile-chart{width:100%;position:relative;margin-top:4px;display:flex;align-items:center;justify-content:center}.tile-chart canvas{width:100%!important;height:100%!important;max-height:inherit}.tile-chart-value{text-align:center;margin-top:6px}.tile-chart-value-text{font-size:22px;font-weight:600;letter-spacing:-.5px}.tile-badge{display:inline-block;font-size:12px;font-weight:500;color:#fff;padding:2px 8px;border-radius:12px;vertical-align:middle;margin-left:4px}.tile-footer{margin-top:8px}.tile-icon-row{display:flex;align-items:flex-start;gap:12px;padding:4px 0}.tile-icon-wrap{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:48px;height:48px}.tile-icon-wrap mat-icon{font-size:36px;width:36px;height:36px;opacity:.85}.tile-icon-content{flex:1;text-align:right;min-width:0}.tile-icon-label{font-size:12px;color:#999;text-transform:uppercase;letter-spacing:.3px;line-height:1.4}.tile-icon-value{font-size:26px;font-weight:600;line-height:1.2;letter-spacing:-.5px}.tile-icon-footer{margin-top:8px}.tile-icon-footer mat-divider{margin-bottom:6px}.tile-icon-footer-content{display:flex;align-items:center;gap:4px;font-size:12px;color:#999}.tile-icon-footer-icon{font-size:16px;width:16px;height:16px;color:#bbb}.tile-value-ghost{width:2.5ch;height:.72em;vertical-align:middle}.tile-value-in{animation:tile-value-in .15s ease-out both}@keyframes tile-value-in{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.tile-value-in{animation:none}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i17.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: i10.BaseChartDirective, selector: "canvas[baseChart]", inputs: ["type", "legend", "data", "options", "plugins", "labels", "datasets"], outputs: ["chartClick", "chartHover"], exportAs: ["base-chart"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14262
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.14", type: TilesComponent, isStandalone: false, selector: "spa-tiles", inputs: { config: "config", lastSearch: "lastSearch", data: "data", reload: "reload" }, outputs: { tileActionSelected: "tileActionSelected", tileClick: "tileClick", tileUnClick: "tileUnClick" }, viewQueries: [{ propertyName: "tileScroller", first: true, predicate: ["tileScroller"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- Tile row layout. THREE modes, and the DEFAULT is the original.\n 'auto' (default, and what every existing page gets): the original bootstrap .row/.col \u2014 tiles stretch to\n fill and shrink to a 150px floor, wrapping when they run out of room. This is byte-for-byte the markup\n that shipped before, restored on the owner's instruction 2026-08-08: the grid I briefly made the default\n forced ONE tile per row on a phone, because a 200px track floor does not fit twice in a ~380px viewport.\n 'wrap': a grid of equal tracks \u2014 opt in where uniform tile widths matter more than filling the row.\n 'carousel': one row, horizontal scroll. The Day Book opts into this.\n The tile body itself is ONE template shared by all three, so the modes can never drift apart. -->\n\n<!-- AUTO \u2014 the original layout, unchanged -->\n<div *ngIf=\"isAuto\" class=\"d-flex row align-items-center justify-content-between\">\n <ng-container *ngFor=\"let tile of tiles\">\n <!-- Changed: tile-clickable class gives pointer + hover lift only on tiles that actually respond to clicks -->\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"col\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" style=\"margin-left: 5px;margin-right: 5px; padding: 10px 16px ; min-width: 150px; margin-top: 5px;\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n</div>\n\n<!-- WRAP / CAROUSEL \u2014 opt-in only -->\n<div *ngIf=\"!isAuto\" class=\"spa-tiles-outer\">\n\n <!-- Carousel only: scroll affordances, shown solely when there is something to scroll to -->\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollLeft\" (click)=\"scrollTiles(-1)\" aria-label=\"Scroll tiles left\">\n <mat-icon>chevron_left</mat-icon>\n </button>\n\n <div #tileScroller class=\"spa-tiles\" [class.tiles-wrap]=\"!isCarousel\" [class.tiles-carousel]=\"isCarousel\" [ngStyle]=\"tilesStyle\" (scroll)=\"onTilesScroll()\">\n <ng-container *ngFor=\"let tile of tiles\">\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"tile-card\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n </div>\n\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollRight\" (click)=\"scrollTiles(1)\" aria-label=\"Scroll tiles right\">\n <mat-icon>chevron_right</mat-icon>\n </button>\n\n</div>\n\n<!-- The tile body, shared by all three modes. ctx() returns a CACHED context object per tile \u2014 building\n `{ $implicit: tile }` inline would hand NgTemplateOutlet a new object every change-detection pass and\n re-create every tile's view, which is the documented cause of a real livelock in this library. -->\n<ng-template #tileBody let-tile>\n\n <!-- Changed: Chart-style tile \u2014 header, prominent chart, optional value, footer -->\n <ng-container *ngIf=\"tile.chart; else standardTile\">\n <!-- Changed: Header with tile name \u2014 left-aligned for better hierarchy -->\n <div class=\"tile-chart-header\">\n <span>{{tile.alias ?? tile.name | camelToWords}}</span>\n </div>\n\n <!-- Changed: Chart fills tile \u2014 uses helper method for reliable data detection -->\n <div class=\"tile-chart\" *ngIf=\"hasTileChartData(tile)\" [style.height.px]=\"tile.chart.height ?? 120\">\n <canvas baseChart\n [type]=\"tile.chart.type\"\n [data]=\"getTileMiniChartData(tile)\"\n [options]=\"getMiniChartOptions(tile)\"\n [plugins]=\"getTileChartPlugins(tile)\">\n </canvas>\n </div>\n\n <!-- Changed: Optional value display below chart \u2014 only show primitive values, skip chart data objects -->\n <div class=\"tile-chart-value\" *ngIf=\"tile.name && isTileValuePrimitive(tile)\">\n <span class=\"tile-chart-value-text\" [style.color]=\"tile.color\">{{tile.prefix ?? ''}}{{data?.[tile.name]}}<span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span></span>\n </div>\n\n <!-- Changed: Footer with divider for chart tiles -->\n <div class=\"tile-footer\" *ngIf=\"tile.footer || tile.info\">\n <mat-divider></mat-divider>\n <div class=\"d-flex align-items-center\" style=\"gap: 4px; color: #9a9a9a; font-size: 12px; margin-top: 6px;\">\n <mat-icon *ngIf=\"tile.footerIcon\" style=\"font-size: 16px; width: 16px; height: 16px;\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" style=\"font-size: 16px; width: 16px; height: 16px; color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Changed: Standard tile \u2014 Paper Dashboard style: icon left, label+value right, optional footer -->\n <ng-template #standardTile>\n <!-- Changed: Icon-style tile \u2014 icon left, label top-right, large value below -->\n <ng-container *ngIf=\"tile.icon; else basicTile\">\n <div class=\"tile-icon-row\">\n <div class=\"tile-icon-wrap\" [style.color]=\"tile.color ?? '#2196f3'\">\n <mat-icon>{{tile.icon}}</mat-icon>\n </div>\n <div class=\"tile-icon-content\">\n <div class=\"tile-icon-label\">{{tile.alias ?? tile.name | camelToWords}}</div>\n <!-- Changed (Quiet Loading D5): while the value is genuinely unknown the slot shows a shimmer chip instead of the old lying `0`; icon, label and footer render normally so the card never resizes. On a reload the previous number stays put and simply updates. -->\n <div class=\"tile-icon-value\" [style.color]=\"tile.color\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\">\n <span *ngIf=\"tile.prefix\">{{tile.prefix}}</span><span *ngIf=\"showValueGhost(tile); else iconTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #iconTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template><span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n </div>\n <!-- Changed: Footer with divider \u2014 info tooltip or custom footer text -->\n <div class=\"tile-icon-footer\" *ngIf=\"tile.info || tile.footer\">\n <mat-divider></mat-divider>\n <div class=\"tile-icon-footer-content\">\n <mat-icon *ngIf=\"tile.footerIcon\" class=\"tile-icon-footer-icon\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" class=\"tile-icon-footer-icon\" style=\"color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Basic tile fallback \u2014 centered number display (no icon) -->\n <ng-template #basicTile>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div style=\"text-align: center;font-size: 30px;\">\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.prefix\" >{{tile.prefix}}</mat-label> &nbsp;\n <!-- Changed (Quiet Loading D5): same value ghost as the icon tile \u2014 chip while unknown, real number otherwise -->\n <mat-label style=\"font-weight:bold; text-align: center;\" [ngStyle]=\"{'color':tile.color }\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\"><span *ngIf=\"showValueGhost(tile); else basicTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #basicTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template></mat-label>&nbsp;\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.suffix\">{{tile.suffix}}</mat-label>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div class=\"d-flex justify-content-center align-items-center\" style=\"text-align: center;\">\n <mat-label style=\"padding-left:5px;padding-right:5px; text-align: center;font-size: 14px;\">{{tile.alias ?? tile.name | camelToWords}}</mat-label>\n <mat-icon *ngIf=\"tile.info\" [matTooltip]=\"tile.info\" matTooltipPosition=\"above\" style=\"font-size: 20px; color:steelblue;\">info</mat-icon>\n </div>\n </div>\n </ng-template>\n </ng-template>\n\n</ng-template>\n", styles: [".card{min-width:180px;flex:1;display:flex;flex-direction:column;align-items:center;padding:5px 10px}.tiles{gap:1;row-gap:5px}.spa-tiles-outer{display:flex;align-items:center;gap:4px}.spa-tiles{flex:1;min-width:0}.tiles-wrap{display:grid;gap:10px;align-items:stretch}.tiles-carousel{display:flex;gap:10px;overflow-x:auto;scroll-behavior:smooth;scroll-snap-type:x proximity;padding-bottom:6px}.tiles-carousel>.tile-card{scroll-snap-align:start;flex:0 0 var(--tile-basis, 200px)}@media (max-width: 700px){.tiles-nav{display:none}.tiles-carousel{gap:8px}.tiles-carousel>.tile-card{flex:0 0 44%}}.tile-card{padding:10px 16px;margin:0}.tiles-nav{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:#0000000a;color:#666;cursor:pointer;transition:background-color .2s ease,color .2s ease}.tiles-nav:hover{background:#2196f31f;color:#2196f3}.tiles-nav mat-icon{font-size:20px;width:20px;height:20px}.col{transition:all .2s ease}.tile-clickable{cursor:pointer}.tile-clickable:hover{transform:translateY(-2px);box-shadow:0 4px 10px #00000021;background-color:#2196f312}.selected-tile{background-color:#e0e0e0;box-shadow:0 4px 8px #0003;transform:translateY(-2px);border:2px solid #3f51b5}.selected-tile mat-label{font-weight:700}.selected-tile:hover{background-color:#e0e0e0}.tile-chart-header{display:flex;justify-content:flex-start;font-size:11px;font-weight:500;color:#999;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}.tile-chart{width:100%;position:relative;margin-top:4px;display:flex;align-items:center;justify-content:center}.tile-chart canvas{width:100%!important;height:100%!important;max-height:inherit}.tile-chart-value{text-align:center;margin-top:6px}.tile-chart-value-text{font-size:22px;font-weight:600;letter-spacing:-.5px}.tile-badge{display:inline-block;font-size:12px;font-weight:500;color:#fff;padding:2px 8px;border-radius:12px;vertical-align:middle;margin-left:4px}.tile-footer{margin-top:8px}.tile-icon-row{display:flex;align-items:flex-start;gap:12px;padding:4px 0}.tile-icon-wrap{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:48px;height:48px}.tile-icon-wrap mat-icon{font-size:36px;width:36px;height:36px;opacity:.85}.tile-icon-content{flex:1;text-align:right;min-width:0}.tile-icon-label{font-size:12px;color:#999;text-transform:uppercase;letter-spacing:.3px;line-height:1.4}.tile-icon-value{font-size:26px;font-weight:600;line-height:1.2;letter-spacing:-.5px}.tile-icon-footer{margin-top:8px}.tile-icon-footer mat-divider{margin-bottom:6px}.tile-icon-footer-content{display:flex;align-items:center;gap:4px;font-size:12px;color:#999}.tile-icon-footer-icon{font-size:16px;width:16px;height:16px;color:#bbb}.tile-value-ghost{width:2.5ch;height:.72em;vertical-align:middle}.tile-value-in{animation:tile-value-in .15s ease-out both}@keyframes tile-value-in{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.tile-value-in{animation:none}}\n"], dependencies: [{ kind: "directive", type: i1$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i3$1.MatLabel, selector: "mat-label" }, { kind: "component", type: i19.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "component", type: i17.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "directive", type: i7.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: i10.BaseChartDirective, selector: "canvas[baseChart]", inputs: ["type", "legend", "data", "options", "plugins", "labels", "datasets"], outputs: ["chartClick", "chartHover"], exportAs: ["base-chart"] }, { kind: "pipe", type: CamelToWordsPipe, name: "camelToWords" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14261
14263
  }
14262
14264
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.14", ngImport: i0, type: TilesComponent, decorators: [{
14263
14265
  type: Component,
14264
- args: [{ selector: 'spa-tiles', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Tile row layout. THREE modes, and the DEFAULT is the original.\n 'auto' (default, and what every existing page gets): the original bootstrap .row/.col \u2014 tiles stretch to\n fill and shrink to a 150px floor, wrapping when they run out of room. This is byte-for-byte the markup\n that shipped before, restored on the owner's instruction 2026-08-08: the grid I briefly made the default\n forced ONE tile per row on a phone, because a 200px track floor does not fit twice in a ~380px viewport.\n 'wrap': a grid of equal tracks \u2014 opt in where uniform tile widths matter more than filling the row.\n 'carousel': one row, horizontal scroll. The Day Book opts into this.\n The tile body itself is ONE template shared by all three, so the modes can never drift apart. -->\n\n<!-- AUTO \u2014 the original layout, unchanged -->\n<div *ngIf=\"isAuto\" class=\"d-flex row align-items-center justify-content-between\">\n <ng-container *ngFor=\"let tile of tiles\">\n <!-- Changed: tile-clickable class gives pointer + hover lift only on tiles that actually respond to clicks -->\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"col\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" style=\"margin-left: 5px;margin-right: 5px; padding: 10px 16px ; min-width: 150px; margin-top: 5px;\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n</div>\n\n<!-- WRAP / CAROUSEL \u2014 opt-in only -->\n<div *ngIf=\"!isAuto\" class=\"spa-tiles-outer\">\n\n <!-- Carousel only: scroll affordances, shown solely when there is something to scroll to -->\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollLeft\" (click)=\"scrollTiles(-1)\" aria-label=\"Scroll tiles left\">\n <mat-icon>chevron_left</mat-icon>\n </button>\n\n <div #tileScroller class=\"spa-tiles\" [class.tiles-wrap]=\"!isCarousel\" [class.tiles-carousel]=\"isCarousel\" [ngStyle]=\"tilesStyle\" (scroll)=\"onTilesScroll()\">\n <ng-container *ngFor=\"let tile of tiles\">\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"tile-card\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" [style.flex]=\"tileBasis ? '0 0 ' + tileBasis : null\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n </div>\n\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollRight\" (click)=\"scrollTiles(1)\" aria-label=\"Scroll tiles right\">\n <mat-icon>chevron_right</mat-icon>\n </button>\n\n</div>\n\n<!-- The tile body, shared by all three modes. ctx() returns a CACHED context object per tile \u2014 building\n `{ $implicit: tile }` inline would hand NgTemplateOutlet a new object every change-detection pass and\n re-create every tile's view, which is the documented cause of a real livelock in this library. -->\n<ng-template #tileBody let-tile>\n\n <!-- Changed: Chart-style tile \u2014 header, prominent chart, optional value, footer -->\n <ng-container *ngIf=\"tile.chart; else standardTile\">\n <!-- Changed: Header with tile name \u2014 left-aligned for better hierarchy -->\n <div class=\"tile-chart-header\">\n <span>{{tile.alias ?? tile.name | camelToWords}}</span>\n </div>\n\n <!-- Changed: Chart fills tile \u2014 uses helper method for reliable data detection -->\n <div class=\"tile-chart\" *ngIf=\"hasTileChartData(tile)\" [style.height.px]=\"tile.chart.height ?? 120\">\n <canvas baseChart\n [type]=\"tile.chart.type\"\n [data]=\"getTileMiniChartData(tile)\"\n [options]=\"getMiniChartOptions(tile)\"\n [plugins]=\"getTileChartPlugins(tile)\">\n </canvas>\n </div>\n\n <!-- Changed: Optional value display below chart \u2014 only show primitive values, skip chart data objects -->\n <div class=\"tile-chart-value\" *ngIf=\"tile.name && isTileValuePrimitive(tile)\">\n <span class=\"tile-chart-value-text\" [style.color]=\"tile.color\">{{tile.prefix ?? ''}}{{data?.[tile.name]}}<span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span></span>\n </div>\n\n <!-- Changed: Footer with divider for chart tiles -->\n <div class=\"tile-footer\" *ngIf=\"tile.footer || tile.info\">\n <mat-divider></mat-divider>\n <div class=\"d-flex align-items-center\" style=\"gap: 4px; color: #9a9a9a; font-size: 12px; margin-top: 6px;\">\n <mat-icon *ngIf=\"tile.footerIcon\" style=\"font-size: 16px; width: 16px; height: 16px;\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" style=\"font-size: 16px; width: 16px; height: 16px; color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Changed: Standard tile \u2014 Paper Dashboard style: icon left, label+value right, optional footer -->\n <ng-template #standardTile>\n <!-- Changed: Icon-style tile \u2014 icon left, label top-right, large value below -->\n <ng-container *ngIf=\"tile.icon; else basicTile\">\n <div class=\"tile-icon-row\">\n <div class=\"tile-icon-wrap\" [style.color]=\"tile.color ?? '#2196f3'\">\n <mat-icon>{{tile.icon}}</mat-icon>\n </div>\n <div class=\"tile-icon-content\">\n <div class=\"tile-icon-label\">{{tile.alias ?? tile.name | camelToWords}}</div>\n <!-- Changed (Quiet Loading D5): while the value is genuinely unknown the slot shows a shimmer chip instead of the old lying `0`; icon, label and footer render normally so the card never resizes. On a reload the previous number stays put and simply updates. -->\n <div class=\"tile-icon-value\" [style.color]=\"tile.color\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\">\n <span *ngIf=\"tile.prefix\">{{tile.prefix}}</span><span *ngIf=\"showValueGhost(tile); else iconTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #iconTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template><span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n </div>\n <!-- Changed: Footer with divider \u2014 info tooltip or custom footer text -->\n <div class=\"tile-icon-footer\" *ngIf=\"tile.info || tile.footer\">\n <mat-divider></mat-divider>\n <div class=\"tile-icon-footer-content\">\n <mat-icon *ngIf=\"tile.footerIcon\" class=\"tile-icon-footer-icon\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" class=\"tile-icon-footer-icon\" style=\"color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Basic tile fallback \u2014 centered number display (no icon) -->\n <ng-template #basicTile>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div style=\"text-align: center;font-size: 30px;\">\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.prefix\" >{{tile.prefix}}</mat-label> &nbsp;\n <!-- Changed (Quiet Loading D5): same value ghost as the icon tile \u2014 chip while unknown, real number otherwise -->\n <mat-label style=\"font-weight:bold; text-align: center;\" [ngStyle]=\"{'color':tile.color }\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\"><span *ngIf=\"showValueGhost(tile); else basicTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #basicTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template></mat-label>&nbsp;\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.suffix\">{{tile.suffix}}</mat-label>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div class=\"d-flex justify-content-center align-items-center\" style=\"text-align: center;\">\n <mat-label style=\"padding-left:5px;padding-right:5px; text-align: center;font-size: 14px;\">{{tile.alias ?? tile.name | camelToWords}}</mat-label>\n <mat-icon *ngIf=\"tile.info\" [matTooltip]=\"tile.info\" matTooltipPosition=\"above\" style=\"font-size: 20px; color:steelblue;\">info</mat-icon>\n </div>\n </div>\n </ng-template>\n </ng-template>\n\n</ng-template>\n", styles: [".card{min-width:180px;flex:1;display:flex;flex-direction:column;align-items:center;padding:5px 10px}.tiles{gap:1;row-gap:5px}.spa-tiles-outer{display:flex;align-items:center;gap:4px}.spa-tiles{flex:1;min-width:0}.tiles-wrap{display:grid;gap:10px;align-items:stretch}.tiles-carousel{display:flex;gap:10px;overflow-x:auto;scroll-behavior:smooth;scroll-snap-type:x proximity;padding-bottom:6px}.tiles-carousel>.tile-card{scroll-snap-align:start}.tile-card{padding:10px 16px;margin:0}.tiles-nav{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:#0000000a;color:#666;cursor:pointer;transition:background-color .2s ease,color .2s ease}.tiles-nav:hover{background:#2196f31f;color:#2196f3}.tiles-nav mat-icon{font-size:20px;width:20px;height:20px}.col{transition:all .2s ease}.tile-clickable{cursor:pointer}.tile-clickable:hover{transform:translateY(-2px);box-shadow:0 4px 10px #00000021;background-color:#2196f312}.selected-tile{background-color:#e0e0e0;box-shadow:0 4px 8px #0003;transform:translateY(-2px);border:2px solid #3f51b5}.selected-tile mat-label{font-weight:700}.selected-tile:hover{background-color:#e0e0e0}.tile-chart-header{display:flex;justify-content:flex-start;font-size:11px;font-weight:500;color:#999;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}.tile-chart{width:100%;position:relative;margin-top:4px;display:flex;align-items:center;justify-content:center}.tile-chart canvas{width:100%!important;height:100%!important;max-height:inherit}.tile-chart-value{text-align:center;margin-top:6px}.tile-chart-value-text{font-size:22px;font-weight:600;letter-spacing:-.5px}.tile-badge{display:inline-block;font-size:12px;font-weight:500;color:#fff;padding:2px 8px;border-radius:12px;vertical-align:middle;margin-left:4px}.tile-footer{margin-top:8px}.tile-icon-row{display:flex;align-items:flex-start;gap:12px;padding:4px 0}.tile-icon-wrap{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:48px;height:48px}.tile-icon-wrap mat-icon{font-size:36px;width:36px;height:36px;opacity:.85}.tile-icon-content{flex:1;text-align:right;min-width:0}.tile-icon-label{font-size:12px;color:#999;text-transform:uppercase;letter-spacing:.3px;line-height:1.4}.tile-icon-value{font-size:26px;font-weight:600;line-height:1.2;letter-spacing:-.5px}.tile-icon-footer{margin-top:8px}.tile-icon-footer mat-divider{margin-bottom:6px}.tile-icon-footer-content{display:flex;align-items:center;gap:4px;font-size:12px;color:#999}.tile-icon-footer-icon{font-size:16px;width:16px;height:16px;color:#bbb}.tile-value-ghost{width:2.5ch;height:.72em;vertical-align:middle}.tile-value-in{animation:tile-value-in .15s ease-out both}@keyframes tile-value-in{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.tile-value-in{animation:none}}\n"] }]
14266
+ args: [{ selector: 'spa-tiles', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Tile row layout. THREE modes, and the DEFAULT is the original.\n 'auto' (default, and what every existing page gets): the original bootstrap .row/.col \u2014 tiles stretch to\n fill and shrink to a 150px floor, wrapping when they run out of room. This is byte-for-byte the markup\n that shipped before, restored on the owner's instruction 2026-08-08: the grid I briefly made the default\n forced ONE tile per row on a phone, because a 200px track floor does not fit twice in a ~380px viewport.\n 'wrap': a grid of equal tracks \u2014 opt in where uniform tile widths matter more than filling the row.\n 'carousel': one row, horizontal scroll. The Day Book opts into this.\n The tile body itself is ONE template shared by all three, so the modes can never drift apart. -->\n\n<!-- AUTO \u2014 the original layout, unchanged -->\n<div *ngIf=\"isAuto\" class=\"d-flex row align-items-center justify-content-between\">\n <ng-container *ngFor=\"let tile of tiles\">\n <!-- Changed: tile-clickable class gives pointer + hover lift only on tiles that actually respond to clicks -->\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"col\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" style=\"margin-left: 5px;margin-right: 5px; padding: 10px 16px ; min-width: 150px; margin-top: 5px;\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n</div>\n\n<!-- WRAP / CAROUSEL \u2014 opt-in only -->\n<div *ngIf=\"!isAuto\" class=\"spa-tiles-outer\">\n\n <!-- Carousel only: scroll affordances, shown solely when there is something to scroll to -->\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollLeft\" (click)=\"scrollTiles(-1)\" aria-label=\"Scroll tiles left\">\n <mat-icon>chevron_left</mat-icon>\n </button>\n\n <div #tileScroller class=\"spa-tiles\" [class.tiles-wrap]=\"!isCarousel\" [class.tiles-carousel]=\"isCarousel\" [ngStyle]=\"tilesStyle\" (scroll)=\"onTilesScroll()\">\n <ng-container *ngFor=\"let tile of tiles\">\n <mat-card *ngIf=\"!isHidden(tile)\" class=\"tile-card\" [class.tile-clickable]=\"isClickable(tile)\" [class.selected-tile]=\"tile.name === selectedTile\" (click)=\"clicked(tile)\">\n <ng-container *ngTemplateOutlet=\"tileBody; context: ctx(tile)\"></ng-container>\n </mat-card>\n </ng-container>\n </div>\n\n <button type=\"button\" class=\"tiles-nav\" *ngIf=\"isCarousel && canScrollRight\" (click)=\"scrollTiles(1)\" aria-label=\"Scroll tiles right\">\n <mat-icon>chevron_right</mat-icon>\n </button>\n\n</div>\n\n<!-- The tile body, shared by all three modes. ctx() returns a CACHED context object per tile \u2014 building\n `{ $implicit: tile }` inline would hand NgTemplateOutlet a new object every change-detection pass and\n re-create every tile's view, which is the documented cause of a real livelock in this library. -->\n<ng-template #tileBody let-tile>\n\n <!-- Changed: Chart-style tile \u2014 header, prominent chart, optional value, footer -->\n <ng-container *ngIf=\"tile.chart; else standardTile\">\n <!-- Changed: Header with tile name \u2014 left-aligned for better hierarchy -->\n <div class=\"tile-chart-header\">\n <span>{{tile.alias ?? tile.name | camelToWords}}</span>\n </div>\n\n <!-- Changed: Chart fills tile \u2014 uses helper method for reliable data detection -->\n <div class=\"tile-chart\" *ngIf=\"hasTileChartData(tile)\" [style.height.px]=\"tile.chart.height ?? 120\">\n <canvas baseChart\n [type]=\"tile.chart.type\"\n [data]=\"getTileMiniChartData(tile)\"\n [options]=\"getMiniChartOptions(tile)\"\n [plugins]=\"getTileChartPlugins(tile)\">\n </canvas>\n </div>\n\n <!-- Changed: Optional value display below chart \u2014 only show primitive values, skip chart data objects -->\n <div class=\"tile-chart-value\" *ngIf=\"tile.name && isTileValuePrimitive(tile)\">\n <span class=\"tile-chart-value-text\" [style.color]=\"tile.color\">{{tile.prefix ?? ''}}{{data?.[tile.name]}}<span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span></span>\n </div>\n\n <!-- Changed: Footer with divider for chart tiles -->\n <div class=\"tile-footer\" *ngIf=\"tile.footer || tile.info\">\n <mat-divider></mat-divider>\n <div class=\"d-flex align-items-center\" style=\"gap: 4px; color: #9a9a9a; font-size: 12px; margin-top: 6px;\">\n <mat-icon *ngIf=\"tile.footerIcon\" style=\"font-size: 16px; width: 16px; height: 16px;\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" style=\"font-size: 16px; width: 16px; height: 16px; color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Changed: Standard tile \u2014 Paper Dashboard style: icon left, label+value right, optional footer -->\n <ng-template #standardTile>\n <!-- Changed: Icon-style tile \u2014 icon left, label top-right, large value below -->\n <ng-container *ngIf=\"tile.icon; else basicTile\">\n <div class=\"tile-icon-row\">\n <div class=\"tile-icon-wrap\" [style.color]=\"tile.color ?? '#2196f3'\">\n <mat-icon>{{tile.icon}}</mat-icon>\n </div>\n <div class=\"tile-icon-content\">\n <div class=\"tile-icon-label\">{{tile.alias ?? tile.name | camelToWords}}</div>\n <!-- Changed (Quiet Loading D5): while the value is genuinely unknown the slot shows a shimmer chip instead of the old lying `0`; icon, label and footer render normally so the card never resizes. On a reload the previous number stays put and simply updates. -->\n <div class=\"tile-icon-value\" [style.color]=\"tile.color\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\">\n <span *ngIf=\"tile.prefix\">{{tile.prefix}}</span><span *ngIf=\"showValueGhost(tile); else iconTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #iconTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template><span *ngIf=\"tile.suffix\"> {{tile.suffix}}</span>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n </div>\n <!-- Changed: Footer with divider \u2014 info tooltip or custom footer text -->\n <div class=\"tile-icon-footer\" *ngIf=\"tile.info || tile.footer\">\n <mat-divider></mat-divider>\n <div class=\"tile-icon-footer-content\">\n <mat-icon *ngIf=\"tile.footerIcon\" class=\"tile-icon-footer-icon\">{{tile.footerIcon}}</mat-icon>\n <mat-icon *ngIf=\"!tile.footerIcon && tile.info\" class=\"tile-icon-footer-icon\" style=\"color: steelblue;\">info</mat-icon>\n <span>{{tile.footer ?? tile.info}}</span>\n </div>\n </div>\n </ng-container>\n\n <!-- Basic tile fallback \u2014 centered number display (no icon) -->\n <ng-template #basicTile>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div style=\"text-align: center;font-size: 30px;\">\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.prefix\" >{{tile.prefix}}</mat-label> &nbsp;\n <!-- Changed (Quiet Loading D5): same value ghost as the icon tile \u2014 chip while unknown, real number otherwise -->\n <mat-label style=\"font-weight:bold; text-align: center;\" [ngStyle]=\"{'color':tile.color }\" [attr.aria-busy]=\"showValueGhost(tile) ? 'true' : null\"><span *ngIf=\"showValueGhost(tile); else basicTileValue\" class=\"tin-skel tin-skel-text tile-value-ghost\" aria-hidden=\"true\"></span><ng-template #basicTileValue><span [class.tile-value-in]=\"effQuietLoading\">{{data?.[tile.name] ?? 0}}</span></ng-template></mat-label>&nbsp;\n <mat-label style=\"font-weight:bold;\" *ngIf=\"tile.suffix\">{{tile.suffix}}</mat-label>\n <span *ngIf=\"tile.badge && data?.[tile.badge]\" class=\"tile-badge\" [style.backgroundColor]=\"tile.badgeColor ?? '#4caf50'\">{{data?.[tile.badge]}}</span>\n </div>\n </div>\n <div class=\"row d-flex justify-content-center align-items-center\">\n <div class=\"d-flex justify-content-center align-items-center\" style=\"text-align: center;\">\n <mat-label style=\"padding-left:5px;padding-right:5px; text-align: center;font-size: 14px;\">{{tile.alias ?? tile.name | camelToWords}}</mat-label>\n <mat-icon *ngIf=\"tile.info\" [matTooltip]=\"tile.info\" matTooltipPosition=\"above\" style=\"font-size: 20px; color:steelblue;\">info</mat-icon>\n </div>\n </div>\n </ng-template>\n </ng-template>\n\n</ng-template>\n", styles: [".card{min-width:180px;flex:1;display:flex;flex-direction:column;align-items:center;padding:5px 10px}.tiles{gap:1;row-gap:5px}.spa-tiles-outer{display:flex;align-items:center;gap:4px}.spa-tiles{flex:1;min-width:0}.tiles-wrap{display:grid;gap:10px;align-items:stretch}.tiles-carousel{display:flex;gap:10px;overflow-x:auto;scroll-behavior:smooth;scroll-snap-type:x proximity;padding-bottom:6px}.tiles-carousel>.tile-card{scroll-snap-align:start;flex:0 0 var(--tile-basis, 200px)}@media (max-width: 700px){.tiles-nav{display:none}.tiles-carousel{gap:8px}.tiles-carousel>.tile-card{flex:0 0 44%}}.tile-card{padding:10px 16px;margin:0}.tiles-nav{flex:0 0 auto;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:none;border-radius:50%;background:#0000000a;color:#666;cursor:pointer;transition:background-color .2s ease,color .2s ease}.tiles-nav:hover{background:#2196f31f;color:#2196f3}.tiles-nav mat-icon{font-size:20px;width:20px;height:20px}.col{transition:all .2s ease}.tile-clickable{cursor:pointer}.tile-clickable:hover{transform:translateY(-2px);box-shadow:0 4px 10px #00000021;background-color:#2196f312}.selected-tile{background-color:#e0e0e0;box-shadow:0 4px 8px #0003;transform:translateY(-2px);border:2px solid #3f51b5}.selected-tile mat-label{font-weight:700}.selected-tile:hover{background-color:#e0e0e0}.tile-chart-header{display:flex;justify-content:flex-start;font-size:11px;font-weight:500;color:#999;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}.tile-chart{width:100%;position:relative;margin-top:4px;display:flex;align-items:center;justify-content:center}.tile-chart canvas{width:100%!important;height:100%!important;max-height:inherit}.tile-chart-value{text-align:center;margin-top:6px}.tile-chart-value-text{font-size:22px;font-weight:600;letter-spacing:-.5px}.tile-badge{display:inline-block;font-size:12px;font-weight:500;color:#fff;padding:2px 8px;border-radius:12px;vertical-align:middle;margin-left:4px}.tile-footer{margin-top:8px}.tile-icon-row{display:flex;align-items:flex-start;gap:12px;padding:4px 0}.tile-icon-wrap{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:48px;height:48px}.tile-icon-wrap mat-icon{font-size:36px;width:36px;height:36px;opacity:.85}.tile-icon-content{flex:1;text-align:right;min-width:0}.tile-icon-label{font-size:12px;color:#999;text-transform:uppercase;letter-spacing:.3px;line-height:1.4}.tile-icon-value{font-size:26px;font-weight:600;line-height:1.2;letter-spacing:-.5px}.tile-icon-footer{margin-top:8px}.tile-icon-footer mat-divider{margin-bottom:6px}.tile-icon-footer-content{display:flex;align-items:center;gap:4px;font-size:12px;color:#999}.tile-icon-footer-icon{font-size:16px;width:16px;height:16px;color:#bbb}.tile-value-ghost{width:2.5ch;height:.72em;vertical-align:middle}.tile-value-in{animation:tile-value-in .15s ease-out both}@keyframes tile-value-in{0%{opacity:0}to{opacity:1}}@media (prefers-reduced-motion: reduce){.tile-value-in{animation:none}}\n"] }]
14265
14267
  }], ctorParameters: () => [{ type: DataServiceLib }, { type: MessageService }, { type: i0.ChangeDetectorRef }, { type: ApiErrorService }], propDecorators: { tileScroller: [{
14266
14268
  type: ViewChild,
14267
14269
  args: ['tileScroller']