@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,442 +1,919 @@
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 => (Object.assign({}, 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 => (Object.assign({}, 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 = Object.assign(Object.assign({}, 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
+ var _a, _b, _c;
165
+ const containerW = (_c = (_b = (_a = this.gridContainer) === null || _a === void 0 ? void 0 : _a.nativeElement) === null || _b === void 0 ? void 0 : _b.clientWidth) !== null && _c !== void 0 ? _c : 800;
166
+ return (containerW - (this.columns - 1) * this.gap) / this.columns;
167
+ }
168
+ colToPixel(col) {
169
+ return col * (this.cellWidth + this.gap);
170
+ }
171
+ rowToPixel(row) {
172
+ return row * (this.rowHeight + this.gap);
173
+ }
174
+ pixelToCol(px) {
175
+ return Math.round(px / (this.cellWidth + this.gap));
176
+ }
177
+ pixelToRow(px) {
178
+ return Math.round(px / (this.rowHeight + this.gap));
179
+ }
180
+ colsToPixelWidth(cols) {
181
+ return cols * this.cellWidth + (cols - 1) * this.gap;
182
+ }
183
+ rowsToPixelHeight(rows) {
184
+ return rows * this.rowHeight + (rows - 1) * this.gap;
185
+ }
186
+ getItemLeft(w) {
187
+ var _a;
188
+ return ((_a = this.dragging) === null || _a === void 0 ? void 0 : _a.id) === w.id ? this.dragging.currentPixelX : this.colToPixel(w.x);
189
+ }
190
+ getItemTop(w) {
191
+ var _a;
192
+ return ((_a = this.dragging) === null || _a === void 0 ? void 0 : _a.id) === w.id ? this.dragging.currentPixelY : this.rowToPixel(w.y);
193
+ }
194
+ getItemWidth(w) {
195
+ var _a;
196
+ return ((_a = this.resizing) === null || _a === void 0 ? void 0 : _a.id) === w.id ? this.resizing.currentPixelW : this.colsToPixelWidth(w.cols);
197
+ }
198
+ getItemHeight(w) {
199
+ var _a;
200
+ return ((_a = this.resizing) === null || _a === void 0 ? void 0 : _a.id) === w.id ? this.resizing.currentPixelH : this.rowsToPixelHeight(w.rows);
201
+ }
202
+ // ── Drag ──
203
+ onDragStart(event, widget) {
204
+ const target = event.target;
205
+ if (target.closest('button, input, select, textarea, a, .resize-handle'))
206
+ return;
207
+ event.preventDefault();
208
+ event.stopPropagation();
209
+ const rect = this.gridContainer.nativeElement.getBoundingClientRect();
210
+ const itemLeft = this.colToPixel(widget.x);
211
+ const itemTop = this.rowToPixel(widget.y);
212
+ this.dragging = {
213
+ id: widget.id,
214
+ offsetX: event.clientX - rect.left - itemLeft,
215
+ offsetY: event.clientY - rect.top - itemTop,
216
+ currentPixelX: itemLeft,
217
+ currentPixelY: itemTop,
218
+ };
219
+ this.previewWidgets = this.widgets.map(w => ({ id: w.id, x: w.x, y: w.y, cols: w.cols, rows: w.rows }));
220
+ this.placeholder = {
221
+ left: itemLeft, top: itemTop,
222
+ width: this.colsToPixelWidth(widget.cols),
223
+ height: this.rowsToPixelHeight(widget.rows),
224
+ };
225
+ this.cdr.markForCheck();
226
+ }
227
+ onResizeStart(event, widget) {
228
+ event.preventDefault();
229
+ event.stopPropagation();
230
+ this.resizing = {
231
+ id: widget.id,
232
+ startMouseX: event.clientX,
233
+ startMouseY: event.clientY,
234
+ startCols: widget.cols,
235
+ startRows: widget.rows,
236
+ currentPixelW: this.colsToPixelWidth(widget.cols),
237
+ currentPixelH: this.rowsToPixelHeight(widget.rows),
238
+ };
239
+ this.previewWidgets = this.widgets.map(w => ({ id: w.id, x: w.x, y: w.y, cols: w.cols, rows: w.rows }));
240
+ this.placeholder = {
241
+ left: this.colToPixel(widget.x), top: this.rowToPixel(widget.y),
242
+ width: this.colsToPixelWidth(widget.cols),
243
+ height: this.rowsToPixelHeight(widget.rows),
244
+ };
245
+ this.cdr.markForCheck();
246
+ }
247
+ onMouseMove(event) {
248
+ if (this.dragging)
249
+ this.handleDragMove(event);
250
+ else if (this.resizing)
251
+ this.handleResizeMove(event);
252
+ }
253
+ onMouseUp(_event) {
254
+ if (this.dragging)
255
+ this.zone.run(() => this.finalizeDrag());
256
+ else if (this.resizing)
257
+ this.zone.run(() => this.finalizeResize());
258
+ }
259
+ handleDragMove(event) {
260
+ if (!this.dragging)
261
+ return;
262
+ const rect = this.gridContainer.nativeElement.getBoundingClientRect();
263
+ const widget = this.widgets.find(w => w.id === this.dragging.id);
264
+ if (!widget)
265
+ return;
266
+ let px = event.clientX - rect.left - this.dragging.offsetX;
267
+ let py = event.clientY - rect.top - this.dragging.offsetY;
268
+ px = Math.max(0, Math.min(px, rect.width - this.colsToPixelWidth(widget.cols)));
269
+ py = Math.max(0, py);
270
+ this.dragging.currentPixelX = px;
271
+ this.dragging.currentPixelY = py;
272
+ const targetCol = Math.max(0, Math.min(this.pixelToCol(px), this.columns - widget.cols));
273
+ const targetRow = Math.max(0, this.pixelToRow(py));
274
+ 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);
275
+ this.placeholder = {
276
+ left: this.colToPixel(targetCol), top: this.rowToPixel(targetRow),
277
+ width: this.colsToPixelWidth(widget.cols),
278
+ height: this.rowsToPixelHeight(widget.rows),
279
+ };
280
+ this.zone.run(() => {
281
+ for (const rw of resolved) {
282
+ if (rw.id === widget.id)
283
+ continue;
284
+ const w = this.widgets.find(ww => ww.id === rw.id);
285
+ if (w) {
286
+ w.x = rw.x;
287
+ w.y = rw.y;
288
+ }
289
+ }
290
+ this.updateContainerHeight();
291
+ this.cdr.markForCheck();
292
+ });
293
+ }
294
+ finalizeDrag() {
295
+ if (!this.dragging)
296
+ return;
297
+ const widget = this.widgets.find(w => w.id === this.dragging.id);
298
+ const targetCol = Math.max(0, Math.min(this.pixelToCol(this.dragging.currentPixelX), this.columns - widget.cols));
299
+ const targetRow = Math.max(0, this.pixelToRow(this.dragging.currentPixelY));
300
+ 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);
301
+ for (const rw of resolved) {
302
+ const w = this.widgets.find(ww => ww.id === rw.id);
303
+ if (w) {
304
+ w.x = rw.x;
305
+ w.y = rw.y;
306
+ w.cols = rw.cols;
307
+ w.rows = rw.rows;
308
+ this.itemChanged.emit(w);
309
+ }
310
+ }
311
+ this.dragging = null;
312
+ this.placeholder = null;
313
+ this.previewWidgets = [];
314
+ this.updateContainerHeight();
315
+ this.layoutChanged.emit(this.widgets);
316
+ this.cdr.markForCheck();
317
+ }
318
+ handleResizeMove(event) {
319
+ if (!this.resizing)
320
+ return;
321
+ const widget = this.widgets.find(w => w.id === this.resizing.id);
322
+ if (!widget)
323
+ return;
324
+ const dx = event.clientX - this.resizing.startMouseX;
325
+ const dy = event.clientY - this.resizing.startMouseY;
326
+ let newCols = Math.max(this.minItemCols, Math.round((this.colsToPixelWidth(this.resizing.startCols) + dx) / (this.cellWidth + this.gap)));
327
+ let newRows = Math.max(this.minItemRows, Math.round((this.rowsToPixelHeight(this.resizing.startRows) + dy) / (this.rowHeight + this.gap)));
328
+ newCols = Math.min(newCols, this.columns - widget.x);
329
+ this.resizing.currentPixelW = this.colsToPixelWidth(newCols);
330
+ this.resizing.currentPixelH = this.rowsToPixelHeight(newRows);
331
+ 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);
332
+ this.placeholder = {
333
+ left: this.colToPixel(widget.x), top: this.rowToPixel(widget.y),
334
+ width: this.colsToPixelWidth(newCols),
335
+ height: this.rowsToPixelHeight(newRows),
336
+ };
337
+ this.zone.run(() => {
338
+ for (const rw of resolved) {
339
+ if (rw.id === widget.id)
340
+ continue;
341
+ const w = this.widgets.find(ww => ww.id === rw.id);
342
+ if (w) {
343
+ w.x = rw.x;
344
+ w.y = rw.y;
345
+ }
346
+ }
347
+ this.updateContainerHeight();
348
+ this.cdr.markForCheck();
349
+ });
350
+ }
351
+ finalizeResize() {
352
+ if (!this.resizing)
353
+ return;
354
+ const widget = this.widgets.find(w => w.id === this.resizing.id);
355
+ let newCols = Math.max(this.minItemCols, Math.round(this.resizing.currentPixelW / (this.cellWidth + this.gap)));
356
+ let newRows = Math.max(this.minItemRows, Math.round(this.resizing.currentPixelH / (this.rowHeight + this.gap)));
357
+ newCols = Math.min(newCols, this.columns - widget.x);
358
+ 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);
359
+ for (const rw of resolved) {
360
+ const w = this.widgets.find(ww => ww.id === rw.id);
361
+ if (w) {
362
+ w.x = rw.x;
363
+ w.y = rw.y;
364
+ w.cols = rw.cols;
365
+ w.rows = rw.rows;
366
+ this.itemChanged.emit(w);
367
+ }
368
+ }
369
+ this.resizing = null;
370
+ this.placeholder = null;
371
+ this.previewWidgets = [];
372
+ this.updateContainerHeight();
373
+ this.layoutChanged.emit(this.widgets);
374
+ this.cdr.markForCheck();
375
+ }
376
+ // ── Utilities ──
377
+ compactAndApply() {
378
+ var _a;
379
+ if (!((_a = this.widgets) === null || _a === void 0 ? void 0 : _a.length))
380
+ return;
381
+ 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);
382
+ for (const rw of compacted) {
383
+ const w = this.widgets.find(ww => ww.id === rw.id);
384
+ if (w) {
385
+ w.x = rw.x;
386
+ w.y = rw.y;
387
+ }
388
+ }
389
+ }
390
+ updateContainerHeight() {
391
+ var _a;
392
+ if (!((_a = this.widgets) === null || _a === void 0 ? void 0 : _a.length)) {
393
+ this.containerHeight = 400;
394
+ return;
395
+ }
396
+ const maxRow = GridEngine.getGridHeight(this.widgets.map(w => ({ id: w.id, x: w.x, y: w.y, cols: w.cols, rows: w.rows })));
397
+ this.containerHeight = this.rowToPixel(maxRow) + this.rowHeight + this.gap;
398
+ }
399
+ }
400
+ 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 });
401
+ 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: `
402
+ <div
403
+ #gridContainer
404
+ class="grid-container"
405
+ [style.height.px]="containerHeight"
406
+ >
407
+ <!-- Placeholder preview -->
408
+ <div
409
+ class="grid-placeholder"
410
+ *ngIf="placeholder"
411
+ [style.left.px]="placeholder.left"
412
+ [style.top.px]="placeholder.top"
413
+ [style.width.px]="placeholder.width"
414
+ [style.height.px]="placeholder.height"
415
+ ></div>
416
+
417
+ <!-- Widget items -->
418
+ <div
419
+ *ngFor="let widget of widgets; trackBy: trackByFn"
420
+ class="grid-item"
421
+ [class.is-dragging]="dragging?.id === widget.id"
422
+ [class.is-resizing]="resizing?.id === widget.id"
423
+ [style.left.px]="getItemLeft(widget)"
424
+ [style.top.px]="getItemTop(widget)"
425
+ [style.width.px]="getItemWidth(widget)"
426
+ [style.height.px]="getItemHeight(widget)"
427
+ [style.z-index]="(dragging?.id === widget.id || resizing?.id === widget.id) ? 100 : 1"
428
+ (mousedown)="onDragStart($event, widget)"
429
+ >
430
+ <!-- Content stamped from parent template, context carries the widget -->
431
+ <ng-container
432
+ *ngIf="cellTemplate"
433
+ [ngTemplateOutlet]="cellTemplate"
434
+ [ngTemplateOutletContext]="{ widget: widget }"
435
+ ></ng-container>
436
+
437
+ <!-- Resize handle -->
438
+ <div class="resize-handle" (mousedown)="onResizeStart($event, widget)">
439
+ <svg width="12" height="12" viewBox="0 0 12 12">
440
+ <path d="M11 1v10H1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
441
+ <path d="M11 5v6H5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
442
+ <path d="M11 9v2H9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
443
+ </svg>
444
+ </div>
445
+ </div>
446
+ </div>
447
+ `, 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 });
448
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: CustomGridComponent, decorators: [{
449
+ type: Component,
450
+ args: [{ selector: 'app-grid', template: `
451
+ <div
452
+ #gridContainer
453
+ class="grid-container"
454
+ [style.height.px]="containerHeight"
455
+ >
456
+ <!-- Placeholder preview -->
457
+ <div
458
+ class="grid-placeholder"
459
+ *ngIf="placeholder"
460
+ [style.left.px]="placeholder.left"
461
+ [style.top.px]="placeholder.top"
462
+ [style.width.px]="placeholder.width"
463
+ [style.height.px]="placeholder.height"
464
+ ></div>
465
+
466
+ <!-- Widget items -->
467
+ <div
468
+ *ngFor="let widget of widgets; trackBy: trackByFn"
469
+ class="grid-item"
470
+ [class.is-dragging]="dragging?.id === widget.id"
471
+ [class.is-resizing]="resizing?.id === widget.id"
472
+ [style.left.px]="getItemLeft(widget)"
473
+ [style.top.px]="getItemTop(widget)"
474
+ [style.width.px]="getItemWidth(widget)"
475
+ [style.height.px]="getItemHeight(widget)"
476
+ [style.z-index]="(dragging?.id === widget.id || resizing?.id === widget.id) ? 100 : 1"
477
+ (mousedown)="onDragStart($event, widget)"
478
+ >
479
+ <!-- Content stamped from parent template, context carries the widget -->
480
+ <ng-container
481
+ *ngIf="cellTemplate"
482
+ [ngTemplateOutlet]="cellTemplate"
483
+ [ngTemplateOutletContext]="{ widget: widget }"
484
+ ></ng-container>
485
+
486
+ <!-- Resize handle -->
487
+ <div class="resize-handle" (mousedown)="onResizeStart($event, widget)">
488
+ <svg width="12" height="12" viewBox="0 0 12 12">
489
+ <path d="M11 1v10H1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
490
+ <path d="M11 5v6H5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
491
+ <path d="M11 9v2H9" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
492
+ </svg>
493
+ </div>
494
+ </div>
495
+ </div>
496
+ `, 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"] }]
497
+ }], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { widgets: [{
498
+ type: Input
499
+ }], columns: [{
500
+ type: Input
501
+ }], gap: [{
502
+ type: Input
503
+ }], rowHeight: [{
504
+ type: Input
505
+ }], minItemCols: [{
506
+ type: Input
507
+ }], minItemRows: [{
508
+ type: Input
509
+ }], itemChanged: [{
510
+ type: Output
511
+ }], layoutChanged: [{
512
+ type: Output
513
+ }], gridContainer: [{
514
+ type: ViewChild,
515
+ args: ['gridContainer', { static: true }]
516
+ }], cellTemplate: [{
517
+ type: ContentChild,
518
+ args: [GridCellDirective, { read: TemplateRef }]
519
+ }] } });
13
520
 
14
521
  class DashboardStateService {
15
- constructor() {
16
- this.STORAGE_KEY = 'xtb_dashboard_layout';
522
+ constructor(zone) {
523
+ this.zone = zone;
524
+ /**
525
+ * Shared key — stores page list + widget metadata (no positions).
526
+ * Read and written by every window.
527
+ */
528
+ this.SHARED_KEY = 'ogidor_shared';
529
+ this.CHANNEL_NAME = 'ogidor_dashboard_sync';
530
+ this.channel = typeof BroadcastChannel !== 'undefined'
531
+ ? new BroadcastChannel(this.CHANNEL_NAME) : null;
17
532
  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
- }
533
+ { id: 'page-1', name: 'Default Workspace', widgets: [] }
44
534
  ];
45
535
  this.pagesSubject = new BehaviorSubject(this.initialPages);
46
536
  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
537
  this.pages$ = this.pagesSubject.asObservable();
54
538
  this.activePageId$ = this.activePageIdSubject.asObservable();
55
- const saved = localStorage.getItem(this.STORAGE_KEY);
56
- if (saved) {
539
+ // Determine whether we are a pop-out window
540
+ const hash = typeof window !== 'undefined'
541
+ ? window.location.hash.replace('#', '').trim() : '';
542
+ this.positionsKey = hash ? `ogidor_positions_${hash}` : 'ogidor_positions_main';
543
+ // 1. Load the shared structural state
544
+ const shared = localStorage.getItem(this.SHARED_KEY);
545
+ if (shared) {
57
546
  try {
58
- this.loadLayout(JSON.parse(saved));
547
+ this._applyShared(JSON.parse(shared), false);
59
548
  }
60
549
  catch (e) {
61
- console.error('Failed to load saved layout', e);
550
+ console.error('[Dashboard] bad shared state', e);
62
551
  }
63
552
  }
553
+ // 2. Overlay this window's saved positions on top
554
+ this._restorePositions();
555
+ // 3. Listen for structural changes broadcast by other windows
556
+ if (this.channel) {
557
+ this.channel.onmessage = (ev) => {
558
+ this.zone.run(() => this._onSyncEvent(ev.data));
559
+ };
560
+ }
64
561
  }
562
+ ngOnDestroy() { var _a; (_a = this.channel) === null || _a === void 0 ? void 0 : _a.close(); }
563
+ // ── Page actions ──────────────────────────────────────────────
65
564
  getActivePage() {
66
565
  return this.pagesSubject.value.find(p => p.id === this.activePageIdSubject.value);
67
566
  }
68
- setActivePage(id) {
69
- this.activePageIdSubject.next(id);
70
- }
567
+ setActivePage(id) { this.activePageIdSubject.next(id); }
71
568
  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();
569
+ const page = { id: `page-${Date.now()}`, name, widgets: [] };
570
+ this.pagesSubject.next([...this.pagesSubject.value, page]);
571
+ this.activePageIdSubject.next(page.id);
572
+ this._saveShared();
573
+ this._broadcast({ type: 'PAGE_ADDED', page });
81
574
  }
82
575
  removePage(id) {
83
576
  const pages = this.pagesSubject.value;
84
577
  if (pages.length <= 1)
85
578
  return;
86
- const updatedPages = pages.filter(p => p.id !== id);
87
- this.pagesSubject.next(updatedPages);
579
+ const updated = pages.filter(p => p.id !== id);
580
+ this.pagesSubject.next(updated);
88
581
  if (this.activePageIdSubject.value === id) {
89
- this.activePageIdSubject.next(updatedPages[0].id);
582
+ this.activePageIdSubject.next(updated[0].id);
90
583
  }
91
- this.saveToLocalStorage();
584
+ this._saveShared();
585
+ this._broadcast({ type: 'PAGE_REMOVED', pageId: id });
92
586
  }
93
- addWidget(type) {
587
+ // ── Widget structural actions (synced to all windows) ─────────
588
+ addWidget(widget) {
94
589
  const activePage = this.getActivePage();
95
590
  if (!activePage)
96
- return;
97
- const newWidget = {
98
- 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)
105
- };
106
- activePage.widgets.push(newWidget);
107
- this.updatePages(this.pagesSubject.value);
591
+ throw new Error('No active page');
592
+ const newWidget = Object.assign({ id: `widget-${Date.now()}`, x: 0, y: 0, cols: 4, rows: 3 }, widget);
593
+ activePage.widgets = [...activePage.widgets, newWidget];
594
+ this.pagesSubject.next([...this.pagesSubject.value]);
595
+ this._saveShared();
596
+ this._savePositions(); // register default position locally
597
+ this._broadcast({ type: 'WIDGET_ADDED', pageId: activePage.id, widget: newWidget });
598
+ return newWidget;
599
+ }
600
+ updateWidget(widgetId, patch) {
601
+ const pages = this.pagesSubject.value;
602
+ for (const page of pages) {
603
+ const w = page.widgets.find(w => w.id === widgetId);
604
+ if (w) {
605
+ Object.assign(w, patch);
606
+ this.pagesSubject.next([...pages]);
607
+ this._saveShared();
608
+ this._broadcast({ type: 'WIDGET_META', widgetId, patch });
609
+ return;
610
+ }
611
+ }
108
612
  }
109
- addWidgetWithData(type, title, data) {
613
+ removeWidget(widgetId) {
110
614
  const activePage = this.getActivePage();
111
615
  if (!activePage)
112
616
  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
- };
617
+ const pageId = activePage.id;
618
+ activePage.widgets = activePage.widgets.filter(w => w.id !== widgetId);
619
+ this.pagesSubject.next([...this.pagesSubject.value]);
620
+ this._saveShared();
621
+ this._savePositions();
622
+ this._broadcast({ type: 'WIDGET_REMOVED', pageId, widgetId });
135
623
  }
624
+ // ── Position actions (local to THIS window only) ──────────────
625
+ /**
626
+ * Update a widget's grid position/size.
627
+ * Saved only for this window — never broadcast to others.
628
+ */
136
629
  updateWidgetPosition(pageId, widgetId, x, y, cols, rows) {
630
+ var _a;
137
631
  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
- }
632
+ const widget = (_a = pages.find(p => p.id === pageId)) === null || _a === void 0 ? void 0 : _a.widgets.find(w => w.id === widgetId);
633
+ if (widget) {
634
+ widget.x = x;
635
+ widget.y = y;
636
+ widget.cols = cols;
637
+ widget.rows = rows;
638
+ this.pagesSubject.next([...pages]);
639
+ this._savePositions(); // local only — intentionally no _saveShared / no broadcast
148
640
  }
149
641
  }
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
- }
642
+ // ── Serialization ─────────────────────────────────────────────
643
+ serializeLayout() {
644
+ return JSON.stringify({
645
+ pages: this.pagesSubject.value,
646
+ activePageId: this.activePageIdSubject.value,
647
+ });
648
+ }
649
+ loadLayout(config) {
650
+ if (!(config === null || config === void 0 ? void 0 : config.pages))
651
+ return;
652
+ this._applyShared(config, true);
653
+ this._saveShared();
654
+ this._savePositions();
655
+ }
656
+ popOutPage(pageId) {
657
+ const url = `${window.location.origin}${window.location.pathname}#${pageId}`;
658
+ window.open(url, `workspace_${pageId}`, 'width=1280,height=800,menubar=no,toolbar=no,location=no,status=no');
156
659
  }
660
+ // ── Private helpers ───────────────────────────────────────────
157
661
  /**
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.
662
+ * Save page list + widget metadata (titles, cardColor, data) — no positions.
163
663
  */
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;
664
+ _saveShared() {
665
+ const stripped = this.pagesSubject.value.map(p => (Object.assign(Object.assign({}, p), { widgets: p.widgets.map(({ id, title, cardColor, data, cols, rows }) =>
666
+ // Keep default cols/rows so new windows get a sensible first-open layout.
667
+ // x/y are intentionally omitted from shared state.
668
+ ({ id, title, cardColor, data, x: 0, y: 0, cols, rows })) })));
669
+ localStorage.setItem(this.SHARED_KEY, JSON.stringify({
670
+ pages: stripped,
671
+ activePageId: this.activePageIdSubject.value,
672
+ }));
673
+ }
674
+ /**
675
+ * Save this window's grid positions (x, y, cols, rows) per widget.
676
+ */
677
+ _savePositions() {
678
+ const positions = {};
679
+ for (const page of this.pagesSubject.value) {
680
+ for (const w of page.widgets) {
681
+ positions[`${page.id}:${w.id}`] = { x: w.x, y: w.y, cols: w.cols, rows: w.rows };
173
682
  }
174
683
  }
175
- console.warn(`[Dashboard] updateWidgetData: widget "${widgetId}" not found.`);
684
+ localStorage.setItem(this.positionsKey, JSON.stringify(positions));
176
685
  }
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 });
686
+ /**
687
+ * Overlay the positions saved for THIS window on top of the current pages.
688
+ */
689
+ _restorePositions() {
690
+ const raw = localStorage.getItem(this.positionsKey);
691
+ if (!raw)
692
+ return;
693
+ try {
694
+ const positions = JSON.parse(raw);
695
+ const pages = this.pagesSubject.value;
696
+ for (const page of pages) {
697
+ for (const w of page.widgets) {
698
+ const pos = positions[`${page.id}:${w.id}`];
699
+ if (pos) {
700
+ w.x = pos.x;
701
+ w.y = pos.y;
702
+ w.cols = pos.cols;
703
+ w.rows = pos.rows;
704
+ }
191
705
  }
192
- this.updatePages(pages);
193
- return;
194
706
  }
707
+ this.pagesSubject.next([...pages]);
708
+ }
709
+ catch (e) {
710
+ console.error('[Dashboard] bad positions state', e);
195
711
  }
196
712
  }
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);
713
+ /**
714
+ * Apply a shared config object (page list + metadata) without touching
715
+ * this window's saved positions.
716
+ */
717
+ _applyShared(config, overwritePositions) {
718
+ if (!(config === null || config === void 0 ? void 0 : config.pages))
719
+ return;
720
+ this.pagesSubject.next(config.pages);
721
+ if (config.activePageId)
722
+ this.activePageIdSubject.next(config.activePageId);
723
+ if (!overwritePositions)
724
+ this._restorePositions();
207
725
  }
208
- loadLayout(config) {
209
- if (config && config.pages) {
210
- this.pagesSubject.next(config.pages);
211
- if (config.activePageId) {
212
- this.activePageIdSubject.next(config.activePageId);
726
+ /**
727
+ * Handle a structural sync event arriving from another window.
728
+ * Position changes are never sent so we never receive them here.
729
+ */
730
+ _onSyncEvent(event) {
731
+ var _a, _b;
732
+ // Work on a shallow clone of pages so Angular detects the change
733
+ const pages = this.pagesSubject.value.map(p => (Object.assign(Object.assign({}, p), { widgets: [...p.widgets] })));
734
+ switch (event.type) {
735
+ case 'PAGE_ADDED':
736
+ if (!pages.find(p => p.id === event.page.id)) {
737
+ pages.push(Object.assign(Object.assign({}, 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((_b = (_a = updated[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : '');
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, Object.assign({}, 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
+ var _a;
790
+ (_a = this.channel) === null || _a === void 0 ? void 0 : _a.postMessage(event);
218
791
  }
219
792
  }
220
- DashboardStateService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
793
+ 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
794
  DashboardStateService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, providedIn: 'root' });
222
795
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, decorators: [{
223
796
  type: Injectable,
224
- args: [{
225
- providedIn: 'root'
226
- }]
227
- }], ctorParameters: function () { return []; } });
797
+ args: [{ providedIn: 'root' }]
798
+ }], ctorParameters: function () { return [{ type: i0.NgZone }]; } });
228
799
 
229
- const DEFAULT_CHART_COLORS = ['#0a84ff', '#30d158', '#ff9f0a', '#bf5af2', '#ff453a'];
230
- const DEFAULT_FORECOLOR = '#8e8e93';
231
- const DEFAULT_CARD_BG = '#2c2c2e';
232
800
  class WidgetRendererComponent {
233
- constructor(stateService) {
234
- this.stateService = stateService;
801
+ constructor() {
235
802
  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 = Object.assign(Object.assign({}, this.widget), { data });
245
- this.initChart();
246
- }
247
- }));
803
+ this.removeRequested = new EventEmitter();
804
+ this.cardBg = 'var(--dash-card-bg, #2c2c2e)';
248
805
  }
249
806
  ngOnChanges(changes) {
250
- if (changes['theme']) {
251
- this.applyStyles();
252
- this.initChart();
253
- }
254
- if (changes['widget'] && !changes['widget'].firstChange) {
255
- this.applyStyles();
256
- this.initChart();
807
+ var _a, _b;
808
+ if (changes['widget'] || changes['theme']) {
809
+ this.cardBg = (_b = (_a = this.widget) === null || _a === void 0 ? void 0 : _a.cardColor) !== null && _b !== void 0 ? _b : 'var(--dash-card-bg, #2c2c2e)';
257
810
  }
258
811
  }
259
- ngOnDestroy() { this.subs.unsubscribe(); }
260
- applyStyles() {
261
- var _a;
262
- this.cardStyles = {
263
- background: (_a = this.widget.cardColor) !== null && _a !== void 0 ? _a : 'var(--dash-card-bg)'
264
- };
265
- }
266
- resolvedColors() {
267
- var _a, _b, _c;
268
- return (_c = (_a = this.widget.colors) !== null && _a !== void 0 ? _a : (_b = this.theme) === null || _b === void 0 ? void 0 : _b.chartColors) !== null && _c !== void 0 ? _c : DEFAULT_CHART_COLORS;
269
- }
270
- initChart() {
271
- var _a, _b, _c, _d;
272
- const isDonut = this.widget.type === 'DONUT';
273
- const colors = this.resolvedColors();
274
- const foreColor = (_b = (_a = this.theme) === null || _a === void 0 ? void 0 : _a.chartForeColor) !== null && _b !== void 0 ? _b : DEFAULT_FORECOLOR;
275
- const lineBarData = !isDonut ? this.widget.data : null;
276
- const donutData = isDonut ? this.widget.data : null;
277
- const categories = (_c = lineBarData === null || lineBarData === void 0 ? void 0 : lineBarData.categories) !== null && _c !== void 0 ? _c : [];
278
- const donutLabels = (_d = donutData === null || donutData === void 0 ? void 0 : donutData.labels) !== null && _d !== void 0 ? _d : ['A', 'B', 'C', 'D', 'E'];
279
- this.chartOptions = {
280
- series: this.widget.data.series,
281
- chart: {
282
- width: '100%', height: '100%',
283
- type: this.widget.type.toLowerCase(),
284
- toolbar: { show: false },
285
- animations: { enabled: true, speed: 400 },
286
- background: 'transparent', foreColor,
287
- parentHeightOffset: 0, sparkline: { enabled: false },
288
- },
289
- theme: { mode: 'dark' },
290
- colors,
291
- stroke: { curve: 'smooth', width: isDonut ? 0 : 3 },
292
- dataLabels: { enabled: false },
293
- legend: { position: 'bottom', labels: { colors: foreColor } },
294
- tooltip: { theme: 'dark' },
295
- plotOptions: {
296
- pie: { donut: { size: '70%', labels: { show: true, total: { show: true, label: 'Total', color: '#fff' } } } },
297
- bar: { borderRadius: 8, columnWidth: '50%' }
298
- },
299
- xaxis: {
300
- categories: categories.length ? categories : undefined,
301
- labels: { style: { colors: foreColor } },
302
- axisBorder: { show: false }, axisTicks: { show: false }
303
- },
304
- yaxis: { labels: { style: { colors: foreColor } } },
305
- labels: isDonut ? donutLabels : undefined,
306
- };
307
- }
308
- onRemove() { this.onRemoveWidget(this.widget.id); }
309
812
  }
310
- WidgetRendererComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: WidgetRendererComponent, deps: [{ token: DashboardStateService }], target: i0.ɵɵFactoryTarget.Component });
311
- 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: `
312
- <div class="widget-container" [ngStyle]="cardStyles">
813
+ WidgetRendererComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: WidgetRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
814
+ 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: `
815
+ <div class="widget-card" [style.background]="cardBg">
816
+ <!-- Header: drag handle + title + actions -->
313
817
  <div class="widget-header">
818
+ <div class="drag-handle" title="Drag to move">
819
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
820
+ <circle cx="4" cy="3" r="1.2"/><circle cx="10" cy="3" r="1.2"/>
821
+ <circle cx="4" cy="7" r="1.2"/><circle cx="10" cy="7" r="1.2"/>
822
+ <circle cx="4" cy="11" r="1.2"/><circle cx="10" cy="11" r="1.2"/>
823
+ </svg>
824
+ </div>
314
825
  <span class="widget-title">{{ widget.title }}</span>
315
826
  <div class="header-actions">
316
- <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit widget">
827
+ <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit">
317
828
  <i class="la la-pen"></i>
318
829
  </button>
319
- <button class="btn-icon btn-close-widget" (click)="onRemove()" title="Remove widget">
830
+ <button class="btn-icon btn-remove" (click)="removeRequested.emit(widget.id)" title="Remove">
320
831
  <i class="la la-times"></i>
321
832
  </button>
322
833
  </div>
323
834
  </div>
835
+
836
+ <!-- Consumer content slot -->
324
837
  <div class="widget-body">
325
- <apx-chart
326
- *ngIf="chartOptions.chart"
327
- style="display:block;width:100%;height:100%;"
328
- [series]="chartOptions.series!"
329
- [chart]="chartOptions.chart!"
330
- [xaxis]="chartOptions.xaxis!"
331
- [stroke]="chartOptions.stroke!"
332
- [dataLabels]="chartOptions.dataLabels!"
333
- [plotOptions]="chartOptions.plotOptions!"
334
- [yaxis]="chartOptions.yaxis!"
335
- [labels]="chartOptions.labels!"
336
- [legend]="chartOptions.legend!"
337
- [colors]="chartOptions.colors!"
338
- [theme]="chartOptions.theme!"
339
- [tooltip]="chartOptions.tooltip!"
340
- ></apx-chart>
838
+ <ng-content></ng-content>
341
839
  </div>
342
840
  </div>
343
- `, 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"] }] });
841
+ `, 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"] });
344
842
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: WidgetRendererComponent, decorators: [{
345
843
  type: Component,
346
844
  args: [{ selector: 'app-widget-renderer', template: `
347
- <div class="widget-container" [ngStyle]="cardStyles">
845
+ <div class="widget-card" [style.background]="cardBg">
846
+ <!-- Header: drag handle + title + actions -->
348
847
  <div class="widget-header">
848
+ <div class="drag-handle" title="Drag to move">
849
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
850
+ <circle cx="4" cy="3" r="1.2"/><circle cx="10" cy="3" r="1.2"/>
851
+ <circle cx="4" cy="7" r="1.2"/><circle cx="10" cy="7" r="1.2"/>
852
+ <circle cx="4" cy="11" r="1.2"/><circle cx="10" cy="11" r="1.2"/>
853
+ </svg>
854
+ </div>
349
855
  <span class="widget-title">{{ widget.title }}</span>
350
856
  <div class="header-actions">
351
- <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit widget">
857
+ <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit">
352
858
  <i class="la la-pen"></i>
353
859
  </button>
354
- <button class="btn-icon btn-close-widget" (click)="onRemove()" title="Remove widget">
860
+ <button class="btn-icon btn-remove" (click)="removeRequested.emit(widget.id)" title="Remove">
355
861
  <i class="la la-times"></i>
356
862
  </button>
357
863
  </div>
358
864
  </div>
865
+
866
+ <!-- Consumer content slot -->
359
867
  <div class="widget-body">
360
- <apx-chart
361
- *ngIf="chartOptions.chart"
362
- style="display:block;width:100%;height:100%;"
363
- [series]="chartOptions.series!"
364
- [chart]="chartOptions.chart!"
365
- [xaxis]="chartOptions.xaxis!"
366
- [stroke]="chartOptions.stroke!"
367
- [dataLabels]="chartOptions.dataLabels!"
368
- [plotOptions]="chartOptions.plotOptions!"
369
- [yaxis]="chartOptions.yaxis!"
370
- [labels]="chartOptions.labels!"
371
- [legend]="chartOptions.legend!"
372
- [colors]="chartOptions.colors!"
373
- [theme]="chartOptions.theme!"
374
- [tooltip]="chartOptions.tooltip!"
375
- ></apx-chart>
868
+ <ng-content></ng-content>
376
869
  </div>
377
870
  </div>
378
- `, 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"] }]
379
- }], ctorParameters: function () { return [{ type: DashboardStateService }]; }, propDecorators: { widget: [{
380
- type: Input
381
- }], onRemoveWidget: [{
871
+ `, 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"] }]
872
+ }], propDecorators: { widget: [{
382
873
  type: Input
383
874
  }], theme: [{
384
875
  type: Input
385
876
  }], editRequested: [{
386
877
  type: Output
878
+ }], removeRequested: [{
879
+ type: Output
387
880
  }] } });
388
881
 
389
882
  const DEFAULT_THEME = {
390
883
  backgroundColor: '#000000',
391
884
  panelColor: '#1c1c1e',
392
885
  widgetCardColor: '#2c2c2e',
393
- chartForeColor: '#8e8e93',
886
+ foreColor: '#8e8e93',
394
887
  accentColor: '#0a84ff',
395
888
  dangerColor: '#ff453a',
396
- chartColors: ['#0a84ff', '#30d158', '#ff9f0a', '#bf5af2', '#ff453a'],
397
889
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
398
890
  };
399
891
  class DashboardComponent {
400
892
  constructor(stateService) {
401
893
  this.stateService = stateService;
894
+ /**
895
+ * Emits when the user clicks "Add Widget".
896
+ * The consumer should open their own dialog and call `stateService.addWidget(...)`.
897
+ */
898
+ this.addWidgetRequested = new EventEmitter();
899
+ /**
900
+ * Emits the Widget when the user clicks the edit (pen) icon on a card.
901
+ * The consumer should open their own edit UI and call `stateService.updateWidget(...)`.
902
+ */
903
+ this.editWidgetRequested = new EventEmitter();
402
904
  this.resolvedTheme = Object.assign({}, DEFAULT_THEME);
403
905
  this.wrapperStyles = {};
404
906
  this.pages = [];
405
907
  this.activePageId = '';
406
- // ── Dialog state ──
407
- this.dialogOpen = false;
408
- this.dialogType = 'LINE';
409
- this.dialogTitle = '';
410
- this.dialogCategories = '';
411
- this.dialogSeries = [{ name: 'Series 1', dataRaw: '' }];
412
- this.dialogSlices = [
413
- { label: 'Slice A', value: null },
414
- { label: 'Slice B', value: null },
415
- { label: 'Slice C', value: null },
416
- ];
417
- this.widgetTypes = [
418
- { value: 'LINE', label: 'Line', icon: 'la-chart-area' },
419
- { value: 'BAR', label: 'Bar', icon: 'la-chart-bar' },
420
- { value: 'DONUT', label: 'Donut', icon: 'la-chart-pie' },
421
- ];
908
+ this.isPoppedOut = false;
422
909
  this.subs = new Subscription();
423
- // ── Edit dialog state ──
424
- this.editDialogOpen = false;
425
- this.editWidgetType = 'LINE';
426
- this.editTitle = '';
427
- this.editCardColor = '';
428
- this.editColors = [];
429
- this.editSeries = [];
430
- this.editCategories = '';
431
- this.editSlices = [];
432
- this.DEFAULT_COLORS = ['#0a84ff', '#30d158', '#ff9f0a', '#bf5af2', '#ff453a'];
433
- this.removeWidgetHandler = (id) => { this.stateService.removeWidget(id); };
434
910
  }
435
911
  ngOnChanges(changes) {
436
912
  if (changes['theme'] || changes['initialLayout'])
437
913
  this.applyTheme();
438
914
  }
439
915
  ngOnInit() {
916
+ var _a;
440
917
  this.applyTheme();
441
918
  if (this.initialLayout) {
442
919
  try {
@@ -446,21 +923,16 @@ class DashboardComponent {
446
923
  console.error('[Dashboard] Failed to parse initialLayout', e);
447
924
  }
448
925
  }
449
- this.options = {
450
- gridType: 'fit',
451
- displayGrid: 'none',
452
- pushItems: true,
453
- draggable: { enabled: true },
454
- resizable: { enabled: true },
455
- minCols: 12, maxCols: 12,
456
- minRows: 12, maxRows: 50,
457
- margin: 20, outerMargin: true,
458
- itemChangeCallback: (item) => {
459
- this.stateService.updateWidgetPosition(this.activePageId, item.id, item.x, item.y, item.cols, item.rows);
460
- }
461
- };
926
+ const hash = (_a = window.location.hash) === null || _a === void 0 ? void 0 : _a.replace('#', '').trim();
927
+ if (hash)
928
+ this.isPoppedOut = true;
462
929
  this.subs.add(this.stateService.pages$.subscribe(pages => {
463
930
  this.pages = pages;
931
+ if (this.isPoppedOut && hash) {
932
+ const target = pages.find(p => p.id === hash);
933
+ if (target)
934
+ this.stateService.setActivePage(hash);
935
+ }
464
936
  this.updateActivePage();
465
937
  }));
466
938
  this.subs.add(this.stateService.activePageId$.subscribe(id => {
@@ -469,6 +941,26 @@ class DashboardComponent {
469
941
  }));
470
942
  }
471
943
  ngOnDestroy() { this.subs.unsubscribe(); }
944
+ onItemChanged(widget) {
945
+ this.stateService.updateWidgetPosition(this.activePageId, widget.id, widget.x, widget.y, widget.cols, widget.rows);
946
+ }
947
+ onRemoveWidget(widgetId) { this.stateService.removeWidget(widgetId); }
948
+ onSelectPage(id) { this.stateService.setActivePage(id); }
949
+ onAddPage() {
950
+ const name = prompt('Workspace name:', `Workspace ${this.pages.length + 1}`);
951
+ if (name)
952
+ this.stateService.addPage(name);
953
+ }
954
+ onRemovePage(event, id) {
955
+ event.stopPropagation();
956
+ if (confirm('Remove this workspace?'))
957
+ this.stateService.removePage(id);
958
+ }
959
+ onPopOut(event, pageId) {
960
+ event.stopPropagation();
961
+ this.stateService.popOutPage(pageId);
962
+ }
963
+ serializeLayout() { return this.stateService.serializeLayout(); }
472
964
  applyTheme() {
473
965
  var _a;
474
966
  this.resolvedTheme = Object.assign(Object.assign({}, DEFAULT_THEME), ((_a = this.theme) !== null && _a !== void 0 ? _a : {}));
@@ -476,7 +968,7 @@ class DashboardComponent {
476
968
  '--dash-bg': this.resolvedTheme.backgroundColor,
477
969
  '--dash-panel-bg': this.resolvedTheme.panelColor,
478
970
  '--dash-card-bg': this.resolvedTheme.widgetCardColor,
479
- '--dash-fore-color': this.resolvedTheme.chartForeColor,
971
+ '--dash-fore-color': this.resolvedTheme.foreColor,
480
972
  '--dash-accent-color': this.resolvedTheme.accentColor,
481
973
  '--dash-danger-color': this.resolvedTheme.dangerColor,
482
974
  '--dash-font-family': this.resolvedTheme.fontFamily,
@@ -485,670 +977,257 @@ class DashboardComponent {
485
977
  updateActivePage() {
486
978
  this.activePage = this.pages.find(p => p.id === this.activePageId);
487
979
  }
488
- // ── Page actions ──
489
- onSelectPage(id) { this.stateService.setActivePage(id); }
490
- onAddPage() {
491
- const name = prompt('Enter workspace name:', `Workspace ${this.pages.length + 1}`);
492
- if (name)
493
- this.stateService.addPage(name);
494
- }
495
- onRemovePage(event, id) {
496
- event.stopPropagation();
497
- if (confirm('Remove this workspace?'))
498
- this.stateService.removePage(id);
499
- }
500
- // ── Dialog ──
501
- openDialog() {
502
- this.dialogType = 'LINE';
503
- this.dialogTitle = '';
504
- this.dialogCategories = '';
505
- this.dialogSeries = [{ name: 'Series 1', dataRaw: '' }];
506
- this.dialogSlices = [
507
- { label: 'Slice A', value: null },
508
- { label: 'Slice B', value: null },
509
- { label: 'Slice C', value: null },
510
- ];
511
- this.dialogOpen = true;
512
- }
513
- closeDialog() { this.dialogOpen = false; }
514
- addSeries() { this.dialogSeries.push({ name: `Series ${this.dialogSeries.length + 1}`, dataRaw: '' }); }
515
- removeSeries(i) { this.dialogSeries.splice(i, 1); }
516
- addSlice() { this.dialogSlices.push({ label: `Slice ${this.dialogSlices.length + 1}`, value: null }); }
517
- removeSlice(i) { this.dialogSlices.splice(i, 1); }
518
- confirmAddWidget() {
519
- const title = this.dialogTitle.trim();
520
- if (!title)
521
- return;
522
- let data;
523
- if (this.dialogType === 'DONUT') {
524
- const validSlices = this.dialogSlices.filter(s => s.value !== null && s.label.trim());
525
- data = {
526
- series: validSlices.length
527
- ? validSlices.map(s => Number(s.value))
528
- : [30, 20, 50],
529
- labels: validSlices.length
530
- ? validSlices.map(s => s.label)
531
- : ['A', 'B', 'C'],
532
- };
533
- }
534
- else {
535
- const parseNums = (raw) => raw.split(',').map(v => parseFloat(v.trim())).filter(n => !isNaN(n));
536
- data = {
537
- series: this.dialogSeries.map(s => ({
538
- name: s.name || 'Series',
539
- data: s.dataRaw.trim()
540
- ? parseNums(s.dataRaw)
541
- : Array.from({ length: 8 }, () => Math.floor(Math.random() * 100)),
542
- })),
543
- categories: this.dialogCategories.trim()
544
- ? this.dialogCategories.split(',').map(c => c.trim())
545
- : undefined,
546
- };
547
- }
548
- this.stateService.addWidgetWithData(this.dialogType, title, data);
549
- this.closeDialog();
550
- }
551
- openEditDialog(widget) {
552
- var _a, _b, _c, _d;
553
- this.editingWidget = widget;
554
- this.editWidgetType = widget.type;
555
- this.editTitle = widget.title;
556
- this.editCardColor = (_a = widget.cardColor) !== null && _a !== void 0 ? _a : this.resolvedTheme.widgetCardColor;
557
- this.editColors = ((_b = widget.colors) === null || _b === void 0 ? void 0 : _b.length)
558
- ? [...widget.colors]
559
- : [...this.resolvedTheme.chartColors];
560
- if (widget.type === 'DONUT') {
561
- const d = widget.data;
562
- this.editSlices = d.series.map((v, i) => {
563
- var _a, _b;
564
- return ({
565
- label: (_b = (_a = d.labels) === null || _a === void 0 ? void 0 : _a[i]) !== null && _b !== void 0 ? _b : `Slice ${i + 1}`,
566
- value: v,
567
- });
568
- });
569
- this.editColors = this.editColors.slice(0, this.editSlices.length);
570
- while (this.editColors.length < this.editSlices.length) {
571
- this.editColors.push(this.DEFAULT_COLORS[this.editColors.length % this.DEFAULT_COLORS.length]);
572
- }
573
- }
574
- else {
575
- const d = widget.data;
576
- this.editSeries = d.series.map((s) => ({ name: s.name, dataRaw: s.data.join(',') }));
577
- this.editCategories = (_d = (_c = d.categories) === null || _c === void 0 ? void 0 : _c.join(',')) !== null && _d !== void 0 ? _d : '';
578
- this.editColors = this.editColors.slice(0, this.editSeries.length);
579
- while (this.editColors.length < this.editSeries.length) {
580
- this.editColors.push(this.DEFAULT_COLORS[this.editColors.length % this.DEFAULT_COLORS.length]);
581
- }
582
- }
583
- this.editDialogOpen = true;
584
- }
585
- closeEditDialog() { this.editDialogOpen = false; this.editingWidget = undefined; }
586
- getEditSeriesLabel(i) {
587
- var _a, _b;
588
- if (this.editWidgetType === 'DONUT')
589
- return ((_a = this.editSlices[i]) === null || _a === void 0 ? void 0 : _a.label) || `Slice ${i + 1}`;
590
- return ((_b = this.editSeries[i]) === null || _b === void 0 ? void 0 : _b.name) || `Series ${i + 1}`;
591
- }
592
- addEditSeries() {
593
- this.editSeries.push({ name: `Series ${this.editSeries.length + 1}`, dataRaw: '' });
594
- this.editColors.push(this.DEFAULT_COLORS[this.editColors.length % this.DEFAULT_COLORS.length]);
595
- }
596
- removeEditSeries(i) { this.editSeries.splice(i, 1); this.editColors.splice(i, 1); }
597
- addEditSlice() {
598
- this.editSlices.push({ label: `Slice ${this.editSlices.length + 1}`, value: null });
599
- this.editColors.push(this.DEFAULT_COLORS[this.editColors.length % this.DEFAULT_COLORS.length]);
600
- }
601
- removeEditSlice(i) { this.editSlices.splice(i, 1); this.editColors.splice(i, 1); }
602
- confirmEditWidget() {
603
- if (!this.editingWidget)
604
- return;
605
- const parseNums = (raw) => raw.split(',').map(v => parseFloat(v.trim())).filter(n => !isNaN(n));
606
- let data;
607
- if (this.editWidgetType === 'DONUT') {
608
- data = {
609
- series: this.editSlices.map(s => Number(s.value) || 0),
610
- labels: this.editSlices.map(s => s.label),
611
- };
612
- }
613
- else {
614
- data = {
615
- series: this.editSeries.map(s => ({
616
- name: s.name || 'Series',
617
- data: s.dataRaw.trim() ? parseNums(s.dataRaw) : [],
618
- })),
619
- categories: this.editCategories.trim()
620
- ? this.editCategories.split(',').map(c => c.trim())
621
- : undefined,
622
- };
623
- }
624
- this.stateService.updateWidgetMeta(this.editingWidget.id, {
625
- title: this.editTitle.trim() || this.editingWidget.title,
626
- colors: [...this.editColors],
627
- cardColor: this.editCardColor,
628
- data,
629
- });
630
- this.closeEditDialog();
631
- }
632
- // ── Widget actions ──
633
- onAddWidget(type) { this.stateService.addWidget(type); }
634
- updateWidgetData(widgetId, data) {
635
- this.stateService.updateWidgetData(widgetId, data);
636
- }
637
- serializeLayout() { return this.stateService.serializeLayout(); }
638
980
  }
639
981
  DashboardComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardComponent, deps: [{ token: DashboardStateService }], target: i0.ɵɵFactoryTarget.Component });
640
- 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: `
641
- <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
642
- <main class="main-content">
643
-
644
- <!-- ── Header ── -->
645
- <header class="dashboard-header">
646
- <!-- Tabs -->
647
- <div class="tabs-container">
648
- <div
649
- *ngFor="let page of pages"
650
- class="tab"
651
- [class.active]="page.id === activePageId"
652
- (click)="onSelectPage(page.id)"
653
- >
654
- <span class="tab-name">{{ page.name }}</span>
655
- <button class="tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)">
656
- <i class="la la-times"></i>
657
- </button>
658
- </div>
659
- <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
660
- <i class="la la-plus"></i>
661
- </button>
662
- </div>
663
-
664
- <!-- Add Widget button -->
665
- <button class="btn-add-widget" (click)="openDialog()" title="Add widget">
666
- <i class="la la-plus"></i>
667
- <span>Add Widget</span>
668
- </button>
982
+ 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: `
983
+ <!-- ══════════════════════════════════════════════════════════
984
+ POPPED-OUT MODE
985
+ ══════════════════════════════════════════════════════════ -->
986
+ <ng-container *ngIf="isPoppedOut; else normalMode">
987
+ <div class="popout-wrapper" [ngStyle]="wrapperStyles">
988
+ <header class="popout-header">
989
+ <span class="popout-title">{{ activePage?.name }}</span>
669
990
  </header>
670
-
671
- <!-- ── Grid ── -->
672
991
  <div class="grid-container">
673
- <gridster [options]="options">
674
- <gridster-item [item]="widget" *ngFor="let widget of activePage?.widgets">
992
+ <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
993
+ <ng-template gridCell let-widget="widget">
675
994
  <app-widget-renderer
676
995
  [widget]="widget"
677
- [onRemoveWidget]="removeWidgetHandler"
678
996
  [theme]="resolvedTheme"
679
- (editRequested)="openEditDialog($event)"
680
- ></app-widget-renderer>
681
- </gridster-item>
682
- </gridster>
997
+ (editRequested)="editWidgetRequested.emit($event)"
998
+ (removeRequested)="onRemoveWidget($event)"
999
+ >
1000
+ <ng-container
1001
+ *ngIf="cellTemplate"
1002
+ [ngTemplateOutlet]="cellTemplate"
1003
+ [ngTemplateOutletContext]="{ widget: widget }">
1004
+ </ng-container>
1005
+ </app-widget-renderer>
1006
+ </ng-template>
1007
+ </app-grid>
683
1008
  </div>
684
- </main>
685
- </div>
686
-
687
- <!-- ── Add Widget Dialog ── -->
688
- <div class="dialog-backdrop" *ngIf="dialogOpen" (click)="closeDialog()"></div>
689
- <div class="dialog" *ngIf="dialogOpen">
690
- <div class="dialog-header">
691
- <span class="dialog-title">Add Widget</span>
692
- <button class="dialog-close" (click)="closeDialog()"><i class="la la-times"></i></button>
693
1009
  </div>
1010
+ </ng-container>
694
1011
 
695
- <div class="dialog-body">
1012
+ <!-- ══════════════════════════════════════════════════════════
1013
+ NORMAL MODE
1014
+ ══════════════════════════════════════════════════════════ -->
1015
+ <ng-template #normalMode>
1016
+ <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
1017
+ <main class="main-content">
696
1018
 
697
- <!-- Chart type picker -->
698
- <div class="field-group">
699
- <label class="field-label">Chart type</label>
700
- <div class="type-picker">
701
- <button
702
- *ngFor="let t of widgetTypes"
703
- class="type-btn"
704
- [class.selected]="dialogType === t.value"
705
- (click)="dialogType = t.value"
706
- >
707
- <i [class]="'la ' + t.icon"></i>
708
- <span>{{ t.label }}</span>
709
- </button>
710
- </div>
711
- </div>
712
-
713
- <!-- Title -->
714
- <div class="field-group">
715
- <label class="field-label">Title</label>
716
- <input class="field-input" [(ngModel)]="dialogTitle" placeholder="My Chart" />
717
- </div>
718
-
719
- <!-- Series (LINE / BAR) -->
720
- <ng-container *ngIf="dialogType !== 'DONUT'">
721
- <div class="field-group">
722
- <div class="field-row-header">
723
- <label class="field-label">Series</label>
724
- <button class="btn-link" (click)="addSeries()"><i class="la la-plus"></i> Add series</button>
725
- </div>
726
- <div class="series-list">
727
- <div class="series-row" *ngFor="let s of dialogSeries; let i = index">
728
- <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Series name" />
729
- <input class="field-input series-data" [(ngModel)]="s.dataRaw"
730
- placeholder="Comma-separated values, e.g. 10,25,40" />
731
- <button class="btn-remove-series" *ngIf="dialogSeries.length > 1" (click)="removeSeries(i)">
732
- <i class="la la-times"></i>
1019
+ <header class="dashboard-header">
1020
+ <div class="tabs-container">
1021
+ <div
1022
+ *ngFor="let page of pages"
1023
+ class="tab"
1024
+ [class.active]="page.id === activePageId"
1025
+ (click)="onSelectPage(page.id)"
1026
+ >
1027
+ <span class="tab-name">{{ page.name }}</span>
1028
+ <button class="tab-action tab-popout" (click)="onPopOut($event, page.id)" title="Open in new window">
1029
+ <i class="la la-external-link-alt"></i>
733
1030
  </button>
734
- </div>
735
- </div>
736
- </div>
737
-
738
- <div class="field-group">
739
- <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
740
- <input class="field-input" [(ngModel)]="dialogCategories" placeholder="Jan,Feb,Mar,Apr" />
741
- </div>
742
- </ng-container>
743
-
744
- <!-- Slices (DONUT) -->
745
- <ng-container *ngIf="dialogType === 'DONUT'">
746
- <div class="field-group">
747
- <div class="field-row-header">
748
- <label class="field-label">Slices</label>
749
- <button class="btn-link" (click)="addSlice()"><i class="la la-plus"></i> Add slice</button>
750
- </div>
751
- <div class="series-list">
752
- <div class="series-row" *ngFor="let sl of dialogSlices; let i = index">
753
- <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
754
- <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
755
- <button class="btn-remove-series" *ngIf="dialogSlices.length > 1" (click)="removeSlice(i)">
1031
+ <button class="tab-action tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)" title="Close">
756
1032
  <i class="la la-times"></i>
757
1033
  </button>
758
1034
  </div>
1035
+ <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
1036
+ <i class="la la-plus"></i>
1037
+ </button>
759
1038
  </div>
760
- </div>
761
- </ng-container>
762
-
763
- </div>
764
-
765
- <div class="dialog-footer">
766
- <button class="btn-cancel" (click)="closeDialog()">Cancel</button>
767
- <button class="btn-confirm" (click)="confirmAddWidget()" [disabled]="!dialogTitle.trim()">
768
- <i class="la la-check"></i> Add Widget
769
- </button>
770
- </div>
771
- </div>
772
- <!-- ── Edit Widget Dialog ── -->
773
- <div class="dialog-backdrop" *ngIf="editDialogOpen" (click)="closeEditDialog()"></div>
774
- <div class="dialog" *ngIf="editDialogOpen">
775
- <div class="dialog-header">
776
- <span class="dialog-title">Edit Widget</span>
777
- <button class="dialog-close" (click)="closeEditDialog()"><i class="la la-times"></i></button>
778
- </div>
779
-
780
- <div class="dialog-body">
781
-
782
- <!-- Title -->
783
- <div class="field-group">
784
- <label class="field-label">Title</label>
785
- <input class="field-input" [(ngModel)]="editTitle" placeholder="Chart title" />
786
- </div>
787
-
788
- <!-- Card background -->
789
- <div class="field-group">
790
- <label class="field-label">Card color</label>
791
- <div class="color-row">
792
- <input type="color" class="color-swatch" [(ngModel)]="editCardColor" />
793
- <input class="field-input" [(ngModel)]="editCardColor" placeholder="#2c2c2e" />
794
- </div>
795
- </div>
796
-
797
- <!-- Series colors -->
798
- <div class="field-group">
799
- <label class="field-label">Series colors</label>
800
- <div class="color-list">
801
- <div class="color-item" *ngFor="let c of editColors; let i = index">
802
- <input type="color" class="color-swatch" [(ngModel)]="editColors[i]" />
803
- <input class="field-input color-hex" [(ngModel)]="editColors[i]" placeholder="#0a84ff" />
804
- <span class="color-label">{{ getEditSeriesLabel(i) }}</span>
805
- </div>
806
- </div>
807
- </div>
808
1039
 
809
- <!-- Data: LINE / BAR -->
810
- <ng-container *ngIf="editWidgetType !== 'DONUT'">
811
- <div class="field-group">
812
- <div class="field-row-header">
813
- <label class="field-label">Series data</label>
814
- <button class="btn-link" (click)="addEditSeries()"><i class="la la-plus"></i> Add</button>
815
- </div>
816
- <div class="series-list">
817
- <div class="series-row" *ngFor="let s of editSeries; let i = index">
818
- <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Name" />
819
- <input class="field-input series-data" [(ngModel)]="s.dataRaw" placeholder="10,20,30" />
820
- <button class="btn-remove-series" *ngIf="editSeries.length > 1" (click)="removeEditSeries(i)">
821
- <i class="la la-times"></i>
822
- </button>
823
- </div>
824
- </div>
825
- </div>
826
- <div class="field-group">
827
- <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
828
- <input class="field-input" [(ngModel)]="editCategories" placeholder="Jan,Feb,Mar" />
829
- </div>
830
- </ng-container>
1040
+ <!-- Add Widget button consumer decides what happens -->
1041
+ <button class="btn-add-widget" (click)="addWidgetRequested.emit()" title="Add widget">
1042
+ <i class="la la-plus"></i>
1043
+ <span>Add Widget</span>
1044
+ </button>
1045
+ </header>
831
1046
 
832
- <!-- Data: DONUT -->
833
- <ng-container *ngIf="editWidgetType === 'DONUT'">
834
- <div class="field-group">
835
- <div class="field-row-header">
836
- <label class="field-label">Slices</label>
837
- <button class="btn-link" (click)="addEditSlice()"><i class="la la-plus"></i> Add</button>
838
- </div>
839
- <div class="series-list">
840
- <div class="series-row" *ngFor="let sl of editSlices; let i = index">
841
- <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
842
- <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
843
- <button class="btn-remove-series" *ngIf="editSlices.length > 1" (click)="removeEditSlice(i)">
844
- <i class="la la-times"></i>
845
- </button>
846
- </div>
847
- </div>
1047
+ <div class="grid-container">
1048
+ <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
1049
+ <ng-template gridCell let-widget="widget">
1050
+ <app-widget-renderer
1051
+ [widget]="widget"
1052
+ [theme]="resolvedTheme"
1053
+ (editRequested)="editWidgetRequested.emit($event)"
1054
+ (removeRequested)="onRemoveWidget($event)"
1055
+ >
1056
+ <!-- Stamp consumer's cell template (if provided) inside the card body -->
1057
+ <ng-container
1058
+ *ngIf="cellTemplate"
1059
+ [ngTemplateOutlet]="cellTemplate"
1060
+ [ngTemplateOutletContext]="{ widget: widget }">
1061
+ </ng-container>
1062
+ </app-widget-renderer>
1063
+ </ng-template>
1064
+ </app-grid>
848
1065
  </div>
849
- </ng-container>
850
-
851
- </div>
852
1066
 
853
- <div class="dialog-footer">
854
- <button class="btn-cancel" (click)="closeEditDialog()">Cancel</button>
855
- <button class="btn-confirm" (click)="confirmEditWidget()">
856
- <i class="la la-check"></i> Save
857
- </button>
1067
+ </main>
858
1068
  </div>
859
- </div>
860
- `, 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"] }] });
1069
+ </ng-template>
1070
+ `, 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]" }] });
861
1071
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardComponent, decorators: [{
862
1072
  type: Component,
863
1073
  args: [{ selector: 'app-dashboard', template: `
864
- <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
865
- <main class="main-content">
866
-
867
- <!-- ── Header ── -->
868
- <header class="dashboard-header">
869
- <!-- Tabs -->
870
- <div class="tabs-container">
871
- <div
872
- *ngFor="let page of pages"
873
- class="tab"
874
- [class.active]="page.id === activePageId"
875
- (click)="onSelectPage(page.id)"
876
- >
877
- <span class="tab-name">{{ page.name }}</span>
878
- <button class="tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)">
879
- <i class="la la-times"></i>
880
- </button>
881
- </div>
882
- <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
883
- <i class="la la-plus"></i>
884
- </button>
885
- </div>
886
-
887
- <!-- Add Widget button -->
888
- <button class="btn-add-widget" (click)="openDialog()" title="Add widget">
889
- <i class="la la-plus"></i>
890
- <span>Add Widget</span>
891
- </button>
1074
+ <!-- ══════════════════════════════════════════════════════════
1075
+ POPPED-OUT MODE
1076
+ ══════════════════════════════════════════════════════════ -->
1077
+ <ng-container *ngIf="isPoppedOut; else normalMode">
1078
+ <div class="popout-wrapper" [ngStyle]="wrapperStyles">
1079
+ <header class="popout-header">
1080
+ <span class="popout-title">{{ activePage?.name }}</span>
892
1081
  </header>
893
-
894
- <!-- ── Grid ── -->
895
1082
  <div class="grid-container">
896
- <gridster [options]="options">
897
- <gridster-item [item]="widget" *ngFor="let widget of activePage?.widgets">
1083
+ <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
1084
+ <ng-template gridCell let-widget="widget">
898
1085
  <app-widget-renderer
899
1086
  [widget]="widget"
900
- [onRemoveWidget]="removeWidgetHandler"
901
1087
  [theme]="resolvedTheme"
902
- (editRequested)="openEditDialog($event)"
903
- ></app-widget-renderer>
904
- </gridster-item>
905
- </gridster>
1088
+ (editRequested)="editWidgetRequested.emit($event)"
1089
+ (removeRequested)="onRemoveWidget($event)"
1090
+ >
1091
+ <ng-container
1092
+ *ngIf="cellTemplate"
1093
+ [ngTemplateOutlet]="cellTemplate"
1094
+ [ngTemplateOutletContext]="{ widget: widget }">
1095
+ </ng-container>
1096
+ </app-widget-renderer>
1097
+ </ng-template>
1098
+ </app-grid>
906
1099
  </div>
907
- </main>
908
- </div>
909
-
910
- <!-- ── Add Widget Dialog ── -->
911
- <div class="dialog-backdrop" *ngIf="dialogOpen" (click)="closeDialog()"></div>
912
- <div class="dialog" *ngIf="dialogOpen">
913
- <div class="dialog-header">
914
- <span class="dialog-title">Add Widget</span>
915
- <button class="dialog-close" (click)="closeDialog()"><i class="la la-times"></i></button>
916
1100
  </div>
1101
+ </ng-container>
917
1102
 
918
- <div class="dialog-body">
919
-
920
- <!-- Chart type picker -->
921
- <div class="field-group">
922
- <label class="field-label">Chart type</label>
923
- <div class="type-picker">
924
- <button
925
- *ngFor="let t of widgetTypes"
926
- class="type-btn"
927
- [class.selected]="dialogType === t.value"
928
- (click)="dialogType = t.value"
929
- >
930
- <i [class]="'la ' + t.icon"></i>
931
- <span>{{ t.label }}</span>
932
- </button>
933
- </div>
934
- </div>
935
-
936
- <!-- Title -->
937
- <div class="field-group">
938
- <label class="field-label">Title</label>
939
- <input class="field-input" [(ngModel)]="dialogTitle" placeholder="My Chart" />
940
- </div>
1103
+ <!-- ══════════════════════════════════════════════════════════
1104
+ NORMAL MODE
1105
+ ══════════════════════════════════════════════════════════ -->
1106
+ <ng-template #normalMode>
1107
+ <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
1108
+ <main class="main-content">
941
1109
 
942
- <!-- Series (LINE / BAR) -->
943
- <ng-container *ngIf="dialogType !== 'DONUT'">
944
- <div class="field-group">
945
- <div class="field-row-header">
946
- <label class="field-label">Series</label>
947
- <button class="btn-link" (click)="addSeries()"><i class="la la-plus"></i> Add series</button>
948
- </div>
949
- <div class="series-list">
950
- <div class="series-row" *ngFor="let s of dialogSeries; let i = index">
951
- <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Series name" />
952
- <input class="field-input series-data" [(ngModel)]="s.dataRaw"
953
- placeholder="Comma-separated values, e.g. 10,25,40" />
954
- <button class="btn-remove-series" *ngIf="dialogSeries.length > 1" (click)="removeSeries(i)">
955
- <i class="la la-times"></i>
1110
+ <header class="dashboard-header">
1111
+ <div class="tabs-container">
1112
+ <div
1113
+ *ngFor="let page of pages"
1114
+ class="tab"
1115
+ [class.active]="page.id === activePageId"
1116
+ (click)="onSelectPage(page.id)"
1117
+ >
1118
+ <span class="tab-name">{{ page.name }}</span>
1119
+ <button class="tab-action tab-popout" (click)="onPopOut($event, page.id)" title="Open in new window">
1120
+ <i class="la la-external-link-alt"></i>
956
1121
  </button>
957
- </div>
958
- </div>
959
- </div>
960
-
961
- <div class="field-group">
962
- <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
963
- <input class="field-input" [(ngModel)]="dialogCategories" placeholder="Jan,Feb,Mar,Apr" />
964
- </div>
965
- </ng-container>
966
-
967
- <!-- Slices (DONUT) -->
968
- <ng-container *ngIf="dialogType === 'DONUT'">
969
- <div class="field-group">
970
- <div class="field-row-header">
971
- <label class="field-label">Slices</label>
972
- <button class="btn-link" (click)="addSlice()"><i class="la la-plus"></i> Add slice</button>
973
- </div>
974
- <div class="series-list">
975
- <div class="series-row" *ngFor="let sl of dialogSlices; let i = index">
976
- <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
977
- <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
978
- <button class="btn-remove-series" *ngIf="dialogSlices.length > 1" (click)="removeSlice(i)">
1122
+ <button class="tab-action tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)" title="Close">
979
1123
  <i class="la la-times"></i>
980
1124
  </button>
981
1125
  </div>
1126
+ <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
1127
+ <i class="la la-plus"></i>
1128
+ </button>
982
1129
  </div>
983
- </div>
984
- </ng-container>
985
-
986
- </div>
987
-
988
- <div class="dialog-footer">
989
- <button class="btn-cancel" (click)="closeDialog()">Cancel</button>
990
- <button class="btn-confirm" (click)="confirmAddWidget()" [disabled]="!dialogTitle.trim()">
991
- <i class="la la-check"></i> Add Widget
992
- </button>
993
- </div>
994
- </div>
995
- <!-- ── Edit Widget Dialog ── -->
996
- <div class="dialog-backdrop" *ngIf="editDialogOpen" (click)="closeEditDialog()"></div>
997
- <div class="dialog" *ngIf="editDialogOpen">
998
- <div class="dialog-header">
999
- <span class="dialog-title">Edit Widget</span>
1000
- <button class="dialog-close" (click)="closeEditDialog()"><i class="la la-times"></i></button>
1001
- </div>
1002
-
1003
- <div class="dialog-body">
1004
-
1005
- <!-- Title -->
1006
- <div class="field-group">
1007
- <label class="field-label">Title</label>
1008
- <input class="field-input" [(ngModel)]="editTitle" placeholder="Chart title" />
1009
- </div>
1010
-
1011
- <!-- Card background -->
1012
- <div class="field-group">
1013
- <label class="field-label">Card color</label>
1014
- <div class="color-row">
1015
- <input type="color" class="color-swatch" [(ngModel)]="editCardColor" />
1016
- <input class="field-input" [(ngModel)]="editCardColor" placeholder="#2c2c2e" />
1017
- </div>
1018
- </div>
1019
-
1020
- <!-- Series colors -->
1021
- <div class="field-group">
1022
- <label class="field-label">Series colors</label>
1023
- <div class="color-list">
1024
- <div class="color-item" *ngFor="let c of editColors; let i = index">
1025
- <input type="color" class="color-swatch" [(ngModel)]="editColors[i]" />
1026
- <input class="field-input color-hex" [(ngModel)]="editColors[i]" placeholder="#0a84ff" />
1027
- <span class="color-label">{{ getEditSeriesLabel(i) }}</span>
1028
- </div>
1029
- </div>
1030
- </div>
1031
1130
 
1032
- <!-- Data: LINE / BAR -->
1033
- <ng-container *ngIf="editWidgetType !== 'DONUT'">
1034
- <div class="field-group">
1035
- <div class="field-row-header">
1036
- <label class="field-label">Series data</label>
1037
- <button class="btn-link" (click)="addEditSeries()"><i class="la la-plus"></i> Add</button>
1038
- </div>
1039
- <div class="series-list">
1040
- <div class="series-row" *ngFor="let s of editSeries; let i = index">
1041
- <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Name" />
1042
- <input class="field-input series-data" [(ngModel)]="s.dataRaw" placeholder="10,20,30" />
1043
- <button class="btn-remove-series" *ngIf="editSeries.length > 1" (click)="removeEditSeries(i)">
1044
- <i class="la la-times"></i>
1045
- </button>
1046
- </div>
1047
- </div>
1048
- </div>
1049
- <div class="field-group">
1050
- <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
1051
- <input class="field-input" [(ngModel)]="editCategories" placeholder="Jan,Feb,Mar" />
1052
- </div>
1053
- </ng-container>
1131
+ <!-- Add Widget button consumer decides what happens -->
1132
+ <button class="btn-add-widget" (click)="addWidgetRequested.emit()" title="Add widget">
1133
+ <i class="la la-plus"></i>
1134
+ <span>Add Widget</span>
1135
+ </button>
1136
+ </header>
1054
1137
 
1055
- <!-- Data: DONUT -->
1056
- <ng-container *ngIf="editWidgetType === 'DONUT'">
1057
- <div class="field-group">
1058
- <div class="field-row-header">
1059
- <label class="field-label">Slices</label>
1060
- <button class="btn-link" (click)="addEditSlice()"><i class="la la-plus"></i> Add</button>
1061
- </div>
1062
- <div class="series-list">
1063
- <div class="series-row" *ngFor="let sl of editSlices; let i = index">
1064
- <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
1065
- <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
1066
- <button class="btn-remove-series" *ngIf="editSlices.length > 1" (click)="removeEditSlice(i)">
1067
- <i class="la la-times"></i>
1068
- </button>
1069
- </div>
1070
- </div>
1138
+ <div class="grid-container">
1139
+ <app-grid [widgets]="activePage?.widgets || []" (itemChanged)="onItemChanged($event)">
1140
+ <ng-template gridCell let-widget="widget">
1141
+ <app-widget-renderer
1142
+ [widget]="widget"
1143
+ [theme]="resolvedTheme"
1144
+ (editRequested)="editWidgetRequested.emit($event)"
1145
+ (removeRequested)="onRemoveWidget($event)"
1146
+ >
1147
+ <!-- Stamp consumer's cell template (if provided) inside the card body -->
1148
+ <ng-container
1149
+ *ngIf="cellTemplate"
1150
+ [ngTemplateOutlet]="cellTemplate"
1151
+ [ngTemplateOutletContext]="{ widget: widget }">
1152
+ </ng-container>
1153
+ </app-widget-renderer>
1154
+ </ng-template>
1155
+ </app-grid>
1071
1156
  </div>
1072
- </ng-container>
1073
-
1074
- </div>
1075
1157
 
1076
- <div class="dialog-footer">
1077
- <button class="btn-cancel" (click)="closeEditDialog()">Cancel</button>
1078
- <button class="btn-confirm" (click)="confirmEditWidget()">
1079
- <i class="la la-check"></i> Save
1080
- </button>
1158
+ </main>
1081
1159
  </div>
1082
- </div>
1083
- `, 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"] }]
1160
+ </ng-template>
1161
+ `, 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"] }]
1084
1162
  }], ctorParameters: function () { return [{ type: DashboardStateService }]; }, propDecorators: { initialLayout: [{
1085
1163
  type: Input
1086
1164
  }], theme: [{
1087
1165
  type: Input
1166
+ }], addWidgetRequested: [{
1167
+ type: Output
1168
+ }], editWidgetRequested: [{
1169
+ type: Output
1170
+ }], cellTemplate: [{
1171
+ type: ContentChild,
1172
+ args: [GridCellDirective, { read: TemplateRef }]
1088
1173
  }] } });
1089
1174
 
1090
1175
  class DashboardModule {
1091
1176
  }
1092
1177
  DashboardModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1093
1178
  DashboardModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, declarations: [DashboardComponent,
1094
- WidgetRendererComponent], imports: [CommonModule,
1095
- FormsModule,
1096
- GridsterModule,
1097
- NgApexchartsModule], exports: [DashboardComponent,
1098
- WidgetRendererComponent] });
1099
- DashboardModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, providers: [DashboardStateService], imports: [CommonModule,
1100
- FormsModule,
1101
- GridsterModule,
1102
- NgApexchartsModule] });
1179
+ WidgetRendererComponent,
1180
+ CustomGridComponent,
1181
+ GridCellDirective], imports: [CommonModule], exports: [DashboardComponent,
1182
+ WidgetRendererComponent,
1183
+ CustomGridComponent,
1184
+ GridCellDirective] });
1185
+ DashboardModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, providers: [DashboardStateService], imports: [CommonModule] });
1103
1186
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, decorators: [{
1104
1187
  type: NgModule,
1105
1188
  args: [{
1106
1189
  declarations: [
1107
1190
  DashboardComponent,
1108
- WidgetRendererComponent
1191
+ WidgetRendererComponent,
1192
+ CustomGridComponent,
1193
+ GridCellDirective,
1109
1194
  ],
1110
1195
  imports: [
1111
1196
  CommonModule,
1112
- FormsModule,
1113
- GridsterModule,
1114
- NgApexchartsModule
1115
1197
  ],
1116
1198
  providers: [DashboardStateService],
1117
1199
  exports: [
1118
1200
  DashboardComponent,
1119
- WidgetRendererComponent
1201
+ WidgetRendererComponent,
1202
+ CustomGridComponent,
1203
+ GridCellDirective,
1120
1204
  ],
1121
- schemas: [NO_ERRORS_SCHEMA]
1122
1205
  }]
1123
1206
  }] });
1124
1207
  /**
1125
- * Root module for local demo / testing purposes.
1126
- * In the final NPM package, users would only import the `DashboardModule`.
1208
+ * Root module for local demo / testing purposes only.
1209
+ * Library consumers import DashboardModule, not AppModule.
1127
1210
  */
1128
1211
  class AppModule {
1129
1212
  }
1130
1213
  AppModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1131
1214
  AppModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: AppModule, bootstrap: [DashboardComponent], imports: [BrowserModule, DashboardModule] });
1132
- AppModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, imports: [BrowserModule,
1133
- DashboardModule] });
1215
+ AppModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, imports: [BrowserModule, DashboardModule] });
1134
1216
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, decorators: [{
1135
1217
  type: NgModule,
1136
1218
  args: [{
1137
- imports: [
1138
- BrowserModule,
1139
- DashboardModule
1140
- ],
1141
- bootstrap: [DashboardComponent]
1219
+ imports: [BrowserModule, DashboardModule],
1220
+ bootstrap: [DashboardComponent],
1142
1221
  }]
1143
1222
  }] });
1144
1223
 
1145
1224
  /*
1146
- * Public API Surface of the dashboard library
1225
+ * Public API Surface of @ogidor/dashboard
1147
1226
  */
1148
1227
 
1149
1228
  /**
1150
1229
  * Generated bundle index. Do not edit.
1151
1230
  */
1152
1231
 
1153
- export { AppModule, DashboardComponent, DashboardModule, DashboardStateService, WidgetRendererComponent };
1232
+ export { AppModule, CustomGridComponent, DashboardComponent, DashboardModule, DashboardStateService, GridCellDirective, WidgetRendererComponent };
1154
1233
  //# sourceMappingURL=ogidor-dashboard.mjs.map