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