@ogidor/dashboard 1.0.3 → 1.0.4

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 (39) hide show
  1. package/README.md +283 -48
  2. package/app/app.module.d.ts +20 -0
  3. package/app/custom-grid.component.d.ts +93 -0
  4. package/app/dashboard-state.service.d.ts +70 -0
  5. package/app/dashboard.component.d.ts +48 -0
  6. package/app/models.d.ts +53 -0
  7. package/app/widget-renderer.component.d.ts +13 -0
  8. package/esm2020/app/app.module.mjs +57 -0
  9. package/esm2020/app/custom-grid.component.mjs +509 -0
  10. package/esm2020/app/dashboard-state.service.mjs +288 -0
  11. package/esm2020/app/dashboard.component.mjs +299 -0
  12. package/esm2020/app/models.mjs +2 -0
  13. package/esm2020/app/widget-renderer.component.mjs +83 -0
  14. package/esm2020/public-api.mjs +10 -0
  15. package/fesm2015/ogidor-dashboard.mjs +1233 -0
  16. package/fesm2015/ogidor-dashboard.mjs.map +1 -0
  17. package/fesm2020/ogidor-dashboard.mjs +1229 -0
  18. package/fesm2020/ogidor-dashboard.mjs.map +1 -0
  19. package/package.json +29 -43
  20. package/{dist-lib/public-api.d.ts → public-api.d.ts} +3 -2
  21. package/dist-lib/README.md +0 -103
  22. package/dist-lib/app/app.module.d.ts +0 -22
  23. package/dist-lib/app/dashboard-state.service.d.ts +0 -49
  24. package/dist-lib/app/dashboard.component.d.ts +0 -80
  25. package/dist-lib/app/models.d.ts +0 -73
  26. package/dist-lib/app/widget-renderer.component.d.ts +0 -40
  27. package/dist-lib/esm2020/app/app.module.mjs +0 -65
  28. package/dist-lib/esm2020/app/dashboard-state.service.mjs +0 -218
  29. package/dist-lib/esm2020/app/dashboard.component.mjs +0 -703
  30. package/dist-lib/esm2020/app/models.mjs +0 -2
  31. package/dist-lib/esm2020/app/widget-renderer.component.mjs +0 -163
  32. package/dist-lib/esm2020/public-api.mjs +0 -9
  33. package/dist-lib/fesm2015/ogidor-dashboard.mjs +0 -1154
  34. package/dist-lib/fesm2015/ogidor-dashboard.mjs.map +0 -1
  35. package/dist-lib/fesm2020/ogidor-dashboard.mjs +0 -1145
  36. package/dist-lib/fesm2020/ogidor-dashboard.mjs.map +0 -1
  37. package/dist-lib/package.json +0 -51
  38. /package/{dist-lib/esm2020 → esm2020}/ogidor-dashboard.mjs +0 -0
  39. /package/{dist-lib/index.d.ts → index.d.ts} +0 -0
@@ -0,0 +1,1229 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Directive, EventEmitter, TemplateRef, Component, ChangeDetectionStrategy, Input, Output, ViewChild, ContentChild, Injectable, NgModule } from '@angular/core';
3
+ import { BrowserModule } from '@angular/platform-browser';
4
+ import * as i2 from '@angular/common';
5
+ import { CommonModule } from '@angular/common';
6
+ import { BehaviorSubject, Subscription } from 'rxjs';
7
+
8
+ /**
9
+ * Directive applied to the template that should be stamped inside each grid cell.
10
+ * Usage:
11
+ * <app-grid [widgets]="...">
12
+ * <ng-template gridCell let-widget="widget">
13
+ * <app-widget-renderer [widget]="widget" ...></app-widget-renderer>
14
+ * </ng-template>
15
+ * </app-grid>
16
+ */
17
+ class GridCellDirective {
18
+ constructor(templateRef) {
19
+ this.templateRef = templateRef;
20
+ }
21
+ }
22
+ GridCellDirective.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: GridCellDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
23
+ GridCellDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "15.2.10", type: GridCellDirective, selector: "[gridCell]", ngImport: i0 });
24
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: GridCellDirective, decorators: [{
25
+ type: Directive,
26
+ args: [{ selector: '[gridCell]' }]
27
+ }], ctorParameters: function () { return [{ type: i0.TemplateRef }]; } });
28
+ class GridEngine {
29
+ /**
30
+ * Check if two rects overlap.
31
+ */
32
+ static collides(a, b) {
33
+ return !(a.x + a.cols <= b.x ||
34
+ b.x + b.cols <= a.x ||
35
+ a.y + a.rows <= b.y ||
36
+ b.y + b.rows <= a.y);
37
+ }
38
+ /**
39
+ * Get ALL widgets that collide with `rect`.
40
+ */
41
+ static getAllCollisions(widgets, rect) {
42
+ return widgets.filter(w => w.id !== rect.id && GridEngine.collides(w, rect));
43
+ }
44
+ /**
45
+ * Sort widgets top-to-bottom, left-to-right.
46
+ */
47
+ static sortByPosition(widgets) {
48
+ return [...widgets].sort((a, b) => a.y - b.y || a.x - b.x);
49
+ }
50
+ /**
51
+ * Move a widget to (x, y) and push any colliding widgets downward.
52
+ * Returns the full list with resolved positions.
53
+ */
54
+ static moveWidget(allWidgets, widget, newX, newY, columns) {
55
+ const widgets = allWidgets.map(w => ({ ...w }));
56
+ const moving = widgets.find(w => w.id === widget.id);
57
+ if (!moving)
58
+ return widgets;
59
+ moving.x = Math.max(0, Math.min(newX, columns - moving.cols));
60
+ moving.y = Math.max(0, newY);
61
+ const sorted = GridEngine.sortByPosition(widgets);
62
+ GridEngine.resolveCollisions(sorted, moving);
63
+ return GridEngine.compact(sorted, columns);
64
+ }
65
+ /**
66
+ * Resize a widget and push colliding widgets down.
67
+ */
68
+ static resizeWidget(allWidgets, widget, newCols, newRows, columns) {
69
+ const widgets = allWidgets.map(w => ({ ...w }));
70
+ const resizing = widgets.find(w => w.id === widget.id);
71
+ if (!resizing)
72
+ return widgets;
73
+ resizing.cols = Math.max(1, Math.min(newCols, columns - resizing.x));
74
+ resizing.rows = Math.max(1, newRows);
75
+ const sorted = GridEngine.sortByPosition(widgets);
76
+ GridEngine.resolveCollisions(sorted, resizing);
77
+ return GridEngine.compact(sorted, columns);
78
+ }
79
+ /**
80
+ * Push all widgets that collide with `movedWidget` downward, recursively.
81
+ */
82
+ static resolveCollisions(widgets, movedWidget) {
83
+ const collisions = GridEngine.getAllCollisions(widgets, movedWidget);
84
+ for (const collider of collisions) {
85
+ collider.y = movedWidget.y + movedWidget.rows;
86
+ GridEngine.resolveCollisions(widgets, collider);
87
+ }
88
+ }
89
+ /**
90
+ * Compact the grid: move every widget as far up as possible without overlapping.
91
+ */
92
+ static compact(widgets, _columns) {
93
+ const sorted = GridEngine.sortByPosition(widgets);
94
+ const placed = [];
95
+ for (const widget of sorted) {
96
+ widget.y = GridEngine.findCompactY(placed, widget);
97
+ placed.push(widget);
98
+ }
99
+ return placed;
100
+ }
101
+ /**
102
+ * Find the highest Y position a widget can occupy without overlapping any already-placed widget.
103
+ */
104
+ static findCompactY(placed, widget) {
105
+ let y = 0;
106
+ while (true) {
107
+ const test = { ...widget, y };
108
+ const collision = placed.find(p => GridEngine.collides(p, test));
109
+ if (!collision)
110
+ return y;
111
+ y = collision.y + collision.rows;
112
+ }
113
+ }
114
+ /**
115
+ * Compute the total number of rows the grid needs.
116
+ */
117
+ static getGridHeight(widgets) {
118
+ if (widgets.length === 0)
119
+ return 0;
120
+ return Math.max(...widgets.map(w => w.y + w.rows));
121
+ }
122
+ }
123
+ /* ═══════════════════════════════════════════════════════════════════════
124
+ CUSTOM GRID COMPONENT
125
+ ═══════════════════════════════════════════════════════════════════════ */
126
+ class CustomGridComponent {
127
+ constructor(zone, cdr) {
128
+ this.zone = zone;
129
+ this.cdr = cdr;
130
+ this.widgets = [];
131
+ this.columns = 12;
132
+ this.gap = 16;
133
+ this.rowHeight = 80;
134
+ this.minItemCols = 1;
135
+ this.minItemRows = 1;
136
+ this.itemChanged = new EventEmitter();
137
+ this.layoutChanged = new EventEmitter();
138
+ this.placeholder = null;
139
+ this.containerHeight = 400;
140
+ this.dragging = null;
141
+ this.resizing = null;
142
+ this.previewWidgets = [];
143
+ this.boundMouseMove = this.onMouseMove.bind(this);
144
+ this.boundMouseUp = this.onMouseUp.bind(this);
145
+ }
146
+ ngOnInit() {
147
+ this.compactAndApply();
148
+ this.updateContainerHeight();
149
+ this.zone.runOutsideAngular(() => {
150
+ window.addEventListener('mousemove', this.boundMouseMove);
151
+ window.addEventListener('mouseup', this.boundMouseUp);
152
+ });
153
+ }
154
+ ngOnChanges(_changes) {
155
+ this.updateContainerHeight();
156
+ }
157
+ ngOnDestroy() {
158
+ window.removeEventListener('mousemove', this.boundMouseMove);
159
+ window.removeEventListener('mouseup', this.boundMouseUp);
160
+ }
161
+ trackByFn(_index, item) { return item.id; }
162
+ // ── Geometry ──
163
+ get cellWidth() {
164
+ const containerW = this.gridContainer?.nativeElement?.clientWidth ?? 800;
165
+ return (containerW - (this.columns - 1) * this.gap) / this.columns;
166
+ }
167
+ colToPixel(col) {
168
+ return col * (this.cellWidth + this.gap);
169
+ }
170
+ rowToPixel(row) {
171
+ return row * (this.rowHeight + this.gap);
172
+ }
173
+ pixelToCol(px) {
174
+ return Math.round(px / (this.cellWidth + this.gap));
175
+ }
176
+ pixelToRow(px) {
177
+ return Math.round(px / (this.rowHeight + this.gap));
178
+ }
179
+ colsToPixelWidth(cols) {
180
+ return cols * this.cellWidth + (cols - 1) * this.gap;
181
+ }
182
+ rowsToPixelHeight(rows) {
183
+ return rows * this.rowHeight + (rows - 1) * this.gap;
184
+ }
185
+ getItemLeft(w) {
186
+ return this.dragging?.id === w.id ? this.dragging.currentPixelX : this.colToPixel(w.x);
187
+ }
188
+ getItemTop(w) {
189
+ return this.dragging?.id === w.id ? this.dragging.currentPixelY : this.rowToPixel(w.y);
190
+ }
191
+ getItemWidth(w) {
192
+ return this.resizing?.id === w.id ? this.resizing.currentPixelW : this.colsToPixelWidth(w.cols);
193
+ }
194
+ getItemHeight(w) {
195
+ return this.resizing?.id === w.id ? this.resizing.currentPixelH : this.rowsToPixelHeight(w.rows);
196
+ }
197
+ // ── Drag ──
198
+ onDragStart(event, widget) {
199
+ const target = event.target;
200
+ if (target.closest('button, input, select, textarea, a, .resize-handle'))
201
+ return;
202
+ event.preventDefault();
203
+ event.stopPropagation();
204
+ const rect = this.gridContainer.nativeElement.getBoundingClientRect();
205
+ const itemLeft = this.colToPixel(widget.x);
206
+ const itemTop = this.rowToPixel(widget.y);
207
+ this.dragging = {
208
+ id: widget.id,
209
+ offsetX: event.clientX - rect.left - itemLeft,
210
+ offsetY: event.clientY - rect.top - itemTop,
211
+ currentPixelX: itemLeft,
212
+ currentPixelY: itemTop,
213
+ };
214
+ this.previewWidgets = this.widgets.map(w => ({ id: w.id, x: w.x, y: w.y, cols: w.cols, rows: w.rows }));
215
+ this.placeholder = {
216
+ left: itemLeft, top: itemTop,
217
+ width: this.colsToPixelWidth(widget.cols),
218
+ height: this.rowsToPixelHeight(widget.rows),
219
+ };
220
+ this.cdr.markForCheck();
221
+ }
222
+ onResizeStart(event, widget) {
223
+ event.preventDefault();
224
+ event.stopPropagation();
225
+ this.resizing = {
226
+ id: widget.id,
227
+ startMouseX: event.clientX,
228
+ startMouseY: event.clientY,
229
+ startCols: widget.cols,
230
+ startRows: widget.rows,
231
+ currentPixelW: this.colsToPixelWidth(widget.cols),
232
+ currentPixelH: this.rowsToPixelHeight(widget.rows),
233
+ };
234
+ this.previewWidgets = this.widgets.map(w => ({ id: w.id, x: w.x, y: w.y, cols: w.cols, rows: w.rows }));
235
+ this.placeholder = {
236
+ left: this.colToPixel(widget.x), top: this.rowToPixel(widget.y),
237
+ width: this.colsToPixelWidth(widget.cols),
238
+ height: this.rowsToPixelHeight(widget.rows),
239
+ };
240
+ this.cdr.markForCheck();
241
+ }
242
+ onMouseMove(event) {
243
+ if (this.dragging)
244
+ this.handleDragMove(event);
245
+ else if (this.resizing)
246
+ this.handleResizeMove(event);
247
+ }
248
+ onMouseUp(_event) {
249
+ if (this.dragging)
250
+ this.zone.run(() => this.finalizeDrag());
251
+ else if (this.resizing)
252
+ this.zone.run(() => this.finalizeResize());
253
+ }
254
+ handleDragMove(event) {
255
+ if (!this.dragging)
256
+ return;
257
+ const rect = this.gridContainer.nativeElement.getBoundingClientRect();
258
+ const widget = this.widgets.find(w => w.id === this.dragging.id);
259
+ if (!widget)
260
+ return;
261
+ let px = event.clientX - rect.left - this.dragging.offsetX;
262
+ let py = event.clientY - rect.top - this.dragging.offsetY;
263
+ px = Math.max(0, Math.min(px, rect.width - this.colsToPixelWidth(widget.cols)));
264
+ py = Math.max(0, py);
265
+ this.dragging.currentPixelX = px;
266
+ this.dragging.currentPixelY = py;
267
+ const targetCol = Math.max(0, Math.min(this.pixelToCol(px), this.columns - widget.cols));
268
+ const targetRow = Math.max(0, this.pixelToRow(py));
269
+ const resolved = GridEngine.moveWidget(this.previewWidgets, { id: widget.id, x: widget.x, y: widget.y, cols: widget.cols, rows: widget.rows }, targetCol, targetRow, this.columns);
270
+ this.placeholder = {
271
+ left: this.colToPixel(targetCol), top: this.rowToPixel(targetRow),
272
+ width: this.colsToPixelWidth(widget.cols),
273
+ height: this.rowsToPixelHeight(widget.rows),
274
+ };
275
+ this.zone.run(() => {
276
+ for (const rw of resolved) {
277
+ if (rw.id === widget.id)
278
+ continue;
279
+ const w = this.widgets.find(ww => ww.id === rw.id);
280
+ if (w) {
281
+ w.x = rw.x;
282
+ w.y = rw.y;
283
+ }
284
+ }
285
+ this.updateContainerHeight();
286
+ this.cdr.markForCheck();
287
+ });
288
+ }
289
+ finalizeDrag() {
290
+ if (!this.dragging)
291
+ return;
292
+ const widget = this.widgets.find(w => w.id === this.dragging.id);
293
+ const targetCol = Math.max(0, Math.min(this.pixelToCol(this.dragging.currentPixelX), this.columns - widget.cols));
294
+ const targetRow = Math.max(0, this.pixelToRow(this.dragging.currentPixelY));
295
+ const resolved = GridEngine.moveWidget(this.widgets.map(w => ({ id: w.id, x: w.x, y: w.y, cols: w.cols, rows: w.rows })), { id: widget.id, x: widget.x, y: widget.y, cols: widget.cols, rows: widget.rows }, targetCol, targetRow, this.columns);
296
+ for (const rw of resolved) {
297
+ const w = this.widgets.find(ww => ww.id === rw.id);
298
+ if (w) {
299
+ w.x = rw.x;
300
+ w.y = rw.y;
301
+ w.cols = rw.cols;
302
+ w.rows = rw.rows;
303
+ this.itemChanged.emit(w);
304
+ }
305
+ }
306
+ this.dragging = null;
307
+ this.placeholder = null;
308
+ this.previewWidgets = [];
309
+ this.updateContainerHeight();
310
+ this.layoutChanged.emit(this.widgets);
311
+ this.cdr.markForCheck();
312
+ }
313
+ handleResizeMove(event) {
314
+ if (!this.resizing)
315
+ return;
316
+ const widget = this.widgets.find(w => w.id === this.resizing.id);
317
+ if (!widget)
318
+ return;
319
+ const dx = event.clientX - this.resizing.startMouseX;
320
+ const dy = event.clientY - this.resizing.startMouseY;
321
+ let newCols = Math.max(this.minItemCols, Math.round((this.colsToPixelWidth(this.resizing.startCols) + dx) / (this.cellWidth + this.gap)));
322
+ let newRows = Math.max(this.minItemRows, Math.round((this.rowsToPixelHeight(this.resizing.startRows) + dy) / (this.rowHeight + this.gap)));
323
+ newCols = Math.min(newCols, this.columns - widget.x);
324
+ this.resizing.currentPixelW = this.colsToPixelWidth(newCols);
325
+ this.resizing.currentPixelH = this.rowsToPixelHeight(newRows);
326
+ const resolved = GridEngine.resizeWidget(this.previewWidgets, { id: widget.id, x: widget.x, y: widget.y, cols: widget.cols, rows: widget.rows }, newCols, newRows, this.columns);
327
+ this.placeholder = {
328
+ left: this.colToPixel(widget.x), top: this.rowToPixel(widget.y),
329
+ width: this.colsToPixelWidth(newCols),
330
+ height: this.rowsToPixelHeight(newRows),
331
+ };
332
+ this.zone.run(() => {
333
+ for (const rw of resolved) {
334
+ if (rw.id === widget.id)
335
+ continue;
336
+ const w = this.widgets.find(ww => ww.id === rw.id);
337
+ if (w) {
338
+ w.x = rw.x;
339
+ w.y = rw.y;
340
+ }
341
+ }
342
+ this.updateContainerHeight();
343
+ this.cdr.markForCheck();
344
+ });
345
+ }
346
+ finalizeResize() {
347
+ if (!this.resizing)
348
+ return;
349
+ const widget = this.widgets.find(w => w.id === this.resizing.id);
350
+ let newCols = Math.max(this.minItemCols, Math.round(this.resizing.currentPixelW / (this.cellWidth + this.gap)));
351
+ let newRows = Math.max(this.minItemRows, Math.round(this.resizing.currentPixelH / (this.rowHeight + this.gap)));
352
+ newCols = Math.min(newCols, this.columns - widget.x);
353
+ const resolved = GridEngine.resizeWidget(this.widgets.map(w => ({ id: w.id, x: w.x, y: w.y, cols: w.cols, rows: w.rows })), { id: widget.id, x: widget.x, y: widget.y, cols: widget.cols, rows: widget.rows }, newCols, newRows, this.columns);
354
+ for (const rw of resolved) {
355
+ const w = this.widgets.find(ww => ww.id === rw.id);
356
+ if (w) {
357
+ w.x = rw.x;
358
+ w.y = rw.y;
359
+ w.cols = rw.cols;
360
+ w.rows = rw.rows;
361
+ this.itemChanged.emit(w);
362
+ }
363
+ }
364
+ this.resizing = null;
365
+ this.placeholder = null;
366
+ this.previewWidgets = [];
367
+ this.updateContainerHeight();
368
+ this.layoutChanged.emit(this.widgets);
369
+ this.cdr.markForCheck();
370
+ }
371
+ // ── Utilities ──
372
+ compactAndApply() {
373
+ if (!this.widgets?.length)
374
+ return;
375
+ const compacted = GridEngine.compact(this.widgets.map(w => ({ id: w.id, x: w.x, y: w.y, cols: w.cols, rows: w.rows })), this.columns);
376
+ for (const rw of compacted) {
377
+ const w = this.widgets.find(ww => ww.id === rw.id);
378
+ if (w) {
379
+ w.x = rw.x;
380
+ w.y = rw.y;
381
+ }
382
+ }
383
+ }
384
+ updateContainerHeight() {
385
+ if (!this.widgets?.length) {
386
+ this.containerHeight = 400;
387
+ return;
388
+ }
389
+ const maxRow = GridEngine.getGridHeight(this.widgets.map(w => ({ id: w.id, x: w.x, y: w.y, cols: w.cols, rows: w.rows })));
390
+ this.containerHeight = this.rowToPixel(maxRow) + this.rowHeight + this.gap;
391
+ }
392
+ }
393
+ CustomGridComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CustomGridComponent, deps: [{ token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
394
+ CustomGridComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: CustomGridComponent, selector: "app-grid", inputs: { widgets: "widgets", columns: "columns", gap: "gap", rowHeight: "rowHeight", minItemCols: "minItemCols", minItemRows: "minItemRows" }, outputs: { itemChanged: "itemChanged", layoutChanged: "layoutChanged" }, queries: [{ propertyName: "cellTemplate", first: true, predicate: GridCellDirective, descendants: true, read: TemplateRef }], viewQueries: [{ propertyName: "gridContainer", first: true, predicate: ["gridContainer"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
395
+ <div
396
+ #gridContainer
397
+ class="grid-container"
398
+ [style.height.px]="containerHeight"
399
+ >
400
+ <!-- Placeholder preview -->
401
+ <div
402
+ class="grid-placeholder"
403
+ *ngIf="placeholder"
404
+ [style.left.px]="placeholder.left"
405
+ [style.top.px]="placeholder.top"
406
+ [style.width.px]="placeholder.width"
407
+ [style.height.px]="placeholder.height"
408
+ ></div>
409
+
410
+ <!-- Widget items -->
411
+ <div
412
+ *ngFor="let widget of widgets; trackBy: trackByFn"
413
+ class="grid-item"
414
+ [class.is-dragging]="dragging?.id === widget.id"
415
+ [class.is-resizing]="resizing?.id === widget.id"
416
+ [style.left.px]="getItemLeft(widget)"
417
+ [style.top.px]="getItemTop(widget)"
418
+ [style.width.px]="getItemWidth(widget)"
419
+ [style.height.px]="getItemHeight(widget)"
420
+ [style.z-index]="(dragging?.id === widget.id || resizing?.id === widget.id) ? 100 : 1"
421
+ (mousedown)="onDragStart($event, widget)"
422
+ >
423
+ <!-- Content stamped from parent template, context carries the widget -->
424
+ <ng-container
425
+ *ngIf="cellTemplate"
426
+ [ngTemplateOutlet]="cellTemplate"
427
+ [ngTemplateOutletContext]="{ widget: widget }"
428
+ ></ng-container>
429
+
430
+ <!-- Resize handle -->
431
+ <div class="resize-handle" (mousedown)="onResizeStart($event, widget)">
432
+ <svg width="12" height="12" viewBox="0 0 12 12">
433
+ <path d="M11 1v10H1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
434
+ <path d="M11 5v6H5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
435
+ <path d="M11 9v2H9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
436
+ </svg>
437
+ </div>
438
+ </div>
439
+ </div>
440
+ `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.grid-container{position:relative;width:100%;min-height:100%;transition:height .25s ease}.grid-placeholder{position:absolute;background:var(--dash-accent-color, #0a84ff);opacity:.12;border-radius:24px;border:2px dashed var(--dash-accent-color, #0a84ff);transition:left .15s ease,top .15s ease,width .15s ease,height .15s ease;pointer-events:none;z-index:0}.grid-item{position:absolute;display:flex;flex-direction:column;transition:left .25s ease,top .25s ease,width .25s ease,height .25s ease;border-radius:24px;overflow:hidden}.grid-item.is-dragging,.grid-item.is-resizing{transition:none!important;opacity:.88;overflow:visible;filter:drop-shadow(0 20px 40px rgba(0,0,0,.45))}.resize-handle{position:absolute;right:4px;bottom:4px;width:22px;height:22px;cursor:nwse-resize;display:flex;align-items:center;justify-content:center;color:#ffffff40;border-radius:6px;transition:color .2s,background .2s;z-index:20}.resize-handle:hover{color:var(--dash-accent-color, #0a84ff);background:rgba(255,255,255,.06)}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
441
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CustomGridComponent, decorators: [{
442
+ type: Component,
443
+ args: [{ selector: 'app-grid', template: `
444
+ <div
445
+ #gridContainer
446
+ class="grid-container"
447
+ [style.height.px]="containerHeight"
448
+ >
449
+ <!-- Placeholder preview -->
450
+ <div
451
+ class="grid-placeholder"
452
+ *ngIf="placeholder"
453
+ [style.left.px]="placeholder.left"
454
+ [style.top.px]="placeholder.top"
455
+ [style.width.px]="placeholder.width"
456
+ [style.height.px]="placeholder.height"
457
+ ></div>
458
+
459
+ <!-- Widget items -->
460
+ <div
461
+ *ngFor="let widget of widgets; trackBy: trackByFn"
462
+ class="grid-item"
463
+ [class.is-dragging]="dragging?.id === widget.id"
464
+ [class.is-resizing]="resizing?.id === widget.id"
465
+ [style.left.px]="getItemLeft(widget)"
466
+ [style.top.px]="getItemTop(widget)"
467
+ [style.width.px]="getItemWidth(widget)"
468
+ [style.height.px]="getItemHeight(widget)"
469
+ [style.z-index]="(dragging?.id === widget.id || resizing?.id === widget.id) ? 100 : 1"
470
+ (mousedown)="onDragStart($event, widget)"
471
+ >
472
+ <!-- Content stamped from parent template, context carries the widget -->
473
+ <ng-container
474
+ *ngIf="cellTemplate"
475
+ [ngTemplateOutlet]="cellTemplate"
476
+ [ngTemplateOutletContext]="{ widget: widget }"
477
+ ></ng-container>
478
+
479
+ <!-- Resize handle -->
480
+ <div class="resize-handle" (mousedown)="onResizeStart($event, widget)">
481
+ <svg width="12" height="12" viewBox="0 0 12 12">
482
+ <path d="M11 1v10H1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
483
+ <path d="M11 5v6H5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
484
+ <path d="M11 9v2H9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
485
+ </svg>
486
+ </div>
487
+ </div>
488
+ </div>
489
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%;height:100%}.grid-container{position:relative;width:100%;min-height:100%;transition:height .25s ease}.grid-placeholder{position:absolute;background:var(--dash-accent-color, #0a84ff);opacity:.12;border-radius:24px;border:2px dashed var(--dash-accent-color, #0a84ff);transition:left .15s ease,top .15s ease,width .15s ease,height .15s ease;pointer-events:none;z-index:0}.grid-item{position:absolute;display:flex;flex-direction:column;transition:left .25s ease,top .25s ease,width .25s ease,height .25s ease;border-radius:24px;overflow:hidden}.grid-item.is-dragging,.grid-item.is-resizing{transition:none!important;opacity:.88;overflow:visible;filter:drop-shadow(0 20px 40px rgba(0,0,0,.45))}.resize-handle{position:absolute;right:4px;bottom:4px;width:22px;height:22px;cursor:nwse-resize;display:flex;align-items:center;justify-content:center;color:#ffffff40;border-radius:6px;transition:color .2s,background .2s;z-index:20}.resize-handle:hover{color:var(--dash-accent-color, #0a84ff);background:rgba(255,255,255,.06)}\n"] }]
490
+ }], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { widgets: [{
491
+ type: Input
492
+ }], columns: [{
493
+ type: Input
494
+ }], gap: [{
495
+ type: Input
496
+ }], rowHeight: [{
497
+ type: Input
498
+ }], minItemCols: [{
499
+ type: Input
500
+ }], minItemRows: [{
501
+ type: Input
502
+ }], itemChanged: [{
503
+ type: Output
504
+ }], layoutChanged: [{
505
+ type: Output
506
+ }], gridContainer: [{
507
+ type: ViewChild,
508
+ args: ['gridContainer', { static: true }]
509
+ }], cellTemplate: [{
510
+ type: ContentChild,
511
+ args: [GridCellDirective, { read: TemplateRef }]
512
+ }] } });
513
+
514
+ class DashboardStateService {
515
+ constructor(zone) {
516
+ this.zone = zone;
517
+ /**
518
+ * Shared key — stores page list + widget metadata (no positions).
519
+ * Read and written by every window.
520
+ */
521
+ this.SHARED_KEY = 'ogidor_shared';
522
+ this.CHANNEL_NAME = 'ogidor_dashboard_sync';
523
+ this.channel = typeof BroadcastChannel !== 'undefined'
524
+ ? new BroadcastChannel(this.CHANNEL_NAME) : null;
525
+ this.initialPages = [
526
+ { id: 'page-1', name: 'Default Workspace', widgets: [] }
527
+ ];
528
+ this.pagesSubject = new BehaviorSubject(this.initialPages);
529
+ this.activePageIdSubject = new BehaviorSubject(this.initialPages[0].id);
530
+ this.pages$ = this.pagesSubject.asObservable();
531
+ this.activePageId$ = this.activePageIdSubject.asObservable();
532
+ // Determine whether we are a pop-out window
533
+ const hash = typeof window !== 'undefined'
534
+ ? window.location.hash.replace('#', '').trim() : '';
535
+ this.positionsKey = hash ? `ogidor_positions_${hash}` : 'ogidor_positions_main';
536
+ // 1. Load the shared structural state
537
+ const shared = localStorage.getItem(this.SHARED_KEY);
538
+ if (shared) {
539
+ try {
540
+ this._applyShared(JSON.parse(shared), false);
541
+ }
542
+ catch (e) {
543
+ console.error('[Dashboard] bad shared state', e);
544
+ }
545
+ }
546
+ // 2. Overlay this window's saved positions on top
547
+ this._restorePositions();
548
+ // 3. Listen for structural changes broadcast by other windows
549
+ if (this.channel) {
550
+ this.channel.onmessage = (ev) => {
551
+ this.zone.run(() => this._onSyncEvent(ev.data));
552
+ };
553
+ }
554
+ }
555
+ ngOnDestroy() { this.channel?.close(); }
556
+ // ── Page actions ──────────────────────────────────────────────
557
+ getActivePage() {
558
+ return this.pagesSubject.value.find(p => p.id === this.activePageIdSubject.value);
559
+ }
560
+ setActivePage(id) { this.activePageIdSubject.next(id); }
561
+ addPage(name) {
562
+ const page = { id: `page-${Date.now()}`, name, widgets: [] };
563
+ this.pagesSubject.next([...this.pagesSubject.value, page]);
564
+ this.activePageIdSubject.next(page.id);
565
+ this._saveShared();
566
+ this._broadcast({ type: 'PAGE_ADDED', page });
567
+ }
568
+ removePage(id) {
569
+ const pages = this.pagesSubject.value;
570
+ if (pages.length <= 1)
571
+ return;
572
+ const updated = pages.filter(p => p.id !== id);
573
+ this.pagesSubject.next(updated);
574
+ if (this.activePageIdSubject.value === id) {
575
+ this.activePageIdSubject.next(updated[0].id);
576
+ }
577
+ this._saveShared();
578
+ this._broadcast({ type: 'PAGE_REMOVED', pageId: id });
579
+ }
580
+ // ── Widget structural actions (synced to all windows) ─────────
581
+ addWidget(widget) {
582
+ const activePage = this.getActivePage();
583
+ if (!activePage)
584
+ throw new Error('No active page');
585
+ const newWidget = {
586
+ id: `widget-${Date.now()}`,
587
+ x: 0, y: 0, cols: 4, rows: 3,
588
+ ...widget,
589
+ };
590
+ activePage.widgets = [...activePage.widgets, newWidget];
591
+ this.pagesSubject.next([...this.pagesSubject.value]);
592
+ this._saveShared();
593
+ this._savePositions(); // register default position locally
594
+ this._broadcast({ type: 'WIDGET_ADDED', pageId: activePage.id, widget: newWidget });
595
+ return newWidget;
596
+ }
597
+ updateWidget(widgetId, patch) {
598
+ const pages = this.pagesSubject.value;
599
+ for (const page of pages) {
600
+ const w = page.widgets.find(w => w.id === widgetId);
601
+ if (w) {
602
+ Object.assign(w, patch);
603
+ this.pagesSubject.next([...pages]);
604
+ this._saveShared();
605
+ this._broadcast({ type: 'WIDGET_META', widgetId, patch });
606
+ return;
607
+ }
608
+ }
609
+ }
610
+ removeWidget(widgetId) {
611
+ const activePage = this.getActivePage();
612
+ if (!activePage)
613
+ return;
614
+ const pageId = activePage.id;
615
+ activePage.widgets = activePage.widgets.filter(w => w.id !== widgetId);
616
+ this.pagesSubject.next([...this.pagesSubject.value]);
617
+ this._saveShared();
618
+ this._savePositions();
619
+ this._broadcast({ type: 'WIDGET_REMOVED', pageId, widgetId });
620
+ }
621
+ // ── Position actions (local to THIS window only) ──────────────
622
+ /**
623
+ * Update a widget's grid position/size.
624
+ * Saved only for this window — never broadcast to others.
625
+ */
626
+ updateWidgetPosition(pageId, widgetId, x, y, cols, rows) {
627
+ const pages = this.pagesSubject.value;
628
+ const widget = pages.find(p => p.id === pageId)?.widgets.find(w => w.id === widgetId);
629
+ if (widget) {
630
+ widget.x = x;
631
+ widget.y = y;
632
+ widget.cols = cols;
633
+ widget.rows = rows;
634
+ this.pagesSubject.next([...pages]);
635
+ this._savePositions(); // local only — intentionally no _saveShared / no broadcast
636
+ }
637
+ }
638
+ // ── Serialization ─────────────────────────────────────────────
639
+ serializeLayout() {
640
+ return JSON.stringify({
641
+ pages: this.pagesSubject.value,
642
+ activePageId: this.activePageIdSubject.value,
643
+ });
644
+ }
645
+ loadLayout(config) {
646
+ if (!config?.pages)
647
+ return;
648
+ this._applyShared(config, true);
649
+ this._saveShared();
650
+ this._savePositions();
651
+ }
652
+ popOutPage(pageId) {
653
+ const url = `${window.location.origin}${window.location.pathname}#${pageId}`;
654
+ window.open(url, `workspace_${pageId}`, 'width=1280,height=800,menubar=no,toolbar=no,location=no,status=no');
655
+ }
656
+ // ── Private helpers ───────────────────────────────────────────
657
+ /**
658
+ * Save page list + widget metadata (titles, cardColor, data) — no positions.
659
+ */
660
+ _saveShared() {
661
+ const stripped = this.pagesSubject.value.map(p => ({
662
+ ...p,
663
+ widgets: p.widgets.map(({ id, title, cardColor, data, cols, rows }) =>
664
+ // Keep default cols/rows so new windows get a sensible first-open layout.
665
+ // x/y are intentionally omitted from shared state.
666
+ ({ id, title, cardColor, data, x: 0, y: 0, cols, rows })),
667
+ }));
668
+ localStorage.setItem(this.SHARED_KEY, JSON.stringify({
669
+ pages: stripped,
670
+ activePageId: this.activePageIdSubject.value,
671
+ }));
672
+ }
673
+ /**
674
+ * Save this window's grid positions (x, y, cols, rows) per widget.
675
+ */
676
+ _savePositions() {
677
+ const positions = {};
678
+ for (const page of this.pagesSubject.value) {
679
+ for (const w of page.widgets) {
680
+ positions[`${page.id}:${w.id}`] = { x: w.x, y: w.y, cols: w.cols, rows: w.rows };
681
+ }
682
+ }
683
+ localStorage.setItem(this.positionsKey, JSON.stringify(positions));
684
+ }
685
+ /**
686
+ * Overlay the positions saved for THIS window on top of the current pages.
687
+ */
688
+ _restorePositions() {
689
+ const raw = localStorage.getItem(this.positionsKey);
690
+ if (!raw)
691
+ return;
692
+ try {
693
+ const positions = JSON.parse(raw);
694
+ const pages = this.pagesSubject.value;
695
+ for (const page of pages) {
696
+ for (const w of page.widgets) {
697
+ const pos = positions[`${page.id}:${w.id}`];
698
+ if (pos) {
699
+ w.x = pos.x;
700
+ w.y = pos.y;
701
+ w.cols = pos.cols;
702
+ w.rows = pos.rows;
703
+ }
704
+ }
705
+ }
706
+ this.pagesSubject.next([...pages]);
707
+ }
708
+ catch (e) {
709
+ console.error('[Dashboard] bad positions state', e);
710
+ }
711
+ }
712
+ /**
713
+ * Apply a shared config object (page list + metadata) without touching
714
+ * this window's saved positions.
715
+ */
716
+ _applyShared(config, overwritePositions) {
717
+ if (!config?.pages)
718
+ return;
719
+ this.pagesSubject.next(config.pages);
720
+ if (config.activePageId)
721
+ this.activePageIdSubject.next(config.activePageId);
722
+ if (!overwritePositions)
723
+ this._restorePositions();
724
+ }
725
+ /**
726
+ * Handle a structural sync event arriving from another window.
727
+ * Position changes are never sent so we never receive them here.
728
+ */
729
+ _onSyncEvent(event) {
730
+ // Work on a shallow clone of pages so Angular detects the change
731
+ const pages = this.pagesSubject.value.map(p => ({
732
+ ...p, widgets: [...p.widgets]
733
+ }));
734
+ switch (event.type) {
735
+ case 'PAGE_ADDED':
736
+ if (!pages.find(p => p.id === event.page.id)) {
737
+ pages.push({ ...event.page, widgets: [...event.page.widgets] });
738
+ this.pagesSubject.next(pages);
739
+ this._saveShared();
740
+ }
741
+ break;
742
+ case 'PAGE_REMOVED': {
743
+ const updated = pages.filter(p => p.id !== event.pageId);
744
+ if (updated.length !== pages.length) {
745
+ this.pagesSubject.next(updated);
746
+ if (this.activePageIdSubject.value === event.pageId) {
747
+ this.activePageIdSubject.next(updated[0]?.id ?? '');
748
+ }
749
+ this._saveShared();
750
+ }
751
+ break;
752
+ }
753
+ case 'WIDGET_ADDED': {
754
+ const page = pages.find(p => p.id === event.pageId);
755
+ if (page && !page.widgets.find(w => w.id === event.widget.id)) {
756
+ page.widgets = [...page.widgets, { ...event.widget }];
757
+ this.pagesSubject.next(pages);
758
+ this._saveShared();
759
+ // Don't touch positions — this window will keep its own layout for
760
+ // existing widgets; the new one starts at its default position.
761
+ }
762
+ break;
763
+ }
764
+ case 'WIDGET_REMOVED': {
765
+ const page = pages.find(p => p.id === event.pageId);
766
+ if (page) {
767
+ page.widgets = page.widgets.filter(w => w.id !== event.widgetId);
768
+ this.pagesSubject.next(pages);
769
+ this._saveShared();
770
+ this._savePositions();
771
+ }
772
+ break;
773
+ }
774
+ case 'WIDGET_META': {
775
+ for (const page of pages) {
776
+ const w = page.widgets.find(w => w.id === event.widgetId);
777
+ if (w) {
778
+ Object.assign(w, event.patch);
779
+ this.pagesSubject.next(pages);
780
+ this._saveShared();
781
+ break;
782
+ }
783
+ }
784
+ break;
785
+ }
786
+ }
787
+ }
788
+ _broadcast(event) {
789
+ this.channel?.postMessage(event);
790
+ }
791
+ }
792
+ DashboardStateService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
793
+ DashboardStateService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, providedIn: 'root' });
794
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, decorators: [{
795
+ type: Injectable,
796
+ args: [{ providedIn: 'root' }]
797
+ }], ctorParameters: function () { return [{ type: i0.NgZone }]; } });
798
+
799
+ class WidgetRendererComponent {
800
+ constructor() {
801
+ this.editRequested = new EventEmitter();
802
+ this.removeRequested = new EventEmitter();
803
+ this.cardBg = 'var(--dash-card-bg, #2c2c2e)';
804
+ }
805
+ ngOnChanges(changes) {
806
+ if (changes['widget'] || changes['theme']) {
807
+ this.cardBg = this.widget?.cardColor ?? 'var(--dash-card-bg, #2c2c2e)';
808
+ }
809
+ }
810
+ }
811
+ WidgetRendererComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: WidgetRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
812
+ WidgetRendererComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: WidgetRendererComponent, selector: "app-widget-renderer", inputs: { widget: "widget", theme: "theme" }, outputs: { editRequested: "editRequested", removeRequested: "removeRequested" }, usesOnChanges: true, ngImport: i0, template: `
813
+ <div class="widget-card" [style.background]="cardBg">
814
+ <!-- Header: drag handle + title + actions -->
815
+ <div class="widget-header">
816
+ <div class="drag-handle" title="Drag to move">
817
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
818
+ <circle cx="4" cy="3" r="1.2"/><circle cx="10" cy="3" r="1.2"/>
819
+ <circle cx="4" cy="7" r="1.2"/><circle cx="10" cy="7" r="1.2"/>
820
+ <circle cx="4" cy="11" r="1.2"/><circle cx="10" cy="11" r="1.2"/>
821
+ </svg>
822
+ </div>
823
+ <span class="widget-title">{{ widget.title }}</span>
824
+ <div class="header-actions">
825
+ <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit">
826
+ <i class="la la-pen"></i>
827
+ </button>
828
+ <button class="btn-icon btn-remove" (click)="removeRequested.emit(widget.id)" title="Remove">
829
+ <i class="la la-times"></i>
830
+ </button>
831
+ </div>
832
+ </div>
833
+
834
+ <!-- Consumer content slot -->
835
+ <div class="widget-body">
836
+ <ng-content></ng-content>
837
+ </div>
838
+ </div>
839
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;width:100%}.widget-card{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;border-radius:24px;overflow:hidden;box-shadow:0 8px 24px #00000040;border:1px solid rgba(255,255,255,.07);transition:box-shadow .2s ease;box-sizing:border-box;background:var(--dash-card-bg, #2c2c2e)}.widget-card:hover{box-shadow:0 12px 32px #00000059}.widget-header{display:flex;align-items:center;gap:8px;padding:12px 14px 8px;flex-shrink:0}.drag-handle{color:#fff3;cursor:grab;display:flex;align-items:center;flex-shrink:0;transition:color .2s}.drag-handle:hover{color:#ffffff80}.widget-title{flex:1;color:#fff;font-size:14px;font-weight:600;letter-spacing:.2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.header-actions{display:flex;gap:4px;align-items:center;opacity:0;transition:opacity .2s}.widget-card:hover .header-actions{opacity:1}.btn-icon{background:rgba(255,255,255,.07);border:none;color:#ffffff80;cursor:pointer;font-size:11px;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}.btn-edit:hover{background:rgba(10,132,255,.22);color:var(--dash-accent-color, #0a84ff)}.btn-remove:hover{background:rgba(255,69,58,.22);color:var(--dash-danger-color, #ff453a)}.widget-body{flex:1;min-height:0;display:flex;flex-direction:column;padding:0 14px 14px;overflow:hidden}\n"] });
840
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: WidgetRendererComponent, decorators: [{
841
+ type: Component,
842
+ args: [{ selector: 'app-widget-renderer', template: `
843
+ <div class="widget-card" [style.background]="cardBg">
844
+ <!-- Header: drag handle + title + actions -->
845
+ <div class="widget-header">
846
+ <div class="drag-handle" title="Drag to move">
847
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
848
+ <circle cx="4" cy="3" r="1.2"/><circle cx="10" cy="3" r="1.2"/>
849
+ <circle cx="4" cy="7" r="1.2"/><circle cx="10" cy="7" r="1.2"/>
850
+ <circle cx="4" cy="11" r="1.2"/><circle cx="10" cy="11" r="1.2"/>
851
+ </svg>
852
+ </div>
853
+ <span class="widget-title">{{ widget.title }}</span>
854
+ <div class="header-actions">
855
+ <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit">
856
+ <i class="la la-pen"></i>
857
+ </button>
858
+ <button class="btn-icon btn-remove" (click)="removeRequested.emit(widget.id)" title="Remove">
859
+ <i class="la la-times"></i>
860
+ </button>
861
+ </div>
862
+ </div>
863
+
864
+ <!-- Consumer content slot -->
865
+ <div class="widget-body">
866
+ <ng-content></ng-content>
867
+ </div>
868
+ </div>
869
+ `, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;width:100%}.widget-card{flex:1;min-height:0;width:100%;display:flex;flex-direction:column;border-radius:24px;overflow:hidden;box-shadow:0 8px 24px #00000040;border:1px solid rgba(255,255,255,.07);transition:box-shadow .2s ease;box-sizing:border-box;background:var(--dash-card-bg, #2c2c2e)}.widget-card:hover{box-shadow:0 12px 32px #00000059}.widget-header{display:flex;align-items:center;gap:8px;padding:12px 14px 8px;flex-shrink:0}.drag-handle{color:#fff3;cursor:grab;display:flex;align-items:center;flex-shrink:0;transition:color .2s}.drag-handle:hover{color:#ffffff80}.widget-title{flex:1;color:#fff;font-size:14px;font-weight:600;letter-spacing:.2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.header-actions{display:flex;gap:4px;align-items:center;opacity:0;transition:opacity .2s}.widget-card:hover .header-actions{opacity:1}.btn-icon{background:rgba(255,255,255,.07);border:none;color:#ffffff80;cursor:pointer;font-size:11px;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}.btn-edit:hover{background:rgba(10,132,255,.22);color:var(--dash-accent-color, #0a84ff)}.btn-remove:hover{background:rgba(255,69,58,.22);color:var(--dash-danger-color, #ff453a)}.widget-body{flex:1;min-height:0;display:flex;flex-direction:column;padding:0 14px 14px;overflow:hidden}\n"] }]
870
+ }], propDecorators: { widget: [{
871
+ type: Input
872
+ }], theme: [{
873
+ type: Input
874
+ }], editRequested: [{
875
+ type: Output
876
+ }], removeRequested: [{
877
+ type: Output
878
+ }] } });
879
+
880
+ const DEFAULT_THEME = {
881
+ backgroundColor: '#000000',
882
+ panelColor: '#1c1c1e',
883
+ widgetCardColor: '#2c2c2e',
884
+ foreColor: '#8e8e93',
885
+ accentColor: '#0a84ff',
886
+ dangerColor: '#ff453a',
887
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
888
+ };
889
+ class DashboardComponent {
890
+ constructor(stateService) {
891
+ this.stateService = stateService;
892
+ /**
893
+ * Emits when the user clicks "Add Widget".
894
+ * The consumer should open their own dialog and call `stateService.addWidget(...)`.
895
+ */
896
+ this.addWidgetRequested = new EventEmitter();
897
+ /**
898
+ * Emits the Widget when the user clicks the edit (pen) icon on a card.
899
+ * The consumer should open their own edit UI and call `stateService.updateWidget(...)`.
900
+ */
901
+ this.editWidgetRequested = new EventEmitter();
902
+ this.resolvedTheme = { ...DEFAULT_THEME };
903
+ this.wrapperStyles = {};
904
+ this.pages = [];
905
+ this.activePageId = '';
906
+ this.isPoppedOut = false;
907
+ this.subs = new Subscription();
908
+ }
909
+ ngOnChanges(changes) {
910
+ if (changes['theme'] || changes['initialLayout'])
911
+ this.applyTheme();
912
+ }
913
+ ngOnInit() {
914
+ this.applyTheme();
915
+ if (this.initialLayout) {
916
+ try {
917
+ this.stateService.loadLayout(JSON.parse(this.initialLayout));
918
+ }
919
+ catch (e) {
920
+ console.error('[Dashboard] Failed to parse initialLayout', e);
921
+ }
922
+ }
923
+ const hash = window.location.hash?.replace('#', '').trim();
924
+ if (hash)
925
+ this.isPoppedOut = true;
926
+ this.subs.add(this.stateService.pages$.subscribe(pages => {
927
+ this.pages = pages;
928
+ if (this.isPoppedOut && hash) {
929
+ const target = pages.find(p => p.id === hash);
930
+ if (target)
931
+ this.stateService.setActivePage(hash);
932
+ }
933
+ this.updateActivePage();
934
+ }));
935
+ this.subs.add(this.stateService.activePageId$.subscribe(id => {
936
+ this.activePageId = id;
937
+ this.updateActivePage();
938
+ }));
939
+ }
940
+ ngOnDestroy() { this.subs.unsubscribe(); }
941
+ onItemChanged(widget) {
942
+ this.stateService.updateWidgetPosition(this.activePageId, widget.id, widget.x, widget.y, widget.cols, widget.rows);
943
+ }
944
+ onRemoveWidget(widgetId) { this.stateService.removeWidget(widgetId); }
945
+ onSelectPage(id) { this.stateService.setActivePage(id); }
946
+ onAddPage() {
947
+ const name = prompt('Workspace name:', `Workspace ${this.pages.length + 1}`);
948
+ if (name)
949
+ this.stateService.addPage(name);
950
+ }
951
+ onRemovePage(event, id) {
952
+ event.stopPropagation();
953
+ if (confirm('Remove this workspace?'))
954
+ this.stateService.removePage(id);
955
+ }
956
+ onPopOut(event, pageId) {
957
+ event.stopPropagation();
958
+ this.stateService.popOutPage(pageId);
959
+ }
960
+ serializeLayout() { return this.stateService.serializeLayout(); }
961
+ applyTheme() {
962
+ this.resolvedTheme = { ...DEFAULT_THEME, ...(this.theme ?? {}) };
963
+ this.wrapperStyles = {
964
+ '--dash-bg': this.resolvedTheme.backgroundColor,
965
+ '--dash-panel-bg': this.resolvedTheme.panelColor,
966
+ '--dash-card-bg': this.resolvedTheme.widgetCardColor,
967
+ '--dash-fore-color': this.resolvedTheme.foreColor,
968
+ '--dash-accent-color': this.resolvedTheme.accentColor,
969
+ '--dash-danger-color': this.resolvedTheme.dangerColor,
970
+ '--dash-font-family': this.resolvedTheme.fontFamily,
971
+ };
972
+ }
973
+ updateActivePage() {
974
+ this.activePage = this.pages.find(p => p.id === this.activePageId);
975
+ }
976
+ }
977
+ DashboardComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardComponent, deps: [{ token: DashboardStateService }], target: i0.ɵɵFactoryTarget.Component });
978
+ DashboardComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: DashboardComponent, selector: "app-dashboard", inputs: { initialLayout: "initialLayout", theme: "theme" }, outputs: { addWidgetRequested: "addWidgetRequested", editWidgetRequested: "editWidgetRequested" }, queries: [{ propertyName: "cellTemplate", first: true, predicate: GridCellDirective, descendants: true, read: TemplateRef }], usesOnChanges: true, ngImport: i0, template: `
979
+ <!-- ══════════════════════════════════════════════════════════
980
+ POPPED-OUT MODE
981
+ ══════════════════════════════════════════════════════════ -->
982
+ <ng-container *ngIf="isPoppedOut; else normalMode">
983
+ <div class="popout-wrapper" [ngStyle]="wrapperStyles">
984
+ <header class="popout-header">
985
+ <span class="popout-title">{{ activePage?.name }}</span>
986
+ </header>
987
+ <div class="grid-container">
988
+ <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
989
+ <ng-template gridCell let-widget="widget">
990
+ <app-widget-renderer
991
+ [widget]="widget"
992
+ [theme]="resolvedTheme"
993
+ (editRequested)="editWidgetRequested.emit($event)"
994
+ (removeRequested)="onRemoveWidget($event)"
995
+ >
996
+ <ng-container
997
+ *ngIf="cellTemplate"
998
+ [ngTemplateOutlet]="cellTemplate"
999
+ [ngTemplateOutletContext]="{ widget: widget }">
1000
+ </ng-container>
1001
+ </app-widget-renderer>
1002
+ </ng-template>
1003
+ </app-grid>
1004
+ </div>
1005
+ </div>
1006
+ </ng-container>
1007
+
1008
+ <!-- ══════════════════════════════════════════════════════════
1009
+ NORMAL MODE
1010
+ ══════════════════════════════════════════════════════════ -->
1011
+ <ng-template #normalMode>
1012
+ <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
1013
+ <main class="main-content">
1014
+
1015
+ <header class="dashboard-header">
1016
+ <div class="tabs-container">
1017
+ <div
1018
+ *ngFor="let page of pages"
1019
+ class="tab"
1020
+ [class.active]="page.id === activePageId"
1021
+ (click)="onSelectPage(page.id)"
1022
+ >
1023
+ <span class="tab-name">{{ page.name }}</span>
1024
+ <button class="tab-action tab-popout" (click)="onPopOut($event, page.id)" title="Open in new window">
1025
+ <i class="la la-external-link-alt"></i>
1026
+ </button>
1027
+ <button class="tab-action tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)" title="Close">
1028
+ <i class="la la-times"></i>
1029
+ </button>
1030
+ </div>
1031
+ <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
1032
+ <i class="la la-plus"></i>
1033
+ </button>
1034
+ </div>
1035
+
1036
+ <!-- Add Widget button — consumer decides what happens -->
1037
+ <button class="btn-add-widget" (click)="addWidgetRequested.emit()" title="Add widget">
1038
+ <i class="la la-plus"></i>
1039
+ <span>Add Widget</span>
1040
+ </button>
1041
+ </header>
1042
+
1043
+ <div class="grid-container">
1044
+ <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
1045
+ <ng-template gridCell let-widget="widget">
1046
+ <app-widget-renderer
1047
+ [widget]="widget"
1048
+ [theme]="resolvedTheme"
1049
+ (editRequested)="editWidgetRequested.emit($event)"
1050
+ (removeRequested)="onRemoveWidget($event)"
1051
+ >
1052
+ <!-- Stamp consumer's cell template (if provided) inside the card body -->
1053
+ <ng-container
1054
+ *ngIf="cellTemplate"
1055
+ [ngTemplateOutlet]="cellTemplate"
1056
+ [ngTemplateOutletContext]="{ widget: widget }">
1057
+ </ng-container>
1058
+ </app-widget-renderer>
1059
+ </ng-template>
1060
+ </app-grid>
1061
+ </div>
1062
+
1063
+ </main>
1064
+ </div>
1065
+ </ng-template>
1066
+ `, isInline: true, styles: [":host{--dash-bg: #000000;--dash-panel-bg: #1c1c1e;--dash-card-bg: #2c2c2e;--dash-fore-color:#8e8e93;--dash-accent-color: #0a84ff;--dash-danger-color: #ff453a;--dash-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif}.dashboard-wrapper{display:flex;height:100vh;width:100vw;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;border-radius:40px;padding:20px;background:var(--dash-panel-bg);box-shadow:0 10px 30px #00000080;border:1px solid rgba(255,255,255,.05)}.popout-wrapper{display:flex;flex-direction:column;height:100vh;width:100vw;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.popout-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;padding:10px 18px;background:var(--dash-panel-bg);border-radius:20px;border:1px solid rgba(255,255,255,.06);flex-shrink:0}.popout-title{font-size:16px;font-weight:700;color:#fff}.popout-wrapper .grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0;background:var(--dash-panel-bg);padding:16px;border:1px solid rgba(255,255,255,.05)}.dashboard-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;gap:12px}.tabs-container{display:flex;align-items:center;gap:8px;background:rgba(44,44,46,.6);backdrop-filter:blur(10px);border-radius:25px;padding:6px}.tab{display:flex;align-items:center;gap:4px;padding:8px 14px 8px 18px;border-radius:20px;cursor:pointer;font-size:14px;font-weight:500;color:var(--dash-fore-color);transition:all .3s cubic-bezier(.25,.8,.25,1)}.tab.active{background:#3a3a3c;color:#fff;box-shadow:0 2px 10px #0003}.tab:hover:not(.active){color:#e5e5ea;background:rgba(255,255,255,.05)}.tab-name{flex:1;white-space:nowrap}.tab-action{background:transparent;border:none;color:var(--dash-fore-color);padding:2px;font-size:11px;cursor:pointer;opacity:0;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}.tab:hover .tab-action{opacity:1}.tab-popout:hover{color:var(--dash-accent-color);background:rgba(10,132,255,.18)}.tab-close:hover{color:var(--dash-danger-color);background:rgba(255,69,58,.2)}.btn-add-page{background:transparent;border:none;color:var(--dash-fore-color);padding:8px 16px;cursor:pointer;border-radius:20px;transition:all .2s}.btn-add-page:hover{color:var(--dash-accent-color);background:rgba(10,132,255,.1)}.btn-add-widget{display:flex;align-items:center;gap:6px;background:var(--dash-accent-color);border:none;color:#fff;font-size:14px;font-weight:600;padding:10px 20px;border-radius:22px;cursor:pointer;white-space:nowrap;transition:opacity .2s,transform .15s;box-shadow:0 4px 14px #0a84ff66}.btn-add-widget:hover{opacity:.9;transform:translateY(-1px)}.btn-add-widget:active{transform:translateY(0)}.main-content .grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0}app-grid{background:transparent;display:block;height:100%;width:100%}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: WidgetRendererComponent, selector: "app-widget-renderer", inputs: ["widget", "theme"], outputs: ["editRequested", "removeRequested"] }, { kind: "component", type: CustomGridComponent, selector: "app-grid", inputs: ["widgets", "columns", "gap", "rowHeight", "minItemCols", "minItemRows"], outputs: ["itemChanged", "layoutChanged"] }, { kind: "directive", type: GridCellDirective, selector: "[gridCell]" }] });
1067
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardComponent, decorators: [{
1068
+ type: Component,
1069
+ args: [{ selector: 'app-dashboard', template: `
1070
+ <!-- ══════════════════════════════════════════════════════════
1071
+ POPPED-OUT MODE
1072
+ ══════════════════════════════════════════════════════════ -->
1073
+ <ng-container *ngIf="isPoppedOut; else normalMode">
1074
+ <div class="popout-wrapper" [ngStyle]="wrapperStyles">
1075
+ <header class="popout-header">
1076
+ <span class="popout-title">{{ activePage?.name }}</span>
1077
+ </header>
1078
+ <div class="grid-container">
1079
+ <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
1080
+ <ng-template gridCell let-widget="widget">
1081
+ <app-widget-renderer
1082
+ [widget]="widget"
1083
+ [theme]="resolvedTheme"
1084
+ (editRequested)="editWidgetRequested.emit($event)"
1085
+ (removeRequested)="onRemoveWidget($event)"
1086
+ >
1087
+ <ng-container
1088
+ *ngIf="cellTemplate"
1089
+ [ngTemplateOutlet]="cellTemplate"
1090
+ [ngTemplateOutletContext]="{ widget: widget }">
1091
+ </ng-container>
1092
+ </app-widget-renderer>
1093
+ </ng-template>
1094
+ </app-grid>
1095
+ </div>
1096
+ </div>
1097
+ </ng-container>
1098
+
1099
+ <!-- ══════════════════════════════════════════════════════════
1100
+ NORMAL MODE
1101
+ ══════════════════════════════════════════════════════════ -->
1102
+ <ng-template #normalMode>
1103
+ <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
1104
+ <main class="main-content">
1105
+
1106
+ <header class="dashboard-header">
1107
+ <div class="tabs-container">
1108
+ <div
1109
+ *ngFor="let page of pages"
1110
+ class="tab"
1111
+ [class.active]="page.id === activePageId"
1112
+ (click)="onSelectPage(page.id)"
1113
+ >
1114
+ <span class="tab-name">{{ page.name }}</span>
1115
+ <button class="tab-action tab-popout" (click)="onPopOut($event, page.id)" title="Open in new window">
1116
+ <i class="la la-external-link-alt"></i>
1117
+ </button>
1118
+ <button class="tab-action tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)" title="Close">
1119
+ <i class="la la-times"></i>
1120
+ </button>
1121
+ </div>
1122
+ <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
1123
+ <i class="la la-plus"></i>
1124
+ </button>
1125
+ </div>
1126
+
1127
+ <!-- Add Widget button — consumer decides what happens -->
1128
+ <button class="btn-add-widget" (click)="addWidgetRequested.emit()" title="Add widget">
1129
+ <i class="la la-plus"></i>
1130
+ <span>Add Widget</span>
1131
+ </button>
1132
+ </header>
1133
+
1134
+ <div class="grid-container">
1135
+ <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
1136
+ <ng-template gridCell let-widget="widget">
1137
+ <app-widget-renderer
1138
+ [widget]="widget"
1139
+ [theme]="resolvedTheme"
1140
+ (editRequested)="editWidgetRequested.emit($event)"
1141
+ (removeRequested)="onRemoveWidget($event)"
1142
+ >
1143
+ <!-- Stamp consumer's cell template (if provided) inside the card body -->
1144
+ <ng-container
1145
+ *ngIf="cellTemplate"
1146
+ [ngTemplateOutlet]="cellTemplate"
1147
+ [ngTemplateOutletContext]="{ widget: widget }">
1148
+ </ng-container>
1149
+ </app-widget-renderer>
1150
+ </ng-template>
1151
+ </app-grid>
1152
+ </div>
1153
+
1154
+ </main>
1155
+ </div>
1156
+ </ng-template>
1157
+ `, styles: [":host{--dash-bg: #000000;--dash-panel-bg: #1c1c1e;--dash-card-bg: #2c2c2e;--dash-fore-color:#8e8e93;--dash-accent-color: #0a84ff;--dash-danger-color: #ff453a;--dash-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif}.dashboard-wrapper{display:flex;height:100vh;width:100vw;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;border-radius:40px;padding:20px;background:var(--dash-panel-bg);box-shadow:0 10px 30px #00000080;border:1px solid rgba(255,255,255,.05)}.popout-wrapper{display:flex;flex-direction:column;height:100vh;width:100vw;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.popout-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;padding:10px 18px;background:var(--dash-panel-bg);border-radius:20px;border:1px solid rgba(255,255,255,.06);flex-shrink:0}.popout-title{font-size:16px;font-weight:700;color:#fff}.popout-wrapper .grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0;background:var(--dash-panel-bg);padding:16px;border:1px solid rgba(255,255,255,.05)}.dashboard-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;gap:12px}.tabs-container{display:flex;align-items:center;gap:8px;background:rgba(44,44,46,.6);backdrop-filter:blur(10px);border-radius:25px;padding:6px}.tab{display:flex;align-items:center;gap:4px;padding:8px 14px 8px 18px;border-radius:20px;cursor:pointer;font-size:14px;font-weight:500;color:var(--dash-fore-color);transition:all .3s cubic-bezier(.25,.8,.25,1)}.tab.active{background:#3a3a3c;color:#fff;box-shadow:0 2px 10px #0003}.tab:hover:not(.active){color:#e5e5ea;background:rgba(255,255,255,.05)}.tab-name{flex:1;white-space:nowrap}.tab-action{background:transparent;border:none;color:var(--dash-fore-color);padding:2px;font-size:11px;cursor:pointer;opacity:0;border-radius:50%;width:20px;height:20px;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}.tab:hover .tab-action{opacity:1}.tab-popout:hover{color:var(--dash-accent-color);background:rgba(10,132,255,.18)}.tab-close:hover{color:var(--dash-danger-color);background:rgba(255,69,58,.2)}.btn-add-page{background:transparent;border:none;color:var(--dash-fore-color);padding:8px 16px;cursor:pointer;border-radius:20px;transition:all .2s}.btn-add-page:hover{color:var(--dash-accent-color);background:rgba(10,132,255,.1)}.btn-add-widget{display:flex;align-items:center;gap:6px;background:var(--dash-accent-color);border:none;color:#fff;font-size:14px;font-weight:600;padding:10px 20px;border-radius:22px;cursor:pointer;white-space:nowrap;transition:opacity .2s,transform .15s;box-shadow:0 4px 14px #0a84ff66}.btn-add-widget:hover{opacity:.9;transform:translateY(-1px)}.btn-add-widget:active{transform:translateY(0)}.main-content .grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0}app-grid{background:transparent;display:block;height:100%;width:100%}\n"] }]
1158
+ }], ctorParameters: function () { return [{ type: DashboardStateService }]; }, propDecorators: { initialLayout: [{
1159
+ type: Input
1160
+ }], theme: [{
1161
+ type: Input
1162
+ }], addWidgetRequested: [{
1163
+ type: Output
1164
+ }], editWidgetRequested: [{
1165
+ type: Output
1166
+ }], cellTemplate: [{
1167
+ type: ContentChild,
1168
+ args: [GridCellDirective, { read: TemplateRef }]
1169
+ }] } });
1170
+
1171
+ class DashboardModule {
1172
+ }
1173
+ DashboardModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1174
+ DashboardModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, declarations: [DashboardComponent,
1175
+ WidgetRendererComponent,
1176
+ CustomGridComponent,
1177
+ GridCellDirective], imports: [CommonModule], exports: [DashboardComponent,
1178
+ WidgetRendererComponent,
1179
+ CustomGridComponent,
1180
+ GridCellDirective] });
1181
+ DashboardModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, providers: [DashboardStateService], imports: [CommonModule] });
1182
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, decorators: [{
1183
+ type: NgModule,
1184
+ args: [{
1185
+ declarations: [
1186
+ DashboardComponent,
1187
+ WidgetRendererComponent,
1188
+ CustomGridComponent,
1189
+ GridCellDirective,
1190
+ ],
1191
+ imports: [
1192
+ CommonModule,
1193
+ ],
1194
+ providers: [DashboardStateService],
1195
+ exports: [
1196
+ DashboardComponent,
1197
+ WidgetRendererComponent,
1198
+ CustomGridComponent,
1199
+ GridCellDirective,
1200
+ ],
1201
+ }]
1202
+ }] });
1203
+ /**
1204
+ * Root module for local demo / testing purposes only.
1205
+ * Library consumers import DashboardModule, not AppModule.
1206
+ */
1207
+ class AppModule {
1208
+ }
1209
+ AppModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1210
+ AppModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: AppModule, bootstrap: [DashboardComponent], imports: [BrowserModule, DashboardModule] });
1211
+ AppModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, imports: [BrowserModule, DashboardModule] });
1212
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, decorators: [{
1213
+ type: NgModule,
1214
+ args: [{
1215
+ imports: [BrowserModule, DashboardModule],
1216
+ bootstrap: [DashboardComponent],
1217
+ }]
1218
+ }] });
1219
+
1220
+ /*
1221
+ * Public API Surface of @ogidor/dashboard
1222
+ */
1223
+
1224
+ /**
1225
+ * Generated bundle index. Do not edit.
1226
+ */
1227
+
1228
+ export { AppModule, CustomGridComponent, DashboardComponent, DashboardModule, DashboardStateService, GridCellDirective, WidgetRendererComponent };
1229
+ //# sourceMappingURL=ogidor-dashboard.mjs.map