@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.
- package/README.md +283 -48
- package/app/app.module.d.ts +7 -9
- package/app/custom-grid.component.d.ts +93 -0
- package/app/dashboard-state.service.d.ts +54 -33
- package/app/dashboard.component.d.ts +27 -59
- package/app/models.d.ts +18 -38
- package/app/widget-renderer.component.d.ts +5 -32
- package/esm2020/app/app.module.mjs +21 -29
- package/esm2020/app/custom-grid.component.mjs +509 -0
- package/esm2020/app/dashboard-state.service.mjs +228 -158
- package/esm2020/app/dashboard.component.mjs +198 -602
- package/esm2020/app/models.mjs +1 -1
- package/esm2020/app/widget-renderer.component.mjs +41 -121
- package/esm2020/public-api.mjs +5 -4
- package/fesm2015/ogidor-dashboard.mjs +988 -909
- package/fesm2015/ogidor-dashboard.mjs.map +1 -1
- package/fesm2020/ogidor-dashboard.mjs +982 -898
- package/fesm2020/ogidor-dashboard.mjs.map +1 -1
- package/package.json +8 -12
- package/public-api.d.ts +3 -2
|
@@ -1,442 +1,919 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
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
|
-
|
|
56
|
-
|
|
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.
|
|
547
|
+
this._applyShared(JSON.parse(shared), false);
|
|
59
548
|
}
|
|
60
549
|
catch (e) {
|
|
61
|
-
console.error('
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
87
|
-
this.pagesSubject.next(
|
|
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(
|
|
582
|
+
this.activePageIdSubject.next(updated[0].id);
|
|
90
583
|
}
|
|
91
|
-
this.
|
|
584
|
+
this._saveShared();
|
|
585
|
+
this._broadcast({ type: 'PAGE_REMOVED', pageId: id });
|
|
92
586
|
}
|
|
93
|
-
|
|
587
|
+
// ── Widget structural actions (synced to all windows) ─────────
|
|
588
|
+
addWidget(widget) {
|
|
94
589
|
const activePage = this.getActivePage();
|
|
95
590
|
if (!activePage)
|
|
96
|
-
|
|
97
|
-
const newWidget = {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
613
|
+
removeWidget(widgetId) {
|
|
110
614
|
const activePage = this.getActivePage();
|
|
111
615
|
if (!activePage)
|
|
112
616
|
return;
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.
|
|
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
|
-
*
|
|
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
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
684
|
+
localStorage.setItem(this.positionsKey, JSON.stringify(positions));
|
|
176
685
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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(
|
|
234
|
-
this.stateService = stateService;
|
|
801
|
+
constructor() {
|
|
235
802
|
this.editRequested = new EventEmitter();
|
|
236
|
-
this.
|
|
237
|
-
this.
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
this.
|
|
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: [
|
|
311
|
-
WidgetRendererComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: WidgetRendererComponent, selector: "app-widget-renderer", inputs: { widget: "widget",
|
|
312
|
-
<div class="widget-
|
|
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"
|
|
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-
|
|
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
|
-
<
|
|
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-
|
|
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-
|
|
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"
|
|
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-
|
|
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
|
-
<
|
|
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-
|
|
379
|
-
}],
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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.
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
<
|
|
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
|
-
<
|
|
674
|
-
<
|
|
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)="
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
1012
|
+
<!-- ══════════════════════════════════════════════════════════
|
|
1013
|
+
NORMAL MODE
|
|
1014
|
+
══════════════════════════════════════════════════════════ -->
|
|
1015
|
+
<ng-template #normalMode>
|
|
1016
|
+
<div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
|
|
1017
|
+
<main class="main-content">
|
|
696
1018
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
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
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
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
|
-
</
|
|
860
|
-
`, isInline: true, styles: [":host{--dash-bg: #000000;--dash-panel-bg: #1c1c1e;--dash-card-bg: #2c2c2e;--dash-fore-color
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
<
|
|
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
|
-
<
|
|
897
|
-
<
|
|
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)="
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
<
|
|
952
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
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
|
-
</
|
|
1083
|
-
`, styles: [":host{--dash-bg: #000000;--dash-panel-bg: #1c1c1e;--dash-card-bg: #2c2c2e;--dash-fore-color
|
|
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
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|