@ogidor/dashboard 1.0.11 → 1.0.13

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Directive, EventEmitter, TemplateRef, Component, ChangeDetectionStrategy, Input, Output, ViewChild, ContentChild, Injectable, Pipe, NgModule } from '@angular/core';
2
+ import { Directive, EventEmitter, TemplateRef, Component, ChangeDetectionStrategy, Input, Output, ViewChild, ContentChild, Injectable, Pipe, HostListener, NgModule } from '@angular/core';
3
3
  import { BrowserModule } from '@angular/platform-browser';
4
4
  import * as i2 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
@@ -880,16 +880,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImpo
880
880
  const DEFAULT_THEME = {
881
881
  backgroundColor: '#000000',
882
882
  panelColor: '#1c1c1e',
883
+ panelBorderColor: 'rgba(255,255,255,0.05)',
883
884
  widgetCardColor: '#2c2c2e',
884
885
  foreColor: '#8e8e93',
885
886
  accentColor: '#0a84ff',
887
+ accentTintColor: 'rgba(10,132,255,0.15)',
888
+ accentTintStrongColor: 'rgba(10,132,255,0.25)',
886
889
  dangerColor: '#ff453a',
890
+ dangerTintColor: 'rgba(255,69,58,0.22)',
887
891
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
888
892
  tabsContainerColor: 'rgba(44,44,46,0.6)',
889
893
  tabActiveColor: '#3a3a3c',
890
894
  tabActiveTextColor: '#ffffff',
891
895
  tabHoverTextColor: '#e5e5ea',
896
+ tabHoverBgColor: 'rgba(255,255,255,0.05)',
892
897
  addWidgetButtonTextColor: '#ffffff',
898
+ overflowBtnBgColor: 'rgba(255,255,255,0.08)',
899
+ overflowBtnBorderColor: 'rgba(255,255,255,0.12)',
900
+ overlayBgColor: 'rgba(0,0,0,0.55)',
901
+ sheetBorderColor: 'rgba(255,255,255,0.08)',
902
+ scrollbarColor: 'rgba(255,255,255,0.15)',
893
903
  popoutTitleColor: '#ffffff',
894
904
  widgetTitleColor: '#ffffff',
895
905
  dragHandleColor: 'rgba(255,255,255,0.2)',
@@ -920,10 +930,19 @@ class DashboardComponent {
920
930
  this.resolvedTheme = { ...DEFAULT_THEME };
921
931
  this.wrapperStyles = {};
922
932
  this.pages = [];
933
+ this.visiblePages = [];
934
+ this.hiddenCount = 0;
923
935
  this.activePageId = '';
924
936
  this.isPoppedOut = false;
925
- this.tabsOverflow = false;
937
+ this.tabSwitcherOpen = false;
926
938
  this.subs = new Subscription();
939
+ // Reserved space for the overflow "+N" button and the add "+" button (px)
940
+ this.OVERFLOW_BTN_W = 80;
941
+ this.ADD_BTN_W = 52;
942
+ this.GAP = 6;
943
+ this.PADDING = 12; // 6px each side
944
+ // Cache of measured tab widths (including gap) indexed by page position
945
+ this._tabWidthCache = [];
927
946
  }
928
947
  ngOnChanges(changes) {
929
948
  if (changes['theme'] || changes['initialLayout'])
@@ -944,11 +963,15 @@ class DashboardComponent {
944
963
  this.isPoppedOut = true;
945
964
  this.subs.add(this.stateService.pages$.subscribe(pages => {
946
965
  this.pages = pages;
966
+ this._tabWidthCache = []; // invalidate cache — page list changed
947
967
  if (this.isPoppedOut && hash) {
948
968
  if (pages.find(p => p.id === hash))
949
969
  this.stateService.setActivePage(hash);
950
970
  }
951
971
  this.updateActivePage();
972
+ this.recalcVisibleTabs();
973
+ // Second pass after Angular has rendered the updated tab elements
974
+ setTimeout(() => this.recalcVisibleTabs());
952
975
  }));
953
976
  this.subs.add(this.stateService.activePageId$.subscribe(id => {
954
977
  this.activePageId = id;
@@ -956,18 +979,16 @@ class DashboardComponent {
956
979
  }));
957
980
  }
958
981
  ngAfterViewInit() {
959
- if (!this.tabsContainerRef)
982
+ if (!this.tabsTrackRef)
960
983
  return;
961
984
  this.zone.runOutsideAngular(() => {
962
985
  this.resizeObserver = new ResizeObserver(() => {
963
- this.zone.run(() => this.checkOverflow());
986
+ this.zone.run(() => this.recalcVisibleTabs());
964
987
  });
965
- this.resizeObserver.observe(this.tabsContainerRef.nativeElement);
966
- });
967
- // Also detect scroll so fade hides when scrolled to end
968
- this.tabsContainerRef.nativeElement.addEventListener('scroll', () => {
969
- this.zone.run(() => this.checkOverflow());
988
+ this.resizeObserver.observe(this.tabsTrackRef.nativeElement);
970
989
  });
990
+ // Initial measurement pass now that the DOM is ready
991
+ setTimeout(() => this.recalcVisibleTabs());
971
992
  }
972
993
  ngOnDestroy() {
973
994
  this.subs.unsubscribe();
@@ -993,12 +1014,82 @@ class DashboardComponent {
993
1014
  this.stateService.popOutPage(pageId);
994
1015
  }
995
1016
  serializeLayout() { return this.stateService.serializeLayout(); }
996
- checkOverflow() {
997
- const el = this.tabsContainerRef?.nativeElement;
998
- if (!el)
1017
+ openTabSwitcher() { this.tabSwitcherOpen = true; }
1018
+ closeTabSwitcher() { this.tabSwitcherOpen = false; }
1019
+ onEscapeKey() { this.closeTabSwitcher(); }
1020
+ onSwitcherSelect(pageId) {
1021
+ this.stateService.setActivePage(pageId);
1022
+ this.closeTabSwitcher();
1023
+ }
1024
+ onSwitcherPopOut(event, pageId) {
1025
+ event.stopPropagation();
1026
+ this.stateService.popOutPage(pageId);
1027
+ this.closeTabSwitcher();
1028
+ }
1029
+ onSwitcherRemove(event, pageId) {
1030
+ event.stopPropagation();
1031
+ if (confirm('Remove this workspace?')) {
1032
+ this.stateService.removePage(pageId);
1033
+ if (this.pages.length <= 1)
1034
+ this.closeTabSwitcher();
1035
+ }
1036
+ }
1037
+ onSwitcherAdd() {
1038
+ const name = prompt('Workspace name:', `Workspace ${this.pages.length + 1}`);
1039
+ if (name)
1040
+ this.stateService.addPage(name);
1041
+ }
1042
+ recalcVisibleTabs() {
1043
+ if (!this.tabsTrackRef || !this.pages.length) {
1044
+ this.visiblePages = this.pages;
1045
+ this.hiddenCount = 0;
1046
+ this.cdr.markForCheck();
999
1047
  return;
1000
- const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 4;
1001
- this.tabsOverflow = !atEnd && el.scrollWidth > el.clientWidth;
1048
+ }
1049
+ const track = this.tabsTrackRef.nativeElement;
1050
+ const trackW = track.clientWidth;
1051
+ // Space always consumed by the add "+" button + padding
1052
+ const baseReserved = this.ADD_BTN_W + this.GAP + this.PADDING;
1053
+ // Space needed for the overflow "+N" button when shown
1054
+ const overflowSlot = this.OVERFLOW_BTN_W + this.GAP;
1055
+ // Measure each real tab element currently in the DOM.
1056
+ // The DOM always renders all visiblePages tabs, so we may not have all tabs
1057
+ // measured yet — fall back to the previous measured cache or a generous estimate.
1058
+ const tabEls = Array.from(track.querySelectorAll('.tab'));
1059
+ // Build a width map by index (only covers currently visible tabs)
1060
+ const measuredWidths = this.pages.map((_, i) => {
1061
+ const el = tabEls[i];
1062
+ return el ? el.getBoundingClientRect().width + this.GAP : this._tabWidthCache[i] ?? 150 + this.GAP;
1063
+ });
1064
+ // Update cache for next call
1065
+ tabEls.forEach((el, i) => {
1066
+ this._tabWidthCache[i] = el.getBoundingClientRect().width + this.GAP;
1067
+ });
1068
+ // How much space the tabs need in total (no overflow button)
1069
+ const totalNeeded = measuredWidths.reduce((s, w) => s + w, 0) - this.GAP; // remove trailing gap
1070
+ if (totalNeeded <= trackW - baseReserved) {
1071
+ // Everything fits — show all
1072
+ this.visiblePages = this.pages;
1073
+ this.hiddenCount = 0;
1074
+ }
1075
+ else {
1076
+ // Not everything fits — fill as many as possible while always leaving room for overflow btn
1077
+ const budget = trackW - baseReserved - overflowSlot;
1078
+ let used = 0;
1079
+ let count = 0;
1080
+ for (let i = 0; i < this.pages.length; i++) {
1081
+ used += measuredWidths[i];
1082
+ if (used - this.GAP <= budget) { // -GAP: last item has no trailing gap
1083
+ count++;
1084
+ }
1085
+ else {
1086
+ break;
1087
+ }
1088
+ }
1089
+ count = Math.max(1, count);
1090
+ this.visiblePages = this.pages.slice(0, count);
1091
+ this.hiddenCount = this.pages.length - count;
1092
+ }
1002
1093
  this.cdr.markForCheck();
1003
1094
  }
1004
1095
  applyTheme() {
@@ -1007,16 +1098,26 @@ class DashboardComponent {
1007
1098
  this.wrapperStyles = {
1008
1099
  '--dash-bg': t.backgroundColor,
1009
1100
  '--dash-panel-bg': t.panelColor,
1101
+ '--dash-panel-border': t.panelBorderColor,
1010
1102
  '--dash-card-bg': t.widgetCardColor,
1011
1103
  '--dash-fore-color': t.foreColor,
1012
1104
  '--dash-accent-color': t.accentColor,
1105
+ '--dash-accent-tint': t.accentTintColor,
1106
+ '--dash-accent-tint-strong': t.accentTintStrongColor,
1013
1107
  '--dash-danger-color': t.dangerColor,
1108
+ '--dash-danger-tint': t.dangerTintColor,
1014
1109
  '--dash-font-family': t.fontFamily,
1015
1110
  '--dash-tabs-container-color': t.tabsContainerColor,
1016
1111
  '--dash-tab-active-color': t.tabActiveColor,
1017
1112
  '--dash-tab-active-text': t.tabActiveTextColor,
1018
1113
  '--dash-tab-hover-text': t.tabHoverTextColor,
1114
+ '--dash-tab-hover-bg': t.tabHoverBgColor,
1019
1115
  '--dash-add-widget-text': t.addWidgetButtonTextColor,
1116
+ '--dash-overflow-btn-bg': t.overflowBtnBgColor,
1117
+ '--dash-overflow-btn-border': t.overflowBtnBorderColor,
1118
+ '--dash-overlay-bg': t.overlayBgColor,
1119
+ '--dash-sheet-border': t.sheetBorderColor,
1120
+ '--dash-scrollbar-color': t.scrollbarColor,
1020
1121
  '--dash-popout-title-color': t.popoutTitleColor,
1021
1122
  '--dash-widget-title-color': t.widgetTitleColor,
1022
1123
  '--dash-drag-handle-color': t.dragHandleColor,
@@ -1029,18 +1130,10 @@ class DashboardComponent {
1029
1130
  }
1030
1131
  updateActivePage() {
1031
1132
  this.activePage = this.pages.find(p => p.id === this.activePageId);
1032
- // scroll active tab into view
1033
- setTimeout(() => {
1034
- if (!this.tabsContainerRef)
1035
- return;
1036
- const active = this.tabsContainerRef.nativeElement.querySelector('.tab.active');
1037
- active?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
1038
- this.checkOverflow();
1039
- });
1040
1133
  }
1041
1134
  }
1042
1135
  DashboardComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardComponent, deps: [{ token: DashboardStateService }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
1043
- DashboardComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: DashboardComponent, selector: "app-dashboard", inputs: { initialLayout: "initialLayout", theme: "theme" }, outputs: { addWidgetRequested: "addWidgetRequested", editWidgetRequested: "editWidgetRequested" }, queries: [{ propertyName: "cellTemplate", first: true, predicate: GridCellDirective, descendants: true, read: TemplateRef }], viewQueries: [{ propertyName: "tabsContainerRef", first: true, predicate: ["tabsContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
1136
+ DashboardComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: DashboardComponent, selector: "app-dashboard", inputs: { initialLayout: "initialLayout", theme: "theme" }, outputs: { addWidgetRequested: "addWidgetRequested", editWidgetRequested: "editWidgetRequested" }, host: { listeners: { "document:keydown.escape": "onEscapeKey()" } }, queries: [{ propertyName: "cellTemplate", first: true, predicate: GridCellDirective, descendants: true, read: TemplateRef }], viewQueries: [{ propertyName: "tabsTrackRef", first: true, predicate: ["tabsTrack"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
1044
1137
  <!-- POPPED-OUT MODE -->
1045
1138
  <ng-container *ngIf="isPoppedOut; else normalMode">
1046
1139
  <div class="popout-wrapper" [ngStyle]="wrapperStyles">
@@ -1069,29 +1162,39 @@ DashboardComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", ver
1069
1162
 
1070
1163
  <header class="dashboard-header">
1071
1164
 
1072
- <!-- Tabs pill — scrollable, fades when overflowing -->
1073
- <div class="tabs-scroll-track">
1074
- <div class="tabs-container" #tabsContainer>
1075
- <div
1076
- *ngFor="let page of pages"
1077
- class="tab"
1078
- [class.active]="page.id === activePageId"
1079
- (click)="onSelectPage(page.id)"
1080
- >
1081
- <span class="tab-name">{{ page.name }}</span>
1082
- <button class="tab-action tab-popout" (click)="onPopOut($event, page.id)" title="Open in new window">
1083
- <i class="la la-external-link-alt"></i>
1084
- </button>
1085
- <button class="tab-action tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)" title="Close">
1086
- <i class="la la-times"></i>
1087
- </button>
1088
- </div>
1089
- <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
1090
- <i class="la la-plus"></i>
1165
+ <!-- Tab bar -->
1166
+ <div class="tabs-track" #tabsTrack>
1167
+ <!-- Visible tabs -->
1168
+ <div
1169
+ *ngFor="let page of visiblePages"
1170
+ class="tab"
1171
+ [class.active]="page.id === activePageId"
1172
+ (click)="onSelectPage(page.id)"
1173
+ >
1174
+ <span class="tab-name">{{ page.name }}</span>
1175
+ <button class="tab-action tab-popout" (click)="onPopOut($event, page.id)" title="Open in new window">
1176
+ <i class="la la-external-link-alt"></i>
1177
+ </button>
1178
+ <button class="tab-action tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)" title="Close">
1179
+ <i class="la la-times"></i>
1091
1180
  </button>
1092
1181
  </div>
1093
- <!-- fade hint shown when scrollable -->
1094
- <div class="tabs-fade-right" *ngIf="tabsOverflow"></div>
1182
+
1183
+ <!-- Overflow button — shown only when there are hidden tabs -->
1184
+ <button
1185
+ *ngIf="hiddenCount > 0"
1186
+ class="btn-overflow"
1187
+ (click)="openTabSwitcher()"
1188
+ title="Show all workspaces"
1189
+ >
1190
+ <span>+{{ hiddenCount }}</span>
1191
+ <i class="la la-th-large"></i>
1192
+ </button>
1193
+
1194
+ <!-- Add page button -->
1195
+ <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
1196
+ <i class="la la-plus"></i>
1197
+ </button>
1095
1198
  </div>
1096
1199
 
1097
1200
  <button class="btn-add-widget" (click)="addWidgetRequested.emit()" title="Add widget">
@@ -1101,6 +1204,49 @@ DashboardComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", ver
1101
1204
 
1102
1205
  </header>
1103
1206
 
1207
+ <!-- ── Tab-switcher overlay (Chrome mobile style) ── -->
1208
+ <div class="tab-switcher-overlay" *ngIf="tabSwitcherOpen" (click)="closeTabSwitcher()">
1209
+ <div class="tab-switcher-sheet" (click)="$event.stopPropagation()">
1210
+
1211
+ <div class="tab-switcher-header">
1212
+ <span class="tab-switcher-title">{{ pages.length }} Workspace{{ pages.length !== 1 ? 's' : '' }}</span>
1213
+ <button class="tab-switcher-close-btn" (click)="closeTabSwitcher()" title="Close">
1214
+ <i class="la la-times"></i>
1215
+ </button>
1216
+ </div>
1217
+
1218
+ <div class="tab-switcher-grid">
1219
+ <div
1220
+ *ngFor="let page of pages"
1221
+ class="tab-card"
1222
+ [class.active]="page.id === activePageId"
1223
+ (click)="onSwitcherSelect(page.id)"
1224
+ >
1225
+ <div class="tab-card-icon">
1226
+ <i class="la la-th-large"></i>
1227
+ </div>
1228
+ <span class="tab-card-name">{{ page.name }}</span>
1229
+ <div class="tab-card-actions">
1230
+ <button class="tab-card-btn tab-card-popout" (click)="onSwitcherPopOut($event, page.id)" title="Open in new window">
1231
+ <i class="la la-external-link-alt"></i>
1232
+ </button>
1233
+ <button class="tab-card-btn tab-card-remove" *ngIf="pages.length > 1" (click)="onSwitcherRemove($event, page.id)" title="Close">
1234
+ <i class="la la-times"></i>
1235
+ </button>
1236
+ </div>
1237
+ </div>
1238
+ </div>
1239
+
1240
+ <div class="tab-switcher-footer">
1241
+ <button class="tab-switcher-add-btn" (click)="onSwitcherAdd()">
1242
+ <i class="la la-plus"></i>
1243
+ <span>New Workspace</span>
1244
+ </button>
1245
+ </div>
1246
+
1247
+ </div>
1248
+ </div>
1249
+
1104
1250
  <div class="grid-container">
1105
1251
  <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
1106
1252
  <ng-template gridCell let-widget="widget">
@@ -1119,7 +1265,7 @@ DashboardComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", ver
1119
1265
  </main>
1120
1266
  </div>
1121
1267
  </ng-template>
1122
- `, isInline: true, styles: [":host{display:flex;flex-direction:column;width:100%;height:100%;min-height:0;--dash-bg: #000000;--dash-panel-bg: #1c1c1e;--dash-card-bg: #2c2c2e;--dash-fore-color: #8e8e93;--dash-accent-color: #0a84ff;--dash-danger-color: #ff453a;--dash-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;--dash-tabs-container-color: rgba(44,44,46,.6);--dash-tab-active-color: #3a3a3c;--dash-tab-active-text: #ffffff;--dash-tab-hover-text: #e5e5ea;--dash-add-widget-text: #ffffff;--dash-popout-title-color: #ffffff;--dash-widget-title-color: #ffffff;--dash-drag-handle-color: rgba(255,255,255,.2);--dash-widget-border-color: rgba(255,255,255,.07);--dash-widget-btn-bg: rgba(255,255,255,.07);--dash-widget-btn-color: rgba(255,255,255,.5);--dash-placeholder-color: #0a84ff;--dash-resize-handle-color: rgba(255,255,255,.25)}.dashboard-wrapper{display:flex;flex:1;min-height:0;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;border-radius:40px;padding:20px;background:var(--dash-panel-bg);border:1px solid rgba(255,255,255,.05)}.popout-wrapper{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.popout-header{display:flex;align-items:center;margin-bottom:16px;padding:10px 18px;background:var(--dash-panel-bg);border-radius:20px;border:1px solid rgba(255,255,255,.06);flex-shrink:0}.popout-title{font-size:16px;font-weight:700;color:var(--dash-popout-title-color)}.popout-wrapper app-grid{flex:1;min-height:0;overflow:auto}.dashboard-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;gap:12px;flex-shrink:0}.tabs-scroll-track{position:relative;flex:1;min-width:0}.tabs-container{display:flex;align-items:center;gap:8px;background:var(--dash-tabs-container-color);backdrop-filter:blur(10px);border-radius:25px;padding:6px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}.tabs-container::-webkit-scrollbar{display:none}.tabs-fade-right{position:absolute;top:0;right:0;width:60px;height:100%;border-radius:0 25px 25px 0;pointer-events:none;background:linear-gradient(to right,transparent,var(--dash-panel-bg))}.tab{display:flex;align-items:center;gap:4px;padding:8px 14px 8px 18px;border-radius:20px;cursor:pointer;font-size:14px;font-weight:500;color:var(--dash-fore-color);white-space:nowrap;flex-shrink:0;transition:all .2s}.tab.active{background:var(--dash-tab-active-color);color:var(--dash-tab-active-text)}.tab:hover:not(.active){color:var(--dash-tab-hover-text);background:rgba(255,255,255,.05)}.tab-name{flex:1;white-space:nowrap}.tab-action{background:transparent;border:none;color:var(--dash-fore-color);padding:2px;font-size:11px;cursor:pointer;opacity:0;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}.tab:hover .tab-action{opacity:1}.tab-popout:hover{color:var(--dash-accent-color);background:rgba(10,132,255,.18)}.tab-close:hover{color:var(--dash-danger-color);background:rgba(255,69,58,.2)}.btn-add-page{background:transparent;border:none;color:var(--dash-fore-color);padding:8px 16px;cursor:pointer;border-radius:20px;flex-shrink:0;transition:all .2s}.btn-add-page:hover{color:var(--dash-accent-color);background:rgba(10,132,255,.1)}.btn-add-widget{display:flex;align-items:center;gap:6px;background:var(--dash-accent-color);border:none;color:var(--dash-add-widget-text);font-size:14px;font-weight:600;padding:10px 20px;border-radius:22px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:opacity .2s,transform .15s}.btn-add-widget:hover{opacity:.9;transform:translateY(-1px)}.btn-add-widget:active{transform:translateY(0)}.main-content .grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0}app-grid{background:transparent;display:block;height:100%;width:100%}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: WidgetRendererComponent, selector: "app-widget-renderer", inputs: ["widget", "theme"], outputs: ["editRequested", "removeRequested"] }, { kind: "component", type: CustomGridComponent, selector: "app-grid", inputs: ["widgets", "columns", "gap", "rowHeight", "minItemCols", "minItemRows"], outputs: ["itemChanged", "layoutChanged"] }, { kind: "directive", type: GridCellDirective, selector: "[gridCell]" }] });
1268
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;width:100%;height:100%;min-height:0;--dash-bg: #000000;--dash-panel-bg: #1c1c1e;--dash-panel-border: rgba(255,255,255,.05);--dash-card-bg: #2c2c2e;--dash-fore-color: #8e8e93;--dash-accent-color: #0a84ff;--dash-accent-tint: rgba(10,132,255,.15);--dash-accent-tint-strong: rgba(10,132,255,.25);--dash-danger-color: #ff453a;--dash-danger-tint: rgba(255,69,58,.22);--dash-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;--dash-tabs-container-color: rgba(44,44,46,.6);--dash-tab-active-color: #3a3a3c;--dash-tab-active-text: #ffffff;--dash-tab-hover-text: #e5e5ea;--dash-tab-hover-bg: rgba(255,255,255,.05);--dash-add-widget-text: #ffffff;--dash-overflow-btn-bg: rgba(255,255,255,.08);--dash-overflow-btn-border: rgba(255,255,255,.12);--dash-overlay-bg: rgba(0,0,0,.55);--dash-sheet-border: rgba(255,255,255,.08);--dash-scrollbar-color: rgba(255,255,255,.15);--dash-popout-title-color: #ffffff;--dash-widget-title-color: #ffffff;--dash-drag-handle-color: rgba(255,255,255,.2);--dash-widget-border-color: rgba(255,255,255,.07);--dash-widget-btn-bg: rgba(255,255,255,.07);--dash-widget-btn-color: rgba(255,255,255,.5);--dash-placeholder-color: #0a84ff;--dash-resize-handle-color: rgba(255,255,255,.25)}.dashboard-wrapper{display:flex;flex:1;min-height:0;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;border-radius:40px;padding:20px;background:var(--dash-panel-bg);border:1px solid var(--dash-panel-border);position:relative}.popout-wrapper{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.popout-header{display:flex;align-items:center;margin-bottom:16px;padding:10px 18px;background:var(--dash-panel-bg);border-radius:20px;border:1px solid var(--dash-panel-border);flex-shrink:0}.popout-title{font-size:16px;font-weight:700;color:var(--dash-popout-title-color)}.popout-wrapper app-grid{flex:1;min-height:0;overflow:auto}.dashboard-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;gap:12px;flex-shrink:0}.tabs-track{display:flex;align-items:center;gap:6px;flex:1;min-width:0;background:var(--dash-tabs-container-color);backdrop-filter:blur(10px);border-radius:25px;padding:6px;overflow:hidden}.tab{display:flex;align-items:center;gap:4px;padding:8px 14px 8px 18px;border-radius:20px;cursor:pointer;font-size:14px;font-weight:500;color:var(--dash-fore-color);white-space:nowrap;flex-shrink:0;transition:background .2s,color .2s}.tab.active{background:var(--dash-tab-active-color);color:var(--dash-tab-active-text)}.tab:hover:not(.active){color:var(--dash-tab-hover-text);background:var(--dash-tab-hover-bg)}.tab-name{white-space:nowrap}.tab-action{background:transparent;border:none;color:var(--dash-fore-color);padding:2px;font-size:11px;cursor:pointer;opacity:0;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}.tab:hover .tab-action{opacity:1}.tab-popout:hover{color:var(--dash-accent-color);background:var(--dash-accent-tint)}.tab-close:hover{color:var(--dash-danger-color);background:var(--dash-danger-tint)}.btn-overflow{display:inline-flex;align-items:center;gap:5px;background:var(--dash-overflow-btn-bg);border:1px solid var(--dash-overflow-btn-border);border-radius:20px;padding:7px 13px;cursor:pointer;font-size:13px;font-weight:600;color:var(--dash-tab-active-text);white-space:nowrap;flex-shrink:0;transition:background .2s,border-color .2s,color .2s}.btn-overflow:hover{background:var(--dash-accent-tint);border-color:var(--dash-accent-color);color:var(--dash-accent-color)}.btn-add-page{background:transparent;border:none;color:var(--dash-fore-color);padding:8px 14px;cursor:pointer;border-radius:20px;flex-shrink:0;transition:all .2s}.btn-add-page:hover{color:var(--dash-accent-color);background:var(--dash-accent-tint)}.tab-switcher-overlay{position:absolute;inset:0;z-index:1000;background:var(--dash-overlay-bg);backdrop-filter:blur(4px);display:flex;align-items:flex-start;justify-content:center;padding-top:60px;animation:overlay-in .18s ease}@keyframes overlay-in{0%{opacity:0}to{opacity:1}}.tab-switcher-sheet{background:var(--dash-panel-bg);border:1px solid var(--dash-sheet-border);border-radius:24px;width:min(520px,calc(100% - 32px));max-height:calc(100% - 80px);display:flex;flex-direction:column;overflow:hidden;animation:sheet-in .22s cubic-bezier(.32,1.2,.5,1)}@keyframes sheet-in{0%{transform:translateY(-16px) scale(.97);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}.tab-switcher-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px 12px;border-bottom:1px solid var(--dash-sheet-border);flex-shrink:0}.tab-switcher-title{font-size:15px;font-weight:700;color:var(--dash-tab-active-text)}.tab-switcher-close-btn{background:var(--dash-widget-btn-bg);border:none;color:var(--dash-fore-color);width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;transition:background .15s,color .15s}.tab-switcher-close-btn:hover{background:var(--dash-danger-tint);color:var(--dash-danger-color)}.tab-switcher-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;padding:16px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--dash-scrollbar-color) transparent}.tab-card{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px 12px;border-radius:16px;background:var(--dash-card-bg);border:1.5px solid transparent;cursor:pointer;transition:border-color .15s,background .15s,transform .12s;position:relative}.tab-card:hover{background:var(--dash-tab-hover-bg);transform:translateY(-2px)}.tab-card.active{border-color:var(--dash-accent-color);background:var(--dash-accent-tint)}.tab-card-icon{width:44px;height:44px;border-radius:12px;background:var(--dash-widget-btn-bg);display:flex;align-items:center;justify-content:center;font-size:20px;color:var(--dash-fore-color)}.tab-card.active .tab-card-icon{background:var(--dash-accent-tint-strong);color:var(--dash-accent-color)}.tab-card-name{font-size:13px;font-weight:500;color:var(--dash-tab-active-text);text-align:center;word-break:break-word;line-height:1.3}.tab-card-actions{display:flex;gap:4px;opacity:0;transition:opacity .15s}.tab-card:hover .tab-card-actions{opacity:1}.tab-card-btn{background:var(--dash-widget-btn-bg);border:none;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:12px;color:var(--dash-fore-color);transition:background .15s,color .15s}.tab-card-popout:hover{background:var(--dash-accent-tint-strong);color:var(--dash-accent-color)}.tab-card-remove:hover{background:var(--dash-danger-tint);color:var(--dash-danger-color)}.tab-switcher-footer{padding:12px 16px 16px;border-top:1px solid var(--dash-sheet-border);flex-shrink:0}.tab-switcher-add-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:11px 20px;border-radius:16px;background:var(--dash-accent-tint);border:1.5px dashed var(--dash-accent-tint-strong);color:var(--dash-accent-color);font-size:14px;font-weight:600;cursor:pointer;transition:background .15s,border-color .15s}.tab-switcher-add-btn:hover{background:var(--dash-accent-tint-strong);border-color:var(--dash-accent-color)}.btn-add-widget{display:flex;align-items:center;gap:6px;background:var(--dash-accent-color);border:none;color:var(--dash-add-widget-text);font-size:14px;font-weight:600;padding:10px 20px;border-radius:22px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:opacity .2s,transform .15s}.btn-add-widget:hover{opacity:.9;transform:translateY(-1px)}.btn-add-widget:active{transform:translateY(0)}.main-content .grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0}app-grid{background:transparent;display:block;height:100%;width:100%}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: WidgetRendererComponent, selector: "app-widget-renderer", inputs: ["widget", "theme"], outputs: ["editRequested", "removeRequested"] }, { kind: "component", type: CustomGridComponent, selector: "app-grid", inputs: ["widgets", "columns", "gap", "rowHeight", "minItemCols", "minItemRows"], outputs: ["itemChanged", "layoutChanged"] }, { kind: "directive", type: GridCellDirective, selector: "[gridCell]" }] });
1123
1269
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardComponent, decorators: [{
1124
1270
  type: Component,
1125
1271
  args: [{ selector: 'app-dashboard', template: `
@@ -1151,29 +1297,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImpo
1151
1297
 
1152
1298
  <header class="dashboard-header">
1153
1299
 
1154
- <!-- Tabs pill — scrollable, fades when overflowing -->
1155
- <div class="tabs-scroll-track">
1156
- <div class="tabs-container" #tabsContainer>
1157
- <div
1158
- *ngFor="let page of pages"
1159
- class="tab"
1160
- [class.active]="page.id === activePageId"
1161
- (click)="onSelectPage(page.id)"
1162
- >
1163
- <span class="tab-name">{{ page.name }}</span>
1164
- <button class="tab-action tab-popout" (click)="onPopOut($event, page.id)" title="Open in new window">
1165
- <i class="la la-external-link-alt"></i>
1166
- </button>
1167
- <button class="tab-action tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)" title="Close">
1168
- <i class="la la-times"></i>
1169
- </button>
1170
- </div>
1171
- <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
1172
- <i class="la la-plus"></i>
1300
+ <!-- Tab bar -->
1301
+ <div class="tabs-track" #tabsTrack>
1302
+ <!-- Visible tabs -->
1303
+ <div
1304
+ *ngFor="let page of visiblePages"
1305
+ class="tab"
1306
+ [class.active]="page.id === activePageId"
1307
+ (click)="onSelectPage(page.id)"
1308
+ >
1309
+ <span class="tab-name">{{ page.name }}</span>
1310
+ <button class="tab-action tab-popout" (click)="onPopOut($event, page.id)" title="Open in new window">
1311
+ <i class="la la-external-link-alt"></i>
1312
+ </button>
1313
+ <button class="tab-action tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)" title="Close">
1314
+ <i class="la la-times"></i>
1173
1315
  </button>
1174
1316
  </div>
1175
- <!-- fade hint shown when scrollable -->
1176
- <div class="tabs-fade-right" *ngIf="tabsOverflow"></div>
1317
+
1318
+ <!-- Overflow button — shown only when there are hidden tabs -->
1319
+ <button
1320
+ *ngIf="hiddenCount > 0"
1321
+ class="btn-overflow"
1322
+ (click)="openTabSwitcher()"
1323
+ title="Show all workspaces"
1324
+ >
1325
+ <span>+{{ hiddenCount }}</span>
1326
+ <i class="la la-th-large"></i>
1327
+ </button>
1328
+
1329
+ <!-- Add page button -->
1330
+ <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
1331
+ <i class="la la-plus"></i>
1332
+ </button>
1177
1333
  </div>
1178
1334
 
1179
1335
  <button class="btn-add-widget" (click)="addWidgetRequested.emit()" title="Add widget">
@@ -1183,6 +1339,49 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImpo
1183
1339
 
1184
1340
  </header>
1185
1341
 
1342
+ <!-- ── Tab-switcher overlay (Chrome mobile style) ── -->
1343
+ <div class="tab-switcher-overlay" *ngIf="tabSwitcherOpen" (click)="closeTabSwitcher()">
1344
+ <div class="tab-switcher-sheet" (click)="$event.stopPropagation()">
1345
+
1346
+ <div class="tab-switcher-header">
1347
+ <span class="tab-switcher-title">{{ pages.length }} Workspace{{ pages.length !== 1 ? 's' : '' }}</span>
1348
+ <button class="tab-switcher-close-btn" (click)="closeTabSwitcher()" title="Close">
1349
+ <i class="la la-times"></i>
1350
+ </button>
1351
+ </div>
1352
+
1353
+ <div class="tab-switcher-grid">
1354
+ <div
1355
+ *ngFor="let page of pages"
1356
+ class="tab-card"
1357
+ [class.active]="page.id === activePageId"
1358
+ (click)="onSwitcherSelect(page.id)"
1359
+ >
1360
+ <div class="tab-card-icon">
1361
+ <i class="la la-th-large"></i>
1362
+ </div>
1363
+ <span class="tab-card-name">{{ page.name }}</span>
1364
+ <div class="tab-card-actions">
1365
+ <button class="tab-card-btn tab-card-popout" (click)="onSwitcherPopOut($event, page.id)" title="Open in new window">
1366
+ <i class="la la-external-link-alt"></i>
1367
+ </button>
1368
+ <button class="tab-card-btn tab-card-remove" *ngIf="pages.length > 1" (click)="onSwitcherRemove($event, page.id)" title="Close">
1369
+ <i class="la la-times"></i>
1370
+ </button>
1371
+ </div>
1372
+ </div>
1373
+ </div>
1374
+
1375
+ <div class="tab-switcher-footer">
1376
+ <button class="tab-switcher-add-btn" (click)="onSwitcherAdd()">
1377
+ <i class="la la-plus"></i>
1378
+ <span>New Workspace</span>
1379
+ </button>
1380
+ </div>
1381
+
1382
+ </div>
1383
+ </div>
1384
+
1186
1385
  <div class="grid-container">
1187
1386
  <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
1188
1387
  <ng-template gridCell let-widget="widget">
@@ -1201,7 +1400,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImpo
1201
1400
  </main>
1202
1401
  </div>
1203
1402
  </ng-template>
1204
- `, styles: [":host{display:flex;flex-direction:column;width:100%;height:100%;min-height:0;--dash-bg: #000000;--dash-panel-bg: #1c1c1e;--dash-card-bg: #2c2c2e;--dash-fore-color: #8e8e93;--dash-accent-color: #0a84ff;--dash-danger-color: #ff453a;--dash-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;--dash-tabs-container-color: rgba(44,44,46,.6);--dash-tab-active-color: #3a3a3c;--dash-tab-active-text: #ffffff;--dash-tab-hover-text: #e5e5ea;--dash-add-widget-text: #ffffff;--dash-popout-title-color: #ffffff;--dash-widget-title-color: #ffffff;--dash-drag-handle-color: rgba(255,255,255,.2);--dash-widget-border-color: rgba(255,255,255,.07);--dash-widget-btn-bg: rgba(255,255,255,.07);--dash-widget-btn-color: rgba(255,255,255,.5);--dash-placeholder-color: #0a84ff;--dash-resize-handle-color: rgba(255,255,255,.25)}.dashboard-wrapper{display:flex;flex:1;min-height:0;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;border-radius:40px;padding:20px;background:var(--dash-panel-bg);border:1px solid rgba(255,255,255,.05)}.popout-wrapper{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.popout-header{display:flex;align-items:center;margin-bottom:16px;padding:10px 18px;background:var(--dash-panel-bg);border-radius:20px;border:1px solid rgba(255,255,255,.06);flex-shrink:0}.popout-title{font-size:16px;font-weight:700;color:var(--dash-popout-title-color)}.popout-wrapper app-grid{flex:1;min-height:0;overflow:auto}.dashboard-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;gap:12px;flex-shrink:0}.tabs-scroll-track{position:relative;flex:1;min-width:0}.tabs-container{display:flex;align-items:center;gap:8px;background:var(--dash-tabs-container-color);backdrop-filter:blur(10px);border-radius:25px;padding:6px;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}.tabs-container::-webkit-scrollbar{display:none}.tabs-fade-right{position:absolute;top:0;right:0;width:60px;height:100%;border-radius:0 25px 25px 0;pointer-events:none;background:linear-gradient(to right,transparent,var(--dash-panel-bg))}.tab{display:flex;align-items:center;gap:4px;padding:8px 14px 8px 18px;border-radius:20px;cursor:pointer;font-size:14px;font-weight:500;color:var(--dash-fore-color);white-space:nowrap;flex-shrink:0;transition:all .2s}.tab.active{background:var(--dash-tab-active-color);color:var(--dash-tab-active-text)}.tab:hover:not(.active){color:var(--dash-tab-hover-text);background:rgba(255,255,255,.05)}.tab-name{flex:1;white-space:nowrap}.tab-action{background:transparent;border:none;color:var(--dash-fore-color);padding:2px;font-size:11px;cursor:pointer;opacity:0;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}.tab:hover .tab-action{opacity:1}.tab-popout:hover{color:var(--dash-accent-color);background:rgba(10,132,255,.18)}.tab-close:hover{color:var(--dash-danger-color);background:rgba(255,69,58,.2)}.btn-add-page{background:transparent;border:none;color:var(--dash-fore-color);padding:8px 16px;cursor:pointer;border-radius:20px;flex-shrink:0;transition:all .2s}.btn-add-page:hover{color:var(--dash-accent-color);background:rgba(10,132,255,.1)}.btn-add-widget{display:flex;align-items:center;gap:6px;background:var(--dash-accent-color);border:none;color:var(--dash-add-widget-text);font-size:14px;font-weight:600;padding:10px 20px;border-radius:22px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:opacity .2s,transform .15s}.btn-add-widget:hover{opacity:.9;transform:translateY(-1px)}.btn-add-widget:active{transform:translateY(0)}.main-content .grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0}app-grid{background:transparent;display:block;height:100%;width:100%}\n"] }]
1403
+ `, styles: [":host{display:flex;flex-direction:column;width:100%;height:100%;min-height:0;--dash-bg: #000000;--dash-panel-bg: #1c1c1e;--dash-panel-border: rgba(255,255,255,.05);--dash-card-bg: #2c2c2e;--dash-fore-color: #8e8e93;--dash-accent-color: #0a84ff;--dash-accent-tint: rgba(10,132,255,.15);--dash-accent-tint-strong: rgba(10,132,255,.25);--dash-danger-color: #ff453a;--dash-danger-tint: rgba(255,69,58,.22);--dash-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;--dash-tabs-container-color: rgba(44,44,46,.6);--dash-tab-active-color: #3a3a3c;--dash-tab-active-text: #ffffff;--dash-tab-hover-text: #e5e5ea;--dash-tab-hover-bg: rgba(255,255,255,.05);--dash-add-widget-text: #ffffff;--dash-overflow-btn-bg: rgba(255,255,255,.08);--dash-overflow-btn-border: rgba(255,255,255,.12);--dash-overlay-bg: rgba(0,0,0,.55);--dash-sheet-border: rgba(255,255,255,.08);--dash-scrollbar-color: rgba(255,255,255,.15);--dash-popout-title-color: #ffffff;--dash-widget-title-color: #ffffff;--dash-drag-handle-color: rgba(255,255,255,.2);--dash-widget-border-color: rgba(255,255,255,.07);--dash-widget-btn-bg: rgba(255,255,255,.07);--dash-widget-btn-color: rgba(255,255,255,.5);--dash-placeholder-color: #0a84ff;--dash-resize-handle-color: rgba(255,255,255,.25)}.dashboard-wrapper{display:flex;flex:1;min-height:0;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;border-radius:40px;padding:20px;background:var(--dash-panel-bg);border:1px solid var(--dash-panel-border);position:relative}.popout-wrapper{display:flex;flex:1;flex-direction:column;min-height:0;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.popout-header{display:flex;align-items:center;margin-bottom:16px;padding:10px 18px;background:var(--dash-panel-bg);border-radius:20px;border:1px solid var(--dash-panel-border);flex-shrink:0}.popout-title{font-size:16px;font-weight:700;color:var(--dash-popout-title-color)}.popout-wrapper app-grid{flex:1;min-height:0;overflow:auto}.dashboard-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;gap:12px;flex-shrink:0}.tabs-track{display:flex;align-items:center;gap:6px;flex:1;min-width:0;background:var(--dash-tabs-container-color);backdrop-filter:blur(10px);border-radius:25px;padding:6px;overflow:hidden}.tab{display:flex;align-items:center;gap:4px;padding:8px 14px 8px 18px;border-radius:20px;cursor:pointer;font-size:14px;font-weight:500;color:var(--dash-fore-color);white-space:nowrap;flex-shrink:0;transition:background .2s,color .2s}.tab.active{background:var(--dash-tab-active-color);color:var(--dash-tab-active-text)}.tab:hover:not(.active){color:var(--dash-tab-hover-text);background:var(--dash-tab-hover-bg)}.tab-name{white-space:nowrap}.tab-action{background:transparent;border:none;color:var(--dash-fore-color);padding:2px;font-size:11px;cursor:pointer;opacity:0;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}.tab:hover .tab-action{opacity:1}.tab-popout:hover{color:var(--dash-accent-color);background:var(--dash-accent-tint)}.tab-close:hover{color:var(--dash-danger-color);background:var(--dash-danger-tint)}.btn-overflow{display:inline-flex;align-items:center;gap:5px;background:var(--dash-overflow-btn-bg);border:1px solid var(--dash-overflow-btn-border);border-radius:20px;padding:7px 13px;cursor:pointer;font-size:13px;font-weight:600;color:var(--dash-tab-active-text);white-space:nowrap;flex-shrink:0;transition:background .2s,border-color .2s,color .2s}.btn-overflow:hover{background:var(--dash-accent-tint);border-color:var(--dash-accent-color);color:var(--dash-accent-color)}.btn-add-page{background:transparent;border:none;color:var(--dash-fore-color);padding:8px 14px;cursor:pointer;border-radius:20px;flex-shrink:0;transition:all .2s}.btn-add-page:hover{color:var(--dash-accent-color);background:var(--dash-accent-tint)}.tab-switcher-overlay{position:absolute;inset:0;z-index:1000;background:var(--dash-overlay-bg);backdrop-filter:blur(4px);display:flex;align-items:flex-start;justify-content:center;padding-top:60px;animation:overlay-in .18s ease}@keyframes overlay-in{0%{opacity:0}to{opacity:1}}.tab-switcher-sheet{background:var(--dash-panel-bg);border:1px solid var(--dash-sheet-border);border-radius:24px;width:min(520px,calc(100% - 32px));max-height:calc(100% - 80px);display:flex;flex-direction:column;overflow:hidden;animation:sheet-in .22s cubic-bezier(.32,1.2,.5,1)}@keyframes sheet-in{0%{transform:translateY(-16px) scale(.97);opacity:0}to{transform:translateY(0) scale(1);opacity:1}}.tab-switcher-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px 12px;border-bottom:1px solid var(--dash-sheet-border);flex-shrink:0}.tab-switcher-title{font-size:15px;font-weight:700;color:var(--dash-tab-active-text)}.tab-switcher-close-btn{background:var(--dash-widget-btn-bg);border:none;color:var(--dash-fore-color);width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;transition:background .15s,color .15s}.tab-switcher-close-btn:hover{background:var(--dash-danger-tint);color:var(--dash-danger-color)}.tab-switcher-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;padding:16px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--dash-scrollbar-color) transparent}.tab-card{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px 12px;border-radius:16px;background:var(--dash-card-bg);border:1.5px solid transparent;cursor:pointer;transition:border-color .15s,background .15s,transform .12s;position:relative}.tab-card:hover{background:var(--dash-tab-hover-bg);transform:translateY(-2px)}.tab-card.active{border-color:var(--dash-accent-color);background:var(--dash-accent-tint)}.tab-card-icon{width:44px;height:44px;border-radius:12px;background:var(--dash-widget-btn-bg);display:flex;align-items:center;justify-content:center;font-size:20px;color:var(--dash-fore-color)}.tab-card.active .tab-card-icon{background:var(--dash-accent-tint-strong);color:var(--dash-accent-color)}.tab-card-name{font-size:13px;font-weight:500;color:var(--dash-tab-active-text);text-align:center;word-break:break-word;line-height:1.3}.tab-card-actions{display:flex;gap:4px;opacity:0;transition:opacity .15s}.tab-card:hover .tab-card-actions{opacity:1}.tab-card-btn{background:var(--dash-widget-btn-bg);border:none;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:12px;color:var(--dash-fore-color);transition:background .15s,color .15s}.tab-card-popout:hover{background:var(--dash-accent-tint-strong);color:var(--dash-accent-color)}.tab-card-remove:hover{background:var(--dash-danger-tint);color:var(--dash-danger-color)}.tab-switcher-footer{padding:12px 16px 16px;border-top:1px solid var(--dash-sheet-border);flex-shrink:0}.tab-switcher-add-btn{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:11px 20px;border-radius:16px;background:var(--dash-accent-tint);border:1.5px dashed var(--dash-accent-tint-strong);color:var(--dash-accent-color);font-size:14px;font-weight:600;cursor:pointer;transition:background .15s,border-color .15s}.tab-switcher-add-btn:hover{background:var(--dash-accent-tint-strong);border-color:var(--dash-accent-color)}.btn-add-widget{display:flex;align-items:center;gap:6px;background:var(--dash-accent-color);border:none;color:var(--dash-add-widget-text);font-size:14px;font-weight:600;padding:10px 20px;border-radius:22px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:opacity .2s,transform .15s}.btn-add-widget:hover{opacity:.9;transform:translateY(-1px)}.btn-add-widget:active{transform:translateY(0)}.main-content .grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0}app-grid{background:transparent;display:block;height:100%;width:100%}\n"] }]
1205
1404
  }], ctorParameters: function () { return [{ type: DashboardStateService }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }]; }, propDecorators: { initialLayout: [{
1206
1405
  type: Input
1207
1406
  }], theme: [{
@@ -1213,9 +1412,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImpo
1213
1412
  }], cellTemplate: [{
1214
1413
  type: ContentChild,
1215
1414
  args: [GridCellDirective, { read: TemplateRef }]
1216
- }], tabsContainerRef: [{
1415
+ }], tabsTrackRef: [{
1217
1416
  type: ViewChild,
1218
- args: ['tabsContainer']
1417
+ args: ['tabsTrack']
1418
+ }], onEscapeKey: [{
1419
+ type: HostListener,
1420
+ args: ['document:keydown.escape']
1219
1421
  }] } });
1220
1422
 
1221
1423
  class DashboardModule {