@dragonworks/ngx-dashboard 20.1.7 → 20.3.1

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,26 +1,30 @@
1
1
  import { CommonModule, isPlatformBrowser } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { signal, computed, ChangeDetectionStrategy, Component, Injectable, inject, InjectionToken, input, model, output, viewChild, ViewContainerRef, DestroyRef, Renderer2, effect, viewChildren, ElementRef, afterNextRender, PLATFORM_ID, untracked } from '@angular/core';
3
+ import { signal, computed, ChangeDetectionStrategy, Component, inject, Injectable, InjectionToken, input, model, output, viewChild, ViewContainerRef, DestroyRef, Renderer2, effect, viewChildren, ElementRef, afterNextRender, PLATFORM_ID, untracked } from '@angular/core';
4
4
  import { signalStoreFeature, withState, withMethods, patchState, withComputed, signalStore, withProps } from '@ngrx/signals';
5
- import * as i1 from '@angular/material/icon';
5
+ import * as i2 from '@angular/material/icon';
6
6
  import { MatIconModule } from '@angular/material/icon';
7
- import * as i2 from '@angular/material/tooltip';
7
+ import * as i2$1 from '@angular/material/tooltip';
8
8
  import { MatTooltipModule } from '@angular/material/tooltip';
9
- import * as i2$1 from '@angular/material/dialog';
9
+ import * as i2$2 from '@angular/material/dialog';
10
10
  import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule, MatDialog } from '@angular/material/dialog';
11
11
  import { firstValueFrom } from 'rxjs';
12
- import * as i1$1 from '@angular/forms';
12
+ import * as i1 from '@angular/forms';
13
13
  import { FormsModule } from '@angular/forms';
14
- import * as i3 from '@angular/material/button';
14
+ import * as i4 from '@angular/material/button';
15
15
  import { MatButtonModule } from '@angular/material/button';
16
- import * as i4 from '@angular/material/radio';
16
+ import * as i4$1 from '@angular/material/radio';
17
17
  import { MatRadioModule } from '@angular/material/radio';
18
- import * as i1$2 from '@angular/material/menu';
18
+ import * as i1$1 from '@angular/material/menu';
19
19
  import { MatMenuTrigger, MatMenuModule } from '@angular/material/menu';
20
- import * as i3$1 from '@angular/material/divider';
20
+ import * as i3 from '@angular/material/divider';
21
21
  import { MatDividerModule } from '@angular/material/divider';
22
22
  import { DomSanitizer } from '@angular/platform-browser';
23
23
 
24
+ // Auto-generated by scripts/generate-versions.js
25
+ // Do not edit manually
26
+ const NGX_DASHBOARD_VERSION = '20.3.1';
27
+
24
28
  /**
25
29
  * Maximum number of columns supported by the grid.
26
30
  * This determines the encoding scheme for cell coordinates.
@@ -172,7 +176,7 @@ const WidgetIdUtils = {
172
176
  */
173
177
  function createEmptyDashboard(dashboardId, rows, columns, gutterSize = '0.5em') {
174
178
  return {
175
- version: '1.0.0',
179
+ version: '1.1.0',
176
180
  dashboardId,
177
181
  rows,
178
182
  columns,
@@ -215,11 +219,13 @@ function createFactoryFromComponent(component) {
215
219
  };
216
220
  }
217
221
 
222
+ // widget-shared-state-provider.ts
223
+
218
224
  class UnknownWidgetComponent {
219
225
  static metadata = {
220
226
  widgetTypeid: '__internal/unknown-widget',
221
- name: 'Unknown Widget',
222
- description: 'Fallback widget for unknown widget types',
227
+ name: $localize `:@@ngx.dashboard.unknown.widget.name:Unknown Widget`,
228
+ description: $localize `:@@ngx.dashboard.unknown.widget.description:Fallback widget for unknown widget types`,
223
229
  svgIcon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M480-280q17 0 28.5-11.5T520-320q0-17-11.5-28.5T480-360q-17 0-28.5 11.5T440-320q0 17 11.5 28.5T480-280Zm-40-160h80v-240h-80v240Zm40 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>',
224
230
  };
225
231
  state = signal({
@@ -239,7 +245,7 @@ class UnknownWidgetComponent {
239
245
  <div class="unknown-widget-container" [matTooltip]="tooltipText()">
240
246
  <mat-icon class="unknown-widget-icon">error_outline</mat-icon>
241
247
  </div>
242
- `, isInline: true, styles: [".unknown-widget-container{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background-color:var(--mat-sys-error);border-radius:8px;container-type:size}.unknown-widget-icon{color:var(--mat-sys-on-error);font-size:clamp(12px,75cqmin,68px);width:clamp(12px,75cqmin,68px);height:clamp(12px,75cqmin,68px)}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
248
+ `, isInline: true, styles: [".unknown-widget-container{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background-color:var(--mat-sys-error);border-radius:8px;container-type:size}.unknown-widget-icon{color:var(--mat-sys-on-error);font-size:clamp(12px,75cqmin,68px);width:clamp(12px,75cqmin,68px);height:clamp(12px,75cqmin,68px)}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
243
249
  }
244
250
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: UnknownWidgetComponent, decorators: [{
245
251
  type: Component,
@@ -254,15 +260,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
254
260
  class DashboardService {
255
261
  #widgetTypes = signal([], ...(ngDevMode ? [{ debugName: "#widgetTypes" }] : []));
256
262
  #widgetFactoryMap = new Map();
263
+ #sharedStateProviders = new Map();
257
264
  #unknownWidgetFactory = createFactoryFromComponent(UnknownWidgetComponent);
258
265
  widgetTypes = this.#widgetTypes.asReadonly(); // make the widget list available as a readonly signal
259
- registerWidgetType(widget) {
260
- if (this.#widgetTypes().some((w) => w.metadata.widgetTypeid === widget.metadata.widgetTypeid)) {
261
- throw new Error(`Widget type '${widget.metadata.widgetTypeid}' is already registered`);
266
+ registerWidgetType(widget, sharedStateProvider) {
267
+ const widgetTypeid = widget.metadata.widgetTypeid;
268
+ if (this.#widgetTypes().some((w) => w.metadata.widgetTypeid === widgetTypeid)) {
269
+ throw new Error(`Widget type '${widgetTypeid}' is already registered`);
270
+ }
271
+ // Register widget factory
272
+ this.#widgetFactoryMap.set(widgetTypeid, createFactoryFromComponent(widget));
273
+ // Register shared state provider if provided
274
+ if (sharedStateProvider) {
275
+ const provider = this.#resolveProvider(sharedStateProvider);
276
+ this.#sharedStateProviders.set(widgetTypeid, provider);
262
277
  }
263
- this.#widgetFactoryMap.set(widget.metadata.widgetTypeid, createFactoryFromComponent(widget));
264
278
  this.#widgetTypes.set([...this.#widgetTypes(), widget]);
265
279
  }
280
+ #resolveProvider(provider) {
281
+ // If it's a class/service, inject it
282
+ if (typeof provider === 'function') {
283
+ return inject(provider);
284
+ }
285
+ return provider;
286
+ }
266
287
  getFactory(widgetTypeid) {
267
288
  const factory = this.#widgetFactoryMap.get(widgetTypeid);
268
289
  if (factory) {
@@ -282,6 +303,49 @@ class DashboardService {
282
303
  },
283
304
  };
284
305
  }
306
+ /**
307
+ * Get the shared state provider for a specific widget type.
308
+ *
309
+ * @param widgetTypeid The widget type identifier
310
+ * @returns The shared state provider, or undefined if none is registered
311
+ */
312
+ getSharedStateProvider(widgetTypeid) {
313
+ return this.#sharedStateProviders.get(widgetTypeid);
314
+ }
315
+ /**
316
+ * Collect shared states for all widget types currently on the dashboard.
317
+ * Called during dashboard export/serialization.
318
+ *
319
+ * @param activeWidgetTypes Set of widget type IDs that are currently in use
320
+ * @returns Map of widget type IDs to their shared states
321
+ */
322
+ collectSharedStates(activeWidgetTypes) {
323
+ const sharedStates = new Map();
324
+ for (const widgetTypeid of activeWidgetTypes) {
325
+ const provider = this.#sharedStateProviders.get(widgetTypeid);
326
+ if (provider) {
327
+ const state = provider.getSharedState();
328
+ if (state !== undefined) {
329
+ sharedStates.set(widgetTypeid, state);
330
+ }
331
+ }
332
+ }
333
+ return sharedStates;
334
+ }
335
+ /**
336
+ * Restore shared states for widget types.
337
+ * Called during dashboard import/deserialization, before widget instances are created.
338
+ *
339
+ * @param states Map of widget type IDs to their shared states
340
+ */
341
+ restoreSharedStates(states) {
342
+ for (const [widgetTypeid, state] of states) {
343
+ const provider = this.#sharedStateProviders.get(widgetTypeid);
344
+ if (provider) {
345
+ provider.setSharedState(state);
346
+ }
347
+ }
348
+ }
285
349
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
286
350
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardService, providedIn: 'root' });
287
351
  }
@@ -366,6 +430,116 @@ function calculateHighlightedZones(dragData, hovered) {
366
430
  return zones;
367
431
  }
368
432
 
433
+ /**
434
+ * Calculate the minimal bounding box for a set of cells.
435
+ * Returns the tightest rectangle that contains all the widgets.
436
+ *
437
+ * @param cells - The cells to calculate bounds for
438
+ * @returns The minimal bounding box, or null if no cells
439
+ */
440
+ function calculateMinimalBoundingBox(cells) {
441
+ if (cells.length === 0) {
442
+ return null;
443
+ }
444
+ let minRow = Infinity;
445
+ let maxRow = -Infinity;
446
+ let minCol = Infinity;
447
+ let maxCol = -Infinity;
448
+ for (const cell of cells) {
449
+ const cellEndRow = cell.row + cell.rowSpan - 1;
450
+ const cellEndCol = cell.col + cell.colSpan - 1;
451
+ minRow = Math.min(minRow, cell.row);
452
+ maxRow = Math.max(maxRow, cellEndRow);
453
+ minCol = Math.min(minCol, cell.col);
454
+ maxCol = Math.max(maxCol, cellEndCol);
455
+ }
456
+ return {
457
+ minRow,
458
+ maxRow,
459
+ minCol,
460
+ maxCol,
461
+ };
462
+ }
463
+ /**
464
+ * Apply selection filtering to extract cells within specified bounds.
465
+ * Calculates new grid dimensions and coordinate offsets for export.
466
+ *
467
+ * @param selection - The grid selection to filter by
468
+ * @param allCells - All cells in the dashboard
469
+ * @param options - Optional filtering options
470
+ * @returns Filtered cells with export parameters
471
+ */
472
+ function applySelectionFilter(selection, allCells, options = {}) {
473
+ // Filter widgets that are completely within the selection bounds
474
+ const filteredCells = allCells.filter((cell) => {
475
+ const cellEndRow = cell.row + cell.rowSpan - 1;
476
+ const cellEndCol = cell.col + cell.colSpan - 1;
477
+ // Widget must be completely within the selection
478
+ return cell.row >= selection.topLeft.row &&
479
+ cell.col >= selection.topLeft.col &&
480
+ cellEndRow <= selection.bottomRight.row &&
481
+ cellEndCol <= selection.bottomRight.col;
482
+ });
483
+ // Determine the actual bounds to use
484
+ let actualMinRow;
485
+ let actualMaxRow;
486
+ let actualMinCol;
487
+ let actualMaxCol;
488
+ if (options.useMinimalBounds && filteredCells.length > 0) {
489
+ // Calculate minimal bounding box of actual widgets
490
+ const bounds = calculateMinimalBoundingBox(filteredCells);
491
+ if (bounds) {
492
+ actualMinRow = bounds.minRow;
493
+ actualMaxRow = bounds.maxRow;
494
+ actualMinCol = bounds.minCol;
495
+ actualMaxCol = bounds.maxCol;
496
+ }
497
+ else {
498
+ // Fallback to selection bounds (shouldn't happen if filteredCells.length > 0)
499
+ actualMinRow = selection.topLeft.row;
500
+ actualMaxRow = selection.bottomRight.row;
501
+ actualMinCol = selection.topLeft.col;
502
+ actualMaxCol = selection.bottomRight.col;
503
+ }
504
+ }
505
+ else {
506
+ // Use selection bounds as-is
507
+ actualMinRow = selection.topLeft.row;
508
+ actualMaxRow = selection.bottomRight.row;
509
+ actualMinCol = selection.topLeft.col;
510
+ actualMaxCol = selection.bottomRight.col;
511
+ }
512
+ // Apply padding if specified (after minimal bounds calculation)
513
+ const padding = options.padding ?? 0;
514
+ let desiredMinRow = actualMinRow;
515
+ let desiredMinCol = actualMinCol;
516
+ if (padding > 0) {
517
+ // Calculate desired bounds (may go below 1)
518
+ desiredMinRow = actualMinRow - padding;
519
+ const desiredMaxRow = actualMaxRow + padding;
520
+ desiredMinCol = actualMinCol - padding;
521
+ const desiredMaxCol = actualMaxCol + padding;
522
+ // Clamp to grid minimum (1-indexed)
523
+ actualMinRow = Math.max(1, desiredMinRow);
524
+ actualMaxRow = desiredMaxRow;
525
+ actualMinCol = Math.max(1, desiredMinCol);
526
+ actualMaxCol = desiredMaxCol;
527
+ }
528
+ // Calculate new grid dimensions from desired bounds (includes full padding)
529
+ const exportRows = actualMaxRow - desiredMinRow + 1;
530
+ const exportColumns = actualMaxCol - desiredMinCol + 1;
531
+ // Calculate offsets for coordinate transformation (from desired, not clamped)
532
+ const rowOffset = desiredMinRow - 1;
533
+ const colOffset = desiredMinCol - 1;
534
+ return {
535
+ cells: filteredCells,
536
+ rows: exportRows,
537
+ columns: exportColumns,
538
+ rowOffset,
539
+ colOffset,
540
+ };
541
+ }
542
+
369
543
  const initialGridConfigState = {
370
544
  rows: 8,
371
545
  columns: 16,
@@ -777,31 +951,60 @@ withMethods((store) => ({
777
951
  store._endResize(apply, {
778
952
  updateWidgetSpan: (cellId, rowSpan, colSpan) => {
779
953
  // Adapter: find widget by cellId and update using widgetId
780
- const widget = store.cells().find(c => CellIdUtils.equals(c.cellId, cellId));
954
+ const widget = store
955
+ .cells()
956
+ .find((c) => CellIdUtils.equals(c.cellId, cellId));
781
957
  if (widget) {
782
958
  store.updateWidgetSpan(widget.widgetId, rowSpan, colSpan);
783
959
  }
784
- }
960
+ },
785
961
  });
786
962
  },
787
963
  // EXPORT/IMPORT METHODS (need access to multiple features)
788
- exportDashboard(getCurrentWidgetStates) {
964
+ exportDashboard(getCurrentWidgetStates, selection, selectionOptions) {
789
965
  // Get live widget states if callback provided, otherwise use stored states
790
966
  const liveWidgetStates = getCurrentWidgetStates?.() || new Map();
967
+ // Determine which widgets to export and grid dimensions
968
+ let widgetsToExport = store.cells();
969
+ let exportRows = store.rows();
970
+ let exportColumns = store.columns();
971
+ let rowOffset = 0;
972
+ let colOffset = 0;
973
+ // Apply selection filtering if specified
974
+ if (selection) {
975
+ const selectionResult = applySelectionFilter(selection, store.cells(), selectionOptions);
976
+ widgetsToExport = selectionResult.cells;
977
+ exportRows = selectionResult.rows;
978
+ exportColumns = selectionResult.columns;
979
+ rowOffset = selectionResult.rowOffset;
980
+ colOffset = selectionResult.colOffset;
981
+ }
982
+ // Collect widget types in use for shared state collection
983
+ const activeWidgetTypes = new Set(widgetsToExport
984
+ .filter((cell) => cell.widgetFactory.widgetTypeid !== '__internal/unknown-widget')
985
+ .map((cell) => cell.widgetFactory.widgetTypeid));
986
+ // Collect shared states from DashboardService
987
+ const sharedStatesMap = store.dashboardService.collectSharedStates(activeWidgetTypes);
988
+ const sharedStates = sharedStatesMap.size > 0
989
+ ? Object.fromEntries(sharedStatesMap)
990
+ : undefined;
791
991
  return {
792
- version: '1.0.0',
992
+ version: '1.1.0',
793
993
  dashboardId: store.dashboardId(),
794
- rows: store.rows(),
795
- columns: store.columns(),
994
+ rows: exportRows,
995
+ columns: exportColumns,
796
996
  gutterSize: store.gutterSize(),
797
- cells: store.cells()
997
+ cells: widgetsToExport
798
998
  .filter((cell) => cell.widgetFactory.widgetTypeid !== '__internal/unknown-widget')
799
999
  .map((cell) => {
800
1000
  const cellIdString = CellIdUtils.toString(cell.cellId);
801
1001
  const currentState = liveWidgetStates.get(cellIdString);
1002
+ // Transform coordinates if selection is specified
1003
+ const exportRow = selection ? cell.row - rowOffset : cell.row;
1004
+ const exportCol = selection ? cell.col - colOffset : cell.col;
802
1005
  return {
803
- row: cell.row,
804
- col: cell.col,
1006
+ row: exportRow,
1007
+ col: exportCol,
805
1008
  rowSpan: cell.rowSpan,
806
1009
  colSpan: cell.colSpan,
807
1010
  flat: cell.flat,
@@ -809,9 +1012,15 @@ withMethods((store) => ({
809
1012
  widgetState: currentState !== undefined ? currentState : cell.widgetState,
810
1013
  };
811
1014
  }),
1015
+ ...(sharedStates && { sharedStates }),
812
1016
  };
813
1017
  },
814
1018
  loadDashboard(data) {
1019
+ // Restore shared states FIRST, before creating widget instances
1020
+ if (data.sharedStates) {
1021
+ const statesMap = new Map(Object.entries(data.sharedStates));
1022
+ store.dashboardService.restoreSharedStates(statesMap);
1023
+ }
815
1024
  // Import full dashboard data with grid configuration
816
1025
  const widgetsById = {};
817
1026
  data.cells.forEach((cellData) => {
@@ -838,34 +1047,6 @@ withMethods((store) => ({
838
1047
  widgetsById,
839
1048
  });
840
1049
  },
841
- // INITIALIZATION METHODS
842
- initializeFromDto(dashboardData) {
843
- // Inline the loadDashboard logic since it's defined later in the same methods block
844
- const widgetsById = {};
845
- dashboardData.cells.forEach((cellData) => {
846
- const factory = store.dashboardService.getFactory(cellData.widgetTypeid);
847
- const widgetId = WidgetIdUtils.generate();
848
- const cell = {
849
- widgetId,
850
- cellId: CellIdUtils.create(cellData.row, cellData.col),
851
- row: cellData.row,
852
- col: cellData.col,
853
- rowSpan: cellData.rowSpan,
854
- colSpan: cellData.colSpan,
855
- flat: cellData.flat,
856
- widgetFactory: factory,
857
- widgetState: cellData.widgetState,
858
- };
859
- widgetsById[WidgetIdUtils.toString(widgetId)] = cell;
860
- });
861
- patchState(store, {
862
- dashboardId: dashboardData.dashboardId,
863
- rows: dashboardData.rows,
864
- columns: dashboardData.columns,
865
- gutterSize: dashboardData.gutterSize,
866
- widgetsById,
867
- });
868
- },
869
1050
  })),
870
1051
  // Cross-feature computed properties that depend on resize + widget data (using utility functions)
871
1052
  withComputed((store) => ({
@@ -904,44 +1085,76 @@ class CellSettingsDialogComponent {
904
1085
  save() {
905
1086
  const newData = {
906
1087
  ...this.data,
907
- flat: this.selectedMode === 'flat'
1088
+ flat: this.selectedMode === 'flat',
908
1089
  };
909
1090
  this.dialogRef.close(newData);
910
1091
  }
911
1092
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellSettingsDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
912
1093
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.1", type: CellSettingsDialogComponent, isStandalone: true, selector: "lib-cell-settings-dialog", ngImport: i0, template: `
913
- <h2 mat-dialog-title>Cell Display Settings</h2>
1094
+ <h2 mat-dialog-title i18n="@@ngx.dashboard.cell.settings.title">
1095
+ Cell Display Settings
1096
+ </h2>
914
1097
  <mat-dialog-content>
915
- <p class="cell-info">Cell ID: <strong>{{ data.id }}</strong></p>
916
-
1098
+ <p class="cell-info" i18n="@@ngx.dashboard.cell.settings.cellId">
1099
+ Cell ID: <strong>{{ data.id }}</strong>
1100
+ </p>
1101
+
917
1102
  <div class="radio-group">
918
1103
  <mat-radio-group [(ngModel)]="selectedMode" name="displayMode">
919
1104
  <mat-radio-button value="normal">
920
1105
  <div class="radio-option">
921
- <div class="option-title">Normal</div>
922
- <div class="option-description">Standard cell display with full content visibility</div>
1106
+ <div
1107
+ class="option-title"
1108
+ i18n="@@ngx.dashboard.cell.settings.mode.normal"
1109
+ >
1110
+ Normal
1111
+ </div>
1112
+ <div
1113
+ class="option-description"
1114
+ i18n="@@ngx.dashboard.cell.settings.mode.normal.description"
1115
+ >
1116
+ Standard cell display with full content visibility
1117
+ </div>
923
1118
  </div>
924
1119
  </mat-radio-button>
925
-
1120
+
926
1121
  <mat-radio-button value="flat">
927
1122
  <div class="radio-option">
928
- <div class="option-title">Flat</div>
929
- <div class="option-description">Simplified display with reduced visual emphasis</div>
1123
+ <div
1124
+ class="option-title"
1125
+ i18n="@@ngx.dashboard.cell.settings.mode.flat"
1126
+ >
1127
+ Flat
1128
+ </div>
1129
+ <div
1130
+ class="option-description"
1131
+ i18n="@@ngx.dashboard.cell.settings.mode.flat.description"
1132
+ >
1133
+ Simplified display with reduced visual emphasis
1134
+ </div>
930
1135
  </div>
931
1136
  </mat-radio-button>
932
1137
  </mat-radio-group>
933
1138
  </div>
934
1139
  </mat-dialog-content>
935
1140
  <mat-dialog-actions align="end">
936
- <button mat-button (click)="onCancel()">Cancel</button>
937
- <button
938
- mat-flat-button
1141
+ <button
1142
+ mat-button
1143
+ (click)="onCancel()"
1144
+ i18n="@@ngx.dashboard.common.cancel"
1145
+ >
1146
+ Cancel
1147
+ </button>
1148
+ <button
1149
+ mat-flat-button
939
1150
  (click)="save()"
940
- [disabled]="selectedMode === currentMode">
1151
+ [disabled]="selectedMode === currentMode"
1152
+ i18n="@@ngx.dashboard.common.apply"
1153
+ >
941
1154
  Apply
942
1155
  </button>
943
1156
  </mat-dialog-actions>
944
- `, isInline: true, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden;padding-top:.5rem}.cell-info{margin:0 0 1.5rem;padding-bottom:1rem}.radio-group{width:100%}mat-radio-group{display:block}mat-radio-button{width:100%;display:block;margin-bottom:1rem}mat-radio-button:last-child{margin-bottom:0}.radio-option{margin-left:.75rem;padding:.25rem 0}.option-title{display:block;margin-bottom:.25rem}.option-description{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i2$1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i2$1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i2$1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i4.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i4.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }] });
1157
+ `, isInline: true, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden;padding-top:.5rem}.cell-info{margin:0 0 1.5rem;padding-bottom:1rem}.radio-group{width:100%}mat-radio-group{display:block}mat-radio-button{width:100%;display:block;margin-bottom:1rem}mat-radio-button:last-child{margin-bottom:0}.radio-option{margin-left:.75rem;padding:.25rem 0}.option-title{display:block;margin-bottom:.25rem}.option-description{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i2$2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i2$2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i2$2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i4$1.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i4$1.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }] });
945
1158
  }
946
1159
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellSettingsDialogComponent, decorators: [{
947
1160
  type: Component,
@@ -952,34 +1165,66 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
952
1165
  MatButtonModule,
953
1166
  MatRadioModule,
954
1167
  ], template: `
955
- <h2 mat-dialog-title>Cell Display Settings</h2>
1168
+ <h2 mat-dialog-title i18n="@@ngx.dashboard.cell.settings.title">
1169
+ Cell Display Settings
1170
+ </h2>
956
1171
  <mat-dialog-content>
957
- <p class="cell-info">Cell ID: <strong>{{ data.id }}</strong></p>
958
-
1172
+ <p class="cell-info" i18n="@@ngx.dashboard.cell.settings.cellId">
1173
+ Cell ID: <strong>{{ data.id }}</strong>
1174
+ </p>
1175
+
959
1176
  <div class="radio-group">
960
1177
  <mat-radio-group [(ngModel)]="selectedMode" name="displayMode">
961
1178
  <mat-radio-button value="normal">
962
1179
  <div class="radio-option">
963
- <div class="option-title">Normal</div>
964
- <div class="option-description">Standard cell display with full content visibility</div>
1180
+ <div
1181
+ class="option-title"
1182
+ i18n="@@ngx.dashboard.cell.settings.mode.normal"
1183
+ >
1184
+ Normal
1185
+ </div>
1186
+ <div
1187
+ class="option-description"
1188
+ i18n="@@ngx.dashboard.cell.settings.mode.normal.description"
1189
+ >
1190
+ Standard cell display with full content visibility
1191
+ </div>
965
1192
  </div>
966
1193
  </mat-radio-button>
967
-
1194
+
968
1195
  <mat-radio-button value="flat">
969
1196
  <div class="radio-option">
970
- <div class="option-title">Flat</div>
971
- <div class="option-description">Simplified display with reduced visual emphasis</div>
1197
+ <div
1198
+ class="option-title"
1199
+ i18n="@@ngx.dashboard.cell.settings.mode.flat"
1200
+ >
1201
+ Flat
1202
+ </div>
1203
+ <div
1204
+ class="option-description"
1205
+ i18n="@@ngx.dashboard.cell.settings.mode.flat.description"
1206
+ >
1207
+ Simplified display with reduced visual emphasis
1208
+ </div>
972
1209
  </div>
973
1210
  </mat-radio-button>
974
1211
  </mat-radio-group>
975
1212
  </div>
976
1213
  </mat-dialog-content>
977
1214
  <mat-dialog-actions align="end">
978
- <button mat-button (click)="onCancel()">Cancel</button>
979
- <button
980
- mat-flat-button
1215
+ <button
1216
+ mat-button
1217
+ (click)="onCancel()"
1218
+ i18n="@@ngx.dashboard.common.cancel"
1219
+ >
1220
+ Cancel
1221
+ </button>
1222
+ <button
1223
+ mat-flat-button
981
1224
  (click)="save()"
982
- [disabled]="selectedMode === currentMode">
1225
+ [disabled]="selectedMode === currentMode"
1226
+ i18n="@@ngx.dashboard.common.apply"
1227
+ >
983
1228
  Apply
984
1229
  </button>
985
1230
  </mat-dialog-actions>
@@ -1184,19 +1429,19 @@ class CellComponent {
1184
1429
  event.stopPropagation();
1185
1430
  const items = [
1186
1431
  {
1187
- label: 'Edit Widget',
1432
+ label: $localize `:@@ngx.dashboard.cell.menu.edit:Edit Widget`,
1188
1433
  icon: 'edit',
1189
1434
  action: () => this.onEdit(),
1190
1435
  disabled: !this.canEdit(),
1191
1436
  },
1192
1437
  {
1193
- label: 'Settings',
1438
+ label: $localize `:@@ngx.dashboard.cell.menu.settings:Settings`,
1194
1439
  icon: 'settings',
1195
1440
  action: () => this.onSettings(),
1196
1441
  },
1197
1442
  { divider: true },
1198
1443
  {
1199
- label: 'Delete',
1444
+ label: $localize `:@@ngx.dashboard.cell.menu.delete:Delete`,
1200
1445
  icon: 'delete',
1201
1446
  action: () => this.onDelete(),
1202
1447
  },
@@ -1313,7 +1558,7 @@ class CellComponent {
1313
1558
  return this.widgetState();
1314
1559
  }
1315
1560
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1316
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: CellComponent, isStandalone: true, selector: "lib-cell", inputs: { widgetId: { classPropertyName: "widgetId", publicName: "widgetId", isSignal: true, isRequired: true, transformFunction: null }, cellId: { classPropertyName: "cellId", publicName: "cellId", isSignal: true, isRequired: true, transformFunction: null }, widgetFactory: { classPropertyName: "widgetFactory", publicName: "widgetFactory", isSignal: true, isRequired: false, transformFunction: null }, widgetState: { classPropertyName: "widgetState", publicName: "widgetState", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: false, transformFunction: null }, flat: { classPropertyName: "flat", publicName: "flat", isSignal: true, isRequired: false, transformFunction: null }, row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, rowSpan: { classPropertyName: "rowSpan", publicName: "rowSpan", isSignal: true, isRequired: false, transformFunction: null }, colSpan: { classPropertyName: "colSpan", publicName: "colSpan", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { row: "rowChange", column: "columnChange", dragStart: "dragStart", dragEnd: "dragEnd", edit: "edit", delete: "delete", settings: "settings", resizeStart: "resizeStart", resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { properties: { "style.grid-row": "gridRowStyle()", "style.grid-column": "gridColumnStyle()", "class.is-dragging": "isDragging()", "class.drag-active": "isDragActive()", "class.flat": "flat() === true" } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<!-- cell.component.html -->\r\n<div\r\n class=\"cell\"\r\n [class.is-resizing]=\"isResizing()\"\r\n [class.flat]=\"flat() === true\"\r\n [draggable]=\"draggable()\"\r\n (dragstart)=\"onDragStart($event)\"\r\n (dragend)=\"onDragEnd()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <div class=\"content-area\">\r\n <ng-template #container></ng-template>\r\n </div>\r\n @if (isEditMode() && !isDragging()) {\r\n <!-- Right resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--right\"\r\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n <!-- Bottom resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--bottom\"\r\n (mousedown)=\"onResizeStart($event, 'vertical')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n }\r\n</div>\r\n\r\n@if (isResizing()) {\r\n<div class=\"resize-preview\">\r\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\r\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\r\n</div>\r\n}\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(.is-dragging) .content-area{pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto;pointer-events:auto;position:relative;z-index:1}.content-area:hover{transform:initial}:host(:not(.is-dragging)) .cell.flat .content-area{pointer-events:auto}:host(:not(.is-dragging)) .cell.flat .content-area:hover{transform:initial}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}:root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1561
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: CellComponent, isStandalone: true, selector: "lib-cell", inputs: { widgetId: { classPropertyName: "widgetId", publicName: "widgetId", isSignal: true, isRequired: true, transformFunction: null }, cellId: { classPropertyName: "cellId", publicName: "cellId", isSignal: true, isRequired: true, transformFunction: null }, widgetFactory: { classPropertyName: "widgetFactory", publicName: "widgetFactory", isSignal: true, isRequired: false, transformFunction: null }, widgetState: { classPropertyName: "widgetState", publicName: "widgetState", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: false, transformFunction: null }, flat: { classPropertyName: "flat", publicName: "flat", isSignal: true, isRequired: false, transformFunction: null }, row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, rowSpan: { classPropertyName: "rowSpan", publicName: "rowSpan", isSignal: true, isRequired: false, transformFunction: null }, colSpan: { classPropertyName: "colSpan", publicName: "colSpan", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { row: "rowChange", column: "columnChange", dragStart: "dragStart", dragEnd: "dragEnd", edit: "edit", delete: "delete", settings: "settings", resizeStart: "resizeStart", resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { properties: { "style.grid-row": "gridRowStyle()", "style.grid-column": "gridColumnStyle()", "class.is-dragging": "isDragging()", "class.drag-active": "isDragActive()", "class.flat": "flat() === true" } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<!-- cell.component.html -->\r\n<div\r\n class=\"cell\"\r\n [class.is-resizing]=\"isResizing()\"\r\n [class.flat]=\"flat() === true\"\r\n [draggable]=\"draggable()\"\r\n (dragstart)=\"onDragStart($event)\"\r\n (dragend)=\"onDragEnd()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <div class=\"content-area\">\r\n <ng-template #container></ng-template>\r\n </div>\r\n @if (isEditMode() && !isDragging()) {\r\n <!-- Right resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--right\"\r\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n <!-- Bottom resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--bottom\"\r\n (mousedown)=\"onResizeStart($event, 'vertical')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n }\r\n</div>\r\n\r\n@if (isResizing()) {\r\n<div class=\"resize-preview\" i18n=\"@@ngx.dashboard.cell.resize.dimensions\">\r\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\r\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\r\n</div>\r\n}\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(.is-dragging) .content-area{pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto;pointer-events:auto;position:relative;z-index:1}.content-area:hover{transform:initial}:host(:not(.is-dragging)) .cell.flat .content-area{pointer-events:auto}:host(:not(.is-dragging)) .cell.flat .content-area:hover{transform:initial}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}:root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1317
1562
  }
1318
1563
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellComponent, decorators: [{
1319
1564
  type: Component,
@@ -1323,18 +1568,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
1323
1568
  '[class.is-dragging]': 'isDragging()',
1324
1569
  '[class.drag-active]': 'isDragActive()',
1325
1570
  '[class.flat]': 'flat() === true',
1326
- }, template: "<!-- cell.component.html -->\r\n<div\r\n class=\"cell\"\r\n [class.is-resizing]=\"isResizing()\"\r\n [class.flat]=\"flat() === true\"\r\n [draggable]=\"draggable()\"\r\n (dragstart)=\"onDragStart($event)\"\r\n (dragend)=\"onDragEnd()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <div class=\"content-area\">\r\n <ng-template #container></ng-template>\r\n </div>\r\n @if (isEditMode() && !isDragging()) {\r\n <!-- Right resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--right\"\r\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n <!-- Bottom resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--bottom\"\r\n (mousedown)=\"onResizeStart($event, 'vertical')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n }\r\n</div>\r\n\r\n@if (isResizing()) {\r\n<div class=\"resize-preview\">\r\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\r\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\r\n</div>\r\n}\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(.is-dragging) .content-area{pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto;pointer-events:auto;position:relative;z-index:1}.content-area:hover{transform:initial}:host(:not(.is-dragging)) .cell.flat .content-area{pointer-events:auto}:host(:not(.is-dragging)) .cell.flat .content-area:hover{transform:initial}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}:root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}\n"] }]
1571
+ }, template: "<!-- cell.component.html -->\r\n<div\r\n class=\"cell\"\r\n [class.is-resizing]=\"isResizing()\"\r\n [class.flat]=\"flat() === true\"\r\n [draggable]=\"draggable()\"\r\n (dragstart)=\"onDragStart($event)\"\r\n (dragend)=\"onDragEnd()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <div class=\"content-area\">\r\n <ng-template #container></ng-template>\r\n </div>\r\n @if (isEditMode() && !isDragging()) {\r\n <!-- Right resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--right\"\r\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n <!-- Bottom resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--bottom\"\r\n (mousedown)=\"onResizeStart($event, 'vertical')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n }\r\n</div>\r\n\r\n@if (isResizing()) {\r\n<div class=\"resize-preview\" i18n=\"@@ngx.dashboard.cell.resize.dimensions\">\r\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\r\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\r\n</div>\r\n}\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(.is-dragging) .content-area{pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto;pointer-events:auto;position:relative;z-index:1}.content-area:hover{transform:initial}:host(:not(.is-dragging)) .cell.flat .content-area{pointer-events:auto}:host(:not(.is-dragging)) .cell.flat .content-area:hover{transform:initial}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}:root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}\n"] }]
1327
1572
  }], ctorParameters: () => [] });
1328
1573
 
1329
1574
  class DashboardViewerComponent {
1330
1575
  #store = inject(DashboardStore);
1576
+ #renderer = inject(Renderer2);
1577
+ #destroyRef = inject(DestroyRef);
1331
1578
  cellComponents = viewChildren(CellComponent, ...(ngDevMode ? [{ debugName: "cellComponents" }] : []));
1579
+ gridElement = viewChild('gridElement', ...(ngDevMode ? [{ debugName: "gridElement" }] : []));
1332
1580
  rows = input.required(...(ngDevMode ? [{ debugName: "rows" }] : []));
1333
1581
  columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : []));
1334
1582
  gutterSize = input('1em', ...(ngDevMode ? [{ debugName: "gutterSize" }] : []));
1335
1583
  gutters = computed(() => this.columns() + 1, ...(ngDevMode ? [{ debugName: "gutters" }] : []));
1584
+ // Selection feature
1585
+ enableSelection = input(false, ...(ngDevMode ? [{ debugName: "enableSelection" }] : []));
1586
+ selectionComplete = output();
1336
1587
  // store signals - read-only
1337
1588
  cells = this.#store.cells;
1589
+ // Selection state
1590
+ isSelecting = signal(false, ...(ngDevMode ? [{ debugName: "isSelecting" }] : []));
1591
+ selectionStart = signal(null, ...(ngDevMode ? [{ debugName: "selectionStart" }] : []));
1592
+ selectionCurrent = signal(null, ...(ngDevMode ? [{ debugName: "selectionCurrent" }] : []));
1593
+ // Computed selection bounds (normalized)
1594
+ selectionBounds = computed(() => {
1595
+ const start = this.selectionStart();
1596
+ const current = this.selectionCurrent();
1597
+ if (!start || !current)
1598
+ return null;
1599
+ return {
1600
+ startRow: Math.min(start.row, current.row),
1601
+ endRow: Math.max(start.row, current.row),
1602
+ startCol: Math.min(start.col, current.col),
1603
+ endCol: Math.max(start.col, current.col),
1604
+ };
1605
+ }, ...(ngDevMode ? [{ debugName: "selectionBounds" }] : []));
1606
+ // Generate array for template iteration
1607
+ rowNumbers = computed(() => Array.from({ length: this.rows() }, (_, i) => i + 1), ...(ngDevMode ? [{ debugName: "rowNumbers" }] : []));
1608
+ colNumbers = computed(() => Array.from({ length: this.columns() }, (_, i) => i + 1), ...(ngDevMode ? [{ debugName: "colNumbers" }] : []));
1609
+ // Document-level event listeners (cleanup needed)
1610
+ #mouseMoveListener;
1611
+ #mouseUpListener;
1338
1612
  constructor() {
1339
1613
  // Sync grid configuration with store when inputs change
1340
1614
  effect(() => {
@@ -1344,32 +1618,101 @@ class DashboardViewerComponent {
1344
1618
  gutterSize: this.gutterSize(),
1345
1619
  });
1346
1620
  });
1621
+ // Clear selection when selection mode is disabled
1622
+ effect(() => {
1623
+ if (!this.enableSelection()) {
1624
+ this.selectionStart.set(null);
1625
+ this.selectionCurrent.set(null);
1626
+ this.isSelecting.set(false);
1627
+ }
1628
+ });
1347
1629
  }
1630
+ // Selection methods
1348
1631
  /**
1349
- * Get current widget states from all cell components.
1350
- * Used during dashboard export to get live widget states.
1632
+ * Check if a cell is within the current selection bounds
1351
1633
  */
1352
- getCurrentWidgetStates() {
1353
- const stateMap = new Map();
1354
- const cells = this.cellComponents();
1355
- for (const cell of cells) {
1356
- const cellId = cell.cellId();
1357
- const currentState = cell.getCurrentWidgetState();
1358
- if (currentState !== undefined) {
1359
- stateMap.set(CellIdUtils.toString(cellId), currentState);
1360
- }
1634
+ isCellSelected(row, col) {
1635
+ const bounds = this.selectionBounds();
1636
+ if (!bounds)
1637
+ return false;
1638
+ return (row >= bounds.startRow &&
1639
+ row <= bounds.endRow &&
1640
+ col >= bounds.startCol &&
1641
+ col <= bounds.endCol);
1642
+ }
1643
+ /**
1644
+ * Handle mouse down on ghost cell to start selection
1645
+ */
1646
+ onGhostCellMouseDown(event, row, col) {
1647
+ if (!this.enableSelection())
1648
+ return;
1649
+ if (event.button !== 0)
1650
+ return; // Only left button
1651
+ event.preventDefault();
1652
+ event.stopPropagation();
1653
+ this.isSelecting.set(true);
1654
+ this.selectionStart.set({ row, col });
1655
+ this.selectionCurrent.set({ row, col });
1656
+ // Add document-level listeners for drag
1657
+ this.#mouseMoveListener = this.#renderer.listen('document', 'mousemove', () => this.onDocumentMouseMove());
1658
+ this.#mouseUpListener = this.#renderer.listen('document', 'mouseup', () => this.onDocumentMouseUp());
1659
+ // Register cleanup
1660
+ this.#destroyRef.onDestroy(() => {
1661
+ this.cleanupListeners();
1662
+ });
1663
+ }
1664
+ /**
1665
+ * Handle mouse enter on ghost cell during selection
1666
+ */
1667
+ onGhostCellMouseEnter(row, col) {
1668
+ if (!this.isSelecting())
1669
+ return;
1670
+ this.selectionCurrent.set({ row, col });
1671
+ }
1672
+ /**
1673
+ * Handle document mouse move during selection
1674
+ */
1675
+ onDocumentMouseMove() {
1676
+ if (!this.isSelecting())
1677
+ return;
1678
+ // The actual selection update is handled by onGhostCellMouseEnter
1679
+ // This just ensures we capture the event
1680
+ }
1681
+ /**
1682
+ * Handle document mouse up to complete selection
1683
+ */
1684
+ onDocumentMouseUp() {
1685
+ if (!this.isSelecting())
1686
+ return;
1687
+ this.isSelecting.set(false);
1688
+ // Emit selection event
1689
+ const bounds = this.selectionBounds();
1690
+ if (bounds) {
1691
+ this.selectionComplete.emit({
1692
+ topLeft: { row: bounds.startRow, col: bounds.startCol },
1693
+ bottomRight: { row: bounds.endRow, col: bounds.endCol },
1694
+ });
1361
1695
  }
1362
- return stateMap;
1696
+ // Clean up listeners
1697
+ this.cleanupListeners();
1698
+ // Don't clear selection - let the parent control when to clear
1699
+ // Selection remains visible until enableSelection becomes false
1363
1700
  }
1364
1701
  /**
1365
- * Export dashboard with live widget states from current component instances.
1366
- * This ensures the most up-to-date widget states are captured.
1702
+ * Clean up document-level event listeners
1367
1703
  */
1368
- exportDashboard() {
1369
- return this.#store.exportDashboard(() => this.getCurrentWidgetStates());
1704
+ cleanupListeners() {
1705
+ if (this.#mouseMoveListener) {
1706
+ this.#mouseMoveListener();
1707
+ this.#mouseMoveListener = undefined;
1708
+ }
1709
+ if (this.#mouseUpListener) {
1710
+ this.#mouseUpListener();
1711
+ this.#mouseUpListener = undefined;
1712
+ }
1370
1713
  }
1371
1714
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1372
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardViewerComponent, isStandalone: true, selector: "ngx-dashboard-viewer", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "rows()", "style.--columns": "columns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "gutters()" } }, viewQueries: [{ propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\">\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["widgetId", "cellId", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1715
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardViewerComponent, isStandalone: true, selector: "ngx-dashboard-viewer", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null }, enableSelection: { classPropertyName: "enableSelection", publicName: "enableSelection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionComplete: "selectionComplete" }, host: { properties: { "style.--rows": "rows()", "style.--columns": "columns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "gutters()" } }, viewQueries: [{ propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }, { propertyName: "gridElement", first: true, predicate: ["gridElement"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\" #gridElement>\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n\r\n<!-- Selection overlay grid - mirror of main grid for cell selection -->\r\n@if (enableSelection()) {\r\n <div class=\"selection-overlay-grid\">\r\n @for (row of rowNumbers(); track row) {\r\n @for (col of colNumbers(); track col) {\r\n <div\r\n class=\"selection-ghost-cell\"\r\n [class.selected]=\"isCellSelected(row, col)\"\r\n [class.selecting]=\"isSelecting()\"\r\n [style.grid-row]=\"row\"\r\n [style.grid-column]=\"col\"\r\n (mousedown)=\"onGhostCellMouseDown($event, row, col)\"\r\n (mouseenter)=\"onGhostCellMouseEnter(row, col)\"\r\n ></div>\r\n }\r\n }\r\n </div>\r\n}\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;position:relative;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}.selection-overlay-grid{position:absolute;top:0;left:0;width:100%;height:100%;display:grid;gap:var(--gutter-size);padding:var(--gutter-size);grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size));z-index:5;pointer-events:auto;-webkit-user-select:none;user-select:none}.selection-ghost-cell{cursor:crosshair;transition:background-color .1s ease,border-radius .1s ease;border-radius:2px}.selection-ghost-cell:hover:not(.selecting){background-color:var(--mat-sys-primary);opacity:.08}.selection-ghost-cell.selected{background-color:var(--mat-sys-primary);opacity:.25;border-radius:4px}.selection-ghost-cell.selecting{cursor:crosshair}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["widgetId", "cellId", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1373
1716
  }
1374
1717
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewerComponent, decorators: [{
1375
1718
  type: Component,
@@ -1378,12 +1721,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
1378
1721
  '[style.--columns]': 'columns()',
1379
1722
  '[style.--gutter-size]': 'gutterSize()',
1380
1723
  '[style.--gutters]': 'gutters()',
1381
- }, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\">\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}\n"] }]
1724
+ }, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\" #gridElement>\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n\r\n<!-- Selection overlay grid - mirror of main grid for cell selection -->\r\n@if (enableSelection()) {\r\n <div class=\"selection-overlay-grid\">\r\n @for (row of rowNumbers(); track row) {\r\n @for (col of colNumbers(); track col) {\r\n <div\r\n class=\"selection-ghost-cell\"\r\n [class.selected]=\"isCellSelected(row, col)\"\r\n [class.selecting]=\"isSelecting()\"\r\n [style.grid-row]=\"row\"\r\n [style.grid-column]=\"col\"\r\n (mousedown)=\"onGhostCellMouseDown($event, row, col)\"\r\n (mouseenter)=\"onGhostCellMouseEnter(row, col)\"\r\n ></div>\r\n }\r\n }\r\n </div>\r\n}\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;position:relative;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}.selection-overlay-grid{position:absolute;top:0;left:0;width:100%;height:100%;display:grid;gap:var(--gutter-size);padding:var(--gutter-size);grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size));z-index:5;pointer-events:auto;-webkit-user-select:none;user-select:none}.selection-ghost-cell{cursor:crosshair;transition:background-color .1s ease,border-radius .1s ease;border-radius:2px}.selection-ghost-cell:hover:not(.selecting){background-color:var(--mat-sys-primary);opacity:.08}.selection-ghost-cell.selected{background-color:var(--mat-sys-primary);opacity:.25;border-radius:4px}.selection-ghost-cell.selecting{cursor:crosshair}\n"] }]
1382
1725
  }], ctorParameters: () => [] });
1383
1726
 
1384
1727
  class CellContextMenuComponent {
1385
1728
  menuTrigger = viewChild.required('menuTrigger', { read: MatMenuTrigger });
1386
1729
  menuService = inject(CellContextMenuService);
1730
+ #renderer = inject(Renderer2);
1731
+ #destroyRef = inject(DestroyRef);
1387
1732
  menuPosition = computed(() => {
1388
1733
  const menu = this.menuService.activeMenu();
1389
1734
  return menu ? { left: `${menu.x}px`, top: `${menu.y}px` } : { left: '0px', top: '0px' };
@@ -1393,6 +1738,23 @@ class CellContextMenuComponent {
1393
1738
  return menu?.items || [];
1394
1739
  }, ...(ngDevMode ? [{ debugName: "menuItems" }] : []));
1395
1740
  constructor() {
1741
+ // Material Menu has a backdrop that blocks events from reaching other elements.
1742
+ // When any right-click occurs while menu is open, we need to:
1743
+ // 1. Close the current menu
1744
+ // 2. Prevent the browser's default context menu
1745
+ //
1746
+ // Users will need to right-click again to open empty cell menus.
1747
+ // This follows standard UX patterns used by most applications.
1748
+ const unlisten = this.#renderer.listen('document', 'contextmenu', (event) => {
1749
+ if (this.menuService.activeMenu()) {
1750
+ // Prevent browser's default context menu when closing widget menu
1751
+ event.preventDefault();
1752
+ this.menuService.hide();
1753
+ }
1754
+ });
1755
+ this.#destroyRef.onDestroy(() => {
1756
+ unlisten();
1757
+ });
1396
1758
  effect(() => {
1397
1759
  const menu = this.menuService.activeMenu();
1398
1760
  if (menu) {
@@ -1488,7 +1850,7 @@ class CellContextMenuComponent {
1488
1850
  }
1489
1851
  }
1490
1852
  </mat-menu>
1491
- `, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i1$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i1$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i1$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3$1.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1853
+ `, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i1$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i1$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i1$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1492
1854
  }
1493
1855
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellContextMenuComponent, decorators: [{
1494
1856
  type: Component,
@@ -1547,6 +1909,232 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
1547
1909
  `, styles: [":host{display:contents}\n"] }]
1548
1910
  }], ctorParameters: () => [] });
1549
1911
 
1912
+ /**
1913
+ * Abstract provider for handling context menu events on empty dashboard cells.
1914
+ * Implement this to provide custom behavior when users right-click on unoccupied grid spaces.
1915
+ *
1916
+ * @example
1917
+ * ```typescript
1918
+ * @Injectable()
1919
+ * export class CustomEmptyCellProvider extends EmptyCellContextProvider {
1920
+ * handleEmptyCellContext(event: MouseEvent, context: EmptyCellContext): void {
1921
+ * event.preventDefault();
1922
+ * // Show custom menu, open dialog, etc.
1923
+ * }
1924
+ * }
1925
+ * ```
1926
+ */
1927
+ class EmptyCellContextProvider {
1928
+ }
1929
+
1930
+ /**
1931
+ * Default empty cell context provider that prevents the browser's context menu
1932
+ * and performs no other action.
1933
+ *
1934
+ * This is the default behavior that allows users to right-click on empty dashboard
1935
+ * cells without triggering the browser's default context menu.
1936
+ */
1937
+ class DefaultEmptyCellContextProvider extends EmptyCellContextProvider {
1938
+ /**
1939
+ * Default empty cell context handler.
1940
+ * The browser context menu is already prevented by the component.
1941
+ * No additional action is taken by default.
1942
+ */
1943
+ handleEmptyCellContext() {
1944
+ // Default behavior: do nothing
1945
+ }
1946
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultEmptyCellContextProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1947
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultEmptyCellContextProvider, providedIn: 'root' });
1948
+ }
1949
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultEmptyCellContextProvider, decorators: [{
1950
+ type: Injectable,
1951
+ args: [{
1952
+ providedIn: 'root',
1953
+ }]
1954
+ }] });
1955
+
1956
+ /**
1957
+ * Injection token for the empty cell context provider.
1958
+ * Use this to provide your custom implementation for handling right-clicks on empty dashboard cells.
1959
+ *
1960
+ * @example
1961
+ * ```typescript
1962
+ * // Provide a custom implementation
1963
+ * providers: [
1964
+ * {
1965
+ * provide: EMPTY_CELL_CONTEXT_PROVIDER,
1966
+ * useClass: MyCustomEmptyCellProvider
1967
+ * }
1968
+ * ]
1969
+ * ```
1970
+ */
1971
+ const EMPTY_CELL_CONTEXT_PROVIDER = new InjectionToken('EmptyCellContextProvider', {
1972
+ providedIn: 'root',
1973
+ factory: () => new DefaultEmptyCellContextProvider(),
1974
+ });
1975
+
1976
+ /**
1977
+ * Service for managing empty cell context menu state.
1978
+ * Similar to CellContextMenuService but specifically for empty cells.
1979
+ *
1980
+ * This service is internal to the library and manages the display state
1981
+ * of the context menu that appears when right-clicking on empty dashboard cells.
1982
+ */
1983
+ class EmptyCellContextMenuService {
1984
+ #activeMenu = signal(null, ...(ngDevMode ? [{ debugName: "#activeMenu" }] : []));
1985
+ #lastSelectedWidgetTypeId = signal(null, ...(ngDevMode ? [{ debugName: "#lastSelectedWidgetTypeId" }] : []));
1986
+ activeMenu = this.#activeMenu.asReadonly();
1987
+ lastSelectedWidgetTypeId = this.#lastSelectedWidgetTypeId.asReadonly();
1988
+ /**
1989
+ * Show the context menu at specific coordinates with given items.
1990
+ *
1991
+ * @param x - X coordinate (clientX from mouse event)
1992
+ * @param y - Y coordinate (clientY from mouse event)
1993
+ * @param items - Menu items to display
1994
+ */
1995
+ show(x, y, items) {
1996
+ this.#activeMenu.set({ x, y, items });
1997
+ }
1998
+ /**
1999
+ * Hide the context menu.
2000
+ */
2001
+ hide() {
2002
+ this.#activeMenu.set(null);
2003
+ }
2004
+ /**
2005
+ * Set the last selected widget type ID for quick-repeat functionality.
2006
+ * Pass null to clear the last selection.
2007
+ *
2008
+ * @param widgetTypeId - The widget type identifier, or null to clear
2009
+ */
2010
+ setLastSelection(widgetTypeId) {
2011
+ this.#lastSelectedWidgetTypeId.set(widgetTypeId);
2012
+ }
2013
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2014
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuService, providedIn: 'root' });
2015
+ }
2016
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuService, decorators: [{
2017
+ type: Injectable,
2018
+ args: [{
2019
+ providedIn: 'root',
2020
+ }]
2021
+ }] });
2022
+
2023
+ /**
2024
+ * Context provider that displays a widget list menu when right-clicking on empty cells.
2025
+ *
2026
+ * This provider shows a Material Design context menu with all available widget types.
2027
+ * When a user clicks on a widget in the menu, it's immediately added to the empty cell.
2028
+ *
2029
+ * @example
2030
+ * ```typescript
2031
+ * // In your component or app config
2032
+ * providers: [
2033
+ * {
2034
+ * provide: EMPTY_CELL_CONTEXT_PROVIDER,
2035
+ * useClass: WidgetListContextMenuProvider
2036
+ * }
2037
+ * ]
2038
+ * ```
2039
+ *
2040
+ * @public
2041
+ */
2042
+ class WidgetListContextMenuProvider extends EmptyCellContextProvider {
2043
+ #menuService = inject(EmptyCellContextMenuService);
2044
+ #dashboardService = inject(DashboardService);
2045
+ /**
2046
+ * Handle empty cell context menu by showing available widgets.
2047
+ *
2048
+ * @param event - The mouse event from the right-click
2049
+ * @param context - Information about the empty cell and dashboard
2050
+ */
2051
+ handleEmptyCellContext(event, context) {
2052
+ event.preventDefault();
2053
+ // Get available widgets from dashboard service
2054
+ const widgets = this.#dashboardService.widgetTypes();
2055
+ // Create menu items from widgets
2056
+ const items = this.#createMenuItems(widgets, context);
2057
+ // Show the context menu at mouse coordinates
2058
+ this.#menuService.show(event.clientX, event.clientY, items);
2059
+ }
2060
+ /**
2061
+ * Create menu items from available widget types.
2062
+ * Each item includes the widget's icon and display name.
2063
+ * If a widget was previously selected, it appears first as a quick-repeat option.
2064
+ *
2065
+ * @param widgets - Array of widget component classes
2066
+ * @param context - The empty cell context with createWidget callback
2067
+ * @returns Array of menu items ready for display
2068
+ */
2069
+ #createMenuItems(widgets, context) {
2070
+ if (widgets.length === 0) {
2071
+ // Show a message if no widgets are registered
2072
+ return [
2073
+ {
2074
+ label: $localize `:@@ngx.dashboard.emptyCellMenu.noWidgets:No widgets available`,
2075
+ disabled: true,
2076
+ action: () => {
2077
+ // No action needed
2078
+ },
2079
+ },
2080
+ ];
2081
+ }
2082
+ // Build standard menu items with widgetTypeId
2083
+ const allItems = widgets.map((widget) => ({
2084
+ label: widget.metadata.name,
2085
+ svgIcon: widget.metadata.svgIcon,
2086
+ widgetTypeId: widget.metadata.widgetTypeid,
2087
+ action: () => this.#createWidget(widget.metadata.widgetTypeid, context),
2088
+ }));
2089
+ // Check if there's a last selected widget to show as quick-repeat
2090
+ const lastSelectedTypeId = this.#menuService.lastSelectedWidgetTypeId();
2091
+ if (!lastSelectedTypeId) {
2092
+ return allItems;
2093
+ }
2094
+ // Find the last selected widget in the list
2095
+ const lastSelectedWidget = widgets.find((w) => w.metadata.widgetTypeid === lastSelectedTypeId);
2096
+ if (!lastSelectedWidget) {
2097
+ // Last selected widget no longer available, return normal list
2098
+ return allItems;
2099
+ }
2100
+ // Create quick-repeat item (duplicate of the last selected widget)
2101
+ const quickRepeatItem = {
2102
+ label: lastSelectedWidget.metadata.name,
2103
+ svgIcon: lastSelectedWidget.metadata.svgIcon,
2104
+ widgetTypeId: lastSelectedWidget.metadata.widgetTypeid,
2105
+ action: () => this.#createWidget(lastSelectedWidget.metadata.widgetTypeid, context),
2106
+ };
2107
+ // Create divider
2108
+ const divider = { divider: true };
2109
+ // Return special structure: [quick-repeat, divider, full list]
2110
+ return [quickRepeatItem, divider, ...allItems];
2111
+ }
2112
+ /**
2113
+ * Create a widget at the empty cell position.
2114
+ * Uses the createWidget callback provided in the context.
2115
+ *
2116
+ * @param widgetTypeid - The widget type identifier to create
2117
+ * @param context - The empty cell context with createWidget callback
2118
+ */
2119
+ #createWidget(widgetTypeid, context) {
2120
+ if (context.createWidget) {
2121
+ const success = context.createWidget(widgetTypeid);
2122
+ if (!success) {
2123
+ console.error(`Failed to create widget '${widgetTypeid}' at (${context.row}, ${context.col})`);
2124
+ }
2125
+ }
2126
+ else {
2127
+ console.warn('createWidget callback not available in EmptyCellContext. ' +
2128
+ 'Ensure you are using a compatible version of the dashboard component.');
2129
+ }
2130
+ }
2131
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListContextMenuProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2132
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListContextMenuProvider });
2133
+ }
2134
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListContextMenuProvider, decorators: [{
2135
+ type: Injectable
2136
+ }] });
2137
+
1550
2138
  // drop-zone.component.ts
1551
2139
  class DropZoneComponent {
1552
2140
  // Required inputs
@@ -1581,6 +2169,10 @@ class DropZoneComponent {
1581
2169
  }, ...(ngDevMode ? [{ debugName: "dropEffect" }] : []));
1582
2170
  #store = inject(DashboardStore);
1583
2171
  #elementRef = inject(ElementRef);
2172
+ #dashboardService = inject(DashboardService);
2173
+ #contextProvider = inject(EMPTY_CELL_CONTEXT_PROVIDER, {
2174
+ optional: true,
2175
+ });
1584
2176
  get nativeElement() {
1585
2177
  return this.#elementRef.nativeElement;
1586
2178
  }
@@ -1625,14 +2217,267 @@ class DropZoneComponent {
1625
2217
  });
1626
2218
  }
1627
2219
  }
2220
+ /**
2221
+ * Handle context menu events on empty cells.
2222
+ * Only active in edit mode. Delegates to the context provider if available.
2223
+ */
2224
+ onContextMenu(event) {
2225
+ if (!this.editMode())
2226
+ return;
2227
+ // Prevent default browser menu and stop propagation in edit mode
2228
+ // stopPropagation prevents the event from reaching the document-level
2229
+ // listener in EmptyCellContextMenuComponent which would immediately hide the menu
2230
+ event.preventDefault();
2231
+ event.stopPropagation();
2232
+ if (this.#contextProvider) {
2233
+ this.#contextProvider.handleEmptyCellContext(event, {
2234
+ row: this.row(),
2235
+ col: this.col(),
2236
+ totalRows: this.#store.rows(),
2237
+ totalColumns: this.#store.columns(),
2238
+ gutterSize: this.#store.gutterSize(),
2239
+ createWidget: (widgetTypeid) => {
2240
+ const factory = this.#dashboardService.getFactory(widgetTypeid);
2241
+ this.#store.createWidget(this.row(), this.col(), factory, undefined);
2242
+ return true; // Widget created successfully
2243
+ },
2244
+ });
2245
+ }
2246
+ }
1628
2247
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DropZoneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1629
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DropZoneComponent, isStandalone: true, selector: "lib-drop-zone", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, col: { classPropertyName: "col", publicName: "col", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, highlight: { classPropertyName: "highlight", publicName: "highlight", isSignal: true, isRequired: false, transformFunction: null }, highlightInvalid: { classPropertyName: "highlightInvalid", publicName: "highlightInvalid", isSignal: true, isRequired: false, transformFunction: null }, highlightResize: { classPropertyName: "highlightResize", publicName: "highlightResize", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dragEnter: "dragEnter", dragExit: "dragExit", dragOver: "dragOver", dragDrop: "dragDrop" }, ngImport: i0, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;line-height:1.1;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2248
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DropZoneComponent, isStandalone: true, selector: "lib-drop-zone", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, col: { classPropertyName: "col", publicName: "col", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, highlight: { classPropertyName: "highlight", publicName: "highlight", isSignal: true, isRequired: false, transformFunction: null }, highlightInvalid: { classPropertyName: "highlightInvalid", publicName: "highlightInvalid", isSignal: true, isRequired: false, transformFunction: null }, highlightResize: { classPropertyName: "highlightResize", publicName: "highlightResize", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dragEnter: "dragEnter", dragExit: "dragExit", dragOver: "dragOver", dragDrop: "dragDrop" }, ngImport: i0, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;line-height:1.1;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1630
2249
  }
1631
2250
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DropZoneComponent, decorators: [{
1632
2251
  type: Component,
1633
- args: [{ selector: 'lib-drop-zone', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;line-height:1.1;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"] }]
2252
+ args: [{ selector: 'lib-drop-zone', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;line-height:1.1;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"] }]
1634
2253
  }] });
1635
2254
 
2255
+ /**
2256
+ * Context menu component for empty dashboard cells.
2257
+ * Displays a list of available widgets that can be added to the empty cell.
2258
+ *
2259
+ * This component is similar to CellContextMenuComponent but designed specifically
2260
+ * for empty cells and includes support for SVG icons from widget metadata.
2261
+ *
2262
+ * @internal This component is for internal library use only
2263
+ */
2264
+ class EmptyCellContextMenuComponent {
2265
+ menuTrigger = viewChild.required('menuTrigger', { read: MatMenuTrigger });
2266
+ menuService = inject(EmptyCellContextMenuService);
2267
+ #sanitizer = inject(DomSanitizer);
2268
+ #renderer = inject(Renderer2);
2269
+ #destroyRef = inject(DestroyRef);
2270
+ menuPosition = computed(() => {
2271
+ const menu = this.menuService.activeMenu();
2272
+ return menu
2273
+ ? { left: `${menu.x}px`, top: `${menu.y}px` }
2274
+ : { left: '0px', top: '0px' };
2275
+ }, ...(ngDevMode ? [{ debugName: "menuPosition" }] : []));
2276
+ menuItems = computed(() => {
2277
+ const menu = this.menuService.activeMenu();
2278
+ return menu?.items || [];
2279
+ }, ...(ngDevMode ? [{ debugName: "menuItems" }] : []));
2280
+ constructor() {
2281
+ // Material Menu has a backdrop that blocks events from reaching other elements.
2282
+ // When any right-click occurs while menu is open, we need to:
2283
+ // 1. Close the current menu
2284
+ // 2. Prevent the browser's default context menu
2285
+ //
2286
+ // Users will need to right-click again to open empty cell menus.
2287
+ // This follows standard UX patterns used by most applications.
2288
+ const unlisten = this.#renderer.listen('document', 'contextmenu', (event) => {
2289
+ if (this.menuService.activeMenu()) {
2290
+ // Prevent browser's default context menu when closing widget menu
2291
+ event.preventDefault();
2292
+ this.menuService.hide();
2293
+ }
2294
+ });
2295
+ this.#destroyRef.onDestroy(() => {
2296
+ unlisten();
2297
+ });
2298
+ // Show menu when service state changes
2299
+ effect(() => {
2300
+ const menu = this.menuService.activeMenu();
2301
+ if (menu) {
2302
+ // Use queueMicrotask to ensure the view is fully initialized
2303
+ // This fixes the issue where the menu disappears on first right-click
2304
+ queueMicrotask(() => {
2305
+ const trigger = this.menuTrigger();
2306
+ if (trigger) {
2307
+ // Close any existing menu first
2308
+ if (trigger.menuOpen) {
2309
+ trigger.closeMenu();
2310
+ }
2311
+ // Open menu - position is handled by signal
2312
+ trigger.openMenu();
2313
+ }
2314
+ });
2315
+ }
2316
+ else {
2317
+ const trigger = this.menuTrigger();
2318
+ if (trigger) {
2319
+ trigger.closeMenu();
2320
+ }
2321
+ }
2322
+ });
2323
+ // Subscribe to menu closed event once when trigger becomes available
2324
+ // This ensures the service state is synchronized when Material menu closes
2325
+ effect((onCleanup) => {
2326
+ const trigger = this.menuTrigger();
2327
+ if (trigger) {
2328
+ const subscription = trigger.menuClosed.subscribe(() => {
2329
+ if (this.menuService.activeMenu()) {
2330
+ this.menuService.hide();
2331
+ }
2332
+ });
2333
+ // Clean up subscription when effect re-runs or component destroys
2334
+ onCleanup(() => subscription.unsubscribe());
2335
+ }
2336
+ });
2337
+ }
2338
+ executeAction(item) {
2339
+ if (!item.divider && item.action) {
2340
+ // Track widget type if provided
2341
+ if (item.widgetTypeId) {
2342
+ this.menuService.setLastSelection(item.widgetTypeId);
2343
+ }
2344
+ item.action();
2345
+ this.menuService.hide();
2346
+ }
2347
+ }
2348
+ sanitizeSvg(svg) {
2349
+ return this.#sanitizer.bypassSecurityTrustHtml(svg);
2350
+ }
2351
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2352
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: EmptyCellContextMenuComponent, isStandalone: true, selector: "lib-empty-cell-context-menu", viewQueries: [{ propertyName: "menuTrigger", first: true, predicate: ["menuTrigger"], descendants: true, read: MatMenuTrigger, isSignal: true }], ngImport: i0, template: `
2353
+ <!-- Hidden trigger for menu positioned at exact mouse coordinates
2354
+
2355
+ IMPORTANT: Angular Material applies its own positioning logic to menus,
2356
+ which by default offsets the menu from the trigger element to avoid overlap.
2357
+ To achieve precise positioning at mouse coordinates, we use these workarounds:
2358
+
2359
+ 1. The trigger container is 1x1px (not 0x0) because Material needs a physical
2360
+ element to calculate position against. Zero-sized elements cause unpredictable
2361
+ positioning.
2362
+
2363
+ 2. We use opacity:0 instead of visibility:hidden to keep the element in the
2364
+ layout flow while making it invisible.
2365
+
2366
+ 3. The button itself is styled to 1x1px with no padding to serve as a precise
2367
+ anchor point for the menu.
2368
+
2369
+ 4. The mat-menu uses [overlapTrigger]="true" to allow the menu to appear
2370
+ directly at the trigger position rather than offset from it.
2371
+
2372
+ This approach ensures the menu appears at the exact mouse coordinates passed
2373
+ from the empty cell context provider's right-click handler.
2374
+ -->
2375
+ <div
2376
+ style="position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
2377
+ [style]="menuPosition()">
2378
+ <button
2379
+ mat-button
2380
+ #menuTrigger="matMenuTrigger"
2381
+ [matMenuTriggerFor]="contextMenu"
2382
+ style="width: 1px; height: 1px; padding: 0; min-width: 0; line-height: 0;">
2383
+ </button>
2384
+ </div>
2385
+
2386
+ <!-- Context menu with widget list -->
2387
+ <mat-menu
2388
+ #contextMenu="matMenu"
2389
+ [overlapTrigger]="true"
2390
+ class="empty-cell-widget-menu">
2391
+ @for (item of menuItems(); track $index) {
2392
+ @if (item.divider) {
2393
+ <mat-divider></mat-divider>
2394
+ } @else {
2395
+ <button
2396
+ mat-menu-item
2397
+ (click)="executeAction(item)"
2398
+ [disabled]="item.disabled"
2399
+ [attr.aria-label]="item.label">
2400
+ @if (item.svgIcon) {
2401
+ <div class="widget-icon" [innerHTML]="sanitizeSvg(item.svgIcon)"></div>
2402
+ } @else if (item.icon) {
2403
+ <mat-icon>{{ item.icon }}</mat-icon>
2404
+ }
2405
+ {{ item.label }}
2406
+ </button>
2407
+ }
2408
+ }
2409
+ </mat-menu>
2410
+ `, isInline: true, styles: [":host{display:contents}.empty-cell-widget-menu{max-height:400px;overflow-y:auto}.widget-icon{width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;vertical-align:middle}.widget-icon :deep(svg){width:20px;height:20px;fill:currentColor}\n"], dependencies: [{ kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i1$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i1$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i1$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2411
+ }
2412
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuComponent, decorators: [{
2413
+ type: Component,
2414
+ args: [{ selector: 'lib-empty-cell-context-menu', standalone: true, imports: [
2415
+ MatMenuModule,
2416
+ MatIconModule,
2417
+ MatDividerModule,
2418
+ MatButtonModule,
2419
+ CommonModule,
2420
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2421
+ <!-- Hidden trigger for menu positioned at exact mouse coordinates
2422
+
2423
+ IMPORTANT: Angular Material applies its own positioning logic to menus,
2424
+ which by default offsets the menu from the trigger element to avoid overlap.
2425
+ To achieve precise positioning at mouse coordinates, we use these workarounds:
2426
+
2427
+ 1. The trigger container is 1x1px (not 0x0) because Material needs a physical
2428
+ element to calculate position against. Zero-sized elements cause unpredictable
2429
+ positioning.
2430
+
2431
+ 2. We use opacity:0 instead of visibility:hidden to keep the element in the
2432
+ layout flow while making it invisible.
2433
+
2434
+ 3. The button itself is styled to 1x1px with no padding to serve as a precise
2435
+ anchor point for the menu.
2436
+
2437
+ 4. The mat-menu uses [overlapTrigger]="true" to allow the menu to appear
2438
+ directly at the trigger position rather than offset from it.
2439
+
2440
+ This approach ensures the menu appears at the exact mouse coordinates passed
2441
+ from the empty cell context provider's right-click handler.
2442
+ -->
2443
+ <div
2444
+ style="position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
2445
+ [style]="menuPosition()">
2446
+ <button
2447
+ mat-button
2448
+ #menuTrigger="matMenuTrigger"
2449
+ [matMenuTriggerFor]="contextMenu"
2450
+ style="width: 1px; height: 1px; padding: 0; min-width: 0; line-height: 0;">
2451
+ </button>
2452
+ </div>
2453
+
2454
+ <!-- Context menu with widget list -->
2455
+ <mat-menu
2456
+ #contextMenu="matMenu"
2457
+ [overlapTrigger]="true"
2458
+ class="empty-cell-widget-menu">
2459
+ @for (item of menuItems(); track $index) {
2460
+ @if (item.divider) {
2461
+ <mat-divider></mat-divider>
2462
+ } @else {
2463
+ <button
2464
+ mat-menu-item
2465
+ (click)="executeAction(item)"
2466
+ [disabled]="item.disabled"
2467
+ [attr.aria-label]="item.label">
2468
+ @if (item.svgIcon) {
2469
+ <div class="widget-icon" [innerHTML]="sanitizeSvg(item.svgIcon)"></div>
2470
+ } @else if (item.icon) {
2471
+ <mat-icon>{{ item.icon }}</mat-icon>
2472
+ }
2473
+ {{ item.label }}
2474
+ </button>
2475
+ }
2476
+ }
2477
+ </mat-menu>
2478
+ `, styles: [":host{display:contents}.empty-cell-widget-menu{max-height:400px;overflow-y:auto}.widget-icon{width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;vertical-align:middle}.widget-icon :deep(svg){width:20px;height:20px;fill:currentColor}\n"] }]
2479
+ }], ctorParameters: () => [] });
2480
+
1636
2481
  // dashboard-editor.component.ts
1637
2482
  class DashboardEditorComponent {
1638
2483
  bottomGridRef = viewChild.required('bottomGrid');
@@ -1738,33 +2583,10 @@ class DashboardEditorComponent {
1738
2583
  this.#store.handleDrop(event.data, event.target);
1739
2584
  // Note: Store handles all validation and error handling internally
1740
2585
  }
1741
- /**
1742
- * Get current widget states from all cell components.
1743
- * Used during dashboard export to get live widget states.
1744
- */
1745
- getCurrentWidgetStates() {
1746
- const stateMap = new Map();
1747
- const cells = this.cellComponents();
1748
- for (const cell of cells) {
1749
- const cellId = cell.cellId();
1750
- const currentState = cell.getCurrentWidgetState();
1751
- if (currentState !== undefined) {
1752
- stateMap.set(CellIdUtils.toString(cellId), currentState);
1753
- }
1754
- }
1755
- return stateMap;
1756
- }
1757
- /**
1758
- * Export dashboard with live widget states from current component instances.
1759
- * This ensures the most up-to-date widget states are captured.
1760
- */
1761
- exportDashboard() {
1762
- return this.#store.exportDashboard(() => this.getCurrentWidgetStates());
1763
- }
1764
2586
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1765
2587
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardEditorComponent, isStandalone: true, selector: "ngx-dashboard-editor", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "rows()", "style.--columns": "columns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "gutters()", "class.is-edit-mode": "true" } }, providers: [
1766
2588
  CellContextMenuService,
1767
- ], viewQueries: [{ propertyName: "bottomGridRef", first: true, predicate: ["bottomGrid"], descendants: true, isSignal: true }, { propertyName: "dropZones", predicate: DropZoneComponent, descendants: true, isSignal: true }, { propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- dashboard-editor.component.html -->\n<div class=\"grid-container\">\n <!-- Bottom grid with drop zones -->\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\n @for (position of dropzonePositions(); track position.id) {\n <lib-drop-zone\n class=\"drop-zone\"\n [row]=\"position.row\"\n [col]=\"position.col\"\n [index]=\"position.index\"\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\n [highlightInvalid]=\"\n invalidHighlightMap().has(createCellId(position.row, position.col))\n \"\n [highlightResize]=\"\n resizePreviewMap().has(createCellId(position.row, position.col))\n \"\n [editMode]=\"true\"\n (dragEnter)=\"onDragEnter($event)\"\n (dragExit)=\"onDragExit()\"\n (dragOver)=\"onDragOver($event)\"\n (dragDrop)=\"onDragDrop($event)\"\n ></lib-drop-zone>\n }\n </div>\n\n <!-- Top grid with interactive cells -->\n <div class=\"grid\" id=\"top-grid\">\n @for (cell of cells(); track cell.widgetId) {\n <lib-cell\n class=\"grid-cell\"\n [widgetId]=\"cell.widgetId\"\n [cellId]=\"cell.cellId\"\n [isEditMode]=\"true\"\n [draggable]=\"true\"\n [row]=\"cell.row\"\n [column]=\"cell.col\"\n [rowSpan]=\"cell.rowSpan\"\n [colSpan]=\"cell.colSpan\"\n [flat]=\"cell.flat\"\n [widgetFactory]=\"cell.widgetFactory\"\n [widgetState]=\"cell.widgetState\"\n (dragStart)=\"onCellDragStart($event)\"\n (dragEnd)=\"dragEnd()\"\n (delete)=\"onCellDelete($event)\"\n (settings)=\"onCellSettings($event)\"\n (resizeStart)=\"onCellResizeStart($event)\"\n (resizeMove)=\"onCellResizeMove($event)\"\n (resizeEnd)=\"onCellResizeEnd($event)\"\n >\n </lib-cell>\n }\n </div>\n</div>\n\n<!-- Context menu -->\n<lib-cell-context-menu></lib-cell-context-menu>\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}:host .grid{background-image:linear-gradient(to right,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px);background-size:var(--tile-size) var(--tile-size),var(--tile-size) var(--tile-size),100% 1px;background-position:var(--tile-offset) var(--tile-offset),var(--tile-offset) var(--tile-offset),bottom;background-repeat:repeat,repeat,no-repeat}.grid-container{position:relative;width:100%;height:100%}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;align-items:stretch;justify-items:stretch;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}#bottom-grid{z-index:1}#top-grid{z-index:2;pointer-events:none}.grid-cell{pointer-events:auto}.grid-cell.is-dragging{pointer-events:none;opacity:.5}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["widgetId", "cellId", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: DropZoneComponent, selector: "lib-drop-zone", inputs: ["row", "col", "index", "highlight", "highlightInvalid", "highlightResize", "editMode"], outputs: ["dragEnter", "dragExit", "dragOver", "dragDrop"] }, { kind: "component", type: CellContextMenuComponent, selector: "lib-cell-context-menu" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2589
+ ], viewQueries: [{ propertyName: "bottomGridRef", first: true, predicate: ["bottomGrid"], descendants: true, isSignal: true }, { propertyName: "dropZones", predicate: DropZoneComponent, descendants: true, isSignal: true }, { propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- dashboard-editor.component.html -->\r\n<div class=\"grid-container\">\r\n <!-- Bottom grid with drop zones -->\r\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\r\n @for (position of dropzonePositions(); track position.id) {\r\n <lib-drop-zone\r\n class=\"drop-zone\"\r\n [row]=\"position.row\"\r\n [col]=\"position.col\"\r\n [index]=\"position.index\"\r\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\r\n [highlightInvalid]=\"\r\n invalidHighlightMap().has(createCellId(position.row, position.col))\r\n \"\r\n [highlightResize]=\"\r\n resizePreviewMap().has(createCellId(position.row, position.col))\r\n \"\r\n [editMode]=\"true\"\r\n (dragEnter)=\"onDragEnter($event)\"\r\n (dragExit)=\"onDragExit()\"\r\n (dragOver)=\"onDragOver($event)\"\r\n (dragDrop)=\"onDragDrop($event)\"\r\n ></lib-drop-zone>\r\n }\r\n </div>\r\n\r\n <!-- Top grid with interactive cells -->\r\n <div class=\"grid\" id=\"top-grid\">\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"true\"\r\n [draggable]=\"true\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n (dragStart)=\"onCellDragStart($event)\"\r\n (dragEnd)=\"dragEnd()\"\r\n (delete)=\"onCellDelete($event)\"\r\n (settings)=\"onCellSettings($event)\"\r\n (resizeStart)=\"onCellResizeStart($event)\"\r\n (resizeMove)=\"onCellResizeMove($event)\"\r\n (resizeEnd)=\"onCellResizeEnd($event)\"\r\n >\r\n </lib-cell>\r\n }\r\n </div>\r\n</div>\r\n\r\n<!-- Context menus -->\r\n<lib-cell-context-menu></lib-cell-context-menu>\r\n<lib-empty-cell-context-menu></lib-empty-cell-context-menu>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}:host .grid{background-image:linear-gradient(to right,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px);background-size:var(--tile-size) var(--tile-size),var(--tile-size) var(--tile-size),100% 1px;background-position:var(--tile-offset) var(--tile-offset),var(--tile-offset) var(--tile-offset),bottom;background-repeat:repeat,repeat,no-repeat}.grid-container{position:relative;width:100%;height:100%}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;align-items:stretch;justify-items:stretch;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}#bottom-grid{z-index:1}#top-grid{z-index:2;pointer-events:none}.grid-cell{pointer-events:auto}.grid-cell.is-dragging{pointer-events:none;opacity:.5}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["widgetId", "cellId", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: DropZoneComponent, selector: "lib-drop-zone", inputs: ["row", "col", "index", "highlight", "highlightInvalid", "highlightResize", "editMode"], outputs: ["dragEnter", "dragExit", "dragOver", "dragDrop"] }, { kind: "component", type: CellContextMenuComponent, selector: "lib-cell-context-menu" }, { kind: "component", type: EmptyCellContextMenuComponent, selector: "lib-empty-cell-context-menu" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1768
2590
  }
1769
2591
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardEditorComponent, decorators: [{
1770
2592
  type: Component,
@@ -1773,6 +2595,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
1773
2595
  CommonModule,
1774
2596
  DropZoneComponent,
1775
2597
  CellContextMenuComponent,
2598
+ EmptyCellContextMenuComponent,
1776
2599
  ], providers: [
1777
2600
  CellContextMenuService,
1778
2601
  ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
@@ -1781,7 +2604,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
1781
2604
  '[style.--gutter-size]': 'gutterSize()',
1782
2605
  '[style.--gutters]': 'gutters()',
1783
2606
  '[class.is-edit-mode]': 'true', // Always in edit mode
1784
- }, template: "<!-- dashboard-editor.component.html -->\n<div class=\"grid-container\">\n <!-- Bottom grid with drop zones -->\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\n @for (position of dropzonePositions(); track position.id) {\n <lib-drop-zone\n class=\"drop-zone\"\n [row]=\"position.row\"\n [col]=\"position.col\"\n [index]=\"position.index\"\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\n [highlightInvalid]=\"\n invalidHighlightMap().has(createCellId(position.row, position.col))\n \"\n [highlightResize]=\"\n resizePreviewMap().has(createCellId(position.row, position.col))\n \"\n [editMode]=\"true\"\n (dragEnter)=\"onDragEnter($event)\"\n (dragExit)=\"onDragExit()\"\n (dragOver)=\"onDragOver($event)\"\n (dragDrop)=\"onDragDrop($event)\"\n ></lib-drop-zone>\n }\n </div>\n\n <!-- Top grid with interactive cells -->\n <div class=\"grid\" id=\"top-grid\">\n @for (cell of cells(); track cell.widgetId) {\n <lib-cell\n class=\"grid-cell\"\n [widgetId]=\"cell.widgetId\"\n [cellId]=\"cell.cellId\"\n [isEditMode]=\"true\"\n [draggable]=\"true\"\n [row]=\"cell.row\"\n [column]=\"cell.col\"\n [rowSpan]=\"cell.rowSpan\"\n [colSpan]=\"cell.colSpan\"\n [flat]=\"cell.flat\"\n [widgetFactory]=\"cell.widgetFactory\"\n [widgetState]=\"cell.widgetState\"\n (dragStart)=\"onCellDragStart($event)\"\n (dragEnd)=\"dragEnd()\"\n (delete)=\"onCellDelete($event)\"\n (settings)=\"onCellSettings($event)\"\n (resizeStart)=\"onCellResizeStart($event)\"\n (resizeMove)=\"onCellResizeMove($event)\"\n (resizeEnd)=\"onCellResizeEnd($event)\"\n >\n </lib-cell>\n }\n </div>\n</div>\n\n<!-- Context menu -->\n<lib-cell-context-menu></lib-cell-context-menu>\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}:host .grid{background-image:linear-gradient(to right,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px);background-size:var(--tile-size) var(--tile-size),var(--tile-size) var(--tile-size),100% 1px;background-position:var(--tile-offset) var(--tile-offset),var(--tile-offset) var(--tile-offset),bottom;background-repeat:repeat,repeat,no-repeat}.grid-container{position:relative;width:100%;height:100%}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;align-items:stretch;justify-items:stretch;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}#bottom-grid{z-index:1}#top-grid{z-index:2;pointer-events:none}.grid-cell{pointer-events:auto}.grid-cell.is-dragging{pointer-events:none;opacity:.5}\n"] }]
2607
+ }, template: "<!-- dashboard-editor.component.html -->\r\n<div class=\"grid-container\">\r\n <!-- Bottom grid with drop zones -->\r\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\r\n @for (position of dropzonePositions(); track position.id) {\r\n <lib-drop-zone\r\n class=\"drop-zone\"\r\n [row]=\"position.row\"\r\n [col]=\"position.col\"\r\n [index]=\"position.index\"\r\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\r\n [highlightInvalid]=\"\r\n invalidHighlightMap().has(createCellId(position.row, position.col))\r\n \"\r\n [highlightResize]=\"\r\n resizePreviewMap().has(createCellId(position.row, position.col))\r\n \"\r\n [editMode]=\"true\"\r\n (dragEnter)=\"onDragEnter($event)\"\r\n (dragExit)=\"onDragExit()\"\r\n (dragOver)=\"onDragOver($event)\"\r\n (dragDrop)=\"onDragDrop($event)\"\r\n ></lib-drop-zone>\r\n }\r\n </div>\r\n\r\n <!-- Top grid with interactive cells -->\r\n <div class=\"grid\" id=\"top-grid\">\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"true\"\r\n [draggable]=\"true\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n (dragStart)=\"onCellDragStart($event)\"\r\n (dragEnd)=\"dragEnd()\"\r\n (delete)=\"onCellDelete($event)\"\r\n (settings)=\"onCellSettings($event)\"\r\n (resizeStart)=\"onCellResizeStart($event)\"\r\n (resizeMove)=\"onCellResizeMove($event)\"\r\n (resizeEnd)=\"onCellResizeEnd($event)\"\r\n >\r\n </lib-cell>\r\n }\r\n </div>\r\n</div>\r\n\r\n<!-- Context menus -->\r\n<lib-cell-context-menu></lib-cell-context-menu>\r\n<lib-empty-cell-context-menu></lib-empty-cell-context-menu>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}:host .grid{background-image:linear-gradient(to right,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px);background-size:var(--tile-size) var(--tile-size),var(--tile-size) var(--tile-size),100% 1px;background-position:var(--tile-offset) var(--tile-offset),var(--tile-offset) var(--tile-offset),bottom;background-repeat:repeat,repeat,no-repeat}.grid-container{position:relative;width:100%;height:100%}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;align-items:stretch;justify-items:stretch;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}#bottom-grid{z-index:1}#top-grid{z-index:2;pointer-events:none}.grid-cell{pointer-events:auto}.grid-cell.is-dragging{pointer-events:none;opacity:.5}\n"] }]
1785
2608
  }], ctorParameters: () => [] });
1786
2609
 
1787
2610
  // dashboard-bridge.service.ts
@@ -2044,6 +2867,7 @@ class DashboardComponent {
2044
2867
  #store = inject(DashboardStore);
2045
2868
  #bridge = inject(DashboardBridgeService);
2046
2869
  #viewport = inject(DashboardViewportService);
2870
+ #emptyCellMenuService = inject(EmptyCellContextMenuService);
2047
2871
  #destroyRef = inject(DestroyRef);
2048
2872
  // Public accessors for template
2049
2873
  store = this.#store;
@@ -2052,6 +2876,9 @@ class DashboardComponent {
2052
2876
  dashboardData = input.required(...(ngDevMode ? [{ debugName: "dashboardData" }] : []));
2053
2877
  editMode = input(false, ...(ngDevMode ? [{ debugName: "editMode" }] : []));
2054
2878
  reservedSpace = input(...(ngDevMode ? [undefined, { debugName: "reservedSpace" }] : []));
2879
+ enableSelection = input(false, ...(ngDevMode ? [{ debugName: "enableSelection" }] : []));
2880
+ // Component outputs
2881
+ selectionComplete = output();
2055
2882
  // Store signals - shared by both child components
2056
2883
  cells = this.#store.cells;
2057
2884
  // ViewChild references for export/import functionality
@@ -2070,7 +2897,7 @@ class DashboardComponent {
2070
2897
  effect(() => {
2071
2898
  const data = this.dashboardData();
2072
2899
  if (data) {
2073
- this.#store.initializeFromDto(data);
2900
+ this.#store.loadDashboard(data);
2074
2901
  // Register with bridge service after dashboard ID is set
2075
2902
  this.#bridge.updateDashboardRegistration(this.#store);
2076
2903
  this.#isInitialized = true;
@@ -2090,6 +2917,16 @@ class DashboardComponent {
2090
2917
  this.#viewport.setReservedSpace(reserved);
2091
2918
  }
2092
2919
  });
2920
+ // Reset last widget selection when exiting edit mode
2921
+ effect(() => {
2922
+ const isEditMode = this.editMode();
2923
+ if (!isEditMode) {
2924
+ // Reset last selection when exiting edit mode
2925
+ untracked(() => {
2926
+ this.#emptyCellMenuService.setLastSelection(null);
2927
+ });
2928
+ }
2929
+ });
2093
2930
  }
2094
2931
  ngOnChanges(changes) {
2095
2932
  // Handle edit mode changes after initialization
@@ -2104,17 +2941,30 @@ class DashboardComponent {
2104
2941
  }
2105
2942
  }
2106
2943
  }
2107
- // Public export/import methods
2108
- exportDashboard() {
2109
- // Delegate to the active child component
2110
- if (this.editMode()) {
2111
- const editor = this.dashboardEditor();
2112
- return editor ? editor.exportDashboard() : this.#store.exportDashboard();
2113
- }
2114
- else {
2115
- const viewer = this.dashboardViewer();
2116
- return viewer ? viewer.exportDashboard() : this.#store.exportDashboard();
2944
+ /**
2945
+ * Get current widget states from all cell components.
2946
+ * Used during dashboard export to get live widget states.
2947
+ */
2948
+ getCurrentWidgetStates() {
2949
+ const stateMap = new Map();
2950
+ // Get cell components from the active child
2951
+ const cells = this.editMode()
2952
+ ? this.dashboardEditor()?.cellComponents()
2953
+ : this.dashboardViewer()?.cellComponents();
2954
+ if (cells) {
2955
+ for (const cell of cells) {
2956
+ const cellId = cell.cellId();
2957
+ const currentState = cell.getCurrentWidgetState();
2958
+ if (currentState !== undefined) {
2959
+ stateMap.set(CellIdUtils.toString(cellId), currentState);
2960
+ }
2961
+ }
2117
2962
  }
2963
+ return stateMap;
2964
+ }
2965
+ exportDashboard(selection, options) {
2966
+ // Export dashboard with live widget states, optionally filtering by selection
2967
+ return this.#store.exportDashboard(() => this.getCurrentWidgetStates(), selection, options);
2118
2968
  }
2119
2969
  loadDashboard(data) {
2120
2970
  this.#store.loadDashboard(data);
@@ -2136,25 +2986,24 @@ class DashboardComponent {
2136
2986
  }
2137
2987
  this.#isPreservingStates = true;
2138
2988
  try {
2139
- let currentWidgetStates = null;
2140
- if (previousEditMode) {
2141
- // Previously in edit mode, collect states from editor
2142
- const editor = this.dashboardEditor();
2143
- if (editor) {
2144
- currentWidgetStates = editor.getCurrentWidgetStates();
2145
- }
2146
- }
2147
- else {
2148
- // Previously in view mode, collect states from viewer
2149
- const viewer = this.dashboardViewer();
2150
- if (viewer) {
2151
- currentWidgetStates = viewer.getCurrentWidgetStates();
2989
+ const stateMap = new Map();
2990
+ // Get cell components from the previously active child
2991
+ const cells = previousEditMode
2992
+ ? this.dashboardEditor()?.cellComponents()
2993
+ : this.dashboardViewer()?.cellComponents();
2994
+ if (cells) {
2995
+ for (const cell of cells) {
2996
+ const cellId = cell.cellId();
2997
+ const currentState = cell.getCurrentWidgetState();
2998
+ if (currentState !== undefined) {
2999
+ stateMap.set(CellIdUtils.toString(cellId), currentState);
3000
+ }
2152
3001
  }
2153
3002
  }
2154
3003
  // Update the store with the live widget states using untracked to avoid triggering effects
2155
- if (currentWidgetStates && currentWidgetStates.size > 0) {
3004
+ if (stateMap.size > 0) {
2156
3005
  untracked(() => {
2157
- this.#store.updateAllWidgetStates(currentWidgetStates);
3006
+ this.#store.updateAllWidgetStates(stateMap);
2158
3007
  });
2159
3008
  }
2160
3009
  }
@@ -2163,7 +3012,7 @@ class DashboardComponent {
2163
3012
  }
2164
3013
  }
2165
3014
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2166
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardComponent, isStandalone: true, selector: "ngx-dashboard", inputs: { dashboardData: { classPropertyName: "dashboardData", publicName: "dashboardData", isSignal: true, isRequired: true, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, reservedSpace: { classPropertyName: "reservedSpace", publicName: "reservedSpace", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "store.rows()", "style.--columns": "store.columns()", "style.--gutter-size": "store.gutterSize()", "style.--gutters": "store.columns() + 1", "class.is-edit-mode": "editMode()", "style.max-width.px": "viewport.constraints().maxWidth", "style.max-height.px": "viewport.constraints().maxHeight" } }, providers: [DashboardStore, DashboardViewportService], viewQueries: [{ propertyName: "dashboardEditor", first: true, predicate: DashboardEditorComponent, descendants: true, isSignal: true }, { propertyName: "dashboardViewer", first: true, predicate: DashboardViewerComponent, descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DashboardViewerComponent, selector: "ngx-dashboard-viewer", inputs: ["rows", "columns", "gutterSize"] }, { kind: "component", type: DashboardEditorComponent, selector: "ngx-dashboard-editor", inputs: ["rows", "columns", "gutterSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3015
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardComponent, isStandalone: true, selector: "ngx-dashboard", inputs: { dashboardData: { classPropertyName: "dashboardData", publicName: "dashboardData", isSignal: true, isRequired: true, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, reservedSpace: { classPropertyName: "reservedSpace", publicName: "reservedSpace", isSignal: true, isRequired: false, transformFunction: null }, enableSelection: { classPropertyName: "enableSelection", publicName: "enableSelection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionComplete: "selectionComplete" }, host: { properties: { "style.--rows": "store.rows()", "style.--columns": "store.columns()", "style.--gutter-size": "store.gutterSize()", "style.--gutters": "store.columns() + 1", "class.is-edit-mode": "editMode()", "style.max-width.px": "viewport.constraints().maxWidth", "style.max-height.px": "viewport.constraints().maxHeight" } }, providers: [DashboardStore, DashboardViewportService], viewQueries: [{ propertyName: "dashboardEditor", first: true, predicate: DashboardEditorComponent, descendants: true, isSignal: true }, { propertyName: "dashboardViewer", first: true, predicate: DashboardViewerComponent, descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n [enableSelection]=\"enableSelection()\"\r\n (selectionComplete)=\"selectionComplete.emit($event)\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DashboardViewerComponent, selector: "ngx-dashboard-viewer", inputs: ["rows", "columns", "gutterSize", "enableSelection"], outputs: ["selectionComplete"] }, { kind: "component", type: DashboardEditorComponent, selector: "ngx-dashboard-editor", inputs: ["rows", "columns", "gutterSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2167
3016
  }
2168
3017
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardComponent, decorators: [{
2169
3018
  type: Component,
@@ -2175,7 +3024,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
2175
3024
  '[class.is-edit-mode]': 'editMode()',
2176
3025
  '[style.max-width.px]': 'viewport.constraints().maxWidth',
2177
3026
  '[style.max-height.px]': 'viewport.constraints().maxHeight',
2178
- }, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"] }]
3027
+ }, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n [enableSelection]=\"enableSelection()\"\r\n (selectionComplete)=\"selectionComplete.emit($event)\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"] }]
2179
3028
  }], ctorParameters: () => [] });
2180
3029
 
2181
3030
  // widget-list.component.ts
@@ -2184,6 +3033,8 @@ class WidgetListComponent {
2184
3033
  #sanitizer = inject(DomSanitizer);
2185
3034
  #renderer = inject(Renderer2);
2186
3035
  #bridge = inject(DashboardBridgeService);
3036
+ // Input to track collapsed state for tooltip display
3037
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
2187
3038
  activeWidget = signal(null, ...(ngDevMode ? [{ debugName: "activeWidget" }] : []));
2188
3039
  // Get grid cell dimensions from bridge service (uses first available dashboard)
2189
3040
  gridCellDimensions = this.#bridge.availableDimensions;
@@ -2238,22 +3089,26 @@ class WidgetListComponent {
2238
3089
  }
2239
3090
  return el;
2240
3091
  }
3092
+ getWidgetAriaLabel(widget) {
3093
+ // Using $localize for the template pattern
3094
+ return $localize `:@@ngx.dashboard.widget.list.item.ariaLabel:${widget.name} widget: ${widget.description}`;
3095
+ }
2241
3096
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2242
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: WidgetListComponent, isStandalone: true, selector: "ngx-dashboard-widget-list", ngImport: i0, template: "<!-- widget-list.component.html -->\r\n<div class=\"widget-list\" role=\"list\" aria-label=\"Available widgets\">\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n role=\"listitem\"\r\n [attr.aria-grabbed]=\"activeWidget() === widget.widgetTypeid\"\r\n [attr.aria-label]=\"widget.name + ' widget: ' + widget.description\"\r\n tabindex=\"0\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\" aria-hidden=\"true\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>", styles: [":host{background-color:var(--mat-sys-surface-container, #f5f5f5);container-type:inline-size}.widget-list{display:flex;flex-direction:column;gap:var(--mat-sys-spacing-2, 8px)}@container (max-width: 200px){.widget-list{gap:var(--mat-sys-spacing-1, 4px)}}@container (min-width: 400px){.widget-list{gap:var(--mat-sys-spacing-3, 12px)}}.widget-list-item{display:flex;align-items:start;gap:var(--mat-sys-spacing-3, 12px);background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);padding:var(--mat-sys-spacing-3, 12px);border-radius:var(--mat-sys-corner-small, 4px);cursor:grab;transition:background-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),border-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),box-shadow var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out);box-shadow:var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, .05))}@container (max-width: 200px){.widget-list-item{padding:var(--mat-sys-spacing-2, 8px);gap:var(--mat-sys-spacing-2, 8px)}}@container (min-width: 400px){.widget-list-item{padding:var(--mat-sys-spacing-4, 16px);gap:var(--mat-sys-spacing-4, 16px)}}.widget-list-item .icon{width:clamp(20px,4vw,28px);height:clamp(20px,4vw,28px);flex-shrink:0;color:var(--mat-sys-on-surface-variant, #5f5f5f);transition:color var(--mat-sys-motion-duration-short2, .15s) var(--mat-sys-motion-easing-standard, ease-in-out)}.widget-list-item .icon ::ng-deep svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2;color:var(--mat-sys-on-surface, #1c1c1c);flex:1;min-width:0}.widget-list-item .content strong{color:var(--mat-sys-on-surface, #1c1c1c);font-weight:500;font-size:clamp(.875rem,2.5vw,1rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item .content small{color:var(--mat-sys-on-surface-variant, #5f5f5f);font-size:clamp(.75rem,2vw,.875rem);margin-top:var(--mat-sys-spacing-1, 4px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item:hover{background-color:var(--mat-sys-surface-container-low, #f0f0f0);box-shadow:var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, .1))}.widget-list-item:hover .icon{color:var(--mat-sys-on-surface, #1c1c1c)}.widget-list-item:active{cursor:grabbing;background-color:var(--mat-sys-surface-container, #f5f5f5)}.widget-list-item.active{background-color:var(--mat-sys-primary-container, #e6f2ff);border-color:var(--mat-sys-primary, #1976d2);color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content strong{color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content small{color:var(--mat-sys-on-primary-container, #004a99);opacity:.8}.widget-list-item.active .icon{color:var(--mat-sys-on-primary-container, #004a99)}.drag-ghost{position:absolute;top:0;left:0;z-index:9999;margin:0;pointer-events:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);border-radius:var(--mat-sys-corner-small, 4px);box-shadow:var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, .15));opacity:.8}.drag-ghost .icon{display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #5f5f5f);opacity:.6}.drag-ghost .icon ::ng-deep svg{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3097
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: WidgetListComponent, isStandalone: true, selector: "ngx-dashboard-widget-list", inputs: { collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!-- widget-list.component.html -->\r\n<div\r\n class=\"widget-list\"\r\n role=\"list\"\r\n i18n-aria-label=\"@@ngx.dashboard.widget.list.available\"\r\n aria-label=\"Available widgets\"\r\n>\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n role=\"listitem\"\r\n [attr.aria-grabbed]=\"activeWidget() === widget.widgetTypeid\"\r\n [attr.aria-label]=\"getWidgetAriaLabel(widget)\"\r\n [matTooltip]=\"widget.description\"\r\n [matTooltipDisabled]=\"!collapsed()\"\r\n matTooltipPosition=\"right\"\r\n tabindex=\"0\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\" aria-hidden=\"true\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{background-color:var(--mat-sys-surface-container, #f5f5f5);container-type:inline-size}.widget-list{display:flex;flex-direction:column;gap:var(--mat-sys-spacing-2, 8px)}@container (max-width: 200px){.widget-list{gap:var(--mat-sys-spacing-1, 4px)}}@container (min-width: 400px){.widget-list{gap:var(--mat-sys-spacing-3, 12px)}}.widget-list-item{display:flex;align-items:start;gap:var(--mat-sys-spacing-3, 12px);background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);padding:var(--mat-sys-spacing-3, 12px);border-radius:var(--mat-sys-corner-small, 4px);cursor:grab;transition:background-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),border-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),box-shadow var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out);box-shadow:var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, .05))}@container (max-width: 200px){.widget-list-item{padding:var(--mat-sys-spacing-2, 8px);gap:var(--mat-sys-spacing-2, 8px)}}@container (min-width: 400px){.widget-list-item{padding:var(--mat-sys-spacing-4, 16px);gap:var(--mat-sys-spacing-4, 16px)}}.widget-list-item .icon{width:clamp(20px,4vw,28px);height:clamp(20px,4vw,28px);flex-shrink:0;color:var(--mat-sys-on-surface-variant, #5f5f5f);transition:color var(--mat-sys-motion-duration-short2, .15s) var(--mat-sys-motion-easing-standard, ease-in-out)}.widget-list-item .icon ::ng-deep svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2;color:var(--mat-sys-on-surface, #1c1c1c);flex:1;min-width:0}.widget-list-item .content strong{color:var(--mat-sys-on-surface, #1c1c1c);font-weight:500;font-size:clamp(.875rem,2.5vw,1rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item .content small{color:var(--mat-sys-on-surface-variant, #5f5f5f);font-size:clamp(.75rem,2vw,.875rem);margin-top:var(--mat-sys-spacing-1, 4px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item:hover{background-color:var(--mat-sys-surface-container-low, #f0f0f0);box-shadow:var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, .1))}.widget-list-item:hover .icon{color:var(--mat-sys-on-surface, #1c1c1c)}.widget-list-item:active{cursor:grabbing;background-color:var(--mat-sys-surface-container, #f5f5f5)}.widget-list-item.active{background-color:var(--mat-sys-primary-container, #e6f2ff);border-color:var(--mat-sys-primary, #1976d2);color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content strong{color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content small{color:var(--mat-sys-on-primary-container, #004a99);opacity:.8}.widget-list-item.active .icon{color:var(--mat-sys-on-primary-container, #004a99)}.drag-ghost{position:absolute;top:0;left:0;z-index:9999;margin:0;pointer-events:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);border-radius:var(--mat-sys-corner-small, 4px);box-shadow:var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, .15));opacity:.8}.drag-ghost .icon{display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #5f5f5f);opacity:.6}.drag-ghost .icon ::ng-deep svg{display:block}\n"], dependencies: [{ kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2243
3098
  }
2244
3099
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListComponent, decorators: [{
2245
3100
  type: Component,
2246
- args: [{ selector: 'ngx-dashboard-widget-list', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- widget-list.component.html -->\r\n<div class=\"widget-list\" role=\"list\" aria-label=\"Available widgets\">\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n role=\"listitem\"\r\n [attr.aria-grabbed]=\"activeWidget() === widget.widgetTypeid\"\r\n [attr.aria-label]=\"widget.name + ' widget: ' + widget.description\"\r\n tabindex=\"0\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\" aria-hidden=\"true\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>", styles: [":host{background-color:var(--mat-sys-surface-container, #f5f5f5);container-type:inline-size}.widget-list{display:flex;flex-direction:column;gap:var(--mat-sys-spacing-2, 8px)}@container (max-width: 200px){.widget-list{gap:var(--mat-sys-spacing-1, 4px)}}@container (min-width: 400px){.widget-list{gap:var(--mat-sys-spacing-3, 12px)}}.widget-list-item{display:flex;align-items:start;gap:var(--mat-sys-spacing-3, 12px);background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);padding:var(--mat-sys-spacing-3, 12px);border-radius:var(--mat-sys-corner-small, 4px);cursor:grab;transition:background-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),border-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),box-shadow var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out);box-shadow:var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, .05))}@container (max-width: 200px){.widget-list-item{padding:var(--mat-sys-spacing-2, 8px);gap:var(--mat-sys-spacing-2, 8px)}}@container (min-width: 400px){.widget-list-item{padding:var(--mat-sys-spacing-4, 16px);gap:var(--mat-sys-spacing-4, 16px)}}.widget-list-item .icon{width:clamp(20px,4vw,28px);height:clamp(20px,4vw,28px);flex-shrink:0;color:var(--mat-sys-on-surface-variant, #5f5f5f);transition:color var(--mat-sys-motion-duration-short2, .15s) var(--mat-sys-motion-easing-standard, ease-in-out)}.widget-list-item .icon ::ng-deep svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2;color:var(--mat-sys-on-surface, #1c1c1c);flex:1;min-width:0}.widget-list-item .content strong{color:var(--mat-sys-on-surface, #1c1c1c);font-weight:500;font-size:clamp(.875rem,2.5vw,1rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item .content small{color:var(--mat-sys-on-surface-variant, #5f5f5f);font-size:clamp(.75rem,2vw,.875rem);margin-top:var(--mat-sys-spacing-1, 4px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item:hover{background-color:var(--mat-sys-surface-container-low, #f0f0f0);box-shadow:var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, .1))}.widget-list-item:hover .icon{color:var(--mat-sys-on-surface, #1c1c1c)}.widget-list-item:active{cursor:grabbing;background-color:var(--mat-sys-surface-container, #f5f5f5)}.widget-list-item.active{background-color:var(--mat-sys-primary-container, #e6f2ff);border-color:var(--mat-sys-primary, #1976d2);color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content strong{color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content small{color:var(--mat-sys-on-primary-container, #004a99);opacity:.8}.widget-list-item.active .icon{color:var(--mat-sys-on-primary-container, #004a99)}.drag-ghost{position:absolute;top:0;left:0;z-index:9999;margin:0;pointer-events:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);border-radius:var(--mat-sys-corner-small, 4px);box-shadow:var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, .15));opacity:.8}.drag-ghost .icon{display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #5f5f5f);opacity:.6}.drag-ghost .icon ::ng-deep svg{display:block}\n"] }]
3101
+ args: [{ selector: 'ngx-dashboard-widget-list', standalone: true, imports: [MatTooltipModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- widget-list.component.html -->\r\n<div\r\n class=\"widget-list\"\r\n role=\"list\"\r\n i18n-aria-label=\"@@ngx.dashboard.widget.list.available\"\r\n aria-label=\"Available widgets\"\r\n>\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n role=\"listitem\"\r\n [attr.aria-grabbed]=\"activeWidget() === widget.widgetTypeid\"\r\n [attr.aria-label]=\"getWidgetAriaLabel(widget)\"\r\n [matTooltip]=\"widget.description\"\r\n [matTooltipDisabled]=\"!collapsed()\"\r\n matTooltipPosition=\"right\"\r\n tabindex=\"0\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\" aria-hidden=\"true\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{background-color:var(--mat-sys-surface-container, #f5f5f5);container-type:inline-size}.widget-list{display:flex;flex-direction:column;gap:var(--mat-sys-spacing-2, 8px)}@container (max-width: 200px){.widget-list{gap:var(--mat-sys-spacing-1, 4px)}}@container (min-width: 400px){.widget-list{gap:var(--mat-sys-spacing-3, 12px)}}.widget-list-item{display:flex;align-items:start;gap:var(--mat-sys-spacing-3, 12px);background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);padding:var(--mat-sys-spacing-3, 12px);border-radius:var(--mat-sys-corner-small, 4px);cursor:grab;transition:background-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),border-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),box-shadow var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out);box-shadow:var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, .05))}@container (max-width: 200px){.widget-list-item{padding:var(--mat-sys-spacing-2, 8px);gap:var(--mat-sys-spacing-2, 8px)}}@container (min-width: 400px){.widget-list-item{padding:var(--mat-sys-spacing-4, 16px);gap:var(--mat-sys-spacing-4, 16px)}}.widget-list-item .icon{width:clamp(20px,4vw,28px);height:clamp(20px,4vw,28px);flex-shrink:0;color:var(--mat-sys-on-surface-variant, #5f5f5f);transition:color var(--mat-sys-motion-duration-short2, .15s) var(--mat-sys-motion-easing-standard, ease-in-out)}.widget-list-item .icon ::ng-deep svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2;color:var(--mat-sys-on-surface, #1c1c1c);flex:1;min-width:0}.widget-list-item .content strong{color:var(--mat-sys-on-surface, #1c1c1c);font-weight:500;font-size:clamp(.875rem,2.5vw,1rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item .content small{color:var(--mat-sys-on-surface-variant, #5f5f5f);font-size:clamp(.75rem,2vw,.875rem);margin-top:var(--mat-sys-spacing-1, 4px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item:hover{background-color:var(--mat-sys-surface-container-low, #f0f0f0);box-shadow:var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, .1))}.widget-list-item:hover .icon{color:var(--mat-sys-on-surface, #1c1c1c)}.widget-list-item:active{cursor:grabbing;background-color:var(--mat-sys-surface-container, #f5f5f5)}.widget-list-item.active{background-color:var(--mat-sys-primary-container, #e6f2ff);border-color:var(--mat-sys-primary, #1976d2);color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content strong{color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content small{color:var(--mat-sys-on-primary-container, #004a99);opacity:.8}.widget-list-item.active .icon{color:var(--mat-sys-on-primary-container, #004a99)}.drag-ghost{position:absolute;top:0;left:0;z-index:9999;margin:0;pointer-events:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);border-radius:var(--mat-sys-corner-small, 4px);box-shadow:var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, .15));opacity:.8}.drag-ghost .icon{display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #5f5f5f);opacity:.6}.drag-ghost .icon ::ng-deep svg{display:block}\n"] }]
2247
3102
  }] });
2248
3103
 
2249
3104
  /*
2250
3105
  * Public API Surface of ngx-dashboard
2251
3106
  */
2252
- // Main dashboard components
3107
+ // Library version
2253
3108
 
2254
3109
  /**
2255
3110
  * Generated bundle index. Do not edit.
2256
3111
  */
2257
3112
 
2258
- export { CELL_SETTINGS_DIALOG_PROVIDER, CellSettingsDialogProvider, DashboardComponent, DashboardService, DefaultCellSettingsDialogProvider, WidgetListComponent, createDefaultDashboard, createEmptyDashboard };
3113
+ export { CELL_SETTINGS_DIALOG_PROVIDER, CellSettingsDialogProvider, DashboardComponent, DashboardService, DefaultCellSettingsDialogProvider, DefaultEmptyCellContextProvider, EMPTY_CELL_CONTEXT_PROVIDER, EmptyCellContextProvider, NGX_DASHBOARD_VERSION, WidgetListComponent, WidgetListContextMenuProvider, createDefaultDashboard, createEmptyDashboard };
2259
3114
  //# sourceMappingURL=dragonworks-ngx-dashboard.mjs.map