@dragonworks/ngx-dashboard 20.2.0 → 20.3.2

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.2';
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,6 +219,8 @@ 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',
@@ -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) => ({
@@ -973,7 +1154,7 @@ class CellSettingsDialogComponent {
973
1154
  Apply
974
1155
  </button>
975
1156
  </mat-dialog-actions>
976
- `, 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"] }] });
977
1158
  }
978
1159
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellSettingsDialogComponent, decorators: [{
979
1160
  type: Component,
@@ -1253,18 +1434,24 @@ class CellComponent {
1253
1434
  action: () => this.onEdit(),
1254
1435
  disabled: !this.canEdit(),
1255
1436
  },
1256
- {
1257
- label: $localize `:@@ngx.dashboard.cell.menu.settings:Settings`,
1258
- icon: 'settings',
1259
- action: () => this.onSettings(),
1260
- },
1261
- { divider: true },
1262
- {
1263
- label: $localize `:@@ngx.dashboard.cell.menu.delete:Delete`,
1264
- icon: 'delete',
1265
- action: () => this.onDelete(),
1266
- },
1267
1437
  ];
1438
+ // Add shared state entry if widget implements the method
1439
+ if (this.canEditSharedState()) {
1440
+ items.push({
1441
+ label: $localize `:@@ngx.dashboard.cell.menu.editShared:Edit Shared State`,
1442
+ icon: 'edit_document',
1443
+ action: () => this.onEditSharedState(),
1444
+ });
1445
+ }
1446
+ items.push({
1447
+ label: $localize `:@@ngx.dashboard.cell.menu.settings:Settings`,
1448
+ icon: 'settings',
1449
+ action: () => this.onSettings(),
1450
+ }, { divider: true }, {
1451
+ label: $localize `:@@ngx.dashboard.cell.menu.delete:Delete`,
1452
+ icon: 'delete',
1453
+ action: () => this.onDelete(),
1454
+ });
1268
1455
  // Position menu at exact mouse coordinates
1269
1456
  this.#contextMenuService.show(event.clientX, event.clientY, items);
1270
1457
  }
@@ -1274,6 +1461,9 @@ class CellComponent {
1274
1461
  }
1275
1462
  return false;
1276
1463
  }
1464
+ canEditSharedState() {
1465
+ return !!this.#widgetRef?.instance?.dashboardEditSharedState;
1466
+ }
1277
1467
  onEdit() {
1278
1468
  this.edit.emit(this.widgetId());
1279
1469
  // Call the widget's edit dialog method if it exists
@@ -1281,6 +1471,11 @@ class CellComponent {
1281
1471
  this.#widgetRef.instance.dashboardEditState();
1282
1472
  }
1283
1473
  }
1474
+ onEditSharedState() {
1475
+ if (this.#widgetRef?.instance?.dashboardEditSharedState) {
1476
+ this.#widgetRef.instance.dashboardEditSharedState();
1477
+ }
1478
+ }
1284
1479
  onDelete() {
1285
1480
  this.delete.emit(this.widgetId());
1286
1481
  }
@@ -1392,13 +1587,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
1392
1587
 
1393
1588
  class DashboardViewerComponent {
1394
1589
  #store = inject(DashboardStore);
1590
+ #renderer = inject(Renderer2);
1591
+ #destroyRef = inject(DestroyRef);
1395
1592
  cellComponents = viewChildren(CellComponent, ...(ngDevMode ? [{ debugName: "cellComponents" }] : []));
1593
+ gridElement = viewChild('gridElement', ...(ngDevMode ? [{ debugName: "gridElement" }] : []));
1396
1594
  rows = input.required(...(ngDevMode ? [{ debugName: "rows" }] : []));
1397
1595
  columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : []));
1398
1596
  gutterSize = input('1em', ...(ngDevMode ? [{ debugName: "gutterSize" }] : []));
1399
1597
  gutters = computed(() => this.columns() + 1, ...(ngDevMode ? [{ debugName: "gutters" }] : []));
1598
+ // Selection feature
1599
+ enableSelection = input(false, ...(ngDevMode ? [{ debugName: "enableSelection" }] : []));
1600
+ selectionComplete = output();
1400
1601
  // store signals - read-only
1401
1602
  cells = this.#store.cells;
1603
+ // Selection state
1604
+ isSelecting = signal(false, ...(ngDevMode ? [{ debugName: "isSelecting" }] : []));
1605
+ selectionStart = signal(null, ...(ngDevMode ? [{ debugName: "selectionStart" }] : []));
1606
+ selectionCurrent = signal(null, ...(ngDevMode ? [{ debugName: "selectionCurrent" }] : []));
1607
+ // Computed selection bounds (normalized)
1608
+ selectionBounds = computed(() => {
1609
+ const start = this.selectionStart();
1610
+ const current = this.selectionCurrent();
1611
+ if (!start || !current)
1612
+ return null;
1613
+ return {
1614
+ startRow: Math.min(start.row, current.row),
1615
+ endRow: Math.max(start.row, current.row),
1616
+ startCol: Math.min(start.col, current.col),
1617
+ endCol: Math.max(start.col, current.col),
1618
+ };
1619
+ }, ...(ngDevMode ? [{ debugName: "selectionBounds" }] : []));
1620
+ // Generate array for template iteration
1621
+ rowNumbers = computed(() => Array.from({ length: this.rows() }, (_, i) => i + 1), ...(ngDevMode ? [{ debugName: "rowNumbers" }] : []));
1622
+ colNumbers = computed(() => Array.from({ length: this.columns() }, (_, i) => i + 1), ...(ngDevMode ? [{ debugName: "colNumbers" }] : []));
1623
+ // Document-level event listeners (cleanup needed)
1624
+ #mouseMoveListener;
1625
+ #mouseUpListener;
1402
1626
  constructor() {
1403
1627
  // Sync grid configuration with store when inputs change
1404
1628
  effect(() => {
@@ -1408,32 +1632,101 @@ class DashboardViewerComponent {
1408
1632
  gutterSize: this.gutterSize(),
1409
1633
  });
1410
1634
  });
1635
+ // Clear selection when selection mode is disabled
1636
+ effect(() => {
1637
+ if (!this.enableSelection()) {
1638
+ this.selectionStart.set(null);
1639
+ this.selectionCurrent.set(null);
1640
+ this.isSelecting.set(false);
1641
+ }
1642
+ });
1411
1643
  }
1644
+ // Selection methods
1412
1645
  /**
1413
- * Get current widget states from all cell components.
1414
- * Used during dashboard export to get live widget states.
1646
+ * Check if a cell is within the current selection bounds
1415
1647
  */
1416
- getCurrentWidgetStates() {
1417
- const stateMap = new Map();
1418
- const cells = this.cellComponents();
1419
- for (const cell of cells) {
1420
- const cellId = cell.cellId();
1421
- const currentState = cell.getCurrentWidgetState();
1422
- if (currentState !== undefined) {
1423
- stateMap.set(CellIdUtils.toString(cellId), currentState);
1424
- }
1648
+ isCellSelected(row, col) {
1649
+ const bounds = this.selectionBounds();
1650
+ if (!bounds)
1651
+ return false;
1652
+ return (row >= bounds.startRow &&
1653
+ row <= bounds.endRow &&
1654
+ col >= bounds.startCol &&
1655
+ col <= bounds.endCol);
1656
+ }
1657
+ /**
1658
+ * Handle mouse down on ghost cell to start selection
1659
+ */
1660
+ onGhostCellMouseDown(event, row, col) {
1661
+ if (!this.enableSelection())
1662
+ return;
1663
+ if (event.button !== 0)
1664
+ return; // Only left button
1665
+ event.preventDefault();
1666
+ event.stopPropagation();
1667
+ this.isSelecting.set(true);
1668
+ this.selectionStart.set({ row, col });
1669
+ this.selectionCurrent.set({ row, col });
1670
+ // Add document-level listeners for drag
1671
+ this.#mouseMoveListener = this.#renderer.listen('document', 'mousemove', () => this.onDocumentMouseMove());
1672
+ this.#mouseUpListener = this.#renderer.listen('document', 'mouseup', () => this.onDocumentMouseUp());
1673
+ // Register cleanup
1674
+ this.#destroyRef.onDestroy(() => {
1675
+ this.cleanupListeners();
1676
+ });
1677
+ }
1678
+ /**
1679
+ * Handle mouse enter on ghost cell during selection
1680
+ */
1681
+ onGhostCellMouseEnter(row, col) {
1682
+ if (!this.isSelecting())
1683
+ return;
1684
+ this.selectionCurrent.set({ row, col });
1685
+ }
1686
+ /**
1687
+ * Handle document mouse move during selection
1688
+ */
1689
+ onDocumentMouseMove() {
1690
+ if (!this.isSelecting())
1691
+ return;
1692
+ // The actual selection update is handled by onGhostCellMouseEnter
1693
+ // This just ensures we capture the event
1694
+ }
1695
+ /**
1696
+ * Handle document mouse up to complete selection
1697
+ */
1698
+ onDocumentMouseUp() {
1699
+ if (!this.isSelecting())
1700
+ return;
1701
+ this.isSelecting.set(false);
1702
+ // Emit selection event
1703
+ const bounds = this.selectionBounds();
1704
+ if (bounds) {
1705
+ this.selectionComplete.emit({
1706
+ topLeft: { row: bounds.startRow, col: bounds.startCol },
1707
+ bottomRight: { row: bounds.endRow, col: bounds.endCol },
1708
+ });
1425
1709
  }
1426
- return stateMap;
1710
+ // Clean up listeners
1711
+ this.cleanupListeners();
1712
+ // Don't clear selection - let the parent control when to clear
1713
+ // Selection remains visible until enableSelection becomes false
1427
1714
  }
1428
1715
  /**
1429
- * Export dashboard with live widget states from current component instances.
1430
- * This ensures the most up-to-date widget states are captured.
1716
+ * Clean up document-level event listeners
1431
1717
  */
1432
- exportDashboard() {
1433
- return this.#store.exportDashboard(() => this.getCurrentWidgetStates());
1718
+ cleanupListeners() {
1719
+ if (this.#mouseMoveListener) {
1720
+ this.#mouseMoveListener();
1721
+ this.#mouseMoveListener = undefined;
1722
+ }
1723
+ if (this.#mouseUpListener) {
1724
+ this.#mouseUpListener();
1725
+ this.#mouseUpListener = undefined;
1726
+ }
1434
1727
  }
1435
1728
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1436
- 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 });
1729
+ 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 });
1437
1730
  }
1438
1731
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewerComponent, decorators: [{
1439
1732
  type: Component,
@@ -1442,12 +1735,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
1442
1735
  '[style.--columns]': 'columns()',
1443
1736
  '[style.--gutter-size]': 'gutterSize()',
1444
1737
  '[style.--gutters]': 'gutters()',
1445
- }, 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"] }]
1738
+ }, 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"] }]
1446
1739
  }], ctorParameters: () => [] });
1447
1740
 
1448
1741
  class CellContextMenuComponent {
1449
1742
  menuTrigger = viewChild.required('menuTrigger', { read: MatMenuTrigger });
1450
1743
  menuService = inject(CellContextMenuService);
1744
+ #renderer = inject(Renderer2);
1745
+ #destroyRef = inject(DestroyRef);
1451
1746
  menuPosition = computed(() => {
1452
1747
  const menu = this.menuService.activeMenu();
1453
1748
  return menu ? { left: `${menu.x}px`, top: `${menu.y}px` } : { left: '0px', top: '0px' };
@@ -1457,6 +1752,23 @@ class CellContextMenuComponent {
1457
1752
  return menu?.items || [];
1458
1753
  }, ...(ngDevMode ? [{ debugName: "menuItems" }] : []));
1459
1754
  constructor() {
1755
+ // Material Menu has a backdrop that blocks events from reaching other elements.
1756
+ // When any right-click occurs while menu is open, we need to:
1757
+ // 1. Close the current menu
1758
+ // 2. Prevent the browser's default context menu
1759
+ //
1760
+ // Users will need to right-click again to open empty cell menus.
1761
+ // This follows standard UX patterns used by most applications.
1762
+ const unlisten = this.#renderer.listen('document', 'contextmenu', (event) => {
1763
+ if (this.menuService.activeMenu()) {
1764
+ // Prevent browser's default context menu when closing widget menu
1765
+ event.preventDefault();
1766
+ this.menuService.hide();
1767
+ }
1768
+ });
1769
+ this.#destroyRef.onDestroy(() => {
1770
+ unlisten();
1771
+ });
1460
1772
  effect(() => {
1461
1773
  const menu = this.menuService.activeMenu();
1462
1774
  if (menu) {
@@ -1552,7 +1864,7 @@ class CellContextMenuComponent {
1552
1864
  }
1553
1865
  }
1554
1866
  </mat-menu>
1555
- `, 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 });
1867
+ `, 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 });
1556
1868
  }
1557
1869
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellContextMenuComponent, decorators: [{
1558
1870
  type: Component,
@@ -1611,6 +1923,232 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
1611
1923
  `, styles: [":host{display:contents}\n"] }]
1612
1924
  }], ctorParameters: () => [] });
1613
1925
 
1926
+ /**
1927
+ * Abstract provider for handling context menu events on empty dashboard cells.
1928
+ * Implement this to provide custom behavior when users right-click on unoccupied grid spaces.
1929
+ *
1930
+ * @example
1931
+ * ```typescript
1932
+ * @Injectable()
1933
+ * export class CustomEmptyCellProvider extends EmptyCellContextProvider {
1934
+ * handleEmptyCellContext(event: MouseEvent, context: EmptyCellContext): void {
1935
+ * event.preventDefault();
1936
+ * // Show custom menu, open dialog, etc.
1937
+ * }
1938
+ * }
1939
+ * ```
1940
+ */
1941
+ class EmptyCellContextProvider {
1942
+ }
1943
+
1944
+ /**
1945
+ * Default empty cell context provider that prevents the browser's context menu
1946
+ * and performs no other action.
1947
+ *
1948
+ * This is the default behavior that allows users to right-click on empty dashboard
1949
+ * cells without triggering the browser's default context menu.
1950
+ */
1951
+ class DefaultEmptyCellContextProvider extends EmptyCellContextProvider {
1952
+ /**
1953
+ * Default empty cell context handler.
1954
+ * The browser context menu is already prevented by the component.
1955
+ * No additional action is taken by default.
1956
+ */
1957
+ handleEmptyCellContext() {
1958
+ // Default behavior: do nothing
1959
+ }
1960
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultEmptyCellContextProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1961
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultEmptyCellContextProvider, providedIn: 'root' });
1962
+ }
1963
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultEmptyCellContextProvider, decorators: [{
1964
+ type: Injectable,
1965
+ args: [{
1966
+ providedIn: 'root',
1967
+ }]
1968
+ }] });
1969
+
1970
+ /**
1971
+ * Injection token for the empty cell context provider.
1972
+ * Use this to provide your custom implementation for handling right-clicks on empty dashboard cells.
1973
+ *
1974
+ * @example
1975
+ * ```typescript
1976
+ * // Provide a custom implementation
1977
+ * providers: [
1978
+ * {
1979
+ * provide: EMPTY_CELL_CONTEXT_PROVIDER,
1980
+ * useClass: MyCustomEmptyCellProvider
1981
+ * }
1982
+ * ]
1983
+ * ```
1984
+ */
1985
+ const EMPTY_CELL_CONTEXT_PROVIDER = new InjectionToken('EmptyCellContextProvider', {
1986
+ providedIn: 'root',
1987
+ factory: () => new DefaultEmptyCellContextProvider(),
1988
+ });
1989
+
1990
+ /**
1991
+ * Service for managing empty cell context menu state.
1992
+ * Similar to CellContextMenuService but specifically for empty cells.
1993
+ *
1994
+ * This service is internal to the library and manages the display state
1995
+ * of the context menu that appears when right-clicking on empty dashboard cells.
1996
+ */
1997
+ class EmptyCellContextMenuService {
1998
+ #activeMenu = signal(null, ...(ngDevMode ? [{ debugName: "#activeMenu" }] : []));
1999
+ #lastSelectedWidgetTypeId = signal(null, ...(ngDevMode ? [{ debugName: "#lastSelectedWidgetTypeId" }] : []));
2000
+ activeMenu = this.#activeMenu.asReadonly();
2001
+ lastSelectedWidgetTypeId = this.#lastSelectedWidgetTypeId.asReadonly();
2002
+ /**
2003
+ * Show the context menu at specific coordinates with given items.
2004
+ *
2005
+ * @param x - X coordinate (clientX from mouse event)
2006
+ * @param y - Y coordinate (clientY from mouse event)
2007
+ * @param items - Menu items to display
2008
+ */
2009
+ show(x, y, items) {
2010
+ this.#activeMenu.set({ x, y, items });
2011
+ }
2012
+ /**
2013
+ * Hide the context menu.
2014
+ */
2015
+ hide() {
2016
+ this.#activeMenu.set(null);
2017
+ }
2018
+ /**
2019
+ * Set the last selected widget type ID for quick-repeat functionality.
2020
+ * Pass null to clear the last selection.
2021
+ *
2022
+ * @param widgetTypeId - The widget type identifier, or null to clear
2023
+ */
2024
+ setLastSelection(widgetTypeId) {
2025
+ this.#lastSelectedWidgetTypeId.set(widgetTypeId);
2026
+ }
2027
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2028
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuService, providedIn: 'root' });
2029
+ }
2030
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuService, decorators: [{
2031
+ type: Injectable,
2032
+ args: [{
2033
+ providedIn: 'root',
2034
+ }]
2035
+ }] });
2036
+
2037
+ /**
2038
+ * Context provider that displays a widget list menu when right-clicking on empty cells.
2039
+ *
2040
+ * This provider shows a Material Design context menu with all available widget types.
2041
+ * When a user clicks on a widget in the menu, it's immediately added to the empty cell.
2042
+ *
2043
+ * @example
2044
+ * ```typescript
2045
+ * // In your component or app config
2046
+ * providers: [
2047
+ * {
2048
+ * provide: EMPTY_CELL_CONTEXT_PROVIDER,
2049
+ * useClass: WidgetListContextMenuProvider
2050
+ * }
2051
+ * ]
2052
+ * ```
2053
+ *
2054
+ * @public
2055
+ */
2056
+ class WidgetListContextMenuProvider extends EmptyCellContextProvider {
2057
+ #menuService = inject(EmptyCellContextMenuService);
2058
+ #dashboardService = inject(DashboardService);
2059
+ /**
2060
+ * Handle empty cell context menu by showing available widgets.
2061
+ *
2062
+ * @param event - The mouse event from the right-click
2063
+ * @param context - Information about the empty cell and dashboard
2064
+ */
2065
+ handleEmptyCellContext(event, context) {
2066
+ event.preventDefault();
2067
+ // Get available widgets from dashboard service
2068
+ const widgets = this.#dashboardService.widgetTypes();
2069
+ // Create menu items from widgets
2070
+ const items = this.#createMenuItems(widgets, context);
2071
+ // Show the context menu at mouse coordinates
2072
+ this.#menuService.show(event.clientX, event.clientY, items);
2073
+ }
2074
+ /**
2075
+ * Create menu items from available widget types.
2076
+ * Each item includes the widget's icon and display name.
2077
+ * If a widget was previously selected, it appears first as a quick-repeat option.
2078
+ *
2079
+ * @param widgets - Array of widget component classes
2080
+ * @param context - The empty cell context with createWidget callback
2081
+ * @returns Array of menu items ready for display
2082
+ */
2083
+ #createMenuItems(widgets, context) {
2084
+ if (widgets.length === 0) {
2085
+ // Show a message if no widgets are registered
2086
+ return [
2087
+ {
2088
+ label: $localize `:@@ngx.dashboard.emptyCellMenu.noWidgets:No widgets available`,
2089
+ disabled: true,
2090
+ action: () => {
2091
+ // No action needed
2092
+ },
2093
+ },
2094
+ ];
2095
+ }
2096
+ // Build standard menu items with widgetTypeId
2097
+ const allItems = widgets.map((widget) => ({
2098
+ label: widget.metadata.name,
2099
+ svgIcon: widget.metadata.svgIcon,
2100
+ widgetTypeId: widget.metadata.widgetTypeid,
2101
+ action: () => this.#createWidget(widget.metadata.widgetTypeid, context),
2102
+ }));
2103
+ // Check if there's a last selected widget to show as quick-repeat
2104
+ const lastSelectedTypeId = this.#menuService.lastSelectedWidgetTypeId();
2105
+ if (!lastSelectedTypeId) {
2106
+ return allItems;
2107
+ }
2108
+ // Find the last selected widget in the list
2109
+ const lastSelectedWidget = widgets.find((w) => w.metadata.widgetTypeid === lastSelectedTypeId);
2110
+ if (!lastSelectedWidget) {
2111
+ // Last selected widget no longer available, return normal list
2112
+ return allItems;
2113
+ }
2114
+ // Create quick-repeat item (duplicate of the last selected widget)
2115
+ const quickRepeatItem = {
2116
+ label: lastSelectedWidget.metadata.name,
2117
+ svgIcon: lastSelectedWidget.metadata.svgIcon,
2118
+ widgetTypeId: lastSelectedWidget.metadata.widgetTypeid,
2119
+ action: () => this.#createWidget(lastSelectedWidget.metadata.widgetTypeid, context),
2120
+ };
2121
+ // Create divider
2122
+ const divider = { divider: true };
2123
+ // Return special structure: [quick-repeat, divider, full list]
2124
+ return [quickRepeatItem, divider, ...allItems];
2125
+ }
2126
+ /**
2127
+ * Create a widget at the empty cell position.
2128
+ * Uses the createWidget callback provided in the context.
2129
+ *
2130
+ * @param widgetTypeid - The widget type identifier to create
2131
+ * @param context - The empty cell context with createWidget callback
2132
+ */
2133
+ #createWidget(widgetTypeid, context) {
2134
+ if (context.createWidget) {
2135
+ const success = context.createWidget(widgetTypeid);
2136
+ if (!success) {
2137
+ console.error(`Failed to create widget '${widgetTypeid}' at (${context.row}, ${context.col})`);
2138
+ }
2139
+ }
2140
+ else {
2141
+ console.warn('createWidget callback not available in EmptyCellContext. ' +
2142
+ 'Ensure you are using a compatible version of the dashboard component.');
2143
+ }
2144
+ }
2145
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListContextMenuProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
2146
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListContextMenuProvider });
2147
+ }
2148
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListContextMenuProvider, decorators: [{
2149
+ type: Injectable
2150
+ }] });
2151
+
1614
2152
  // drop-zone.component.ts
1615
2153
  class DropZoneComponent {
1616
2154
  // Required inputs
@@ -1645,6 +2183,10 @@ class DropZoneComponent {
1645
2183
  }, ...(ngDevMode ? [{ debugName: "dropEffect" }] : []));
1646
2184
  #store = inject(DashboardStore);
1647
2185
  #elementRef = inject(ElementRef);
2186
+ #dashboardService = inject(DashboardService);
2187
+ #contextProvider = inject(EMPTY_CELL_CONTEXT_PROVIDER, {
2188
+ optional: true,
2189
+ });
1648
2190
  get nativeElement() {
1649
2191
  return this.#elementRef.nativeElement;
1650
2192
  }
@@ -1689,14 +2231,267 @@ class DropZoneComponent {
1689
2231
  });
1690
2232
  }
1691
2233
  }
2234
+ /**
2235
+ * Handle context menu events on empty cells.
2236
+ * Only active in edit mode. Delegates to the context provider if available.
2237
+ */
2238
+ onContextMenu(event) {
2239
+ if (!this.editMode())
2240
+ return;
2241
+ // Prevent default browser menu and stop propagation in edit mode
2242
+ // stopPropagation prevents the event from reaching the document-level
2243
+ // listener in EmptyCellContextMenuComponent which would immediately hide the menu
2244
+ event.preventDefault();
2245
+ event.stopPropagation();
2246
+ if (this.#contextProvider) {
2247
+ this.#contextProvider.handleEmptyCellContext(event, {
2248
+ row: this.row(),
2249
+ col: this.col(),
2250
+ totalRows: this.#store.rows(),
2251
+ totalColumns: this.#store.columns(),
2252
+ gutterSize: this.#store.gutterSize(),
2253
+ createWidget: (widgetTypeid) => {
2254
+ const factory = this.#dashboardService.getFactory(widgetTypeid);
2255
+ this.#store.createWidget(this.row(), this.col(), factory, undefined);
2256
+ return true; // Widget created successfully
2257
+ },
2258
+ });
2259
+ }
2260
+ }
1692
2261
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DropZoneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1693
- 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 });
2262
+ 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 });
1694
2263
  }
1695
2264
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DropZoneComponent, decorators: [{
1696
2265
  type: Component,
1697
- 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"] }]
2266
+ 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"] }]
1698
2267
  }] });
1699
2268
 
2269
+ /**
2270
+ * Context menu component for empty dashboard cells.
2271
+ * Displays a list of available widgets that can be added to the empty cell.
2272
+ *
2273
+ * This component is similar to CellContextMenuComponent but designed specifically
2274
+ * for empty cells and includes support for SVG icons from widget metadata.
2275
+ *
2276
+ * @internal This component is for internal library use only
2277
+ */
2278
+ class EmptyCellContextMenuComponent {
2279
+ menuTrigger = viewChild.required('menuTrigger', { read: MatMenuTrigger });
2280
+ menuService = inject(EmptyCellContextMenuService);
2281
+ #sanitizer = inject(DomSanitizer);
2282
+ #renderer = inject(Renderer2);
2283
+ #destroyRef = inject(DestroyRef);
2284
+ menuPosition = computed(() => {
2285
+ const menu = this.menuService.activeMenu();
2286
+ return menu
2287
+ ? { left: `${menu.x}px`, top: `${menu.y}px` }
2288
+ : { left: '0px', top: '0px' };
2289
+ }, ...(ngDevMode ? [{ debugName: "menuPosition" }] : []));
2290
+ menuItems = computed(() => {
2291
+ const menu = this.menuService.activeMenu();
2292
+ return menu?.items || [];
2293
+ }, ...(ngDevMode ? [{ debugName: "menuItems" }] : []));
2294
+ constructor() {
2295
+ // Material Menu has a backdrop that blocks events from reaching other elements.
2296
+ // When any right-click occurs while menu is open, we need to:
2297
+ // 1. Close the current menu
2298
+ // 2. Prevent the browser's default context menu
2299
+ //
2300
+ // Users will need to right-click again to open empty cell menus.
2301
+ // This follows standard UX patterns used by most applications.
2302
+ const unlisten = this.#renderer.listen('document', 'contextmenu', (event) => {
2303
+ if (this.menuService.activeMenu()) {
2304
+ // Prevent browser's default context menu when closing widget menu
2305
+ event.preventDefault();
2306
+ this.menuService.hide();
2307
+ }
2308
+ });
2309
+ this.#destroyRef.onDestroy(() => {
2310
+ unlisten();
2311
+ });
2312
+ // Show menu when service state changes
2313
+ effect(() => {
2314
+ const menu = this.menuService.activeMenu();
2315
+ if (menu) {
2316
+ // Use queueMicrotask to ensure the view is fully initialized
2317
+ // This fixes the issue where the menu disappears on first right-click
2318
+ queueMicrotask(() => {
2319
+ const trigger = this.menuTrigger();
2320
+ if (trigger) {
2321
+ // Close any existing menu first
2322
+ if (trigger.menuOpen) {
2323
+ trigger.closeMenu();
2324
+ }
2325
+ // Open menu - position is handled by signal
2326
+ trigger.openMenu();
2327
+ }
2328
+ });
2329
+ }
2330
+ else {
2331
+ const trigger = this.menuTrigger();
2332
+ if (trigger) {
2333
+ trigger.closeMenu();
2334
+ }
2335
+ }
2336
+ });
2337
+ // Subscribe to menu closed event once when trigger becomes available
2338
+ // This ensures the service state is synchronized when Material menu closes
2339
+ effect((onCleanup) => {
2340
+ const trigger = this.menuTrigger();
2341
+ if (trigger) {
2342
+ const subscription = trigger.menuClosed.subscribe(() => {
2343
+ if (this.menuService.activeMenu()) {
2344
+ this.menuService.hide();
2345
+ }
2346
+ });
2347
+ // Clean up subscription when effect re-runs or component destroys
2348
+ onCleanup(() => subscription.unsubscribe());
2349
+ }
2350
+ });
2351
+ }
2352
+ executeAction(item) {
2353
+ if (!item.divider && item.action) {
2354
+ // Track widget type if provided
2355
+ if (item.widgetTypeId) {
2356
+ this.menuService.setLastSelection(item.widgetTypeId);
2357
+ }
2358
+ item.action();
2359
+ this.menuService.hide();
2360
+ }
2361
+ }
2362
+ sanitizeSvg(svg) {
2363
+ return this.#sanitizer.bypassSecurityTrustHtml(svg);
2364
+ }
2365
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2366
+ 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: `
2367
+ <!-- Hidden trigger for menu positioned at exact mouse coordinates
2368
+
2369
+ IMPORTANT: Angular Material applies its own positioning logic to menus,
2370
+ which by default offsets the menu from the trigger element to avoid overlap.
2371
+ To achieve precise positioning at mouse coordinates, we use these workarounds:
2372
+
2373
+ 1. The trigger container is 1x1px (not 0x0) because Material needs a physical
2374
+ element to calculate position against. Zero-sized elements cause unpredictable
2375
+ positioning.
2376
+
2377
+ 2. We use opacity:0 instead of visibility:hidden to keep the element in the
2378
+ layout flow while making it invisible.
2379
+
2380
+ 3. The button itself is styled to 1x1px with no padding to serve as a precise
2381
+ anchor point for the menu.
2382
+
2383
+ 4. The mat-menu uses [overlapTrigger]="true" to allow the menu to appear
2384
+ directly at the trigger position rather than offset from it.
2385
+
2386
+ This approach ensures the menu appears at the exact mouse coordinates passed
2387
+ from the empty cell context provider's right-click handler.
2388
+ -->
2389
+ <div
2390
+ style="position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
2391
+ [style]="menuPosition()">
2392
+ <button
2393
+ mat-button
2394
+ #menuTrigger="matMenuTrigger"
2395
+ [matMenuTriggerFor]="contextMenu"
2396
+ style="width: 1px; height: 1px; padding: 0; min-width: 0; line-height: 0;">
2397
+ </button>
2398
+ </div>
2399
+
2400
+ <!-- Context menu with widget list -->
2401
+ <mat-menu
2402
+ #contextMenu="matMenu"
2403
+ [overlapTrigger]="true"
2404
+ class="empty-cell-widget-menu">
2405
+ @for (item of menuItems(); track $index) {
2406
+ @if (item.divider) {
2407
+ <mat-divider></mat-divider>
2408
+ } @else {
2409
+ <button
2410
+ mat-menu-item
2411
+ (click)="executeAction(item)"
2412
+ [disabled]="item.disabled"
2413
+ [attr.aria-label]="item.label">
2414
+ @if (item.svgIcon) {
2415
+ <div class="widget-icon" [innerHTML]="sanitizeSvg(item.svgIcon)"></div>
2416
+ } @else if (item.icon) {
2417
+ <mat-icon>{{ item.icon }}</mat-icon>
2418
+ }
2419
+ {{ item.label }}
2420
+ </button>
2421
+ }
2422
+ }
2423
+ </mat-menu>
2424
+ `, 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 });
2425
+ }
2426
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: EmptyCellContextMenuComponent, decorators: [{
2427
+ type: Component,
2428
+ args: [{ selector: 'lib-empty-cell-context-menu', standalone: true, imports: [
2429
+ MatMenuModule,
2430
+ MatIconModule,
2431
+ MatDividerModule,
2432
+ MatButtonModule,
2433
+ CommonModule,
2434
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2435
+ <!-- Hidden trigger for menu positioned at exact mouse coordinates
2436
+
2437
+ IMPORTANT: Angular Material applies its own positioning logic to menus,
2438
+ which by default offsets the menu from the trigger element to avoid overlap.
2439
+ To achieve precise positioning at mouse coordinates, we use these workarounds:
2440
+
2441
+ 1. The trigger container is 1x1px (not 0x0) because Material needs a physical
2442
+ element to calculate position against. Zero-sized elements cause unpredictable
2443
+ positioning.
2444
+
2445
+ 2. We use opacity:0 instead of visibility:hidden to keep the element in the
2446
+ layout flow while making it invisible.
2447
+
2448
+ 3. The button itself is styled to 1x1px with no padding to serve as a precise
2449
+ anchor point for the menu.
2450
+
2451
+ 4. The mat-menu uses [overlapTrigger]="true" to allow the menu to appear
2452
+ directly at the trigger position rather than offset from it.
2453
+
2454
+ This approach ensures the menu appears at the exact mouse coordinates passed
2455
+ from the empty cell context provider's right-click handler.
2456
+ -->
2457
+ <div
2458
+ style="position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
2459
+ [style]="menuPosition()">
2460
+ <button
2461
+ mat-button
2462
+ #menuTrigger="matMenuTrigger"
2463
+ [matMenuTriggerFor]="contextMenu"
2464
+ style="width: 1px; height: 1px; padding: 0; min-width: 0; line-height: 0;">
2465
+ </button>
2466
+ </div>
2467
+
2468
+ <!-- Context menu with widget list -->
2469
+ <mat-menu
2470
+ #contextMenu="matMenu"
2471
+ [overlapTrigger]="true"
2472
+ class="empty-cell-widget-menu">
2473
+ @for (item of menuItems(); track $index) {
2474
+ @if (item.divider) {
2475
+ <mat-divider></mat-divider>
2476
+ } @else {
2477
+ <button
2478
+ mat-menu-item
2479
+ (click)="executeAction(item)"
2480
+ [disabled]="item.disabled"
2481
+ [attr.aria-label]="item.label">
2482
+ @if (item.svgIcon) {
2483
+ <div class="widget-icon" [innerHTML]="sanitizeSvg(item.svgIcon)"></div>
2484
+ } @else if (item.icon) {
2485
+ <mat-icon>{{ item.icon }}</mat-icon>
2486
+ }
2487
+ {{ item.label }}
2488
+ </button>
2489
+ }
2490
+ }
2491
+ </mat-menu>
2492
+ `, 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"] }]
2493
+ }], ctorParameters: () => [] });
2494
+
1700
2495
  // dashboard-editor.component.ts
1701
2496
  class DashboardEditorComponent {
1702
2497
  bottomGridRef = viewChild.required('bottomGrid');
@@ -1802,33 +2597,10 @@ class DashboardEditorComponent {
1802
2597
  this.#store.handleDrop(event.data, event.target);
1803
2598
  // Note: Store handles all validation and error handling internally
1804
2599
  }
1805
- /**
1806
- * Get current widget states from all cell components.
1807
- * Used during dashboard export to get live widget states.
1808
- */
1809
- getCurrentWidgetStates() {
1810
- const stateMap = new Map();
1811
- const cells = this.cellComponents();
1812
- for (const cell of cells) {
1813
- const cellId = cell.cellId();
1814
- const currentState = cell.getCurrentWidgetState();
1815
- if (currentState !== undefined) {
1816
- stateMap.set(CellIdUtils.toString(cellId), currentState);
1817
- }
1818
- }
1819
- return stateMap;
1820
- }
1821
- /**
1822
- * Export dashboard with live widget states from current component instances.
1823
- * This ensures the most up-to-date widget states are captured.
1824
- */
1825
- exportDashboard() {
1826
- return this.#store.exportDashboard(() => this.getCurrentWidgetStates());
1827
- }
1828
2600
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1829
2601
  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: [
1830
2602
  CellContextMenuService,
1831
- ], 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 });
2603
+ ], 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 });
1832
2604
  }
1833
2605
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardEditorComponent, decorators: [{
1834
2606
  type: Component,
@@ -1837,6 +2609,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
1837
2609
  CommonModule,
1838
2610
  DropZoneComponent,
1839
2611
  CellContextMenuComponent,
2612
+ EmptyCellContextMenuComponent,
1840
2613
  ], providers: [
1841
2614
  CellContextMenuService,
1842
2615
  ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
@@ -1845,7 +2618,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
1845
2618
  '[style.--gutter-size]': 'gutterSize()',
1846
2619
  '[style.--gutters]': 'gutters()',
1847
2620
  '[class.is-edit-mode]': 'true', // Always in edit mode
1848
- }, 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"] }]
2621
+ }, 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"] }]
1849
2622
  }], ctorParameters: () => [] });
1850
2623
 
1851
2624
  // dashboard-bridge.service.ts
@@ -2108,6 +2881,7 @@ class DashboardComponent {
2108
2881
  #store = inject(DashboardStore);
2109
2882
  #bridge = inject(DashboardBridgeService);
2110
2883
  #viewport = inject(DashboardViewportService);
2884
+ #emptyCellMenuService = inject(EmptyCellContextMenuService);
2111
2885
  #destroyRef = inject(DestroyRef);
2112
2886
  // Public accessors for template
2113
2887
  store = this.#store;
@@ -2116,6 +2890,9 @@ class DashboardComponent {
2116
2890
  dashboardData = input.required(...(ngDevMode ? [{ debugName: "dashboardData" }] : []));
2117
2891
  editMode = input(false, ...(ngDevMode ? [{ debugName: "editMode" }] : []));
2118
2892
  reservedSpace = input(...(ngDevMode ? [undefined, { debugName: "reservedSpace" }] : []));
2893
+ enableSelection = input(false, ...(ngDevMode ? [{ debugName: "enableSelection" }] : []));
2894
+ // Component outputs
2895
+ selectionComplete = output();
2119
2896
  // Store signals - shared by both child components
2120
2897
  cells = this.#store.cells;
2121
2898
  // ViewChild references for export/import functionality
@@ -2134,7 +2911,7 @@ class DashboardComponent {
2134
2911
  effect(() => {
2135
2912
  const data = this.dashboardData();
2136
2913
  if (data) {
2137
- this.#store.initializeFromDto(data);
2914
+ this.#store.loadDashboard(data);
2138
2915
  // Register with bridge service after dashboard ID is set
2139
2916
  this.#bridge.updateDashboardRegistration(this.#store);
2140
2917
  this.#isInitialized = true;
@@ -2154,6 +2931,16 @@ class DashboardComponent {
2154
2931
  this.#viewport.setReservedSpace(reserved);
2155
2932
  }
2156
2933
  });
2934
+ // Reset last widget selection when exiting edit mode
2935
+ effect(() => {
2936
+ const isEditMode = this.editMode();
2937
+ if (!isEditMode) {
2938
+ // Reset last selection when exiting edit mode
2939
+ untracked(() => {
2940
+ this.#emptyCellMenuService.setLastSelection(null);
2941
+ });
2942
+ }
2943
+ });
2157
2944
  }
2158
2945
  ngOnChanges(changes) {
2159
2946
  // Handle edit mode changes after initialization
@@ -2168,17 +2955,30 @@ class DashboardComponent {
2168
2955
  }
2169
2956
  }
2170
2957
  }
2171
- // Public export/import methods
2172
- exportDashboard() {
2173
- // Delegate to the active child component
2174
- if (this.editMode()) {
2175
- const editor = this.dashboardEditor();
2176
- return editor ? editor.exportDashboard() : this.#store.exportDashboard();
2177
- }
2178
- else {
2179
- const viewer = this.dashboardViewer();
2180
- return viewer ? viewer.exportDashboard() : this.#store.exportDashboard();
2958
+ /**
2959
+ * Get current widget states from all cell components.
2960
+ * Used during dashboard export to get live widget states.
2961
+ */
2962
+ getCurrentWidgetStates() {
2963
+ const stateMap = new Map();
2964
+ // Get cell components from the active child
2965
+ const cells = this.editMode()
2966
+ ? this.dashboardEditor()?.cellComponents()
2967
+ : this.dashboardViewer()?.cellComponents();
2968
+ if (cells) {
2969
+ for (const cell of cells) {
2970
+ const cellId = cell.cellId();
2971
+ const currentState = cell.getCurrentWidgetState();
2972
+ if (currentState !== undefined) {
2973
+ stateMap.set(CellIdUtils.toString(cellId), currentState);
2974
+ }
2975
+ }
2181
2976
  }
2977
+ return stateMap;
2978
+ }
2979
+ exportDashboard(selection, options) {
2980
+ // Export dashboard with live widget states, optionally filtering by selection
2981
+ return this.#store.exportDashboard(() => this.getCurrentWidgetStates(), selection, options);
2182
2982
  }
2183
2983
  loadDashboard(data) {
2184
2984
  this.#store.loadDashboard(data);
@@ -2200,25 +3000,24 @@ class DashboardComponent {
2200
3000
  }
2201
3001
  this.#isPreservingStates = true;
2202
3002
  try {
2203
- let currentWidgetStates = null;
2204
- if (previousEditMode) {
2205
- // Previously in edit mode, collect states from editor
2206
- const editor = this.dashboardEditor();
2207
- if (editor) {
2208
- currentWidgetStates = editor.getCurrentWidgetStates();
2209
- }
2210
- }
2211
- else {
2212
- // Previously in view mode, collect states from viewer
2213
- const viewer = this.dashboardViewer();
2214
- if (viewer) {
2215
- currentWidgetStates = viewer.getCurrentWidgetStates();
3003
+ const stateMap = new Map();
3004
+ // Get cell components from the previously active child
3005
+ const cells = previousEditMode
3006
+ ? this.dashboardEditor()?.cellComponents()
3007
+ : this.dashboardViewer()?.cellComponents();
3008
+ if (cells) {
3009
+ for (const cell of cells) {
3010
+ const cellId = cell.cellId();
3011
+ const currentState = cell.getCurrentWidgetState();
3012
+ if (currentState !== undefined) {
3013
+ stateMap.set(CellIdUtils.toString(cellId), currentState);
3014
+ }
2216
3015
  }
2217
3016
  }
2218
3017
  // Update the store with the live widget states using untracked to avoid triggering effects
2219
- if (currentWidgetStates && currentWidgetStates.size > 0) {
3018
+ if (stateMap.size > 0) {
2220
3019
  untracked(() => {
2221
- this.#store.updateAllWidgetStates(currentWidgetStates);
3020
+ this.#store.updateAllWidgetStates(stateMap);
2222
3021
  });
2223
3022
  }
2224
3023
  }
@@ -2227,7 +3026,7 @@ class DashboardComponent {
2227
3026
  }
2228
3027
  }
2229
3028
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2230
- 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 });
3029
+ 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 });
2231
3030
  }
2232
3031
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardComponent, decorators: [{
2233
3032
  type: Component,
@@ -2239,7 +3038,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImpor
2239
3038
  '[class.is-edit-mode]': 'editMode()',
2240
3039
  '[style.max-width.px]': 'viewport.constraints().maxWidth',
2241
3040
  '[style.max-height.px]': 'viewport.constraints().maxHeight',
2242
- }, 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"] }]
3041
+ }, 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"] }]
2243
3042
  }], ctorParameters: () => [] });
2244
3043
 
2245
3044
  // widget-list.component.ts
@@ -2248,6 +3047,8 @@ class WidgetListComponent {
2248
3047
  #sanitizer = inject(DomSanitizer);
2249
3048
  #renderer = inject(Renderer2);
2250
3049
  #bridge = inject(DashboardBridgeService);
3050
+ // Input to track collapsed state for tooltip display
3051
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
2251
3052
  activeWidget = signal(null, ...(ngDevMode ? [{ debugName: "activeWidget" }] : []));
2252
3053
  // Get grid cell dimensions from bridge service (uses first available dashboard)
2253
3054
  gridCellDimensions = this.#bridge.availableDimensions;
@@ -2307,21 +3108,21 @@ class WidgetListComponent {
2307
3108
  return $localize `:@@ngx.dashboard.widget.list.item.ariaLabel:${widget.name} widget: ${widget.description}`;
2308
3109
  }
2309
3110
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2310
- 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\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 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"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3111
+ 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 });
2311
3112
  }
2312
3113
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListComponent, decorators: [{
2313
3114
  type: Component,
2314
- args: [{ selector: 'ngx-dashboard-widget-list', standalone: true, imports: [], 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 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"] }]
3115
+ 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"] }]
2315
3116
  }] });
2316
3117
 
2317
3118
  /*
2318
3119
  * Public API Surface of ngx-dashboard
2319
3120
  */
2320
- // Main dashboard components
3121
+ // Library version
2321
3122
 
2322
3123
  /**
2323
3124
  * Generated bundle index. Do not edit.
2324
3125
  */
2325
3126
 
2326
- export { CELL_SETTINGS_DIALOG_PROVIDER, CellSettingsDialogProvider, DashboardComponent, DashboardService, DefaultCellSettingsDialogProvider, WidgetListComponent, createDefaultDashboard, createEmptyDashboard };
3127
+ export { CELL_SETTINGS_DIALOG_PROVIDER, CellSettingsDialogProvider, DashboardComponent, DashboardService, DefaultCellSettingsDialogProvider, DefaultEmptyCellContextProvider, EMPTY_CELL_CONTEXT_PROVIDER, EmptyCellContextProvider, NGX_DASHBOARD_VERSION, WidgetListComponent, WidgetListContextMenuProvider, createDefaultDashboard, createEmptyDashboard };
2327
3128
  //# sourceMappingURL=dragonworks-ngx-dashboard.mjs.map