@dragonworks/ngx-dashboard-widgets 20.0.6 → 20.1.1

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.
Files changed (34) hide show
  1. package/fesm2022/dragonworks-ngx-dashboard-widgets.mjs +2251 -0
  2. package/fesm2022/dragonworks-ngx-dashboard-widgets.mjs.map +1 -0
  3. package/index.d.ts +532 -0
  4. package/package.json +42 -31
  5. package/ng-package.json +0 -7
  6. package/src/lib/arrow-widget/arrow-state-dialog.component.ts +0 -187
  7. package/src/lib/arrow-widget/arrow-widget.component.html +0 -9
  8. package/src/lib/arrow-widget/arrow-widget.component.scss +0 -52
  9. package/src/lib/arrow-widget/arrow-widget.component.ts +0 -78
  10. package/src/lib/arrow-widget/arrow-widget.metadata.ts +0 -3
  11. package/src/lib/clock-widget/analog-clock/analog-clock.component.html +0 -66
  12. package/src/lib/clock-widget/analog-clock/analog-clock.component.scss +0 -103
  13. package/src/lib/clock-widget/analog-clock/analog-clock.component.ts +0 -120
  14. package/src/lib/clock-widget/clock-state-dialog.component.ts +0 -170
  15. package/src/lib/clock-widget/clock-widget.component.html +0 -16
  16. package/src/lib/clock-widget/clock-widget.component.scss +0 -160
  17. package/src/lib/clock-widget/clock-widget.component.ts +0 -87
  18. package/src/lib/clock-widget/clock-widget.metadata.ts +0 -42
  19. package/src/lib/clock-widget/digital-clock/__tests__/digital-clock.component.spec.ts +0 -276
  20. package/src/lib/clock-widget/digital-clock/digital-clock.component.html +0 -1
  21. package/src/lib/clock-widget/digital-clock/digital-clock.component.scss +0 -43
  22. package/src/lib/clock-widget/digital-clock/digital-clock.component.ts +0 -105
  23. package/src/lib/directives/__tests__/responsive-text.directive.spec.ts +0 -906
  24. package/src/lib/directives/responsive-text.directive.ts +0 -334
  25. package/src/lib/label-widget/__tests__/label-widget.component.spec.ts +0 -539
  26. package/src/lib/label-widget/label-state-dialog.component.ts +0 -385
  27. package/src/lib/label-widget/label-widget.component.html +0 -21
  28. package/src/lib/label-widget/label-widget.component.scss +0 -112
  29. package/src/lib/label-widget/label-widget.component.ts +0 -96
  30. package/src/lib/label-widget/label-widget.metadata.ts +0 -3
  31. package/src/public-api.ts +0 -7
  32. package/tsconfig.lib.json +0 -15
  33. package/tsconfig.lib.prod.json +0 -11
  34. package/tsconfig.spec.json +0 -14
@@ -0,0 +1,2251 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, signal, computed, Component, input, numberAttribute, booleanAttribute, ElementRef, NgZone, PLATFORM_ID, DestroyRef, Directive, ChangeDetectionStrategy, Renderer2, viewChild, effect, LOCALE_ID, afterNextRender } from '@angular/core';
3
+ import { DomSanitizer } from '@angular/platform-browser';
4
+ import * as i2 from '@angular/material/dialog';
5
+ import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule, MatDialog } from '@angular/material/dialog';
6
+ import { CommonModule, isPlatformBrowser } from '@angular/common';
7
+ import * as i1 from '@angular/forms';
8
+ import { FormsModule } from '@angular/forms';
9
+ import * as i3 from '@angular/material/button';
10
+ import { MatButtonModule } from '@angular/material/button';
11
+ import * as i4 from '@angular/material/form-field';
12
+ import { MatFormFieldModule } from '@angular/material/form-field';
13
+ import * as i5 from '@angular/material/select';
14
+ import { MatSelectModule } from '@angular/material/select';
15
+ import * as i6 from '@angular/material/slider';
16
+ import { MatSliderModule } from '@angular/material/slider';
17
+ import * as i7 from '@angular/material/slide-toggle';
18
+ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
19
+ import * as i5$1 from '@angular/material/input';
20
+ import { MatInputModule } from '@angular/material/input';
21
+ import * as i3$1 from '@angular/material/radio';
22
+ import { MatRadioModule } from '@angular/material/radio';
23
+ import { from, map, of } from 'rxjs';
24
+ import { toSignal } from '@angular/core/rxjs-interop';
25
+
26
+ // arrow-widget.metadata.ts
27
+ const svgIcon$3 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="M320-120v-320H120l360-440 360 440H640v320H320Zm80-80h160v-320h111L480-754 289-520h111v320Zm80-320Z"/></svg>';
28
+
29
+ class ArrowStateDialogComponent {
30
+ data = inject(MAT_DIALOG_DATA);
31
+ dialogRef = inject((MatDialogRef));
32
+ // State signals
33
+ direction = signal(this.data.direction, ...(ngDevMode ? [{ debugName: "direction" }] : []));
34
+ opacity = signal(this.data.opacity ?? 1, ...(ngDevMode ? [{ debugName: "opacity" }] : []));
35
+ hasBackground = signal(this.data.hasBackground ?? true, ...(ngDevMode ? [{ debugName: "hasBackground" }] : []));
36
+ transparentBackground = signal(!(this.data.hasBackground ?? true), ...(ngDevMode ? [{ debugName: "transparentBackground" }] : []));
37
+ // Store original values for comparison
38
+ originalDirection = this.data.direction;
39
+ originalOpacity = this.data.opacity ?? 1;
40
+ originalHasBackground = this.data.hasBackground ?? true;
41
+ // Computed values
42
+ rotation = computed(() => {
43
+ const rotationMap = {
44
+ up: 0,
45
+ right: 90,
46
+ down: 180,
47
+ left: 270,
48
+ };
49
+ return rotationMap[this.direction()];
50
+ }, ...(ngDevMode ? [{ debugName: "rotation" }] : []));
51
+ rotationTransform = computed(() => `rotate(${this.rotation()}deg)`, ...(ngDevMode ? [{ debugName: "rotationTransform" }] : []));
52
+ directionName = computed(() => {
53
+ const nameMap = {
54
+ up: 'Up',
55
+ right: 'Right',
56
+ down: 'Down',
57
+ left: 'Left',
58
+ };
59
+ return nameMap[this.direction()];
60
+ }, ...(ngDevMode ? [{ debugName: "directionName" }] : []));
61
+ hasChanged = computed(() => this.direction() !== this.originalDirection ||
62
+ this.opacity() !== this.originalOpacity ||
63
+ this.hasBackground() !== this.originalHasBackground, ...(ngDevMode ? [{ debugName: "hasChanged" }] : []));
64
+ formatOpacity(value) {
65
+ return Math.round(value * 100);
66
+ }
67
+ onBackgroundToggle(hasBackground) {
68
+ this.hasBackground.set(hasBackground);
69
+ this.transparentBackground.set(!hasBackground);
70
+ }
71
+ onCancel() {
72
+ this.dialogRef.close();
73
+ }
74
+ save() {
75
+ this.dialogRef.close({
76
+ direction: this.direction(),
77
+ opacity: this.opacity(),
78
+ hasBackground: this.hasBackground(),
79
+ });
80
+ }
81
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ArrowStateDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
82
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.1", type: ArrowStateDialogComponent, isStandalone: true, selector: "lib-arrow-state-dialog", ngImport: i0, template: `
83
+ <h2 mat-dialog-title>Arrow Settings</h2>
84
+ <mat-dialog-content>
85
+ <!-- Direction Selection -->
86
+ <mat-form-field appearance="outline" class="direction-field">
87
+ <mat-label>Arrow Direction</mat-label>
88
+ <mat-select
89
+ [value]="direction()"
90
+ (selectionChange)="direction.set($any($event.value))"
91
+ >
92
+ <mat-option value="up">Up</mat-option>
93
+ <mat-option value="right">Right</mat-option>
94
+ <mat-option value="down">Down</mat-option>
95
+ <mat-option value="left">Left</mat-option>
96
+ </mat-select>
97
+ </mat-form-field>
98
+
99
+ <!-- Opacity Slider -->
100
+ <div class="slider-field">
101
+ <div class="field-label">Opacity: {{ formatOpacity(opacity()) }}%</div>
102
+ <mat-slider [min]="0.1" [max]="1" [step]="0.1">
103
+ <input matSliderThumb [(ngModel)]="opacity" />
104
+ </mat-slider>
105
+ </div>
106
+
107
+ <!-- Background Toggle -->
108
+ <div class="toggle-field">
109
+ <mat-slide-toggle
110
+ [checked]="hasBackground()"
111
+ (change)="onBackgroundToggle($event.checked)">
112
+ Background
113
+ </mat-slide-toggle>
114
+ <span class="toggle-hint">Adds a background behind the arrow</span>
115
+ </div>
116
+ </mat-dialog-content>
117
+
118
+ <mat-dialog-actions align="end">
119
+ <button mat-button (click)="onCancel()">Cancel</button>
120
+ <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
121
+ Save
122
+ </button>
123
+ </mat-dialog-actions>
124
+ `, isInline: true, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden}mat-form-field{width:100%;display:block;margin-bottom:1rem}.direction-field{margin-top:1rem}.slider-field{margin-bottom:1.5rem;margin-right:1rem}.field-label{display:block;margin-bottom:.5rem}mat-slider{width:100%;display:block}.toggle-field{display:flex;align-items:center;gap:.75rem;margin-bottom:.5rem}.toggle-hint{margin:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i5.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSliderModule }, { kind: "component", type: i6.MatSlider, selector: "mat-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "color", "disableRipple", "max", "step", "displayWith"], exportAs: ["matSlider"] }, { kind: "directive", type: i6.MatSliderThumb, selector: "input[matSliderThumb]", inputs: ["value"], outputs: ["valueChange", "dragStart", "dragEnd"], exportAs: ["matSliderThumb"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }] });
125
+ }
126
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ArrowStateDialogComponent, decorators: [{
127
+ type: Component,
128
+ args: [{ selector: 'lib-arrow-state-dialog', standalone: true, imports: [
129
+ CommonModule,
130
+ FormsModule,
131
+ MatDialogModule,
132
+ MatButtonModule,
133
+ MatFormFieldModule,
134
+ MatSelectModule,
135
+ MatSliderModule,
136
+ MatSlideToggleModule,
137
+ ], template: `
138
+ <h2 mat-dialog-title>Arrow Settings</h2>
139
+ <mat-dialog-content>
140
+ <!-- Direction Selection -->
141
+ <mat-form-field appearance="outline" class="direction-field">
142
+ <mat-label>Arrow Direction</mat-label>
143
+ <mat-select
144
+ [value]="direction()"
145
+ (selectionChange)="direction.set($any($event.value))"
146
+ >
147
+ <mat-option value="up">Up</mat-option>
148
+ <mat-option value="right">Right</mat-option>
149
+ <mat-option value="down">Down</mat-option>
150
+ <mat-option value="left">Left</mat-option>
151
+ </mat-select>
152
+ </mat-form-field>
153
+
154
+ <!-- Opacity Slider -->
155
+ <div class="slider-field">
156
+ <div class="field-label">Opacity: {{ formatOpacity(opacity()) }}%</div>
157
+ <mat-slider [min]="0.1" [max]="1" [step]="0.1">
158
+ <input matSliderThumb [(ngModel)]="opacity" />
159
+ </mat-slider>
160
+ </div>
161
+
162
+ <!-- Background Toggle -->
163
+ <div class="toggle-field">
164
+ <mat-slide-toggle
165
+ [checked]="hasBackground()"
166
+ (change)="onBackgroundToggle($event.checked)">
167
+ Background
168
+ </mat-slide-toggle>
169
+ <span class="toggle-hint">Adds a background behind the arrow</span>
170
+ </div>
171
+ </mat-dialog-content>
172
+
173
+ <mat-dialog-actions align="end">
174
+ <button mat-button (click)="onCancel()">Cancel</button>
175
+ <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
176
+ Save
177
+ </button>
178
+ </mat-dialog-actions>
179
+ `, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden}mat-form-field{width:100%;display:block;margin-bottom:1rem}.direction-field{margin-top:1rem}.slider-field{margin-bottom:1.5rem;margin-right:1rem}.field-label{display:block;margin-bottom:.5rem}mat-slider{width:100%;display:block}.toggle-field{display:flex;align-items:center;gap:.75rem;margin-bottom:.5rem}.toggle-hint{margin:0}\n"] }]
180
+ }] });
181
+
182
+ // arrow-widget.component.ts
183
+ class ArrowWidgetComponent {
184
+ static metadata = {
185
+ widgetTypeid: '@ngx-dashboard/arrow-widget',
186
+ name: 'Arrow',
187
+ description: 'A generic arrow',
188
+ svgIcon: svgIcon$3,
189
+ };
190
+ #sanitizer = inject(DomSanitizer);
191
+ #dialog = inject(MatDialog);
192
+ safeSvgIcon = this.#sanitizer.bypassSecurityTrustHtml(svgIcon$3);
193
+ state = signal({
194
+ direction: 'up',
195
+ opacity: 0.3,
196
+ hasBackground: true,
197
+ }, ...(ngDevMode ? [{ debugName: "state" }] : []));
198
+ // Computed rotation
199
+ rotationAngle = computed(() => {
200
+ const rotationMap = {
201
+ up: 0,
202
+ right: 90,
203
+ down: 180,
204
+ left: 270,
205
+ };
206
+ return rotationMap[this.state().direction];
207
+ }, ...(ngDevMode ? [{ debugName: "rotationAngle" }] : []));
208
+ dashboardSetState(state) {
209
+ if (state) {
210
+ this.state.update((current) => ({
211
+ ...current,
212
+ ...state,
213
+ }));
214
+ }
215
+ }
216
+ dashboardGetState() {
217
+ return this.state();
218
+ }
219
+ dashboardEditState() {
220
+ const dialogRef = this.#dialog.open(ArrowStateDialogComponent, {
221
+ data: this.state(),
222
+ width: '400px',
223
+ maxWidth: '90vw',
224
+ disableClose: false,
225
+ autoFocus: false,
226
+ });
227
+ dialogRef.afterClosed().subscribe((result) => {
228
+ if (result) {
229
+ this.state.set(result);
230
+ }
231
+ });
232
+ }
233
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ArrowWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
234
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.1", type: ArrowWidgetComponent, isStandalone: true, selector: "ngx-dashboard-arrow-widget", ngImport: i0, template: "<!-- arrow-widget.component.html -->\r\n<div class=\"svg-wrapper\" [class.has-background]=\"state().hasBackground\">\r\n <div\r\n class=\"svg-placeholder\"\r\n [innerHTML]=\"safeSvgIcon\"\r\n [style.transform]=\"'rotate(' + rotationAngle() + 'deg)'\"\r\n [style.opacity]=\"state().opacity\"\r\n ></div>\r\n</div>\r\n", styles: [":host{display:block;container-type:size;width:100%;height:100%;overflow:hidden}.svg-wrapper{display:flex;align-items:center;justify-content:center;height:100%;width:100%;box-sizing:border-box;transition:background-color var(--mat-sys-motion-duration-medium2) var(--mat-sys-motion-easing-standard)}.svg-wrapper.has-background{background-color:var(--mat-sys-surface-container-high);border-radius:4px}.svg-placeholder{width:min(80cqw,80cqh);aspect-ratio:1/1;opacity:.3;transition:transform .3s ease-in-out,opacity .3s ease,color .2s ease;transform-origin:center center;color:var(--mat-sys-on-surface-variant, #6c757d)}.has-background .svg-placeholder{color:var(--mat-sys-on-surface, #1f1f1f)}.svg-placeholder ::ng-deep svg{width:100%;height:100%;display:block}.svg-wrapper:hover .svg-placeholder{color:var(--mat-sys-primary, #6750a4)}\n"] });
235
+ }
236
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ArrowWidgetComponent, decorators: [{
237
+ type: Component,
238
+ args: [{ selector: 'ngx-dashboard-arrow-widget', imports: [], template: "<!-- arrow-widget.component.html -->\r\n<div class=\"svg-wrapper\" [class.has-background]=\"state().hasBackground\">\r\n <div\r\n class=\"svg-placeholder\"\r\n [innerHTML]=\"safeSvgIcon\"\r\n [style.transform]=\"'rotate(' + rotationAngle() + 'deg)'\"\r\n [style.opacity]=\"state().opacity\"\r\n ></div>\r\n</div>\r\n", styles: [":host{display:block;container-type:size;width:100%;height:100%;overflow:hidden}.svg-wrapper{display:flex;align-items:center;justify-content:center;height:100%;width:100%;box-sizing:border-box;transition:background-color var(--mat-sys-motion-duration-medium2) var(--mat-sys-motion-easing-standard)}.svg-wrapper.has-background{background-color:var(--mat-sys-surface-container-high);border-radius:4px}.svg-placeholder{width:min(80cqw,80cqh);aspect-ratio:1/1;opacity:.3;transition:transform .3s ease-in-out,opacity .3s ease,color .2s ease;transform-origin:center center;color:var(--mat-sys-on-surface-variant, #6c757d)}.has-background .svg-placeholder{color:var(--mat-sys-on-surface, #1f1f1f)}.svg-placeholder ::ng-deep svg{width:100%;height:100%;display:block}.svg-wrapper:hover .svg-placeholder{color:var(--mat-sys-primary, #6750a4)}\n"] }]
239
+ }] });
240
+
241
+ // label-widget.metadata.ts
242
+ const svgIcon$2 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="M280-280h280v-80H280v80Zm0-160h400v-80H280v80Zm0-160h400v-80H280v80Zm-80 480q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm0-560v560-560Z"/></svg>';
243
+
244
+ class LabelStateDialogComponent {
245
+ data = inject(MAT_DIALOG_DATA);
246
+ dialogRef = inject((MatDialogRef));
247
+ // State signals
248
+ label = signal(this.data.label ?? '', ...(ngDevMode ? [{ debugName: "label" }] : []));
249
+ fontSize = signal(this.data.fontSize ?? 16, ...(ngDevMode ? [{ debugName: "fontSize" }] : []));
250
+ alignment = signal(this.data.alignment ?? 'center', ...(ngDevMode ? [{ debugName: "alignment" }] : []));
251
+ fontWeight = signal(this.data.fontWeight ?? 'normal', ...(ngDevMode ? [{ debugName: "fontWeight" }] : []));
252
+ opacity = signal(this.data.opacity ?? 1, ...(ngDevMode ? [{ debugName: "opacity" }] : []));
253
+ hasBackground = signal(this.data.hasBackground ?? true, ...(ngDevMode ? [{ debugName: "hasBackground" }] : []));
254
+ transparentBackground = signal(!(this.data.hasBackground ?? true), ...(ngDevMode ? [{ debugName: "transparentBackground" }] : []));
255
+ responsive = signal(this.data.responsive ?? false, ...(ngDevMode ? [{ debugName: "responsive" }] : []));
256
+ // Responsive font size constraints
257
+ minFontSize = signal(this.data.minFontSize ?? 8, ...(ngDevMode ? [{ debugName: "minFontSize" }] : []));
258
+ maxFontSize = signal(this.data.maxFontSize ?? 64, ...(ngDevMode ? [{ debugName: "maxFontSize" }] : []));
259
+ // Store original values for comparison
260
+ originalLabel = this.data.label ?? '';
261
+ originalFontSize = this.data.fontSize ?? 16;
262
+ originalAlignment = this.data.alignment ?? 'center';
263
+ originalFontWeight = this.data.fontWeight ?? 'normal';
264
+ originalOpacity = this.data.opacity ?? 1;
265
+ originalHasBackground = this.data.hasBackground ?? true;
266
+ originalResponsive = this.data.responsive ?? false;
267
+ originalMinFontSize = this.data.minFontSize ?? 8;
268
+ originalMaxFontSize = this.data.maxFontSize ?? 64;
269
+ // Validation computed properties
270
+ isMinFontSizeValid = computed(() => {
271
+ const min = this.minFontSize();
272
+ return min >= 8 && min <= 24;
273
+ }, ...(ngDevMode ? [{ debugName: "isMinFontSizeValid" }] : []));
274
+ isMaxFontSizeValid = computed(() => {
275
+ const max = this.maxFontSize();
276
+ return max >= 16 && max <= 128;
277
+ }, ...(ngDevMode ? [{ debugName: "isMaxFontSizeValid" }] : []));
278
+ isFontSizeRangeValid = computed(() => this.minFontSize() < this.maxFontSize(), ...(ngDevMode ? [{ debugName: "isFontSizeRangeValid" }] : []));
279
+ isFormValid = computed(() => this.isMinFontSizeValid() &&
280
+ this.isMaxFontSizeValid() &&
281
+ this.isFontSizeRangeValid(), ...(ngDevMode ? [{ debugName: "isFormValid" }] : []));
282
+ // Computed values
283
+ hasChanged = computed(() => this.label() !== this.originalLabel ||
284
+ this.fontSize() !== this.originalFontSize ||
285
+ this.alignment() !== this.originalAlignment ||
286
+ this.fontWeight() !== this.originalFontWeight ||
287
+ this.opacity() !== this.originalOpacity ||
288
+ this.hasBackground() !== this.originalHasBackground ||
289
+ this.responsive() !== this.originalResponsive ||
290
+ this.minFontSize() !== this.originalMinFontSize ||
291
+ this.maxFontSize() !== this.originalMaxFontSize, ...(ngDevMode ? [{ debugName: "hasChanged" }] : []));
292
+ formatOpacity(value) {
293
+ return Math.round(value * 100);
294
+ }
295
+ formatOpacitySlider = (value) => {
296
+ return `${Math.round(value * 100)}%`;
297
+ };
298
+ // Validation methods with robust min < max enforcement
299
+ validateAndCorrectMinFontSize(value) {
300
+ // Clamp to valid range
301
+ const corrected = Math.max(8, Math.min(24, value));
302
+ this.minFontSize.set(corrected);
303
+ // Ensure min < max with adequate gap
304
+ if (corrected >= this.maxFontSize()) {
305
+ const newMax = Math.min(128, corrected + 8); // Ensure at least 8px gap
306
+ this.maxFontSize.set(newMax);
307
+ }
308
+ }
309
+ validateAndCorrectMaxFontSize(value) {
310
+ // Clamp to valid range
311
+ const corrected = Math.max(16, Math.min(128, value));
312
+ this.maxFontSize.set(corrected);
313
+ // Ensure min < max with adequate gap
314
+ if (corrected <= this.minFontSize()) {
315
+ const newMin = Math.max(8, corrected - 8); // Ensure at least 8px gap
316
+ this.minFontSize.set(newMin);
317
+ }
318
+ }
319
+ onBackgroundToggle(hasWhiteBackground) {
320
+ this.hasBackground.set(hasWhiteBackground);
321
+ this.transparentBackground.set(!hasWhiteBackground);
322
+ }
323
+ onCancel() {
324
+ this.dialogRef.close();
325
+ }
326
+ save() {
327
+ this.dialogRef.close({
328
+ label: this.label(),
329
+ fontSize: this.fontSize(),
330
+ alignment: this.alignment(),
331
+ fontWeight: this.fontWeight(),
332
+ opacity: this.opacity(),
333
+ hasBackground: this.hasBackground(),
334
+ responsive: this.responsive(),
335
+ minFontSize: this.minFontSize(),
336
+ maxFontSize: this.maxFontSize(),
337
+ });
338
+ }
339
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: LabelStateDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
340
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: LabelStateDialogComponent, isStandalone: true, selector: "lib-label-state-dialog", ngImport: i0, template: `
341
+ <h2 mat-dialog-title>Label Settings</h2>
342
+ <mat-dialog-content>
343
+ <mat-form-field appearance="outline" class="label-text-field">
344
+ <mat-label>Label Text</mat-label>
345
+ <input
346
+ matInput
347
+ type="text"
348
+ [value]="label()"
349
+ (input)="label.set($any($event.target).value)"
350
+ placeholder="Enter your label text..."
351
+ />
352
+ </mat-form-field>
353
+
354
+ <!-- Responsive Text Toggle -->
355
+ <div class="toggle-section">
356
+ <mat-slide-toggle
357
+ [checked]="responsive()"
358
+ (change)="responsive.set($event.checked)">
359
+ Responsive Text
360
+ </mat-slide-toggle>
361
+ <span class="toggle-description"
362
+ >Automatically adjust text size to fit the widget</span
363
+ >
364
+ </div>
365
+
366
+ <!-- Responsive Font Size Constraints (only shown when responsive is enabled) -->
367
+ @if (responsive()) {
368
+ <div class="responsive-section">
369
+ <div class="section-label">Font Size Limits</div>
370
+ <div class="row-layout">
371
+ <mat-form-field appearance="outline">
372
+ <mat-label>Min Size (px)</mat-label>
373
+ <input
374
+ matInput
375
+ type="number"
376
+ [value]="minFontSize()"
377
+ (input)="validateAndCorrectMinFontSize(+$any($event.target).value)"
378
+ (blur)="validateAndCorrectMinFontSize(minFontSize())"
379
+ min="8"
380
+ max="24"
381
+ placeholder="8"
382
+ />
383
+ @if (!isMinFontSizeValid() || !isFontSizeRangeValid()) {
384
+ <mat-error>
385
+ @if (!isMinFontSizeValid()) {
386
+ Must be between 8-24px
387
+ } @else {
388
+ Must be less than max size
389
+ }
390
+ </mat-error>
391
+ } @else {
392
+ <mat-hint>8-24px range</mat-hint>
393
+ }
394
+ </mat-form-field>
395
+
396
+ <mat-form-field appearance="outline">
397
+ <mat-label>Max Size (px)</mat-label>
398
+ <input
399
+ matInput
400
+ type="number"
401
+ [value]="maxFontSize()"
402
+ (input)="validateAndCorrectMaxFontSize(+$any($event.target).value)"
403
+ (blur)="validateAndCorrectMaxFontSize(maxFontSize())"
404
+ min="16"
405
+ max="128"
406
+ placeholder="64"
407
+ />
408
+ @if (!isMaxFontSizeValid() || !isFontSizeRangeValid()) {
409
+ <mat-error>
410
+ @if (!isMaxFontSizeValid()) {
411
+ Must be between 16-128px
412
+ } @else {
413
+ Must be greater than min size
414
+ }
415
+ </mat-error>
416
+ } @else {
417
+ <mat-hint>16-128px range</mat-hint>
418
+ }
419
+ </mat-form-field>
420
+ </div>
421
+ </div>
422
+ }
423
+
424
+ <div class="row-layout">
425
+ <mat-form-field appearance="outline">
426
+ <mat-label>Font Size (px)</mat-label>
427
+ <input
428
+ matInput
429
+ type="number"
430
+ [value]="fontSize()"
431
+ (input)="fontSize.set(+$any($event.target).value)"
432
+ [disabled]="responsive()"
433
+ min="8"
434
+ max="48"
435
+ placeholder="16"
436
+ />
437
+ </mat-form-field>
438
+
439
+ <mat-form-field appearance="outline">
440
+ <mat-label>Alignment</mat-label>
441
+ <mat-select
442
+ [value]="alignment()"
443
+ (selectionChange)="alignment.set($any($event.value))"
444
+ >
445
+ <mat-option value="left">Left</mat-option>
446
+ <mat-option value="center">Center</mat-option>
447
+ <mat-option value="right">Right</mat-option>
448
+ </mat-select>
449
+ </mat-form-field>
450
+ </div>
451
+
452
+ <mat-form-field appearance="outline">
453
+ <mat-label>Font Weight</mat-label>
454
+ <mat-select
455
+ [value]="fontWeight()"
456
+ (selectionChange)="fontWeight.set($any($event.value))"
457
+ >
458
+ <mat-option value="normal">Normal</mat-option>
459
+ <mat-option value="bold">Bold</mat-option>
460
+ </mat-select>
461
+ </mat-form-field>
462
+
463
+ <!-- Opacity Slider -->
464
+ <div class="slider-section">
465
+ <div class="slider-label">Opacity: {{ formatOpacity(opacity()) }}%</div>
466
+ <mat-slider [min]="0.1" [max]="1" [step]="0.1">
467
+ <input matSliderThumb [(ngModel)]="opacity" />
468
+ </mat-slider>
469
+ </div>
470
+
471
+ <!-- Background Toggle -->
472
+ <div class="toggle-section">
473
+ <mat-slide-toggle
474
+ [checked]="!transparentBackground()"
475
+ (change)="onBackgroundToggle($event.checked)">
476
+ Background
477
+ </mat-slide-toggle>
478
+ <span class="toggle-description"
479
+ >Adds a background behind the text</span
480
+ >
481
+ </div>
482
+ </mat-dialog-content>
483
+
484
+ <mat-dialog-actions align="end">
485
+ <button mat-button (click)="onCancel()">Cancel</button>
486
+ <button mat-flat-button (click)="save()" [disabled]="!hasChanged() || !isFormValid()">
487
+ Save
488
+ </button>
489
+ </mat-dialog-actions>
490
+ `, isInline: true, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden}mat-form-field{width:100%;display:block;margin-bottom:1rem}.label-text-field{margin-top:1rem}.row-layout{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem}.row-layout mat-form-field{margin-bottom:0}.slider-section{margin-bottom:1.5rem;margin-right:1rem}.slider-label{display:block;margin-bottom:.5rem}mat-slider{width:100%;display:block}.toggle-section{display:flex;align-items:center;gap:.75rem;margin-bottom:1rem}.toggle-description{margin:0}.responsive-section{margin-bottom:1.5rem;padding:1rem;border-radius:12px;background-color:var(--mat-app-surface-variant, rgba(var(--mat-app-on-surface-rgb, 0, 0, 0), .05));border:1px solid var(--mat-app-outline-variant, rgba(var(--mat-app-on-surface-rgb, 0, 0, 0), .12))}.section-label{display:block;margin-bottom:.75rem;font-weight:500;color:var(--mat-app-on-surface-variant, rgba(var(--mat-app-on-surface-rgb, 0, 0, 0), .6))}.responsive-section .row-layout{margin-bottom:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "directive", type: i4.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i4.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i5.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i5.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSliderModule }, { kind: "component", type: i6.MatSlider, selector: "mat-slider", inputs: ["disabled", "discrete", "showTickMarks", "min", "color", "disableRipple", "max", "step", "displayWith"], exportAs: ["matSlider"] }, { kind: "directive", type: i6.MatSliderThumb, selector: "input[matSliderThumb]", inputs: ["value"], outputs: ["valueChange", "dragStart", "dragEnd"], exportAs: ["matSliderThumb"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }] });
491
+ }
492
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: LabelStateDialogComponent, decorators: [{
493
+ type: Component,
494
+ args: [{ selector: 'lib-label-state-dialog', standalone: true, imports: [
495
+ CommonModule,
496
+ FormsModule,
497
+ MatDialogModule,
498
+ MatButtonModule,
499
+ MatFormFieldModule,
500
+ MatInputModule,
501
+ MatSelectModule,
502
+ MatSliderModule,
503
+ MatSlideToggleModule, // Add this import
504
+ ], template: `
505
+ <h2 mat-dialog-title>Label Settings</h2>
506
+ <mat-dialog-content>
507
+ <mat-form-field appearance="outline" class="label-text-field">
508
+ <mat-label>Label Text</mat-label>
509
+ <input
510
+ matInput
511
+ type="text"
512
+ [value]="label()"
513
+ (input)="label.set($any($event.target).value)"
514
+ placeholder="Enter your label text..."
515
+ />
516
+ </mat-form-field>
517
+
518
+ <!-- Responsive Text Toggle -->
519
+ <div class="toggle-section">
520
+ <mat-slide-toggle
521
+ [checked]="responsive()"
522
+ (change)="responsive.set($event.checked)">
523
+ Responsive Text
524
+ </mat-slide-toggle>
525
+ <span class="toggle-description"
526
+ >Automatically adjust text size to fit the widget</span
527
+ >
528
+ </div>
529
+
530
+ <!-- Responsive Font Size Constraints (only shown when responsive is enabled) -->
531
+ @if (responsive()) {
532
+ <div class="responsive-section">
533
+ <div class="section-label">Font Size Limits</div>
534
+ <div class="row-layout">
535
+ <mat-form-field appearance="outline">
536
+ <mat-label>Min Size (px)</mat-label>
537
+ <input
538
+ matInput
539
+ type="number"
540
+ [value]="minFontSize()"
541
+ (input)="validateAndCorrectMinFontSize(+$any($event.target).value)"
542
+ (blur)="validateAndCorrectMinFontSize(minFontSize())"
543
+ min="8"
544
+ max="24"
545
+ placeholder="8"
546
+ />
547
+ @if (!isMinFontSizeValid() || !isFontSizeRangeValid()) {
548
+ <mat-error>
549
+ @if (!isMinFontSizeValid()) {
550
+ Must be between 8-24px
551
+ } @else {
552
+ Must be less than max size
553
+ }
554
+ </mat-error>
555
+ } @else {
556
+ <mat-hint>8-24px range</mat-hint>
557
+ }
558
+ </mat-form-field>
559
+
560
+ <mat-form-field appearance="outline">
561
+ <mat-label>Max Size (px)</mat-label>
562
+ <input
563
+ matInput
564
+ type="number"
565
+ [value]="maxFontSize()"
566
+ (input)="validateAndCorrectMaxFontSize(+$any($event.target).value)"
567
+ (blur)="validateAndCorrectMaxFontSize(maxFontSize())"
568
+ min="16"
569
+ max="128"
570
+ placeholder="64"
571
+ />
572
+ @if (!isMaxFontSizeValid() || !isFontSizeRangeValid()) {
573
+ <mat-error>
574
+ @if (!isMaxFontSizeValid()) {
575
+ Must be between 16-128px
576
+ } @else {
577
+ Must be greater than min size
578
+ }
579
+ </mat-error>
580
+ } @else {
581
+ <mat-hint>16-128px range</mat-hint>
582
+ }
583
+ </mat-form-field>
584
+ </div>
585
+ </div>
586
+ }
587
+
588
+ <div class="row-layout">
589
+ <mat-form-field appearance="outline">
590
+ <mat-label>Font Size (px)</mat-label>
591
+ <input
592
+ matInput
593
+ type="number"
594
+ [value]="fontSize()"
595
+ (input)="fontSize.set(+$any($event.target).value)"
596
+ [disabled]="responsive()"
597
+ min="8"
598
+ max="48"
599
+ placeholder="16"
600
+ />
601
+ </mat-form-field>
602
+
603
+ <mat-form-field appearance="outline">
604
+ <mat-label>Alignment</mat-label>
605
+ <mat-select
606
+ [value]="alignment()"
607
+ (selectionChange)="alignment.set($any($event.value))"
608
+ >
609
+ <mat-option value="left">Left</mat-option>
610
+ <mat-option value="center">Center</mat-option>
611
+ <mat-option value="right">Right</mat-option>
612
+ </mat-select>
613
+ </mat-form-field>
614
+ </div>
615
+
616
+ <mat-form-field appearance="outline">
617
+ <mat-label>Font Weight</mat-label>
618
+ <mat-select
619
+ [value]="fontWeight()"
620
+ (selectionChange)="fontWeight.set($any($event.value))"
621
+ >
622
+ <mat-option value="normal">Normal</mat-option>
623
+ <mat-option value="bold">Bold</mat-option>
624
+ </mat-select>
625
+ </mat-form-field>
626
+
627
+ <!-- Opacity Slider -->
628
+ <div class="slider-section">
629
+ <div class="slider-label">Opacity: {{ formatOpacity(opacity()) }}%</div>
630
+ <mat-slider [min]="0.1" [max]="1" [step]="0.1">
631
+ <input matSliderThumb [(ngModel)]="opacity" />
632
+ </mat-slider>
633
+ </div>
634
+
635
+ <!-- Background Toggle -->
636
+ <div class="toggle-section">
637
+ <mat-slide-toggle
638
+ [checked]="!transparentBackground()"
639
+ (change)="onBackgroundToggle($event.checked)">
640
+ Background
641
+ </mat-slide-toggle>
642
+ <span class="toggle-description"
643
+ >Adds a background behind the text</span
644
+ >
645
+ </div>
646
+ </mat-dialog-content>
647
+
648
+ <mat-dialog-actions align="end">
649
+ <button mat-button (click)="onCancel()">Cancel</button>
650
+ <button mat-flat-button (click)="save()" [disabled]="!hasChanged() || !isFormValid()">
651
+ Save
652
+ </button>
653
+ </mat-dialog-actions>
654
+ `, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden}mat-form-field{width:100%;display:block;margin-bottom:1rem}.label-text-field{margin-top:1rem}.row-layout{display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-bottom:1rem}.row-layout mat-form-field{margin-bottom:0}.slider-section{margin-bottom:1.5rem;margin-right:1rem}.slider-label{display:block;margin-bottom:.5rem}mat-slider{width:100%;display:block}.toggle-section{display:flex;align-items:center;gap:.75rem;margin-bottom:1rem}.toggle-description{margin:0}.responsive-section{margin-bottom:1.5rem;padding:1rem;border-radius:12px;background-color:var(--mat-app-surface-variant, rgba(var(--mat-app-on-surface-rgb, 0, 0, 0), .05));border:1px solid var(--mat-app-outline-variant, rgba(var(--mat-app-on-surface-rgb, 0, 0, 0), .12))}.section-label{display:block;margin-bottom:.75rem;font-weight:500;color:var(--mat-app-on-surface-variant, rgba(var(--mat-app-on-surface-rgb, 0, 0, 0), .6))}.responsive-section .row-layout{margin-bottom:0}\n"] }]
655
+ }] });
656
+
657
+ /**
658
+ * Directive that automatically adjusts font size to fit text within its parent container.
659
+ * Uses canvas-based measurement for performance and DOM verification for accuracy.
660
+ *
661
+ * @example
662
+ * <div class="container">
663
+ * <span responsiveText [minFontSize]="12" [maxFontSize]="72">Dynamic text here</span>
664
+ * </div>
665
+ */
666
+ class ResponsiveTextDirective {
667
+ /* ───────────────────────── Inputs with transforms ─────────────── */
668
+ /** Minimum font-size in pixels (accessibility floor) */
669
+ minFontSize = input(8, ...(ngDevMode ? [{ debugName: "minFontSize", transform: numberAttribute }] : [{ transform: numberAttribute }]));
670
+ /** Maximum font-size in pixels (layout ceiling) */
671
+ maxFontSize = input(512, ...(ngDevMode ? [{ debugName: "maxFontSize", transform: numberAttribute }] : [{ transform: numberAttribute }]));
672
+ /**
673
+ * Line-height: pass a multiplier (e.g. 1.1) or absolute px value.
674
+ * For single-line text a multiplier < 10 is treated as unitless.
675
+ */
676
+ lineHeight = input(1.1, ...(ngDevMode ? [{ debugName: "lineHeight", transform: numberAttribute }] : [{ transform: numberAttribute }]));
677
+ /** Whether to observe text mutations after first render */
678
+ observeMutations = input(true, ...(ngDevMode ? [{ debugName: "observeMutations", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
679
+ /** Debounce delay in ms for resize/mutation callbacks */
680
+ debounceMs = input(16, ...(ngDevMode ? [{ debugName: "debounceMs", transform: numberAttribute }] : [{ transform: numberAttribute }]));
681
+ /* ───────────────────────── Private state ───────────────────────── */
682
+ el = inject(ElementRef);
683
+ zone = inject(NgZone);
684
+ platformId = inject(PLATFORM_ID);
685
+ destroyRef = inject(DestroyRef);
686
+ // Canvas context - lazy initialization
687
+ _ctx;
688
+ get ctx() {
689
+ if (!this._ctx) {
690
+ const canvas = document.createElement('canvas');
691
+ this._ctx = canvas.getContext('2d', {
692
+ willReadFrequently: true,
693
+ alpha: false,
694
+ });
695
+ }
696
+ return this._ctx;
697
+ }
698
+ ro;
699
+ mo;
700
+ fitTimeout;
701
+ // Cache for performance
702
+ lastText = '';
703
+ lastMaxW = 0;
704
+ lastMaxH = 0;
705
+ lastFontSize = 0;
706
+ constructor() {
707
+ // Set up cleanup on component destruction using modern DestroyRef
708
+ this.destroyRef.onDestroy(() => {
709
+ this.cleanup();
710
+ });
711
+ }
712
+ ngAfterViewInit() {
713
+ if (!isPlatformBrowser(this.platformId))
714
+ return;
715
+ // Set initial styles
716
+ const span = this.el.nativeElement;
717
+ span.style.transition = 'font-size 0.1s ease-out';
718
+ // All observer callbacks run outside Angular's zone
719
+ this.zone.runOutsideAngular(() => {
720
+ this.fit();
721
+ this.observeResize();
722
+ if (this.observeMutations()) {
723
+ this.observeText();
724
+ }
725
+ });
726
+ }
727
+ /* ───────────────────── Core fitting logic ───────────────────── */
728
+ /**
729
+ * Debounced fit handler to prevent excessive recalculations
730
+ */
731
+ requestFit = () => {
732
+ if (this.fitTimeout) {
733
+ cancelAnimationFrame(this.fitTimeout);
734
+ }
735
+ this.fitTimeout = requestAnimationFrame(() => {
736
+ this.fit();
737
+ });
738
+ };
739
+ /**
740
+ * Recalculate & apply the ideal font-size
741
+ */
742
+ fit = () => {
743
+ const span = this.el.nativeElement;
744
+ const parent = span.parentElement;
745
+ if (!parent)
746
+ return;
747
+ const text = span.textContent?.trim() || '';
748
+ if (!text) {
749
+ span.style.fontSize = `${this.minFontSize()}px`;
750
+ return;
751
+ }
752
+ const { maxW, maxH } = this.getAvailableSpace(parent);
753
+ // Check cache to avoid redundant calculations
754
+ if (text === this.lastText &&
755
+ maxW === this.lastMaxW &&
756
+ maxH === this.lastMaxH &&
757
+ this.lastFontSize > 0) {
758
+ return;
759
+ }
760
+ // Calculate with conservative buffer for sub-pixel accuracy
761
+ const ideal = this.calcFit(text, maxW * 0.98, maxH * 0.98);
762
+ span.style.fontSize = `${ideal}px`;
763
+ // DOM verification pass
764
+ this.verifyFit(span, maxW, maxH, ideal);
765
+ // Update cache
766
+ this.lastText = text;
767
+ this.lastMaxW = maxW;
768
+ this.lastMaxH = maxH;
769
+ this.lastFontSize = parseFloat(span.style.fontSize);
770
+ };
771
+ /**
772
+ * Calculate available space accounting for padding and borders
773
+ */
774
+ getAvailableSpace(parent) {
775
+ const cs = getComputedStyle(parent);
776
+ const maxW = parent.clientWidth -
777
+ parseFloat(cs.paddingLeft) -
778
+ parseFloat(cs.paddingRight);
779
+ const maxH = parent.clientHeight -
780
+ parseFloat(cs.paddingTop) -
781
+ parseFloat(cs.paddingBottom);
782
+ return { maxW: Math.max(0, maxW), maxH: Math.max(0, maxH) };
783
+ }
784
+ /**
785
+ * DOM-based verification to handle sub-pixel discrepancies
786
+ */
787
+ verifyFit(span, maxW, maxH, ideal) {
788
+ // Simple synchronous verification
789
+ if (span.scrollWidth > maxW || span.scrollHeight > maxH) {
790
+ let safe = ideal;
791
+ let iterations = 0;
792
+ const maxIterations = 10;
793
+ while (iterations < maxIterations &&
794
+ safe > this.minFontSize() &&
795
+ (span.scrollWidth > maxW || span.scrollHeight > maxH)) {
796
+ safe -= 0.25;
797
+ span.style.fontSize = `${safe}px`;
798
+ iterations++;
799
+ }
800
+ // Update cache with verified size
801
+ this.lastFontSize = safe;
802
+ }
803
+ }
804
+ /* ───────────────────── Binary search algorithm ────────────────── */
805
+ /**
806
+ * Binary search for optimal font size using canvas measurements
807
+ */
808
+ calcFit(text, maxW, maxH, precision = 0.1) {
809
+ if (maxW <= 0 || maxH <= 0)
810
+ return this.minFontSize();
811
+ const computedStyle = getComputedStyle(this.el.nativeElement);
812
+ const fontFamily = computedStyle.fontFamily || 'sans-serif';
813
+ const fontWeight = computedStyle.fontWeight || '400';
814
+ let lo = this.minFontSize();
815
+ let hi = this.maxFontSize();
816
+ let bestFit = this.minFontSize();
817
+ while (hi - lo > precision) {
818
+ const mid = (hi + lo) / 2;
819
+ this.ctx.font = `${fontWeight} ${mid}px ${fontFamily}`;
820
+ const metrics = this.ctx.measureText(text);
821
+ const width = metrics.width;
822
+ // Calculate height based on available metrics
823
+ const height = this.calculateTextHeight(metrics, mid);
824
+ if (width <= maxW && height <= maxH) {
825
+ bestFit = mid;
826
+ lo = mid;
827
+ }
828
+ else {
829
+ hi = mid;
830
+ }
831
+ }
832
+ return Math.floor(bestFit * 100) / 100;
833
+ }
834
+ /**
835
+ * Calculate text height from metrics
836
+ */
837
+ calculateTextHeight(metrics, fontSize) {
838
+ // Use font bounding box metrics if available
839
+ if (metrics.fontBoundingBoxAscent && metrics.fontBoundingBoxDescent) {
840
+ return metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
841
+ }
842
+ // Fallback to actual bounding box
843
+ if (metrics.actualBoundingBoxAscent && metrics.actualBoundingBoxDescent) {
844
+ return metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
845
+ }
846
+ // Final fallback using line height
847
+ return this.lineHeight() < 10
848
+ ? fontSize * this.lineHeight()
849
+ : this.lineHeight();
850
+ }
851
+ /* ───────────────────────── Observers ─────────────────────────── */
852
+ /**
853
+ * Observe parent container resizes
854
+ */
855
+ observeResize() {
856
+ if (!('ResizeObserver' in window))
857
+ return;
858
+ this.ro = new ResizeObserver((entries) => {
859
+ // Only trigger if size actually changed
860
+ const entry = entries[0];
861
+ if (entry?.contentRect) {
862
+ this.requestFit();
863
+ }
864
+ });
865
+ const parent = this.el.nativeElement.parentElement;
866
+ if (parent) {
867
+ this.ro.observe(parent);
868
+ }
869
+ }
870
+ /**
871
+ * Observe text content changes
872
+ */
873
+ observeText() {
874
+ if (!('MutationObserver' in window))
875
+ return;
876
+ this.mo = new MutationObserver((mutations) => {
877
+ // Check if text actually changed
878
+ const hasTextChange = mutations.some((m) => m.type === 'characterData' ||
879
+ (m.type === 'childList' &&
880
+ (m.addedNodes.length > 0 || m.removedNodes.length > 0)));
881
+ if (hasTextChange) {
882
+ this.requestFit();
883
+ }
884
+ });
885
+ this.mo.observe(this.el.nativeElement, {
886
+ characterData: true,
887
+ childList: true,
888
+ subtree: true,
889
+ });
890
+ }
891
+ /**
892
+ * Cleanup resources
893
+ */
894
+ cleanup() {
895
+ this.ro?.disconnect();
896
+ this.mo?.disconnect();
897
+ if (this.fitTimeout) {
898
+ cancelAnimationFrame(this.fitTimeout);
899
+ }
900
+ // Clear canvas context
901
+ this._ctx = undefined;
902
+ }
903
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ResponsiveTextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
904
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.2.1", type: ResponsiveTextDirective, isStandalone: true, selector: "[libResponsiveText]", inputs: { minFontSize: { classPropertyName: "minFontSize", publicName: "minFontSize", isSignal: true, isRequired: false, transformFunction: null }, maxFontSize: { classPropertyName: "maxFontSize", publicName: "maxFontSize", isSignal: true, isRequired: false, transformFunction: null }, lineHeight: { classPropertyName: "lineHeight", publicName: "lineHeight", isSignal: true, isRequired: false, transformFunction: null }, observeMutations: { classPropertyName: "observeMutations", publicName: "observeMutations", isSignal: true, isRequired: false, transformFunction: null }, debounceMs: { classPropertyName: "debounceMs", publicName: "debounceMs", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.display": "\"block\"", "style.width": "\"100%\"", "style.white-space": "\"nowrap\"", "style.overflow": "\"visible\"" } }, ngImport: i0 });
905
+ }
906
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ResponsiveTextDirective, decorators: [{
907
+ type: Directive,
908
+ args: [{
909
+ selector: '[libResponsiveText]',
910
+ standalone: true,
911
+ host: {
912
+ '[style.display]': '"block"',
913
+ '[style.width]': '"100%"',
914
+ '[style.white-space]': '"nowrap"',
915
+ '[style.overflow]': '"visible"',
916
+ },
917
+ }]
918
+ }], ctorParameters: () => [] });
919
+
920
+ // label-widget.component.ts
921
+ class LabelWidgetComponent {
922
+ static metadata = {
923
+ widgetTypeid: '@ngx-dashboard/label-widget',
924
+ name: 'Label',
925
+ description: 'A generic text label',
926
+ svgIcon: svgIcon$2,
927
+ };
928
+ #sanitizer = inject(DomSanitizer);
929
+ #dialog = inject(MatDialog);
930
+ safeSvgIcon = this.#sanitizer.bypassSecurityTrustHtml(svgIcon$2);
931
+ state = signal({
932
+ label: '',
933
+ fontSize: 16,
934
+ alignment: 'center',
935
+ fontWeight: 'normal',
936
+ opacity: 1,
937
+ hasBackground: true,
938
+ responsive: false,
939
+ minFontSize: 8, // Accessible minimum for responsive text
940
+ maxFontSize: 64, // Practical maximum for widget display
941
+ }, ...(ngDevMode ? [{ debugName: "state" }] : []));
942
+ dashboardSetState(state) {
943
+ if (state) {
944
+ this.state.update((current) => ({
945
+ ...current,
946
+ ...state,
947
+ }));
948
+ }
949
+ }
950
+ dashboardGetState() {
951
+ return { ...this.state() };
952
+ }
953
+ dashboardEditState() {
954
+ const dialogRef = this.#dialog.open(LabelStateDialogComponent, {
955
+ data: this.dashboardGetState(),
956
+ width: '400px',
957
+ maxWidth: '90vw',
958
+ disableClose: false,
959
+ autoFocus: false,
960
+ });
961
+ dialogRef
962
+ .afterClosed()
963
+ .subscribe((result) => {
964
+ if (result) {
965
+ this.state.set(result);
966
+ }
967
+ });
968
+ }
969
+ get hasContent() {
970
+ return !!this.state().label?.trim();
971
+ }
972
+ get label() {
973
+ return this.state().label?.trim();
974
+ }
975
+ // Computed properties for responsive font size limits with fallbacks
976
+ minFontSize = computed(() => this.state().minFontSize ?? 8, ...(ngDevMode ? [{ debugName: "minFontSize" }] : []));
977
+ maxFontSize = computed(() => this.state().maxFontSize ?? 64, ...(ngDevMode ? [{ debugName: "maxFontSize" }] : []));
978
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: LabelWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
979
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: LabelWidgetComponent, isStandalone: true, selector: "ngx-dashboard-label-widget", ngImport: i0, template: "@if (hasContent) {\r\n<div\r\n class=\"label-widget\"\r\n [style.fontSize.rem]=\"state().responsive ? null : state().fontSize! / 16\"\r\n [style.--widget-opacity]=\"state().opacity\"\r\n [class.text-left]=\"state().alignment === 'left'\"\r\n [class.text-right]=\"state().alignment === 'right'\"\r\n [class.font-bold]=\"state().fontWeight === 'bold'\"\r\n [class.has-background]=\"state().hasBackground\"\r\n>\r\n @if (state().responsive) {\r\n <div class=\"label-text\" libResponsiveText [minFontSize]=\"minFontSize()\" [maxFontSize]=\"maxFontSize()\">{{ label }}</div>\r\n } @else {\r\n <div class=\"label-text\">{{ label }}</div>\r\n }\r\n</div>\r\n} @else {\r\n<div class=\"svg-wrapper\" [class.has-background]=\"state().hasBackground\">\r\n <div class=\"svg-placeholder\" [innerHTML]=\"safeSvgIcon\"></div>\r\n</div>\r\n}\r\n", styles: [":host{display:block;container-type:size;width:100%;height:100%;overflow:hidden}.svg-wrapper,.label-widget{display:flex;align-items:center;justify-content:center;height:100%;width:100%;box-sizing:border-box;transition:background-color var(--mat-sys-motion-duration-medium2) var(--mat-sys-motion-easing-standard)}.has-background.svg-wrapper,.has-background.label-widget{background-color:var(--mat-sys-surface-container-high);border-radius:4px}.label-widget{overflow:hidden;container-type:size;padding:var(--mat-sys-spacing-4);color:var(--mat-sys-on-surface-variant, #6c757d);opacity:var(--widget-opacity, 1)}.label-widget.text-left{justify-content:flex-start}.label-widget.text-right{justify-content:flex-end}.label-widget.has-background{color:var(--mat-sys-on-surface, #1f1f1f)}.label-widget:hover{opacity:.3;color:var(--mat-sys-primary, #6750a4)}.label-text{width:100%;text-align:center;overflow-wrap:break-word;transition:color .2s ease}.text-left .label-text{text-align:left}.text-right .label-text{text-align:right}.font-bold .label-text{font-weight:700}.label-text[responsiveText]{overflow-wrap:normal}.svg-wrapper{overflow:hidden}.svg-placeholder{width:min(80cqw,80cqh);aspect-ratio:1/1;opacity:.3;transition:transform .3s ease-in-out,opacity .3s ease,color .2s ease;transform-origin:center center;color:var(--mat-sys-on-surface-variant, #6c757d)}.has-background .svg-placeholder{color:var(--mat-sys-on-surface, #1f1f1f)}.svg-placeholder ::ng-deep svg{width:100%;height:100%;display:block}.svg-wrapper:hover .svg-placeholder{color:var(--mat-sys-primary, #6750a4)}\n"], dependencies: [{ kind: "directive", type: ResponsiveTextDirective, selector: "[libResponsiveText]", inputs: ["minFontSize", "maxFontSize", "lineHeight", "observeMutations", "debounceMs"] }] });
980
+ }
981
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: LabelWidgetComponent, decorators: [{
982
+ type: Component,
983
+ args: [{ selector: 'ngx-dashboard-label-widget', imports: [ResponsiveTextDirective], template: "@if (hasContent) {\r\n<div\r\n class=\"label-widget\"\r\n [style.fontSize.rem]=\"state().responsive ? null : state().fontSize! / 16\"\r\n [style.--widget-opacity]=\"state().opacity\"\r\n [class.text-left]=\"state().alignment === 'left'\"\r\n [class.text-right]=\"state().alignment === 'right'\"\r\n [class.font-bold]=\"state().fontWeight === 'bold'\"\r\n [class.has-background]=\"state().hasBackground\"\r\n>\r\n @if (state().responsive) {\r\n <div class=\"label-text\" libResponsiveText [minFontSize]=\"minFontSize()\" [maxFontSize]=\"maxFontSize()\">{{ label }}</div>\r\n } @else {\r\n <div class=\"label-text\">{{ label }}</div>\r\n }\r\n</div>\r\n} @else {\r\n<div class=\"svg-wrapper\" [class.has-background]=\"state().hasBackground\">\r\n <div class=\"svg-placeholder\" [innerHTML]=\"safeSvgIcon\"></div>\r\n</div>\r\n}\r\n", styles: [":host{display:block;container-type:size;width:100%;height:100%;overflow:hidden}.svg-wrapper,.label-widget{display:flex;align-items:center;justify-content:center;height:100%;width:100%;box-sizing:border-box;transition:background-color var(--mat-sys-motion-duration-medium2) var(--mat-sys-motion-easing-standard)}.has-background.svg-wrapper,.has-background.label-widget{background-color:var(--mat-sys-surface-container-high);border-radius:4px}.label-widget{overflow:hidden;container-type:size;padding:var(--mat-sys-spacing-4);color:var(--mat-sys-on-surface-variant, #6c757d);opacity:var(--widget-opacity, 1)}.label-widget.text-left{justify-content:flex-start}.label-widget.text-right{justify-content:flex-end}.label-widget.has-background{color:var(--mat-sys-on-surface, #1f1f1f)}.label-widget:hover{opacity:.3;color:var(--mat-sys-primary, #6750a4)}.label-text{width:100%;text-align:center;overflow-wrap:break-word;transition:color .2s ease}.text-left .label-text{text-align:left}.text-right .label-text{text-align:right}.font-bold .label-text{font-weight:700}.label-text[responsiveText]{overflow-wrap:normal}.svg-wrapper{overflow:hidden}.svg-placeholder{width:min(80cqw,80cqh);aspect-ratio:1/1;opacity:.3;transition:transform .3s ease-in-out,opacity .3s ease,color .2s ease;transform-origin:center center;color:var(--mat-sys-on-surface-variant, #6c757d)}.has-background .svg-placeholder{color:var(--mat-sys-on-surface, #1f1f1f)}.svg-placeholder ::ng-deep svg{width:100%;height:100%;display:block}.svg-wrapper:hover .svg-placeholder{color:var(--mat-sys-primary, #6750a4)}\n"] }]
984
+ }] });
985
+
986
+ const svgIcon$1 = `
987
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" preserveAspectRatio="xMidYMid meet">
988
+ <use transform="matrix(-1,0,0,1,800,0)" href="#one-half" />
989
+ <g id="one-half">
990
+ <g id="one-fourth">
991
+ <path d="m400 40v107" stroke-width="26.7" stroke="currentColor" />
992
+ <g id="one-twelfth">
993
+ <path
994
+ d="m580 88.233-42.5 73.612"
995
+ stroke-width="26.7"
996
+ stroke="currentColor"
997
+ />
998
+ <g id="one-thirtieth">
999
+ <path
1000
+ id="one-sixtieth"
1001
+ d="m437.63 41.974-3.6585 34.808"
1002
+ stroke-width="13.6"
1003
+ stroke="currentColor"
1004
+ />
1005
+ <use transform="rotate(6 400 400)" href="#one-sixtieth" />
1006
+ </g>
1007
+ <use transform="rotate(12 400 400)" href="#one-thirtieth" />
1008
+ </g>
1009
+ <use transform="rotate(30 400 400)" href="#one-twelfth" />
1010
+ <use transform="rotate(60 400 400)" href="#one-twelfth" />
1011
+ </g>
1012
+ <use transform="rotate(90 400 400)" href="#one-fourth" />
1013
+ </g>
1014
+ <path
1015
+ class="clock-hour-hand"
1016
+ id="anim-clock-hour-hand"
1017
+ fill="currentColor"
1018
+ d="m 381.925,476 h 36.15 l 5e-4,-300.03008 L 400,156.25 381.9245,175.96992 Z"
1019
+ transform="rotate(110.2650694444, 400, 400)"
1020
+ />
1021
+ <path
1022
+ class="clock-minute-hand"
1023
+ id="anim-clock-minute-hand"
1024
+ fill="currentColor"
1025
+ d="M 412.063,496.87456 H 387.937 L 385.249,65.68306 400,52.75 414.751,65.68306 Z"
1026
+ transform="rotate(243.1808333333, 400, 400)"
1027
+ />
1028
+ </svg>
1029
+ `;
1030
+
1031
+ class ClockStateDialogComponent {
1032
+ data = inject(MAT_DIALOG_DATA);
1033
+ dialogRef = inject((MatDialogRef));
1034
+ // State signals
1035
+ mode = signal(this.data.mode ?? 'digital', ...(ngDevMode ? [{ debugName: "mode" }] : []));
1036
+ hasBackground = signal(this.data.hasBackground ?? true, ...(ngDevMode ? [{ debugName: "hasBackground" }] : []));
1037
+ timeFormat = signal(this.data.timeFormat ?? '24h', ...(ngDevMode ? [{ debugName: "timeFormat" }] : []));
1038
+ showSeconds = signal(this.data.showSeconds ?? true, ...(ngDevMode ? [{ debugName: "showSeconds" }] : []));
1039
+ // Store original values for comparison
1040
+ originalMode = this.data.mode ?? 'digital';
1041
+ originalHasBackground = this.data.hasBackground ?? true;
1042
+ originalTimeFormat = this.data.timeFormat ?? '24h';
1043
+ originalShowSeconds = this.data.showSeconds ?? true;
1044
+ // Computed values
1045
+ hasChanged = computed(() => this.mode() !== this.originalMode ||
1046
+ this.hasBackground() !== this.originalHasBackground ||
1047
+ this.timeFormat() !== this.originalTimeFormat ||
1048
+ this.showSeconds() !== this.originalShowSeconds, ...(ngDevMode ? [{ debugName: "hasChanged" }] : []));
1049
+ onCancel() {
1050
+ this.dialogRef.close();
1051
+ }
1052
+ save() {
1053
+ this.dialogRef.close({
1054
+ mode: this.mode(),
1055
+ hasBackground: this.hasBackground(),
1056
+ timeFormat: this.timeFormat(),
1057
+ showSeconds: this.showSeconds(),
1058
+ });
1059
+ }
1060
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ClockStateDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1061
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: ClockStateDialogComponent, isStandalone: true, selector: "lib-clock-state-dialog", ngImport: i0, template: `
1062
+ <h2 mat-dialog-title>Clock Settings</h2>
1063
+ <mat-dialog-content>
1064
+ <div class="mode-selection">
1065
+ <label class="section-label" for="mode-selection-group">Display Mode</label>
1066
+ <mat-radio-group
1067
+ id="mode-selection-group"
1068
+ [value]="mode()"
1069
+ (change)="mode.set($any($event.value))"
1070
+ >
1071
+ <mat-radio-button value="digital">Digital</mat-radio-button>
1072
+ <mat-radio-button value="analog">Analog</mat-radio-button>
1073
+ </mat-radio-group>
1074
+ </div>
1075
+
1076
+ <!-- Time Format (only for digital mode) -->
1077
+ @if (mode() === 'digital') {
1078
+ <div class="format-selection">
1079
+ <label class="section-label" for="time-format-group">Time Format</label>
1080
+ <mat-radio-group
1081
+ id="time-format-group"
1082
+ [value]="timeFormat()"
1083
+ (change)="timeFormat.set($any($event.value))"
1084
+ >
1085
+ <mat-radio-button value="24h">24 Hour (14:30:45)</mat-radio-button>
1086
+ <mat-radio-button value="12h">12 Hour (2:30:45 PM)</mat-radio-button>
1087
+ </mat-radio-group>
1088
+ </div>
1089
+ }
1090
+
1091
+ <!-- Show Seconds Toggle (for both digital and analog modes) -->
1092
+ <div class="toggle-section">
1093
+ <mat-slide-toggle
1094
+ [checked]="showSeconds()"
1095
+ (change)="showSeconds.set($event.checked)">
1096
+ Show Seconds
1097
+ </mat-slide-toggle>
1098
+ <span class="toggle-description">
1099
+ @if (mode() === 'digital') {
1100
+ Display seconds in the time
1101
+ } @else {
1102
+ Show the second hand on the clock
1103
+ }
1104
+ </span>
1105
+ </div>
1106
+
1107
+ <!-- Background Toggle -->
1108
+ <div class="toggle-section">
1109
+ <mat-slide-toggle
1110
+ [checked]="hasBackground()"
1111
+ (change)="hasBackground.set($event.checked)">
1112
+ Background
1113
+ </mat-slide-toggle>
1114
+ <span class="toggle-description"
1115
+ >Adds a background behind the clock</span
1116
+ >
1117
+ </div>
1118
+ </mat-dialog-content>
1119
+
1120
+ <mat-dialog-actions align="end">
1121
+ <button mat-button (click)="onCancel()">Cancel</button>
1122
+ <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
1123
+ Save
1124
+ </button>
1125
+ </mat-dialog-actions>
1126
+ `, isInline: true, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden}.mode-selection,.format-selection{margin-top:1rem;margin-bottom:2rem}.section-label{display:block;margin-bottom:.75rem;font-weight:500}mat-radio-group{display:flex;flex-direction:column;gap:.75rem}mat-radio-button{margin:0}.toggle-section{display:flex;align-items:center;gap:.75rem;margin-bottom:.5rem}.toggle-description{margin:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i3$1.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i3$1.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }] });
1127
+ }
1128
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ClockStateDialogComponent, decorators: [{
1129
+ type: Component,
1130
+ args: [{ selector: 'lib-clock-state-dialog', standalone: true, imports: [
1131
+ CommonModule,
1132
+ FormsModule,
1133
+ MatDialogModule,
1134
+ MatButtonModule,
1135
+ MatRadioModule,
1136
+ MatSlideToggleModule,
1137
+ ], template: `
1138
+ <h2 mat-dialog-title>Clock Settings</h2>
1139
+ <mat-dialog-content>
1140
+ <div class="mode-selection">
1141
+ <label class="section-label" for="mode-selection-group">Display Mode</label>
1142
+ <mat-radio-group
1143
+ id="mode-selection-group"
1144
+ [value]="mode()"
1145
+ (change)="mode.set($any($event.value))"
1146
+ >
1147
+ <mat-radio-button value="digital">Digital</mat-radio-button>
1148
+ <mat-radio-button value="analog">Analog</mat-radio-button>
1149
+ </mat-radio-group>
1150
+ </div>
1151
+
1152
+ <!-- Time Format (only for digital mode) -->
1153
+ @if (mode() === 'digital') {
1154
+ <div class="format-selection">
1155
+ <label class="section-label" for="time-format-group">Time Format</label>
1156
+ <mat-radio-group
1157
+ id="time-format-group"
1158
+ [value]="timeFormat()"
1159
+ (change)="timeFormat.set($any($event.value))"
1160
+ >
1161
+ <mat-radio-button value="24h">24 Hour (14:30:45)</mat-radio-button>
1162
+ <mat-radio-button value="12h">12 Hour (2:30:45 PM)</mat-radio-button>
1163
+ </mat-radio-group>
1164
+ </div>
1165
+ }
1166
+
1167
+ <!-- Show Seconds Toggle (for both digital and analog modes) -->
1168
+ <div class="toggle-section">
1169
+ <mat-slide-toggle
1170
+ [checked]="showSeconds()"
1171
+ (change)="showSeconds.set($event.checked)">
1172
+ Show Seconds
1173
+ </mat-slide-toggle>
1174
+ <span class="toggle-description">
1175
+ @if (mode() === 'digital') {
1176
+ Display seconds in the time
1177
+ } @else {
1178
+ Show the second hand on the clock
1179
+ }
1180
+ </span>
1181
+ </div>
1182
+
1183
+ <!-- Background Toggle -->
1184
+ <div class="toggle-section">
1185
+ <mat-slide-toggle
1186
+ [checked]="hasBackground()"
1187
+ (change)="hasBackground.set($event.checked)">
1188
+ Background
1189
+ </mat-slide-toggle>
1190
+ <span class="toggle-description"
1191
+ >Adds a background behind the clock</span
1192
+ >
1193
+ </div>
1194
+ </mat-dialog-content>
1195
+
1196
+ <mat-dialog-actions align="end">
1197
+ <button mat-button (click)="onCancel()">Cancel</button>
1198
+ <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
1199
+ Save
1200
+ </button>
1201
+ </mat-dialog-actions>
1202
+ `, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden}.mode-selection,.format-selection{margin-top:1rem;margin-bottom:2rem}.section-label{display:block;margin-bottom:.75rem;font-weight:500}mat-radio-group{display:flex;flex-direction:column;gap:.75rem}mat-radio-button{margin:0}.toggle-section{display:flex;align-items:center;gap:.75rem;margin-bottom:.5rem}.toggle-description{margin:0}\n"] }]
1203
+ }] });
1204
+
1205
+ class DigitalClockComponent {
1206
+ #destroyRef = inject(DestroyRef);
1207
+ // Inputs
1208
+ timeFormat = input('24h', ...(ngDevMode ? [{ debugName: "timeFormat" }] : []));
1209
+ showSeconds = input(true, ...(ngDevMode ? [{ debugName: "showSeconds" }] : []));
1210
+ hasBackground = input(false, ...(ngDevMode ? [{ debugName: "hasBackground" }] : []));
1211
+ // Time tracking
1212
+ currentTime = signal(new Date(), ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
1213
+ formattedTime = computed(() => {
1214
+ const time = this.currentTime();
1215
+ const format = this.timeFormat();
1216
+ const showSecs = this.showSeconds();
1217
+ return this.#formatTime(time, format, showSecs);
1218
+ }, ...(ngDevMode ? [{ debugName: "formattedTime" }] : []));
1219
+ #intervalId = null;
1220
+ #formatTime(time, format, showSecs) {
1221
+ let hours = time.getHours();
1222
+ const minutes = time.getMinutes();
1223
+ const seconds = time.getSeconds();
1224
+ // Pad with leading zeros
1225
+ const mm = minutes.toString().padStart(2, '0');
1226
+ const ss = seconds.toString().padStart(2, '0');
1227
+ if (format === '12h') {
1228
+ // 12-hour format with AM/PM
1229
+ const ampm = hours >= 12 ? 'PM' : 'AM';
1230
+ hours = hours % 12;
1231
+ if (hours === 0)
1232
+ hours = 12; // Convert 0 to 12 for 12 AM/PM
1233
+ const hh = hours.toString().padStart(2, '0');
1234
+ return showSecs ? `${hh}:${mm}:${ss} ${ampm}` : `${hh}:${mm} ${ampm}`;
1235
+ }
1236
+ else {
1237
+ // 24-hour format
1238
+ const hh = hours.toString().padStart(2, '0');
1239
+ return showSecs ? `${hh}:${mm}:${ss}` : `${hh}:${mm}`;
1240
+ }
1241
+ }
1242
+ constructor() {
1243
+ // Set up time update timer
1244
+ this.#startTimer();
1245
+ // Clean up timer on component destruction
1246
+ this.#destroyRef.onDestroy(() => {
1247
+ this.#stopTimer();
1248
+ });
1249
+ }
1250
+ #startTimer() {
1251
+ // Sync to the next second boundary for smooth start
1252
+ const now = new Date();
1253
+ const msUntilNextSecond = 1000 - now.getMilliseconds();
1254
+ setTimeout(() => {
1255
+ this.currentTime.set(new Date());
1256
+ // Start the regular 1-second interval
1257
+ this.#intervalId = window.setInterval(() => {
1258
+ this.currentTime.set(new Date());
1259
+ }, 1000);
1260
+ }, msUntilNextSecond);
1261
+ }
1262
+ #stopTimer() {
1263
+ if (this.#intervalId !== null) {
1264
+ clearInterval(this.#intervalId);
1265
+ this.#intervalId = null;
1266
+ }
1267
+ }
1268
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DigitalClockComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1269
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.2.1", type: DigitalClockComponent, isStandalone: true, selector: "lib-digital-clock", inputs: { timeFormat: { classPropertyName: "timeFormat", publicName: "timeFormat", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null }, hasBackground: { classPropertyName: "hasBackground", publicName: "hasBackground", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.has-background": "hasBackground()", "class.show-pm": "timeFormat() === \"12h\"", "class.show-seconds": "showSeconds()" }, classAttribute: "clock-widget digital" }, ngImport: i0, template: "<div responsiveText class=\"digital-time\">{{ formattedTime() }}</div>\r\n", styles: [":host{display:flex;align-items:center;justify-content:center;height:100%;width:100%;box-sizing:border-box;transition:background-color var(--mat-sys-motion-duration-medium2) var(--mat-sys-motion-easing-standard);padding:var(--mat-sys-spacing-4);color:var(--mat-sys-on-surface-variant, #6c757d)}:host.has-background{background-color:var(--mat-sys-surface-container-high);border-radius:4px;color:var(--mat-sys-on-surface, #1f1f1f)}:host:hover{opacity:.8;color:var(--mat-sys-primary, #6750a4)}.digital-time{font-size:clamp(8px,min(20cqw,50cqh),200px);font-family:monospace;font-weight:500;letter-spacing:.05em;transition:color .2s ease}:host.show-pm.show-seconds .digital-time{font-size:clamp(8px,min(15cqw,50cqh),200px)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1270
+ }
1271
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: DigitalClockComponent, decorators: [{
1272
+ type: Component,
1273
+ args: [{ selector: 'lib-digital-clock', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1274
+ '[class.has-background]': 'hasBackground()',
1275
+ '[class.show-pm]': 'timeFormat() === "12h"',
1276
+ '[class.show-seconds]': 'showSeconds()',
1277
+ class: 'clock-widget digital',
1278
+ }, template: "<div responsiveText class=\"digital-time\">{{ formattedTime() }}</div>\r\n", styles: [":host{display:flex;align-items:center;justify-content:center;height:100%;width:100%;box-sizing:border-box;transition:background-color var(--mat-sys-motion-duration-medium2) var(--mat-sys-motion-easing-standard);padding:var(--mat-sys-spacing-4);color:var(--mat-sys-on-surface-variant, #6c757d)}:host.has-background{background-color:var(--mat-sys-surface-container-high);border-radius:4px;color:var(--mat-sys-on-surface, #1f1f1f)}:host:hover{opacity:.8;color:var(--mat-sys-primary, #6750a4)}.digital-time{font-size:clamp(8px,min(20cqw,50cqh),200px);font-family:monospace;font-weight:500;letter-spacing:.05em;transition:color .2s ease}:host.show-pm.show-seconds .digital-time{font-size:clamp(8px,min(15cqw,50cqh),200px)}\n"] }]
1279
+ }], ctorParameters: () => [] });
1280
+
1281
+ class AnalogClockComponent {
1282
+ #destroyRef = inject(DestroyRef);
1283
+ #renderer = inject(Renderer2);
1284
+ // Inputs
1285
+ hasBackground = input(false, ...(ngDevMode ? [{ debugName: "hasBackground" }] : []));
1286
+ showSeconds = input(true, ...(ngDevMode ? [{ debugName: "showSeconds" }] : []));
1287
+ // ViewChild references for clock hands
1288
+ hourHand = viewChild('hourHand', ...(ngDevMode ? [{ debugName: "hourHand" }] : []));
1289
+ minuteHand = viewChild('minuteHand', ...(ngDevMode ? [{ debugName: "minuteHand" }] : []));
1290
+ secondHand = viewChild('secondHand', ...(ngDevMode ? [{ debugName: "secondHand" }] : []));
1291
+ // Time tracking
1292
+ currentTime = signal(new Date(), ...(ngDevMode ? [{ debugName: "currentTime" }] : []));
1293
+ // Computed rotation signals
1294
+ secondHandRotation = computed(() => {
1295
+ const seconds = this.currentTime().getSeconds();
1296
+ return seconds * 6; // 360° / 60s = 6° per second
1297
+ }, ...(ngDevMode ? [{ debugName: "secondHandRotation" }] : []));
1298
+ minuteHandRotation = computed(() => {
1299
+ const time = this.currentTime();
1300
+ const minutes = time.getMinutes();
1301
+ const seconds = time.getSeconds();
1302
+ return minutes * 6 + seconds / 10; // Smooth minute hand movement
1303
+ }, ...(ngDevMode ? [{ debugName: "minuteHandRotation" }] : []));
1304
+ hourHandRotation = computed(() => {
1305
+ const time = this.currentTime();
1306
+ const hours = time.getHours() % 12;
1307
+ const minutes = time.getMinutes();
1308
+ const seconds = time.getSeconds();
1309
+ return hours * 30 + minutes / 2 + seconds / 120; // Smooth hour hand movement
1310
+ }, ...(ngDevMode ? [{ debugName: "hourHandRotation" }] : []));
1311
+ #intervalId = null;
1312
+ constructor() {
1313
+ // Set up time update timer
1314
+ this.#startTimer();
1315
+ // Clean up timer on component destruction
1316
+ this.#destroyRef.onDestroy(() => {
1317
+ this.#stopTimer();
1318
+ });
1319
+ // Update DOM when rotations change
1320
+ effect(() => {
1321
+ this.#updateClockHands();
1322
+ });
1323
+ }
1324
+ #startTimer() {
1325
+ // Sync to the next second boundary for smooth start
1326
+ const now = new Date();
1327
+ const msUntilNextSecond = 1000 - now.getMilliseconds();
1328
+ setTimeout(() => {
1329
+ this.currentTime.set(new Date());
1330
+ // Start the regular 1-second interval
1331
+ this.#intervalId = window.setInterval(() => {
1332
+ this.currentTime.set(new Date());
1333
+ }, 1000);
1334
+ }, msUntilNextSecond);
1335
+ }
1336
+ #stopTimer() {
1337
+ if (this.#intervalId !== null) {
1338
+ clearInterval(this.#intervalId);
1339
+ this.#intervalId = null;
1340
+ }
1341
+ }
1342
+ #updateClockHands() {
1343
+ const hourElement = this.hourHand()?.nativeElement;
1344
+ const minuteElement = this.minuteHand()?.nativeElement;
1345
+ const secondElement = this.secondHand()?.nativeElement;
1346
+ if (hourElement) {
1347
+ this.#renderer.setAttribute(hourElement, 'transform', `rotate(${this.hourHandRotation()}, 400, 400)`);
1348
+ }
1349
+ if (minuteElement) {
1350
+ this.#renderer.setAttribute(minuteElement, 'transform', `rotate(${this.minuteHandRotation()}, 400, 400)`);
1351
+ }
1352
+ if (secondElement && this.showSeconds()) {
1353
+ this.#renderer.setAttribute(secondElement, 'transform', `rotate(${this.secondHandRotation()}, 400, 400)`);
1354
+ }
1355
+ }
1356
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: AnalogClockComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1357
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.2.1", type: AnalogClockComponent, isStandalone: true, selector: "lib-analog-clock", inputs: { hasBackground: { classPropertyName: "hasBackground", publicName: "hasBackground", isSignal: true, isRequired: false, transformFunction: null }, showSeconds: { classPropertyName: "showSeconds", publicName: "showSeconds", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.has-background": "hasBackground()", "class.show-seconds": "showSeconds()" }, classAttribute: "clock-widget analog" }, viewQueries: [{ propertyName: "hourHand", first: true, predicate: ["hourHand"], descendants: true, isSignal: true }, { propertyName: "minuteHand", first: true, predicate: ["minuteHand"], descendants: true, isSignal: true }, { propertyName: "secondHand", first: true, predicate: ["secondHand"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"analog-clock-container\">\r\n <div class=\"aspect-ratio-box\">\r\n <svg\r\n version=\"1.1\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n viewBox=\"0 0 800 800\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n >\r\n <!-- Optional face circle; uncomment if you want a visible outline by default -->\r\n <!-- <circle cx=\"400\" cy=\"400\" r=\"400\" fill=\"transparent\" stroke=\"currentColor\" stroke-width=\"2\" /> -->\r\n\r\n <use transform=\"matrix(-1,0,0,1,800,0)\" href=\"#one-half\" />\r\n <g id=\"one-half\">\r\n <g id=\"one-fourth\">\r\n <!-- 12 / 3 / 6 / 9 heavy marks -->\r\n <path d=\"m400 40v107\" stroke-width=\"26.7\" stroke=\"currentColor\" />\r\n <g id=\"one-twelfth\">\r\n <!-- 30\u00B0 heavy marks -->\r\n <path\r\n d=\"m580 88.233-42.5 73.612\"\r\n stroke-width=\"26.7\"\r\n stroke=\"currentColor\"\r\n />\r\n <g id=\"one-thirtieth\">\r\n <!-- minute/second ticks -->\r\n <path\r\n id=\"one-sixtieth\"\r\n d=\"m437.63 41.974-3.6585 34.808\"\r\n stroke-width=\"13.6\"\r\n stroke=\"currentColor\"\r\n />\r\n <use transform=\"rotate(6 400 400)\" href=\"#one-sixtieth\" />\r\n </g>\r\n <use transform=\"rotate(12 400 400)\" href=\"#one-thirtieth\" />\r\n </g>\r\n <use transform=\"rotate(30 400 400)\" href=\"#one-twelfth\" />\r\n <use transform=\"rotate(60 400 400)\" href=\"#one-twelfth\" />\r\n </g>\r\n <use transform=\"rotate(90 400 400)\" href=\"#one-fourth\" />\r\n </g>\r\n\r\n <!-- Hands -->\r\n <path\r\n class=\"clock-hour-hand\"\r\n id=\"anim-clock-hour-hand\"\r\n #hourHand\r\n d=\"m 381.925,476 h 36.15 l 5e-4,-300.03008 L 400,156.25 381.9245,175.96992 Z\"\r\n transform=\"rotate(110.2650694444, 400, 400)\"\r\n />\r\n <path\r\n class=\"clock-minute-hand\"\r\n id=\"anim-clock-minute-hand\"\r\n #minuteHand\r\n d=\"M 412.063,496.87456 H 387.937 L 385.249,65.68306 400,52.75 414.751,65.68306 Z\"\r\n transform=\"rotate(243.1808333333, 400, 400)\"\r\n />\r\n <path\r\n class=\"clock-second-hand\"\r\n id=\"anim-clock-second-hand\"\r\n #secondHand\r\n d=\"M 397.317,63.51744 395.91962,168.4 C 374.575,170.5125 358.2,188.365 358.2,210 c 0,21.635 16.3,39 36.61214,41.47594 L 391.52847,498 h 16.94306 L 405.1868,251.47593 C 425.5,249 441.8,231.635 441.8,210 c 2e-5,-21.635 -16.375,-39.4875 -37.71971,-41.6 L 402.683,63.51744 400,60 Z M 400,190.534 c 10.888,0 19.466,8.866 19.466,19.466 0,10.6 -8.578,19.466 -19.466,19.466 -10.888,0 -19.466,-8.866 -19.466,-19.466 0,-10.6 8.578,-19.466 19.466,-19.466 z\"\r\n transform=\"rotate(190.85, 400, 400)\"\r\n />\r\n </svg>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100%}.analog-clock-container{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.analog-clock-container .aspect-ratio-box{position:relative;width:100%;max-width:100%;max-height:100%;aspect-ratio:1/1}@supports not (aspect-ratio: 1/1){.analog-clock-container .aspect-ratio-box:before{content:\"\";display:block;padding-bottom:100%}.analog-clock-container .aspect-ratio-box svg{position:absolute;top:0;left:0;width:100%;height:100%}}.analog-clock-container .aspect-ratio-box svg{display:block;width:100%;height:100%}.analog-clock-container .aspect-ratio-box svg path:not(.clock-hour-hand):not(.clock-minute-hand):not(.clock-second-hand){stroke:var(--mat-sys-on-surface, #1d1b20)}.analog-clock-container .aspect-ratio-box svg .clock-hour-hand{fill:var(--mat-sys-on-surface, #1d1b20)}.analog-clock-container .aspect-ratio-box svg .clock-minute-hand{fill:var(--mat-sys-on-surface, #1d1b20)}.analog-clock-container .aspect-ratio-box svg .clock-second-hand{fill:var(--mat-sys-primary, #6750a4)}:host:not(.show-seconds) .clock-second-hand{display:none}:host.has-background svg circle{fill:var(--mat-sys-surface, #fffbfe)}:host:hover{opacity:.8}:host:hover svg .clock-hour-hand,:host:hover svg .clock-minute-hand{fill:var(--mat-sys-primary, #6750a4)}:host.clock-widget.analog{container-type:size;container-name:analog-clock}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1358
+ }
1359
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: AnalogClockComponent, decorators: [{
1360
+ type: Component,
1361
+ args: [{ selector: 'lib-analog-clock', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1362
+ '[class.has-background]': 'hasBackground()',
1363
+ '[class.show-seconds]': 'showSeconds()',
1364
+ 'class': 'clock-widget analog'
1365
+ }, template: "<div class=\"analog-clock-container\">\r\n <div class=\"aspect-ratio-box\">\r\n <svg\r\n version=\"1.1\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n viewBox=\"0 0 800 800\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n >\r\n <!-- Optional face circle; uncomment if you want a visible outline by default -->\r\n <!-- <circle cx=\"400\" cy=\"400\" r=\"400\" fill=\"transparent\" stroke=\"currentColor\" stroke-width=\"2\" /> -->\r\n\r\n <use transform=\"matrix(-1,0,0,1,800,0)\" href=\"#one-half\" />\r\n <g id=\"one-half\">\r\n <g id=\"one-fourth\">\r\n <!-- 12 / 3 / 6 / 9 heavy marks -->\r\n <path d=\"m400 40v107\" stroke-width=\"26.7\" stroke=\"currentColor\" />\r\n <g id=\"one-twelfth\">\r\n <!-- 30\u00B0 heavy marks -->\r\n <path\r\n d=\"m580 88.233-42.5 73.612\"\r\n stroke-width=\"26.7\"\r\n stroke=\"currentColor\"\r\n />\r\n <g id=\"one-thirtieth\">\r\n <!-- minute/second ticks -->\r\n <path\r\n id=\"one-sixtieth\"\r\n d=\"m437.63 41.974-3.6585 34.808\"\r\n stroke-width=\"13.6\"\r\n stroke=\"currentColor\"\r\n />\r\n <use transform=\"rotate(6 400 400)\" href=\"#one-sixtieth\" />\r\n </g>\r\n <use transform=\"rotate(12 400 400)\" href=\"#one-thirtieth\" />\r\n </g>\r\n <use transform=\"rotate(30 400 400)\" href=\"#one-twelfth\" />\r\n <use transform=\"rotate(60 400 400)\" href=\"#one-twelfth\" />\r\n </g>\r\n <use transform=\"rotate(90 400 400)\" href=\"#one-fourth\" />\r\n </g>\r\n\r\n <!-- Hands -->\r\n <path\r\n class=\"clock-hour-hand\"\r\n id=\"anim-clock-hour-hand\"\r\n #hourHand\r\n d=\"m 381.925,476 h 36.15 l 5e-4,-300.03008 L 400,156.25 381.9245,175.96992 Z\"\r\n transform=\"rotate(110.2650694444, 400, 400)\"\r\n />\r\n <path\r\n class=\"clock-minute-hand\"\r\n id=\"anim-clock-minute-hand\"\r\n #minuteHand\r\n d=\"M 412.063,496.87456 H 387.937 L 385.249,65.68306 400,52.75 414.751,65.68306 Z\"\r\n transform=\"rotate(243.1808333333, 400, 400)\"\r\n />\r\n <path\r\n class=\"clock-second-hand\"\r\n id=\"anim-clock-second-hand\"\r\n #secondHand\r\n d=\"M 397.317,63.51744 395.91962,168.4 C 374.575,170.5125 358.2,188.365 358.2,210 c 0,21.635 16.3,39 36.61214,41.47594 L 391.52847,498 h 16.94306 L 405.1868,251.47593 C 425.5,249 441.8,231.635 441.8,210 c 2e-5,-21.635 -16.375,-39.4875 -37.71971,-41.6 L 402.683,63.51744 400,60 Z M 400,190.534 c 10.888,0 19.466,8.866 19.466,19.466 0,10.6 -8.578,19.466 -19.466,19.466 -10.888,0 -19.466,-8.866 -19.466,-19.466 0,-10.6 8.578,-19.466 19.466,-19.466 z\"\r\n transform=\"rotate(190.85, 400, 400)\"\r\n />\r\n </svg>\r\n </div>\r\n</div>\r\n", styles: [":host{display:block;width:100%;height:100%}.analog-clock-container{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.analog-clock-container .aspect-ratio-box{position:relative;width:100%;max-width:100%;max-height:100%;aspect-ratio:1/1}@supports not (aspect-ratio: 1/1){.analog-clock-container .aspect-ratio-box:before{content:\"\";display:block;padding-bottom:100%}.analog-clock-container .aspect-ratio-box svg{position:absolute;top:0;left:0;width:100%;height:100%}}.analog-clock-container .aspect-ratio-box svg{display:block;width:100%;height:100%}.analog-clock-container .aspect-ratio-box svg path:not(.clock-hour-hand):not(.clock-minute-hand):not(.clock-second-hand){stroke:var(--mat-sys-on-surface, #1d1b20)}.analog-clock-container .aspect-ratio-box svg .clock-hour-hand{fill:var(--mat-sys-on-surface, #1d1b20)}.analog-clock-container .aspect-ratio-box svg .clock-minute-hand{fill:var(--mat-sys-on-surface, #1d1b20)}.analog-clock-container .aspect-ratio-box svg .clock-second-hand{fill:var(--mat-sys-primary, #6750a4)}:host:not(.show-seconds) .clock-second-hand{display:none}:host.has-background svg circle{fill:var(--mat-sys-surface, #fffbfe)}:host:hover{opacity:.8}:host:hover svg .clock-hour-hand,:host:hover svg .clock-minute-hand{fill:var(--mat-sys-primary, #6750a4)}:host.clock-widget.analog{container-type:size;container-name:analog-clock}\n"] }]
1366
+ }], ctorParameters: () => [] });
1367
+
1368
+ // clock-widget.component.ts
1369
+ class ClockWidgetComponent {
1370
+ static metadata = {
1371
+ widgetTypeid: '@ngx-dashboard/clock-widget',
1372
+ name: 'Clock',
1373
+ description: 'Display time in analog or digital format',
1374
+ svgIcon: svgIcon$1,
1375
+ };
1376
+ #sanitizer = inject(DomSanitizer);
1377
+ #dialog = inject(MatDialog);
1378
+ safeSvgIcon = this.#sanitizer.bypassSecurityTrustHtml(svgIcon$1);
1379
+ state = signal({
1380
+ mode: 'analog',
1381
+ hasBackground: true,
1382
+ timeFormat: '24h',
1383
+ showSeconds: true,
1384
+ }, ...(ngDevMode ? [{ debugName: "state" }] : []));
1385
+ constructor() {
1386
+ // No timer logic needed - DigitalClock manages its own time
1387
+ }
1388
+ dashboardSetState(state) {
1389
+ if (state) {
1390
+ this.state.update((current) => ({
1391
+ ...current,
1392
+ ...state,
1393
+ }));
1394
+ }
1395
+ }
1396
+ dashboardGetState() {
1397
+ return { ...this.state() };
1398
+ }
1399
+ dashboardEditState() {
1400
+ const dialogRef = this.#dialog.open(ClockStateDialogComponent, {
1401
+ data: this.dashboardGetState(),
1402
+ width: '400px',
1403
+ maxWidth: '90vw',
1404
+ disableClose: false,
1405
+ autoFocus: false,
1406
+ });
1407
+ dialogRef
1408
+ .afterClosed()
1409
+ .subscribe((result) => {
1410
+ if (result) {
1411
+ this.state.set(result);
1412
+ }
1413
+ });
1414
+ }
1415
+ get isAnalog() {
1416
+ return this.state().mode === 'analog';
1417
+ }
1418
+ get isDigital() {
1419
+ return this.state().mode === 'digital';
1420
+ }
1421
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ClockWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1422
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: ClockWidgetComponent, isStandalone: true, selector: "ngx-dashboard-clock-widget", ngImport: i0, template: "@if (isDigital) {\r\n <lib-digital-clock\r\n [timeFormat]=\"state().timeFormat || '24h'\"\r\n [showSeconds]=\"state().showSeconds ?? true\"\r\n [hasBackground]=\"state().hasBackground ?? false\"\r\n />\r\n} @else if (isAnalog) {\r\n <lib-analog-clock\r\n [hasBackground]=\"state().hasBackground ?? false\"\r\n [showSeconds]=\"state().showSeconds ?? true\"\r\n />\r\n} @else {\r\n<div class=\"svg-wrapper\" [class.has-background]=\"state().hasBackground\">\r\n <div class=\"svg-placeholder\" [innerHTML]=\"safeSvgIcon\"></div>\r\n</div>\r\n}", styles: [":host{display:block;container-type:size;width:100%;height:100%;overflow:hidden}.svg-wrapper,.clock-widget{display:flex;align-items:center;justify-content:center;height:100%;width:100%;box-sizing:border-box;transition:background-color var(--mat-sys-motion-duration-medium2) var(--mat-sys-motion-easing-standard)}.has-background.svg-wrapper,.has-background.clock-widget{background-color:var(--mat-sys-surface-container-high);border-radius:4px}.clock-widget{padding:var(--mat-sys-spacing-4);color:var(--mat-sys-on-surface-variant, #6c757d)}.clock-widget.has-background{color:var(--mat-sys-on-surface, #1f1f1f)}.clock-widget:hover{opacity:.8;color:var(--mat-sys-primary, #6750a4)}.analog-clock{width:min(80cqw,80cqh);aspect-ratio:1/1;position:relative}.clock-face{width:100%;height:100%;border:2px solid currentColor;border-radius:50%;position:relative}.clock-face:before,.clock-face:after{content:\"\";position:absolute;background-color:currentColor;left:50%;transform:translate(-50%)}.clock-face:before{width:2px;height:10%;top:0}.clock-face:after{width:2px;height:10%;bottom:0}.hour-hand,.minute-hand{position:absolute;background-color:currentColor;left:50%;bottom:50%;transform-origin:50% 100%;border-radius:2px}.hour-hand{width:4px;height:25%;transform:translate(-50%) rotate(30deg)}.minute-hand{width:2px;height:35%;transform:translate(-50%) rotate(90deg)}.center-dot{position:absolute;width:8px;height:8px;background-color:currentColor;border-radius:50%;top:50%;left:50%;transform:translate(-50%,-50%)}.svg-wrapper{overflow:hidden}.svg-placeholder{width:min(80cqw,80cqh);aspect-ratio:1/1;opacity:.3;transition:transform .3s ease-in-out,opacity .3s ease;transform-origin:center center}.svg-placeholder ::ng-deep svg{width:100%;height:100%;display:block}.svg-placeholder ::ng-deep svg .clock-face{stroke:var(--mat-sys-on-surface, #1d1b20)}.svg-placeholder ::ng-deep svg .clock-hour-hand{fill:var(--mat-sys-on-surface, #1d1b20)}.svg-placeholder ::ng-deep svg .clock-minute-hand{fill:var(--mat-sys-on-surface, #1d1b20)}.svg-placeholder ::ng-deep svg .clock-second-hand{fill:var(--mat-sys-primary, #6750a4)}.has-background .svg-placeholder ::ng-deep svg circle{fill:var(--mat-sys-surface, #fffbfe)}\n"], dependencies: [{ kind: "component", type: DigitalClockComponent, selector: "lib-digital-clock", inputs: ["timeFormat", "showSeconds", "hasBackground"] }, { kind: "component", type: AnalogClockComponent, selector: "lib-analog-clock", inputs: ["hasBackground", "showSeconds"] }] });
1423
+ }
1424
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: ClockWidgetComponent, decorators: [{
1425
+ type: Component,
1426
+ args: [{ selector: 'ngx-dashboard-clock-widget', standalone: true, imports: [DigitalClockComponent, AnalogClockComponent], template: "@if (isDigital) {\r\n <lib-digital-clock\r\n [timeFormat]=\"state().timeFormat || '24h'\"\r\n [showSeconds]=\"state().showSeconds ?? true\"\r\n [hasBackground]=\"state().hasBackground ?? false\"\r\n />\r\n} @else if (isAnalog) {\r\n <lib-analog-clock\r\n [hasBackground]=\"state().hasBackground ?? false\"\r\n [showSeconds]=\"state().showSeconds ?? true\"\r\n />\r\n} @else {\r\n<div class=\"svg-wrapper\" [class.has-background]=\"state().hasBackground\">\r\n <div class=\"svg-placeholder\" [innerHTML]=\"safeSvgIcon\"></div>\r\n</div>\r\n}", styles: [":host{display:block;container-type:size;width:100%;height:100%;overflow:hidden}.svg-wrapper,.clock-widget{display:flex;align-items:center;justify-content:center;height:100%;width:100%;box-sizing:border-box;transition:background-color var(--mat-sys-motion-duration-medium2) var(--mat-sys-motion-easing-standard)}.has-background.svg-wrapper,.has-background.clock-widget{background-color:var(--mat-sys-surface-container-high);border-radius:4px}.clock-widget{padding:var(--mat-sys-spacing-4);color:var(--mat-sys-on-surface-variant, #6c757d)}.clock-widget.has-background{color:var(--mat-sys-on-surface, #1f1f1f)}.clock-widget:hover{opacity:.8;color:var(--mat-sys-primary, #6750a4)}.analog-clock{width:min(80cqw,80cqh);aspect-ratio:1/1;position:relative}.clock-face{width:100%;height:100%;border:2px solid currentColor;border-radius:50%;position:relative}.clock-face:before,.clock-face:after{content:\"\";position:absolute;background-color:currentColor;left:50%;transform:translate(-50%)}.clock-face:before{width:2px;height:10%;top:0}.clock-face:after{width:2px;height:10%;bottom:0}.hour-hand,.minute-hand{position:absolute;background-color:currentColor;left:50%;bottom:50%;transform-origin:50% 100%;border-radius:2px}.hour-hand{width:4px;height:25%;transform:translate(-50%) rotate(30deg)}.minute-hand{width:2px;height:35%;transform:translate(-50%) rotate(90deg)}.center-dot{position:absolute;width:8px;height:8px;background-color:currentColor;border-radius:50%;top:50%;left:50%;transform:translate(-50%,-50%)}.svg-wrapper{overflow:hidden}.svg-placeholder{width:min(80cqw,80cqh);aspect-ratio:1/1;opacity:.3;transition:transform .3s ease-in-out,opacity .3s ease;transform-origin:center center}.svg-placeholder ::ng-deep svg{width:100%;height:100%;display:block}.svg-placeholder ::ng-deep svg .clock-face{stroke:var(--mat-sys-on-surface, #1d1b20)}.svg-placeholder ::ng-deep svg .clock-hour-hand{fill:var(--mat-sys-on-surface, #1d1b20)}.svg-placeholder ::ng-deep svg .clock-minute-hand{fill:var(--mat-sys-on-surface, #1d1b20)}.svg-placeholder ::ng-deep svg .clock-second-hand{fill:var(--mat-sys-primary, #6750a4)}.has-background .svg-placeholder ::ng-deep svg circle{fill:var(--mat-sys-surface, #fffbfe)}\n"] }]
1427
+ }], ctorParameters: () => [] });
1428
+
1429
+ /**
1430
+ * Responsive radial gauge component with hybrid sizing and thickness control.
1431
+ *
1432
+ * This component provides a highly flexible gauge system with three independent
1433
+ * control dimensions that can be mixed and matched for different use cases:
1434
+ *
1435
+ * ## Size Control:
1436
+ * - **Fixed Size**: Use manual `size` input (traditional behavior)
1437
+ * - **Container Responsive**: Enable `fitToContainer` for automatic sizing
1438
+ *
1439
+ * ## Thickness Control:
1440
+ * - **Manual Thickness**: Use individual thickness inputs (traditional behavior)
1441
+ * - **Proportional Thickness**: Enable `responsiveMode` for size-based scaling
1442
+ *
1443
+ * ## Usage Scenarios:
1444
+ *
1445
+ * ### 1. Dashboard Widgets (Recommended)
1446
+ * ```html
1447
+ * <ngx-radial-gauge
1448
+ * [value]="cpuUsage"
1449
+ * [fitToContainer]="true"
1450
+ * [responsiveMode]="true"
1451
+ * [sizeToThicknessRatio]="12" />
1452
+ * ```
1453
+ * **Best for**: Grid layouts, dashboard panels, adaptive containers
1454
+ * **Behavior**: Automatically resizes to fit available space while maintaining
1455
+ * consistent proportional appearance across all sizes.
1456
+ *
1457
+ * ### 2. Fixed Layouts (Traditional)
1458
+ * ```html
1459
+ * <ngx-radial-gauge
1460
+ * [value]="temperature"
1461
+ * [size]="300"
1462
+ * [outerThickness]="36"
1463
+ * [innerThickness]="12" />
1464
+ * ```
1465
+ * **Best for**: Static designs, precise sizing requirements, print layouts
1466
+ * **Behavior**: Exact pixel control over all dimensions, predictable appearance.
1467
+ *
1468
+ * ### 3. Scalable Designs
1469
+ * ```html
1470
+ * <ngx-radial-gauge
1471
+ * [value]="batteryLevel"
1472
+ * [size]="gaugeSize"
1473
+ * [responsiveMode]="true"
1474
+ * [sizeToThicknessRatio]="20" />
1475
+ * ```
1476
+ * **Best for**: User-configurable sizing, responsive breakpoints, zoom interfaces
1477
+ * **Behavior**: Manual size control with automatic thickness scaling. As size
1478
+ * increases/decreases, ring thickness scales proportionally to maintain visual balance.
1479
+ *
1480
+ * ## Mathematical Relationships:
1481
+ *
1482
+ * When `responsiveMode=true`, thickness follows this formula:
1483
+ * ```
1484
+ * baseThickness = effectiveSize / sizeToThicknessRatio
1485
+ * outerThickness = baseThickness × responsiveProportions.outer (default: 3)
1486
+ * innerThickness = baseThickness × responsiveProportions.inner (default: 1)
1487
+ * gap = baseThickness × responsiveProportions.gap (default: 0.5)
1488
+ * totalThickness = baseThickness × 4.5 (outer + inner + gap)
1489
+ * ```
1490
+ *
1491
+ * Example with 300px gauge and ratio=20 (ultra-thin):
1492
+ * - baseThickness = 15px
1493
+ * - outerThickness = 45px (15×3)
1494
+ * - innerThickness = 15px (15×1)
1495
+ * - gap = 7.5px (15×0.5)
1496
+ * - totalThickness = 67.5px (22.5% of diameter)
1497
+ *
1498
+ * ## Container Responsiveness:
1499
+ *
1500
+ * When `fitToContainer=true`, the component uses ResizeObserver to:
1501
+ * 1. Monitor parent container dimension changes
1502
+ * 2. Calculate maximum diameter maintaining 2:1 aspect ratio (width:height)
1503
+ * 3. Apply containerPadding for safe margins
1504
+ * 4. Update gauge size in real-time
1505
+ *
1506
+ * This provides true responsive behavior for dashboard widgets, grid layouts,
1507
+ * and adaptive interfaces.
1508
+ *
1509
+ * ## Accessibility:
1510
+ *
1511
+ * The component implements ARIA meter role with proper labeling:
1512
+ * - `role="meter"` for semantic meaning
1513
+ * - `aria-valuemin/max/now` for screen readers
1514
+ * - `aria-label` with contextual information
1515
+ * - Internationalized number formatting
1516
+ *
1517
+ */
1518
+ class RadialGaugeComponent {
1519
+ valueTextEl = viewChild('valueText', ...(ngDevMode ? [{ debugName: "valueTextEl" }] : []));
1520
+ valueGroupEl = viewChild('valueGroup', ...(ngDevMode ? [{ debugName: "valueGroupEl" }] : []));
1521
+ refTextEl = viewChild.required('refText');
1522
+ // Core Inputs - Value and Range
1523
+ value = input(0, ...(ngDevMode ? [{ debugName: "value" }] : []));
1524
+ min = input(0, ...(ngDevMode ? [{ debugName: "min" }] : []));
1525
+ max = input(100, ...(ngDevMode ? [{ debugName: "max" }] : []));
1526
+ segments = input(...(ngDevMode ? [undefined, { debugName: "segments" }] : []));
1527
+ title = input('Gauge', ...(ngDevMode ? [{ debugName: "title" }] : []));
1528
+ description = input('', ...(ngDevMode ? [{ debugName: "description" }] : []));
1529
+ segmentGapPx = input(4, ...(ngDevMode ? [{ debugName: "segmentGapPx" }] : []));
1530
+ // Widget styling inputs
1531
+ /**
1532
+ * Whether the gauge should display with a background.
1533
+ * Affects text color contrast and other visual elements.
1534
+ * @default false
1535
+ */
1536
+ hasBackground = input(false, ...(ngDevMode ? [{ debugName: "hasBackground" }] : []));
1537
+ /**
1538
+ * Whether to display the numeric value label in the center of the gauge.
1539
+ * @default true
1540
+ */
1541
+ showValueLabel = input(true, ...(ngDevMode ? [{ debugName: "showValueLabel" }] : []));
1542
+ // Size Control Inputs
1543
+ /**
1544
+ * Base gauge diameter in pixels. Used as fallback when fitToContainer is false.
1545
+ * @default 300
1546
+ */
1547
+ size = input(300, ...(ngDevMode ? [{ debugName: "size" }] : []));
1548
+ /**
1549
+ * Automatically resize gauge to fit its container dimensions.
1550
+ * When true, the gauge will observe container size changes and adjust accordingly.
1551
+ * Maintains semicircle aspect ratio (2:1 width:height).
1552
+ * @default false
1553
+ */
1554
+ fitToContainer = input(false, ...(ngDevMode ? [{ debugName: "fitToContainer" }] : []));
1555
+ /**
1556
+ * Padding in pixels to maintain from container edges when fitToContainer is true.
1557
+ * @default 10
1558
+ */
1559
+ containerPadding = input(10, ...(ngDevMode ? [{ debugName: "containerPadding" }] : []));
1560
+ // Thickness Control Inputs
1561
+ /**
1562
+ * Use proportional thickness scaling based on gauge size.
1563
+ * When true, all thickness values are calculated as multiples of baseThickness.
1564
+ * Overrides manual outerThickness, innerThickness, and gap inputs.
1565
+ * @default false
1566
+ */
1567
+ responsiveMode = input(false, ...(ngDevMode ? [{ debugName: "responsiveMode" }] : []));
1568
+ /**
1569
+ * Ratio used to calculate base thickness from gauge size.
1570
+ * baseThickness = effectiveSize / sizeToThicknessRatio
1571
+ * Higher values create thinner gauge rings for ultra-thin appearance.
1572
+ * @default 20
1573
+ * @example
1574
+ * - ratio=15: thicker rings (bt = size/15)
1575
+ * - ratio=20: ultra-thin balanced appearance (bt = size/20)
1576
+ * - ratio=30: extremely thin rings (bt = size/30)
1577
+ */
1578
+ sizeToThicknessRatio = input(20, ...(ngDevMode ? [{ debugName: "sizeToThicknessRatio" }] : []));
1579
+ /**
1580
+ * Proportional multipliers for responsive thickness calculations.
1581
+ * - outer: Multiplier for outer ring thickness (default: 3)
1582
+ * - inner: Multiplier for inner ring thickness (default: 1)
1583
+ * - gap: Multiplier for gap between rings (default: 0.5)
1584
+ * Total thickness = baseThickness × (outer + inner + gap) = bt × 4.5
1585
+ * @default { outer: 3, inner: 1, gap: 0.5 }
1586
+ */
1587
+ responsiveProportions = input({ outer: 3, inner: 1, gap: 0.5 }, ...(ngDevMode ? [{ debugName: "responsiveProportions" }] : []));
1588
+ // Manual Thickness Inputs (used when responsiveMode is false)
1589
+ /**
1590
+ * Manual outer ring thickness in pixels. Ignored when responsiveMode is true.
1591
+ * @default 36
1592
+ */
1593
+ outerThickness = input(36, ...(ngDevMode ? [{ debugName: "outerThickness" }] : []));
1594
+ /**
1595
+ * Manual inner ring thickness in pixels. Ignored when responsiveMode is true.
1596
+ * @default 12
1597
+ */
1598
+ innerThickness = input(12, ...(ngDevMode ? [{ debugName: "innerThickness" }] : []));
1599
+ /**
1600
+ * Manual gap between rings in pixels. Ignored when responsiveMode is true.
1601
+ * @default 8
1602
+ */
1603
+ gap = input(8, ...(ngDevMode ? [{ debugName: "gap" }] : []));
1604
+ titleId = `rg-title-${Math.random().toString(36).slice(2)}`;
1605
+ descId = `rg-desc-${Math.random().toString(36).slice(2)}`;
1606
+ clipId = `rg-clip-${Math.random().toString(36).slice(2)}`;
1607
+ locale = inject(LOCALE_ID);
1608
+ elementRef = inject((ElementRef));
1609
+ destroyRef = inject(DestroyRef);
1610
+ nf = new Intl.NumberFormat(this.locale, {
1611
+ maximumFractionDigits: 1,
1612
+ });
1613
+ // Container Size Detection
1614
+ /**
1615
+ * Tracks the container's available size for responsive sizing.
1616
+ * Updated by ResizeObserver when fitToContainer is enabled.
1617
+ * @private
1618
+ */
1619
+ containerSize = signal(null, ...(ngDevMode ? [{ debugName: "containerSize" }] : []));
1620
+ /**
1621
+ * ResizeObserver instance for monitoring container size changes.
1622
+ * Created when fitToContainer is enabled, destroyed on component cleanup.
1623
+ * @private
1624
+ */
1625
+ resizeObserver = null;
1626
+ viewReady = toSignal(from(new Promise((resolve) => afterNextRender(resolve))).pipe(map(() => true)), { initialValue: false });
1627
+ fontsReady = toSignal(typeof document !== 'undefined' && 'fonts' in document
1628
+ ? from(document.fonts.ready).pipe(map(() => true))
1629
+ : of(true), // SSR or older browsers: treat as ready
1630
+ { initialValue: false });
1631
+ constructor() {
1632
+ this.destroyRef.onDestroy(() => {
1633
+ if (this.resizeObserver) {
1634
+ this.resizeObserver.disconnect();
1635
+ this.resizeObserver = null;
1636
+ }
1637
+ });
1638
+ }
1639
+ /**
1640
+ * Effect that manages ResizeObserver lifecycle based on fitToContainer input.
1641
+ * Automatically connects/disconnects observer when the input changes.
1642
+ * @private
1643
+ */
1644
+ containerObserverEffect = effect(() => {
1645
+ const shouldObserve = this.fitToContainer();
1646
+ if (shouldObserve && !this.resizeObserver) {
1647
+ // Create and start observing
1648
+ this.resizeObserver = new ResizeObserver((entries) => {
1649
+ const entry = entries[0];
1650
+ if (!entry)
1651
+ return;
1652
+ const { width, height } = entry.contentRect;
1653
+ const padding = this.containerPadding();
1654
+ const availW = Math.max(0, width - padding * 2);
1655
+ const availH = Math.max(0, height - padding);
1656
+ let sFromW;
1657
+ let sFromH;
1658
+ if (this.responsiveMode()) {
1659
+ // In responsive mode: outerThickness = 3 * baseThickness = 3 * size / ratio
1660
+ // Total space needed = size + outerThickness = size + 3*size/ratio = size * (1 + 3/ratio)
1661
+ const ratio = this.sizeToThicknessRatio();
1662
+ const spaceFactor = 1 + 3 / ratio; // Total space factor
1663
+ sFromW = availW / spaceFactor;
1664
+ sFromH = (2 * availH) / spaceFactor;
1665
+ }
1666
+ else {
1667
+ // Manual thickness: outer thickness is fixed
1668
+ const outerT = this.outerThickness();
1669
+ sFromW = Math.max(0, availW - outerT);
1670
+ sFromH = Math.max(0, 2 * availH - outerT);
1671
+ }
1672
+ const maxDiameter = Math.min(sFromW, sFromH);
1673
+ this.containerSize.set(Math.max(maxDiameter, 50));
1674
+ });
1675
+ this.resizeObserver.observe(this.elementRef.nativeElement);
1676
+ }
1677
+ else if (!shouldObserve && this.resizeObserver) {
1678
+ // Stop observing and cleanup
1679
+ this.resizeObserver.disconnect();
1680
+ this.resizeObserver = null;
1681
+ this.containerSize.set(null);
1682
+ }
1683
+ }, ...(ngDevMode ? [{ debugName: "containerObserverEffect" }] : []));
1684
+ // ── Build the reference string reactively ───────────────────────────────────
1685
+ referenceString = computed(() => {
1686
+ const ref = this.labelReference();
1687
+ if (typeof ref === 'string')
1688
+ return ref;
1689
+ if (typeof ref === 'number' && ref > 0) {
1690
+ const g = this.referenceGlyph() ?? '0';
1691
+ return g.repeat(ref);
1692
+ }
1693
+ return this.formattedLabel(); // measure actual label
1694
+ }, ...(ngDevMode ? [{ debugName: "referenceString" }] : []));
1695
+ // ── Core transform: center + uniform scale to fit the reserved box ──────────
1696
+ valueTransform = computed(() => {
1697
+ if (!this.showValueLabel())
1698
+ return '';
1699
+ // ensure we wait for first paint + font shaping
1700
+ this.viewReady();
1701
+ this.fontsReady();
1702
+ const cx = this.centerX();
1703
+ const cy = this.centerY();
1704
+ const r = this.legendInnerRadius();
1705
+ const pad = this.labelPadding();
1706
+ const boxWidth = Math.max(0, 2 * r - 2 * pad);
1707
+ const boxHeight = Math.max(0, r - pad);
1708
+ // If geometry is degenerate, just center.
1709
+ if (!boxWidth || !boxHeight)
1710
+ return `translate(${cx},${cy})`;
1711
+ // Measure the actual label (for height) and the reference (for width)
1712
+ const labelEl = this.valueTextEl()?.nativeElement;
1713
+ const refEl = this.refTextEl().nativeElement;
1714
+ if (!labelEl)
1715
+ return `translate(${cx},${cy})`;
1716
+ // Important: ensure text nodes are up to date before reading BBox
1717
+ // (Angular's computed/effect guarantees sync within the same microtask)
1718
+ const labelBox = this.safeBBox(labelEl);
1719
+ const refBox = this.safeBBox(refEl);
1720
+ // Use reference width and actual label height
1721
+ const widthForFit = refBox.width || labelBox.width || 1;
1722
+ const heightForFit = labelBox.height || refBox.height || 1;
1723
+ const s = Math.min(boxWidth / widthForFit, boxHeight / heightForFit) *
1724
+ this.baselineSafety();
1725
+ return `translate(${cx},${cy}) scale(${s})`;
1726
+ }, ...(ngDevMode ? [{ debugName: "valueTransform" }] : []));
1727
+ /** Guarded getBBox that avoids 0/NaN on detached or invisible nodes. */
1728
+ safeBBox(node) {
1729
+ try {
1730
+ const box = node.getBBox();
1731
+ // Firefox/Safari can occasionally return 0 when text hasn’t painted yet; fall back to a rough estimate.
1732
+ if (box && (box.width > 0 || box.height > 0))
1733
+ return box;
1734
+ }
1735
+ catch {
1736
+ /* ignore */
1737
+ }
1738
+ // Fallback guess to avoid divide-by-zero (tuned small; will get corrected next tick)
1739
+ return new DOMRect(0, 0, 1, 1);
1740
+ }
1741
+ // Responsive Size and Thickness Calculations
1742
+ /**
1743
+ * The effective gauge diameter, accounting for container sizing and manual size input.
1744
+ * Priority: containerSize (when fitToContainer=true) > manual size input
1745
+ * @returns Effective diameter in pixels
1746
+ *
1747
+ * @example
1748
+ * // Fixed size mode
1749
+ * fitToContainer=false, size=300 → effectiveSize=300
1750
+ *
1751
+ * // Container responsive mode
1752
+ * fitToContainer=true, container=400px wide → effectiveSize=380 (minus padding)
1753
+ */
1754
+ effectiveSize = computed(() => {
1755
+ const containerDiameter = this.containerSize();
1756
+ if (this.fitToContainer() && containerDiameter !== null) {
1757
+ return containerDiameter;
1758
+ }
1759
+ return this.size();
1760
+ }, ...(ngDevMode ? [{ debugName: "effectiveSize" }] : []));
1761
+ /**
1762
+ * Base thickness calculated from effective size for proportional scaling.
1763
+ * Only used when responsiveMode is enabled.
1764
+ * Formula: baseThickness = effectiveSize / sizeToThicknessRatio
1765
+ * @returns Base thickness in pixels, or 0 when responsiveMode is false
1766
+ *
1767
+ * @example
1768
+ * // effectiveSize=300, sizeToThicknessRatio=12
1769
+ * baseThickness = 300/12 = 25px
1770
+ * // Total ring thickness = 25 × 4.5 = 112.5px (37.5% of diameter)
1771
+ */
1772
+ baseThickness = computed(() => {
1773
+ if (!this.responsiveMode())
1774
+ return 0;
1775
+ return this.effectiveSize() / this.sizeToThicknessRatio();
1776
+ }, ...(ngDevMode ? [{ debugName: "baseThickness" }] : []));
1777
+ /**
1778
+ * Effective outer ring thickness, supporting both manual and responsive modes.
1779
+ * - Responsive mode: baseThickness × responsiveProportions.outer
1780
+ * - Manual mode: outerThickness input value
1781
+ * @returns Outer ring thickness in pixels
1782
+ */
1783
+ effectiveOuterThickness = computed(() => {
1784
+ if (this.responsiveMode()) {
1785
+ return this.baseThickness() * this.responsiveProportions().outer;
1786
+ }
1787
+ return this.outerThickness();
1788
+ }, ...(ngDevMode ? [{ debugName: "effectiveOuterThickness" }] : []));
1789
+ /**
1790
+ * Effective inner ring thickness, supporting both manual and responsive modes.
1791
+ * - Responsive mode: baseThickness × responsiveProportions.inner
1792
+ * - Manual mode: innerThickness input value
1793
+ * @returns Inner ring thickness in pixels
1794
+ */
1795
+ effectiveInnerThickness = computed(() => {
1796
+ if (this.responsiveMode()) {
1797
+ return this.baseThickness() * this.responsiveProportions().inner;
1798
+ }
1799
+ return this.innerThickness();
1800
+ }, ...(ngDevMode ? [{ debugName: "effectiveInnerThickness" }] : []));
1801
+ /**
1802
+ * Effective gap between rings, supporting both manual and responsive modes.
1803
+ * - Responsive mode: baseThickness × responsiveProportions.gap
1804
+ * - Manual mode: gap input value
1805
+ * @returns Gap between rings in pixels
1806
+ */
1807
+ effectiveGap = computed(() => {
1808
+ if (this.responsiveMode()) {
1809
+ return this.baseThickness() * this.responsiveProportions().gap;
1810
+ }
1811
+ return this.gap();
1812
+ }, ...(ngDevMode ? [{ debugName: "effectiveGap" }] : []));
1813
+ // SVG Layout Calculations
1814
+ svgPadding = computed(() => this.effectiveOuterThickness() / 2, ...(ngDevMode ? [{ debugName: "svgPadding" }] : []));
1815
+ svgWidth = computed(() => this.effectiveSize() + this.effectiveOuterThickness(), ...(ngDevMode ? [{ debugName: "svgWidth" }] : []));
1816
+ svgHeight = computed(() => Math.ceil(this.effectiveSize() / 2 + this.effectiveOuterThickness() / 2), ...(ngDevMode ? [{ debugName: "svgHeight" }] : []));
1817
+ centerX = computed(() => this.effectiveSize() / 2 + this.effectiveOuterThickness() / 2, ...(ngDevMode ? [{ debugName: "centerX" }] : []));
1818
+ centerY = computed(() => this.effectiveSize() / 2 + this.effectiveOuterThickness() / 2, ...(ngDevMode ? [{ debugName: "centerY" }] : []));
1819
+ /**
1820
+ * If a string is provided, we measure it and allocate space for that width.
1821
+ * If a number is provided, we build a string of that many `referenceGlyph`s.
1822
+ * If omitted, we fall back to measuring the actual label.
1823
+ */
1824
+ labelReference = input(undefined, ...(ngDevMode ? [{ debugName: "labelReference" }] : []));
1825
+ /** Glyph to repeat when labelReference is a number (defaults to '0'). */
1826
+ referenceGlyph = input('0', ...(ngDevMode ? [{ debugName: "referenceGlyph" }] : []));
1827
+ /** Extra breathing room inside the inner semicircle box (in px). */
1828
+ labelPadding = input(4, ...(ngDevMode ? [{ debugName: "labelPadding" }] : []));
1829
+ /** Safety multiplier to avoid clipping ascenders/descenders. */
1830
+ baselineSafety = input(0.95, ...(ngDevMode ? [{ debugName: "baselineSafety" }] : []));
1831
+ outerRadius = computed(() => this.effectiveSize() / 2, ...(ngDevMode ? [{ debugName: "outerRadius" }] : []));
1832
+ innerRadius = computed(() => this.outerRadius() -
1833
+ this.effectiveOuterThickness() / 2 -
1834
+ this.effectiveGap(), ...(ngDevMode ? [{ debugName: "innerRadius" }] : []));
1835
+ legendOuterRadius = computed(() => this.outerRadius() -
1836
+ this.effectiveOuterThickness() / 2 -
1837
+ this.effectiveGap() -
1838
+ this.effectiveInnerThickness() / 2, ...(ngDevMode ? [{ debugName: "legendOuterRadius" }] : []));
1839
+ legendInnerRadius = computed(() => this.legendOuterRadius() - this.effectiveInnerThickness(), ...(ngDevMode ? [{ debugName: "legendInnerRadius" }] : []));
1840
+ startAngle = -180;
1841
+ endAngle = 0;
1842
+ clampedValue = computed(() => this.clamp(this.value(), this.min(), this.max()), ...(ngDevMode ? [{ debugName: "clampedValue" }] : []));
1843
+ percentage = computed(() => {
1844
+ const range = this.max() - this.min();
1845
+ if (range === 0)
1846
+ return 0;
1847
+ return (this.clampedValue() - this.min()) / range;
1848
+ }, ...(ngDevMode ? [{ debugName: "percentage" }] : []));
1849
+ percent = computed(() => Math.round(this.percentage() * 100), ...(ngDevMode ? [{ debugName: "percent" }] : []));
1850
+ defaultSegments = computed(() => {
1851
+ const minVal = this.min();
1852
+ const maxVal = this.max();
1853
+ const range = maxVal - minVal;
1854
+ return [
1855
+ {
1856
+ from: minVal,
1857
+ to: minVal + 0.6 * range,
1858
+ color: 'var(--gauge-value-critical, #dc2626)',
1859
+ },
1860
+ {
1861
+ from: minVal + 0.6 * range,
1862
+ to: minVal + 0.8 * range,
1863
+ color: 'var(--gauge-value-warning, #f59e0b)',
1864
+ },
1865
+ {
1866
+ from: minVal + 0.8 * range,
1867
+ to: maxVal,
1868
+ color: 'var(--gauge-value-good, #10b981)',
1869
+ },
1870
+ ];
1871
+ }, ...(ngDevMode ? [{ debugName: "defaultSegments" }] : []));
1872
+ actualSegments = computed(() => this.segments() || this.defaultSegments(), ...(ngDevMode ? [{ debugName: "actualSegments" }] : []));
1873
+ formattedLabel = computed(() => this.nf.format(this.clampedValue()), ...(ngDevMode ? [{ debugName: "formattedLabel" }] : []));
1874
+ valueColor = computed(() => {
1875
+ const v = this.clampedValue();
1876
+ const segs = this.actualSegments();
1877
+ for (const s of segs) {
1878
+ if (v >= s.from && v <= s.to)
1879
+ return s.color;
1880
+ }
1881
+ return segs.at(-1)?.color ?? 'var(--mat-sys-primary)';
1882
+ }, ...(ngDevMode ? [{ debugName: "valueColor" }] : []));
1883
+ backgroundArcPath = computed(() => this.createArcPath(this.outerRadius(), this.startAngle, this.endAngle), ...(ngDevMode ? [{ debugName: "backgroundArcPath" }] : []));
1884
+ segmentPaths = computed(() => {
1885
+ const segs = this.actualSegments();
1886
+ const minVal = this.min();
1887
+ const maxVal = this.max();
1888
+ const range = maxVal - minVal;
1889
+ if (!range)
1890
+ return [];
1891
+ const r = this.legendOuterRadius();
1892
+ const gapDeg = this.gapDegreesForRadius(this.segmentGapPx(), r);
1893
+ return segs
1894
+ .map((s, i) => {
1895
+ const startPct = this.clamp((s.from - minVal) / range, 0, 1);
1896
+ const endPct = this.clamp((s.to - minVal) / range, 0, 1);
1897
+ let a0 = this.angleForPercentage(startPct);
1898
+ let a1 = this.angleForPercentage(endPct);
1899
+ if (i > 0)
1900
+ a0 += gapDeg / 2;
1901
+ if (i < segs.length - 1)
1902
+ a1 -= gapDeg / 2;
1903
+ if (a1 <= a0)
1904
+ return null;
1905
+ return { path: this.createArcPath(r, a0, a1), color: s.color };
1906
+ })
1907
+ .filter((x) => !!x);
1908
+ }, ...(ngDevMode ? [{ debugName: "segmentPaths" }] : []));
1909
+ ariaLabel = computed(() => `${this.title()}: ${this.formattedLabel()} (range ${this.min()}–${this.max()})`, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
1910
+ clamp(v, min, max) {
1911
+ return Math.min(Math.max(v, min), max);
1912
+ }
1913
+ /**
1914
+ * Converts a percentage (0-1) to an angle position on the gauge arc.
1915
+ * The gauge spans from startAngle (-180°) to endAngle (0°), creating a semicircle.
1916
+ * @param p - Percentage value between 0 and 1
1917
+ * @returns Angle in degrees for the given percentage along the gauge arc
1918
+ * @example
1919
+ * angleForPercentage(0) => -180° (start of gauge)
1920
+ * angleForPercentage(0.5) => -90° (middle of gauge)
1921
+ * angleForPercentage(1) => 0° (end of gauge)
1922
+ */
1923
+ angleForPercentage(p) {
1924
+ return this.startAngle + (this.endAngle - this.startAngle) * p;
1925
+ }
1926
+ /**
1927
+ * Converts polar coordinates (radius, angle) to Cartesian coordinates (x, y).
1928
+ * Uses standard trigonometric conversion where angle 0° points to the right (3 o'clock).
1929
+ * @param cx - Center X coordinate of the circle
1930
+ * @param cy - Center Y coordinate of the circle
1931
+ * @param r - Radius distance from center
1932
+ * @param angle - Angle in degrees (0° = right, 90° = down, 180° = left, -90° = up)
1933
+ * @returns Object with x and y Cartesian coordinates
1934
+ * @example
1935
+ * polarToCartesian(100, 100, 50, 0) => {x: 150, y: 100} // 3 o'clock
1936
+ * polarToCartesian(100, 100, 50, -90) => {x: 100, y: 50} // 12 o'clock
1937
+ */
1938
+ polarToCartesian(cx, cy, r, angle) {
1939
+ const a = (angle * Math.PI) / 180;
1940
+ return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) };
1941
+ }
1942
+ /**
1943
+ * Creates an SVG path string for a circular arc segment.
1944
+ * Uses SVG arc path commands to draw an arc from start angle to end angle.
1945
+ * @param r - Radius of the arc
1946
+ * @param a0 - Starting angle in degrees
1947
+ * @param a1 - Ending angle in degrees
1948
+ * @returns SVG path string defining the arc
1949
+ * @example
1950
+ * createArcPath(50, -180, 0) => "M cx-50 cy A 50 50 0 1 1 cx+50 cy"
1951
+ * This creates a semicircle from left (-180°) to right (0°)
1952
+ *
1953
+ * SVG Arc Parameters:
1954
+ * - rx, ry: Radii (equal for circular arc)
1955
+ * - x-axis-rotation: 0 (no rotation for circles)
1956
+ * - large-arc-flag: 1 if arc > 180°, 0 otherwise
1957
+ * - sweep-flag: 1 for clockwise, 0 for counter-clockwise
1958
+ */
1959
+ createArcPath(r, a0, a1) {
1960
+ const cx = this.centerX(), cy = this.centerY();
1961
+ const start = this.polarToCartesian(cx, cy, r, a0);
1962
+ const end = this.polarToCartesian(cx, cy, r, a1);
1963
+ const largeArc = Math.abs(a1 - a0) > 180 ? 1 : 0;
1964
+ const sweep = a1 > a0 ? 1 : 0;
1965
+ return `M ${start.x} ${start.y} A ${r} ${r} 0 ${largeArc} ${sweep} ${end.x} ${end.y}`;
1966
+ }
1967
+ /**
1968
+ * Calculates the angular gap in degrees needed for a specific pixel gap at a given radius.
1969
+ * Used to create visual separation between legend segments.
1970
+ * @param px - Desired gap size in pixels
1971
+ * @param r - Radius at which the gap will appear
1972
+ * @returns Gap size in degrees, clamped between 0° and 180°
1973
+ * @example
1974
+ * For a 4px gap on a radius of 100px:
1975
+ * Arc length = π * 100 = 314.16px (semicircle)
1976
+ * Degrees = 180 * (4 / 314.16) ≈ 2.3°
1977
+ *
1978
+ * Mathematical basis:
1979
+ * - Semicircle arc length = π * r
1980
+ * - Ratio of gap to semicircle = px / (π * r)
1981
+ * - Convert ratio to degrees by multiplying by 180°
1982
+ */
1983
+ gapDegreesForRadius(px, r) {
1984
+ const semicircumference = Math.PI * r;
1985
+ return 180 * this.clamp(px / semicircumference, 0, 1);
1986
+ }
1987
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RadialGaugeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1988
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: RadialGaugeComponent, isStandalone: true, selector: "ngx-radial-gauge", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, segments: { classPropertyName: "segments", publicName: "segments", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, segmentGapPx: { classPropertyName: "segmentGapPx", publicName: "segmentGapPx", isSignal: true, isRequired: false, transformFunction: null }, hasBackground: { classPropertyName: "hasBackground", publicName: "hasBackground", isSignal: true, isRequired: false, transformFunction: null }, showValueLabel: { classPropertyName: "showValueLabel", publicName: "showValueLabel", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fitToContainer: { classPropertyName: "fitToContainer", publicName: "fitToContainer", isSignal: true, isRequired: false, transformFunction: null }, containerPadding: { classPropertyName: "containerPadding", publicName: "containerPadding", isSignal: true, isRequired: false, transformFunction: null }, responsiveMode: { classPropertyName: "responsiveMode", publicName: "responsiveMode", isSignal: true, isRequired: false, transformFunction: null }, sizeToThicknessRatio: { classPropertyName: "sizeToThicknessRatio", publicName: "sizeToThicknessRatio", isSignal: true, isRequired: false, transformFunction: null }, responsiveProportions: { classPropertyName: "responsiveProportions", publicName: "responsiveProportions", isSignal: true, isRequired: false, transformFunction: null }, outerThickness: { classPropertyName: "outerThickness", publicName: "outerThickness", isSignal: true, isRequired: false, transformFunction: null }, innerThickness: { classPropertyName: "innerThickness", publicName: "innerThickness", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, labelReference: { classPropertyName: "labelReference", publicName: "labelReference", isSignal: true, isRequired: false, transformFunction: null }, referenceGlyph: { classPropertyName: "referenceGlyph", publicName: "referenceGlyph", isSignal: true, isRequired: false, transformFunction: null }, labelPadding: { classPropertyName: "labelPadding", publicName: "labelPadding", isSignal: true, isRequired: false, transformFunction: null }, baselineSafety: { classPropertyName: "baselineSafety", publicName: "baselineSafety", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "meter" }, properties: { "attr.aria-label": "ariaLabel()", "attr.aria-valuemin": "min()", "attr.aria-valuemax": "max()", "attr.aria-valuenow": "clampedValue()", "attr.aria-valuetext": "formattedLabel()", "attr.aria-labelledby": "titleId", "attr.aria-describedby": "descId", "class.fit-container": "fitToContainer()", "class.has-background": "hasBackground()" } }, viewQueries: [{ propertyName: "valueTextEl", first: true, predicate: ["valueText"], descendants: true, isSignal: true }, { propertyName: "valueGroupEl", first: true, predicate: ["valueGroup"], descendants: true, isSignal: true }, { propertyName: "refTextEl", first: true, predicate: ["refText"], descendants: true, isSignal: true }], ngImport: i0, template: "@let w = svgWidth(); @let h = svgHeight(); @let cy = centerY(); @let pct =\r\npercent();\r\n\r\n<svg\r\n [attr.width]=\"fitToContainer() ? null : w\"\r\n [attr.height]=\"fitToContainer() ? null : h\"\r\n [attr.viewBox]=\"'0 0 ' + w + ' ' + h\"\r\n [class.responsive]=\"fitToContainer()\"\r\n class=\"gauge-svg\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n>\r\n <title [attr.id]=\"titleId\">{{ title() }}</title>\r\n @if (description()) {\r\n <desc [attr.id]=\"descId\">{{ description() }}</desc>\r\n }\r\n\r\n <defs>\r\n <clipPath [attr.id]=\"clipId\">\r\n <!-- Give a tiny extra room equal to half of the outer stroke to avoid anti-alias cutoff at the baseline -->\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n [attr.width]=\"w\"\r\n [attr.height]=\"cy + effectiveOuterThickness() / 2\"\r\n />\r\n </clipPath>\r\n </defs>\r\n\r\n <g [attr.clip-path]=\"'url(#' + clipId + ')'\">\r\n <path\r\n [attr.d]=\"backgroundArcPath()\"\r\n pathLength=\"100\"\r\n fill=\"none\"\r\n class=\"gauge-background\"\r\n [attr.stroke-width]=\"effectiveOuterThickness()\"\r\n stroke-linecap=\"butt\"\r\n />\r\n\r\n <path\r\n [attr.d]=\"backgroundArcPath()\"\r\n pathLength=\"100\"\r\n fill=\"none\"\r\n class=\"gauge-value\"\r\n [attr.stroke]=\"valueColor()\"\r\n [attr.stroke-width]=\"effectiveOuterThickness()\"\r\n stroke-linecap=\"butt\"\r\n [attr.stroke-dasharray]=\"pct + ' 100'\"\r\n />\r\n\r\n @for (segment of segmentPaths(); track segment.path) {\r\n <path\r\n [attr.d]=\"segment.path\"\r\n fill=\"none\"\r\n [attr.stroke]=\"segment.color\"\r\n [attr.stroke-width]=\"effectiveInnerThickness()\"\r\n stroke-linecap=\"butt\"\r\n class=\"gauge-segment\"\r\n />\r\n }\r\n </g>\r\n\r\n @if (showValueLabel()) {\r\n <g #valueGroup [attr.transform]=\"valueTransform()\">\r\n <text\r\n #valueText\r\n class=\"gauge-value-text\"\r\n x=\"0\"\r\n y=\"0\"\r\n text-anchor=\"middle\"\r\n alignment-baseline=\"baseline\"\r\n dy=\"-0.75\"\r\n >\r\n {{ formattedLabel() }}\r\n </text>\r\n </g>\r\n }\r\n\r\n <!-- Hidden reference text used ONLY for width measurement -->\r\n <g style=\"visibility: hidden; pointer-events: none\" aria-hidden=\"true\">\r\n <text\r\n #refText\r\n x=\"0\"\r\n y=\"0\"\r\n text-anchor=\"start\"\r\n dominant-baseline=\"alphabetic\"\r\n >\r\n {{ referenceString() }}\r\n </text>\r\n </g>\r\n</svg>\r\n", styles: [":host{display:block;--gauge-outer-bg: var(--mat-sys-surface-variant, #e0e0e0);--gauge-value-good: var(--mat-sys-tertiary, #10b981);--gauge-value-warning: var(--mat-sys-secondary, #f59e0b);--gauge-value-critical: var(--mat-sys-error, #dc2626)}:host.fit-container{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.gauge-svg{display:block;margin-inline:auto;max-width:100%;height:auto;shape-rendering:geometricPrecision}.gauge-svg.responsive{max-width:100%;max-height:100%;width:auto;height:auto}.gauge-background{stroke:var(--gauge-outer-bg);transition:stroke .2s ease}.gauge-value{transition:stroke-dasharray .2s ease,stroke .2s ease}.gauge-segment{transition:stroke .2s ease;opacity:.9}.gauge-segment:hover{opacity:1}@media (prefers-reduced-motion: reduce){.gauge-value,.gauge-segment{transition:none}}@media (prefers-contrast: high){.gauge-background{stroke-width:2;stroke:var(--mat-sys-outline, #000000)}.gauge-segment{opacity:1}}@media (prefers-color-scheme: dark){:host{--gauge-outer-bg: #374151}}.gauge-value-text{fill:var(--mat-sys-on-surface-variant, #6c757d);font-family:var(--mat-sys-typescale-body-large-font, \"Roboto\", sans-serif);font-weight:var(--mat-sys-typescale-body-large-weight, 400);transition:fill var(--mat-sys-motion-duration-short2, .2s) var(--mat-sys-motion-easing-standard, ease)}:host.has-background .gauge-value-text{fill:var(--mat-sys-on-surface, #1f1f1f)}:host:hover .gauge-value-text{fill:var(--mat-sys-primary, #6750a4)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1989
+ }
1990
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RadialGaugeComponent, decorators: [{
1991
+ type: Component,
1992
+ args: [{ selector: 'ngx-radial-gauge', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, host: {
1993
+ role: 'meter',
1994
+ '[attr.aria-label]': 'ariaLabel()',
1995
+ '[attr.aria-valuemin]': 'min()',
1996
+ '[attr.aria-valuemax]': 'max()',
1997
+ '[attr.aria-valuenow]': 'clampedValue()',
1998
+ '[attr.aria-valuetext]': 'formattedLabel()',
1999
+ '[attr.aria-labelledby]': 'titleId',
2000
+ '[attr.aria-describedby]': 'descId',
2001
+ '[class.fit-container]': 'fitToContainer()',
2002
+ '[class.has-background]': 'hasBackground()',
2003
+ }, template: "@let w = svgWidth(); @let h = svgHeight(); @let cy = centerY(); @let pct =\r\npercent();\r\n\r\n<svg\r\n [attr.width]=\"fitToContainer() ? null : w\"\r\n [attr.height]=\"fitToContainer() ? null : h\"\r\n [attr.viewBox]=\"'0 0 ' + w + ' ' + h\"\r\n [class.responsive]=\"fitToContainer()\"\r\n class=\"gauge-svg\"\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n>\r\n <title [attr.id]=\"titleId\">{{ title() }}</title>\r\n @if (description()) {\r\n <desc [attr.id]=\"descId\">{{ description() }}</desc>\r\n }\r\n\r\n <defs>\r\n <clipPath [attr.id]=\"clipId\">\r\n <!-- Give a tiny extra room equal to half of the outer stroke to avoid anti-alias cutoff at the baseline -->\r\n <rect\r\n x=\"0\"\r\n y=\"0\"\r\n [attr.width]=\"w\"\r\n [attr.height]=\"cy + effectiveOuterThickness() / 2\"\r\n />\r\n </clipPath>\r\n </defs>\r\n\r\n <g [attr.clip-path]=\"'url(#' + clipId + ')'\">\r\n <path\r\n [attr.d]=\"backgroundArcPath()\"\r\n pathLength=\"100\"\r\n fill=\"none\"\r\n class=\"gauge-background\"\r\n [attr.stroke-width]=\"effectiveOuterThickness()\"\r\n stroke-linecap=\"butt\"\r\n />\r\n\r\n <path\r\n [attr.d]=\"backgroundArcPath()\"\r\n pathLength=\"100\"\r\n fill=\"none\"\r\n class=\"gauge-value\"\r\n [attr.stroke]=\"valueColor()\"\r\n [attr.stroke-width]=\"effectiveOuterThickness()\"\r\n stroke-linecap=\"butt\"\r\n [attr.stroke-dasharray]=\"pct + ' 100'\"\r\n />\r\n\r\n @for (segment of segmentPaths(); track segment.path) {\r\n <path\r\n [attr.d]=\"segment.path\"\r\n fill=\"none\"\r\n [attr.stroke]=\"segment.color\"\r\n [attr.stroke-width]=\"effectiveInnerThickness()\"\r\n stroke-linecap=\"butt\"\r\n class=\"gauge-segment\"\r\n />\r\n }\r\n </g>\r\n\r\n @if (showValueLabel()) {\r\n <g #valueGroup [attr.transform]=\"valueTransform()\">\r\n <text\r\n #valueText\r\n class=\"gauge-value-text\"\r\n x=\"0\"\r\n y=\"0\"\r\n text-anchor=\"middle\"\r\n alignment-baseline=\"baseline\"\r\n dy=\"-0.75\"\r\n >\r\n {{ formattedLabel() }}\r\n </text>\r\n </g>\r\n }\r\n\r\n <!-- Hidden reference text used ONLY for width measurement -->\r\n <g style=\"visibility: hidden; pointer-events: none\" aria-hidden=\"true\">\r\n <text\r\n #refText\r\n x=\"0\"\r\n y=\"0\"\r\n text-anchor=\"start\"\r\n dominant-baseline=\"alphabetic\"\r\n >\r\n {{ referenceString() }}\r\n </text>\r\n </g>\r\n</svg>\r\n", styles: [":host{display:block;--gauge-outer-bg: var(--mat-sys-surface-variant, #e0e0e0);--gauge-value-good: var(--mat-sys-tertiary, #10b981);--gauge-value-warning: var(--mat-sys-secondary, #f59e0b);--gauge-value-critical: var(--mat-sys-error, #dc2626)}:host.fit-container{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.gauge-svg{display:block;margin-inline:auto;max-width:100%;height:auto;shape-rendering:geometricPrecision}.gauge-svg.responsive{max-width:100%;max-height:100%;width:auto;height:auto}.gauge-background{stroke:var(--gauge-outer-bg);transition:stroke .2s ease}.gauge-value{transition:stroke-dasharray .2s ease,stroke .2s ease}.gauge-segment{transition:stroke .2s ease;opacity:.9}.gauge-segment:hover{opacity:1}@media (prefers-reduced-motion: reduce){.gauge-value,.gauge-segment{transition:none}}@media (prefers-contrast: high){.gauge-background{stroke-width:2;stroke:var(--mat-sys-outline, #000000)}.gauge-segment{opacity:1}}@media (prefers-color-scheme: dark){:host{--gauge-outer-bg: #374151}}.gauge-value-text{fill:var(--mat-sys-on-surface-variant, #6c757d);font-family:var(--mat-sys-typescale-body-large-font, \"Roboto\", sans-serif);font-weight:var(--mat-sys-typescale-body-large-weight, 400);transition:fill var(--mat-sys-motion-duration-short2, .2s) var(--mat-sys-motion-easing-standard, ease)}:host.has-background .gauge-value-text{fill:var(--mat-sys-on-surface, #1f1f1f)}:host:hover .gauge-value-text{fill:var(--mat-sys-primary, #6750a4)}\n"] }]
2004
+ }], ctorParameters: () => [] });
2005
+
2006
+ // radial-gauge-widget.metadata.ts
2007
+ const svgIcon = `
2008
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 55" fill="currentColor">
2009
+ <defs>
2010
+ <clipPath id="gauge-clip"><rect x="0" y="0" width="100" height="52"/></clipPath>
2011
+
2012
+ <!-- Outer arc geometry (radius 40, stroke 8) -->
2013
+ <path id="outerArc" d="M 10 50 A 40 40 0 0 1 90 50" pathLength="100"/>
2014
+
2015
+ <!-- Inner arc geometry (radius 31) -->
2016
+ <path id="innerArc" d="M 19 50 A 31 31 0 0 1 81 50" pathLength="100"/>
2017
+ </defs>
2018
+
2019
+ <g clip-path="url(#gauge-clip)" stroke="currentColor" fill="none" stroke-linecap="butt">
2020
+ <!-- Outer background arc -->
2021
+ <use href="#outerArc" stroke-width="8" opacity="0.2"/>
2022
+
2023
+ <!-- Value arc: 65% -->
2024
+ <use href="#outerArc" stroke-width="8" stroke-dasharray="65 100"/>
2025
+
2026
+ <!-- Inner legend segments (single geometry with dash windows) -->
2027
+ <!-- 0–60% -->
2028
+ <use href="#innerArc" stroke-width="4" opacity="0.2"
2029
+ stroke-dasharray="60 100" stroke-dashoffset="0"/>
2030
+ <!-- 60–80% -->
2031
+ <use href="#innerArc" stroke-width="4" opacity="0.4"
2032
+ stroke-dasharray="20 100" stroke-dashoffset="60"/>
2033
+ <!-- 0–100% (full half-circle), same color as value arc -->
2034
+ <use href="#innerArc" stroke-width="4"
2035
+ stroke-dasharray="100 100" stroke-dashoffset="0"/>
2036
+ <!-- (Alternatively, you can omit dash attributes entirely on this one:
2037
+ <use href="#innerArc" stroke-width="4"/> ) -->
2038
+ </g>
2039
+ </svg>
2040
+ `;
2041
+
2042
+ class RadialGaugeStateDialogComponent {
2043
+ data = inject(MAT_DIALOG_DATA);
2044
+ dialogRef = inject((MatDialogRef));
2045
+ localState = {
2046
+ value: this.data.value ?? 50,
2047
+ colorProfile: this.data.colorProfile ?? 'dynamic',
2048
+ active: this.data.active ?? false,
2049
+ hasBackground: this.data.hasBackground ?? true,
2050
+ showValueLabel: this.data.showValueLabel ?? true,
2051
+ };
2052
+ onCancel() {
2053
+ this.dialogRef.close();
2054
+ }
2055
+ onSave() {
2056
+ this.dialogRef.close(this.localState);
2057
+ }
2058
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RadialGaugeStateDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2059
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.2.1", type: RadialGaugeStateDialogComponent, isStandalone: true, selector: "lib-radial-gauge-state-dialog", ngImport: i0, template: `
2060
+ <h2 mat-dialog-title>Radial Gauge Settings</h2>
2061
+ <mat-dialog-content>
2062
+ <mat-form-field>
2063
+ <mat-label>Value (0-100)</mat-label>
2064
+ <input
2065
+ matInput
2066
+ type="number"
2067
+ [(ngModel)]="localState.value"
2068
+ min="0"
2069
+ max="100"
2070
+ />
2071
+ </mat-form-field>
2072
+
2073
+ <div class="section">
2074
+ <h4>Color Profile</h4>
2075
+ <mat-radio-group [(ngModel)]="localState.colorProfile">
2076
+ <mat-radio-button value="dynamic">Dynamic (Theme Colors)</mat-radio-button>
2077
+ <mat-radio-button value="static">Static (Performance Colors)</mat-radio-button>
2078
+ </mat-radio-group>
2079
+ </div>
2080
+
2081
+ <div class="toggle-section">
2082
+ <mat-slide-toggle [(ngModel)]="localState.active">
2083
+ Active Display
2084
+ </mat-slide-toggle>
2085
+ <p class="toggle-description">Display live gauge instead of passive icon</p>
2086
+ </div>
2087
+
2088
+ <div class="toggle-section">
2089
+ <mat-slide-toggle [(ngModel)]="localState.hasBackground">
2090
+ Background
2091
+ </mat-slide-toggle>
2092
+ <p class="toggle-description">Add a background color to the widget</p>
2093
+ </div>
2094
+
2095
+ <div class="toggle-section">
2096
+ <mat-slide-toggle [(ngModel)]="localState.showValueLabel">
2097
+ Show Value Label
2098
+ </mat-slide-toggle>
2099
+ <p class="toggle-description">Display numeric value in gauge center</p>
2100
+ </div>
2101
+ </mat-dialog-content>
2102
+
2103
+ <mat-dialog-actions align="end">
2104
+ <button mat-button (click)="onCancel()">Cancel</button>
2105
+ <button mat-flat-button (click)="onSave()">Save</button>
2106
+ </mat-dialog-actions>
2107
+ `, isInline: true, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden}mat-form-field{width:100%;display:block;margin-bottom:1rem}.section{margin-bottom:1.5rem}.section h4{margin:0 0 .5rem;font-size:.875rem;font-weight:500;color:var(--mat-sys-on-surface, #1f1f1f)}mat-radio-group{display:flex;flex-direction:column;gap:.5rem}.toggle-section{display:flex;align-items:center;gap:.75rem;margin-bottom:.5rem}.toggle-description{margin:0;flex:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i4.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i4.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i5$1.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatRadioModule }, { kind: "directive", type: i3$1.MatRadioGroup, selector: "mat-radio-group", inputs: ["color", "name", "labelPosition", "value", "selected", "disabled", "required", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioGroup"] }, { kind: "component", type: i3$1.MatRadioButton, selector: "mat-radio-button", inputs: ["id", "name", "aria-label", "aria-labelledby", "aria-describedby", "disableRipple", "tabIndex", "checked", "value", "labelPosition", "disabled", "required", "color", "disabledInteractive"], outputs: ["change"], exportAs: ["matRadioButton"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.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: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
2108
+ }
2109
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RadialGaugeStateDialogComponent, decorators: [{
2110
+ type: Component,
2111
+ args: [{ selector: 'lib-radial-gauge-state-dialog', standalone: true, imports: [
2112
+ CommonModule,
2113
+ MatDialogModule,
2114
+ MatButtonModule,
2115
+ MatFormFieldModule,
2116
+ MatInputModule,
2117
+ MatSlideToggleModule,
2118
+ MatRadioModule,
2119
+ FormsModule,
2120
+ ], template: `
2121
+ <h2 mat-dialog-title>Radial Gauge Settings</h2>
2122
+ <mat-dialog-content>
2123
+ <mat-form-field>
2124
+ <mat-label>Value (0-100)</mat-label>
2125
+ <input
2126
+ matInput
2127
+ type="number"
2128
+ [(ngModel)]="localState.value"
2129
+ min="0"
2130
+ max="100"
2131
+ />
2132
+ </mat-form-field>
2133
+
2134
+ <div class="section">
2135
+ <h4>Color Profile</h4>
2136
+ <mat-radio-group [(ngModel)]="localState.colorProfile">
2137
+ <mat-radio-button value="dynamic">Dynamic (Theme Colors)</mat-radio-button>
2138
+ <mat-radio-button value="static">Static (Performance Colors)</mat-radio-button>
2139
+ </mat-radio-group>
2140
+ </div>
2141
+
2142
+ <div class="toggle-section">
2143
+ <mat-slide-toggle [(ngModel)]="localState.active">
2144
+ Active Display
2145
+ </mat-slide-toggle>
2146
+ <p class="toggle-description">Display live gauge instead of passive icon</p>
2147
+ </div>
2148
+
2149
+ <div class="toggle-section">
2150
+ <mat-slide-toggle [(ngModel)]="localState.hasBackground">
2151
+ Background
2152
+ </mat-slide-toggle>
2153
+ <p class="toggle-description">Add a background color to the widget</p>
2154
+ </div>
2155
+
2156
+ <div class="toggle-section">
2157
+ <mat-slide-toggle [(ngModel)]="localState.showValueLabel">
2158
+ Show Value Label
2159
+ </mat-slide-toggle>
2160
+ <p class="toggle-description">Display numeric value in gauge center</p>
2161
+ </div>
2162
+ </mat-dialog-content>
2163
+
2164
+ <mat-dialog-actions align="end">
2165
+ <button mat-button (click)="onCancel()">Cancel</button>
2166
+ <button mat-flat-button (click)="onSave()">Save</button>
2167
+ </mat-dialog-actions>
2168
+ `, styles: ["mat-dialog-content{display:block;overflow-y:auto;overflow-x:hidden}mat-form-field{width:100%;display:block;margin-bottom:1rem}.section{margin-bottom:1.5rem}.section h4{margin:0 0 .5rem;font-size:.875rem;font-weight:500;color:var(--mat-sys-on-surface, #1f1f1f)}mat-radio-group{display:flex;flex-direction:column;gap:.5rem}.toggle-section{display:flex;align-items:center;gap:.75rem;margin-bottom:.5rem}.toggle-description{margin:0;flex:1}\n"] }]
2169
+ }] });
2170
+
2171
+ // radial-gauge-widget.component.ts
2172
+ class RadialGaugeWidgetComponent {
2173
+ static metadata = {
2174
+ widgetTypeid: '@ngx-dashboard/radial-gauge-widget',
2175
+ name: 'Radial Gauge',
2176
+ description: 'A semi-circular gauge indicator',
2177
+ svgIcon,
2178
+ };
2179
+ #dialog = inject(MatDialog);
2180
+ #sanitizer = inject(DomSanitizer);
2181
+ safeSvgIcon = this.#sanitizer.bypassSecurityTrustHtml(svgIcon);
2182
+ state = signal({
2183
+ value: 50,
2184
+ colorProfile: 'dynamic',
2185
+ active: false,
2186
+ hasBackground: true,
2187
+ showValueLabel: true,
2188
+ }, ...(ngDevMode ? [{ debugName: "state" }] : []));
2189
+ segments = computed(() => {
2190
+ const profile = this.state().colorProfile || 'dynamic';
2191
+ if (profile === 'static') {
2192
+ // Static performance segments (like CPU usage example)
2193
+ return [
2194
+ { from: 0, to: 25, color: '#dc2626' }, // Poor - red
2195
+ { from: 25, to: 50, color: '#f59e0b' }, // Fair - orange
2196
+ { from: 50, to: 75, color: '#3b82f6' }, // Good - blue
2197
+ { from: 75, to: 100, color: '#10b981' }, // Excellent - green
2198
+ ];
2199
+ }
2200
+ else {
2201
+ // Dynamic theme-aware segments (like demo gauge preview)
2202
+ return [
2203
+ { from: 0, to: 60, color: 'var(--mat-sys-error)' },
2204
+ { from: 60, to: 80, color: 'var(--mat-sys-secondary)' },
2205
+ { from: 80, to: 100, color: 'var(--mat-sys-tertiary)' },
2206
+ ];
2207
+ }
2208
+ }, ...(ngDevMode ? [{ debugName: "segments" }] : []));
2209
+ dashboardSetState(state) {
2210
+ if (state) {
2211
+ this.state.update((current) => ({
2212
+ ...current,
2213
+ ...state,
2214
+ }));
2215
+ }
2216
+ }
2217
+ dashboardGetState() {
2218
+ return this.state();
2219
+ }
2220
+ dashboardEditState() {
2221
+ const dialogRef = this.#dialog.open(RadialGaugeStateDialogComponent, {
2222
+ data: this.state(),
2223
+ width: '400px',
2224
+ maxWidth: '90vw',
2225
+ disableClose: false,
2226
+ autoFocus: false,
2227
+ });
2228
+ dialogRef.afterClosed().subscribe((result) => {
2229
+ if (result) {
2230
+ this.state.set(result);
2231
+ }
2232
+ });
2233
+ }
2234
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RadialGaugeWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2235
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.2.1", type: RadialGaugeWidgetComponent, isStandalone: true, selector: "ngx-dashboard-radial-gauge-widget", ngImport: i0, template: "<!-- radial-gauge-widget.component.html -->\r\n<div class=\"widget-container\" [class.has-background]=\"state().hasBackground\">\r\n @if (state().active) {\r\n <!-- Active mode: Show live gauge -->\r\n <div class=\"gauge-container\">\r\n <ngx-radial-gauge\r\n [value]=\"state().value || 0\"\r\n [min]=\"0\"\r\n [max]=\"100\"\r\n [fitToContainer]=\"true\"\r\n [responsiveMode]=\"true\"\r\n [segments]=\"segments()\"\r\n [outerThickness]=\"24\"\r\n [innerThickness]=\"8\"\r\n [gap]=\"4\"\r\n [segmentGapPx]=\"2\"\r\n [labelReference]=\"'00000'\"\r\n [referenceGlyph]=\"'0'\"\r\n [hasBackground]=\"state().hasBackground || false\"\r\n [showValueLabel]=\"state().showValueLabel ?? true\"\r\n />\r\n </div>\r\n } @else {\r\n <!-- Passive mode: Show static icon -->\r\n <div class=\"icon-container\">\r\n <div class=\"svg-placeholder\" [innerHTML]=\"safeSvgIcon\"></div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:size;width:100%;height:100%;overflow:hidden}.widget-container{height:100%;width:100%;display:flex;align-items:center;justify-content:center;position:relative;box-sizing:border-box;transition:background-color var(--mat-sys-motion-duration-medium2) var(--mat-sys-motion-easing-standard)}.widget-container.has-background{background-color:var(--mat-sys-surface-container-high);border-radius:4px}.gauge-container{height:100%;width:100%;display:flex;align-items:center;justify-content:center;position:relative;box-sizing:border-box}ngx-radial-gauge{width:100%;height:100%;min-height:0;flex:1}.icon-container{height:100%;width:100%;display:flex;align-items:center;justify-content:center;position:relative;box-sizing:border-box}.svg-placeholder{width:80%;height:80%;display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #6c757d);transition:color .2s ease}.svg-placeholder :deep(svg){width:100%;height:100%;max-width:100px;max-height:100px;fill:currentColor}.has-background .svg-placeholder{color:var(--mat-sys-on-surface, #1f1f1f)}.widget-container:hover .svg-placeholder{color:var(--mat-sys-primary, #6750a4)}\n"], dependencies: [{ kind: "component", type: RadialGaugeComponent, selector: "ngx-radial-gauge", inputs: ["value", "min", "max", "segments", "title", "description", "segmentGapPx", "hasBackground", "showValueLabel", "size", "fitToContainer", "containerPadding", "responsiveMode", "sizeToThicknessRatio", "responsiveProportions", "outerThickness", "innerThickness", "gap", "labelReference", "referenceGlyph", "labelPadding", "baselineSafety"] }] });
2236
+ }
2237
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.2.1", ngImport: i0, type: RadialGaugeWidgetComponent, decorators: [{
2238
+ type: Component,
2239
+ args: [{ selector: 'ngx-dashboard-radial-gauge-widget', imports: [RadialGaugeComponent], template: "<!-- radial-gauge-widget.component.html -->\r\n<div class=\"widget-container\" [class.has-background]=\"state().hasBackground\">\r\n @if (state().active) {\r\n <!-- Active mode: Show live gauge -->\r\n <div class=\"gauge-container\">\r\n <ngx-radial-gauge\r\n [value]=\"state().value || 0\"\r\n [min]=\"0\"\r\n [max]=\"100\"\r\n [fitToContainer]=\"true\"\r\n [responsiveMode]=\"true\"\r\n [segments]=\"segments()\"\r\n [outerThickness]=\"24\"\r\n [innerThickness]=\"8\"\r\n [gap]=\"4\"\r\n [segmentGapPx]=\"2\"\r\n [labelReference]=\"'00000'\"\r\n [referenceGlyph]=\"'0'\"\r\n [hasBackground]=\"state().hasBackground || false\"\r\n [showValueLabel]=\"state().showValueLabel ?? true\"\r\n />\r\n </div>\r\n } @else {\r\n <!-- Passive mode: Show static icon -->\r\n <div class=\"icon-container\">\r\n <div class=\"svg-placeholder\" [innerHTML]=\"safeSvgIcon\"></div>\r\n </div>\r\n }\r\n</div>\r\n", styles: [":host{display:block;container-type:size;width:100%;height:100%;overflow:hidden}.widget-container{height:100%;width:100%;display:flex;align-items:center;justify-content:center;position:relative;box-sizing:border-box;transition:background-color var(--mat-sys-motion-duration-medium2) var(--mat-sys-motion-easing-standard)}.widget-container.has-background{background-color:var(--mat-sys-surface-container-high);border-radius:4px}.gauge-container{height:100%;width:100%;display:flex;align-items:center;justify-content:center;position:relative;box-sizing:border-box}ngx-radial-gauge{width:100%;height:100%;min-height:0;flex:1}.icon-container{height:100%;width:100%;display:flex;align-items:center;justify-content:center;position:relative;box-sizing:border-box}.svg-placeholder{width:80%;height:80%;display:flex;align-items:center;justify-content:center;color:var(--mat-sys-on-surface-variant, #6c757d);transition:color .2s ease}.svg-placeholder :deep(svg){width:100%;height:100%;max-width:100px;max-height:100px;fill:currentColor}.has-background .svg-placeholder{color:var(--mat-sys-on-surface, #1f1f1f)}.widget-container:hover .svg-placeholder{color:var(--mat-sys-primary, #6750a4)}\n"] }]
2240
+ }] });
2241
+
2242
+ /*
2243
+ * Public API Surface of ngx-dashboard-widgets
2244
+ */
2245
+
2246
+ /**
2247
+ * Generated bundle index. Do not edit.
2248
+ */
2249
+
2250
+ export { ArrowWidgetComponent, ClockWidgetComponent, LabelWidgetComponent, RadialGaugeComponent, RadialGaugeWidgetComponent, ResponsiveTextDirective };
2251
+ //# sourceMappingURL=dragonworks-ngx-dashboard-widgets.mjs.map