@ogidor/dashboard 1.0.2 → 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.
@@ -1,433 +1,910 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, EventEmitter, Component, Input, Output, NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
3
- import { BehaviorSubject, Subject, Subscription } from 'rxjs';
2
+ import { Directive, EventEmitter, TemplateRef, Component, ChangeDetectionStrategy, Input, Output, ViewChild, ContentChild, Injectable, NgModule } from '@angular/core';
3
+ import { BrowserModule } from '@angular/platform-browser';
4
4
  import * as i2 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
6
- import * as i3$1 from '@angular/forms';
7
- import { FormsModule } from '@angular/forms';
8
- import * as i4 from 'angular-gridster2';
9
- import { GridsterModule } from 'angular-gridster2';
10
- import * as i3 from 'ng-apexcharts';
11
- import { NgApexchartsModule } from 'ng-apexcharts';
12
- import { BrowserModule } from '@angular/platform-browser';
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
+ }] } });
13
513
 
14
514
  class DashboardStateService {
15
- constructor() {
16
- this.STORAGE_KEY = 'xtb_dashboard_layout';
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;
17
525
  this.initialPages = [
18
- {
19
- id: 'page-1',
20
- name: 'Default Workspace',
21
- widgets: [
22
- {
23
- id: 'w-1',
24
- type: 'LINE',
25
- x: 0,
26
- y: 0,
27
- cols: 4,
28
- rows: 4,
29
- title: 'Market Trend',
30
- data: { series: [{ name: 'Price', data: [30, 40, 35, 50, 49, 60, 70, 91, 125] }] }
31
- },
32
- {
33
- id: 'w-2',
34
- type: 'DONUT',
35
- x: 4,
36
- y: 0,
37
- cols: 2,
38
- rows: 4,
39
- title: 'Asset Allocation',
40
- data: { series: [44, 55, 41, 17, 15] }
41
- }
42
- ]
43
- }
526
+ { id: 'page-1', name: 'Default Workspace', widgets: [] }
44
527
  ];
45
528
  this.pagesSubject = new BehaviorSubject(this.initialPages);
46
529
  this.activePageIdSubject = new BehaviorSubject(this.initialPages[0].id);
47
- /**
48
- * Emits whenever a widget's data is updated programmatically.
49
- * Key = widget id, value = new data payload.
50
- */
51
- this.widgetDataSubject = new Subject();
52
- this.widgetData$ = this.widgetDataSubject.asObservable();
53
530
  this.pages$ = this.pagesSubject.asObservable();
54
531
  this.activePageId$ = this.activePageIdSubject.asObservable();
55
- const saved = localStorage.getItem(this.STORAGE_KEY);
56
- if (saved) {
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) {
57
539
  try {
58
- this.loadLayout(JSON.parse(saved));
540
+ this._applyShared(JSON.parse(shared), false);
59
541
  }
60
542
  catch (e) {
61
- console.error('Failed to load saved layout', e);
543
+ console.error('[Dashboard] bad shared state', e);
62
544
  }
63
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
+ }
64
554
  }
555
+ ngOnDestroy() { this.channel?.close(); }
556
+ // ── Page actions ──────────────────────────────────────────────
65
557
  getActivePage() {
66
558
  return this.pagesSubject.value.find(p => p.id === this.activePageIdSubject.value);
67
559
  }
68
- setActivePage(id) {
69
- this.activePageIdSubject.next(id);
70
- }
560
+ setActivePage(id) { this.activePageIdSubject.next(id); }
71
561
  addPage(name) {
72
- const newPage = {
73
- id: `page-${Date.now()}`,
74
- name,
75
- widgets: []
76
- };
77
- const updatedPages = [...this.pagesSubject.value, newPage];
78
- this.pagesSubject.next(updatedPages);
79
- this.activePageIdSubject.next(newPage.id);
80
- this.saveToLocalStorage();
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 });
81
567
  }
82
568
  removePage(id) {
83
569
  const pages = this.pagesSubject.value;
84
570
  if (pages.length <= 1)
85
571
  return;
86
- const updatedPages = pages.filter(p => p.id !== id);
87
- this.pagesSubject.next(updatedPages);
572
+ const updated = pages.filter(p => p.id !== id);
573
+ this.pagesSubject.next(updated);
88
574
  if (this.activePageIdSubject.value === id) {
89
- this.activePageIdSubject.next(updatedPages[0].id);
575
+ this.activePageIdSubject.next(updated[0].id);
90
576
  }
91
- this.saveToLocalStorage();
577
+ this._saveShared();
578
+ this._broadcast({ type: 'PAGE_REMOVED', pageId: id });
92
579
  }
93
- addWidget(type) {
580
+ // ── Widget structural actions (synced to all windows) ─────────
581
+ addWidget(widget) {
94
582
  const activePage = this.getActivePage();
95
583
  if (!activePage)
96
- return;
584
+ throw new Error('No active page');
97
585
  const newWidget = {
98
586
  id: `widget-${Date.now()}`,
99
- type,
100
- x: 0, y: 0,
101
- cols: type === 'DONUT' ? 2 : 4,
102
- rows: 4,
103
- title: `${type.charAt(0) + type.slice(1).toLowerCase()} Chart`,
104
- data: this.getDefaultDataForType(type)
587
+ x: 0, y: 0, cols: 4, rows: 3,
588
+ ...widget,
105
589
  };
106
- activePage.widgets.push(newWidget);
107
- this.updatePages(this.pagesSubject.value);
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;
108
596
  }
109
- addWidgetWithData(type, title, data) {
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) {
110
611
  const activePage = this.getActivePage();
111
612
  if (!activePage)
112
613
  return;
113
- const newWidget = {
114
- id: `widget-${Date.now()}`,
115
- type,
116
- x: 0, y: 0,
117
- cols: type === 'DONUT' ? 3 : 6,
118
- rows: 4,
119
- title,
120
- data
121
- };
122
- activePage.widgets.push(newWidget);
123
- this.updatePages(this.pagesSubject.value);
124
- }
125
- getDefaultDataForType(type) {
126
- if (type === 'DONUT') {
127
- return { series: [30, 20, 50] };
128
- }
129
- return {
130
- series: [{
131
- name: 'Sample Data',
132
- data: Array.from({ length: 10 }, () => Math.floor(Math.random() * 100))
133
- }]
134
- };
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 });
135
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
+ */
136
626
  updateWidgetPosition(pageId, widgetId, x, y, cols, rows) {
137
627
  const pages = this.pagesSubject.value;
138
- const page = pages.find(p => p.id === pageId);
139
- if (page) {
140
- const widget = page.widgets.find(w => w.id === widgetId);
141
- if (widget) {
142
- widget.x = x;
143
- widget.y = y;
144
- widget.cols = cols;
145
- widget.rows = rows;
146
- this.updatePages(pages);
147
- }
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
148
636
  }
149
637
  }
150
- removeWidget(widgetId) {
151
- const activePage = this.getActivePage();
152
- if (activePage) {
153
- activePage.widgets = activePage.widgets.filter(w => w.id !== widgetId);
154
- this.updatePages(this.pagesSubject.value);
155
- }
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();
156
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 ───────────────────────────────────────────
157
657
  /**
158
- * Push new data into a widget by its id.
159
- * The widget's chart will re-render immediately.
160
- *
161
- * @param widgetId The `id` of the target widget.
162
- * @param data New data — `LineBarData` for LINE/BAR, `DonutData` for DONUT.
658
+ * Save page list + widget metadata (titles, cardColor, data) — no positions.
163
659
  */
164
- updateWidgetData(widgetId, data) {
165
- const pages = this.pagesSubject.value;
166
- for (const page of pages) {
167
- const widget = page.widgets.find(w => w.id === widgetId);
168
- if (widget) {
169
- widget.data = data;
170
- this.widgetDataSubject.next({ widgetId, data });
171
- this.updatePages(pages);
172
- return;
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 };
173
681
  }
174
682
  }
175
- console.warn(`[Dashboard] updateWidgetData: widget "${widgetId}" not found.`);
683
+ localStorage.setItem(this.positionsKey, JSON.stringify(positions));
176
684
  }
177
- updateWidgetMeta(widgetId, meta) {
178
- const pages = this.pagesSubject.value;
179
- for (const page of pages) {
180
- const widget = page.widgets.find(w => w.id === widgetId);
181
- if (widget) {
182
- if (meta.title !== undefined)
183
- widget.title = meta.title;
184
- if (meta.colors !== undefined)
185
- widget.colors = meta.colors;
186
- if (meta.cardColor !== undefined)
187
- widget.cardColor = meta.cardColor;
188
- if (meta.data !== undefined) {
189
- widget.data = meta.data;
190
- this.widgetDataSubject.next({ widgetId, data: meta.data });
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
+ }
191
704
  }
192
- this.updatePages(pages);
193
- return;
194
705
  }
706
+ this.pagesSubject.next([...pages]);
707
+ }
708
+ catch (e) {
709
+ console.error('[Dashboard] bad positions state', e);
195
710
  }
196
711
  }
197
- updatePages(pages) {
198
- this.pagesSubject.next([...pages]);
199
- this.saveToLocalStorage();
200
- }
201
- serializeLayout() {
202
- const config = {
203
- pages: this.pagesSubject.value,
204
- activePageId: this.activePageIdSubject.value
205
- };
206
- return JSON.stringify(config);
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();
207
724
  }
208
- loadLayout(config) {
209
- if (config && config.pages) {
210
- this.pagesSubject.next(config.pages);
211
- if (config.activePageId) {
212
- this.activePageIdSubject.next(config.activePageId);
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;
213
785
  }
214
786
  }
215
787
  }
216
- saveToLocalStorage() {
217
- localStorage.setItem(this.STORAGE_KEY, this.serializeLayout());
788
+ _broadcast(event) {
789
+ this.channel?.postMessage(event);
218
790
  }
219
791
  }
220
- DashboardStateService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
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 });
221
793
  DashboardStateService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, providedIn: 'root' });
222
794
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, decorators: [{
223
795
  type: Injectable,
224
- args: [{
225
- providedIn: 'root'
226
- }]
227
- }], ctorParameters: function () { return []; } });
796
+ args: [{ providedIn: 'root' }]
797
+ }], ctorParameters: function () { return [{ type: i0.NgZone }]; } });
228
798
 
229
- const DEFAULT_CHART_COLORS = ['#0a84ff', '#30d158', '#ff9f0a', '#bf5af2', '#ff453a'];
230
- const DEFAULT_FORECOLOR = '#8e8e93';
231
- const DEFAULT_CARD_BG = '#2c2c2e';
232
799
  class WidgetRendererComponent {
233
- constructor(stateService) {
234
- this.stateService = stateService;
800
+ constructor() {
235
801
  this.editRequested = new EventEmitter();
236
- this.cardStyles = {};
237
- this.subs = new Subscription();
238
- }
239
- ngOnInit() {
240
- this.applyStyles();
241
- this.initChart();
242
- this.subs.add(this.stateService.widgetData$.subscribe(({ widgetId, data }) => {
243
- if (widgetId === this.widget.id) {
244
- this.widget = { ...this.widget, data };
245
- this.initChart();
246
- }
247
- }));
802
+ this.removeRequested = new EventEmitter();
803
+ this.cardBg = 'var(--dash-card-bg, #2c2c2e)';
248
804
  }
249
805
  ngOnChanges(changes) {
250
- if (changes['theme']) {
251
- this.applyStyles();
252
- this.initChart();
806
+ if (changes['widget'] || changes['theme']) {
807
+ this.cardBg = this.widget?.cardColor ?? 'var(--dash-card-bg, #2c2c2e)';
253
808
  }
254
- if (changes['widget'] && !changes['widget'].firstChange) {
255
- this.applyStyles();
256
- this.initChart();
257
- }
258
- }
259
- ngOnDestroy() { this.subs.unsubscribe(); }
260
- applyStyles() {
261
- this.cardStyles = {
262
- background: this.widget.cardColor ?? 'var(--dash-card-bg)'
263
- };
264
- }
265
- resolvedColors() {
266
- return this.widget.colors ?? this.theme?.chartColors ?? DEFAULT_CHART_COLORS;
267
809
  }
268
- initChart() {
269
- const isDonut = this.widget.type === 'DONUT';
270
- const colors = this.resolvedColors();
271
- const foreColor = this.theme?.chartForeColor ?? DEFAULT_FORECOLOR;
272
- const lineBarData = !isDonut ? this.widget.data : null;
273
- const donutData = isDonut ? this.widget.data : null;
274
- const categories = lineBarData?.categories ?? [];
275
- const donutLabels = donutData?.labels ?? ['A', 'B', 'C', 'D', 'E'];
276
- this.chartOptions = {
277
- series: this.widget.data.series,
278
- chart: {
279
- width: '100%', height: '100%',
280
- type: this.widget.type.toLowerCase(),
281
- toolbar: { show: false },
282
- animations: { enabled: true, speed: 400 },
283
- background: 'transparent', foreColor,
284
- parentHeightOffset: 0, sparkline: { enabled: false },
285
- },
286
- theme: { mode: 'dark' },
287
- colors,
288
- stroke: { curve: 'smooth', width: isDonut ? 0 : 3 },
289
- dataLabels: { enabled: false },
290
- legend: { position: 'bottom', labels: { colors: foreColor } },
291
- tooltip: { theme: 'dark' },
292
- plotOptions: {
293
- pie: { donut: { size: '70%', labels: { show: true, total: { show: true, label: 'Total', color: '#fff' } } } },
294
- bar: { borderRadius: 8, columnWidth: '50%' }
295
- },
296
- xaxis: {
297
- categories: categories.length ? categories : undefined,
298
- labels: { style: { colors: foreColor } },
299
- axisBorder: { show: false }, axisTicks: { show: false }
300
- },
301
- yaxis: { labels: { style: { colors: foreColor } } },
302
- labels: isDonut ? donutLabels : undefined,
303
- };
304
- }
305
- onRemove() { this.onRemoveWidget(this.widget.id); }
306
810
  }
307
- WidgetRendererComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: WidgetRendererComponent, deps: [{ token: DashboardStateService }], target: i0.ɵɵFactoryTarget.Component });
308
- WidgetRendererComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: WidgetRendererComponent, selector: "app-widget-renderer", inputs: { widget: "widget", onRemoveWidget: "onRemoveWidget", theme: "theme" }, outputs: { editRequested: "editRequested" }, usesOnChanges: true, ngImport: i0, template: `
309
- <div class="widget-container" [ngStyle]="cardStyles">
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 -->
310
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>
311
823
  <span class="widget-title">{{ widget.title }}</span>
312
824
  <div class="header-actions">
313
- <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit widget">
825
+ <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit">
314
826
  <i class="la la-pen"></i>
315
827
  </button>
316
- <button class="btn-icon btn-close-widget" (click)="onRemove()" title="Remove widget">
828
+ <button class="btn-icon btn-remove" (click)="removeRequested.emit(widget.id)" title="Remove">
317
829
  <i class="la la-times"></i>
318
830
  </button>
319
831
  </div>
320
832
  </div>
833
+
834
+ <!-- Consumer content slot -->
321
835
  <div class="widget-body">
322
- <apx-chart
323
- *ngIf="chartOptions.chart"
324
- style="display:block;width:100%;height:100%;"
325
- [series]="chartOptions.series!"
326
- [chart]="chartOptions.chart!"
327
- [xaxis]="chartOptions.xaxis!"
328
- [stroke]="chartOptions.stroke!"
329
- [dataLabels]="chartOptions.dataLabels!"
330
- [plotOptions]="chartOptions.plotOptions!"
331
- [yaxis]="chartOptions.yaxis!"
332
- [labels]="chartOptions.labels!"
333
- [legend]="chartOptions.legend!"
334
- [colors]="chartOptions.colors!"
335
- [theme]="chartOptions.theme!"
336
- [tooltip]="chartOptions.tooltip!"
337
- ></apx-chart>
836
+ <ng-content></ng-content>
338
837
  </div>
339
838
  </div>
340
- `, isInline: true, styles: [".widget-container{height:100%;width:100%;display:flex;flex-direction:column;border-radius:30px;overflow:hidden;box-shadow:0 8px 24px #00000040;border:1px solid rgba(255,255,255,.08);transition:box-shadow .2s ease;box-sizing:border-box;background:var(--dash-card-bg)}.widget-container:hover{box-shadow:0 12px 32px #00000059}.widget-header{padding:14px 16px 4px;display:flex;justify-content:space-between;align-items:center;cursor:move;flex-shrink:0}.widget-title{color:#fff;font-size:15px;font-weight:600;letter-spacing:.3px}.header-actions{display:flex;gap:6px;align-items:center}.btn-icon{background:rgba(255,255,255,.08);border:none;color:var(--dash-fore-color);cursor:pointer;font-size:12px;width:26px;height:26px;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,.25);color:var(--dash-accent-color)}.btn-close-widget:hover{background:rgba(255,69,58,.25);color:var(--dash-danger-color)}.widget-body{flex:1;min-height:0;padding:4px 12px 12px;display:flex;flex-direction:column}\n"], dependencies: [{ kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.ChartComponent, selector: "apx-chart", inputs: ["chart", "annotations", "colors", "dataLabels", "series", "stroke", "labels", "legend", "markers", "noData", "fill", "tooltip", "plotOptions", "responsive", "xaxis", "yaxis", "forecastDataPoints", "grid", "states", "title", "subtitle", "theme", "autoUpdateSeries"] }] });
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"] });
341
840
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: WidgetRendererComponent, decorators: [{
342
841
  type: Component,
343
842
  args: [{ selector: 'app-widget-renderer', template: `
344
- <div class="widget-container" [ngStyle]="cardStyles">
843
+ <div class="widget-card" [style.background]="cardBg">
844
+ <!-- Header: drag handle + title + actions -->
345
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>
346
853
  <span class="widget-title">{{ widget.title }}</span>
347
854
  <div class="header-actions">
348
- <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit widget">
855
+ <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit">
349
856
  <i class="la la-pen"></i>
350
857
  </button>
351
- <button class="btn-icon btn-close-widget" (click)="onRemove()" title="Remove widget">
858
+ <button class="btn-icon btn-remove" (click)="removeRequested.emit(widget.id)" title="Remove">
352
859
  <i class="la la-times"></i>
353
860
  </button>
354
861
  </div>
355
862
  </div>
863
+
864
+ <!-- Consumer content slot -->
356
865
  <div class="widget-body">
357
- <apx-chart
358
- *ngIf="chartOptions.chart"
359
- style="display:block;width:100%;height:100%;"
360
- [series]="chartOptions.series!"
361
- [chart]="chartOptions.chart!"
362
- [xaxis]="chartOptions.xaxis!"
363
- [stroke]="chartOptions.stroke!"
364
- [dataLabels]="chartOptions.dataLabels!"
365
- [plotOptions]="chartOptions.plotOptions!"
366
- [yaxis]="chartOptions.yaxis!"
367
- [labels]="chartOptions.labels!"
368
- [legend]="chartOptions.legend!"
369
- [colors]="chartOptions.colors!"
370
- [theme]="chartOptions.theme!"
371
- [tooltip]="chartOptions.tooltip!"
372
- ></apx-chart>
866
+ <ng-content></ng-content>
373
867
  </div>
374
868
  </div>
375
- `, styles: [".widget-container{height:100%;width:100%;display:flex;flex-direction:column;border-radius:30px;overflow:hidden;box-shadow:0 8px 24px #00000040;border:1px solid rgba(255,255,255,.08);transition:box-shadow .2s ease;box-sizing:border-box;background:var(--dash-card-bg)}.widget-container:hover{box-shadow:0 12px 32px #00000059}.widget-header{padding:14px 16px 4px;display:flex;justify-content:space-between;align-items:center;cursor:move;flex-shrink:0}.widget-title{color:#fff;font-size:15px;font-weight:600;letter-spacing:.3px}.header-actions{display:flex;gap:6px;align-items:center}.btn-icon{background:rgba(255,255,255,.08);border:none;color:var(--dash-fore-color);cursor:pointer;font-size:12px;width:26px;height:26px;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,.25);color:var(--dash-accent-color)}.btn-close-widget:hover{background:rgba(255,69,58,.25);color:var(--dash-danger-color)}.widget-body{flex:1;min-height:0;padding:4px 12px 12px;display:flex;flex-direction:column}\n"] }]
376
- }], ctorParameters: function () { return [{ type: DashboardStateService }]; }, propDecorators: { widget: [{
377
- type: Input
378
- }], onRemoveWidget: [{
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: [{
379
871
  type: Input
380
872
  }], theme: [{
381
873
  type: Input
382
874
  }], editRequested: [{
383
875
  type: Output
876
+ }], removeRequested: [{
877
+ type: Output
384
878
  }] } });
385
879
 
386
880
  const DEFAULT_THEME = {
387
881
  backgroundColor: '#000000',
388
882
  panelColor: '#1c1c1e',
389
883
  widgetCardColor: '#2c2c2e',
390
- chartForeColor: '#8e8e93',
884
+ foreColor: '#8e8e93',
391
885
  accentColor: '#0a84ff',
392
886
  dangerColor: '#ff453a',
393
- chartColors: ['#0a84ff', '#30d158', '#ff9f0a', '#bf5af2', '#ff453a'],
394
887
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
395
888
  };
396
889
  class DashboardComponent {
397
890
  constructor(stateService) {
398
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();
399
902
  this.resolvedTheme = { ...DEFAULT_THEME };
400
903
  this.wrapperStyles = {};
401
904
  this.pages = [];
402
905
  this.activePageId = '';
403
- // ── Dialog state ──
404
- this.dialogOpen = false;
405
- this.dialogType = 'LINE';
406
- this.dialogTitle = '';
407
- this.dialogCategories = '';
408
- this.dialogSeries = [{ name: 'Series 1', dataRaw: '' }];
409
- this.dialogSlices = [
410
- { label: 'Slice A', value: null },
411
- { label: 'Slice B', value: null },
412
- { label: 'Slice C', value: null },
413
- ];
414
- this.widgetTypes = [
415
- { value: 'LINE', label: 'Line', icon: 'la-chart-area' },
416
- { value: 'BAR', label: 'Bar', icon: 'la-chart-bar' },
417
- { value: 'DONUT', label: 'Donut', icon: 'la-chart-pie' },
418
- ];
906
+ this.isPoppedOut = false;
419
907
  this.subs = new Subscription();
420
- // ── Edit dialog state ──
421
- this.editDialogOpen = false;
422
- this.editWidgetType = 'LINE';
423
- this.editTitle = '';
424
- this.editCardColor = '';
425
- this.editColors = [];
426
- this.editSeries = [];
427
- this.editCategories = '';
428
- this.editSlices = [];
429
- this.DEFAULT_COLORS = ['#0a84ff', '#30d158', '#ff9f0a', '#bf5af2', '#ff453a'];
430
- this.removeWidgetHandler = (id) => { this.stateService.removeWidget(id); };
431
908
  }
432
909
  ngOnChanges(changes) {
433
910
  if (changes['theme'] || changes['initialLayout'])
@@ -443,21 +920,16 @@ class DashboardComponent {
443
920
  console.error('[Dashboard] Failed to parse initialLayout', e);
444
921
  }
445
922
  }
446
- this.options = {
447
- gridType: 'fit',
448
- displayGrid: 'none',
449
- pushItems: true,
450
- draggable: { enabled: true },
451
- resizable: { enabled: true },
452
- minCols: 12, maxCols: 12,
453
- minRows: 12, maxRows: 50,
454
- margin: 20, outerMargin: true,
455
- itemChangeCallback: (item) => {
456
- this.stateService.updateWidgetPosition(this.activePageId, item.id, item.x, item.y, item.cols, item.rows);
457
- }
458
- };
923
+ const hash = window.location.hash?.replace('#', '').trim();
924
+ if (hash)
925
+ this.isPoppedOut = true;
459
926
  this.subs.add(this.stateService.pages$.subscribe(pages => {
460
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
+ }
461
933
  this.updateActivePage();
462
934
  }));
463
935
  this.subs.add(this.stateService.activePageId$.subscribe(id => {
@@ -466,25 +938,13 @@ class DashboardComponent {
466
938
  }));
467
939
  }
468
940
  ngOnDestroy() { this.subs.unsubscribe(); }
469
- applyTheme() {
470
- this.resolvedTheme = { ...DEFAULT_THEME, ...(this.theme ?? {}) };
471
- this.wrapperStyles = {
472
- '--dash-bg': this.resolvedTheme.backgroundColor,
473
- '--dash-panel-bg': this.resolvedTheme.panelColor,
474
- '--dash-card-bg': this.resolvedTheme.widgetCardColor,
475
- '--dash-fore-color': this.resolvedTheme.chartForeColor,
476
- '--dash-accent-color': this.resolvedTheme.accentColor,
477
- '--dash-danger-color': this.resolvedTheme.dangerColor,
478
- '--dash-font-family': this.resolvedTheme.fontFamily,
479
- };
480
- }
481
- updateActivePage() {
482
- this.activePage = this.pages.find(p => p.id === this.activePageId);
941
+ onItemChanged(widget) {
942
+ this.stateService.updateWidgetPosition(this.activePageId, widget.id, widget.x, widget.y, widget.cols, widget.rows);
483
943
  }
484
- // ── Page actions ──
944
+ onRemoveWidget(widgetId) { this.stateService.removeWidget(widgetId); }
485
945
  onSelectPage(id) { this.stateService.setActivePage(id); }
486
946
  onAddPage() {
487
- const name = prompt('Enter workspace name:', `Workspace ${this.pages.length + 1}`);
947
+ const name = prompt('Workspace name:', `Workspace ${this.pages.length + 1}`);
488
948
  if (name)
489
949
  this.stateService.addPage(name);
490
950
  }
@@ -493,653 +953,277 @@ class DashboardComponent {
493
953
  if (confirm('Remove this workspace?'))
494
954
  this.stateService.removePage(id);
495
955
  }
496
- // ── Dialog ──
497
- openDialog() {
498
- this.dialogType = 'LINE';
499
- this.dialogTitle = '';
500
- this.dialogCategories = '';
501
- this.dialogSeries = [{ name: 'Series 1', dataRaw: '' }];
502
- this.dialogSlices = [
503
- { label: 'Slice A', value: null },
504
- { label: 'Slice B', value: null },
505
- { label: 'Slice C', value: null },
506
- ];
507
- this.dialogOpen = true;
508
- }
509
- closeDialog() { this.dialogOpen = false; }
510
- addSeries() { this.dialogSeries.push({ name: `Series ${this.dialogSeries.length + 1}`, dataRaw: '' }); }
511
- removeSeries(i) { this.dialogSeries.splice(i, 1); }
512
- addSlice() { this.dialogSlices.push({ label: `Slice ${this.dialogSlices.length + 1}`, value: null }); }
513
- removeSlice(i) { this.dialogSlices.splice(i, 1); }
514
- confirmAddWidget() {
515
- const title = this.dialogTitle.trim();
516
- if (!title)
517
- return;
518
- let data;
519
- if (this.dialogType === 'DONUT') {
520
- const validSlices = this.dialogSlices.filter(s => s.value !== null && s.label.trim());
521
- data = {
522
- series: validSlices.length
523
- ? validSlices.map(s => Number(s.value))
524
- : [30, 20, 50],
525
- labels: validSlices.length
526
- ? validSlices.map(s => s.label)
527
- : ['A', 'B', 'C'],
528
- };
529
- }
530
- else {
531
- const parseNums = (raw) => raw.split(',').map(v => parseFloat(v.trim())).filter(n => !isNaN(n));
532
- data = {
533
- series: this.dialogSeries.map(s => ({
534
- name: s.name || 'Series',
535
- data: s.dataRaw.trim()
536
- ? parseNums(s.dataRaw)
537
- : Array.from({ length: 8 }, () => Math.floor(Math.random() * 100)),
538
- })),
539
- categories: this.dialogCategories.trim()
540
- ? this.dialogCategories.split(',').map(c => c.trim())
541
- : undefined,
542
- };
543
- }
544
- this.stateService.addWidgetWithData(this.dialogType, title, data);
545
- this.closeDialog();
546
- }
547
- openEditDialog(widget) {
548
- this.editingWidget = widget;
549
- this.editWidgetType = widget.type;
550
- this.editTitle = widget.title;
551
- this.editCardColor = widget.cardColor ?? this.resolvedTheme.widgetCardColor;
552
- this.editColors = widget.colors?.length
553
- ? [...widget.colors]
554
- : [...this.resolvedTheme.chartColors];
555
- if (widget.type === 'DONUT') {
556
- const d = widget.data;
557
- this.editSlices = d.series.map((v, i) => ({
558
- label: d.labels?.[i] ?? `Slice ${i + 1}`,
559
- value: v,
560
- }));
561
- this.editColors = this.editColors.slice(0, this.editSlices.length);
562
- while (this.editColors.length < this.editSlices.length) {
563
- this.editColors.push(this.DEFAULT_COLORS[this.editColors.length % this.DEFAULT_COLORS.length]);
564
- }
565
- }
566
- else {
567
- const d = widget.data;
568
- this.editSeries = d.series.map((s) => ({ name: s.name, dataRaw: s.data.join(',') }));
569
- this.editCategories = d.categories?.join(',') ?? '';
570
- this.editColors = this.editColors.slice(0, this.editSeries.length);
571
- while (this.editColors.length < this.editSeries.length) {
572
- this.editColors.push(this.DEFAULT_COLORS[this.editColors.length % this.DEFAULT_COLORS.length]);
573
- }
574
- }
575
- this.editDialogOpen = true;
576
- }
577
- closeEditDialog() { this.editDialogOpen = false; this.editingWidget = undefined; }
578
- getEditSeriesLabel(i) {
579
- if (this.editWidgetType === 'DONUT')
580
- return this.editSlices[i]?.label || `Slice ${i + 1}`;
581
- return this.editSeries[i]?.name || `Series ${i + 1}`;
582
- }
583
- addEditSeries() {
584
- this.editSeries.push({ name: `Series ${this.editSeries.length + 1}`, dataRaw: '' });
585
- this.editColors.push(this.DEFAULT_COLORS[this.editColors.length % this.DEFAULT_COLORS.length]);
586
- }
587
- removeEditSeries(i) { this.editSeries.splice(i, 1); this.editColors.splice(i, 1); }
588
- addEditSlice() {
589
- this.editSlices.push({ label: `Slice ${this.editSlices.length + 1}`, value: null });
590
- this.editColors.push(this.DEFAULT_COLORS[this.editColors.length % this.DEFAULT_COLORS.length]);
956
+ onPopOut(event, pageId) {
957
+ event.stopPropagation();
958
+ this.stateService.popOutPage(pageId);
591
959
  }
592
- removeEditSlice(i) { this.editSlices.splice(i, 1); this.editColors.splice(i, 1); }
593
- confirmEditWidget() {
594
- if (!this.editingWidget)
595
- return;
596
- const parseNums = (raw) => raw.split(',').map(v => parseFloat(v.trim())).filter(n => !isNaN(n));
597
- let data;
598
- if (this.editWidgetType === 'DONUT') {
599
- data = {
600
- series: this.editSlices.map(s => Number(s.value) || 0),
601
- labels: this.editSlices.map(s => s.label),
602
- };
603
- }
604
- else {
605
- data = {
606
- series: this.editSeries.map(s => ({
607
- name: s.name || 'Series',
608
- data: s.dataRaw.trim() ? parseNums(s.dataRaw) : [],
609
- })),
610
- categories: this.editCategories.trim()
611
- ? this.editCategories.split(',').map(c => c.trim())
612
- : undefined,
613
- };
614
- }
615
- this.stateService.updateWidgetMeta(this.editingWidget.id, {
616
- title: this.editTitle.trim() || this.editingWidget.title,
617
- colors: [...this.editColors],
618
- cardColor: this.editCardColor,
619
- data,
620
- });
621
- this.closeEditDialog();
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
+ };
622
972
  }
623
- // ── Widget actions ──
624
- onAddWidget(type) { this.stateService.addWidget(type); }
625
- updateWidgetData(widgetId, data) {
626
- this.stateService.updateWidgetData(widgetId, data);
973
+ updateActivePage() {
974
+ this.activePage = this.pages.find(p => p.id === this.activePageId);
627
975
  }
628
- serializeLayout() { return this.stateService.serializeLayout(); }
629
976
  }
630
977
  DashboardComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardComponent, deps: [{ token: DashboardStateService }], target: i0.ɵɵFactoryTarget.Component });
631
- DashboardComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: DashboardComponent, selector: "app-dashboard", inputs: { initialLayout: "initialLayout", theme: "theme" }, usesOnChanges: true, ngImport: i0, template: `
632
- <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
633
- <main class="main-content">
634
-
635
- <!-- ── Header ── -->
636
- <header class="dashboard-header">
637
- <!-- Tabs -->
638
- <div class="tabs-container">
639
- <div
640
- *ngFor="let page of pages"
641
- class="tab"
642
- [class.active]="page.id === activePageId"
643
- (click)="onSelectPage(page.id)"
644
- >
645
- <span class="tab-name">{{ page.name }}</span>
646
- <button class="tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)">
647
- <i class="la la-times"></i>
648
- </button>
649
- </div>
650
- <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
651
- <i class="la la-plus"></i>
652
- </button>
653
- </div>
654
-
655
- <!-- Add Widget button -->
656
- <button class="btn-add-widget" (click)="openDialog()" title="Add widget">
657
- <i class="la la-plus"></i>
658
- <span>Add Widget</span>
659
- </button>
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>
660
986
  </header>
661
-
662
- <!-- ── Grid ── -->
663
987
  <div class="grid-container">
664
- <gridster [options]="options">
665
- <gridster-item [item]="widget" *ngFor="let widget of activePage?.widgets">
988
+ <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
989
+ <ng-template gridCell let-widget="widget">
666
990
  <app-widget-renderer
667
991
  [widget]="widget"
668
- [onRemoveWidget]="removeWidgetHandler"
669
992
  [theme]="resolvedTheme"
670
- (editRequested)="openEditDialog($event)"
671
- ></app-widget-renderer>
672
- </gridster-item>
673
- </gridster>
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>
674
1004
  </div>
675
- </main>
676
- </div>
677
-
678
- <!-- ── Add Widget Dialog ── -->
679
- <div class="dialog-backdrop" *ngIf="dialogOpen" (click)="closeDialog()"></div>
680
- <div class="dialog" *ngIf="dialogOpen">
681
- <div class="dialog-header">
682
- <span class="dialog-title">Add Widget</span>
683
- <button class="dialog-close" (click)="closeDialog()"><i class="la la-times"></i></button>
684
1005
  </div>
1006
+ </ng-container>
685
1007
 
686
- <div class="dialog-body">
687
-
688
- <!-- Chart type picker -->
689
- <div class="field-group">
690
- <label class="field-label">Chart type</label>
691
- <div class="type-picker">
692
- <button
693
- *ngFor="let t of widgetTypes"
694
- class="type-btn"
695
- [class.selected]="dialogType === t.value"
696
- (click)="dialogType = t.value"
697
- >
698
- <i [class]="'la ' + t.icon"></i>
699
- <span>{{ t.label }}</span>
700
- </button>
701
- </div>
702
- </div>
1008
+ <!-- ══════════════════════════════════════════════════════════
1009
+ NORMAL MODE
1010
+ ══════════════════════════════════════════════════════════ -->
1011
+ <ng-template #normalMode>
1012
+ <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
1013
+ <main class="main-content">
703
1014
 
704
- <!-- Title -->
705
- <div class="field-group">
706
- <label class="field-label">Title</label>
707
- <input class="field-input" [(ngModel)]="dialogTitle" placeholder="My Chart" />
708
- </div>
709
-
710
- <!-- Series (LINE / BAR) -->
711
- <ng-container *ngIf="dialogType !== 'DONUT'">
712
- <div class="field-group">
713
- <div class="field-row-header">
714
- <label class="field-label">Series</label>
715
- <button class="btn-link" (click)="addSeries()"><i class="la la-plus"></i> Add series</button>
716
- </div>
717
- <div class="series-list">
718
- <div class="series-row" *ngFor="let s of dialogSeries; let i = index">
719
- <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Series name" />
720
- <input class="field-input series-data" [(ngModel)]="s.dataRaw"
721
- placeholder="Comma-separated values, e.g. 10,25,40" />
722
- <button class="btn-remove-series" *ngIf="dialogSeries.length > 1" (click)="removeSeries(i)">
723
- <i class="la la-times"></i>
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>
724
1026
  </button>
725
- </div>
726
- </div>
727
- </div>
728
-
729
- <div class="field-group">
730
- <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
731
- <input class="field-input" [(ngModel)]="dialogCategories" placeholder="Jan,Feb,Mar,Apr" />
732
- </div>
733
- </ng-container>
734
-
735
- <!-- Slices (DONUT) -->
736
- <ng-container *ngIf="dialogType === 'DONUT'">
737
- <div class="field-group">
738
- <div class="field-row-header">
739
- <label class="field-label">Slices</label>
740
- <button class="btn-link" (click)="addSlice()"><i class="la la-plus"></i> Add slice</button>
741
- </div>
742
- <div class="series-list">
743
- <div class="series-row" *ngFor="let sl of dialogSlices; let i = index">
744
- <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
745
- <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
746
- <button class="btn-remove-series" *ngIf="dialogSlices.length > 1" (click)="removeSlice(i)">
1027
+ <button class="tab-action tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)" title="Close">
747
1028
  <i class="la la-times"></i>
748
1029
  </button>
749
1030
  </div>
1031
+ <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
1032
+ <i class="la la-plus"></i>
1033
+ </button>
750
1034
  </div>
751
- </div>
752
- </ng-container>
753
-
754
- </div>
755
-
756
- <div class="dialog-footer">
757
- <button class="btn-cancel" (click)="closeDialog()">Cancel</button>
758
- <button class="btn-confirm" (click)="confirmAddWidget()" [disabled]="!dialogTitle.trim()">
759
- <i class="la la-check"></i> Add Widget
760
- </button>
761
- </div>
762
- </div>
763
- <!-- ── Edit Widget Dialog ── -->
764
- <div class="dialog-backdrop" *ngIf="editDialogOpen" (click)="closeEditDialog()"></div>
765
- <div class="dialog" *ngIf="editDialogOpen">
766
- <div class="dialog-header">
767
- <span class="dialog-title">Edit Widget</span>
768
- <button class="dialog-close" (click)="closeEditDialog()"><i class="la la-times"></i></button>
769
- </div>
770
-
771
- <div class="dialog-body">
772
-
773
- <!-- Title -->
774
- <div class="field-group">
775
- <label class="field-label">Title</label>
776
- <input class="field-input" [(ngModel)]="editTitle" placeholder="Chart title" />
777
- </div>
778
-
779
- <!-- Card background -->
780
- <div class="field-group">
781
- <label class="field-label">Card color</label>
782
- <div class="color-row">
783
- <input type="color" class="color-swatch" [(ngModel)]="editCardColor" />
784
- <input class="field-input" [(ngModel)]="editCardColor" placeholder="#2c2c2e" />
785
- </div>
786
- </div>
787
-
788
- <!-- Series colors -->
789
- <div class="field-group">
790
- <label class="field-label">Series colors</label>
791
- <div class="color-list">
792
- <div class="color-item" *ngFor="let c of editColors; let i = index">
793
- <input type="color" class="color-swatch" [(ngModel)]="editColors[i]" />
794
- <input class="field-input color-hex" [(ngModel)]="editColors[i]" placeholder="#0a84ff" />
795
- <span class="color-label">{{ getEditSeriesLabel(i) }}</span>
796
- </div>
797
- </div>
798
- </div>
799
1035
 
800
- <!-- Data: LINE / BAR -->
801
- <ng-container *ngIf="editWidgetType !== 'DONUT'">
802
- <div class="field-group">
803
- <div class="field-row-header">
804
- <label class="field-label">Series data</label>
805
- <button class="btn-link" (click)="addEditSeries()"><i class="la la-plus"></i> Add</button>
806
- </div>
807
- <div class="series-list">
808
- <div class="series-row" *ngFor="let s of editSeries; let i = index">
809
- <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Name" />
810
- <input class="field-input series-data" [(ngModel)]="s.dataRaw" placeholder="10,20,30" />
811
- <button class="btn-remove-series" *ngIf="editSeries.length > 1" (click)="removeEditSeries(i)">
812
- <i class="la la-times"></i>
813
- </button>
814
- </div>
815
- </div>
816
- </div>
817
- <div class="field-group">
818
- <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
819
- <input class="field-input" [(ngModel)]="editCategories" placeholder="Jan,Feb,Mar" />
820
- </div>
821
- </ng-container>
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>
822
1042
 
823
- <!-- Data: DONUT -->
824
- <ng-container *ngIf="editWidgetType === 'DONUT'">
825
- <div class="field-group">
826
- <div class="field-row-header">
827
- <label class="field-label">Slices</label>
828
- <button class="btn-link" (click)="addEditSlice()"><i class="la la-plus"></i> Add</button>
829
- </div>
830
- <div class="series-list">
831
- <div class="series-row" *ngFor="let sl of editSlices; let i = index">
832
- <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
833
- <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
834
- <button class="btn-remove-series" *ngIf="editSlices.length > 1" (click)="removeEditSlice(i)">
835
- <i class="la la-times"></i>
836
- </button>
837
- </div>
838
- </div>
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>
839
1061
  </div>
840
- </ng-container>
841
1062
 
1063
+ </main>
842
1064
  </div>
843
-
844
- <div class="dialog-footer">
845
- <button class="btn-cancel" (click)="closeEditDialog()">Cancel</button>
846
- <button class="btn-confirm" (click)="confirmEditWidget()">
847
- <i class="la la-check"></i> Save
848
- </button>
849
- </div>
850
- </div>
851
- `, 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)}.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;padding:8px 20px;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{margin-right:8px}.tab-close{background:transparent;border:none;color:var(--dash-fore-color);padding:2px;font-size:12px;cursor:pointer;opacity:0;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s}.tab-close:hover{color:var(--dash-danger-color);background:rgba(255,69,58,.2)}.tab:hover .tab-close{opacity:1}.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)}.grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0}gridster{background:transparent;height:100%!important}.dialog-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(6px);z-index:900}.dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:901;width:min(520px,calc(100vw - 32px));max-height:calc(100vh - 64px);display:flex;flex-direction:column;background:var(--dash-panel-bg);border-radius:28px;border:1px solid rgba(255,255,255,.1);box-shadow:0 24px 60px #000000b3;overflow:hidden}.dialog-header{display:flex;align-items:center;justify-content:space-between;padding:22px 24px 16px;border-bottom:1px solid rgba(255,255,255,.07)}.dialog-title{font-size:17px;font-weight:700;color:#fff}.dialog-close{background:rgba(255,255,255,.08);border:none;color:var(--dash-fore-color);width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;transition:all .2s}.dialog-close:hover{background:rgba(255,69,58,.25);color:var(--dash-danger-color)}.dialog-body{flex:1;overflow-y:auto;padding:20px 24px;display:flex;flex-direction:column;gap:20px}.field-group{display:flex;flex-direction:column;gap:8px}.field-label{font-size:13px;font-weight:600;color:var(--dash-fore-color);text-transform:uppercase;letter-spacing:.5px}.field-input{background:var(--dash-card-bg);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:10px 14px;color:#fff;font-size:14px;outline:none;width:100%;box-sizing:border-box}.field-input:focus{border-color:var(--dash-accent-color)}.type-picker{display:flex;gap:10px}.type-btn{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;padding:14px 10px;background:var(--dash-card-bg);border:2px solid transparent;border-radius:16px;color:var(--dash-fore-color);font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.type-btn.selected{border-color:var(--dash-accent-color);color:var(--dash-accent-color);background:rgba(10,132,255,.12)}.btn-link{background:transparent;border:none;color:var(--dash-accent-color);font-size:13px;font-weight:600;cursor:pointer}.dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:16px 24px 22px;border-top:1px solid rgba(255,255,255,.07)}.btn-cancel{background:transparent;border:1px solid rgba(255,255,255,.12);color:var(--dash-fore-color);padding:10px 20px;border-radius:20px;cursor:pointer}.btn-confirm{background:var(--dash-accent-color);border:none;color:#fff;padding:10px 22px;border-radius:20px;font-weight:600;cursor:pointer}.btn-confirm:disabled{opacity:.4;cursor:not-allowed}\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.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i3$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4.GridsterComponent, selector: "gridster", inputs: ["options"] }, { kind: "component", type: i4.GridsterItemComponent, selector: "gridster-item", inputs: ["item"], outputs: ["itemInit", "itemChange", "itemResize"] }, { kind: "component", type: WidgetRendererComponent, selector: "app-widget-renderer", inputs: ["widget", "onRemoveWidget", "theme"], outputs: ["editRequested"] }] });
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]" }] });
852
1067
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardComponent, decorators: [{
853
1068
  type: Component,
854
1069
  args: [{ selector: 'app-dashboard', template: `
855
- <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
856
- <main class="main-content">
857
-
858
- <!-- ── Header ── -->
859
- <header class="dashboard-header">
860
- <!-- Tabs -->
861
- <div class="tabs-container">
862
- <div
863
- *ngFor="let page of pages"
864
- class="tab"
865
- [class.active]="page.id === activePageId"
866
- (click)="onSelectPage(page.id)"
867
- >
868
- <span class="tab-name">{{ page.name }}</span>
869
- <button class="tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)">
870
- <i class="la la-times"></i>
871
- </button>
872
- </div>
873
- <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
874
- <i class="la la-plus"></i>
875
- </button>
876
- </div>
877
-
878
- <!-- Add Widget button -->
879
- <button class="btn-add-widget" (click)="openDialog()" title="Add widget">
880
- <i class="la la-plus"></i>
881
- <span>Add Widget</span>
882
- </button>
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>
883
1077
  </header>
884
-
885
- <!-- ── Grid ── -->
886
1078
  <div class="grid-container">
887
- <gridster [options]="options">
888
- <gridster-item [item]="widget" *ngFor="let widget of activePage?.widgets">
1079
+ <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
1080
+ <ng-template gridCell let-widget="widget">
889
1081
  <app-widget-renderer
890
1082
  [widget]="widget"
891
- [onRemoveWidget]="removeWidgetHandler"
892
1083
  [theme]="resolvedTheme"
893
- (editRequested)="openEditDialog($event)"
894
- ></app-widget-renderer>
895
- </gridster-item>
896
- </gridster>
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>
897
1095
  </div>
898
- </main>
899
- </div>
900
-
901
- <!-- ── Add Widget Dialog ── -->
902
- <div class="dialog-backdrop" *ngIf="dialogOpen" (click)="closeDialog()"></div>
903
- <div class="dialog" *ngIf="dialogOpen">
904
- <div class="dialog-header">
905
- <span class="dialog-title">Add Widget</span>
906
- <button class="dialog-close" (click)="closeDialog()"><i class="la la-times"></i></button>
907
1096
  </div>
1097
+ </ng-container>
908
1098
 
909
- <div class="dialog-body">
910
-
911
- <!-- Chart type picker -->
912
- <div class="field-group">
913
- <label class="field-label">Chart type</label>
914
- <div class="type-picker">
915
- <button
916
- *ngFor="let t of widgetTypes"
917
- class="type-btn"
918
- [class.selected]="dialogType === t.value"
919
- (click)="dialogType = t.value"
920
- >
921
- <i [class]="'la ' + t.icon"></i>
922
- <span>{{ t.label }}</span>
923
- </button>
924
- </div>
925
- </div>
1099
+ <!-- ══════════════════════════════════════════════════════════
1100
+ NORMAL MODE
1101
+ ══════════════════════════════════════════════════════════ -->
1102
+ <ng-template #normalMode>
1103
+ <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
1104
+ <main class="main-content">
926
1105
 
927
- <!-- Title -->
928
- <div class="field-group">
929
- <label class="field-label">Title</label>
930
- <input class="field-input" [(ngModel)]="dialogTitle" placeholder="My Chart" />
931
- </div>
932
-
933
- <!-- Series (LINE / BAR) -->
934
- <ng-container *ngIf="dialogType !== 'DONUT'">
935
- <div class="field-group">
936
- <div class="field-row-header">
937
- <label class="field-label">Series</label>
938
- <button class="btn-link" (click)="addSeries()"><i class="la la-plus"></i> Add series</button>
939
- </div>
940
- <div class="series-list">
941
- <div class="series-row" *ngFor="let s of dialogSeries; let i = index">
942
- <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Series name" />
943
- <input class="field-input series-data" [(ngModel)]="s.dataRaw"
944
- placeholder="Comma-separated values, e.g. 10,25,40" />
945
- <button class="btn-remove-series" *ngIf="dialogSeries.length > 1" (click)="removeSeries(i)">
946
- <i class="la la-times"></i>
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>
947
1117
  </button>
948
- </div>
949
- </div>
950
- </div>
951
-
952
- <div class="field-group">
953
- <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
954
- <input class="field-input" [(ngModel)]="dialogCategories" placeholder="Jan,Feb,Mar,Apr" />
955
- </div>
956
- </ng-container>
957
-
958
- <!-- Slices (DONUT) -->
959
- <ng-container *ngIf="dialogType === 'DONUT'">
960
- <div class="field-group">
961
- <div class="field-row-header">
962
- <label class="field-label">Slices</label>
963
- <button class="btn-link" (click)="addSlice()"><i class="la la-plus"></i> Add slice</button>
964
- </div>
965
- <div class="series-list">
966
- <div class="series-row" *ngFor="let sl of dialogSlices; let i = index">
967
- <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
968
- <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
969
- <button class="btn-remove-series" *ngIf="dialogSlices.length > 1" (click)="removeSlice(i)">
1118
+ <button class="tab-action tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)" title="Close">
970
1119
  <i class="la la-times"></i>
971
1120
  </button>
972
1121
  </div>
1122
+ <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
1123
+ <i class="la la-plus"></i>
1124
+ </button>
973
1125
  </div>
974
- </div>
975
- </ng-container>
976
-
977
- </div>
978
-
979
- <div class="dialog-footer">
980
- <button class="btn-cancel" (click)="closeDialog()">Cancel</button>
981
- <button class="btn-confirm" (click)="confirmAddWidget()" [disabled]="!dialogTitle.trim()">
982
- <i class="la la-check"></i> Add Widget
983
- </button>
984
- </div>
985
- </div>
986
- <!-- ── Edit Widget Dialog ── -->
987
- <div class="dialog-backdrop" *ngIf="editDialogOpen" (click)="closeEditDialog()"></div>
988
- <div class="dialog" *ngIf="editDialogOpen">
989
- <div class="dialog-header">
990
- <span class="dialog-title">Edit Widget</span>
991
- <button class="dialog-close" (click)="closeEditDialog()"><i class="la la-times"></i></button>
992
- </div>
993
-
994
- <div class="dialog-body">
995
-
996
- <!-- Title -->
997
- <div class="field-group">
998
- <label class="field-label">Title</label>
999
- <input class="field-input" [(ngModel)]="editTitle" placeholder="Chart title" />
1000
- </div>
1001
-
1002
- <!-- Card background -->
1003
- <div class="field-group">
1004
- <label class="field-label">Card color</label>
1005
- <div class="color-row">
1006
- <input type="color" class="color-swatch" [(ngModel)]="editCardColor" />
1007
- <input class="field-input" [(ngModel)]="editCardColor" placeholder="#2c2c2e" />
1008
- </div>
1009
- </div>
1010
-
1011
- <!-- Series colors -->
1012
- <div class="field-group">
1013
- <label class="field-label">Series colors</label>
1014
- <div class="color-list">
1015
- <div class="color-item" *ngFor="let c of editColors; let i = index">
1016
- <input type="color" class="color-swatch" [(ngModel)]="editColors[i]" />
1017
- <input class="field-input color-hex" [(ngModel)]="editColors[i]" placeholder="#0a84ff" />
1018
- <span class="color-label">{{ getEditSeriesLabel(i) }}</span>
1019
- </div>
1020
- </div>
1021
- </div>
1022
1126
 
1023
- <!-- Data: LINE / BAR -->
1024
- <ng-container *ngIf="editWidgetType !== 'DONUT'">
1025
- <div class="field-group">
1026
- <div class="field-row-header">
1027
- <label class="field-label">Series data</label>
1028
- <button class="btn-link" (click)="addEditSeries()"><i class="la la-plus"></i> Add</button>
1029
- </div>
1030
- <div class="series-list">
1031
- <div class="series-row" *ngFor="let s of editSeries; let i = index">
1032
- <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Name" />
1033
- <input class="field-input series-data" [(ngModel)]="s.dataRaw" placeholder="10,20,30" />
1034
- <button class="btn-remove-series" *ngIf="editSeries.length > 1" (click)="removeEditSeries(i)">
1035
- <i class="la la-times"></i>
1036
- </button>
1037
- </div>
1038
- </div>
1039
- </div>
1040
- <div class="field-group">
1041
- <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
1042
- <input class="field-input" [(ngModel)]="editCategories" placeholder="Jan,Feb,Mar" />
1043
- </div>
1044
- </ng-container>
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>
1045
1133
 
1046
- <!-- Data: DONUT -->
1047
- <ng-container *ngIf="editWidgetType === 'DONUT'">
1048
- <div class="field-group">
1049
- <div class="field-row-header">
1050
- <label class="field-label">Slices</label>
1051
- <button class="btn-link" (click)="addEditSlice()"><i class="la la-plus"></i> Add</button>
1052
- </div>
1053
- <div class="series-list">
1054
- <div class="series-row" *ngFor="let sl of editSlices; let i = index">
1055
- <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
1056
- <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
1057
- <button class="btn-remove-series" *ngIf="editSlices.length > 1" (click)="removeEditSlice(i)">
1058
- <i class="la la-times"></i>
1059
- </button>
1060
- </div>
1061
- </div>
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>
1062
1152
  </div>
1063
- </ng-container>
1064
-
1065
- </div>
1066
1153
 
1067
- <div class="dialog-footer">
1068
- <button class="btn-cancel" (click)="closeEditDialog()">Cancel</button>
1069
- <button class="btn-confirm" (click)="confirmEditWidget()">
1070
- <i class="la la-check"></i> Save
1071
- </button>
1154
+ </main>
1072
1155
  </div>
1073
- </div>
1074
- `, 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)}.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;padding:8px 20px;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{margin-right:8px}.tab-close{background:transparent;border:none;color:var(--dash-fore-color);padding:2px;font-size:12px;cursor:pointer;opacity:0;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s}.tab-close:hover{color:var(--dash-danger-color);background:rgba(255,69,58,.2)}.tab:hover .tab-close{opacity:1}.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)}.grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0}gridster{background:transparent;height:100%!important}.dialog-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(6px);z-index:900}.dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:901;width:min(520px,calc(100vw - 32px));max-height:calc(100vh - 64px);display:flex;flex-direction:column;background:var(--dash-panel-bg);border-radius:28px;border:1px solid rgba(255,255,255,.1);box-shadow:0 24px 60px #000000b3;overflow:hidden}.dialog-header{display:flex;align-items:center;justify-content:space-between;padding:22px 24px 16px;border-bottom:1px solid rgba(255,255,255,.07)}.dialog-title{font-size:17px;font-weight:700;color:#fff}.dialog-close{background:rgba(255,255,255,.08);border:none;color:var(--dash-fore-color);width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;transition:all .2s}.dialog-close:hover{background:rgba(255,69,58,.25);color:var(--dash-danger-color)}.dialog-body{flex:1;overflow-y:auto;padding:20px 24px;display:flex;flex-direction:column;gap:20px}.field-group{display:flex;flex-direction:column;gap:8px}.field-label{font-size:13px;font-weight:600;color:var(--dash-fore-color);text-transform:uppercase;letter-spacing:.5px}.field-input{background:var(--dash-card-bg);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:10px 14px;color:#fff;font-size:14px;outline:none;width:100%;box-sizing:border-box}.field-input:focus{border-color:var(--dash-accent-color)}.type-picker{display:flex;gap:10px}.type-btn{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;padding:14px 10px;background:var(--dash-card-bg);border:2px solid transparent;border-radius:16px;color:var(--dash-fore-color);font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.type-btn.selected{border-color:var(--dash-accent-color);color:var(--dash-accent-color);background:rgba(10,132,255,.12)}.btn-link{background:transparent;border:none;color:var(--dash-accent-color);font-size:13px;font-weight:600;cursor:pointer}.dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:16px 24px 22px;border-top:1px solid rgba(255,255,255,.07)}.btn-cancel{background:transparent;border:1px solid rgba(255,255,255,.12);color:var(--dash-fore-color);padding:10px 20px;border-radius:20px;cursor:pointer}.btn-confirm{background:var(--dash-accent-color);border:none;color:#fff;padding:10px 22px;border-radius:20px;font-weight:600;cursor:pointer}.btn-confirm:disabled{opacity:.4;cursor:not-allowed}\n"] }]
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"] }]
1075
1158
  }], ctorParameters: function () { return [{ type: DashboardStateService }]; }, propDecorators: { initialLayout: [{
1076
1159
  type: Input
1077
1160
  }], theme: [{
1078
1161
  type: Input
1162
+ }], addWidgetRequested: [{
1163
+ type: Output
1164
+ }], editWidgetRequested: [{
1165
+ type: Output
1166
+ }], cellTemplate: [{
1167
+ type: ContentChild,
1168
+ args: [GridCellDirective, { read: TemplateRef }]
1079
1169
  }] } });
1080
1170
 
1081
1171
  class DashboardModule {
1082
1172
  }
1083
1173
  DashboardModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1084
1174
  DashboardModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, declarations: [DashboardComponent,
1085
- WidgetRendererComponent], imports: [CommonModule,
1086
- FormsModule,
1087
- GridsterModule,
1088
- NgApexchartsModule], exports: [DashboardComponent,
1089
- WidgetRendererComponent] });
1090
- DashboardModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, providers: [DashboardStateService], imports: [CommonModule,
1091
- FormsModule,
1092
- GridsterModule,
1093
- NgApexchartsModule] });
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] });
1094
1182
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, decorators: [{
1095
1183
  type: NgModule,
1096
1184
  args: [{
1097
1185
  declarations: [
1098
1186
  DashboardComponent,
1099
- WidgetRendererComponent
1187
+ WidgetRendererComponent,
1188
+ CustomGridComponent,
1189
+ GridCellDirective,
1100
1190
  ],
1101
1191
  imports: [
1102
1192
  CommonModule,
1103
- FormsModule,
1104
- GridsterModule,
1105
- NgApexchartsModule
1106
1193
  ],
1107
1194
  providers: [DashboardStateService],
1108
1195
  exports: [
1109
1196
  DashboardComponent,
1110
- WidgetRendererComponent
1197
+ WidgetRendererComponent,
1198
+ CustomGridComponent,
1199
+ GridCellDirective,
1111
1200
  ],
1112
- schemas: [NO_ERRORS_SCHEMA]
1113
1201
  }]
1114
1202
  }] });
1115
1203
  /**
1116
- * Root module for local demo / testing purposes.
1117
- * In the final NPM package, users would only import the `DashboardModule`.
1204
+ * Root module for local demo / testing purposes only.
1205
+ * Library consumers import DashboardModule, not AppModule.
1118
1206
  */
1119
1207
  class AppModule {
1120
1208
  }
1121
1209
  AppModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1122
1210
  AppModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: AppModule, bootstrap: [DashboardComponent], imports: [BrowserModule, DashboardModule] });
1123
- AppModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, imports: [BrowserModule,
1124
- DashboardModule] });
1211
+ AppModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, imports: [BrowserModule, DashboardModule] });
1125
1212
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, decorators: [{
1126
1213
  type: NgModule,
1127
1214
  args: [{
1128
- imports: [
1129
- BrowserModule,
1130
- DashboardModule
1131
- ],
1132
- bootstrap: [DashboardComponent]
1215
+ imports: [BrowserModule, DashboardModule],
1216
+ bootstrap: [DashboardComponent],
1133
1217
  }]
1134
1218
  }] });
1135
1219
 
1136
1220
  /*
1137
- * Public API Surface of the dashboard library
1221
+ * Public API Surface of @ogidor/dashboard
1138
1222
  */
1139
1223
 
1140
1224
  /**
1141
1225
  * Generated bundle index. Do not edit.
1142
1226
  */
1143
1227
 
1144
- export { AppModule, DashboardComponent, DashboardModule, DashboardStateService, WidgetRendererComponent };
1228
+ export { AppModule, CustomGridComponent, DashboardComponent, DashboardModule, DashboardStateService, GridCellDirective, WidgetRendererComponent };
1145
1229
  //# sourceMappingURL=ogidor-dashboard.mjs.map