@dragonworks/ngx-dashboard 20.0.6 → 20.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.
Files changed (78) hide show
  1. package/fesm2022/dragonworks-ngx-dashboard.mjs +2250 -0
  2. package/fesm2022/dragonworks-ngx-dashboard.mjs.map +1 -0
  3. package/index.d.ts +727 -0
  4. package/package.json +45 -34
  5. package/ng-package.json +0 -7
  6. package/src/lib/__tests__/dashboard-component-widget-state-integration.spec.ts +0 -537
  7. package/src/lib/cell/__tests__/cell-resize.component.spec.ts +0 -442
  8. package/src/lib/cell/__tests__/cell.component.spec.ts +0 -541
  9. package/src/lib/cell/cell-context-menu.component.ts +0 -138
  10. package/src/lib/cell/cell-context-menu.service.ts +0 -36
  11. package/src/lib/cell/cell.component.html +0 -37
  12. package/src/lib/cell/cell.component.scss +0 -198
  13. package/src/lib/cell/cell.component.ts +0 -375
  14. package/src/lib/dashboard/dashboard.component.html +0 -18
  15. package/src/lib/dashboard/dashboard.component.scss +0 -17
  16. package/src/lib/dashboard/dashboard.component.ts +0 -187
  17. package/src/lib/dashboard-editor/dashboard-editor.component.html +0 -57
  18. package/src/lib/dashboard-editor/dashboard-editor.component.scss +0 -87
  19. package/src/lib/dashboard-editor/dashboard-editor.component.ts +0 -219
  20. package/src/lib/dashboard-viewer/__tests__/dashboard-viewer.component.spec.ts +0 -258
  21. package/src/lib/dashboard-viewer/dashboard-viewer.component.html +0 -20
  22. package/src/lib/dashboard-viewer/dashboard-viewer.component.scss +0 -50
  23. package/src/lib/dashboard-viewer/dashboard-viewer.component.ts +0 -70
  24. package/src/lib/drop-zone/__tests__/drop-zone.component.spec.ts +0 -465
  25. package/src/lib/drop-zone/drop-zone.component.html +0 -20
  26. package/src/lib/drop-zone/drop-zone.component.scss +0 -67
  27. package/src/lib/drop-zone/drop-zone.component.ts +0 -122
  28. package/src/lib/internal-widgets/unknown-widget/unknown-widget.component.ts +0 -72
  29. package/src/lib/models/cell-data.ts +0 -13
  30. package/src/lib/models/cell-dialog.ts +0 -7
  31. package/src/lib/models/cell-id.ts +0 -85
  32. package/src/lib/models/cell-position.ts +0 -15
  33. package/src/lib/models/dashboard-data.dto.ts +0 -44
  34. package/src/lib/models/dashboard-data.utils.ts +0 -49
  35. package/src/lib/models/drag-data.ts +0 -6
  36. package/src/lib/models/index.ts +0 -11
  37. package/src/lib/models/reserved-space.ts +0 -24
  38. package/src/lib/models/widget-factory.ts +0 -33
  39. package/src/lib/models/widget-id.ts +0 -70
  40. package/src/lib/models/widget.ts +0 -21
  41. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.component.ts +0 -127
  42. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.provider.ts +0 -15
  43. package/src/lib/providers/cell-settings-dialog/cell-settings-dialog.tokens.ts +0 -20
  44. package/src/lib/providers/cell-settings-dialog/default-cell-settings-dialog.provider.ts +0 -32
  45. package/src/lib/providers/cell-settings-dialog/index.ts +0 -3
  46. package/src/lib/providers/index.ts +0 -1
  47. package/src/lib/services/__tests__/dashboard-bridge.service.spec.ts +0 -220
  48. package/src/lib/services/__tests__/dashboard-viewport.service.spec.ts +0 -362
  49. package/src/lib/services/dashboard-bridge.service.ts +0 -155
  50. package/src/lib/services/dashboard-viewport.service.ts +0 -148
  51. package/src/lib/services/dashboard.service.ts +0 -62
  52. package/src/lib/store/__tests__/dashboard-store-collision-detection.spec.ts +0 -756
  53. package/src/lib/store/__tests__/dashboard-store-computed-properties.spec.ts +0 -974
  54. package/src/lib/store/__tests__/dashboard-store-drag-drop.spec.ts +0 -279
  55. package/src/lib/store/__tests__/dashboard-store-export-import.spec.ts +0 -780
  56. package/src/lib/store/__tests__/dashboard-store-grid-config.spec.ts +0 -128
  57. package/src/lib/store/__tests__/dashboard-store-query-methods.spec.ts +0 -229
  58. package/src/lib/store/__tests__/dashboard-store-resize-operations.spec.ts +0 -652
  59. package/src/lib/store/__tests__/dashboard-store-widget-management.spec.ts +0 -461
  60. package/src/lib/store/__tests__/dashboard-store-widget-state-preservation.spec.ts +0 -369
  61. package/src/lib/store/dashboard-store.ts +0 -239
  62. package/src/lib/store/features/drag-drop.feature.ts +0 -140
  63. package/src/lib/store/features/grid-config.feature.ts +0 -43
  64. package/src/lib/store/features/resize.feature.ts +0 -140
  65. package/src/lib/store/features/utils/collision.utils.ts +0 -89
  66. package/src/lib/store/features/utils/grid-query-internal.utils.ts +0 -37
  67. package/src/lib/store/features/utils/resize.utils.ts +0 -165
  68. package/src/lib/store/features/widget-management.feature.ts +0 -158
  69. package/src/lib/styles/_dashboard-grid-vars.scss +0 -11
  70. package/src/lib/widget-list/__tests__/widget-list-bridge-integration.spec.ts +0 -137
  71. package/src/lib/widget-list/widget-list.component.html +0 -22
  72. package/src/lib/widget-list/widget-list.component.scss +0 -154
  73. package/src/lib/widget-list/widget-list.component.ts +0 -106
  74. package/src/public-api.ts +0 -21
  75. package/src/test-setup.ts +0 -10
  76. package/tsconfig.lib.json +0 -15
  77. package/tsconfig.lib.prod.json +0 -11
  78. package/tsconfig.spec.json +0 -14
@@ -0,0 +1,2250 @@
1
+ import { CommonModule, isPlatformBrowser } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { signal, computed, ChangeDetectionStrategy, Component, Injectable, inject, InjectionToken, input, model, output, viewChild, ViewContainerRef, DestroyRef, Renderer2, effect, viewChildren, ElementRef, afterNextRender, PLATFORM_ID, untracked } from '@angular/core';
4
+ import { signalStoreFeature, withState, withMethods, patchState, withComputed, signalStore, withProps } from '@ngrx/signals';
5
+ import * as i1 from '@angular/material/icon';
6
+ import { MatIconModule } from '@angular/material/icon';
7
+ import * as i2 from '@angular/material/tooltip';
8
+ import { MatTooltipModule } from '@angular/material/tooltip';
9
+ import * as i2$1 from '@angular/material/dialog';
10
+ import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule, MatDialog } from '@angular/material/dialog';
11
+ import { firstValueFrom } from 'rxjs';
12
+ import * as i1$1 from '@angular/forms';
13
+ import { FormsModule } from '@angular/forms';
14
+ import * as i3 from '@angular/material/button';
15
+ import { MatButtonModule } from '@angular/material/button';
16
+ import * as i4 from '@angular/material/radio';
17
+ import { MatRadioModule } from '@angular/material/radio';
18
+ import * as i1$2 from '@angular/material/menu';
19
+ import { MatMenuTrigger, MatMenuModule } from '@angular/material/menu';
20
+ import * as i3$1 from '@angular/material/divider';
21
+ import { MatDividerModule } from '@angular/material/divider';
22
+ import { DomSanitizer } from '@angular/platform-browser';
23
+
24
+ /**
25
+ * Maximum number of columns supported by the grid.
26
+ * This determines the encoding scheme for cell coordinates.
27
+ */
28
+ const MAX_COLUMNS = 1024;
29
+ /**
30
+ * Utility functions for working with CellId branded type.
31
+ */
32
+ const CellIdUtils = {
33
+ /**
34
+ * Creates a CellId from row and column coordinates.
35
+ * @param row - The row number (1-based)
36
+ * @param col - The column number (1-based)
37
+ * @returns A branded CellId that encodes both coordinates
38
+ * @throws Error if row or col is less than 1, or if col exceeds MAX_COLUMNS
39
+ */
40
+ create(row, col) {
41
+ if (row < 1 || col < 1) {
42
+ throw new Error(`Invalid cell coordinates: row=${row}, col=${col}. Both must be >= 1`);
43
+ }
44
+ if (col >= MAX_COLUMNS) {
45
+ throw new Error(`Column ${col} exceeds maximum of ${MAX_COLUMNS - 1}`);
46
+ }
47
+ return (row * MAX_COLUMNS + col);
48
+ },
49
+ /**
50
+ * Decodes a CellId back into row and column coordinates.
51
+ * @param cellId - The CellId to decode
52
+ * @returns A tuple of [row, col] coordinates (1-based)
53
+ */
54
+ decode(cellId) {
55
+ const id = cellId;
56
+ const row = Math.floor(id / MAX_COLUMNS);
57
+ const col = id % MAX_COLUMNS;
58
+ return [row, col];
59
+ },
60
+ /**
61
+ * Gets the row coordinate from a CellId.
62
+ * @param cellId - The CellId to extract row from
63
+ * @returns The row number (1-based)
64
+ */
65
+ getRow(cellId) {
66
+ return Math.floor(cellId / MAX_COLUMNS);
67
+ },
68
+ /**
69
+ * Gets the column coordinate from a CellId.
70
+ * @param cellId - The CellId to extract column from
71
+ * @returns The column number (1-based)
72
+ */
73
+ getCol(cellId) {
74
+ return cellId % MAX_COLUMNS;
75
+ },
76
+ /**
77
+ * Creates a string representation of a CellId for debugging/display purposes.
78
+ * @param cellId - The CellId to convert to string
79
+ * @returns A string in the format "row-col"
80
+ */
81
+ toString(cellId) {
82
+ const [row, col] = this.decode(cellId);
83
+ return `${row}-${col}`;
84
+ },
85
+ /**
86
+ * Checks if two CellIds are equal.
87
+ * @param a - First CellId
88
+ * @param b - Second CellId
89
+ * @returns True if the CellIds represent the same cell
90
+ */
91
+ equals(a, b) {
92
+ return a === b;
93
+ },
94
+ };
95
+
96
+ /**
97
+ * Utility functions for working with WidgetId branded type.
98
+ * WidgetIds are UUIDs that uniquely identify widget instances throughout their lifecycle,
99
+ * independent of their position on the dashboard grid.
100
+ */
101
+ const WidgetIdUtils = {
102
+ /**
103
+ * Generates a new unique WidgetId.
104
+ * Uses crypto.randomUUID() when available, falls back to timestamp-based ID for older browsers.
105
+ * @returns A new unique WidgetId
106
+ */
107
+ generate() {
108
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
109
+ return crypto.randomUUID();
110
+ }
111
+ // Fallback for older browsers
112
+ return `widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
113
+ },
114
+ /**
115
+ * Validates if a string is a valid WidgetId format.
116
+ * @param id - The string to validate
117
+ * @returns True if the string is a valid WidgetId format
118
+ */
119
+ validate(id) {
120
+ // UUID v4 format or fallback format
121
+ return (/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id) ||
122
+ /^widget-\d+-[a-z0-9]{9}$/i.test(id));
123
+ },
124
+ /**
125
+ * Converts a WidgetId to a string for use as map keys or serialization.
126
+ * @param id - The WidgetId to convert
127
+ * @returns The string representation of the WidgetId
128
+ */
129
+ toString(id) {
130
+ return id;
131
+ },
132
+ /**
133
+ * Creates a WidgetId from a string, validating the format.
134
+ * @param str - The string to convert to WidgetId
135
+ * @returns A WidgetId
136
+ * @throws Error if the string is not a valid WidgetId format
137
+ */
138
+ fromString(str) {
139
+ if (!this.validate(str)) {
140
+ throw new Error(`Invalid WidgetId format: ${str}`);
141
+ }
142
+ return str;
143
+ },
144
+ /**
145
+ * Checks if two WidgetIds are equal.
146
+ * @param a - First WidgetId
147
+ * @param b - Second WidgetId
148
+ * @returns True if the WidgetIds are the same
149
+ */
150
+ equals(a, b) {
151
+ return a === b;
152
+ },
153
+ };
154
+
155
+ /**
156
+ * Creates an empty dashboard configuration with the specified dimensions.
157
+ * This is a convenience function for creating a basic dashboard without any cells.
158
+ *
159
+ * @param dashboardId - Unique identifier for the dashboard (managed by client)
160
+ * @param rows - Number of rows in the dashboard grid
161
+ * @param columns - Number of columns in the dashboard grid
162
+ * @param gutterSize - CSS size for the gutter between cells (default: '0.5em')
163
+ * @returns A DashboardDataDto configured with the specified dimensions and no cells
164
+ *
165
+ * @example
166
+ * // Create an 8x16 dashboard with default gutter
167
+ * const dashboard = createEmptyDashboard('my-dashboard-1', 8, 16);
168
+ *
169
+ * @example
170
+ * // Create a 5x10 dashboard with custom gutter
171
+ * const dashboard = createEmptyDashboard('my-dashboard-2', 5, 10, '0.5rem');
172
+ */
173
+ function createEmptyDashboard(dashboardId, rows, columns, gutterSize = '0.5em') {
174
+ return {
175
+ version: '1.0.0',
176
+ dashboardId,
177
+ rows,
178
+ columns,
179
+ gutterSize,
180
+ cells: [],
181
+ };
182
+ }
183
+ /**
184
+ * Creates a default dashboard configuration with standard dimensions.
185
+ * This provides a reasonable starting point for most use cases.
186
+ *
187
+ * @param dashboardId - Unique identifier for the dashboard (managed by client)
188
+ * @returns A DashboardDataDto with 8 rows, 16 columns, and 0.5em gutter
189
+ *
190
+ * @example
191
+ * const dashboard = createDefaultDashboard('my-dashboard-id');
192
+ */
193
+ function createDefaultDashboard(dashboardId) {
194
+ return createEmptyDashboard(dashboardId, 8, 16, '0.5em');
195
+ }
196
+
197
+ /**
198
+ * Default reserved space when none is specified
199
+ */
200
+ const DEFAULT_RESERVED_SPACE = {
201
+ top: 0,
202
+ right: 0,
203
+ bottom: 0,
204
+ left: 0
205
+ };
206
+
207
+ function createFactoryFromComponent(component) {
208
+ return {
209
+ ...component.metadata,
210
+ createInstance(container, state) {
211
+ const ref = container.createComponent(component);
212
+ ref.instance.dashboardSetState?.(state);
213
+ return ref;
214
+ },
215
+ };
216
+ }
217
+
218
+ class UnknownWidgetComponent {
219
+ static metadata = {
220
+ widgetTypeid: '__internal/unknown-widget',
221
+ name: 'Unknown Widget',
222
+ description: 'Fallback widget for unknown widget types',
223
+ svgIcon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M480-280q17 0 28.5-11.5T520-320q0-17-11.5-28.5T480-360q-17 0-28.5 11.5T440-320q0 17 11.5 28.5T480-280Zm-40-160h80v-240h-80v240Zm40 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z"/></svg>',
224
+ };
225
+ state = signal({
226
+ originalWidgetTypeid: 'unknown',
227
+ }, ...(ngDevMode ? [{ debugName: "state" }] : []));
228
+ tooltipText = computed(() => `${this.state().originalWidgetTypeid}`, ...(ngDevMode ? [{ debugName: "tooltipText" }] : []));
229
+ dashboardSetState(state) {
230
+ if (state && typeof state === 'object' && 'originalWidgetTypeid' in state) {
231
+ this.state.set(state);
232
+ }
233
+ }
234
+ dashboardGetState() {
235
+ return this.state();
236
+ }
237
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: UnknownWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
238
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.1", type: UnknownWidgetComponent, isStandalone: true, selector: "lib-unknown-widget", ngImport: i0, template: `
239
+ <div class="unknown-widget-container" [matTooltip]="tooltipText()">
240
+ <mat-icon class="unknown-widget-icon">error_outline</mat-icon>
241
+ </div>
242
+ `, isInline: true, styles: [".unknown-widget-container{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background-color:var(--mat-sys-error);border-radius:8px;container-type:size}.unknown-widget-icon{color:var(--mat-sys-on-error);font-size:clamp(12px,75cqmin,68px);width:clamp(12px,75cqmin,68px);height:clamp(12px,75cqmin,68px)}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
243
+ }
244
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: UnknownWidgetComponent, decorators: [{
245
+ type: Component,
246
+ args: [{ selector: 'lib-unknown-widget', imports: [MatIconModule, MatTooltipModule], template: `
247
+ <div class="unknown-widget-container" [matTooltip]="tooltipText()">
248
+ <mat-icon class="unknown-widget-icon">error_outline</mat-icon>
249
+ </div>
250
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".unknown-widget-container{display:flex;align-items:center;justify-content:center;width:100%;height:100%;background-color:var(--mat-sys-error);border-radius:8px;container-type:size}.unknown-widget-icon{color:var(--mat-sys-on-error);font-size:clamp(12px,75cqmin,68px);width:clamp(12px,75cqmin,68px);height:clamp(12px,75cqmin,68px)}\n"] }]
251
+ }] });
252
+
253
+ // dashboard.service.ts
254
+ class DashboardService {
255
+ #widgetTypes = signal([], ...(ngDevMode ? [{ debugName: "#widgetTypes" }] : []));
256
+ #widgetFactoryMap = new Map();
257
+ #unknownWidgetFactory = createFactoryFromComponent(UnknownWidgetComponent);
258
+ widgetTypes = this.#widgetTypes.asReadonly(); // make the widget list available as a readonly signal
259
+ registerWidgetType(widget) {
260
+ if (this.#widgetTypes().some((w) => w.metadata.widgetTypeid === widget.metadata.widgetTypeid)) {
261
+ throw new Error(`Widget type '${widget.metadata.widgetTypeid}' is already registered`);
262
+ }
263
+ this.#widgetFactoryMap.set(widget.metadata.widgetTypeid, createFactoryFromComponent(widget));
264
+ this.#widgetTypes.set([...this.#widgetTypes(), widget]);
265
+ }
266
+ getFactory(widgetTypeid) {
267
+ const factory = this.#widgetFactoryMap.get(widgetTypeid);
268
+ if (factory) {
269
+ return factory;
270
+ }
271
+ // Return fallback factory for unknown widget types
272
+ console.warn(`Unknown widget type: ${widgetTypeid}, using fallback error widget`);
273
+ // Create a custom factory that preserves the original widget type ID in state
274
+ return {
275
+ ...this.#unknownWidgetFactory,
276
+ createInstance: (container, state) => {
277
+ const ref = this.#unknownWidgetFactory.createInstance(container, {
278
+ originalWidgetTypeid: widgetTypeid,
279
+ ...(state && typeof state === 'object' ? state : {}),
280
+ });
281
+ return ref;
282
+ },
283
+ };
284
+ }
285
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
286
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardService, providedIn: 'root' });
287
+ }
288
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardService, decorators: [{
289
+ type: Injectable,
290
+ args: [{
291
+ providedIn: 'root',
292
+ }]
293
+ }] });
294
+
295
+ // Internal utility functions used by collision detection, resize logic, and tests
296
+ const GridQueryInternalUtils = {
297
+ isCellOccupied(cells, row, col, excludeWidgetId) {
298
+ return cells.some((cell) => {
299
+ // Skip checking against the widget being dragged (use widgetId for stable identity)
300
+ if (excludeWidgetId && cell.widgetId === excludeWidgetId)
301
+ return false;
302
+ const endRow = cell.row + cell.rowSpan - 1;
303
+ const endCol = cell.col + cell.colSpan - 1;
304
+ return (cell.row <= row && row <= endRow && cell.col <= col && col <= endCol);
305
+ });
306
+ },
307
+ isOutOfBounds(targetRow, targetCol, spanRow, spanCol, maxRows, maxColumns) {
308
+ const rowLimit = targetRow + spanRow - 1;
309
+ const colLimit = targetCol + spanCol - 1;
310
+ return rowLimit > maxRows || colLimit > maxColumns;
311
+ },
312
+ getCellAt(cells, row, col) {
313
+ return cells.find((cell) => cell.row === row && cell.col === col) ?? null;
314
+ },
315
+ };
316
+
317
+ function calculateCollisionInfo(dragData, hovered, cells, rows, columns) {
318
+ if (!dragData || !hovered) {
319
+ return {
320
+ hasCollisions: false,
321
+ invalidCells: [],
322
+ outOfBounds: false,
323
+ footprint: [],
324
+ };
325
+ }
326
+ const isCell = dragData.kind === 'cell';
327
+ const rowSpan = isCell ? dragData.content.rowSpan : 1;
328
+ const colSpan = isCell ? dragData.content.colSpan : 1;
329
+ // Check bounds
330
+ const outOfBounds = GridQueryInternalUtils.isOutOfBounds(hovered.row, hovered.col, rowSpan, colSpan, rows, columns);
331
+ // Generate footprint
332
+ const footprint = [];
333
+ for (let r = 0; r < rowSpan; r++) {
334
+ for (let c = 0; c < colSpan; c++) {
335
+ footprint.push({ row: hovered.row + r, col: hovered.col + c });
336
+ }
337
+ }
338
+ const excludeWidgetId = isCell ? dragData.content.widgetId : undefined;
339
+ // Check for actual collisions with other widgets (not self)
340
+ const hasCollisions = footprint.some((pos) => GridQueryInternalUtils.isCellOccupied(cells, pos.row, pos.col, excludeWidgetId));
341
+ // Generate invalid cell IDs
342
+ const invalidCells = [];
343
+ if (hasCollisions || outOfBounds) {
344
+ for (const pos of footprint) {
345
+ invalidCells.push(CellIdUtils.create(pos.row, pos.col));
346
+ }
347
+ }
348
+ return {
349
+ hasCollisions,
350
+ invalidCells,
351
+ outOfBounds,
352
+ footprint,
353
+ };
354
+ }
355
+ function calculateHighlightedZones(dragData, hovered) {
356
+ if (!dragData || !hovered)
357
+ return [];
358
+ const zones = [];
359
+ const rowSpan = dragData.kind === 'cell' ? dragData.content.rowSpan : 1;
360
+ const colSpan = dragData.kind === 'cell' ? dragData.content.colSpan : 1;
361
+ for (let r = 0; r < rowSpan; r++) {
362
+ for (let c = 0; c < colSpan; c++) {
363
+ zones.push({ row: hovered.row + r, col: hovered.col + c });
364
+ }
365
+ }
366
+ return zones;
367
+ }
368
+
369
+ const initialGridConfigState = {
370
+ rows: 8,
371
+ columns: 16,
372
+ gutterSize: '0.5em',
373
+ isEditMode: false,
374
+ gridCellDimensions: { width: 0, height: 0 },
375
+ };
376
+ const withGridConfig = () => signalStoreFeature(withState(initialGridConfigState), withMethods((store) => ({
377
+ setGridConfig(config) {
378
+ patchState(store, config);
379
+ },
380
+ setGridCellDimensions(width, height) {
381
+ patchState(store, { gridCellDimensions: { width, height } });
382
+ },
383
+ toggleEditMode() {
384
+ patchState(store, { isEditMode: !store.isEditMode() });
385
+ },
386
+ setEditMode(isEditMode) {
387
+ patchState(store, { isEditMode });
388
+ },
389
+ })));
390
+
391
+ const initialWidgetManagementState = {
392
+ widgetsById: {},
393
+ };
394
+ const withWidgetManagement = () => signalStoreFeature(withState(initialWidgetManagementState),
395
+ // Computed cells array - lazy evaluation, automatic memoization
396
+ withComputed((store) => ({
397
+ cells: computed(() => Object.values(store.widgetsById())),
398
+ })), withMethods((store) => ({
399
+ addWidget(cell) {
400
+ const widgetKey = WidgetIdUtils.toString(cell.widgetId);
401
+ patchState(store, {
402
+ widgetsById: { ...store.widgetsById(), [widgetKey]: cell },
403
+ });
404
+ },
405
+ removeWidget(widgetId) {
406
+ const widgetKey = WidgetIdUtils.toString(widgetId);
407
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
408
+ const { [widgetKey]: _, ...remaining } = store.widgetsById();
409
+ patchState(store, { widgetsById: remaining });
410
+ },
411
+ updateWidgetPosition(widgetId, row, col) {
412
+ const widgetKey = WidgetIdUtils.toString(widgetId);
413
+ const existingWidget = store.widgetsById()[widgetKey];
414
+ if (existingWidget) {
415
+ // Update position and recalculate cellId based on new position
416
+ const newCellId = CellIdUtils.create(row, col);
417
+ patchState(store, {
418
+ widgetsById: {
419
+ ...store.widgetsById(),
420
+ [widgetKey]: { ...existingWidget, row, col, cellId: newCellId },
421
+ },
422
+ });
423
+ }
424
+ },
425
+ createWidget(row, col, widgetFactory, widgetState) {
426
+ const widgetId = WidgetIdUtils.generate(); // Generate unique widget ID
427
+ const cellId = CellIdUtils.create(row, col); // Calculate position-based cell ID
428
+ const cell = {
429
+ widgetId,
430
+ cellId,
431
+ row,
432
+ col,
433
+ rowSpan: 1,
434
+ colSpan: 1,
435
+ widgetFactory,
436
+ widgetState,
437
+ };
438
+ const widgetKey = WidgetIdUtils.toString(widgetId);
439
+ patchState(store, {
440
+ widgetsById: { ...store.widgetsById(), [widgetKey]: cell },
441
+ });
442
+ },
443
+ updateCellSettings(widgetId, flat) {
444
+ const widgetKey = WidgetIdUtils.toString(widgetId);
445
+ const existingWidget = store.widgetsById()[widgetKey];
446
+ if (existingWidget) {
447
+ patchState(store, {
448
+ widgetsById: {
449
+ ...store.widgetsById(),
450
+ [widgetKey]: { ...existingWidget, flat },
451
+ },
452
+ });
453
+ }
454
+ },
455
+ updateWidgetSpan(widgetId, rowSpan, colSpan) {
456
+ const widgetKey = WidgetIdUtils.toString(widgetId);
457
+ const existingWidget = store.widgetsById()[widgetKey];
458
+ if (existingWidget) {
459
+ patchState(store, {
460
+ widgetsById: {
461
+ ...store.widgetsById(),
462
+ [widgetKey]: { ...existingWidget, rowSpan, colSpan },
463
+ },
464
+ });
465
+ }
466
+ },
467
+ updateWidgetState(widgetId, widgetState) {
468
+ const widgetKey = WidgetIdUtils.toString(widgetId);
469
+ const existingWidget = store.widgetsById()[widgetKey];
470
+ if (existingWidget) {
471
+ patchState(store, {
472
+ widgetsById: {
473
+ ...store.widgetsById(),
474
+ [widgetKey]: { ...existingWidget, widgetState },
475
+ },
476
+ });
477
+ }
478
+ },
479
+ updateAllWidgetStates(cellStates) {
480
+ const updatedWidgetsById = { ...store.widgetsById() };
481
+ // Convert cell ID keys to widget IDs and update states
482
+ for (const [cellIdString, newState] of cellStates) {
483
+ // Find the widget with the matching cell ID
484
+ const widget = Object.values(updatedWidgetsById).find(w => CellIdUtils.toString(w.cellId) === cellIdString);
485
+ if (widget) {
486
+ const widgetIdString = WidgetIdUtils.toString(widget.widgetId);
487
+ updatedWidgetsById[widgetIdString] = {
488
+ ...updatedWidgetsById[widgetIdString],
489
+ widgetState: newState,
490
+ };
491
+ }
492
+ }
493
+ patchState(store, { widgetsById: updatedWidgetsById });
494
+ },
495
+ clearDashboard() {
496
+ patchState(store, { widgetsById: {} });
497
+ },
498
+ })));
499
+
500
+ const initialDragDropState = {
501
+ dragData: null,
502
+ hoveredDropZone: null,
503
+ };
504
+ const withDragDrop = () => signalStoreFeature(withState(initialDragDropState), withComputed((store) => ({
505
+ // Highlighted zones during drag
506
+ highlightedZones: computed(() => calculateHighlightedZones(store.dragData(), store.hoveredDropZone())),
507
+ })), withComputed((store) => ({
508
+ // Map for quick highlight lookup - reuse highlightedZones computation
509
+ highlightMap: computed(() => {
510
+ const zones = store.highlightedZones();
511
+ const map = new Set();
512
+ for (const z of zones) {
513
+ map.add(CellIdUtils.create(z.row, z.col));
514
+ }
515
+ return map;
516
+ }),
517
+ })), withMethods((store) => ({
518
+ startDrag(dragData) {
519
+ patchState(store, { dragData });
520
+ },
521
+ endDrag() {
522
+ patchState(store, {
523
+ dragData: null,
524
+ hoveredDropZone: null,
525
+ });
526
+ },
527
+ setHoveredDropZone(zone) {
528
+ patchState(store, { hoveredDropZone: zone });
529
+ },
530
+ })),
531
+ // Second withMethods block for drop handling that can access endDrag
532
+ withMethods((store) => ({
533
+ // Drop handling logic with dependency injection
534
+ _handleDrop(dragData, targetPosition, dependencies) {
535
+ // 1. Validate placement using existing collision detection
536
+ const collisionInfo = calculateCollisionInfo(dragData, targetPosition, dependencies.cells, dependencies.rows, dependencies.columns);
537
+ // 2. End drag state first
538
+ store.endDrag();
539
+ // 3. Early return if invalid placement
540
+ if (collisionInfo.hasCollisions || collisionInfo.outOfBounds) {
541
+ return false;
542
+ }
543
+ // 4. Handle widget creation from palette
544
+ if (dragData.kind === 'widget') {
545
+ const factory = dependencies.dashboardService.getFactory(dragData.content.widgetTypeid);
546
+ dependencies.createWidget(targetPosition.row, targetPosition.col, factory, undefined);
547
+ return true;
548
+ }
549
+ // 5. Handle cell movement
550
+ if (dragData.kind === 'cell') {
551
+ dependencies.updateWidgetPosition(dragData.content.widgetId, // Use widgetId instead of cellId
552
+ targetPosition.row, targetPosition.col);
553
+ return true;
554
+ }
555
+ return false;
556
+ },
557
+ })));
558
+
559
+ function getMaxColSpan(cellId, row, col, cells, columns) {
560
+ const currentCell = cells.find((c) => CellIdUtils.equals(c.cellId, cellId));
561
+ if (!currentCell)
562
+ return 1;
563
+ // Start from current position and check each column until we hit a boundary or collision
564
+ let maxSpan = 1;
565
+ for (let testCol = col + 1; testCol <= columns; testCol++) {
566
+ // Check if this column is free for all rows the widget spans
567
+ let columnIsFree = true;
568
+ for (let testRow = row; testRow < row + currentCell.rowSpan; testRow++) {
569
+ const occupied = cells.some((cell) => {
570
+ if (CellIdUtils.equals(cell.cellId, cellId))
571
+ return false;
572
+ const wStartCol = cell.col;
573
+ const wEndCol = cell.col + cell.colSpan - 1;
574
+ const wStartRow = cell.row;
575
+ const wEndRow = cell.row + cell.rowSpan - 1;
576
+ return (testCol >= wStartCol &&
577
+ testCol <= wEndCol &&
578
+ testRow >= wStartRow &&
579
+ testRow <= wEndRow);
580
+ });
581
+ if (occupied) {
582
+ columnIsFree = false;
583
+ break;
584
+ }
585
+ }
586
+ if (!columnIsFree) {
587
+ break; // Hit a collision, stop here
588
+ }
589
+ maxSpan = testCol - col + 1; // Update max span to include this column
590
+ }
591
+ return maxSpan;
592
+ }
593
+ function getMaxRowSpan(cellId, row, col, cells, rows) {
594
+ const currentCell = cells.find((c) => CellIdUtils.equals(c.cellId, cellId));
595
+ if (!currentCell)
596
+ return 1;
597
+ // Start from current position and check each row until we hit a boundary or collision
598
+ let maxSpan = 1;
599
+ for (let testRow = row + 1; testRow <= rows; testRow++) {
600
+ // Check if this row is free for all columns the widget spans
601
+ let rowIsFree = true;
602
+ for (let testCol = col; testCol < col + currentCell.colSpan; testCol++) {
603
+ const occupied = cells.some((cell) => {
604
+ if (CellIdUtils.equals(cell.cellId, cellId))
605
+ return false;
606
+ const wStartRow = cell.row;
607
+ const wEndRow = cell.row + cell.rowSpan - 1;
608
+ const wStartCol = cell.col;
609
+ const wEndCol = cell.col + cell.colSpan - 1;
610
+ return (testRow >= wStartRow &&
611
+ testRow <= wEndRow &&
612
+ testCol >= wStartCol &&
613
+ testCol <= wEndCol);
614
+ });
615
+ if (occupied) {
616
+ rowIsFree = false;
617
+ break;
618
+ }
619
+ }
620
+ if (!rowIsFree) {
621
+ break; // Hit a collision, stop here
622
+ }
623
+ maxSpan = testRow - row + 1; // Update max span to include this row
624
+ }
625
+ return maxSpan;
626
+ }
627
+ function calculateResizePreview(resizeData, direction, delta, cells, rows, columns) {
628
+ const cell = cells.find((c) => CellIdUtils.equals(c.cellId, resizeData.cellId));
629
+ if (!cell)
630
+ return null;
631
+ if (direction === 'horizontal') {
632
+ // Calculate the desired span based on the delta
633
+ const desiredColSpan = Math.max(1, resizeData.originalColSpan + delta);
634
+ // Get the maximum allowed span
635
+ const maxColSpan = getMaxColSpan(cell.cellId, cell.row, cell.col, cells, columns);
636
+ // Clamp to the maximum
637
+ const newColSpan = Math.min(desiredColSpan, maxColSpan);
638
+ return {
639
+ rowSpan: resizeData.previewRowSpan,
640
+ colSpan: newColSpan,
641
+ };
642
+ }
643
+ else {
644
+ // Calculate the desired span based on the delta
645
+ const desiredRowSpan = Math.max(1, resizeData.originalRowSpan + delta);
646
+ // Get the maximum allowed span
647
+ const maxRowSpan = getMaxRowSpan(cell.cellId, cell.row, cell.col, cells, rows);
648
+ // Clamp to the maximum
649
+ const newRowSpan = Math.min(desiredRowSpan, maxRowSpan);
650
+ return {
651
+ rowSpan: newRowSpan,
652
+ colSpan: resizeData.previewColSpan,
653
+ };
654
+ }
655
+ }
656
+
657
+ const initialResizeState = {
658
+ resizeData: null,
659
+ };
660
+ // Utility functions for resize preview computations
661
+ const ResizePreviewUtils = {
662
+ computePreviewCells(resizeData, cells) {
663
+ if (!resizeData)
664
+ return [];
665
+ const cell = cells.find((cell) => CellIdUtils.equals(cell.cellId, resizeData.cellId));
666
+ if (!cell)
667
+ return [];
668
+ const previewCells = [];
669
+ for (let r = 0; r < resizeData.previewRowSpan; r++) {
670
+ for (let c = 0; c < resizeData.previewColSpan; c++) {
671
+ previewCells.push({
672
+ row: cell.row + r,
673
+ col: cell.col + c,
674
+ });
675
+ }
676
+ }
677
+ return previewCells;
678
+ },
679
+ computePreviewMap(previewCells) {
680
+ const map = new Set();
681
+ for (const cell of previewCells) {
682
+ map.add(CellIdUtils.create(cell.row, cell.col));
683
+ }
684
+ return map;
685
+ },
686
+ };
687
+ const withResize = () => signalStoreFeature(withState(initialResizeState), withMethods((store) => ({
688
+ // Resize methods that need cross-feature dependencies
689
+ _startResize(cellId, dependencies) {
690
+ const cell = dependencies.cells.find((c) => CellIdUtils.equals(c.cellId, cellId));
691
+ if (!cell)
692
+ return;
693
+ patchState(store, {
694
+ resizeData: {
695
+ cellId,
696
+ originalRowSpan: cell.rowSpan,
697
+ originalColSpan: cell.colSpan,
698
+ previewRowSpan: cell.rowSpan,
699
+ previewColSpan: cell.colSpan,
700
+ },
701
+ });
702
+ },
703
+ _updateResizePreview(direction, delta, dependencies) {
704
+ const resizeData = store.resizeData();
705
+ if (!resizeData)
706
+ return;
707
+ const newSpans = calculateResizePreview(resizeData, direction, delta, dependencies.cells, dependencies.rows, dependencies.columns);
708
+ if (newSpans) {
709
+ patchState(store, {
710
+ resizeData: {
711
+ ...resizeData,
712
+ previewRowSpan: newSpans.rowSpan,
713
+ previewColSpan: newSpans.colSpan,
714
+ },
715
+ });
716
+ }
717
+ },
718
+ _endResize(apply, dependencies) {
719
+ const resizeData = store.resizeData();
720
+ if (!resizeData)
721
+ return;
722
+ if (apply &&
723
+ (resizeData.previewRowSpan !== resizeData.originalRowSpan ||
724
+ resizeData.previewColSpan !== resizeData.originalColSpan)) {
725
+ dependencies.updateWidgetSpan(resizeData.cellId, resizeData.previewRowSpan, resizeData.previewColSpan);
726
+ }
727
+ patchState(store, { resizeData: null });
728
+ },
729
+ })));
730
+
731
+ const initialState = {
732
+ dashboardId: '',
733
+ };
734
+ const DashboardStore = signalStore(withState(initialState), withProps(() => ({
735
+ dashboardService: inject(DashboardService),
736
+ })), withGridConfig(), withWidgetManagement(), withResize(), withDragDrop(),
737
+ // Cross-feature computed properties (need access to multiple features)
738
+ withComputed((store) => ({
739
+ // Invalid zones (collision detection)
740
+ invalidHighlightMap: computed(() => {
741
+ const collisionInfo = calculateCollisionInfo(store.dragData(), store.hoveredDropZone(), store.cells(), store.rows(), store.columns());
742
+ return new Set(collisionInfo.invalidCells);
743
+ }),
744
+ // Check if placement would be valid (for drop validation)
745
+ isValidPlacement: computed(() => {
746
+ const collisionInfo = calculateCollisionInfo(store.dragData(), store.hoveredDropZone(), store.cells(), store.rows(), store.columns());
747
+ return !collisionInfo.hasCollisions && !collisionInfo.outOfBounds;
748
+ }),
749
+ })),
750
+ // Cross-feature methods (need access to multiple features)
751
+ withMethods((store) => ({
752
+ // DROP HANDLING (delegate to drag-drop feature with dependency injection)
753
+ handleDrop(dragData, targetPosition) {
754
+ return store._handleDrop(dragData, targetPosition, {
755
+ cells: store.cells(),
756
+ rows: store.rows(),
757
+ columns: store.columns(),
758
+ dashboardService: store.dashboardService,
759
+ createWidget: store.createWidget,
760
+ updateWidgetPosition: store.updateWidgetPosition,
761
+ });
762
+ },
763
+ // RESIZE METHODS (delegate to resize feature with dependency injection)
764
+ startResize(cellId) {
765
+ store._startResize(cellId, {
766
+ cells: store.cells(),
767
+ });
768
+ },
769
+ updateResizePreview(direction, delta) {
770
+ store._updateResizePreview(direction, delta, {
771
+ cells: store.cells(),
772
+ rows: store.rows(),
773
+ columns: store.columns(),
774
+ });
775
+ },
776
+ endResize(apply) {
777
+ store._endResize(apply, {
778
+ updateWidgetSpan: (cellId, rowSpan, colSpan) => {
779
+ // Adapter: find widget by cellId and update using widgetId
780
+ const widget = store.cells().find(c => CellIdUtils.equals(c.cellId, cellId));
781
+ if (widget) {
782
+ store.updateWidgetSpan(widget.widgetId, rowSpan, colSpan);
783
+ }
784
+ }
785
+ });
786
+ },
787
+ // EXPORT/IMPORT METHODS (need access to multiple features)
788
+ exportDashboard(getCurrentWidgetStates) {
789
+ // Get live widget states if callback provided, otherwise use stored states
790
+ const liveWidgetStates = getCurrentWidgetStates?.() || new Map();
791
+ return {
792
+ version: '1.0.0',
793
+ dashboardId: store.dashboardId(),
794
+ rows: store.rows(),
795
+ columns: store.columns(),
796
+ gutterSize: store.gutterSize(),
797
+ cells: store.cells()
798
+ .filter((cell) => cell.widgetFactory.widgetTypeid !== '__internal/unknown-widget')
799
+ .map((cell) => {
800
+ const cellIdString = CellIdUtils.toString(cell.cellId);
801
+ const currentState = liveWidgetStates.get(cellIdString);
802
+ return {
803
+ row: cell.row,
804
+ col: cell.col,
805
+ rowSpan: cell.rowSpan,
806
+ colSpan: cell.colSpan,
807
+ flat: cell.flat,
808
+ widgetTypeid: cell.widgetFactory.widgetTypeid,
809
+ widgetState: currentState !== undefined ? currentState : cell.widgetState,
810
+ };
811
+ }),
812
+ };
813
+ },
814
+ loadDashboard(data) {
815
+ // Import full dashboard data with grid configuration
816
+ const widgetsById = {};
817
+ data.cells.forEach((cellData) => {
818
+ const factory = store.dashboardService.getFactory(cellData.widgetTypeid);
819
+ const widgetId = WidgetIdUtils.generate();
820
+ const cell = {
821
+ widgetId,
822
+ cellId: CellIdUtils.create(cellData.row, cellData.col),
823
+ row: cellData.row,
824
+ col: cellData.col,
825
+ rowSpan: cellData.rowSpan,
826
+ colSpan: cellData.colSpan,
827
+ flat: cellData.flat,
828
+ widgetFactory: factory,
829
+ widgetState: cellData.widgetState,
830
+ };
831
+ widgetsById[WidgetIdUtils.toString(widgetId)] = cell;
832
+ });
833
+ patchState(store, {
834
+ dashboardId: data.dashboardId,
835
+ rows: data.rows,
836
+ columns: data.columns,
837
+ gutterSize: data.gutterSize,
838
+ widgetsById,
839
+ });
840
+ },
841
+ // INITIALIZATION METHODS
842
+ initializeFromDto(dashboardData) {
843
+ // Inline the loadDashboard logic since it's defined later in the same methods block
844
+ const widgetsById = {};
845
+ dashboardData.cells.forEach((cellData) => {
846
+ const factory = store.dashboardService.getFactory(cellData.widgetTypeid);
847
+ const widgetId = WidgetIdUtils.generate();
848
+ const cell = {
849
+ widgetId,
850
+ cellId: CellIdUtils.create(cellData.row, cellData.col),
851
+ row: cellData.row,
852
+ col: cellData.col,
853
+ rowSpan: cellData.rowSpan,
854
+ colSpan: cellData.colSpan,
855
+ flat: cellData.flat,
856
+ widgetFactory: factory,
857
+ widgetState: cellData.widgetState,
858
+ };
859
+ widgetsById[WidgetIdUtils.toString(widgetId)] = cell;
860
+ });
861
+ patchState(store, {
862
+ dashboardId: dashboardData.dashboardId,
863
+ rows: dashboardData.rows,
864
+ columns: dashboardData.columns,
865
+ gutterSize: dashboardData.gutterSize,
866
+ widgetsById,
867
+ });
868
+ },
869
+ })),
870
+ // Cross-feature computed properties that depend on resize + widget data (using utility functions)
871
+ withComputed((store) => ({
872
+ // Compute preview cells during resize using utility function
873
+ resizePreviewCells: computed(() => {
874
+ return ResizePreviewUtils.computePreviewCells(store.resizeData(), store.cells());
875
+ }),
876
+ })),
877
+ // Second computed block that depends on the first
878
+ withComputed((store) => ({
879
+ // Map for resize preview highlighting using utility function
880
+ resizePreviewMap: computed(() => {
881
+ return ResizePreviewUtils.computePreviewMap(store.resizePreviewCells());
882
+ }),
883
+ })));
884
+
885
+ /**
886
+ * Abstract provider for cell settings dialogs.
887
+ * Implement this to provide custom dialog solutions.
888
+ */
889
+ class CellSettingsDialogProvider {
890
+ }
891
+
892
+ class CellSettingsDialogComponent {
893
+ selectedMode;
894
+ currentMode;
895
+ data = inject(MAT_DIALOG_DATA);
896
+ dialogRef = inject((MatDialogRef));
897
+ constructor() {
898
+ this.currentMode = this.data.flat ? 'flat' : 'normal';
899
+ this.selectedMode = this.currentMode;
900
+ }
901
+ onCancel() {
902
+ this.dialogRef.close();
903
+ }
904
+ save() {
905
+ const newData = {
906
+ ...this.data,
907
+ flat: this.selectedMode === 'flat'
908
+ };
909
+ this.dialogRef.close(newData);
910
+ }
911
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellSettingsDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
912
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.1", type: CellSettingsDialogComponent, isStandalone: true, selector: "lib-cell-settings-dialog", ngImport: i0, template: `
913
+ <h2 mat-dialog-title>Cell Display Settings</h2>
914
+ <mat-dialog-content>
915
+ <p class="cell-info">Cell ID: <strong>{{ data.id }}</strong></p>
916
+
917
+ <div class="radio-group">
918
+ <mat-radio-group [(ngModel)]="selectedMode" name="displayMode">
919
+ <mat-radio-button value="normal">
920
+ <div class="radio-option">
921
+ <div class="option-title">Normal</div>
922
+ <div class="option-description">Standard cell display with full content visibility</div>
923
+ </div>
924
+ </mat-radio-button>
925
+
926
+ <mat-radio-button value="flat">
927
+ <div class="radio-option">
928
+ <div class="option-title">Flat</div>
929
+ <div class="option-description">Simplified display with reduced visual emphasis</div>
930
+ </div>
931
+ </mat-radio-button>
932
+ </mat-radio-group>
933
+ </div>
934
+ </mat-dialog-content>
935
+ <mat-dialog-actions align="end">
936
+ <button mat-button (click)="onCancel()">Cancel</button>
937
+ <button
938
+ mat-flat-button
939
+ (click)="save()"
940
+ [disabled]="selectedMode === currentMode">
941
+ Apply
942
+ </button>
943
+ </mat-dialog-actions>
944
+ `, isInline: true, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden;padding-top:.5rem}.cell-info{margin:0 0 1.5rem;padding-bottom:1rem}.radio-group{width:100%}mat-radio-group{display:block}mat-radio-button{width:100%;display:block;margin-bottom:1rem}mat-radio-button:last-child{margin-bottom:0}.radio-option{margin-left:.75rem;padding:.25rem 0}.option-title{display:block;margin-bottom:.25rem}.option-description{display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i2$1.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i2$1.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i2$1.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i4.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i4.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }] });
945
+ }
946
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellSettingsDialogComponent, decorators: [{
947
+ type: Component,
948
+ args: [{ selector: 'lib-cell-settings-dialog', standalone: true, imports: [
949
+ CommonModule,
950
+ FormsModule,
951
+ MatDialogModule,
952
+ MatButtonModule,
953
+ MatRadioModule,
954
+ ], template: `
955
+ <h2 mat-dialog-title>Cell Display Settings</h2>
956
+ <mat-dialog-content>
957
+ <p class="cell-info">Cell ID: <strong>{{ data.id }}</strong></p>
958
+
959
+ <div class="radio-group">
960
+ <mat-radio-group [(ngModel)]="selectedMode" name="displayMode">
961
+ <mat-radio-button value="normal">
962
+ <div class="radio-option">
963
+ <div class="option-title">Normal</div>
964
+ <div class="option-description">Standard cell display with full content visibility</div>
965
+ </div>
966
+ </mat-radio-button>
967
+
968
+ <mat-radio-button value="flat">
969
+ <div class="radio-option">
970
+ <div class="option-title">Flat</div>
971
+ <div class="option-description">Simplified display with reduced visual emphasis</div>
972
+ </div>
973
+ </mat-radio-button>
974
+ </mat-radio-group>
975
+ </div>
976
+ </mat-dialog-content>
977
+ <mat-dialog-actions align="end">
978
+ <button mat-button (click)="onCancel()">Cancel</button>
979
+ <button
980
+ mat-flat-button
981
+ (click)="save()"
982
+ [disabled]="selectedMode === currentMode">
983
+ Apply
984
+ </button>
985
+ </mat-dialog-actions>
986
+ `, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden;padding-top:.5rem}.cell-info{margin:0 0 1.5rem;padding-bottom:1rem}.radio-group{width:100%}mat-radio-group{display:block}mat-radio-button{width:100%;display:block;margin-bottom:1rem}mat-radio-button:last-child{margin-bottom:0}.radio-option{margin-left:.75rem;padding:.25rem 0}.option-title{display:block;margin-bottom:.25rem}.option-description{display:block}\n"] }]
987
+ }], ctorParameters: () => [] });
988
+
989
+ /**
990
+ * Default cell dialog provider that uses Material Design dialogs.
991
+ * Provides a modern, accessible dialog experience for cell settings.
992
+ */
993
+ class DefaultCellSettingsDialogProvider extends CellSettingsDialogProvider {
994
+ dialog = inject(MatDialog);
995
+ async openCellSettings(data) {
996
+ const dialogRef = this.dialog.open(CellSettingsDialogComponent, {
997
+ data,
998
+ width: '400px',
999
+ maxWidth: '90vw',
1000
+ disableClose: false,
1001
+ autoFocus: false,
1002
+ });
1003
+ const result = await firstValueFrom(dialogRef.afterClosed());
1004
+ return result;
1005
+ }
1006
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultCellSettingsDialogProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
1007
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultCellSettingsDialogProvider, providedIn: 'root' });
1008
+ }
1009
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DefaultCellSettingsDialogProvider, decorators: [{
1010
+ type: Injectable,
1011
+ args: [{
1012
+ providedIn: 'root',
1013
+ }]
1014
+ }] });
1015
+
1016
+ /**
1017
+ * Injection token for the cell dialog provider.
1018
+ * Use this to provide your custom dialog implementation.
1019
+ *
1020
+ * @example
1021
+ * ```typescript
1022
+ * providers: [
1023
+ * { provide: CELL_SETTINGS_DIALOG_PROVIDER, useClass: MyCellSettingsDialogProvider }
1024
+ * ]
1025
+ * ```
1026
+ */
1027
+ const CELL_SETTINGS_DIALOG_PROVIDER = new InjectionToken('CellSettingsDialogProvider', {
1028
+ providedIn: 'root',
1029
+ factory: () => new DefaultCellSettingsDialogProvider(),
1030
+ });
1031
+
1032
+ class CellContextMenuService {
1033
+ #activeMenu = signal(null, ...(ngDevMode ? [{ debugName: "#activeMenu" }] : []));
1034
+ activeMenu = this.#activeMenu.asReadonly();
1035
+ show(x, y, items) {
1036
+ this.#activeMenu.set({ x, y, items });
1037
+ }
1038
+ hide() {
1039
+ this.#activeMenu.set(null);
1040
+ }
1041
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellContextMenuService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1042
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellContextMenuService });
1043
+ }
1044
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellContextMenuService, decorators: [{
1045
+ type: Injectable
1046
+ }] });
1047
+
1048
+ // cell.component.ts
1049
+ class CellComponent {
1050
+ widgetId = input.required(...(ngDevMode ? [{ debugName: "widgetId" }] : [])); // Unique widget instance identifier
1051
+ cellId = input.required(...(ngDevMode ? [{ debugName: "cellId" }] : [])); // Current grid position
1052
+ widgetFactory = input(undefined, ...(ngDevMode ? [{ debugName: "widgetFactory" }] : []));
1053
+ widgetState = input(undefined, ...(ngDevMode ? [{ debugName: "widgetState" }] : []));
1054
+ isEditMode = input(false, ...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
1055
+ flat = input(undefined, ...(ngDevMode ? [{ debugName: "flat" }] : []));
1056
+ row = model.required(...(ngDevMode ? [{ debugName: "row" }] : []));
1057
+ column = model.required(...(ngDevMode ? [{ debugName: "column" }] : []));
1058
+ rowSpan = input(1, ...(ngDevMode ? [{ debugName: "rowSpan" }] : []));
1059
+ colSpan = input(1, ...(ngDevMode ? [{ debugName: "colSpan" }] : []));
1060
+ draggable = input(false, ...(ngDevMode ? [{ debugName: "draggable" }] : []));
1061
+ dragStart = output();
1062
+ dragEnd = output();
1063
+ edit = output();
1064
+ delete = output();
1065
+ settings = output();
1066
+ resizeStart = output();
1067
+ resizeMove = output();
1068
+ resizeEnd = output();
1069
+ container = viewChild.required('container', { read: ViewContainerRef });
1070
+ #store = inject(DashboardStore);
1071
+ #dialogProvider = inject(CELL_SETTINGS_DIALOG_PROVIDER);
1072
+ #destroyRef = inject(DestroyRef);
1073
+ #renderer = inject(Renderer2);
1074
+ #contextMenuService = inject(CellContextMenuService, {
1075
+ optional: true,
1076
+ });
1077
+ #widgetRef;
1078
+ // Document event listeners cleanup function
1079
+ // Performance: Only created when actively resizing, not for every cell
1080
+ #documentListeners;
1081
+ isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : []));
1082
+ gridRowStyle = computed(() => `${this.row()} / span ${this.rowSpan()}`, ...(ngDevMode ? [{ debugName: "gridRowStyle" }] : []));
1083
+ gridColumnStyle = computed(() => `${this.column()} / span ${this.colSpan()}`, ...(ngDevMode ? [{ debugName: "gridColumnStyle" }] : []));
1084
+ isResizing = computed(() => {
1085
+ const resizeData = this.#store.resizeData();
1086
+ return resizeData
1087
+ ? CellIdUtils.equals(resizeData.cellId, this.cellId())
1088
+ : false;
1089
+ }, ...(ngDevMode ? [{ debugName: "isResizing" }] : []));
1090
+ isDragActive = computed(() => !!this.#store.dragData(), ...(ngDevMode ? [{ debugName: "isDragActive" }] : []));
1091
+ resizeData = this.#store.resizeData;
1092
+ gridCellDimensions = this.#store.gridCellDimensions;
1093
+ resizeDirection = signal(null, ...(ngDevMode ? [{ debugName: "resizeDirection" }] : []));
1094
+ resizeStartPos = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "resizeStartPos" }] : []));
1095
+ constructor() {
1096
+ // widget creation - triggers when factory or state changes
1097
+ effect(() => {
1098
+ const factory = this.widgetFactory();
1099
+ const state = this.widgetState();
1100
+ const container = this.container();
1101
+ if (factory && container) {
1102
+ // Clean up previous widget
1103
+ this.#widgetRef?.destroy();
1104
+ // Create new widget
1105
+ container.clear();
1106
+ try {
1107
+ this.#widgetRef = factory.createInstance(container, state);
1108
+ }
1109
+ catch (error) {
1110
+ console.error('Failed to create widget:', error);
1111
+ this.#widgetRef = undefined;
1112
+ }
1113
+ }
1114
+ });
1115
+ // Auto cleanup on destroy
1116
+ this.#destroyRef.onDestroy(() => {
1117
+ this.#widgetRef?.destroy();
1118
+ this.#widgetRef = undefined;
1119
+ // Clean up any active document listeners
1120
+ this.#cleanupDocumentListeners();
1121
+ });
1122
+ }
1123
+ /**
1124
+ * Setup document-level event listeners for resize operations
1125
+ * Performance: Only creates listeners when actively resizing (not for every cell)
1126
+ * Angular-idiomatic: Uses Renderer2 for dynamic listener management
1127
+ */
1128
+ setupDocumentListeners() {
1129
+ // Clean up any existing listeners first
1130
+ this.#cleanupDocumentListeners();
1131
+ // Create document listeners with proper cleanup functions
1132
+ const unlistenMove = this.#renderer.listen('document', 'mousemove', this.handleResizeMove.bind(this));
1133
+ const unlistenUp = this.#renderer.listen('document', 'mouseup', this.handleResizeEnd.bind(this));
1134
+ // Store cleanup function for later use
1135
+ this.#documentListeners = () => {
1136
+ unlistenMove();
1137
+ unlistenUp();
1138
+ };
1139
+ }
1140
+ /**
1141
+ * Clean up document-level event listeners
1142
+ * Called on resize end and component destruction
1143
+ */
1144
+ #cleanupDocumentListeners() {
1145
+ if (this.#documentListeners) {
1146
+ this.#documentListeners();
1147
+ this.#documentListeners = undefined;
1148
+ }
1149
+ }
1150
+ setPosition(row, column) {
1151
+ this.row.set(row);
1152
+ this.column.set(column);
1153
+ }
1154
+ onDragStart(event) {
1155
+ if (!event.dataTransfer)
1156
+ return;
1157
+ event.dataTransfer.effectAllowed = 'move';
1158
+ const cell = {
1159
+ cellId: this.cellId(),
1160
+ widgetId: this.widgetId(),
1161
+ row: this.row(),
1162
+ col: this.column(),
1163
+ rowSpan: this.rowSpan(),
1164
+ colSpan: this.colSpan(),
1165
+ };
1166
+ const content = { kind: 'cell', content: cell };
1167
+ this.dragStart.emit(content);
1168
+ this.isDragging.set(true);
1169
+ }
1170
+ onDragEnd( /*_: DragEvent*/) {
1171
+ this.isDragging.set(false);
1172
+ this.dragEnd.emit();
1173
+ }
1174
+ /**
1175
+ * Handle context menu events (called from template)
1176
+ * Performance: Element-specific event binding, not document-level
1177
+ * Angular-idiomatic: Template event binding instead of fromEvent
1178
+ */
1179
+ onContextMenu(event) {
1180
+ if (!this.isEditMode() || !this.#contextMenuService)
1181
+ return;
1182
+ event.preventDefault();
1183
+ event.stopPropagation();
1184
+ const items = [
1185
+ {
1186
+ label: 'Edit Widget',
1187
+ icon: 'edit',
1188
+ action: () => this.onEdit(),
1189
+ disabled: !this.canEdit(),
1190
+ },
1191
+ {
1192
+ label: 'Settings',
1193
+ icon: 'settings',
1194
+ action: () => this.onSettings(),
1195
+ },
1196
+ { divider: true },
1197
+ {
1198
+ label: 'Delete',
1199
+ icon: 'delete',
1200
+ action: () => this.onDelete(),
1201
+ },
1202
+ ];
1203
+ // Position menu at exact mouse coordinates
1204
+ this.#contextMenuService.show(event.clientX, event.clientY, items);
1205
+ }
1206
+ canEdit() {
1207
+ if (this.#widgetRef?.instance?.dashboardEditState) {
1208
+ return true;
1209
+ }
1210
+ return false;
1211
+ }
1212
+ onEdit() {
1213
+ this.edit.emit(this.widgetId());
1214
+ // Call the widget's edit dialog method if it exists
1215
+ if (this.#widgetRef?.instance?.dashboardEditState) {
1216
+ this.#widgetRef.instance.dashboardEditState();
1217
+ }
1218
+ }
1219
+ onDelete() {
1220
+ this.delete.emit(this.widgetId());
1221
+ }
1222
+ async onSettings() {
1223
+ const currentSettings = {
1224
+ id: CellIdUtils.toString(this.cellId()), // Use cellId for display position
1225
+ flat: this.flat(),
1226
+ };
1227
+ try {
1228
+ const result = await this.#dialogProvider.openCellSettings(currentSettings);
1229
+ if (result) {
1230
+ this.settings.emit({
1231
+ id: this.widgetId(),
1232
+ flat: result.flat ?? false,
1233
+ });
1234
+ }
1235
+ }
1236
+ catch (error) {
1237
+ console.error('Error opening cell settings dialog:', error);
1238
+ }
1239
+ }
1240
+ /**
1241
+ * Start resize operation and setup document listeners
1242
+ * Performance: Only THIS cell creates document listeners when actively resizing
1243
+ * RxJS-free: Uses Renderer2 for dynamic listener management
1244
+ */
1245
+ onResizeStart(event, direction) {
1246
+ event.preventDefault();
1247
+ event.stopPropagation();
1248
+ this.resizeDirection.set(direction);
1249
+ this.resizeStartPos.set({ x: event.clientX, y: event.clientY });
1250
+ this.resizeStart.emit({ cellId: this.cellId(), direction });
1251
+ // Setup document listeners only when actively resizing
1252
+ this.setupDocumentListeners();
1253
+ const cursorClass = direction === 'horizontal' ? 'cursor-col-resize' : 'cursor-row-resize';
1254
+ this.#renderer.addClass(document.body, cursorClass);
1255
+ }
1256
+ /**
1257
+ * Handle resize move events (called from document listener)
1258
+ * Performance: Only called for the actively resizing cell
1259
+ * Bound method: Maintains component context without arrow functions
1260
+ */
1261
+ handleResizeMove(event) {
1262
+ const direction = this.resizeDirection();
1263
+ if (!direction)
1264
+ return;
1265
+ const startPos = this.resizeStartPos();
1266
+ const cellSize = this.gridCellDimensions();
1267
+ if (direction === 'horizontal') {
1268
+ const deltaX = event.clientX - startPos.x;
1269
+ const deltaSpan = Math.round(deltaX / cellSize.width);
1270
+ this.resizeMove.emit({ cellId: this.cellId(), direction, delta: deltaSpan });
1271
+ }
1272
+ else {
1273
+ const deltaY = event.clientY - startPos.y;
1274
+ const deltaSpan = Math.round(deltaY / cellSize.height);
1275
+ this.resizeMove.emit({ cellId: this.cellId(), direction, delta: deltaSpan });
1276
+ }
1277
+ }
1278
+ /**
1279
+ * Handle resize end events (called from document listener)
1280
+ * Performance: Cleans up document listeners immediately after resize
1281
+ * State cleanup: Resets resize direction to stop further event processing
1282
+ */
1283
+ handleResizeEnd() {
1284
+ this.#renderer.removeClass(document.body, 'cursor-col-resize');
1285
+ this.#renderer.removeClass(document.body, 'cursor-row-resize');
1286
+ // Clean up document listeners immediately
1287
+ this.#cleanupDocumentListeners();
1288
+ this.resizeEnd.emit({ cellId: this.cellId(), apply: true });
1289
+ this.resizeDirection.set(null);
1290
+ }
1291
+ /**
1292
+ * Get the current widget state by calling dashboardGetState() on the widget instance.
1293
+ * Used during dashboard export to get live widget state instead of stale stored state.
1294
+ */
1295
+ getCurrentWidgetState() {
1296
+ if (!this.#widgetRef?.instance) {
1297
+ return undefined;
1298
+ }
1299
+ // Call dashboardGetState() if the widget implements it
1300
+ if (typeof this.#widgetRef.instance.dashboardGetState === 'function') {
1301
+ return this.#widgetRef.instance.dashboardGetState();
1302
+ }
1303
+ // Fall back to stored state if widget doesn't implement dashboardGetState
1304
+ return this.widgetState();
1305
+ }
1306
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1307
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: CellComponent, isStandalone: true, selector: "lib-cell", inputs: { widgetId: { classPropertyName: "widgetId", publicName: "widgetId", isSignal: true, isRequired: true, transformFunction: null }, cellId: { classPropertyName: "cellId", publicName: "cellId", isSignal: true, isRequired: true, transformFunction: null }, widgetFactory: { classPropertyName: "widgetFactory", publicName: "widgetFactory", isSignal: true, isRequired: false, transformFunction: null }, widgetState: { classPropertyName: "widgetState", publicName: "widgetState", isSignal: true, isRequired: false, transformFunction: null }, isEditMode: { classPropertyName: "isEditMode", publicName: "isEditMode", isSignal: true, isRequired: false, transformFunction: null }, flat: { classPropertyName: "flat", publicName: "flat", isSignal: true, isRequired: false, transformFunction: null }, row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null }, rowSpan: { classPropertyName: "rowSpan", publicName: "rowSpan", isSignal: true, isRequired: false, transformFunction: null }, colSpan: { classPropertyName: "colSpan", publicName: "colSpan", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { row: "rowChange", column: "columnChange", dragStart: "dragStart", dragEnd: "dragEnd", edit: "edit", delete: "delete", settings: "settings", resizeStart: "resizeStart", resizeMove: "resizeMove", resizeEnd: "resizeEnd" }, host: { properties: { "style.grid-row": "gridRowStyle()", "style.grid-column": "gridColumnStyle()", "class.is-dragging": "isDragging()", "class.drag-active": "isDragActive()", "class.flat": "flat() === true" } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef, isSignal: true }], ngImport: i0, template: "<!-- cell.component.html -->\r\n<div\r\n class=\"cell\"\r\n [class.is-resizing]=\"isResizing()\"\r\n [class.flat]=\"flat() === true\"\r\n [draggable]=\"draggable()\"\r\n (dragstart)=\"onDragStart($event)\"\r\n (dragend)=\"onDragEnd()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <div class=\"content-area\">\r\n <ng-template #container></ng-template>\r\n </div>\r\n @if (isEditMode() && !isDragging()) {\r\n <!-- Right resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--right\"\r\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n <!-- Bottom resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--bottom\"\r\n (mousedown)=\"onResizeStart($event, 'vertical')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n }\r\n</div>\r\n\r\n@if (isResizing()) {\r\n<div class=\"resize-preview\">\r\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\r\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\r\n</div>\r\n}\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(.is-dragging) .content-area{pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto;pointer-events:auto;position:relative;z-index:1}.content-area:hover{transform:initial}:host(:not(.is-dragging)) .cell.flat .content-area{pointer-events:auto}:host(:not(.is-dragging)) .cell.flat .content-area:hover{transform:initial}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}:root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1308
+ }
1309
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellComponent, decorators: [{
1310
+ type: Component,
1311
+ args: [{ selector: 'lib-cell', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1312
+ '[style.grid-row]': 'gridRowStyle()',
1313
+ '[style.grid-column]': 'gridColumnStyle()',
1314
+ '[class.is-dragging]': 'isDragging()',
1315
+ '[class.drag-active]': 'isDragActive()',
1316
+ '[class.flat]': 'flat() === true',
1317
+ }, template: "<!-- cell.component.html -->\r\n<div\r\n class=\"cell\"\r\n [class.is-resizing]=\"isResizing()\"\r\n [class.flat]=\"flat() === true\"\r\n [draggable]=\"draggable()\"\r\n (dragstart)=\"onDragStart($event)\"\r\n (dragend)=\"onDragEnd()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <div class=\"content-area\">\r\n <ng-template #container></ng-template>\r\n </div>\r\n @if (isEditMode() && !isDragging()) {\r\n <!-- Right resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--right\"\r\n (mousedown)=\"onResizeStart($event, 'horizontal')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n <!-- Bottom resize handle -->\r\n <div\r\n class=\"resize-handle resize-handle--bottom\"\r\n (mousedown)=\"onResizeStart($event, 'vertical')\"\r\n >\r\n <div class=\"resize-handle-line\"></div>\r\n </div>\r\n }\r\n</div>\r\n\r\n@if (isResizing()) {\r\n<div class=\"resize-preview\">\r\n {{ resizeData()?.previewColSpan ?? colSpan() }} \u00D7\r\n {{ resizeData()?.previewRowSpan ?? rowSpan() }}\r\n</div>\r\n}\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative;z-index:1;container-type:inline-size}:host(.drag-active):not(.is-dragging){pointer-events:none}:host(.is-dragging){z-index:100;opacity:.5;pointer-events:none}:host(.is-dragging) .content-area{pointer-events:none}:host(:hover) .resize-handle{opacity:1}.cell{width:100%;height:100%;border-radius:4px;box-shadow:0 2px 6px #0000001a;padding:0;box-sizing:border-box;overflow:hidden;position:relative;container-type:inline-size}.cell:hover{box-shadow:0 4px 10px #00000026;transform:translateY(-2px)}.cell.flat{box-shadow:none;border:none}.cell.flat:hover{box-shadow:none;transform:none;border-color:#bdbdbd}.cell.resizing{-webkit-user-select:none;user-select:none}.content-area{width:100%;height:100%;overflow:auto;pointer-events:auto;position:relative;z-index:1}.content-area:hover{transform:initial}:host(:not(.is-dragging)) .cell.flat .content-area{pointer-events:auto}:host(:not(.is-dragging)) .cell.flat .content-area:hover{transform:initial}.resize-handle{position:absolute;z-index:20}.resize-handle--right{cursor:col-resize;width:16px;height:100%;right:-8px;top:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--right:hover{opacity:1}.resize-handle--right:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle--bottom{cursor:row-resize;width:100%;height:16px;bottom:-8px;left:0;display:flex;align-items:center;justify-content:center;opacity:0}.resize-handle--bottom:hover{opacity:1}.resize-handle--bottom:hover .resize-handle-line{background-color:var(--mat-sys-primary-container)}.resize-handle-line{background-color:#0000001a}.resize-handle--right .resize-handle-line{width:8px;height:40px;border-radius:2px}.resize-handle--bottom .resize-handle-line{width:40px;height:8px;border-radius:2px}.resize-preview{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background-color:var(--mat-sys-primary);color:var(--mat-sys-on-primary);padding:4px 12px;border-radius:4px;font-size:14px;font-weight:500;pointer-events:none;z-index:30}.cell.is-resizing{opacity:.6}.cell.is-resizing .resize-handle{background-color:#2196f380}:root .cursor-col-resize{cursor:col-resize!important}:root .cursor-row-resize{cursor:row-resize!important}\n"] }]
1318
+ }], ctorParameters: () => [] });
1319
+
1320
+ class DashboardViewerComponent {
1321
+ #store = inject(DashboardStore);
1322
+ cellComponents = viewChildren(CellComponent, ...(ngDevMode ? [{ debugName: "cellComponents" }] : []));
1323
+ rows = input.required(...(ngDevMode ? [{ debugName: "rows" }] : []));
1324
+ columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : []));
1325
+ gutterSize = input('1em', ...(ngDevMode ? [{ debugName: "gutterSize" }] : []));
1326
+ gutters = computed(() => this.columns() + 1, ...(ngDevMode ? [{ debugName: "gutters" }] : []));
1327
+ // store signals - read-only
1328
+ cells = this.#store.cells;
1329
+ constructor() {
1330
+ // Sync grid configuration with store when inputs change
1331
+ effect(() => {
1332
+ this.#store.setGridConfig({
1333
+ rows: this.rows(),
1334
+ columns: this.columns(),
1335
+ gutterSize: this.gutterSize(),
1336
+ });
1337
+ });
1338
+ }
1339
+ /**
1340
+ * Get current widget states from all cell components.
1341
+ * Used during dashboard export to get live widget states.
1342
+ */
1343
+ getCurrentWidgetStates() {
1344
+ const stateMap = new Map();
1345
+ const cells = this.cellComponents();
1346
+ for (const cell of cells) {
1347
+ const cellId = cell.cellId();
1348
+ const currentState = cell.getCurrentWidgetState();
1349
+ if (currentState !== undefined) {
1350
+ stateMap.set(CellIdUtils.toString(cellId), currentState);
1351
+ }
1352
+ }
1353
+ return stateMap;
1354
+ }
1355
+ /**
1356
+ * Export dashboard with live widget states from current component instances.
1357
+ * This ensures the most up-to-date widget states are captured.
1358
+ */
1359
+ exportDashboard() {
1360
+ return this.#store.exportDashboard(() => this.getCurrentWidgetStates());
1361
+ }
1362
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1363
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardViewerComponent, isStandalone: true, selector: "ngx-dashboard-viewer", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "rows()", "style.--columns": "columns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "gutters()" } }, viewQueries: [{ propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\">\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["widgetId", "cellId", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1364
+ }
1365
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewerComponent, decorators: [{
1366
+ type: Component,
1367
+ args: [{ selector: 'ngx-dashboard-viewer', standalone: true, imports: [CellComponent, CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1368
+ '[style.--rows]': 'rows()',
1369
+ '[style.--columns]': 'columns()',
1370
+ '[style.--gutter-size]': 'gutterSize()',
1371
+ '[style.--gutters]': 'gutters()',
1372
+ }, template: "<!-- Dashboard viewer - read-only grid -->\r\n<div class=\"grid top-grid\">\r\n @for (cell of cells(); track cell.widgetId) {\r\n <lib-cell\r\n class=\"grid-cell\"\r\n [widgetId]=\"cell.widgetId\"\r\n [cellId]=\"cell.cellId\"\r\n [isEditMode]=\"false\"\r\n [draggable]=\"false\"\r\n [row]=\"cell.row\"\r\n [column]=\"cell.col\"\r\n [rowSpan]=\"cell.rowSpan\"\r\n [colSpan]=\"cell.colSpan\"\r\n [flat]=\"cell.flat\"\r\n [widgetFactory]=\"cell.widgetFactory\"\r\n [widgetState]=\"cell.widgetState\"\r\n >\r\n </lib-cell>\r\n }\r\n</div>\r\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto;background-color:var(--mat-sys-surface-container)}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);width:100%;height:100%;box-sizing:border-box;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}.grid-cell{pointer-events:none}.grid-cell:not(.flat){pointer-events:auto;cursor:default}.grid-cell:not(.flat) .content-area{pointer-events:none}.top-grid{z-index:2;pointer-events:none}\n"] }]
1373
+ }], ctorParameters: () => [] });
1374
+
1375
+ class CellContextMenuComponent {
1376
+ menuTrigger = viewChild.required('menuTrigger', { read: MatMenuTrigger });
1377
+ menuService = inject(CellContextMenuService);
1378
+ menuPosition = computed(() => {
1379
+ const menu = this.menuService.activeMenu();
1380
+ return menu ? { left: `${menu.x}px`, top: `${menu.y}px` } : { left: '0px', top: '0px' };
1381
+ }, ...(ngDevMode ? [{ debugName: "menuPosition" }] : []));
1382
+ menuItems = computed(() => {
1383
+ const menu = this.menuService.activeMenu();
1384
+ return menu?.items || [];
1385
+ }, ...(ngDevMode ? [{ debugName: "menuItems" }] : []));
1386
+ constructor() {
1387
+ effect(() => {
1388
+ const menu = this.menuService.activeMenu();
1389
+ if (menu) {
1390
+ // Use queueMicrotask to ensure the view is fully initialized
1391
+ // This fixes the issue where the menu disappears on first right-click
1392
+ queueMicrotask(() => {
1393
+ const trigger = this.menuTrigger();
1394
+ if (trigger) {
1395
+ // Close any existing menu first
1396
+ if (trigger.menuOpen) {
1397
+ trigger.closeMenu();
1398
+ }
1399
+ // Open menu - position is handled by signal
1400
+ trigger.openMenu();
1401
+ }
1402
+ });
1403
+ }
1404
+ else {
1405
+ const trigger = this.menuTrigger();
1406
+ if (trigger) {
1407
+ trigger.closeMenu();
1408
+ }
1409
+ }
1410
+ });
1411
+ // Hide service menu when Material menu closes
1412
+ effect(() => {
1413
+ const trigger = this.menuTrigger();
1414
+ if (trigger) {
1415
+ trigger.menuClosed.subscribe(() => {
1416
+ if (this.menuService.activeMenu()) {
1417
+ this.menuService.hide();
1418
+ }
1419
+ });
1420
+ }
1421
+ });
1422
+ }
1423
+ executeAction(item) {
1424
+ if (!item.divider && item.action) {
1425
+ item.action();
1426
+ this.menuService.hide();
1427
+ }
1428
+ }
1429
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellContextMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1430
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: CellContextMenuComponent, isStandalone: true, selector: "lib-cell-context-menu", viewQueries: [{ propertyName: "menuTrigger", first: true, predicate: ["menuTrigger"], descendants: true, read: MatMenuTrigger, isSignal: true }], ngImport: i0, template: `
1431
+ <!-- Hidden trigger for menu positioned at exact mouse coordinates
1432
+
1433
+ IMPORTANT: Angular Material applies its own positioning logic to menus,
1434
+ which by default offsets the menu from the trigger element to avoid overlap.
1435
+ To achieve precise positioning at mouse coordinates, we use these workarounds:
1436
+
1437
+ 1. The trigger container is 1x1px (not 0x0) because Material needs a physical
1438
+ element to calculate position against. Zero-sized elements cause unpredictable
1439
+ positioning.
1440
+
1441
+ 2. We use opacity:0 instead of visibility:hidden to keep the element in the
1442
+ layout flow while making it invisible.
1443
+
1444
+ 3. The button itself is styled to 1x1px with no padding to serve as a precise
1445
+ anchor point for the menu.
1446
+
1447
+ 4. The mat-menu uses [overlapTrigger]="true" to allow the menu to appear
1448
+ directly at the trigger position rather than offset from it.
1449
+
1450
+ This approach ensures the menu appears at the exact mouse coordinates passed
1451
+ from the cell component's right-click handler.
1452
+ -->
1453
+ <div
1454
+ style="position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
1455
+ [style]="menuPosition()">
1456
+ <button
1457
+ mat-button
1458
+ #menuTrigger="matMenuTrigger"
1459
+ [matMenuTriggerFor]="contextMenu"
1460
+ style="width: 1px; height: 1px; padding: 0; min-width: 0; line-height: 0;">
1461
+ </button>
1462
+ </div>
1463
+
1464
+ <!-- Context menu -->
1465
+ <mat-menu #contextMenu="matMenu" [overlapTrigger]="true">
1466
+ @for (item of menuItems(); track $index) {
1467
+ @if (item.divider) {
1468
+ <mat-divider></mat-divider>
1469
+ } @else {
1470
+ <button
1471
+ mat-menu-item
1472
+ (click)="executeAction(item)"
1473
+ [disabled]="item.disabled">
1474
+ @if (item.icon) {
1475
+ <mat-icon>{{ item.icon }}</mat-icon>
1476
+ }
1477
+ <span>{{ item.label }}</span>
1478
+ </button>
1479
+ }
1480
+ }
1481
+ </mat-menu>
1482
+ `, isInline: true, styles: [":host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i1$2.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i1$2.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i1$2.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i3$1.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1483
+ }
1484
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: CellContextMenuComponent, decorators: [{
1485
+ type: Component,
1486
+ args: [{ selector: 'lib-cell-context-menu', standalone: true, imports: [MatMenuModule, MatIconModule, MatDividerModule, MatButtonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1487
+ <!-- Hidden trigger for menu positioned at exact mouse coordinates
1488
+
1489
+ IMPORTANT: Angular Material applies its own positioning logic to menus,
1490
+ which by default offsets the menu from the trigger element to avoid overlap.
1491
+ To achieve precise positioning at mouse coordinates, we use these workarounds:
1492
+
1493
+ 1. The trigger container is 1x1px (not 0x0) because Material needs a physical
1494
+ element to calculate position against. Zero-sized elements cause unpredictable
1495
+ positioning.
1496
+
1497
+ 2. We use opacity:0 instead of visibility:hidden to keep the element in the
1498
+ layout flow while making it invisible.
1499
+
1500
+ 3. The button itself is styled to 1x1px with no padding to serve as a precise
1501
+ anchor point for the menu.
1502
+
1503
+ 4. The mat-menu uses [overlapTrigger]="true" to allow the menu to appear
1504
+ directly at the trigger position rather than offset from it.
1505
+
1506
+ This approach ensures the menu appears at the exact mouse coordinates passed
1507
+ from the cell component's right-click handler.
1508
+ -->
1509
+ <div
1510
+ style="position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;"
1511
+ [style]="menuPosition()">
1512
+ <button
1513
+ mat-button
1514
+ #menuTrigger="matMenuTrigger"
1515
+ [matMenuTriggerFor]="contextMenu"
1516
+ style="width: 1px; height: 1px; padding: 0; min-width: 0; line-height: 0;">
1517
+ </button>
1518
+ </div>
1519
+
1520
+ <!-- Context menu -->
1521
+ <mat-menu #contextMenu="matMenu" [overlapTrigger]="true">
1522
+ @for (item of menuItems(); track $index) {
1523
+ @if (item.divider) {
1524
+ <mat-divider></mat-divider>
1525
+ } @else {
1526
+ <button
1527
+ mat-menu-item
1528
+ (click)="executeAction(item)"
1529
+ [disabled]="item.disabled">
1530
+ @if (item.icon) {
1531
+ <mat-icon>{{ item.icon }}</mat-icon>
1532
+ }
1533
+ <span>{{ item.label }}</span>
1534
+ </button>
1535
+ }
1536
+ }
1537
+ </mat-menu>
1538
+ `, styles: [":host{display:contents}\n"] }]
1539
+ }], ctorParameters: () => [] });
1540
+
1541
+ // drop-zone.component.ts
1542
+ class DropZoneComponent {
1543
+ // Required inputs
1544
+ row = input.required(...(ngDevMode ? [{ debugName: "row" }] : []));
1545
+ col = input.required(...(ngDevMode ? [{ debugName: "col" }] : []));
1546
+ index = input.required(...(ngDevMode ? [{ debugName: "index" }] : []));
1547
+ // Optional inputs with defaults
1548
+ highlight = input(false, ...(ngDevMode ? [{ debugName: "highlight" }] : []));
1549
+ highlightInvalid = input(false, ...(ngDevMode ? [{ debugName: "highlightInvalid" }] : []));
1550
+ highlightResize = input(false, ...(ngDevMode ? [{ debugName: "highlightResize" }] : []));
1551
+ editMode = input(false, ...(ngDevMode ? [{ debugName: "editMode" }] : []));
1552
+ // Outputs
1553
+ dragEnter = output();
1554
+ dragExit = output();
1555
+ dragOver = output();
1556
+ dragDrop = output();
1557
+ // Computed properties
1558
+ dropZoneId = computed(() => `drop-zone-${this.row()}-${this.col()}`, ...(ngDevMode ? [{ debugName: "dropZoneId" }] : []));
1559
+ dropData = computed(() => ({
1560
+ row: this.row(),
1561
+ col: this.col(),
1562
+ }), ...(ngDevMode ? [{ debugName: "dropData" }] : []));
1563
+ // Abstract drag state from store
1564
+ dragData = computed(() => this.#store.dragData(), ...(ngDevMode ? [{ debugName: "dragData" }] : []));
1565
+ // Computed drop effect based on drag data and validity
1566
+ dropEffect = computed(() => {
1567
+ const data = this.dragData();
1568
+ if (!data || this.highlightInvalid()) {
1569
+ return 'none';
1570
+ }
1571
+ return data.kind === 'cell' ? 'move' : 'copy';
1572
+ }, ...(ngDevMode ? [{ debugName: "dropEffect" }] : []));
1573
+ #store = inject(DashboardStore);
1574
+ #elementRef = inject(ElementRef);
1575
+ get nativeElement() {
1576
+ return this.#elementRef.nativeElement;
1577
+ }
1578
+ onDragEnter(event) {
1579
+ event.preventDefault();
1580
+ event.stopPropagation();
1581
+ this.dragEnter.emit({ row: this.row(), col: this.col() });
1582
+ }
1583
+ onDragOver(event) {
1584
+ event.preventDefault();
1585
+ event.stopPropagation();
1586
+ if (event.dataTransfer && this.dragData()) {
1587
+ event.dataTransfer.dropEffect = this.dropEffect();
1588
+ }
1589
+ this.dragOver.emit({ row: this.row(), col: this.col() });
1590
+ }
1591
+ onDragLeave(event) {
1592
+ event.preventDefault();
1593
+ event.stopPropagation();
1594
+ // Only emit if actually leaving the element (not entering a child)
1595
+ if (this.#isLeavingElement(event)) {
1596
+ this.dragExit.emit();
1597
+ }
1598
+ }
1599
+ #isLeavingElement(event) {
1600
+ const rect = this.#elementRef.nativeElement.getBoundingClientRect();
1601
+ return (event.clientX <= rect.left ||
1602
+ event.clientX >= rect.right ||
1603
+ event.clientY <= rect.top ||
1604
+ event.clientY >= rect.bottom);
1605
+ }
1606
+ onDrop(event) {
1607
+ event.preventDefault();
1608
+ event.stopPropagation();
1609
+ if (!event.dataTransfer)
1610
+ return;
1611
+ const data = this.dragData();
1612
+ if (data) {
1613
+ this.dragDrop.emit({
1614
+ data,
1615
+ target: { row: this.row(), col: this.col() },
1616
+ });
1617
+ }
1618
+ }
1619
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DropZoneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1620
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DropZoneComponent, isStandalone: true, selector: "lib-drop-zone", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, col: { classPropertyName: "col", publicName: "col", isSignal: true, isRequired: true, transformFunction: null }, index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null }, highlight: { classPropertyName: "highlight", publicName: "highlight", isSignal: true, isRequired: false, transformFunction: null }, highlightInvalid: { classPropertyName: "highlightInvalid", publicName: "highlightInvalid", isSignal: true, isRequired: false, transformFunction: null }, highlightResize: { classPropertyName: "highlightResize", publicName: "highlightResize", isSignal: true, isRequired: false, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dragEnter: "dragEnter", dragExit: "dragExit", dragOver: "dragOver", dragDrop: "dragDrop" }, ngImport: i0, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;color:#64646499;pointer-events:none;-webkit-user-select:none;user-select:none;z-index:-1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1621
+ }
1622
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DropZoneComponent, decorators: [{
1623
+ type: Component,
1624
+ args: [{ selector: 'lib-drop-zone', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- drop-zone.component.html -->\r\n<div\r\n class=\"drop-zone\"\r\n [class.drop-zone--highlight]=\"highlight() && !highlightInvalid()\"\r\n [class.drop-zone--invalid]=\"highlightInvalid()\"\r\n [class.drop-zone--resize]=\"highlightResize()\"\r\n [style.grid-row]=\"row()\"\r\n [style.grid-column]=\"col()\"\r\n (dragenter)=\"onDragEnter($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n>\r\n @if (editMode()) {\r\n <div class=\"edit-mode-cell-number\">\r\n {{ index() }}<br />\r\n {{ row() }},{{ col() }}\r\n </div>\r\n }\r\n</div>\r\n", styles: [".drop-zone{width:100%;height:100%;z-index:0;align-self:stretch;justify-self:stretch;display:block;box-sizing:border-box}.drop-zone--active,.drop-zone--highlight{background-color:#80808080}.drop-zone--invalid{background-color:light-dark(color-mix(in srgb,var(--mat-sys-error) 40%,white),color-mix(in srgb,var(--mat-sys-error) 80%,black))}.drop-zone--resize{background-color:#2196f34d;outline:1px solid rgba(33,150,243,.6)}.edit-mode-cell-number{font-size:10px;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"] }]
1625
+ }] });
1626
+
1627
+ // dashboard-editor.component.ts
1628
+ class DashboardEditorComponent {
1629
+ bottomGridRef = viewChild.required('bottomGrid');
1630
+ dropZones = viewChildren(DropZoneComponent, ...(ngDevMode ? [{ debugName: "dropZones" }] : []));
1631
+ cellComponents = viewChildren(CellComponent, ...(ngDevMode ? [{ debugName: "cellComponents" }] : []));
1632
+ #store = inject(DashboardStore);
1633
+ #destroyRef = inject(DestroyRef);
1634
+ #resizeObserver;
1635
+ rows = input.required(...(ngDevMode ? [{ debugName: "rows" }] : []));
1636
+ columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : []));
1637
+ gutterSize = input('1em', ...(ngDevMode ? [{ debugName: "gutterSize" }] : []));
1638
+ gutters = computed(() => this.columns() + 1, ...(ngDevMode ? [{ debugName: "gutters" }] : []));
1639
+ // store signals
1640
+ cells = this.#store.cells;
1641
+ highlightedZones = this.#store.highlightedZones;
1642
+ highlightMap = this.#store.highlightMap;
1643
+ invalidHighlightMap = this.#store.invalidHighlightMap;
1644
+ hoveredDropZone = this.#store.hoveredDropZone;
1645
+ resizePreviewMap = this.#store.resizePreviewMap;
1646
+ // Generate all possible cell positions for the grid
1647
+ dropzonePositions = computed(() => {
1648
+ const positions = [];
1649
+ for (let row = 1; row <= this.rows(); row++) {
1650
+ for (let col = 1; col <= this.columns(); col++) {
1651
+ positions.push({
1652
+ row,
1653
+ col,
1654
+ id: `dropzone-${row}-${col}`,
1655
+ index: (row - 1) * this.columns() + col,
1656
+ });
1657
+ }
1658
+ }
1659
+ return positions;
1660
+ }, ...(ngDevMode ? [{ debugName: "dropzonePositions" }] : []));
1661
+ // Helper method for template
1662
+ createCellId(row, col) {
1663
+ return CellIdUtils.create(row, col);
1664
+ }
1665
+ constructor() {
1666
+ // Sync grid configuration with store when inputs change
1667
+ effect(() => {
1668
+ this.#store.setGridConfig({
1669
+ rows: this.rows(),
1670
+ columns: this.columns(),
1671
+ gutterSize: this.gutterSize(),
1672
+ });
1673
+ });
1674
+ // Observe grid size after rendering
1675
+ afterNextRender(() => {
1676
+ this.#observeGridSize();
1677
+ });
1678
+ // Always set edit mode to true
1679
+ effect(() => {
1680
+ this.#store.setEditMode(true);
1681
+ });
1682
+ }
1683
+ #observeGridSize() {
1684
+ const gridEl = this.bottomGridRef()?.nativeElement;
1685
+ if (!gridEl || this.#resizeObserver)
1686
+ return;
1687
+ this.#resizeObserver = new ResizeObserver(() => {
1688
+ const dropZonesList = this.dropZones();
1689
+ const firstDropZone = dropZonesList[0];
1690
+ if (!firstDropZone)
1691
+ return;
1692
+ const el = firstDropZone.nativeElement;
1693
+ if (!el)
1694
+ return;
1695
+ const rect = el.getBoundingClientRect();
1696
+ const width = rect.width;
1697
+ const height = rect.height;
1698
+ this.#store.setGridCellDimensions(width, height);
1699
+ });
1700
+ this.#resizeObserver.observe(gridEl);
1701
+ // Register cleanup with DestroyRef for automatic memory management
1702
+ this.#destroyRef.onDestroy(() => {
1703
+ this.#resizeObserver?.disconnect();
1704
+ this.#resizeObserver = undefined;
1705
+ });
1706
+ }
1707
+ // Pure delegation methods - no business logic in component
1708
+ addWidget = (cellData) => this.#store.addWidget(cellData);
1709
+ updateCellPosition = (id, row, column) => this.#store.updateWidgetPosition(id, row, column);
1710
+ updateCellSpan = (id, colSpan, rowSpan) => this.#store.updateWidgetSpan(id, rowSpan, colSpan);
1711
+ updateCellSettings = (id, flat) => this.#store.updateCellSettings(id, flat);
1712
+ // Pure delegation - drag and drop event handlers
1713
+ onDragOver = (event) => this.#store.setHoveredDropZone(event);
1714
+ onDragEnter = (event) => this.onDragOver(event);
1715
+ onDragExit = () => this.#store.setHoveredDropZone(null);
1716
+ dragEnd = () => this.#store.endDrag();
1717
+ // Pure delegation - cell event handlers
1718
+ onCellDelete = (id) => this.#store.removeWidget(id);
1719
+ onCellSettings = (event) => this.updateCellSettings(event.id, event.flat);
1720
+ onCellResize = (event) => this.updateCellSpan(event.id, event.colSpan, event.rowSpan);
1721
+ // Handle drag events from cell component
1722
+ onCellDragStart = (dragData) => this.#store.startDrag(dragData);
1723
+ // Handle resize events from cell component
1724
+ onCellResizeStart = (event) => this.#store.startResize(event.cellId);
1725
+ onCellResizeMove = (event) => this.#store.updateResizePreview(event.direction, event.delta);
1726
+ onCellResizeEnd = (event) => this.#store.endResize(event.apply);
1727
+ // Handle drop events by delegating to store's business logic
1728
+ onDragDrop(event) {
1729
+ this.#store.handleDrop(event.data, event.target);
1730
+ // Note: Store handles all validation and error handling internally
1731
+ }
1732
+ /**
1733
+ * Get current widget states from all cell components.
1734
+ * Used during dashboard export to get live widget states.
1735
+ */
1736
+ getCurrentWidgetStates() {
1737
+ const stateMap = new Map();
1738
+ const cells = this.cellComponents();
1739
+ for (const cell of cells) {
1740
+ const cellId = cell.cellId();
1741
+ const currentState = cell.getCurrentWidgetState();
1742
+ if (currentState !== undefined) {
1743
+ stateMap.set(CellIdUtils.toString(cellId), currentState);
1744
+ }
1745
+ }
1746
+ return stateMap;
1747
+ }
1748
+ /**
1749
+ * Export dashboard with live widget states from current component instances.
1750
+ * This ensures the most up-to-date widget states are captured.
1751
+ */
1752
+ exportDashboard() {
1753
+ return this.#store.exportDashboard(() => this.getCurrentWidgetStates());
1754
+ }
1755
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1756
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardEditorComponent, isStandalone: true, selector: "ngx-dashboard-editor", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "rows()", "style.--columns": "columns()", "style.--gutter-size": "gutterSize()", "style.--gutters": "gutters()", "class.is-edit-mode": "true" } }, providers: [
1757
+ CellContextMenuService,
1758
+ ], viewQueries: [{ propertyName: "bottomGridRef", first: true, predicate: ["bottomGrid"], descendants: true, isSignal: true }, { propertyName: "dropZones", predicate: DropZoneComponent, descendants: true, isSignal: true }, { propertyName: "cellComponents", predicate: CellComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- dashboard-editor.component.html -->\n<div class=\"grid-container\">\n <!-- Bottom grid with drop zones -->\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\n @for (position of dropzonePositions(); track position.id) {\n <lib-drop-zone\n class=\"drop-zone\"\n [row]=\"position.row\"\n [col]=\"position.col\"\n [index]=\"position.index\"\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\n [highlightInvalid]=\"\n invalidHighlightMap().has(createCellId(position.row, position.col))\n \"\n [highlightResize]=\"\n resizePreviewMap().has(createCellId(position.row, position.col))\n \"\n [editMode]=\"true\"\n (dragEnter)=\"onDragEnter($event)\"\n (dragExit)=\"onDragExit()\"\n (dragOver)=\"onDragOver($event)\"\n (dragDrop)=\"onDragDrop($event)\"\n ></lib-drop-zone>\n }\n </div>\n\n <!-- Top grid with interactive cells -->\n <div class=\"grid\" id=\"top-grid\">\n @for (cell of cells(); track cell.widgetId) {\n <lib-cell\n class=\"grid-cell\"\n [widgetId]=\"cell.widgetId\"\n [cellId]=\"cell.cellId\"\n [isEditMode]=\"true\"\n [draggable]=\"true\"\n [row]=\"cell.row\"\n [column]=\"cell.col\"\n [rowSpan]=\"cell.rowSpan\"\n [colSpan]=\"cell.colSpan\"\n [flat]=\"cell.flat\"\n [widgetFactory]=\"cell.widgetFactory\"\n [widgetState]=\"cell.widgetState\"\n (dragStart)=\"onCellDragStart($event)\"\n (dragEnd)=\"dragEnd()\"\n (delete)=\"onCellDelete($event)\"\n (settings)=\"onCellSettings($event)\"\n (resizeStart)=\"onCellResizeStart($event)\"\n (resizeMove)=\"onCellResizeMove($event)\"\n (resizeEnd)=\"onCellResizeEnd($event)\"\n >\n </lib-cell>\n }\n </div>\n</div>\n\n<!-- Context menu -->\n<lib-cell-context-menu></lib-cell-context-menu>\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}:host .grid{background-image:linear-gradient(to right,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px);background-size:var(--tile-size) var(--tile-size),var(--tile-size) var(--tile-size),100% 1px;background-position:var(--tile-offset) var(--tile-offset),var(--tile-offset) var(--tile-offset),bottom;background-repeat:repeat,repeat,no-repeat}.grid-container{position:relative;width:100%;height:100%}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;align-items:stretch;justify-items:stretch;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}#bottom-grid{z-index:1}#top-grid{z-index:2;pointer-events:none}.grid-cell{pointer-events:auto}.grid-cell.is-dragging{pointer-events:none;opacity:.5}\n"], dependencies: [{ kind: "component", type: CellComponent, selector: "lib-cell", inputs: ["widgetId", "cellId", "widgetFactory", "widgetState", "isEditMode", "flat", "row", "column", "rowSpan", "colSpan", "draggable"], outputs: ["rowChange", "columnChange", "dragStart", "dragEnd", "edit", "delete", "settings", "resizeStart", "resizeMove", "resizeEnd"] }, { kind: "ngmodule", type: CommonModule }, { kind: "component", type: DropZoneComponent, selector: "lib-drop-zone", inputs: ["row", "col", "index", "highlight", "highlightInvalid", "highlightResize", "editMode"], outputs: ["dragEnter", "dragExit", "dragOver", "dragDrop"] }, { kind: "component", type: CellContextMenuComponent, selector: "lib-cell-context-menu" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1759
+ }
1760
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardEditorComponent, decorators: [{
1761
+ type: Component,
1762
+ args: [{ selector: 'ngx-dashboard-editor', standalone: true, imports: [
1763
+ CellComponent,
1764
+ CommonModule,
1765
+ DropZoneComponent,
1766
+ CellContextMenuComponent,
1767
+ ], providers: [
1768
+ CellContextMenuService,
1769
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1770
+ '[style.--rows]': 'rows()',
1771
+ '[style.--columns]': 'columns()',
1772
+ '[style.--gutter-size]': 'gutterSize()',
1773
+ '[style.--gutters]': 'gutters()',
1774
+ '[class.is-edit-mode]': 'true', // Always in edit mode
1775
+ }, template: "<!-- dashboard-editor.component.html -->\n<div class=\"grid-container\">\n <!-- Bottom grid with drop zones -->\n <div class=\"grid\" id=\"bottom-grid\" #bottomGrid>\n @for (position of dropzonePositions(); track position.id) {\n <lib-drop-zone\n class=\"drop-zone\"\n [row]=\"position.row\"\n [col]=\"position.col\"\n [index]=\"position.index\"\n [highlight]=\"highlightMap().has(createCellId(position.row, position.col))\"\n [highlightInvalid]=\"\n invalidHighlightMap().has(createCellId(position.row, position.col))\n \"\n [highlightResize]=\"\n resizePreviewMap().has(createCellId(position.row, position.col))\n \"\n [editMode]=\"true\"\n (dragEnter)=\"onDragEnter($event)\"\n (dragExit)=\"onDragExit()\"\n (dragOver)=\"onDragOver($event)\"\n (dragDrop)=\"onDragDrop($event)\"\n ></lib-drop-zone>\n }\n </div>\n\n <!-- Top grid with interactive cells -->\n <div class=\"grid\" id=\"top-grid\">\n @for (cell of cells(); track cell.widgetId) {\n <lib-cell\n class=\"grid-cell\"\n [widgetId]=\"cell.widgetId\"\n [cellId]=\"cell.cellId\"\n [isEditMode]=\"true\"\n [draggable]=\"true\"\n [row]=\"cell.row\"\n [column]=\"cell.col\"\n [rowSpan]=\"cell.rowSpan\"\n [colSpan]=\"cell.colSpan\"\n [flat]=\"cell.flat\"\n [widgetFactory]=\"cell.widgetFactory\"\n [widgetState]=\"cell.widgetState\"\n (dragStart)=\"onCellDragStart($event)\"\n (dragEnd)=\"dragEnd()\"\n (delete)=\"onCellDelete($event)\"\n (settings)=\"onCellSettings($event)\"\n (resizeStart)=\"onCellResizeStart($event)\"\n (resizeMove)=\"onCellResizeMove($event)\"\n (resizeEnd)=\"onCellResizeEnd($event)\"\n >\n </lib-cell>\n }\n </div>\n</div>\n\n<!-- Context menu -->\n<lib-cell-context-menu></lib-cell-context-menu>\n", styles: ["@charset \"UTF-8\";:host{--cell-size: calc( 100cqi / var(--columns) - var(--gutter-size) * var(--gutters) / var(--columns) );--tile-size: calc(var(--cell-size) + var(--gutter-size));--tile-offset: calc( var(--gutter-size) + var(--cell-size) + var(--gutter-size) / 2 );display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}:host .grid{background-image:linear-gradient(to right,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px),linear-gradient(to bottom,rgba(100,100,100,.12) 1px,transparent 1px);background-size:var(--tile-size) var(--tile-size),var(--tile-size) var(--tile-size),100% 1px;background-position:var(--tile-offset) var(--tile-offset),var(--tile-offset) var(--tile-offset),bottom;background-repeat:repeat,repeat,no-repeat}.grid-container{position:relative;width:100%;height:100%}.grid{display:grid;gap:var(--gutter-size);padding:var(--gutter-size);position:absolute;inset:0;width:100%;height:100%;box-sizing:border-box;align-items:stretch;justify-items:stretch;grid-template-columns:repeat(var(--columns),var(--cell-size));grid-template-rows:repeat(var(--rows),var(--cell-size))}#bottom-grid{z-index:1}#top-grid{z-index:2;pointer-events:none}.grid-cell{pointer-events:auto}.grid-cell.is-dragging{pointer-events:none;opacity:.5}\n"] }]
1776
+ }], ctorParameters: () => [] });
1777
+
1778
+ // dashboard-bridge.service.ts
1779
+ /**
1780
+ * Internal bridge service that coordinates between component-scoped DashboardStore instances
1781
+ * and standalone components like WidgetListComponent.
1782
+ *
1783
+ * This service is NOT part of the public API and should remain internal to the library.
1784
+ */
1785
+ class DashboardBridgeService {
1786
+ // Map of registered dashboard instances with their reactive dimensions
1787
+ dashboards = signal(new Map(), ...(ngDevMode ? [{ debugName: "dashboards" }] : []));
1788
+ /**
1789
+ * Register a dashboard store instance using its dashboard ID
1790
+ */
1791
+ registerDashboard(store) {
1792
+ const dashboardId = store.dashboardId();
1793
+ // If dashboard ID is not set yet, we'll register later when it's available
1794
+ if (!dashboardId) {
1795
+ return;
1796
+ }
1797
+ this.dashboards.update(dashboards => {
1798
+ const newMap = new Map(dashboards);
1799
+ newMap.set(dashboardId, {
1800
+ store,
1801
+ dimensions: store.gridCellDimensions
1802
+ });
1803
+ return newMap;
1804
+ });
1805
+ }
1806
+ /**
1807
+ * Unregister a dashboard store instance
1808
+ */
1809
+ unregisterDashboard(store) {
1810
+ const dashboardId = store.dashboardId();
1811
+ if (!dashboardId) {
1812
+ return;
1813
+ }
1814
+ this.dashboards.update(dashboards => {
1815
+ const newMap = new Map(dashboards);
1816
+ newMap.delete(dashboardId);
1817
+ return newMap;
1818
+ });
1819
+ }
1820
+ /**
1821
+ * Get cell dimensions for a specific dashboard instance
1822
+ */
1823
+ getDashboardDimensions(dashboardId) {
1824
+ const dashboard = this.dashboards().get(dashboardId);
1825
+ return dashboard?.dimensions() || { width: 100, height: 100 };
1826
+ }
1827
+ /**
1828
+ * Get all available dashboard dimensions (for widget lists to choose from)
1829
+ * Returns the first available dashboard's dimensions as fallback
1830
+ */
1831
+ availableDimensions = computed(() => {
1832
+ const dashboardEntries = Array.from(this.dashboards().values());
1833
+ if (dashboardEntries.length === 0) {
1834
+ return { width: 100, height: 100 }; // fallback
1835
+ }
1836
+ // Return dimensions from first available dashboard with fallback for undefined
1837
+ return dashboardEntries[0].dimensions() || { width: 100, height: 100 };
1838
+ }, ...(ngDevMode ? [{ debugName: "availableDimensions" }] : []));
1839
+ /**
1840
+ * Start drag operation on the first available dashboard
1841
+ * (Widget lists need some dashboard to coordinate with during drag)
1842
+ */
1843
+ startDrag(dragData) {
1844
+ const dashboardEntries = Array.from(this.dashboards().values());
1845
+ if (dashboardEntries.length > 0) {
1846
+ dashboardEntries[0].store.startDrag(dragData);
1847
+ }
1848
+ }
1849
+ /**
1850
+ * End drag operation on all dashboards
1851
+ * (Safer to notify all in case drag state got distributed)
1852
+ */
1853
+ endDrag() {
1854
+ for (const dashboard of this.dashboards().values()) {
1855
+ dashboard.store.endDrag();
1856
+ }
1857
+ }
1858
+ /**
1859
+ * Get all registered dashboard IDs
1860
+ */
1861
+ registeredDashboards = computed(() => Array.from(this.dashboards().keys()), ...(ngDevMode ? [{ debugName: "registeredDashboards" }] : []));
1862
+ /**
1863
+ * Get the number of registered dashboards
1864
+ */
1865
+ dashboardCount = computed(() => this.dashboards().size, ...(ngDevMode ? [{ debugName: "dashboardCount" }] : []));
1866
+ /**
1867
+ * Check if any dashboards are registered
1868
+ */
1869
+ hasDashboards = computed(() => this.dashboards().size > 0, ...(ngDevMode ? [{ debugName: "hasDashboards" }] : []));
1870
+ /**
1871
+ * Update registration for a dashboard store when its ID becomes available
1872
+ */
1873
+ updateDashboardRegistration(store) {
1874
+ this.registerDashboard(store);
1875
+ }
1876
+ /**
1877
+ * Get grid configuration for a specific dashboard
1878
+ */
1879
+ getDashboardGridConfig(dashboardId) {
1880
+ const dashboard = this.dashboards().get(dashboardId);
1881
+ if (!dashboard) {
1882
+ return null;
1883
+ }
1884
+ const store = dashboard.store;
1885
+ return {
1886
+ rows: store.rows(),
1887
+ columns: store.columns(),
1888
+ gutterSize: store.gutterSize()
1889
+ };
1890
+ }
1891
+ /**
1892
+ * Get the store instance for a specific dashboard ID
1893
+ */
1894
+ getDashboardStore(dashboardId) {
1895
+ const dashboard = this.dashboards().get(dashboardId);
1896
+ return dashboard?.store || null;
1897
+ }
1898
+ /**
1899
+ * Get all registered dashboards (for viewport service integration)
1900
+ */
1901
+ getAllDashboards() {
1902
+ return this.dashboards();
1903
+ }
1904
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardBridgeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1905
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardBridgeService, providedIn: 'root' });
1906
+ }
1907
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardBridgeService, decorators: [{
1908
+ type: Injectable,
1909
+ args: [{ providedIn: 'root' }]
1910
+ }] });
1911
+
1912
+ /**
1913
+ * Internal component-scoped service that provides viewport-aware constraints for a single dashboard.
1914
+ * Each dashboard component gets its own instance of this service.
1915
+ *
1916
+ * This service is NOT part of the public API and should remain internal to the library.
1917
+ */
1918
+ class DashboardViewportService {
1919
+ platformId = inject(PLATFORM_ID);
1920
+ destroyRef = inject(DestroyRef);
1921
+ store = inject(DashboardStore);
1922
+ viewportSize = signal({ width: 0, height: 0 }, ...(ngDevMode ? [{ debugName: "viewportSize" }] : []));
1923
+ reservedSpace = signal(DEFAULT_RESERVED_SPACE, ...(ngDevMode ? [{ debugName: "reservedSpace" }] : []));
1924
+ resizeObserver = null;
1925
+ constructor() {
1926
+ if (isPlatformBrowser(this.platformId)) {
1927
+ this.initializeViewportTracking();
1928
+ }
1929
+ }
1930
+ /**
1931
+ * Initialize viewport size tracking using ResizeObserver on the window
1932
+ */
1933
+ initializeViewportTracking() {
1934
+ // Use ResizeObserver on document.documentElement for accurate viewport tracking
1935
+ this.resizeObserver = new ResizeObserver((entries) => {
1936
+ if (entries.length > 0) {
1937
+ const entry = entries[0];
1938
+ const { inlineSize, blockSize } = entry.contentBoxSize[0];
1939
+ this.viewportSize.set({
1940
+ width: inlineSize,
1941
+ height: blockSize
1942
+ });
1943
+ }
1944
+ });
1945
+ this.resizeObserver.observe(document.documentElement);
1946
+ // Initial size
1947
+ this.viewportSize.set({
1948
+ width: window.innerWidth,
1949
+ height: window.innerHeight
1950
+ });
1951
+ // Cleanup on destroy
1952
+ this.destroyRef.onDestroy(() => {
1953
+ this.resizeObserver?.disconnect();
1954
+ });
1955
+ }
1956
+ /**
1957
+ * Set reserved space that should be excluded from dashboard calculations
1958
+ * (e.g., toolbar height, widget list width, padding)
1959
+ */
1960
+ setReservedSpace(space) {
1961
+ this.reservedSpace.set(space);
1962
+ }
1963
+ /**
1964
+ * Get current viewport size
1965
+ */
1966
+ currentViewportSize = this.viewportSize.asReadonly();
1967
+ /**
1968
+ * Get current reserved space
1969
+ */
1970
+ currentReservedSpace = this.reservedSpace.asReadonly();
1971
+ /**
1972
+ * Calculate available space for dashboard after accounting for reserved areas
1973
+ */
1974
+ availableSpace = computed(() => {
1975
+ const viewport = this.viewportSize();
1976
+ const reserved = this.reservedSpace();
1977
+ return {
1978
+ width: Math.max(0, viewport.width - reserved.left - reserved.right),
1979
+ height: Math.max(0, viewport.height - reserved.top - reserved.bottom)
1980
+ };
1981
+ }, ...(ngDevMode ? [{ debugName: "availableSpace" }] : []));
1982
+ /**
1983
+ * Calculate dashboard constraints for this dashboard instance
1984
+ */
1985
+ constraints = computed(() => {
1986
+ const availableSize = this.availableSpace();
1987
+ // Get grid configuration from our component's store
1988
+ const rows = this.store.rows();
1989
+ const columns = this.store.columns();
1990
+ if (rows === 0 || columns === 0) {
1991
+ return {
1992
+ maxWidth: availableSize.width,
1993
+ maxHeight: availableSize.height,
1994
+ constrainedBy: 'none'
1995
+ };
1996
+ }
1997
+ // Calculate aspect ratio
1998
+ const aspectRatio = columns / rows;
1999
+ // Calculate maximum size that fits within available space
2000
+ const maxWidthFromHeight = availableSize.height * aspectRatio;
2001
+ const maxHeightFromWidth = availableSize.width / aspectRatio;
2002
+ let maxWidth;
2003
+ let maxHeight;
2004
+ let constrainedBy;
2005
+ if (maxWidthFromHeight <= availableSize.width) {
2006
+ // Height is the limiting factor
2007
+ maxWidth = maxWidthFromHeight;
2008
+ maxHeight = availableSize.height;
2009
+ constrainedBy = 'height';
2010
+ }
2011
+ else {
2012
+ // Width is the limiting factor
2013
+ maxWidth = availableSize.width;
2014
+ maxHeight = maxHeightFromWidth;
2015
+ constrainedBy = 'width';
2016
+ }
2017
+ return {
2018
+ maxWidth: Math.max(0, maxWidth),
2019
+ maxHeight: Math.max(0, maxHeight),
2020
+ constrainedBy
2021
+ };
2022
+ }, ...(ngDevMode ? [{ debugName: "constraints" }] : []));
2023
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2024
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewportService });
2025
+ }
2026
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardViewportService, decorators: [{
2027
+ type: Injectable
2028
+ }], ctorParameters: () => [] });
2029
+
2030
+ // dashboard.component.ts
2031
+ //
2032
+ // A performant, modular, and fully reactive Angular dashboard container that orchestrates between
2033
+ // editing and viewing modes — with clean component separation and no external dependencies.
2034
+ class DashboardComponent {
2035
+ #store = inject(DashboardStore);
2036
+ #bridge = inject(DashboardBridgeService);
2037
+ #viewport = inject(DashboardViewportService);
2038
+ #destroyRef = inject(DestroyRef);
2039
+ // Public accessors for template
2040
+ store = this.#store;
2041
+ viewport = this.#viewport;
2042
+ // Component inputs
2043
+ dashboardData = input.required(...(ngDevMode ? [{ debugName: "dashboardData" }] : []));
2044
+ editMode = input(false, ...(ngDevMode ? [{ debugName: "editMode" }] : []));
2045
+ reservedSpace = input(...(ngDevMode ? [undefined, { debugName: "reservedSpace" }] : []));
2046
+ // Store signals - shared by both child components
2047
+ cells = this.#store.cells;
2048
+ // ViewChild references for export/import functionality
2049
+ dashboardEditor = viewChild(DashboardEditorComponent, ...(ngDevMode ? [{ debugName: "dashboardEditor" }] : []));
2050
+ dashboardViewer = viewChild(DashboardViewerComponent, ...(ngDevMode ? [{ debugName: "dashboardViewer" }] : []));
2051
+ // Track if we're in the middle of preserving states
2052
+ #isPreservingStates = false;
2053
+ // Track if component has been initialized
2054
+ #isInitialized = false;
2055
+ constructor() {
2056
+ // Cleanup registration when component is destroyed
2057
+ this.#destroyRef.onDestroy(() => {
2058
+ this.#bridge.unregisterDashboard(this.#store);
2059
+ });
2060
+ // Initialize from dashboardData
2061
+ effect(() => {
2062
+ const data = this.dashboardData();
2063
+ if (data) {
2064
+ this.#store.initializeFromDto(data);
2065
+ // Register with bridge service after dashboard ID is set
2066
+ this.#bridge.updateDashboardRegistration(this.#store);
2067
+ this.#isInitialized = true;
2068
+ }
2069
+ });
2070
+ // Sync edit mode with store (without triggering state preservation)
2071
+ effect(() => {
2072
+ const editMode = this.editMode();
2073
+ untracked(() => {
2074
+ this.#store.setEditMode(editMode);
2075
+ });
2076
+ });
2077
+ // Sync reserved space input with viewport service
2078
+ effect(() => {
2079
+ const reserved = this.reservedSpace();
2080
+ if (reserved) {
2081
+ this.#viewport.setReservedSpace(reserved);
2082
+ }
2083
+ });
2084
+ }
2085
+ ngOnChanges(changes) {
2086
+ // Handle edit mode changes after initialization
2087
+ if (changes['editMode'] &&
2088
+ !changes['editMode'].firstChange &&
2089
+ this.#isInitialized) {
2090
+ const previousValue = changes['editMode'].previousValue;
2091
+ const currentValue = changes['editMode'].currentValue;
2092
+ if (previousValue !== currentValue) {
2093
+ // Preserve widget states before the mode switch
2094
+ this.#preserveWidgetStatesBeforeModeSwitch(previousValue);
2095
+ }
2096
+ }
2097
+ }
2098
+ // Public export/import methods
2099
+ exportDashboard() {
2100
+ // Delegate to the active child component
2101
+ if (this.editMode()) {
2102
+ const editor = this.dashboardEditor();
2103
+ return editor ? editor.exportDashboard() : this.#store.exportDashboard();
2104
+ }
2105
+ else {
2106
+ const viewer = this.dashboardViewer();
2107
+ return viewer ? viewer.exportDashboard() : this.#store.exportDashboard();
2108
+ }
2109
+ }
2110
+ loadDashboard(data) {
2111
+ this.#store.loadDashboard(data);
2112
+ }
2113
+ getCurrentDashboardData() {
2114
+ return this.exportDashboard();
2115
+ }
2116
+ clearDashboard() {
2117
+ this.#store.clearDashboard();
2118
+ }
2119
+ /**
2120
+ * Preserve widget states before switching modes by collecting live states
2121
+ * from the currently active component and updating the store.
2122
+ */
2123
+ #preserveWidgetStatesBeforeModeSwitch(previousEditMode) {
2124
+ // Prevent re-entrant calls
2125
+ if (this.#isPreservingStates) {
2126
+ return;
2127
+ }
2128
+ this.#isPreservingStates = true;
2129
+ try {
2130
+ let currentWidgetStates = null;
2131
+ if (previousEditMode) {
2132
+ // Previously in edit mode, collect states from editor
2133
+ const editor = this.dashboardEditor();
2134
+ if (editor) {
2135
+ currentWidgetStates = editor.getCurrentWidgetStates();
2136
+ }
2137
+ }
2138
+ else {
2139
+ // Previously in view mode, collect states from viewer
2140
+ const viewer = this.dashboardViewer();
2141
+ if (viewer) {
2142
+ currentWidgetStates = viewer.getCurrentWidgetStates();
2143
+ }
2144
+ }
2145
+ // Update the store with the live widget states using untracked to avoid triggering effects
2146
+ if (currentWidgetStates && currentWidgetStates.size > 0) {
2147
+ untracked(() => {
2148
+ this.#store.updateAllWidgetStates(currentWidgetStates);
2149
+ });
2150
+ }
2151
+ }
2152
+ finally {
2153
+ this.#isPreservingStates = false;
2154
+ }
2155
+ }
2156
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2157
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: DashboardComponent, isStandalone: true, selector: "ngx-dashboard", inputs: { dashboardData: { classPropertyName: "dashboardData", publicName: "dashboardData", isSignal: true, isRequired: true, transformFunction: null }, editMode: { classPropertyName: "editMode", publicName: "editMode", isSignal: true, isRequired: false, transformFunction: null }, reservedSpace: { classPropertyName: "reservedSpace", publicName: "reservedSpace", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.--rows": "store.rows()", "style.--columns": "store.columns()", "style.--gutter-size": "store.gutterSize()", "style.--gutters": "store.columns() + 1", "class.is-edit-mode": "editMode()", "style.max-width.px": "viewport.constraints().maxWidth", "style.max-height.px": "viewport.constraints().maxHeight" } }, providers: [DashboardStore, DashboardViewportService], viewQueries: [{ propertyName: "dashboardEditor", first: true, predicate: DashboardEditorComponent, descendants: true, isSignal: true }, { propertyName: "dashboardViewer", first: true, predicate: DashboardViewerComponent, descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DashboardViewerComponent, selector: "ngx-dashboard-viewer", inputs: ["rows", "columns", "gutterSize"] }, { kind: "component", type: DashboardEditorComponent, selector: "ngx-dashboard-editor", inputs: ["rows", "columns", "gutterSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2158
+ }
2159
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DashboardComponent, decorators: [{
2160
+ type: Component,
2161
+ args: [{ selector: 'ngx-dashboard', standalone: true, imports: [CommonModule, DashboardViewerComponent, DashboardEditorComponent], providers: [DashboardStore, DashboardViewportService], changeDetection: ChangeDetectionStrategy.OnPush, host: {
2162
+ '[style.--rows]': 'store.rows()',
2163
+ '[style.--columns]': 'store.columns()',
2164
+ '[style.--gutter-size]': 'store.gutterSize()',
2165
+ '[style.--gutters]': 'store.columns() + 1',
2166
+ '[class.is-edit-mode]': 'editMode()',
2167
+ '[style.max-width.px]': 'viewport.constraints().maxWidth',
2168
+ '[style.max-height.px]': 'viewport.constraints().maxHeight',
2169
+ }, template: "<!-- dashboard.component.html -->\r\n<div class=\"grid-container\">\r\n @if (editMode()) {\r\n <!-- Full editor with drag & drop capabilities -->\r\n <ngx-dashboard-editor\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-editor>\r\n } @else {\r\n <!-- Read-only viewer -->\r\n <ngx-dashboard-viewer\r\n [rows]=\"store.rows()\"\r\n [columns]=\"store.columns()\"\r\n [gutterSize]=\"store.gutterSize()\"\r\n ></ngx-dashboard-viewer>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:inline-size;box-sizing:border-box;aspect-ratio:var(--columns)/var(--rows);width:100%;height:auto}.grid-container{position:relative;width:100%;height:100%}\n"] }]
2170
+ }], ctorParameters: () => [] });
2171
+
2172
+ // widget-list.component.ts
2173
+ class WidgetListComponent {
2174
+ #service = inject(DashboardService);
2175
+ #sanitizer = inject(DomSanitizer);
2176
+ #renderer = inject(Renderer2);
2177
+ #bridge = inject(DashboardBridgeService);
2178
+ activeWidget = signal(null, ...(ngDevMode ? [{ debugName: "activeWidget" }] : []));
2179
+ // Get grid cell dimensions from bridge service (uses first available dashboard)
2180
+ gridCellDimensions = this.#bridge.availableDimensions;
2181
+ widgets = computed(() => this.#service.widgetTypes().map((w) => ({
2182
+ ...w.metadata,
2183
+ safeSvgIcon: this.#sanitizer.bypassSecurityTrustHtml(w.metadata.svgIcon),
2184
+ })), ...(ngDevMode ? [{ debugName: "widgets" }] : []));
2185
+ onDragStart(event, widget) {
2186
+ if (!event.dataTransfer)
2187
+ return;
2188
+ event.dataTransfer.effectAllowed = 'copy';
2189
+ const dragData = {
2190
+ kind: 'widget',
2191
+ content: widget,
2192
+ };
2193
+ this.activeWidget.set(widget.widgetTypeid);
2194
+ this.#bridge.startDrag(dragData);
2195
+ // Create custom drag ghost for better UX
2196
+ const ghost = this.#createDragGhost(widget.svgIcon);
2197
+ document.body.appendChild(ghost);
2198
+ // Force reflow to ensure element is rendered
2199
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2200
+ const _reflow = ghost.offsetHeight;
2201
+ event.dataTransfer.setDragImage(ghost, 10, 10);
2202
+ // Delay removal to ensure browser has time to snapshot the drag image
2203
+ setTimeout(() => ghost.remove());
2204
+ }
2205
+ onDragEnd() {
2206
+ this.activeWidget.set(null);
2207
+ this.#bridge.endDrag();
2208
+ }
2209
+ #createDragGhost(svgIcon) {
2210
+ const dimensions = this.gridCellDimensions();
2211
+ const el = this.#renderer.createElement('div');
2212
+ this.#renderer.addClass(el, 'drag-ghost');
2213
+ // Set dimensions using CSS custom properties for dynamic sizing
2214
+ this.#renderer.setStyle(el, 'width', `${dimensions.width}px`);
2215
+ this.#renderer.setStyle(el, 'height', `${dimensions.height}px`);
2216
+ if (svgIcon) {
2217
+ const iconWrapper = this.#renderer.createElement('div');
2218
+ this.#renderer.addClass(iconWrapper, 'icon');
2219
+ iconWrapper.innerHTML = svgIcon;
2220
+ const svg = iconWrapper.querySelector('svg');
2221
+ if (svg) {
2222
+ // Size the SVG to 80% of the cell dimensions
2223
+ svg.setAttribute('width', `${dimensions.width * 0.8}`);
2224
+ svg.setAttribute('height', `${dimensions.height * 0.8}`);
2225
+ // Remove hardcoded fill and opacity - let CSS handle styling
2226
+ svg.removeAttribute('fill');
2227
+ }
2228
+ el.appendChild(iconWrapper);
2229
+ }
2230
+ return el;
2231
+ }
2232
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2233
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: WidgetListComponent, isStandalone: true, selector: "ngx-dashboard-widget-list", ngImport: i0, template: "<!-- widget-list.component.html -->\r\n<div class=\"widget-list\" role=\"list\" aria-label=\"Available widgets\">\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n role=\"listitem\"\r\n [attr.aria-grabbed]=\"activeWidget() === widget.widgetTypeid\"\r\n [attr.aria-label]=\"widget.name + ' widget: ' + widget.description\"\r\n tabindex=\"0\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\" aria-hidden=\"true\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>", styles: [":host{background-color:var(--mat-sys-surface-container, #f5f5f5);container-type:inline-size}.widget-list{display:flex;flex-direction:column;gap:var(--mat-sys-spacing-2, 8px)}@container (max-width: 200px){.widget-list{gap:var(--mat-sys-spacing-1, 4px)}}@container (min-width: 400px){.widget-list{gap:var(--mat-sys-spacing-3, 12px)}}.widget-list-item{display:flex;align-items:start;gap:var(--mat-sys-spacing-3, 12px);background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);padding:var(--mat-sys-spacing-3, 12px);border-radius:var(--mat-sys-corner-small, 4px);cursor:grab;transition:background-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),border-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),box-shadow var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out);box-shadow:var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, .05))}@container (max-width: 200px){.widget-list-item{padding:var(--mat-sys-spacing-2, 8px);gap:var(--mat-sys-spacing-2, 8px)}}@container (min-width: 400px){.widget-list-item{padding:var(--mat-sys-spacing-4, 16px);gap:var(--mat-sys-spacing-4, 16px)}}.widget-list-item .icon{width:clamp(20px,4vw,28px);height:clamp(20px,4vw,28px);flex-shrink:0;color:var(--mat-sys-on-surface-variant, #5f5f5f);transition:color var(--mat-sys-motion-duration-short2, .15s) var(--mat-sys-motion-easing-standard, ease-in-out)}.widget-list-item .icon ::ng-deep svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2;color:var(--mat-sys-on-surface, #1c1c1c);flex:1;min-width:0}.widget-list-item .content strong{color:var(--mat-sys-on-surface, #1c1c1c);font-weight:500;font-size:clamp(.875rem,2.5vw,1rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item .content small{color:var(--mat-sys-on-surface-variant, #5f5f5f);font-size:clamp(.75rem,2vw,.875rem);margin-top:var(--mat-sys-spacing-1, 4px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item:hover{background-color:var(--mat-sys-surface-container-low, #f0f0f0);box-shadow:var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, .1))}.widget-list-item:hover .icon{color:var(--mat-sys-on-surface, #1c1c1c)}.widget-list-item:active{cursor:grabbing;background-color:var(--mat-sys-surface-container, #f5f5f5)}.widget-list-item.active{background-color:var(--mat-sys-primary-container, #e6f2ff);border-color:var(--mat-sys-primary, #1976d2);color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content strong{color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content small{color:var(--mat-sys-on-primary-container, #004a99);opacity:.8}.widget-list-item.active .icon{color:var(--mat-sys-on-primary-container, #004a99)}.drag-ghost{position:absolute;top:0;left:0;z-index:9999;margin:0;pointer-events:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);border-radius:var(--mat-sys-corner-small, 4px);box-shadow:var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, .15));opacity:.8}.drag-ghost .icon{display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #5f5f5f);opacity:.6}.drag-ghost .icon ::ng-deep svg{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2234
+ }
2235
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: WidgetListComponent, decorators: [{
2236
+ type: Component,
2237
+ args: [{ selector: 'ngx-dashboard-widget-list', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- widget-list.component.html -->\r\n<div class=\"widget-list\" role=\"list\" aria-label=\"Available widgets\">\r\n @for (widget of widgets(); track widget.widgetTypeid) {\r\n <div\r\n class=\"widget-list-item\"\r\n [class.active]=\"activeWidget() === widget.widgetTypeid\"\r\n draggable=\"true\"\r\n (dragstart)=\"onDragStart($event, widget)\"\r\n (dragend)=\"onDragEnd()\"\r\n role=\"listitem\"\r\n [attr.aria-grabbed]=\"activeWidget() === widget.widgetTypeid\"\r\n [attr.aria-label]=\"widget.name + ' widget: ' + widget.description\"\r\n tabindex=\"0\"\r\n >\r\n <div class=\"icon\" [innerHTML]=\"widget.safeSvgIcon\" aria-hidden=\"true\"></div>\r\n <div class=\"content\">\r\n <strong>{{ widget.name }}</strong>\r\n <small>{{ widget.description }}</small>\r\n </div>\r\n </div>\r\n }\r\n</div>", styles: [":host{background-color:var(--mat-sys-surface-container, #f5f5f5);container-type:inline-size}.widget-list{display:flex;flex-direction:column;gap:var(--mat-sys-spacing-2, 8px)}@container (max-width: 200px){.widget-list{gap:var(--mat-sys-spacing-1, 4px)}}@container (min-width: 400px){.widget-list{gap:var(--mat-sys-spacing-3, 12px)}}.widget-list-item{display:flex;align-items:start;gap:var(--mat-sys-spacing-3, 12px);background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);padding:var(--mat-sys-spacing-3, 12px);border-radius:var(--mat-sys-corner-small, 4px);cursor:grab;transition:background-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),border-color var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out),box-shadow var(--mat-sys-motion-duration-medium2, .3s) var(--mat-sys-motion-easing-standard, ease-in-out);box-shadow:var(--mat-sys-elevation-level1, 0 1px 2px rgba(0, 0, 0, .05))}@container (max-width: 200px){.widget-list-item{padding:var(--mat-sys-spacing-2, 8px);gap:var(--mat-sys-spacing-2, 8px)}}@container (min-width: 400px){.widget-list-item{padding:var(--mat-sys-spacing-4, 16px);gap:var(--mat-sys-spacing-4, 16px)}}.widget-list-item .icon{width:clamp(20px,4vw,28px);height:clamp(20px,4vw,28px);flex-shrink:0;color:var(--mat-sys-on-surface-variant, #5f5f5f);transition:color var(--mat-sys-motion-duration-short2, .15s) var(--mat-sys-motion-easing-standard, ease-in-out)}.widget-list-item .icon ::ng-deep svg{width:100%;height:100%;display:block}.widget-list-item .content{display:flex;flex-direction:column;line-height:1.2;color:var(--mat-sys-on-surface, #1c1c1c);flex:1;min-width:0}.widget-list-item .content strong{color:var(--mat-sys-on-surface, #1c1c1c);font-weight:500;font-size:clamp(.875rem,2.5vw,1rem);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item .content small{color:var(--mat-sys-on-surface-variant, #5f5f5f);font-size:clamp(.75rem,2vw,.875rem);margin-top:var(--mat-sys-spacing-1, 4px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.widget-list-item:hover{background-color:var(--mat-sys-surface-container-low, #f0f0f0);box-shadow:var(--mat-sys-elevation-level2, 0 2px 4px rgba(0, 0, 0, .1))}.widget-list-item:hover .icon{color:var(--mat-sys-on-surface, #1c1c1c)}.widget-list-item:active{cursor:grabbing;background-color:var(--mat-sys-surface-container, #f5f5f5)}.widget-list-item.active{background-color:var(--mat-sys-primary-container, #e6f2ff);border-color:var(--mat-sys-primary, #1976d2);color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content strong{color:var(--mat-sys-on-primary-container, #004a99)}.widget-list-item.active .content small{color:var(--mat-sys-on-primary-container, #004a99);opacity:.8}.widget-list-item.active .icon{color:var(--mat-sys-on-primary-container, #004a99)}.drag-ghost{position:absolute;top:0;left:0;z-index:9999;margin:0;pointer-events:none;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background-color:var(--mat-sys-surface, #ffffff);border:1px solid var(--mat-sys-outline-variant, #c7c7c7);border-radius:var(--mat-sys-corner-small, 4px);box-shadow:var(--mat-sys-elevation-level3, 0 4px 6px rgba(0, 0, 0, .15));opacity:.8}.drag-ghost .icon{display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #5f5f5f);opacity:.6}.drag-ghost .icon ::ng-deep svg{display:block}\n"] }]
2238
+ }] });
2239
+
2240
+ /*
2241
+ * Public API Surface of ngx-dashboard
2242
+ */
2243
+ // Main dashboard components
2244
+
2245
+ /**
2246
+ * Generated bundle index. Do not edit.
2247
+ */
2248
+
2249
+ export { CELL_SETTINGS_DIALOG_PROVIDER, CellIdUtils, CellSettingsDialogProvider, DEFAULT_RESERVED_SPACE, DashboardComponent, DashboardEditorComponent, DashboardService, DashboardViewerComponent, DefaultCellSettingsDialogProvider, WidgetIdUtils, WidgetListComponent, createDefaultDashboard, createEmptyDashboard, createFactoryFromComponent };
2250
+ //# sourceMappingURL=dragonworks-ngx-dashboard.mjs.map