@ogidor/dashboard 1.0.0

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.
@@ -0,0 +1,1153 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, EventEmitter, Component, Input, Output, NgModule } from '@angular/core';
3
+ import { BehaviorSubject, Subject, Subscription } from 'rxjs';
4
+ import * as i2 from '@angular/common';
5
+ import { CommonModule } from '@angular/common';
6
+ import * as i3$1 from '@angular/forms';
7
+ import { FormsModule } from '@angular/forms';
8
+ import * as i4 from 'angular-gridster2';
9
+ import { GridsterModule } from 'angular-gridster2';
10
+ import * as i3 from 'ng-apexcharts';
11
+ import { NgApexchartsModule } from 'ng-apexcharts';
12
+ import { BrowserModule } from '@angular/platform-browser';
13
+
14
+ class DashboardStateService {
15
+ constructor() {
16
+ this.STORAGE_KEY = 'xtb_dashboard_layout';
17
+ 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
+ }
44
+ ];
45
+ this.pagesSubject = new BehaviorSubject(this.initialPages);
46
+ 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
+ this.pages$ = this.pagesSubject.asObservable();
54
+ this.activePageId$ = this.activePageIdSubject.asObservable();
55
+ const saved = localStorage.getItem(this.STORAGE_KEY);
56
+ if (saved) {
57
+ try {
58
+ this.loadLayout(JSON.parse(saved));
59
+ }
60
+ catch (e) {
61
+ console.error('Failed to load saved layout', e);
62
+ }
63
+ }
64
+ }
65
+ getActivePage() {
66
+ return this.pagesSubject.value.find(p => p.id === this.activePageIdSubject.value);
67
+ }
68
+ setActivePage(id) {
69
+ this.activePageIdSubject.next(id);
70
+ }
71
+ addPage(name) {
72
+ const newPage = {
73
+ id: `page-${Date.now()}`,
74
+ name,
75
+ widgets: []
76
+ };
77
+ const updatedPages = [...this.pagesSubject.value, newPage];
78
+ this.pagesSubject.next(updatedPages);
79
+ this.activePageIdSubject.next(newPage.id);
80
+ this.saveToLocalStorage();
81
+ }
82
+ removePage(id) {
83
+ const pages = this.pagesSubject.value;
84
+ if (pages.length <= 1)
85
+ return;
86
+ const updatedPages = pages.filter(p => p.id !== id);
87
+ this.pagesSubject.next(updatedPages);
88
+ if (this.activePageIdSubject.value === id) {
89
+ this.activePageIdSubject.next(updatedPages[0].id);
90
+ }
91
+ this.saveToLocalStorage();
92
+ }
93
+ addWidget(type) {
94
+ const activePage = this.getActivePage();
95
+ if (!activePage)
96
+ return;
97
+ const newWidget = {
98
+ id: `widget-${Date.now()}`,
99
+ type,
100
+ x: 0, y: 0,
101
+ cols: type === 'DONUT' ? 2 : 4,
102
+ rows: 4,
103
+ title: `${type.charAt(0) + type.slice(1).toLowerCase()} Chart`,
104
+ data: this.getDefaultDataForType(type)
105
+ };
106
+ activePage.widgets.push(newWidget);
107
+ this.updatePages(this.pagesSubject.value);
108
+ }
109
+ addWidgetWithData(type, title, data) {
110
+ const activePage = this.getActivePage();
111
+ if (!activePage)
112
+ return;
113
+ const newWidget = {
114
+ id: `widget-${Date.now()}`,
115
+ type,
116
+ x: 0, y: 0,
117
+ cols: type === 'DONUT' ? 3 : 6,
118
+ rows: 4,
119
+ title,
120
+ data
121
+ };
122
+ activePage.widgets.push(newWidget);
123
+ this.updatePages(this.pagesSubject.value);
124
+ }
125
+ getDefaultDataForType(type) {
126
+ if (type === 'DONUT') {
127
+ return { series: [30, 20, 50] };
128
+ }
129
+ return {
130
+ series: [{
131
+ name: 'Sample Data',
132
+ data: Array.from({ length: 10 }, () => Math.floor(Math.random() * 100))
133
+ }]
134
+ };
135
+ }
136
+ updateWidgetPosition(pageId, widgetId, x, y, cols, rows) {
137
+ const pages = this.pagesSubject.value;
138
+ const page = pages.find(p => p.id === pageId);
139
+ if (page) {
140
+ const widget = page.widgets.find(w => w.id === widgetId);
141
+ if (widget) {
142
+ widget.x = x;
143
+ widget.y = y;
144
+ widget.cols = cols;
145
+ widget.rows = rows;
146
+ this.updatePages(pages);
147
+ }
148
+ }
149
+ }
150
+ removeWidget(widgetId) {
151
+ const activePage = this.getActivePage();
152
+ if (activePage) {
153
+ activePage.widgets = activePage.widgets.filter(w => w.id !== widgetId);
154
+ this.updatePages(this.pagesSubject.value);
155
+ }
156
+ }
157
+ /**
158
+ * Push new data into a widget by its id.
159
+ * The widget's chart will re-render immediately.
160
+ *
161
+ * @param widgetId The `id` of the target widget.
162
+ * @param data New data — `LineBarData` for LINE/BAR, `DonutData` for DONUT.
163
+ */
164
+ updateWidgetData(widgetId, data) {
165
+ const pages = this.pagesSubject.value;
166
+ for (const page of pages) {
167
+ const widget = page.widgets.find(w => w.id === widgetId);
168
+ if (widget) {
169
+ widget.data = data;
170
+ this.widgetDataSubject.next({ widgetId, data });
171
+ this.updatePages(pages);
172
+ return;
173
+ }
174
+ }
175
+ console.warn(`[Dashboard] updateWidgetData: widget "${widgetId}" not found.`);
176
+ }
177
+ updateWidgetMeta(widgetId, meta) {
178
+ const pages = this.pagesSubject.value;
179
+ for (const page of pages) {
180
+ const widget = page.widgets.find(w => w.id === widgetId);
181
+ if (widget) {
182
+ if (meta.title !== undefined)
183
+ widget.title = meta.title;
184
+ if (meta.colors !== undefined)
185
+ widget.colors = meta.colors;
186
+ if (meta.cardColor !== undefined)
187
+ widget.cardColor = meta.cardColor;
188
+ if (meta.data !== undefined) {
189
+ widget.data = meta.data;
190
+ this.widgetDataSubject.next({ widgetId, data: meta.data });
191
+ }
192
+ this.updatePages(pages);
193
+ return;
194
+ }
195
+ }
196
+ }
197
+ updatePages(pages) {
198
+ this.pagesSubject.next([...pages]);
199
+ this.saveToLocalStorage();
200
+ }
201
+ serializeLayout() {
202
+ const config = {
203
+ pages: this.pagesSubject.value,
204
+ activePageId: this.activePageIdSubject.value
205
+ };
206
+ return JSON.stringify(config);
207
+ }
208
+ loadLayout(config) {
209
+ if (config && config.pages) {
210
+ this.pagesSubject.next(config.pages);
211
+ if (config.activePageId) {
212
+ this.activePageIdSubject.next(config.activePageId);
213
+ }
214
+ }
215
+ }
216
+ saveToLocalStorage() {
217
+ localStorage.setItem(this.STORAGE_KEY, this.serializeLayout());
218
+ }
219
+ }
220
+ DashboardStateService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
221
+ DashboardStateService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, providedIn: 'root' });
222
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardStateService, decorators: [{
223
+ type: Injectable,
224
+ args: [{
225
+ providedIn: 'root'
226
+ }]
227
+ }], ctorParameters: function () { return []; } });
228
+
229
+ const DEFAULT_CHART_COLORS = ['#0a84ff', '#30d158', '#ff9f0a', '#bf5af2', '#ff453a'];
230
+ const DEFAULT_FORECOLOR = '#8e8e93';
231
+ const DEFAULT_CARD_BG = '#2c2c2e';
232
+ class WidgetRendererComponent {
233
+ constructor(stateService) {
234
+ this.stateService = stateService;
235
+ this.editRequested = new EventEmitter();
236
+ this.cardStyles = {};
237
+ this.subs = new Subscription();
238
+ }
239
+ ngOnInit() {
240
+ this.applyStyles();
241
+ this.initChart();
242
+ this.subs.add(this.stateService.widgetData$.subscribe(({ widgetId, data }) => {
243
+ if (widgetId === this.widget.id) {
244
+ this.widget = Object.assign(Object.assign({}, this.widget), { data });
245
+ this.initChart();
246
+ }
247
+ }));
248
+ }
249
+ ngOnChanges(changes) {
250
+ if (changes['theme']) {
251
+ this.applyStyles();
252
+ this.initChart();
253
+ }
254
+ if (changes['widget'] && !changes['widget'].firstChange) {
255
+ this.applyStyles();
256
+ this.initChart();
257
+ }
258
+ }
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
+ }
310
+ WidgetRendererComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: WidgetRendererComponent, deps: [{ token: DashboardStateService }], target: i0.ɵɵFactoryTarget.Component });
311
+ WidgetRendererComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: WidgetRendererComponent, selector: "app-widget-renderer", inputs: { widget: "widget", onRemoveWidget: "onRemoveWidget", theme: "theme" }, outputs: { editRequested: "editRequested" }, usesOnChanges: true, ngImport: i0, template: `
312
+ <div class="widget-container" [ngStyle]="cardStyles">
313
+ <div class="widget-header">
314
+ <span class="widget-title">{{ widget.title }}</span>
315
+ <div class="header-actions">
316
+ <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit widget">
317
+ <i class="la la-pen"></i>
318
+ </button>
319
+ <button class="btn-icon btn-close-widget" (click)="onRemove()" title="Remove widget">
320
+ <i class="la la-times"></i>
321
+ </button>
322
+ </div>
323
+ </div>
324
+ <div class="widget-body">
325
+ <apx-chart
326
+ *ngIf="chartOptions.chart"
327
+ style="display:block;width:100%;height:100%;"
328
+ [series]="chartOptions.series!"
329
+ [chart]="chartOptions.chart!"
330
+ [xaxis]="chartOptions.xaxis!"
331
+ [stroke]="chartOptions.stroke!"
332
+ [dataLabels]="chartOptions.dataLabels!"
333
+ [plotOptions]="chartOptions.plotOptions!"
334
+ [yaxis]="chartOptions.yaxis!"
335
+ [labels]="chartOptions.labels!"
336
+ [legend]="chartOptions.legend!"
337
+ [colors]="chartOptions.colors!"
338
+ [theme]="chartOptions.theme!"
339
+ [tooltip]="chartOptions.tooltip!"
340
+ ></apx-chart>
341
+ </div>
342
+ </div>
343
+ `, isInline: true, styles: [".widget-container{height:100%;width:100%;display:flex;flex-direction:column;border-radius:30px;overflow:hidden;box-shadow:0 8px 24px #00000040;border:1px solid rgba(255,255,255,.08);transition:box-shadow .2s ease;box-sizing:border-box;background:var(--dash-card-bg)}.widget-container:hover{box-shadow:0 12px 32px #00000059}.widget-header{padding:14px 16px 4px;display:flex;justify-content:space-between;align-items:center;cursor:move;flex-shrink:0}.widget-title{color:#fff;font-size:15px;font-weight:600;letter-spacing:.3px}.header-actions{display:flex;gap:6px;align-items:center}.btn-icon{background:rgba(255,255,255,.08);border:none;color:var(--dash-fore-color);cursor:pointer;font-size:12px;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}.btn-edit:hover{background:rgba(10,132,255,.25);color:var(--dash-accent-color)}.btn-close-widget:hover{background:rgba(255,69,58,.25);color:var(--dash-danger-color)}.widget-body{flex:1;min-height:0;padding:4px 12px 12px;display:flex;flex-direction:column}\n"], dependencies: [{ kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i3.ChartComponent, selector: "apx-chart", inputs: ["chart", "annotations", "colors", "dataLabels", "series", "stroke", "labels", "legend", "markers", "noData", "fill", "tooltip", "plotOptions", "responsive", "xaxis", "yaxis", "forecastDataPoints", "grid", "states", "title", "subtitle", "theme", "autoUpdateSeries"] }] });
344
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: WidgetRendererComponent, decorators: [{
345
+ type: Component,
346
+ args: [{ selector: 'app-widget-renderer', template: `
347
+ <div class="widget-container" [ngStyle]="cardStyles">
348
+ <div class="widget-header">
349
+ <span class="widget-title">{{ widget.title }}</span>
350
+ <div class="header-actions">
351
+ <button class="btn-icon btn-edit" (click)="editRequested.emit(widget)" title="Edit widget">
352
+ <i class="la la-pen"></i>
353
+ </button>
354
+ <button class="btn-icon btn-close-widget" (click)="onRemove()" title="Remove widget">
355
+ <i class="la la-times"></i>
356
+ </button>
357
+ </div>
358
+ </div>
359
+ <div class="widget-body">
360
+ <apx-chart
361
+ *ngIf="chartOptions.chart"
362
+ style="display:block;width:100%;height:100%;"
363
+ [series]="chartOptions.series!"
364
+ [chart]="chartOptions.chart!"
365
+ [xaxis]="chartOptions.xaxis!"
366
+ [stroke]="chartOptions.stroke!"
367
+ [dataLabels]="chartOptions.dataLabels!"
368
+ [plotOptions]="chartOptions.plotOptions!"
369
+ [yaxis]="chartOptions.yaxis!"
370
+ [labels]="chartOptions.labels!"
371
+ [legend]="chartOptions.legend!"
372
+ [colors]="chartOptions.colors!"
373
+ [theme]="chartOptions.theme!"
374
+ [tooltip]="chartOptions.tooltip!"
375
+ ></apx-chart>
376
+ </div>
377
+ </div>
378
+ `, styles: [".widget-container{height:100%;width:100%;display:flex;flex-direction:column;border-radius:30px;overflow:hidden;box-shadow:0 8px 24px #00000040;border:1px solid rgba(255,255,255,.08);transition:box-shadow .2s ease;box-sizing:border-box;background:var(--dash-card-bg)}.widget-container:hover{box-shadow:0 12px 32px #00000059}.widget-header{padding:14px 16px 4px;display:flex;justify-content:space-between;align-items:center;cursor:move;flex-shrink:0}.widget-title{color:#fff;font-size:15px;font-weight:600;letter-spacing:.3px}.header-actions{display:flex;gap:6px;align-items:center}.btn-icon{background:rgba(255,255,255,.08);border:none;color:var(--dash-fore-color);cursor:pointer;font-size:12px;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}.btn-edit:hover{background:rgba(10,132,255,.25);color:var(--dash-accent-color)}.btn-close-widget:hover{background:rgba(255,69,58,.25);color:var(--dash-danger-color)}.widget-body{flex:1;min-height:0;padding:4px 12px 12px;display:flex;flex-direction:column}\n"] }]
379
+ }], ctorParameters: function () { return [{ type: DashboardStateService }]; }, propDecorators: { widget: [{
380
+ type: Input
381
+ }], onRemoveWidget: [{
382
+ type: Input
383
+ }], theme: [{
384
+ type: Input
385
+ }], editRequested: [{
386
+ type: Output
387
+ }] } });
388
+
389
+ const DEFAULT_THEME = {
390
+ backgroundColor: '#000000',
391
+ panelColor: '#1c1c1e',
392
+ widgetCardColor: '#2c2c2e',
393
+ chartForeColor: '#8e8e93',
394
+ accentColor: '#0a84ff',
395
+ dangerColor: '#ff453a',
396
+ chartColors: ['#0a84ff', '#30d158', '#ff9f0a', '#bf5af2', '#ff453a'],
397
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
398
+ };
399
+ class DashboardComponent {
400
+ constructor(stateService) {
401
+ this.stateService = stateService;
402
+ this.resolvedTheme = Object.assign({}, DEFAULT_THEME);
403
+ this.wrapperStyles = {};
404
+ this.pages = [];
405
+ this.activePageId = '';
406
+ // ── Dialog state ──
407
+ this.dialogOpen = false;
408
+ this.dialogType = 'LINE';
409
+ this.dialogTitle = '';
410
+ this.dialogCategories = '';
411
+ this.dialogSeries = [{ name: 'Series 1', dataRaw: '' }];
412
+ this.dialogSlices = [
413
+ { label: 'Slice A', value: null },
414
+ { label: 'Slice B', value: null },
415
+ { label: 'Slice C', value: null },
416
+ ];
417
+ this.widgetTypes = [
418
+ { value: 'LINE', label: 'Line', icon: 'la-chart-area' },
419
+ { value: 'BAR', label: 'Bar', icon: 'la-chart-bar' },
420
+ { value: 'DONUT', label: 'Donut', icon: 'la-chart-pie' },
421
+ ];
422
+ 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
+ }
435
+ ngOnChanges(changes) {
436
+ if (changes['theme'] || changes['initialLayout'])
437
+ this.applyTheme();
438
+ }
439
+ ngOnInit() {
440
+ this.applyTheme();
441
+ if (this.initialLayout) {
442
+ try {
443
+ this.stateService.loadLayout(JSON.parse(this.initialLayout));
444
+ }
445
+ catch (e) {
446
+ console.error('[Dashboard] Failed to parse initialLayout', e);
447
+ }
448
+ }
449
+ this.options = {
450
+ gridType: 'fit',
451
+ displayGrid: 'none',
452
+ pushItems: true,
453
+ draggable: { enabled: true },
454
+ resizable: { enabled: true },
455
+ minCols: 12, maxCols: 12,
456
+ minRows: 12, maxRows: 50,
457
+ margin: 20, outerMargin: true,
458
+ itemChangeCallback: (item) => {
459
+ this.stateService.updateWidgetPosition(this.activePageId, item.id, item.x, item.y, item.cols, item.rows);
460
+ }
461
+ };
462
+ this.subs.add(this.stateService.pages$.subscribe(pages => {
463
+ this.pages = pages;
464
+ this.updateActivePage();
465
+ }));
466
+ this.subs.add(this.stateService.activePageId$.subscribe(id => {
467
+ this.activePageId = id;
468
+ this.updateActivePage();
469
+ }));
470
+ }
471
+ ngOnDestroy() { this.subs.unsubscribe(); }
472
+ applyTheme() {
473
+ var _a;
474
+ this.resolvedTheme = Object.assign(Object.assign({}, DEFAULT_THEME), ((_a = this.theme) !== null && _a !== void 0 ? _a : {}));
475
+ this.wrapperStyles = {
476
+ '--dash-bg': this.resolvedTheme.backgroundColor,
477
+ '--dash-panel-bg': this.resolvedTheme.panelColor,
478
+ '--dash-card-bg': this.resolvedTheme.widgetCardColor,
479
+ '--dash-fore-color': this.resolvedTheme.chartForeColor,
480
+ '--dash-accent-color': this.resolvedTheme.accentColor,
481
+ '--dash-danger-color': this.resolvedTheme.dangerColor,
482
+ '--dash-font-family': this.resolvedTheme.fontFamily,
483
+ };
484
+ }
485
+ updateActivePage() {
486
+ this.activePage = this.pages.find(p => p.id === this.activePageId);
487
+ }
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
+ }
639
+ DashboardComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardComponent, deps: [{ token: DashboardStateService }], target: i0.ɵɵFactoryTarget.Component });
640
+ DashboardComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "15.2.10", type: DashboardComponent, selector: "app-dashboard", inputs: { initialLayout: "initialLayout", theme: "theme" }, usesOnChanges: true, ngImport: i0, template: `
641
+ <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
642
+ <main class="main-content">
643
+
644
+ <!-- ── Header ── -->
645
+ <header class="dashboard-header">
646
+ <!-- Tabs -->
647
+ <div class="tabs-container">
648
+ <div
649
+ *ngFor="let page of pages"
650
+ class="tab"
651
+ [class.active]="page.id === activePageId"
652
+ (click)="onSelectPage(page.id)"
653
+ >
654
+ <span class="tab-name">{{ page.name }}</span>
655
+ <button class="tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)">
656
+ <i class="la la-times"></i>
657
+ </button>
658
+ </div>
659
+ <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
660
+ <i class="la la-plus"></i>
661
+ </button>
662
+ </div>
663
+
664
+ <!-- Add Widget button -->
665
+ <button class="btn-add-widget" (click)="openDialog()" title="Add widget">
666
+ <i class="la la-plus"></i>
667
+ <span>Add Widget</span>
668
+ </button>
669
+ </header>
670
+
671
+ <!-- ── Grid ── -->
672
+ <div class="grid-container">
673
+ <gridster [options]="options">
674
+ <gridster-item [item]="widget" *ngFor="let widget of activePage?.widgets">
675
+ <app-widget-renderer
676
+ [widget]="widget"
677
+ [onRemoveWidget]="removeWidgetHandler"
678
+ [theme]="resolvedTheme"
679
+ (editRequested)="openEditDialog($event)"
680
+ ></app-widget-renderer>
681
+ </gridster-item>
682
+ </gridster>
683
+ </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
+ </div>
694
+
695
+ <div class="dialog-body">
696
+
697
+ <!-- Chart type picker -->
698
+ <div class="field-group">
699
+ <label class="field-label">Chart type</label>
700
+ <div class="type-picker">
701
+ <button
702
+ *ngFor="let t of widgetTypes"
703
+ class="type-btn"
704
+ [class.selected]="dialogType === t.value"
705
+ (click)="dialogType = t.value"
706
+ >
707
+ <i [class]="'la ' + t.icon"></i>
708
+ <span>{{ t.label }}</span>
709
+ </button>
710
+ </div>
711
+ </div>
712
+
713
+ <!-- Title -->
714
+ <div class="field-group">
715
+ <label class="field-label">Title</label>
716
+ <input class="field-input" [(ngModel)]="dialogTitle" placeholder="My Chart" />
717
+ </div>
718
+
719
+ <!-- Series (LINE / BAR) -->
720
+ <ng-container *ngIf="dialogType !== 'DONUT'">
721
+ <div class="field-group">
722
+ <div class="field-row-header">
723
+ <label class="field-label">Series</label>
724
+ <button class="btn-link" (click)="addSeries()"><i class="la la-plus"></i> Add series</button>
725
+ </div>
726
+ <div class="series-list">
727
+ <div class="series-row" *ngFor="let s of dialogSeries; let i = index">
728
+ <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Series name" />
729
+ <input class="field-input series-data" [(ngModel)]="s.dataRaw"
730
+ placeholder="Comma-separated values, e.g. 10,25,40" />
731
+ <button class="btn-remove-series" *ngIf="dialogSeries.length > 1" (click)="removeSeries(i)">
732
+ <i class="la la-times"></i>
733
+ </button>
734
+ </div>
735
+ </div>
736
+ </div>
737
+
738
+ <div class="field-group">
739
+ <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
740
+ <input class="field-input" [(ngModel)]="dialogCategories" placeholder="Jan,Feb,Mar,Apr" />
741
+ </div>
742
+ </ng-container>
743
+
744
+ <!-- Slices (DONUT) -->
745
+ <ng-container *ngIf="dialogType === 'DONUT'">
746
+ <div class="field-group">
747
+ <div class="field-row-header">
748
+ <label class="field-label">Slices</label>
749
+ <button class="btn-link" (click)="addSlice()"><i class="la la-plus"></i> Add slice</button>
750
+ </div>
751
+ <div class="series-list">
752
+ <div class="series-row" *ngFor="let sl of dialogSlices; let i = index">
753
+ <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
754
+ <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
755
+ <button class="btn-remove-series" *ngIf="dialogSlices.length > 1" (click)="removeSlice(i)">
756
+ <i class="la la-times"></i>
757
+ </button>
758
+ </div>
759
+ </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
+
809
+ <!-- Data: LINE / BAR -->
810
+ <ng-container *ngIf="editWidgetType !== 'DONUT'">
811
+ <div class="field-group">
812
+ <div class="field-row-header">
813
+ <label class="field-label">Series data</label>
814
+ <button class="btn-link" (click)="addEditSeries()"><i class="la la-plus"></i> Add</button>
815
+ </div>
816
+ <div class="series-list">
817
+ <div class="series-row" *ngFor="let s of editSeries; let i = index">
818
+ <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Name" />
819
+ <input class="field-input series-data" [(ngModel)]="s.dataRaw" placeholder="10,20,30" />
820
+ <button class="btn-remove-series" *ngIf="editSeries.length > 1" (click)="removeEditSeries(i)">
821
+ <i class="la la-times"></i>
822
+ </button>
823
+ </div>
824
+ </div>
825
+ </div>
826
+ <div class="field-group">
827
+ <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
828
+ <input class="field-input" [(ngModel)]="editCategories" placeholder="Jan,Feb,Mar" />
829
+ </div>
830
+ </ng-container>
831
+
832
+ <!-- Data: DONUT -->
833
+ <ng-container *ngIf="editWidgetType === 'DONUT'">
834
+ <div class="field-group">
835
+ <div class="field-row-header">
836
+ <label class="field-label">Slices</label>
837
+ <button class="btn-link" (click)="addEditSlice()"><i class="la la-plus"></i> Add</button>
838
+ </div>
839
+ <div class="series-list">
840
+ <div class="series-row" *ngFor="let sl of editSlices; let i = index">
841
+ <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
842
+ <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
843
+ <button class="btn-remove-series" *ngIf="editSlices.length > 1" (click)="removeEditSlice(i)">
844
+ <i class="la la-times"></i>
845
+ </button>
846
+ </div>
847
+ </div>
848
+ </div>
849
+ </ng-container>
850
+
851
+ </div>
852
+
853
+ <div class="dialog-footer">
854
+ <button class="btn-cancel" (click)="closeEditDialog()">Cancel</button>
855
+ <button class="btn-confirm" (click)="confirmEditWidget()">
856
+ <i class="la la-check"></i> Save
857
+ </button>
858
+ </div>
859
+ </div>
860
+ `, isInline: true, styles: [":host{--dash-bg: #000000;--dash-panel-bg: #1c1c1e;--dash-card-bg: #2c2c2e;--dash-fore-color: #8e8e93;--dash-accent-color: #0a84ff;--dash-danger-color: #ff453a;--dash-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif}.dashboard-wrapper{display:flex;height:100vh;width:100vw;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;border-radius:40px;padding:20px;background:var(--dash-panel-bg);box-shadow:0 10px 30px #00000080;border:1px solid rgba(255,255,255,.05)}.dashboard-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;gap:12px}.tabs-container{display:flex;align-items:center;gap:8px;background:rgba(44,44,46,.6);backdrop-filter:blur(10px);border-radius:25px;padding:6px}.tab{display:flex;align-items:center;padding:8px 20px;border-radius:20px;cursor:pointer;font-size:14px;font-weight:500;color:var(--dash-fore-color);transition:all .3s cubic-bezier(.25,.8,.25,1)}.tab.active{background:#3a3a3c;color:#fff;box-shadow:0 2px 10px #0003}.tab:hover:not(.active){color:#e5e5ea;background:rgba(255,255,255,.05)}.tab-name{margin-right:8px}.tab-close{background:transparent;border:none;color:var(--dash-fore-color);padding:2px;font-size:12px;cursor:pointer;opacity:0;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s}.tab-close:hover{color:var(--dash-danger-color);background:rgba(255,69,58,.2)}.tab:hover .tab-close{opacity:1}.btn-add-page{background:transparent;border:none;color:var(--dash-fore-color);padding:8px 16px;cursor:pointer;border-radius:20px;transition:all .2s}.btn-add-page:hover{color:var(--dash-accent-color);background:rgba(10,132,255,.1)}.btn-add-widget{display:flex;align-items:center;gap:6px;background:var(--dash-accent-color);border:none;color:#fff;font-size:14px;font-weight:600;padding:10px 20px;border-radius:22px;cursor:pointer;white-space:nowrap;transition:opacity .2s,transform .15s;box-shadow:0 4px 14px #0a84ff66}.btn-add-widget:hover{opacity:.9;transform:translateY(-1px)}.btn-add-widget:active{transform:translateY(0)}.grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0}gridster{background:transparent;height:100%!important}.dialog-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(6px);z-index:900}.dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:901;width:min(520px,calc(100vw - 32px));max-height:calc(100vh - 64px);display:flex;flex-direction:column;background:var(--dash-panel-bg);border-radius:28px;border:1px solid rgba(255,255,255,.1);box-shadow:0 24px 60px #000000b3;overflow:hidden}.dialog-header{display:flex;align-items:center;justify-content:space-between;padding:22px 24px 16px;border-bottom:1px solid rgba(255,255,255,.07)}.dialog-title{font-size:17px;font-weight:700;color:#fff}.dialog-close{background:rgba(255,255,255,.08);border:none;color:var(--dash-fore-color);width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;transition:all .2s}.dialog-close:hover{background:rgba(255,69,58,.25);color:var(--dash-danger-color)}.dialog-body{flex:1;overflow-y:auto;padding:20px 24px;display:flex;flex-direction:column;gap:20px}.field-group{display:flex;flex-direction:column;gap:8px}.field-label{font-size:13px;font-weight:600;color:var(--dash-fore-color);text-transform:uppercase;letter-spacing:.5px}.field-input{background:var(--dash-card-bg);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:10px 14px;color:#fff;font-size:14px;outline:none;width:100%;box-sizing:border-box}.field-input:focus{border-color:var(--dash-accent-color)}.type-picker{display:flex;gap:10px}.type-btn{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;padding:14px 10px;background:var(--dash-card-bg);border:2px solid transparent;border-radius:16px;color:var(--dash-fore-color);font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.type-btn.selected{border-color:var(--dash-accent-color);color:var(--dash-accent-color);background:rgba(10,132,255,.12)}.btn-link{background:transparent;border:none;color:var(--dash-accent-color);font-size:13px;font-weight:600;cursor:pointer}.dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:16px 24px 22px;border-top:1px solid rgba(255,255,255,.07)}.btn-cancel{background:transparent;border:1px solid rgba(255,255,255,.12);color:var(--dash-fore-color);padding:10px 20px;border-radius:20px;cursor:pointer}.btn-confirm{background:var(--dash-accent-color);border:none;color:#fff;padding:10px 22px;border-radius:20px;font-weight:600;cursor:pointer}.btn-confirm:disabled{opacity:.4;cursor:not-allowed}\n"], dependencies: [{ kind: "directive", type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i3$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i4.GridsterComponent, selector: "gridster", inputs: ["options"] }, { kind: "component", type: i4.GridsterItemComponent, selector: "gridster-item", inputs: ["item"], outputs: ["itemInit", "itemChange", "itemResize"] }, { kind: "component", type: WidgetRendererComponent, selector: "app-widget-renderer", inputs: ["widget", "onRemoveWidget", "theme"], outputs: ["editRequested"] }] });
861
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardComponent, decorators: [{
862
+ type: Component,
863
+ args: [{ selector: 'app-dashboard', template: `
864
+ <div class="dashboard-wrapper" [ngStyle]="wrapperStyles">
865
+ <main class="main-content">
866
+
867
+ <!-- ── Header ── -->
868
+ <header class="dashboard-header">
869
+ <!-- Tabs -->
870
+ <div class="tabs-container">
871
+ <div
872
+ *ngFor="let page of pages"
873
+ class="tab"
874
+ [class.active]="page.id === activePageId"
875
+ (click)="onSelectPage(page.id)"
876
+ >
877
+ <span class="tab-name">{{ page.name }}</span>
878
+ <button class="tab-close" *ngIf="pages.length > 1" (click)="onRemovePage($event, page.id)">
879
+ <i class="la la-times"></i>
880
+ </button>
881
+ </div>
882
+ <button class="btn-add-page" (click)="onAddPage()" title="New workspace">
883
+ <i class="la la-plus"></i>
884
+ </button>
885
+ </div>
886
+
887
+ <!-- Add Widget button -->
888
+ <button class="btn-add-widget" (click)="openDialog()" title="Add widget">
889
+ <i class="la la-plus"></i>
890
+ <span>Add Widget</span>
891
+ </button>
892
+ </header>
893
+
894
+ <!-- ── Grid ── -->
895
+ <div class="grid-container">
896
+ <gridster [options]="options">
897
+ <gridster-item [item]="widget" *ngFor="let widget of activePage?.widgets">
898
+ <app-widget-renderer
899
+ [widget]="widget"
900
+ [onRemoveWidget]="removeWidgetHandler"
901
+ [theme]="resolvedTheme"
902
+ (editRequested)="openEditDialog($event)"
903
+ ></app-widget-renderer>
904
+ </gridster-item>
905
+ </gridster>
906
+ </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
+ </div>
917
+
918
+ <div class="dialog-body">
919
+
920
+ <!-- Chart type picker -->
921
+ <div class="field-group">
922
+ <label class="field-label">Chart type</label>
923
+ <div class="type-picker">
924
+ <button
925
+ *ngFor="let t of widgetTypes"
926
+ class="type-btn"
927
+ [class.selected]="dialogType === t.value"
928
+ (click)="dialogType = t.value"
929
+ >
930
+ <i [class]="'la ' + t.icon"></i>
931
+ <span>{{ t.label }}</span>
932
+ </button>
933
+ </div>
934
+ </div>
935
+
936
+ <!-- Title -->
937
+ <div class="field-group">
938
+ <label class="field-label">Title</label>
939
+ <input class="field-input" [(ngModel)]="dialogTitle" placeholder="My Chart" />
940
+ </div>
941
+
942
+ <!-- Series (LINE / BAR) -->
943
+ <ng-container *ngIf="dialogType !== 'DONUT'">
944
+ <div class="field-group">
945
+ <div class="field-row-header">
946
+ <label class="field-label">Series</label>
947
+ <button class="btn-link" (click)="addSeries()"><i class="la la-plus"></i> Add series</button>
948
+ </div>
949
+ <div class="series-list">
950
+ <div class="series-row" *ngFor="let s of dialogSeries; let i = index">
951
+ <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Series name" />
952
+ <input class="field-input series-data" [(ngModel)]="s.dataRaw"
953
+ placeholder="Comma-separated values, e.g. 10,25,40" />
954
+ <button class="btn-remove-series" *ngIf="dialogSeries.length > 1" (click)="removeSeries(i)">
955
+ <i class="la la-times"></i>
956
+ </button>
957
+ </div>
958
+ </div>
959
+ </div>
960
+
961
+ <div class="field-group">
962
+ <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
963
+ <input class="field-input" [(ngModel)]="dialogCategories" placeholder="Jan,Feb,Mar,Apr" />
964
+ </div>
965
+ </ng-container>
966
+
967
+ <!-- Slices (DONUT) -->
968
+ <ng-container *ngIf="dialogType === 'DONUT'">
969
+ <div class="field-group">
970
+ <div class="field-row-header">
971
+ <label class="field-label">Slices</label>
972
+ <button class="btn-link" (click)="addSlice()"><i class="la la-plus"></i> Add slice</button>
973
+ </div>
974
+ <div class="series-list">
975
+ <div class="series-row" *ngFor="let sl of dialogSlices; let i = index">
976
+ <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
977
+ <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
978
+ <button class="btn-remove-series" *ngIf="dialogSlices.length > 1" (click)="removeSlice(i)">
979
+ <i class="la la-times"></i>
980
+ </button>
981
+ </div>
982
+ </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
+
1032
+ <!-- Data: LINE / BAR -->
1033
+ <ng-container *ngIf="editWidgetType !== 'DONUT'">
1034
+ <div class="field-group">
1035
+ <div class="field-row-header">
1036
+ <label class="field-label">Series data</label>
1037
+ <button class="btn-link" (click)="addEditSeries()"><i class="la la-plus"></i> Add</button>
1038
+ </div>
1039
+ <div class="series-list">
1040
+ <div class="series-row" *ngFor="let s of editSeries; let i = index">
1041
+ <input class="field-input series-name" [(ngModel)]="s.name" placeholder="Name" />
1042
+ <input class="field-input series-data" [(ngModel)]="s.dataRaw" placeholder="10,20,30" />
1043
+ <button class="btn-remove-series" *ngIf="editSeries.length > 1" (click)="removeEditSeries(i)">
1044
+ <i class="la la-times"></i>
1045
+ </button>
1046
+ </div>
1047
+ </div>
1048
+ </div>
1049
+ <div class="field-group">
1050
+ <label class="field-label">X-axis labels <span class="field-hint">(optional, comma-separated)</span></label>
1051
+ <input class="field-input" [(ngModel)]="editCategories" placeholder="Jan,Feb,Mar" />
1052
+ </div>
1053
+ </ng-container>
1054
+
1055
+ <!-- Data: DONUT -->
1056
+ <ng-container *ngIf="editWidgetType === 'DONUT'">
1057
+ <div class="field-group">
1058
+ <div class="field-row-header">
1059
+ <label class="field-label">Slices</label>
1060
+ <button class="btn-link" (click)="addEditSlice()"><i class="la la-plus"></i> Add</button>
1061
+ </div>
1062
+ <div class="series-list">
1063
+ <div class="series-row" *ngFor="let sl of editSlices; let i = index">
1064
+ <input class="field-input series-name" [(ngModel)]="sl.label" placeholder="Label" />
1065
+ <input class="field-input series-data" [(ngModel)]="sl.value" type="number" placeholder="Value" />
1066
+ <button class="btn-remove-series" *ngIf="editSlices.length > 1" (click)="removeEditSlice(i)">
1067
+ <i class="la la-times"></i>
1068
+ </button>
1069
+ </div>
1070
+ </div>
1071
+ </div>
1072
+ </ng-container>
1073
+
1074
+ </div>
1075
+
1076
+ <div class="dialog-footer">
1077
+ <button class="btn-cancel" (click)="closeEditDialog()">Cancel</button>
1078
+ <button class="btn-confirm" (click)="confirmEditWidget()">
1079
+ <i class="la la-check"></i> Save
1080
+ </button>
1081
+ </div>
1082
+ </div>
1083
+ `, styles: [":host{--dash-bg: #000000;--dash-panel-bg: #1c1c1e;--dash-card-bg: #2c2c2e;--dash-fore-color: #8e8e93;--dash-accent-color: #0a84ff;--dash-danger-color: #ff453a;--dash-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif}.dashboard-wrapper{display:flex;height:100vh;width:100vw;overflow:hidden;padding:16px;box-sizing:border-box;background:var(--dash-bg);color:var(--dash-fore-color);font-family:var(--dash-font-family)}.main-content{flex:1;display:flex;flex-direction:column;overflow:hidden;border-radius:40px;padding:20px;background:var(--dash-panel-bg);box-shadow:0 10px 30px #00000080;border:1px solid rgba(255,255,255,.05)}.dashboard-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;gap:12px}.tabs-container{display:flex;align-items:center;gap:8px;background:rgba(44,44,46,.6);backdrop-filter:blur(10px);border-radius:25px;padding:6px}.tab{display:flex;align-items:center;padding:8px 20px;border-radius:20px;cursor:pointer;font-size:14px;font-weight:500;color:var(--dash-fore-color);transition:all .3s cubic-bezier(.25,.8,.25,1)}.tab.active{background:#3a3a3c;color:#fff;box-shadow:0 2px 10px #0003}.tab:hover:not(.active){color:#e5e5ea;background:rgba(255,255,255,.05)}.tab-name{margin-right:8px}.tab-close{background:transparent;border:none;color:var(--dash-fore-color);padding:2px;font-size:12px;cursor:pointer;opacity:0;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:all .2s}.tab-close:hover{color:var(--dash-danger-color);background:rgba(255,69,58,.2)}.tab:hover .tab-close{opacity:1}.btn-add-page{background:transparent;border:none;color:var(--dash-fore-color);padding:8px 16px;cursor:pointer;border-radius:20px;transition:all .2s}.btn-add-page:hover{color:var(--dash-accent-color);background:rgba(10,132,255,.1)}.btn-add-widget{display:flex;align-items:center;gap:6px;background:var(--dash-accent-color);border:none;color:#fff;font-size:14px;font-weight:600;padding:10px 20px;border-radius:22px;cursor:pointer;white-space:nowrap;transition:opacity .2s,transform .15s;box-shadow:0 4px 14px #0a84ff66}.btn-add-widget:hover{opacity:.9;transform:translateY(-1px)}.btn-add-widget:active{transform:translateY(0)}.grid-container{flex:1;overflow:auto;border-radius:24px;min-height:0}gridster{background:transparent;height:100%!important}.dialog-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(6px);z-index:900}.dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:901;width:min(520px,calc(100vw - 32px));max-height:calc(100vh - 64px);display:flex;flex-direction:column;background:var(--dash-panel-bg);border-radius:28px;border:1px solid rgba(255,255,255,.1);box-shadow:0 24px 60px #000000b3;overflow:hidden}.dialog-header{display:flex;align-items:center;justify-content:space-between;padding:22px 24px 16px;border-bottom:1px solid rgba(255,255,255,.07)}.dialog-title{font-size:17px;font-weight:700;color:#fff}.dialog-close{background:rgba(255,255,255,.08);border:none;color:var(--dash-fore-color);width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:14px;transition:all .2s}.dialog-close:hover{background:rgba(255,69,58,.25);color:var(--dash-danger-color)}.dialog-body{flex:1;overflow-y:auto;padding:20px 24px;display:flex;flex-direction:column;gap:20px}.field-group{display:flex;flex-direction:column;gap:8px}.field-label{font-size:13px;font-weight:600;color:var(--dash-fore-color);text-transform:uppercase;letter-spacing:.5px}.field-input{background:var(--dash-card-bg);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:10px 14px;color:#fff;font-size:14px;outline:none;width:100%;box-sizing:border-box}.field-input:focus{border-color:var(--dash-accent-color)}.type-picker{display:flex;gap:10px}.type-btn{flex:1;display:flex;flex-direction:column;align-items:center;gap:6px;padding:14px 10px;background:var(--dash-card-bg);border:2px solid transparent;border-radius:16px;color:var(--dash-fore-color);font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.type-btn.selected{border-color:var(--dash-accent-color);color:var(--dash-accent-color);background:rgba(10,132,255,.12)}.btn-link{background:transparent;border:none;color:var(--dash-accent-color);font-size:13px;font-weight:600;cursor:pointer}.dialog-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:16px 24px 22px;border-top:1px solid rgba(255,255,255,.07)}.btn-cancel{background:transparent;border:1px solid rgba(255,255,255,.12);color:var(--dash-fore-color);padding:10px 20px;border-radius:20px;cursor:pointer}.btn-confirm{background:var(--dash-accent-color);border:none;color:#fff;padding:10px 22px;border-radius:20px;font-weight:600;cursor:pointer}.btn-confirm:disabled{opacity:.4;cursor:not-allowed}\n"] }]
1084
+ }], ctorParameters: function () { return [{ type: DashboardStateService }]; }, propDecorators: { initialLayout: [{
1085
+ type: Input
1086
+ }], theme: [{
1087
+ type: Input
1088
+ }] } });
1089
+
1090
+ class DashboardModule {
1091
+ }
1092
+ DashboardModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1093
+ DashboardModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, declarations: [DashboardComponent,
1094
+ WidgetRendererComponent], imports: [CommonModule,
1095
+ FormsModule,
1096
+ GridsterModule,
1097
+ NgApexchartsModule], exports: [DashboardComponent,
1098
+ WidgetRendererComponent] });
1099
+ DashboardModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, providers: [DashboardStateService], imports: [CommonModule,
1100
+ FormsModule,
1101
+ GridsterModule,
1102
+ NgApexchartsModule] });
1103
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: DashboardModule, decorators: [{
1104
+ type: NgModule,
1105
+ args: [{
1106
+ declarations: [
1107
+ DashboardComponent,
1108
+ WidgetRendererComponent
1109
+ ],
1110
+ imports: [
1111
+ CommonModule,
1112
+ FormsModule,
1113
+ GridsterModule,
1114
+ NgApexchartsModule
1115
+ ],
1116
+ providers: [DashboardStateService],
1117
+ exports: [
1118
+ DashboardComponent,
1119
+ WidgetRendererComponent
1120
+ ]
1121
+ }]
1122
+ }] });
1123
+ /**
1124
+ * Root module for local demo / testing purposes.
1125
+ * In the final NPM package, users would only import the `DashboardModule`.
1126
+ */
1127
+ class AppModule {
1128
+ }
1129
+ AppModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1130
+ AppModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.2.10", ngImport: i0, type: AppModule, bootstrap: [DashboardComponent], imports: [BrowserModule, DashboardModule] });
1131
+ AppModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, imports: [BrowserModule,
1132
+ DashboardModule] });
1133
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.2.10", ngImport: i0, type: AppModule, decorators: [{
1134
+ type: NgModule,
1135
+ args: [{
1136
+ imports: [
1137
+ BrowserModule,
1138
+ DashboardModule
1139
+ ],
1140
+ bootstrap: [DashboardComponent]
1141
+ }]
1142
+ }] });
1143
+
1144
+ /*
1145
+ * Public API Surface of the dashboard library
1146
+ */
1147
+
1148
+ /**
1149
+ * Generated bundle index. Do not edit.
1150
+ */
1151
+
1152
+ export { AppModule, DashboardComponent, DashboardModule, DashboardStateService, WidgetRendererComponent };
1153
+ //# sourceMappingURL=ogidor-dashboard.mjs.map