@dragonworks/ngx-dashboard 22.0.0 → 22.1.0

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.
@@ -23,7 +23,7 @@ import { isPlatformBrowser } from '@angular/common';
23
23
 
24
24
  // Auto-generated by scripts/generate-versions.js
25
25
  // Do not edit manually
26
- const NGX_DASHBOARD_VERSION = '22.0.0';
26
+ const NGX_DASHBOARD_VERSION = '22.1.0';
27
27
 
28
28
  /**
29
29
  * Maximum number of columns supported by the grid.
@@ -559,6 +559,38 @@ function applySelectionFilter(selection, allCells, options = {}) {
559
559
  };
560
560
  }
561
561
 
562
+ /** Floor a requested track count to a positive integer; non-finite -> 1. */
563
+ function sanitizeRequest(value) {
564
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
565
+ }
566
+ /**
567
+ * Clamp a requested grid size to the smallest size that still contains every
568
+ * widget's full footprint (the content floor). Pure: computes the result but
569
+ * does not mutate any state, so it is shared by both the committed resize
570
+ * (`setGridSize`) and the in-progress drag preview.
571
+ *
572
+ * Values below 1 are treated as 1; fractional values are floored.
573
+ */
574
+ function clampGridSize(requestedRows, requestedColumns, cells) {
575
+ // The bounding box's max edges are span-aware (a widget at col 14 spanning
576
+ // 3 columns needs at least 16 columns). null = empty dashboard.
577
+ const bounds = calculateMinimalBoundingBox(cells);
578
+ const minRows = bounds?.maxRow ?? 1;
579
+ const minColumns = bounds?.maxCol ?? 1;
580
+ // Sanitize non-finite input (NaN/Infinity from e.g. parseInt of empty user
581
+ // input) to 1 so it can never reach committed grid state; clamp-to-content
582
+ // then floors it at the occupied extent.
583
+ const reqRows = sanitizeRequest(requestedRows);
584
+ const reqColumns = sanitizeRequest(requestedColumns);
585
+ const rows = Math.max(minRows, reqRows);
586
+ const columns = Math.max(minColumns, reqColumns);
587
+ return {
588
+ rows,
589
+ columns,
590
+ clamped: rows !== reqRows || columns !== reqColumns,
591
+ };
592
+ }
593
+
562
594
  const initialGridConfigState = {
563
595
  rows: 8,
564
596
  columns: 16,
@@ -722,6 +754,9 @@ const initialDragDropState = {
722
754
  hoveredDropZone: null,
723
755
  };
724
756
  const withDragDrop = () => signalStoreFeature(withState(initialDragDropState), withComputed((store) => ({
757
+ // True while any widget drag is in progress. Shared by components that
758
+ // need to suppress conflicting affordances during a drag.
759
+ isDragActive: computed(() => !!store.dragData()),
725
760
  // Highlighted zones during drag
726
761
  highlightedZones: computed(() => calculateHighlightedZones(store.dragData(), store.hoveredDropZone())),
727
762
  })), withComputed((store) => ({
@@ -948,6 +983,32 @@ const withResize = () => signalStoreFeature(withState(initialResizeState), withM
948
983
  },
949
984
  })));
950
985
 
986
+ const initialGridResizeState = {
987
+ gridResizePreview: null,
988
+ };
989
+ const withGridResize = () => signalStoreFeature(withState(initialGridResizeState), withMethods((store) => ({
990
+ // Needs cross-feature dependencies (current size + cells for the floor),
991
+ // injected by the store wrapper. Skips the patch when the clamped result
992
+ // is unchanged — e.g. dragging further past the content floor yields new
993
+ // raw deltas but the same clamped preview — avoiding redundant re-renders.
994
+ _previewGridResize(deltaRows, deltaColumns, dependencies) {
995
+ const preview = clampGridSize(dependencies.rows + deltaRows, dependencies.columns + deltaColumns, dependencies.cells);
996
+ const current = store.gridResizePreview();
997
+ if (current &&
998
+ current.rows === preview.rows &&
999
+ current.columns === preview.columns &&
1000
+ current.clamped === preview.clamped) {
1001
+ return;
1002
+ }
1003
+ patchState(store, { gridResizePreview: preview });
1004
+ },
1005
+ clearGridResizePreview() {
1006
+ if (store.gridResizePreview() !== null) {
1007
+ patchState(store, { gridResizePreview: null });
1008
+ }
1009
+ },
1010
+ })));
1011
+
951
1012
  /** Returns the intended widget type ID, falling back to the factory's type ID */
952
1013
  function effectiveWidgetTypeid(cell) {
953
1014
  return cell.widgetTypeid ?? cell.widgetFactory.widgetTypeid;
@@ -957,9 +1018,15 @@ const initialState = {
957
1018
  };
958
1019
  const DashboardStore = signalStore(withState(initialState), withProps(() => ({
959
1020
  dashboardService: inject(DashboardService),
960
- })), withGridConfig(), withWidgetManagement(), withResize(), withDragDrop(),
1021
+ })), withGridConfig(), withWidgetManagement(), withResize(), withGridResize(), withDragDrop(),
961
1022
  // Cross-feature computed properties (need access to multiple features)
962
1023
  withComputed((store) => ({
1024
+ // Effective grid size: the live resize preview when a handle drag is in
1025
+ // progress, otherwise the committed size. Consumed wherever the rendered
1026
+ // grid size matters (editor template, outer frame, viewport letterboxing)
1027
+ // so they all reflow together during a drag.
1028
+ effectiveRows: computed(() => store.gridResizePreview()?.rows ?? store.rows()),
1029
+ effectiveColumns: computed(() => store.gridResizePreview()?.columns ?? store.columns()),
963
1030
  // Invalid zones (collision detection)
964
1031
  invalidHighlightMap: computed(() => {
965
1032
  const collisionInfo = calculateCollisionInfo(store.dragData(), store.hoveredDropZone(), store.cells(), store.rows(), store.columns());
@@ -1010,6 +1077,24 @@ withMethods((store) => ({
1010
1077
  },
1011
1078
  });
1012
1079
  },
1080
+ // GRID RESIZE (change row/column counts on a populated dashboard)
1081
+ // Clamp-to-content policy: a requested size that would push a widget out
1082
+ // of bounds is snapped up to the smallest size that still contains every
1083
+ // widget's full footprint, so shrinking never orphans a widget.
1084
+ setGridSize(rows, columns) {
1085
+ const result = clampGridSize(rows, columns, store.cells());
1086
+ store.setGridConfig({ rows: result.rows, columns: result.columns });
1087
+ return result;
1088
+ },
1089
+ // Preview a relative grid resize during a handle drag (no commit). Stores
1090
+ // the clamped target in gridResizePreview so the editor can live-reflow.
1091
+ previewGridResize(deltaRows, deltaColumns) {
1092
+ store._previewGridResize(deltaRows, deltaColumns, {
1093
+ rows: store.rows(),
1094
+ columns: store.columns(),
1095
+ cells: store.cells(),
1096
+ });
1097
+ },
1013
1098
  // EXPORT/IMPORT METHODS (need access to multiple features)
1014
1099
  exportDashboard(getCurrentWidgetStates, selection, selectionOptions) {
1015
1100
  // Get live widget states if callback provided, otherwise use stored states
@@ -1105,6 +1190,27 @@ withMethods((store) => ({
1105
1190
  });
1106
1191
  },
1107
1192
  })),
1193
+ // End a grid resize gesture atomically (mirrors withResize._endResize):
1194
+ // clear the live preview, no-op on a zero delta, otherwise commit the
1195
+ // relative resize. Returns null (no committed change) when the delta is zero
1196
+ // or clamp-to-content leaves the size unchanged, so callers don't signal a
1197
+ // resize that did nothing. Split into its own block so it can call the
1198
+ // absolute setGridSize above (siblings in one withMethods block aren't
1199
+ // visible to each other).
1200
+ withMethods((store) => ({
1201
+ endGridResize(deltaRows, deltaColumns) {
1202
+ store.clearGridResizePreview();
1203
+ if (deltaRows === 0 && deltaColumns === 0)
1204
+ return null;
1205
+ const beforeRows = store.rows();
1206
+ const beforeColumns = store.columns();
1207
+ const result = store.setGridSize(beforeRows + deltaRows, beforeColumns + deltaColumns);
1208
+ if (result.rows === beforeRows && result.columns === beforeColumns) {
1209
+ return null;
1210
+ }
1211
+ return result;
1212
+ },
1213
+ })),
1108
1214
  // Cross-feature computed properties that depend on resize + widget data (using utility functions)
1109
1215
  withComputed((store) => ({
1110
1216
  // Compute preview cells during resize using utility function
@@ -1404,8 +1510,7 @@ class CellComponent {
1404
1510
  : false;
1405
1511
  }, /* @ts-ignore */
1406
1512
  ...(ngDevMode ? [{ debugName: "isResizing" }] : /* istanbul ignore next */ []));
1407
- isDragActive = computed(() => !!this.#store.dragData(), /* @ts-ignore */
1408
- ...(ngDevMode ? [{ debugName: "isDragActive" }] : /* istanbul ignore next */ []));
1513
+ isDragActive = this.#store.isDragActive;
1409
1514
  resizeData = this.#store.resizeData;
1410
1515
  gridCellDimensions = this.#store.gridCellDimensions;
1411
1516
  resizeDirection = signal(null, /* @ts-ignore */
@@ -1647,7 +1752,7 @@ class CellComponent {
1647
1752
  return this.widgetState();
1648
1753
  }
1649
1754
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: CellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1650
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", type: CellComponent, isStandalone: true, selector: "lib-cell", inputs: { widgetId: { classPropertyName: "widgetId", publicName: "widgetId", isSignal: true, isRequired: true, transformFunction: null }, cellId: { classPropertyName: "cellId", publicName: "cellId", isSignal: true, isRequired: true, transformFunction: null }, widgetFactory: { classPropertyName: "widgetFactory", publicName: "widgetFactory", isSignal: true, isRequired: false, transformFunction: null }, widgetState: { classPropertyName: "widgetState", publicName: "widgetState", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: false, transformFunction: null }, flat: { classPropertyName: "flat", publicName: "flat", isSignal: true, isRequired: false, transformFunction: null }, row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, rowSpan: { classPropertyName: "rowSpan", publicName: "rowSpan", isSignal: true, isRequired: false, transformFunction: null }, colSpan: { classPropertyName: "colSpan", publicName: "colSpan", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { row: "rowChange", column: "columnChange", dragStart: "dragStart", dragEnd: "dragEnd", edit: "edit", delete: "delete", settings: "settings", resizeStart: "resizeStart", resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { properties: { "style.grid-row": "gridRowStyle()", "style.grid-column": "gridColumnStyle()", "class.is-dragging": "isDragging()", "class.drag-active": "isDragActive()", "class.flat": "flat() === true" } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<!-- cell.component.html -->\n<div\n class=\"cell\"\n [class.is-resizing]=\"isResizing()\"\n [class.flat]=\"flat() === true\"\n [draggable]=\"draggable()\"\n (dragstart)=\"onDragStart($event)\"\n (dragend)=\"onDragEnd()\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n <div class=\"content-area\">\n <ng-template #container></ng-template>\n </div>\n @if (isEditMode() && !isDragging()) {\n <!-- Right resize handle -->\n <div\n class=\"resize-handle resize-handle--right\"\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\n >\n <div class=\"resize-handle-line\"></div>\n </div>\n <!-- Bottom resize handle -->\n <div\n class=\"resize-handle resize-handle--bottom\"\n (mousedown)=\"onResizeStart($event, 'vertical')\"\n >\n <div class=\"resize-handle-line\"></div>\n </div>\n }\n</div>\n\n@if (isResizing()) {\n<div class=\"resize-preview\" i18n=\"@@ngx.dashboard.cell.resize.dimensions\">\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\n</div>\n}\n", styles: [":host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(.is-dragging) .content-area{pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto;pointer-events:auto;position:relative;z-index:1}.content-area:hover{transform:initial}:host(:not(.is-dragging)) .cell.flat .content-area{pointer-events:auto}:host(:not(.is-dragging)) .cell.flat .content-area:hover{transform:initial}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}:root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1755
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", type: CellComponent, isStandalone: true, selector: "lib-cell", inputs: { widgetId: { classPropertyName: "widgetId", publicName: "widgetId", isSignal: true, isRequired: true, transformFunction: null }, cellId: { classPropertyName: "cellId", publicName: "cellId", isSignal: true, isRequired: true, transformFunction: null }, widgetFactory: { classPropertyName: "widgetFactory", publicName: "widgetFactory", isSignal: true, isRequired: false, transformFunction: null }, widgetState: { classPropertyName: "widgetState", publicName: "widgetState", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: false, transformFunction: null }, flat: { classPropertyName: "flat", publicName: "flat", isSignal: true, isRequired: false, transformFunction: null }, row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, rowSpan: { classPropertyName: "rowSpan", publicName: "rowSpan", isSignal: true, isRequired: false, transformFunction: null }, colSpan: { classPropertyName: "colSpan", publicName: "colSpan", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { row: "rowChange", column: "columnChange", dragStart: "dragStart", dragEnd: "dragEnd", edit: "edit", delete: "delete", settings: "settings", resizeStart: "resizeStart", resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { properties: { "style.grid-row": "gridRowStyle()", "style.grid-column": "gridColumnStyle()", "class.is-dragging": "isDragging()", "class.drag-active": "isDragActive()", "class.flat": "flat() === true" } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<!-- cell.component.html -->\n<div\n class=\"cell\"\n [class.is-resizing]=\"isResizing()\"\n [class.flat]=\"flat() === true\"\n [draggable]=\"draggable()\"\n (dragstart)=\"onDragStart($event)\"\n (dragend)=\"onDragEnd()\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n <div class=\"content-area\">\n <ng-template #container></ng-template>\n </div>\n @if (isEditMode() && !isDragging()) {\n <!-- Right resize handle -->\n <div\n class=\"resize-handle resize-handle--right\"\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\n >\n <div class=\"resize-handle-line\"></div>\n </div>\n <!-- Bottom resize handle -->\n <div\n class=\"resize-handle resize-handle--bottom\"\n (mousedown)=\"onResizeStart($event, 'vertical')\"\n >\n <div class=\"resize-handle-line\"></div>\n </div>\n }\n</div>\n\n@if (isResizing()) {\n<div class=\"resize-preview\" i18n=\"@@ngx.dashboard.cell.resize.dimensions\">\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\n</div>\n}\n", styles: [":root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}:root .cursor-nwse-resize{cursor:nwse-resize!important}:host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(.is-dragging) .content-area{pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto;pointer-events:auto;position:relative;z-index:1}.content-area:hover{transform:initial}:host(:not(.is-dragging)) .cell.flat .content-area{pointer-events:auto}:host(:not(.is-dragging)) .cell.flat .content-area:hover{transform:initial}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1651
1756
  }
1652
1757
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: CellComponent, decorators: [{
1653
1758
  type: Component,
@@ -1657,7 +1762,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
1657
1762
  '[class.is-dragging]': 'isDragging()',
1658
1763
  '[class.drag-active]': 'isDragActive()',
1659
1764
  '[class.flat]': 'flat() === true',
1660
- }, template: "<!-- cell.component.html -->\n<div\n class=\"cell\"\n [class.is-resizing]=\"isResizing()\"\n [class.flat]=\"flat() === true\"\n [draggable]=\"draggable()\"\n (dragstart)=\"onDragStart($event)\"\n (dragend)=\"onDragEnd()\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n <div class=\"content-area\">\n <ng-template #container></ng-template>\n </div>\n @if (isEditMode() && !isDragging()) {\n <!-- Right resize handle -->\n <div\n class=\"resize-handle resize-handle--right\"\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\n >\n <div class=\"resize-handle-line\"></div>\n </div>\n <!-- Bottom resize handle -->\n <div\n class=\"resize-handle resize-handle--bottom\"\n (mousedown)=\"onResizeStart($event, 'vertical')\"\n >\n <div class=\"resize-handle-line\"></div>\n </div>\n }\n</div>\n\n@if (isResizing()) {\n<div class=\"resize-preview\" i18n=\"@@ngx.dashboard.cell.resize.dimensions\">\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\n</div>\n}\n", styles: [":host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(.is-dragging) .content-area{pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto;pointer-events:auto;position:relative;z-index:1}.content-area:hover{transform:initial}:host(:not(.is-dragging)) .cell.flat .content-area{pointer-events:auto}:host(:not(.is-dragging)) .cell.flat .content-area:hover{transform:initial}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}:root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}\n"] }]
1765
+ }, template: "<!-- cell.component.html -->\n<div\n class=\"cell\"\n [class.is-resizing]=\"isResizing()\"\n [class.flat]=\"flat() === true\"\n [draggable]=\"draggable()\"\n (dragstart)=\"onDragStart($event)\"\n (dragend)=\"onDragEnd()\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n <div class=\"content-area\">\n <ng-template #container></ng-template>\n </div>\n @if (isEditMode() && !isDragging()) {\n <!-- Right resize handle -->\n <div\n class=\"resize-handle resize-handle--right\"\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\n >\n <div class=\"resize-handle-line\"></div>\n </div>\n <!-- Bottom resize handle -->\n <div\n class=\"resize-handle resize-handle--bottom\"\n (mousedown)=\"onResizeStart($event, 'vertical')\"\n >\n <div class=\"resize-handle-line\"></div>\n </div>\n }\n</div>\n\n@if (isResizing()) {\n<div class=\"resize-preview\" i18n=\"@@ngx.dashboard.cell.resize.dimensions\">\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\n</div>\n}\n", styles: [":root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}:root .cursor-nwse-resize{cursor:nwse-resize!important}:host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(.is-dragging) .content-area{pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto;pointer-events:auto;position:relative;z-index:1}.content-area:hover{transform:initial}:host(:not(.is-dragging)) .cell.flat .content-area{pointer-events:auto}:host(:not(.is-dragging)) .cell.flat .content-area:hover{transform:initial}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}\n"] }]
1661
1766
  }], ctorParameters: () => [], propDecorators: { widgetId: [{ type: i0.Input, args: [{ isSignal: true, alias: "widgetId", required: true }] }], cellId: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellId", required: true }] }], widgetFactory: [{ type: i0.Input, args: [{ isSignal: true, alias: "widgetFactory", required: false }] }], widgetState: [{ type: i0.Input, args: [{ isSignal: true, alias: "widgetState", required: false }] }], isEditMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "isEditMode", required: false }] }], flat: [{ type: i0.Input, args: [{ isSignal: true, alias: "flat", required: false }] }], row: [{ type: i0.Input, args: [{ isSignal: true, alias: "row", required: true }] }, { type: i0.Output, args: ["rowChange"] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }, { type: i0.Output, args: ["columnChange"] }], rowSpan: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowSpan", required: false }] }], colSpan: [{ type: i0.Input, args: [{ isSignal: true, alias: "colSpan", required: false }] }], draggable: [{ type: i0.Input, args: [{ isSignal: true, alias: "draggable", required: false }] }], dragStart: [{ type: i0.Output, args: ["dragStart"] }], dragEnd: [{ type: i0.Output, args: ["dragEnd"] }], edit: [{ type: i0.Output, args: ["edit"] }], delete: [{ type: i0.Output, args: ["delete"] }], settings: [{ type: i0.Output, args: ["settings"] }], resizeStart: [{ type: i0.Output, args: ["resizeStart"] }], resizeMove: [{ type: i0.Output, args: ["resizeMove"] }], resizeEnd: [{ type: i0.Output, args: ["resizeEnd"] }], container: [{ type: i0.ViewChild, args: ['container', { ...{ read: ViewContainerRef }, isSignal: true }] }] } });
1662
1767
 
1663
1768
  /**
@@ -2370,6 +2475,8 @@ class DropZoneComponent {
2370
2475
  ...(ngDevMode ? [{ debugName: "highlightInvalid" }] : /* istanbul ignore next */ []));
2371
2476
  highlightResize = input(false, /* @ts-ignore */
2372
2477
  ...(ngDevMode ? [{ debugName: "highlightResize" }] : /* istanbul ignore next */ []));
2478
+ highlightPreview = input(false, /* @ts-ignore */
2479
+ ...(ngDevMode ? [{ debugName: "highlightPreview" }] : /* istanbul ignore next */ []));
2373
2480
  editMode = input(false, /* @ts-ignore */
2374
2481
  ...(ngDevMode ? [{ debugName: "editMode" }] : /* istanbul ignore next */ []));
2375
2482
  // Outputs
@@ -2475,12 +2582,12 @@ class DropZoneComponent {
2475
2582
  }
2476
2583
  }
2477
2584
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: DropZoneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2478
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", 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 -->\n<div\n class=\"drop-zone\"\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\n [class.drop-zone--invalid]=\"highlightInvalid()\"\n [class.drop-zone--resize]=\"highlightResize()\"\n [style.grid-row]=\"row()\"\n [style.grid-column]=\"col()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n @if (editMode()) {\n <div class=\"edit-mode-cell-number\">\n {{ index() }}<br />\n {{ row() }},{{ col() }}\n </div>\n }\n</div>\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"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2585
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", 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 }, highlightPreview: { classPropertyName: "highlightPreview", publicName: "highlightPreview", 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 -->\n<div\n class=\"drop-zone\"\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\n [class.drop-zone--invalid]=\"highlightInvalid()\"\n [class.drop-zone--resize]=\"highlightResize()\"\n [class.drop-zone--preview]=\"highlightPreview()\"\n [style.grid-row]=\"row()\"\n [style.grid-column]=\"col()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n @if (editMode()) {\n <div class=\"edit-mode-cell-number\">\n {{ index() }}<br />\n {{ row() }},{{ col() }}\n </div>\n }\n</div>\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)}.drop-zone--preview{background-color:color-mix(in srgb,var(--mat-sys-primary) 18%,transparent);outline:1px dashed color-mix(in srgb,var(--mat-sys-primary) 50%,transparent)}.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"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2479
2586
  }
2480
2587
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: DropZoneComponent, decorators: [{
2481
2588
  type: Component,
2482
- args: [{ selector: 'lib-drop-zone', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- drop-zone.component.html -->\n<div\n class=\"drop-zone\"\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\n [class.drop-zone--invalid]=\"highlightInvalid()\"\n [class.drop-zone--resize]=\"highlightResize()\"\n [style.grid-row]=\"row()\"\n [style.grid-column]=\"col()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n @if (editMode()) {\n <div class=\"edit-mode-cell-number\">\n {{ index() }}<br />\n {{ row() }},{{ col() }}\n </div>\n }\n</div>\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"] }]
2483
- }], propDecorators: { row: [{ type: i0.Input, args: [{ isSignal: true, alias: "row", required: true }] }], col: [{ type: i0.Input, args: [{ isSignal: true, alias: "col", required: true }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: true }] }], highlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlight", required: false }] }], highlightInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightInvalid", required: false }] }], highlightResize: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightResize", required: false }] }], editMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "editMode", required: false }] }], dragEnter: [{ type: i0.Output, args: ["dragEnter"] }], dragExit: [{ type: i0.Output, args: ["dragExit"] }], dragOver: [{ type: i0.Output, args: ["dragOver"] }], dragDrop: [{ type: i0.Output, args: ["dragDrop"] }] } });
2589
+ args: [{ selector: 'lib-drop-zone', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- drop-zone.component.html -->\n<div\n class=\"drop-zone\"\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\n [class.drop-zone--invalid]=\"highlightInvalid()\"\n [class.drop-zone--resize]=\"highlightResize()\"\n [class.drop-zone--preview]=\"highlightPreview()\"\n [style.grid-row]=\"row()\"\n [style.grid-column]=\"col()\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n @if (editMode()) {\n <div class=\"edit-mode-cell-number\">\n {{ index() }}<br />\n {{ row() }},{{ col() }}\n </div>\n }\n</div>\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)}.drop-zone--preview{background-color:color-mix(in srgb,var(--mat-sys-primary) 18%,transparent);outline:1px dashed color-mix(in srgb,var(--mat-sys-primary) 50%,transparent)}.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"] }]
2590
+ }], propDecorators: { row: [{ type: i0.Input, args: [{ isSignal: true, alias: "row", required: true }] }], col: [{ type: i0.Input, args: [{ isSignal: true, alias: "col", required: true }] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: true }] }], highlight: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlight", required: false }] }], highlightInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightInvalid", required: false }] }], highlightResize: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightResize", required: false }] }], highlightPreview: [{ type: i0.Input, args: [{ isSignal: true, alias: "highlightPreview", required: false }] }], editMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "editMode", required: false }] }], dragEnter: [{ type: i0.Output, args: ["dragEnter"] }], dragExit: [{ type: i0.Output, args: ["dragExit"] }], dragOver: [{ type: i0.Output, args: ["dragOver"] }], dragDrop: [{ type: i0.Output, args: ["dragDrop"] }] } });
2484
2591
 
2485
2592
  /**
2486
2593
  * Context menu component for empty dashboard cells.
@@ -2709,6 +2816,155 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2709
2816
  `, 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"] }]
2710
2817
  }], ctorParameters: () => [], propDecorators: { menuTrigger: [{ type: i0.ViewChild, args: ['menuTrigger', { ...{ read: MatMenuTrigger }, isSignal: true }] }] } });
2711
2818
 
2819
+ // grid-resize-handle.component.ts
2820
+ //
2821
+ // A small, self-contained drag handle for resizing the dashboard grid's
2822
+ // row/column counts from the editor. Uses PointerEvents (with pointer capture)
2823
+ // so mouse, touch and pen all work uniformly, and reports a whole-grid delta
2824
+ // in track counts rather than a per-widget span change.
2825
+ //
2826
+ // The handle is intentionally store-agnostic: it converts pointer movement
2827
+ // into track-count deltas using the cell footprint passed in, and emits them.
2828
+ // The editor owns the actual grid mutation (clamp-to-content lives there).
2829
+ /**
2830
+ * Round half away from zero so inward and outward half-cell drags behave
2831
+ * symmetrically. `Math.round` rounds 0.5 to 1 but -0.5 to 0, which makes a
2832
+ * half-cell shrink "stick" while a half-cell grow responds.
2833
+ */
2834
+ function symmetricRound(value) {
2835
+ return Math.sign(value) * Math.round(Math.abs(value));
2836
+ }
2837
+ class GridResizeHandleComponent {
2838
+ axis = input.required(/* @ts-ignore */
2839
+ ...(ngDevMode ? [{ debugName: "axis" }] : /* istanbul ignore next */ []));
2840
+ /** Current grid cell footprint in px; converts pointer delta to track counts. */
2841
+ cellWidth = input.required(/* @ts-ignore */
2842
+ ...(ngDevMode ? [{ debugName: "cellWidth" }] : /* istanbul ignore next */ []));
2843
+ cellHeight = input.required(/* @ts-ignore */
2844
+ ...(ngDevMode ? [{ debugName: "cellHeight" }] : /* istanbul ignore next */ []));
2845
+ resizeMove = output();
2846
+ resizeEnd = output();
2847
+ #host = inject(ElementRef);
2848
+ #renderer = inject(Renderer2);
2849
+ #destroyRef = inject(DestroyRef);
2850
+ isActive = signal(false, /* @ts-ignore */
2851
+ ...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
2852
+ #startX = 0;
2853
+ #startY = 0;
2854
+ // Cell footprint snapshot taken at gesture start. Reading the live signal
2855
+ // would feed back on itself: the editor's live preview shrinks cells as
2856
+ // columns are added, which would shrink this px->track divisor mid-drag and
2857
+ // make the grid overshoot.
2858
+ #cellWidth = 0;
2859
+ #cellHeight = 0;
2860
+ #pointerId = null;
2861
+ #delta = { deltaColumns: 0, deltaRows: 0 };
2862
+ #cleanup;
2863
+ constructor() {
2864
+ // Defensive: drop listeners / capture if the handle is torn down mid-drag.
2865
+ this.#destroyRef.onDestroy(() => this.#stop());
2866
+ }
2867
+ onResizeStart(event) {
2868
+ event.preventDefault();
2869
+ event.stopPropagation();
2870
+ this.#startX = event.clientX;
2871
+ this.#startY = event.clientY;
2872
+ this.#cellWidth = this.cellWidth();
2873
+ this.#cellHeight = this.cellHeight();
2874
+ this.#delta = { deltaColumns: 0, deltaRows: 0 };
2875
+ this.isActive.set(true);
2876
+ // Capture so the gesture keeps receiving events even if the pointer leaves
2877
+ // the thin handle strip. try/catch tolerates environments without capture
2878
+ // support (TypeError) and synthetic test events with no active pointer.
2879
+ try {
2880
+ this.#host.nativeElement.setPointerCapture(event.pointerId);
2881
+ this.#pointerId = event.pointerId;
2882
+ }
2883
+ catch {
2884
+ // No capture; the document listeners still drive the gesture.
2885
+ }
2886
+ // Document/window listeners only exist while actively dragging this handle.
2887
+ const unlistenMove = this.#renderer.listen('document', 'pointermove', this.#onMove);
2888
+ const unlistenUp = this.#renderer.listen('document', 'pointerup', this.#onUp);
2889
+ const unlistenCancel = this.#renderer.listen('document', 'pointercancel', this.#onAbort);
2890
+ // A pointerup that lands outside the window never fires; window blur is the
2891
+ // backstop that aborts a gesture the browser has otherwise abandoned.
2892
+ const unlistenBlur = this.#renderer.listen('window', 'blur', this.#onAbort);
2893
+ this.#cleanup = () => {
2894
+ unlistenMove();
2895
+ unlistenUp();
2896
+ unlistenCancel();
2897
+ unlistenBlur();
2898
+ };
2899
+ this.#renderer.addClass(document.body, this.#cursorClass());
2900
+ }
2901
+ // Arrow fields keep `this` bound when used as Renderer2 listener callbacks.
2902
+ #onMove = (event) => {
2903
+ const axis = this.axis();
2904
+ const next = {
2905
+ deltaColumns: axis !== 'vertical' && this.#cellWidth > 0
2906
+ ? symmetricRound((event.clientX - this.#startX) / this.#cellWidth)
2907
+ : 0,
2908
+ deltaRows: axis !== 'horizontal' && this.#cellHeight > 0
2909
+ ? symmetricRound((event.clientY - this.#startY) / this.#cellHeight)
2910
+ : 0,
2911
+ };
2912
+ // Only emit when the rounded track delta actually changes, so sub-cell
2913
+ // pointer movement doesn't spam the live preview.
2914
+ if (next.deltaColumns === this.#delta.deltaColumns &&
2915
+ next.deltaRows === this.#delta.deltaRows) {
2916
+ return;
2917
+ }
2918
+ this.#delta = next;
2919
+ this.resizeMove.emit(next);
2920
+ };
2921
+ #onUp = () => this.#finish(this.#delta);
2922
+ // Abort (pointercancel / window blur): discard the in-progress delta so the
2923
+ // editor clears the preview without committing a half-finished gesture.
2924
+ #onAbort = () => this.#finish({ deltaColumns: 0, deltaRows: 0 });
2925
+ #finish(delta) {
2926
+ if (!this.isActive())
2927
+ return;
2928
+ this.#stop();
2929
+ this.resizeEnd.emit(delta);
2930
+ }
2931
+ #stop() {
2932
+ this.isActive.set(false);
2933
+ this.#renderer.removeClass(document.body, this.#cursorClass());
2934
+ if (this.#pointerId !== null) {
2935
+ try {
2936
+ this.#host.nativeElement.releasePointerCapture(this.#pointerId);
2937
+ }
2938
+ catch {
2939
+ // Capture may already be gone (e.g. pointercancel released it).
2940
+ }
2941
+ }
2942
+ this.#pointerId = null;
2943
+ this.#cleanup?.();
2944
+ this.#cleanup = undefined;
2945
+ }
2946
+ #cursorClass() {
2947
+ switch (this.axis()) {
2948
+ case 'horizontal':
2949
+ return 'cursor-col-resize';
2950
+ case 'vertical':
2951
+ return 'cursor-row-resize';
2952
+ default:
2953
+ return 'cursor-nwse-resize';
2954
+ }
2955
+ }
2956
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: GridResizeHandleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2957
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.4", type: GridResizeHandleComponent, isStandalone: true, selector: "lib-grid-resize-handle", inputs: { axis: { classPropertyName: "axis", publicName: "axis", isSignal: true, isRequired: true, transformFunction: null }, cellWidth: { classPropertyName: "cellWidth", publicName: "cellWidth", isSignal: true, isRequired: true, transformFunction: null }, cellHeight: { classPropertyName: "cellHeight", publicName: "cellHeight", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { listeners: { "pointerdown": "onResizeStart($event)" }, properties: { "class": "'axis-' + axis()", "class.is-active": "isActive()" } }, ngImport: i0, template: "<!-- grid-resize-handle.component.html -->\n<div class=\"grip-line\"></div>\n", styles: [":root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}:root .cursor-nwse-resize{cursor:nwse-resize!important}:host{position:absolute;z-index:25;display:flex;align-items:center;justify-content:center;opacity:.3;transition:opacity .15s ease;touch-action:none}:host(.axis-horizontal){cursor:col-resize;top:0;right:0;width:max(var(--gutter-size, 8px),6px);height:100%}:host(.axis-vertical){cursor:row-resize;left:0;bottom:0;width:100%;height:max(var(--gutter-size, 8px),6px)}:host(.axis-both){cursor:nwse-resize;right:0;bottom:0;width:max(var(--gutter-size, 8px),8px);height:max(var(--gutter-size, 8px),8px);z-index:26}:host(:hover),:host(.is-active){opacity:1}.grip-line{background-color:var(--mat-sys-outline-variant);border-radius:2px}:host(.axis-horizontal) .grip-line{width:4px;height:48px;max-height:60%}:host(.axis-vertical) .grip-line{width:48px;max-width:60%;height:4px}:host(.axis-both) .grip-line{width:6px;height:6px;border-bottom-right-radius:3px}:host(:hover) .grip-line,:host(.is-active) .grip-line{background-color:var(--mat-sys-primary)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2958
+ }
2959
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: GridResizeHandleComponent, decorators: [{
2960
+ type: Component,
2961
+ args: [{ selector: 'lib-grid-resize-handle', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, host: {
2962
+ '[class]': "'axis-' + axis()",
2963
+ '[class.is-active]': 'isActive()',
2964
+ '(pointerdown)': 'onResizeStart($event)',
2965
+ }, template: "<!-- grid-resize-handle.component.html -->\n<div class=\"grip-line\"></div>\n", styles: [":root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}:root .cursor-nwse-resize{cursor:nwse-resize!important}:host{position:absolute;z-index:25;display:flex;align-items:center;justify-content:center;opacity:.3;transition:opacity .15s ease;touch-action:none}:host(.axis-horizontal){cursor:col-resize;top:0;right:0;width:max(var(--gutter-size, 8px),6px);height:100%}:host(.axis-vertical){cursor:row-resize;left:0;bottom:0;width:100%;height:max(var(--gutter-size, 8px),6px)}:host(.axis-both){cursor:nwse-resize;right:0;bottom:0;width:max(var(--gutter-size, 8px),8px);height:max(var(--gutter-size, 8px),8px);z-index:26}:host(:hover),:host(.is-active){opacity:1}.grip-line{background-color:var(--mat-sys-outline-variant);border-radius:2px}:host(.axis-horizontal) .grip-line{width:4px;height:48px;max-height:60%}:host(.axis-vertical) .grip-line{width:48px;max-width:60%;height:4px}:host(.axis-both) .grip-line{width:6px;height:6px;border-bottom-right-radius:3px}:host(:hover) .grip-line,:host(.is-active) .grip-line{background-color:var(--mat-sys-primary)}\n"] }]
2966
+ }], ctorParameters: () => [], propDecorators: { axis: [{ type: i0.Input, args: [{ isSignal: true, alias: "axis", required: true }] }], cellWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellWidth", required: true }] }], cellHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellHeight", required: true }] }], resizeMove: [{ type: i0.Output, args: ["resizeMove"] }], resizeEnd: [{ type: i0.Output, args: ["resizeEnd"] }] } });
2967
+
2712
2968
  // dashboard-editor.component.ts
2713
2969
  class DashboardEditorComponent {
2714
2970
  bottomGridRef = viewChild.required('bottomGrid');
@@ -2725,8 +2981,8 @@ class DashboardEditorComponent {
2725
2981
  ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
2726
2982
  gutterSize = input('1em', /* @ts-ignore */
2727
2983
  ...(ngDevMode ? [{ debugName: "gutterSize" }] : /* istanbul ignore next */ []));
2728
- gutters = computed(() => this.columns() + 1, /* @ts-ignore */
2729
- ...(ngDevMode ? [{ debugName: "gutters" }] : /* istanbul ignore next */ []));
2984
+ // Emitted when a grid resize handle commits a new size (after clamp-to-content).
2985
+ gridResized = output();
2730
2986
  // store signals
2731
2987
  cells = this.#store.cells;
2732
2988
  highlightedZones = this.#store.highlightedZones;
@@ -2734,22 +2990,48 @@ class DashboardEditorComponent {
2734
2990
  invalidHighlightMap = this.#store.invalidHighlightMap;
2735
2991
  hoveredDropZone = this.#store.hoveredDropZone;
2736
2992
  resizePreviewMap = this.#store.resizePreviewMap;
2737
- // Generate all possible cell positions for the grid
2993
+ cellDimensions = this.#store.gridCellDimensions;
2994
+ gridResizePreview = this.#store.gridResizePreview;
2995
+ // Effective grid size (live preview when dragging, else committed) — shared
2996
+ // from the store so the editor grid, the outer frame and the viewport
2997
+ // letterboxing all reflow together. Drives the grid template, aspect-ratio
2998
+ // and drop zones so the editor reflows under the handle without committing.
2999
+ effectiveRows = this.#store.effectiveRows;
3000
+ effectiveColumns = this.#store.effectiveColumns;
3001
+ // Hide grid resize handles while a widget drag is in progress to avoid
3002
+ // conflicting gestures.
3003
+ isDragActive = this.#store.isDragActive;
3004
+ // Axes rendered as grid resize handles (right edge, bottom edge, corner).
3005
+ resizeAxes = [
3006
+ 'horizontal',
3007
+ 'vertical',
3008
+ 'both',
3009
+ ];
3010
+ // Generate all possible cell positions for the grid (using the effective
3011
+ // size so the drop-zone grid grows/shrinks with the live resize preview).
2738
3012
  dropzonePositions = computed(() => {
3013
+ const rows = this.effectiveRows();
3014
+ const columns = this.effectiveColumns();
2739
3015
  const positions = [];
2740
- for (let row = 1; row <= this.rows(); row++) {
2741
- for (let col = 1; col <= this.columns(); col++) {
3016
+ for (let row = 1; row <= rows; row++) {
3017
+ for (let col = 1; col <= columns; col++) {
2742
3018
  positions.push({
2743
3019
  row,
2744
3020
  col,
2745
3021
  id: `dropzone-${row}-${col}`,
2746
- index: (row - 1) * this.columns() + col,
3022
+ index: (row - 1) * columns + col,
2747
3023
  });
2748
3024
  }
2749
3025
  }
2750
3026
  return positions;
2751
3027
  }, /* @ts-ignore */
2752
3028
  ...(ngDevMode ? [{ debugName: "dropzonePositions" }] : /* istanbul ignore next */ []));
3029
+ // True for drop zones in the about-to-be-added region during a grow preview,
3030
+ // so the template can tint the tracks the resize is adding.
3031
+ isPreviewAdded(row, col) {
3032
+ return (this.gridResizePreview() !== null &&
3033
+ (row > this.rows() || col > this.columns()));
3034
+ }
2753
3035
  // Helper method for template
2754
3036
  createCellId(row, col) {
2755
3037
  return CellIdUtils.create(row, col);
@@ -2771,6 +3053,10 @@ class DashboardEditorComponent {
2771
3053
  effect(() => {
2772
3054
  this.#store.setEditMode(true);
2773
3055
  });
3056
+ // Drop any in-progress resize preview if the editor is torn down mid-drag
3057
+ // (e.g. editMode toggled off): the store outlives this component, so a
3058
+ // stale preview would otherwise render a phantom grid on the next mount.
3059
+ this.#destroyRef.onDestroy(() => this.#store.clearGridResizePreview());
2774
3060
  }
2775
3061
  #observeGridSize() {
2776
3062
  const gridEl = this.bottomGridRef()?.nativeElement;
@@ -2821,10 +3107,22 @@ class DashboardEditorComponent {
2821
3107
  this.#store.handleDrop(event.data, event.target);
2822
3108
  // Note: Store handles all validation and error handling internally
2823
3109
  }
3110
+ // Live preview while dragging a handle: the store reflows the grid to the
3111
+ // clamped target size without committing.
3112
+ onGridResizeMove(delta) {
3113
+ this.#store.previewGridResize(delta.deltaRows, delta.deltaColumns);
3114
+ }
3115
+ // Commit a grid resize gesture. The store clears the preview, no-ops on a
3116
+ // zero delta, and otherwise commits the clamped relative resize.
3117
+ onGridResizeEnd(delta) {
3118
+ const result = this.#store.endGridResize(delta.deltaRows, delta.deltaColumns);
3119
+ if (result)
3120
+ this.gridResized.emit(result);
3121
+ }
2824
3122
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: DashboardEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2825
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", 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: [
3123
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", 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 } }, outputs: { gridResized: "gridResized" }, host: { properties: { "style.--rows": "effectiveRows()", "style.--columns": "effectiveColumns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "effectiveColumns() + 1", "class.is-edit-mode": "true" } }, providers: [
2826
3124
  CellContextMenuService,
2827
- ], 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 menus -->\n<lib-cell-context-menu></lib-cell-context-menu>\n<lib-empty-cell-context-menu></lib-empty-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: "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 });
3125
+ ], 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 [highlightPreview]=\"isPreviewAdded(position.row, position.col)\"\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 <!-- Grid resize handles (right edge, bottom edge, bottom-right corner).\n Hidden during a widget drag to avoid conflicting gestures. -->\n @if (!isDragActive()) {\n @for (axis of resizeAxes; track axis) {\n <lib-grid-resize-handle\n [axis]=\"axis\"\n [cellWidth]=\"cellDimensions().width\"\n [cellHeight]=\"cellDimensions().height\"\n (resizeMove)=\"onGridResizeMove($event)\"\n (resizeEnd)=\"onGridResizeEnd($event)\"\n ></lib-grid-resize-handle>\n }\n }\n\n <!-- Live size badge shown while a resize handle is being dragged. -->\n @if (gridResizePreview(); as preview) {\n <div\n class=\"grid-resize-badge\"\n [class.grid-resize-badge--clamped]=\"preview.clamped\"\n i18n=\"@@ngx.dashboard.editor.gridResize.dimensions\"\n >\n {{ preview.columns }} \u00D7 {{ preview.rows }}\n </div>\n }\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 menus -->\n<lib-cell-context-menu></lib-cell-context-menu>\n<lib-empty-cell-context-menu></lib-empty-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}.grid-resize-badge{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:30;padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary)}.grid-resize-badge--clamped{background-color:var(--mat-sys-error);color:var(--mat-sys-on-error)}\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: "component", type: DropZoneComponent, selector: "lib-drop-zone", inputs: ["row", "col", "index", "highlight", "highlightInvalid", "highlightResize", "highlightPreview", "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" }, { kind: "component", type: GridResizeHandleComponent, selector: "lib-grid-resize-handle", inputs: ["axis", "cellWidth", "cellHeight"], outputs: ["resizeMove", "resizeEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2828
3126
  }
2829
3127
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: DashboardEditorComponent, decorators: [{
2830
3128
  type: Component,
@@ -2832,17 +3130,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImpor
2832
3130
  CellComponent,
2833
3131
  DropZoneComponent,
2834
3132
  CellContextMenuComponent,
2835
- EmptyCellContextMenuComponent
3133
+ EmptyCellContextMenuComponent,
3134
+ GridResizeHandleComponent
2836
3135
  ], providers: [
2837
3136
  CellContextMenuService,
2838
3137
  ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
2839
- '[style.--rows]': 'rows()',
2840
- '[style.--columns]': 'columns()',
3138
+ '[style.--rows]': 'effectiveRows()',
3139
+ '[style.--columns]': 'effectiveColumns()',
2841
3140
  '[style.--gutter-size]': 'gutterSize()',
2842
- '[style.--gutters]': 'gutters()',
3141
+ '[style.--gutters]': 'effectiveColumns() + 1',
2843
3142
  '[class.is-edit-mode]': 'true', // Always in edit mode
2844
- }, 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 menus -->\n<lib-cell-context-menu></lib-cell-context-menu>\n<lib-empty-cell-context-menu></lib-empty-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"] }]
2845
- }], ctorParameters: () => [], propDecorators: { bottomGridRef: [{ type: i0.ViewChild, args: ['bottomGrid', { isSignal: true }] }], dropZones: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => DropZoneComponent), { isSignal: true }] }], cellComponents: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => CellComponent), { isSignal: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], gutterSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "gutterSize", required: false }] }] } });
3143
+ }, 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 [highlightPreview]=\"isPreviewAdded(position.row, position.col)\"\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 <!-- Grid resize handles (right edge, bottom edge, bottom-right corner).\n Hidden during a widget drag to avoid conflicting gestures. -->\n @if (!isDragActive()) {\n @for (axis of resizeAxes; track axis) {\n <lib-grid-resize-handle\n [axis]=\"axis\"\n [cellWidth]=\"cellDimensions().width\"\n [cellHeight]=\"cellDimensions().height\"\n (resizeMove)=\"onGridResizeMove($event)\"\n (resizeEnd)=\"onGridResizeEnd($event)\"\n ></lib-grid-resize-handle>\n }\n }\n\n <!-- Live size badge shown while a resize handle is being dragged. -->\n @if (gridResizePreview(); as preview) {\n <div\n class=\"grid-resize-badge\"\n [class.grid-resize-badge--clamped]=\"preview.clamped\"\n i18n=\"@@ngx.dashboard.editor.gridResize.dimensions\"\n >\n {{ preview.columns }} \u00D7 {{ preview.rows }}\n </div>\n }\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 menus -->\n<lib-cell-context-menu></lib-cell-context-menu>\n<lib-empty-cell-context-menu></lib-empty-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}.grid-resize-badge{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:30;padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary)}.grid-resize-badge--clamped{background-color:var(--mat-sys-error);color:var(--mat-sys-on-error)}\n"] }]
3144
+ }], ctorParameters: () => [], propDecorators: { bottomGridRef: [{ type: i0.ViewChild, args: ['bottomGrid', { isSignal: true }] }], dropZones: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => DropZoneComponent), { isSignal: true }] }], cellComponents: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => CellComponent), { isSignal: true }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], gutterSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "gutterSize", required: false }] }], gridResized: [{ type: i0.Output, args: ["gridResized"] }] } });
2846
3145
 
2847
3146
  // dashboard-bridge.service.ts
2848
3147
  /**
@@ -3061,9 +3360,10 @@ class DashboardViewportService {
3061
3360
  */
3062
3361
  constraints = computed(() => {
3063
3362
  const availableSize = this.availableSpace();
3064
- // Get grid configuration from our component's store
3065
- const rows = this.store.rows();
3066
- const columns = this.store.columns();
3363
+ // Use the effective (preview-aware) size so the letterboxed frame reflows
3364
+ // to fit during a grid-resize drag instead of overflowing/scrolling.
3365
+ const rows = this.store.effectiveRows();
3366
+ const columns = this.store.effectiveColumns();
3067
3367
  if (rows === 0 || columns === 0) {
3068
3368
  return {
3069
3369
  maxWidth: availableSize.width,
@@ -3133,6 +3433,7 @@ class DashboardComponent {
3133
3433
  ...(ngDevMode ? [{ debugName: "dragThreshold" }] : /* istanbul ignore next */ []));
3134
3434
  // Component outputs
3135
3435
  selectionComplete = output();
3436
+ gridResized = output();
3136
3437
  // Store signals - shared by both child components
3137
3438
  cells = this.#store.cells;
3138
3439
  // ViewChild references for export/import functionality
@@ -3233,6 +3534,27 @@ class DashboardComponent {
3233
3534
  clearDashboard() {
3234
3535
  this.#store.clearDashboard();
3235
3536
  }
3537
+ /**
3538
+ * Resize the dashboard grid to the given row/column counts.
3539
+ *
3540
+ * Uses a clamp-to-content policy: a size that would push an existing widget
3541
+ * out of bounds is snapped up to the smallest size that still contains every
3542
+ * widget, so shrinking never orphans a widget. The applied size (which may
3543
+ * differ from the request when clamped) is returned and emitted via
3544
+ * `gridResized`. Values below 1 are treated as 1; fractional values are
3545
+ * floored.
3546
+ */
3547
+ setGridSize(rows, columns) {
3548
+ const beforeRows = this.#store.rows();
3549
+ const beforeColumns = this.#store.columns();
3550
+ const result = this.#store.setGridSize(rows, columns);
3551
+ // Only signal a resize when the committed size actually changed, matching
3552
+ // the handle-drag path (store.endGridResize).
3553
+ if (result.rows !== beforeRows || result.columns !== beforeColumns) {
3554
+ this.gridResized.emit(result);
3555
+ }
3556
+ return result;
3557
+ }
3236
3558
  /**
3237
3559
  * Forwards to the active viewer. No-op in edit mode.
3238
3560
  * See `DashboardViewerComponent.clearSelection()`.
@@ -3277,20 +3599,20 @@ class DashboardComponent {
3277
3599
  }
3278
3600
  }
3279
3601
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: DashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3280
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", 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 }, selectionModifier: { classPropertyName: "selectionModifier", publicName: "selectionModifier", isSignal: true, isRequired: false, transformFunction: null }, dragThreshold: { classPropertyName: "dragThreshold", publicName: "dragThreshold", 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 -->\n<div class=\"grid-container\">\n @if (editMode()) {\n <!-- Full editor with drag & drop capabilities -->\n <ngx-dashboard-editor\n [rows]=\"store.rows()\"\n [columns]=\"store.columns()\"\n [gutterSize]=\"store.gutterSize()\"\n ></ngx-dashboard-editor>\n } @else {\n <!-- Read-only viewer -->\n <ngx-dashboard-viewer\n [rows]=\"store.rows()\"\n [columns]=\"store.columns()\"\n [gutterSize]=\"store.gutterSize()\"\n [enableSelection]=\"enableSelection()\"\n [selectionModifier]=\"selectionModifier()\"\n [dragThreshold]=\"dragThreshold()\"\n (selectionComplete)=\"selectionComplete.emit($event)\"\n ></ngx-dashboard-viewer>\n }\n</div>\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: "component", type: DashboardViewerComponent, selector: "ngx-dashboard-viewer", inputs: ["rows", "columns", "gutterSize", "enableSelection", "selectionModifier", "dragThreshold"], outputs: ["selectionComplete"] }, { kind: "component", type: DashboardEditorComponent, selector: "ngx-dashboard-editor", inputs: ["rows", "columns", "gutterSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3602
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.4", 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 }, selectionModifier: { classPropertyName: "selectionModifier", publicName: "selectionModifier", isSignal: true, isRequired: false, transformFunction: null }, dragThreshold: { classPropertyName: "dragThreshold", publicName: "dragThreshold", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionComplete: "selectionComplete", gridResized: "gridResized" }, host: { properties: { "style.--rows": "store.effectiveRows()", "style.--columns": "store.effectiveColumns()", "style.--gutter-size": "store.gutterSize()", "style.--gutters": "store.effectiveColumns() + 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 -->\n<div class=\"grid-container\">\n @if (editMode()) {\n <!-- Full editor with drag & drop capabilities -->\n <ngx-dashboard-editor\n [rows]=\"store.rows()\"\n [columns]=\"store.columns()\"\n [gutterSize]=\"store.gutterSize()\"\n (gridResized)=\"gridResized.emit($event)\"\n ></ngx-dashboard-editor>\n } @else {\n <!-- Read-only viewer -->\n <ngx-dashboard-viewer\n [rows]=\"store.rows()\"\n [columns]=\"store.columns()\"\n [gutterSize]=\"store.gutterSize()\"\n [enableSelection]=\"enableSelection()\"\n [selectionModifier]=\"selectionModifier()\"\n [dragThreshold]=\"dragThreshold()\"\n (selectionComplete)=\"selectionComplete.emit($event)\"\n ></ngx-dashboard-viewer>\n }\n</div>\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: "component", type: DashboardViewerComponent, selector: "ngx-dashboard-viewer", inputs: ["rows", "columns", "gutterSize", "enableSelection", "selectionModifier", "dragThreshold"], outputs: ["selectionComplete"] }, { kind: "component", type: DashboardEditorComponent, selector: "ngx-dashboard-editor", inputs: ["rows", "columns", "gutterSize"], outputs: ["gridResized"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3281
3603
  }
3282
3604
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.4", ngImport: i0, type: DashboardComponent, decorators: [{
3283
3605
  type: Component,
3284
3606
  args: [{ selector: 'ngx-dashboard', standalone: true, imports: [DashboardViewerComponent, DashboardEditorComponent], providers: [DashboardStore, DashboardViewportService], changeDetection: ChangeDetectionStrategy.OnPush, host: {
3285
- '[style.--rows]': 'store.rows()',
3286
- '[style.--columns]': 'store.columns()',
3607
+ '[style.--rows]': 'store.effectiveRows()',
3608
+ '[style.--columns]': 'store.effectiveColumns()',
3287
3609
  '[style.--gutter-size]': 'store.gutterSize()',
3288
- '[style.--gutters]': 'store.columns() + 1',
3610
+ '[style.--gutters]': 'store.effectiveColumns() + 1',
3289
3611
  '[class.is-edit-mode]': 'editMode()',
3290
3612
  '[style.max-width.px]': 'viewport.constraints().maxWidth',
3291
3613
  '[style.max-height.px]': 'viewport.constraints().maxHeight',
3292
- }, template: "<!-- dashboard.component.html -->\n<div class=\"grid-container\">\n @if (editMode()) {\n <!-- Full editor with drag & drop capabilities -->\n <ngx-dashboard-editor\n [rows]=\"store.rows()\"\n [columns]=\"store.columns()\"\n [gutterSize]=\"store.gutterSize()\"\n ></ngx-dashboard-editor>\n } @else {\n <!-- Read-only viewer -->\n <ngx-dashboard-viewer\n [rows]=\"store.rows()\"\n [columns]=\"store.columns()\"\n [gutterSize]=\"store.gutterSize()\"\n [enableSelection]=\"enableSelection()\"\n [selectionModifier]=\"selectionModifier()\"\n [dragThreshold]=\"dragThreshold()\"\n (selectionComplete)=\"selectionComplete.emit($event)\"\n ></ngx-dashboard-viewer>\n }\n</div>\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"] }]
3293
- }], ctorParameters: () => [], propDecorators: { dashboardData: [{ type: i0.Input, args: [{ isSignal: true, alias: "dashboardData", required: true }] }], editMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "editMode", required: false }] }], reservedSpace: [{ type: i0.Input, args: [{ isSignal: true, alias: "reservedSpace", required: false }] }], enableSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableSelection", required: false }] }], selectionModifier: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionModifier", required: false }] }], dragThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragThreshold", required: false }] }], selectionComplete: [{ type: i0.Output, args: ["selectionComplete"] }], dashboardEditor: [{ type: i0.ViewChild, args: [i0.forwardRef(() => DashboardEditorComponent), { isSignal: true }] }], dashboardViewer: [{ type: i0.ViewChild, args: [i0.forwardRef(() => DashboardViewerComponent), { isSignal: true }] }] } });
3614
+ }, template: "<!-- dashboard.component.html -->\n<div class=\"grid-container\">\n @if (editMode()) {\n <!-- Full editor with drag & drop capabilities -->\n <ngx-dashboard-editor\n [rows]=\"store.rows()\"\n [columns]=\"store.columns()\"\n [gutterSize]=\"store.gutterSize()\"\n (gridResized)=\"gridResized.emit($event)\"\n ></ngx-dashboard-editor>\n } @else {\n <!-- Read-only viewer -->\n <ngx-dashboard-viewer\n [rows]=\"store.rows()\"\n [columns]=\"store.columns()\"\n [gutterSize]=\"store.gutterSize()\"\n [enableSelection]=\"enableSelection()\"\n [selectionModifier]=\"selectionModifier()\"\n [dragThreshold]=\"dragThreshold()\"\n (selectionComplete)=\"selectionComplete.emit($event)\"\n ></ngx-dashboard-viewer>\n }\n</div>\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"] }]
3615
+ }], ctorParameters: () => [], propDecorators: { dashboardData: [{ type: i0.Input, args: [{ isSignal: true, alias: "dashboardData", required: true }] }], editMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "editMode", required: false }] }], reservedSpace: [{ type: i0.Input, args: [{ isSignal: true, alias: "reservedSpace", required: false }] }], enableSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableSelection", required: false }] }], selectionModifier: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionModifier", required: false }] }], dragThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragThreshold", required: false }] }], selectionComplete: [{ type: i0.Output, args: ["selectionComplete"] }], gridResized: [{ type: i0.Output, args: ["gridResized"] }], dashboardEditor: [{ type: i0.ViewChild, args: [i0.forwardRef(() => DashboardEditorComponent), { isSignal: true }] }], dashboardViewer: [{ type: i0.ViewChild, args: [i0.forwardRef(() => DashboardViewerComponent), { isSignal: true }] }] } });
3294
3616
 
3295
3617
  // widget-list.component.ts
3296
3618
  class WidgetListComponent {