@dragonworks/ngx-dashboard-widgets 20.0.5 → 20.0.6

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/ng-package.json +7 -0
  2. package/package.json +31 -42
  3. package/src/lib/arrow-widget/arrow-state-dialog.component.ts +187 -0
  4. package/src/lib/arrow-widget/arrow-widget.component.html +9 -0
  5. package/src/lib/arrow-widget/arrow-widget.component.scss +52 -0
  6. package/src/lib/arrow-widget/arrow-widget.component.ts +78 -0
  7. package/src/lib/arrow-widget/arrow-widget.metadata.ts +3 -0
  8. package/src/lib/clock-widget/analog-clock/analog-clock.component.html +66 -0
  9. package/src/lib/clock-widget/analog-clock/analog-clock.component.scss +103 -0
  10. package/src/lib/clock-widget/analog-clock/analog-clock.component.ts +120 -0
  11. package/src/lib/clock-widget/clock-state-dialog.component.ts +170 -0
  12. package/src/lib/clock-widget/clock-widget.component.html +16 -0
  13. package/src/lib/clock-widget/clock-widget.component.scss +160 -0
  14. package/src/lib/clock-widget/clock-widget.component.ts +87 -0
  15. package/src/lib/clock-widget/clock-widget.metadata.ts +42 -0
  16. package/src/lib/clock-widget/digital-clock/__tests__/digital-clock.component.spec.ts +276 -0
  17. package/src/lib/clock-widget/digital-clock/digital-clock.component.html +1 -0
  18. package/src/lib/clock-widget/digital-clock/digital-clock.component.scss +43 -0
  19. package/src/lib/clock-widget/digital-clock/digital-clock.component.ts +105 -0
  20. package/src/lib/directives/__tests__/responsive-text.directive.spec.ts +906 -0
  21. package/src/lib/directives/responsive-text.directive.ts +334 -0
  22. package/src/lib/label-widget/__tests__/label-widget.component.spec.ts +539 -0
  23. package/src/lib/label-widget/label-state-dialog.component.ts +385 -0
  24. package/src/lib/label-widget/label-widget.component.html +21 -0
  25. package/src/lib/label-widget/label-widget.component.scss +112 -0
  26. package/src/lib/label-widget/label-widget.component.ts +96 -0
  27. package/src/lib/label-widget/label-widget.metadata.ts +3 -0
  28. package/src/public-api.ts +7 -0
  29. package/tsconfig.lib.json +15 -0
  30. package/tsconfig.lib.prod.json +11 -0
  31. package/tsconfig.spec.json +14 -0
  32. package/fesm2022/dragonworks-ngx-dashboard-widgets.mjs +0 -1428
  33. package/fesm2022/dragonworks-ngx-dashboard-widgets.mjs.map +0 -1
  34. package/index.d.ts +0 -151
@@ -1,1428 +0,0 @@
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 } 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
-
24
- // arrow-widget.metadata.ts
25
- const svgIcon$2 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M320-120v-320H120l360-440 360 440H640v320H320Zm80-80h160v-320h111L480-754 289-520h111v320Zm80-320Z"/></svg>';
26
-
27
- class ArrowStateDialogComponent {
28
- data = inject(MAT_DIALOG_DATA);
29
- dialogRef = inject((MatDialogRef));
30
- // State signals
31
- direction = signal(this.data.direction);
32
- opacity = signal(this.data.opacity ?? 1);
33
- hasBackground = signal(this.data.hasBackground ?? true);
34
- transparentBackground = signal(!(this.data.hasBackground ?? true));
35
- // Store original values for comparison
36
- originalDirection = this.data.direction;
37
- originalOpacity = this.data.opacity ?? 1;
38
- originalHasBackground = this.data.hasBackground ?? true;
39
- // Computed values
40
- rotation = computed(() => {
41
- const rotationMap = {
42
- up: 0,
43
- right: 90,
44
- down: 180,
45
- left: 270,
46
- };
47
- return rotationMap[this.direction()];
48
- });
49
- rotationTransform = computed(() => `rotate(${this.rotation()}deg)`);
50
- directionName = computed(() => {
51
- const nameMap = {
52
- up: 'Up',
53
- right: 'Right',
54
- down: 'Down',
55
- left: 'Left',
56
- };
57
- return nameMap[this.direction()];
58
- });
59
- hasChanged = computed(() => this.direction() !== this.originalDirection ||
60
- this.opacity() !== this.originalOpacity ||
61
- this.hasBackground() !== this.originalHasBackground);
62
- formatOpacity(value) {
63
- return Math.round(value * 100);
64
- }
65
- onBackgroundToggle(hasBackground) {
66
- this.hasBackground.set(hasBackground);
67
- this.transparentBackground.set(!hasBackground);
68
- }
69
- onCancel() {
70
- this.dialogRef.close();
71
- }
72
- save() {
73
- this.dialogRef.close({
74
- direction: this.direction(),
75
- opacity: this.opacity(),
76
- hasBackground: this.hasBackground(),
77
- });
78
- }
79
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ArrowStateDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
80
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.6", type: ArrowStateDialogComponent, isStandalone: true, selector: "lib-arrow-state-dialog", ngImport: i0, template: `
81
- <h2 mat-dialog-title>Arrow Settings</h2>
82
- <mat-dialog-content>
83
- <!-- Direction Selection -->
84
- <mat-form-field appearance="outline" class="direction-field">
85
- <mat-label>Arrow Direction</mat-label>
86
- <mat-select
87
- [value]="direction()"
88
- (selectionChange)="direction.set($any($event.value))"
89
- >
90
- <mat-option value="up">Up</mat-option>
91
- <mat-option value="right">Right</mat-option>
92
- <mat-option value="down">Down</mat-option>
93
- <mat-option value="left">Left</mat-option>
94
- </mat-select>
95
- </mat-form-field>
96
-
97
- <!-- Opacity Slider -->
98
- <div class="slider-field">
99
- <div class="field-label">Opacity: {{ formatOpacity(opacity()) }}%</div>
100
- <mat-slider [min]="0.1" [max]="1" [step]="0.1">
101
- <input matSliderThumb [(ngModel)]="opacity" />
102
- </mat-slider>
103
- </div>
104
-
105
- <!-- Background Toggle -->
106
- <div class="toggle-field">
107
- <mat-slide-toggle
108
- [checked]="hasBackground()"
109
- (change)="onBackgroundToggle($event.checked)">
110
- Background
111
- </mat-slide-toggle>
112
- <span class="toggle-hint">Adds a background behind the arrow</span>
113
- </div>
114
- </mat-dialog-content>
115
-
116
- <mat-dialog-actions align="end">
117
- <button mat-button (click)="onCancel()">Cancel</button>
118
- <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
119
- Save
120
- </button>
121
- </mat-dialog-actions>
122
- `, 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"] }] });
123
- }
124
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ArrowStateDialogComponent, decorators: [{
125
- type: Component,
126
- args: [{ selector: 'lib-arrow-state-dialog', standalone: true, imports: [
127
- CommonModule,
128
- FormsModule,
129
- MatDialogModule,
130
- MatButtonModule,
131
- MatFormFieldModule,
132
- MatSelectModule,
133
- MatSliderModule,
134
- MatSlideToggleModule,
135
- ], template: `
136
- <h2 mat-dialog-title>Arrow Settings</h2>
137
- <mat-dialog-content>
138
- <!-- Direction Selection -->
139
- <mat-form-field appearance="outline" class="direction-field">
140
- <mat-label>Arrow Direction</mat-label>
141
- <mat-select
142
- [value]="direction()"
143
- (selectionChange)="direction.set($any($event.value))"
144
- >
145
- <mat-option value="up">Up</mat-option>
146
- <mat-option value="right">Right</mat-option>
147
- <mat-option value="down">Down</mat-option>
148
- <mat-option value="left">Left</mat-option>
149
- </mat-select>
150
- </mat-form-field>
151
-
152
- <!-- Opacity Slider -->
153
- <div class="slider-field">
154
- <div class="field-label">Opacity: {{ formatOpacity(opacity()) }}%</div>
155
- <mat-slider [min]="0.1" [max]="1" [step]="0.1">
156
- <input matSliderThumb [(ngModel)]="opacity" />
157
- </mat-slider>
158
- </div>
159
-
160
- <!-- Background Toggle -->
161
- <div class="toggle-field">
162
- <mat-slide-toggle
163
- [checked]="hasBackground()"
164
- (change)="onBackgroundToggle($event.checked)">
165
- Background
166
- </mat-slide-toggle>
167
- <span class="toggle-hint">Adds a background behind the arrow</span>
168
- </div>
169
- </mat-dialog-content>
170
-
171
- <mat-dialog-actions align="end">
172
- <button mat-button (click)="onCancel()">Cancel</button>
173
- <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
174
- Save
175
- </button>
176
- </mat-dialog-actions>
177
- `, 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"] }]
178
- }] });
179
-
180
- // arrow-widget.component.ts
181
- class ArrowWidgetComponent {
182
- static metadata = {
183
- widgetTypeid: '@default/arrow-widget',
184
- name: 'Arrow',
185
- description: 'A generic arrow',
186
- svgIcon: svgIcon$2,
187
- };
188
- #sanitizer = inject(DomSanitizer);
189
- #dialog = inject(MatDialog);
190
- safeSvgIcon = this.#sanitizer.bypassSecurityTrustHtml(svgIcon$2);
191
- state = signal({
192
- direction: 'up',
193
- opacity: 0.3,
194
- hasBackground: true,
195
- });
196
- // Computed rotation
197
- rotationAngle = computed(() => {
198
- const rotationMap = {
199
- up: 0,
200
- right: 90,
201
- down: 180,
202
- left: 270,
203
- };
204
- return rotationMap[this.state().direction];
205
- });
206
- dashboardSetState(state) {
207
- if (state) {
208
- this.state.update((current) => ({
209
- ...current,
210
- ...state,
211
- }));
212
- }
213
- }
214
- dashboardGetState() {
215
- return this.state();
216
- }
217
- dashboardEditState() {
218
- const dialogRef = this.#dialog.open(ArrowStateDialogComponent, {
219
- data: this.state(),
220
- width: '400px',
221
- maxWidth: '90vw',
222
- disableClose: false,
223
- autoFocus: false,
224
- });
225
- dialogRef.afterClosed().subscribe((result) => {
226
- if (result) {
227
- this.state.set(result);
228
- }
229
- });
230
- }
231
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ArrowWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
232
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.6", 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;transform-origin:center center}.svg-placeholder ::ng-deep svg{width:100%;height:100%;display:block;fill:var(--mat-sys-on-surface-variant, #6c757d);transition:fill .2s ease}.has-background .svg-placeholder ::ng-deep svg{fill:var(--mat-sys-on-surface, #1f1f1f)}.svg-wrapper:hover .svg-placeholder ::ng-deep svg{fill:var(--mat-sys-primary, #6750a4)}\n"] });
233
- }
234
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ArrowWidgetComponent, decorators: [{
235
- type: Component,
236
- 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;transform-origin:center center}.svg-placeholder ::ng-deep svg{width:100%;height:100%;display:block;fill:var(--mat-sys-on-surface-variant, #6c757d);transition:fill .2s ease}.has-background .svg-placeholder ::ng-deep svg{fill:var(--mat-sys-on-surface, #1f1f1f)}.svg-wrapper:hover .svg-placeholder ::ng-deep svg{fill:var(--mat-sys-primary, #6750a4)}\n"] }]
237
- }] });
238
-
239
- // label-widget.metadata.ts
240
- const svgIcon$1 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path 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>';
241
-
242
- class LabelStateDialogComponent {
243
- data = inject(MAT_DIALOG_DATA);
244
- dialogRef = inject((MatDialogRef));
245
- // State signals
246
- label = signal(this.data.label ?? '');
247
- fontSize = signal(this.data.fontSize ?? 16);
248
- alignment = signal(this.data.alignment ?? 'center');
249
- fontWeight = signal(this.data.fontWeight ?? 'normal');
250
- opacity = signal(this.data.opacity ?? 1);
251
- hasBackground = signal(this.data.hasBackground ?? true);
252
- transparentBackground = signal(!(this.data.hasBackground ?? true));
253
- responsive = signal(this.data.responsive ?? false);
254
- // Responsive font size constraints
255
- minFontSize = signal(this.data.minFontSize ?? 8);
256
- maxFontSize = signal(this.data.maxFontSize ?? 64);
257
- // Store original values for comparison
258
- originalLabel = this.data.label ?? '';
259
- originalFontSize = this.data.fontSize ?? 16;
260
- originalAlignment = this.data.alignment ?? 'center';
261
- originalFontWeight = this.data.fontWeight ?? 'normal';
262
- originalOpacity = this.data.opacity ?? 1;
263
- originalHasBackground = this.data.hasBackground ?? true;
264
- originalResponsive = this.data.responsive ?? false;
265
- originalMinFontSize = this.data.minFontSize ?? 8;
266
- originalMaxFontSize = this.data.maxFontSize ?? 64;
267
- // Validation computed properties
268
- isMinFontSizeValid = computed(() => {
269
- const min = this.minFontSize();
270
- return min >= 8 && min <= 24;
271
- });
272
- isMaxFontSizeValid = computed(() => {
273
- const max = this.maxFontSize();
274
- return max >= 16 && max <= 128;
275
- });
276
- isFontSizeRangeValid = computed(() => this.minFontSize() < this.maxFontSize());
277
- isFormValid = computed(() => this.isMinFontSizeValid() &&
278
- this.isMaxFontSizeValid() &&
279
- this.isFontSizeRangeValid());
280
- // Computed values
281
- hasChanged = computed(() => this.label() !== this.originalLabel ||
282
- this.fontSize() !== this.originalFontSize ||
283
- this.alignment() !== this.originalAlignment ||
284
- this.fontWeight() !== this.originalFontWeight ||
285
- this.opacity() !== this.originalOpacity ||
286
- this.hasBackground() !== this.originalHasBackground ||
287
- this.responsive() !== this.originalResponsive ||
288
- this.minFontSize() !== this.originalMinFontSize ||
289
- this.maxFontSize() !== this.originalMaxFontSize);
290
- formatOpacity(value) {
291
- return Math.round(value * 100);
292
- }
293
- formatOpacitySlider = (value) => {
294
- return `${Math.round(value * 100)}%`;
295
- };
296
- // Validation methods with robust min < max enforcement
297
- validateAndCorrectMinFontSize(value) {
298
- // Clamp to valid range
299
- const corrected = Math.max(8, Math.min(24, value));
300
- this.minFontSize.set(corrected);
301
- // Ensure min < max with adequate gap
302
- if (corrected >= this.maxFontSize()) {
303
- const newMax = Math.min(128, corrected + 8); // Ensure at least 8px gap
304
- this.maxFontSize.set(newMax);
305
- }
306
- }
307
- validateAndCorrectMaxFontSize(value) {
308
- // Clamp to valid range
309
- const corrected = Math.max(16, Math.min(128, value));
310
- this.maxFontSize.set(corrected);
311
- // Ensure min < max with adequate gap
312
- if (corrected <= this.minFontSize()) {
313
- const newMin = Math.max(8, corrected - 8); // Ensure at least 8px gap
314
- this.minFontSize.set(newMin);
315
- }
316
- }
317
- onBackgroundToggle(hasWhiteBackground) {
318
- this.hasBackground.set(hasWhiteBackground);
319
- this.transparentBackground.set(!hasWhiteBackground);
320
- }
321
- onCancel() {
322
- this.dialogRef.close();
323
- }
324
- save() {
325
- this.dialogRef.close({
326
- label: this.label(),
327
- fontSize: this.fontSize(),
328
- alignment: this.alignment(),
329
- fontWeight: this.fontWeight(),
330
- opacity: this.opacity(),
331
- hasBackground: this.hasBackground(),
332
- responsive: this.responsive(),
333
- minFontSize: this.minFontSize(),
334
- maxFontSize: this.maxFontSize(),
335
- });
336
- }
337
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: LabelStateDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
338
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: LabelStateDialogComponent, isStandalone: true, selector: "lib-label-state-dialog", ngImport: i0, template: `
339
- <h2 mat-dialog-title>Label Settings</h2>
340
- <mat-dialog-content>
341
- <mat-form-field appearance="outline" class="label-text-field">
342
- <mat-label>Label Text</mat-label>
343
- <input
344
- matInput
345
- type="text"
346
- [value]="label()"
347
- (input)="label.set($any($event.target).value)"
348
- placeholder="Enter your label text..."
349
- />
350
- </mat-form-field>
351
-
352
- <!-- Responsive Text Toggle -->
353
- <div class="toggle-section">
354
- <mat-slide-toggle
355
- [checked]="responsive()"
356
- (change)="responsive.set($event.checked)">
357
- Responsive Text
358
- </mat-slide-toggle>
359
- <span class="toggle-description"
360
- >Automatically adjust text size to fit the widget</span
361
- >
362
- </div>
363
-
364
- <!-- Responsive Font Size Constraints (only shown when responsive is enabled) -->
365
- @if (responsive()) {
366
- <div class="responsive-section">
367
- <div class="section-label">Font Size Limits</div>
368
- <div class="row-layout">
369
- <mat-form-field appearance="outline">
370
- <mat-label>Min Size (px)</mat-label>
371
- <input
372
- matInput
373
- type="number"
374
- [value]="minFontSize()"
375
- (input)="validateAndCorrectMinFontSize(+$any($event.target).value)"
376
- (blur)="validateAndCorrectMinFontSize(minFontSize())"
377
- min="8"
378
- max="24"
379
- placeholder="8"
380
- />
381
- @if (!isMinFontSizeValid() || !isFontSizeRangeValid()) {
382
- <mat-error>
383
- @if (!isMinFontSizeValid()) {
384
- Must be between 8-24px
385
- } @else {
386
- Must be less than max size
387
- }
388
- </mat-error>
389
- } @else {
390
- <mat-hint>8-24px range</mat-hint>
391
- }
392
- </mat-form-field>
393
-
394
- <mat-form-field appearance="outline">
395
- <mat-label>Max Size (px)</mat-label>
396
- <input
397
- matInput
398
- type="number"
399
- [value]="maxFontSize()"
400
- (input)="validateAndCorrectMaxFontSize(+$any($event.target).value)"
401
- (blur)="validateAndCorrectMaxFontSize(maxFontSize())"
402
- min="16"
403
- max="128"
404
- placeholder="64"
405
- />
406
- @if (!isMaxFontSizeValid() || !isFontSizeRangeValid()) {
407
- <mat-error>
408
- @if (!isMaxFontSizeValid()) {
409
- Must be between 16-128px
410
- } @else {
411
- Must be greater than min size
412
- }
413
- </mat-error>
414
- } @else {
415
- <mat-hint>16-128px range</mat-hint>
416
- }
417
- </mat-form-field>
418
- </div>
419
- </div>
420
- }
421
-
422
- <div class="row-layout">
423
- <mat-form-field appearance="outline">
424
- <mat-label>Font Size (px)</mat-label>
425
- <input
426
- matInput
427
- type="number"
428
- [value]="fontSize()"
429
- (input)="fontSize.set(+$any($event.target).value)"
430
- [disabled]="responsive()"
431
- min="8"
432
- max="48"
433
- placeholder="16"
434
- />
435
- </mat-form-field>
436
-
437
- <mat-form-field appearance="outline">
438
- <mat-label>Alignment</mat-label>
439
- <mat-select
440
- [value]="alignment()"
441
- (selectionChange)="alignment.set($any($event.value))"
442
- >
443
- <mat-option value="left">Left</mat-option>
444
- <mat-option value="center">Center</mat-option>
445
- <mat-option value="right">Right</mat-option>
446
- </mat-select>
447
- </mat-form-field>
448
- </div>
449
-
450
- <mat-form-field appearance="outline">
451
- <mat-label>Font Weight</mat-label>
452
- <mat-select
453
- [value]="fontWeight()"
454
- (selectionChange)="fontWeight.set($any($event.value))"
455
- >
456
- <mat-option value="normal">Normal</mat-option>
457
- <mat-option value="bold">Bold</mat-option>
458
- </mat-select>
459
- </mat-form-field>
460
-
461
- <!-- Opacity Slider -->
462
- <div class="slider-section">
463
- <div class="slider-label">Opacity: {{ formatOpacity(opacity()) }}%</div>
464
- <mat-slider [min]="0.1" [max]="1" [step]="0.1">
465
- <input matSliderThumb [(ngModel)]="opacity" />
466
- </mat-slider>
467
- </div>
468
-
469
- <!-- Background Toggle -->
470
- <div class="toggle-section">
471
- <mat-slide-toggle
472
- [checked]="!transparentBackground()"
473
- (change)="onBackgroundToggle($event.checked)">
474
- Background
475
- </mat-slide-toggle>
476
- <span class="toggle-description"
477
- >Adds a background behind the text</span
478
- >
479
- </div>
480
- </mat-dialog-content>
481
-
482
- <mat-dialog-actions align="end">
483
- <button mat-button (click)="onCancel()">Cancel</button>
484
- <button mat-flat-button (click)="save()" [disabled]="!hasChanged() || !isFormValid()">
485
- Save
486
- </button>
487
- </mat-dialog-actions>
488
- `, 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"] }] });
489
- }
490
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: LabelStateDialogComponent, decorators: [{
491
- type: Component,
492
- args: [{ selector: 'lib-label-state-dialog', standalone: true, imports: [
493
- CommonModule,
494
- FormsModule,
495
- MatDialogModule,
496
- MatButtonModule,
497
- MatFormFieldModule,
498
- MatInputModule,
499
- MatSelectModule,
500
- MatSliderModule,
501
- MatSlideToggleModule, // Add this import
502
- ], template: `
503
- <h2 mat-dialog-title>Label Settings</h2>
504
- <mat-dialog-content>
505
- <mat-form-field appearance="outline" class="label-text-field">
506
- <mat-label>Label Text</mat-label>
507
- <input
508
- matInput
509
- type="text"
510
- [value]="label()"
511
- (input)="label.set($any($event.target).value)"
512
- placeholder="Enter your label text..."
513
- />
514
- </mat-form-field>
515
-
516
- <!-- Responsive Text Toggle -->
517
- <div class="toggle-section">
518
- <mat-slide-toggle
519
- [checked]="responsive()"
520
- (change)="responsive.set($event.checked)">
521
- Responsive Text
522
- </mat-slide-toggle>
523
- <span class="toggle-description"
524
- >Automatically adjust text size to fit the widget</span
525
- >
526
- </div>
527
-
528
- <!-- Responsive Font Size Constraints (only shown when responsive is enabled) -->
529
- @if (responsive()) {
530
- <div class="responsive-section">
531
- <div class="section-label">Font Size Limits</div>
532
- <div class="row-layout">
533
- <mat-form-field appearance="outline">
534
- <mat-label>Min Size (px)</mat-label>
535
- <input
536
- matInput
537
- type="number"
538
- [value]="minFontSize()"
539
- (input)="validateAndCorrectMinFontSize(+$any($event.target).value)"
540
- (blur)="validateAndCorrectMinFontSize(minFontSize())"
541
- min="8"
542
- max="24"
543
- placeholder="8"
544
- />
545
- @if (!isMinFontSizeValid() || !isFontSizeRangeValid()) {
546
- <mat-error>
547
- @if (!isMinFontSizeValid()) {
548
- Must be between 8-24px
549
- } @else {
550
- Must be less than max size
551
- }
552
- </mat-error>
553
- } @else {
554
- <mat-hint>8-24px range</mat-hint>
555
- }
556
- </mat-form-field>
557
-
558
- <mat-form-field appearance="outline">
559
- <mat-label>Max Size (px)</mat-label>
560
- <input
561
- matInput
562
- type="number"
563
- [value]="maxFontSize()"
564
- (input)="validateAndCorrectMaxFontSize(+$any($event.target).value)"
565
- (blur)="validateAndCorrectMaxFontSize(maxFontSize())"
566
- min="16"
567
- max="128"
568
- placeholder="64"
569
- />
570
- @if (!isMaxFontSizeValid() || !isFontSizeRangeValid()) {
571
- <mat-error>
572
- @if (!isMaxFontSizeValid()) {
573
- Must be between 16-128px
574
- } @else {
575
- Must be greater than min size
576
- }
577
- </mat-error>
578
- } @else {
579
- <mat-hint>16-128px range</mat-hint>
580
- }
581
- </mat-form-field>
582
- </div>
583
- </div>
584
- }
585
-
586
- <div class="row-layout">
587
- <mat-form-field appearance="outline">
588
- <mat-label>Font Size (px)</mat-label>
589
- <input
590
- matInput
591
- type="number"
592
- [value]="fontSize()"
593
- (input)="fontSize.set(+$any($event.target).value)"
594
- [disabled]="responsive()"
595
- min="8"
596
- max="48"
597
- placeholder="16"
598
- />
599
- </mat-form-field>
600
-
601
- <mat-form-field appearance="outline">
602
- <mat-label>Alignment</mat-label>
603
- <mat-select
604
- [value]="alignment()"
605
- (selectionChange)="alignment.set($any($event.value))"
606
- >
607
- <mat-option value="left">Left</mat-option>
608
- <mat-option value="center">Center</mat-option>
609
- <mat-option value="right">Right</mat-option>
610
- </mat-select>
611
- </mat-form-field>
612
- </div>
613
-
614
- <mat-form-field appearance="outline">
615
- <mat-label>Font Weight</mat-label>
616
- <mat-select
617
- [value]="fontWeight()"
618
- (selectionChange)="fontWeight.set($any($event.value))"
619
- >
620
- <mat-option value="normal">Normal</mat-option>
621
- <mat-option value="bold">Bold</mat-option>
622
- </mat-select>
623
- </mat-form-field>
624
-
625
- <!-- Opacity Slider -->
626
- <div class="slider-section">
627
- <div class="slider-label">Opacity: {{ formatOpacity(opacity()) }}%</div>
628
- <mat-slider [min]="0.1" [max]="1" [step]="0.1">
629
- <input matSliderThumb [(ngModel)]="opacity" />
630
- </mat-slider>
631
- </div>
632
-
633
- <!-- Background Toggle -->
634
- <div class="toggle-section">
635
- <mat-slide-toggle
636
- [checked]="!transparentBackground()"
637
- (change)="onBackgroundToggle($event.checked)">
638
- Background
639
- </mat-slide-toggle>
640
- <span class="toggle-description"
641
- >Adds a background behind the text</span
642
- >
643
- </div>
644
- </mat-dialog-content>
645
-
646
- <mat-dialog-actions align="end">
647
- <button mat-button (click)="onCancel()">Cancel</button>
648
- <button mat-flat-button (click)="save()" [disabled]="!hasChanged() || !isFormValid()">
649
- Save
650
- </button>
651
- </mat-dialog-actions>
652
- `, 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"] }]
653
- }] });
654
-
655
- /**
656
- * Directive that automatically adjusts font size to fit text within its parent container.
657
- * Uses canvas-based measurement for performance and DOM verification for accuracy.
658
- *
659
- * @example
660
- * <div class="container">
661
- * <span responsiveText [minFontSize]="12" [maxFontSize]="72">Dynamic text here</span>
662
- * </div>
663
- */
664
- class ResponsiveTextDirective {
665
- /* ───────────────────────── Inputs with transforms ─────────────── */
666
- /** Minimum font-size in pixels (accessibility floor) */
667
- minFontSize = input(8, { transform: numberAttribute });
668
- /** Maximum font-size in pixels (layout ceiling) */
669
- maxFontSize = input(512, { transform: numberAttribute });
670
- /**
671
- * Line-height: pass a multiplier (e.g. 1.1) or absolute px value.
672
- * For single-line text a multiplier < 10 is treated as unitless.
673
- */
674
- lineHeight = input(1.1, { transform: numberAttribute });
675
- /** Whether to observe text mutations after first render */
676
- observeMutations = input(true, { transform: booleanAttribute });
677
- /** Debounce delay in ms for resize/mutation callbacks */
678
- debounceMs = input(16, { transform: numberAttribute });
679
- /* ───────────────────────── Private state ───────────────────────── */
680
- el = inject(ElementRef);
681
- zone = inject(NgZone);
682
- platformId = inject(PLATFORM_ID);
683
- destroyRef = inject(DestroyRef);
684
- // Canvas context - lazy initialization
685
- _ctx;
686
- get ctx() {
687
- if (!this._ctx) {
688
- const canvas = document.createElement('canvas');
689
- this._ctx = canvas.getContext('2d', {
690
- willReadFrequently: true,
691
- alpha: false,
692
- });
693
- }
694
- return this._ctx;
695
- }
696
- ro;
697
- mo;
698
- fitTimeout;
699
- // Cache for performance
700
- lastText = '';
701
- lastMaxW = 0;
702
- lastMaxH = 0;
703
- lastFontSize = 0;
704
- /* ───────────────────────── Lifecycle ──────────────────────────── */
705
- ngAfterViewInit() {
706
- if (!isPlatformBrowser(this.platformId))
707
- return;
708
- // Set initial styles
709
- const span = this.el.nativeElement;
710
- span.style.transition = 'font-size 0.1s ease-out';
711
- // All observer callbacks run outside Angular's zone
712
- this.zone.runOutsideAngular(() => {
713
- this.fit();
714
- this.observeResize();
715
- if (this.observeMutations()) {
716
- this.observeText();
717
- }
718
- });
719
- }
720
- ngOnDestroy() {
721
- this.cleanup();
722
- }
723
- /* ───────────────────── Core fitting logic ───────────────────── */
724
- /**
725
- * Debounced fit handler to prevent excessive recalculations
726
- */
727
- requestFit = () => {
728
- if (this.fitTimeout) {
729
- cancelAnimationFrame(this.fitTimeout);
730
- }
731
- this.fitTimeout = requestAnimationFrame(() => {
732
- this.fit();
733
- });
734
- };
735
- /**
736
- * Recalculate & apply the ideal font-size
737
- */
738
- fit = () => {
739
- const span = this.el.nativeElement;
740
- const parent = span.parentElement;
741
- if (!parent)
742
- return;
743
- const text = span.textContent?.trim() || '';
744
- if (!text) {
745
- span.style.fontSize = `${this.minFontSize()}px`;
746
- return;
747
- }
748
- const { maxW, maxH } = this.getAvailableSpace(parent);
749
- // Check cache to avoid redundant calculations
750
- if (text === this.lastText &&
751
- maxW === this.lastMaxW &&
752
- maxH === this.lastMaxH &&
753
- this.lastFontSize > 0) {
754
- return;
755
- }
756
- // Calculate with conservative buffer for sub-pixel accuracy
757
- const ideal = this.calcFit(text, maxW * 0.98, maxH * 0.98);
758
- span.style.fontSize = `${ideal}px`;
759
- // DOM verification pass
760
- this.verifyFit(span, maxW, maxH, ideal);
761
- // Update cache
762
- this.lastText = text;
763
- this.lastMaxW = maxW;
764
- this.lastMaxH = maxH;
765
- this.lastFontSize = parseFloat(span.style.fontSize);
766
- };
767
- /**
768
- * Calculate available space accounting for padding and borders
769
- */
770
- getAvailableSpace(parent) {
771
- const cs = getComputedStyle(parent);
772
- const maxW = parent.clientWidth -
773
- parseFloat(cs.paddingLeft) -
774
- parseFloat(cs.paddingRight);
775
- const maxH = parent.clientHeight -
776
- parseFloat(cs.paddingTop) -
777
- parseFloat(cs.paddingBottom);
778
- return { maxW: Math.max(0, maxW), maxH: Math.max(0, maxH) };
779
- }
780
- /**
781
- * DOM-based verification to handle sub-pixel discrepancies
782
- */
783
- verifyFit(span, maxW, maxH, ideal) {
784
- // Simple synchronous verification
785
- if (span.scrollWidth > maxW || span.scrollHeight > maxH) {
786
- let safe = ideal;
787
- let iterations = 0;
788
- const maxIterations = 10;
789
- while (iterations < maxIterations &&
790
- safe > this.minFontSize() &&
791
- (span.scrollWidth > maxW || span.scrollHeight > maxH)) {
792
- safe -= 0.25;
793
- span.style.fontSize = `${safe}px`;
794
- iterations++;
795
- }
796
- // Update cache with verified size
797
- this.lastFontSize = safe;
798
- }
799
- }
800
- /* ───────────────────── Binary search algorithm ────────────────── */
801
- /**
802
- * Binary search for optimal font size using canvas measurements
803
- */
804
- calcFit(text, maxW, maxH, precision = 0.1) {
805
- if (maxW <= 0 || maxH <= 0)
806
- return this.minFontSize();
807
- const computedStyle = getComputedStyle(this.el.nativeElement);
808
- const fontFamily = computedStyle.fontFamily || 'sans-serif';
809
- const fontWeight = computedStyle.fontWeight || '400';
810
- let lo = this.minFontSize();
811
- let hi = this.maxFontSize();
812
- let bestFit = this.minFontSize();
813
- while (hi - lo > precision) {
814
- const mid = (hi + lo) / 2;
815
- this.ctx.font = `${fontWeight} ${mid}px ${fontFamily}`;
816
- const metrics = this.ctx.measureText(text);
817
- const width = metrics.width;
818
- // Calculate height based on available metrics
819
- const height = this.calculateTextHeight(metrics, mid);
820
- if (width <= maxW && height <= maxH) {
821
- bestFit = mid;
822
- lo = mid;
823
- }
824
- else {
825
- hi = mid;
826
- }
827
- }
828
- return Math.floor(bestFit * 100) / 100;
829
- }
830
- /**
831
- * Calculate text height from metrics
832
- */
833
- calculateTextHeight(metrics, fontSize) {
834
- // Use font bounding box metrics if available
835
- if (metrics.fontBoundingBoxAscent && metrics.fontBoundingBoxDescent) {
836
- return metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
837
- }
838
- // Fallback to actual bounding box
839
- if (metrics.actualBoundingBoxAscent && metrics.actualBoundingBoxDescent) {
840
- return metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
841
- }
842
- // Final fallback using line height
843
- return this.lineHeight() < 10
844
- ? fontSize * this.lineHeight()
845
- : this.lineHeight();
846
- }
847
- /* ───────────────────────── Observers ─────────────────────────── */
848
- /**
849
- * Observe parent container resizes
850
- */
851
- observeResize() {
852
- if (!('ResizeObserver' in window))
853
- return;
854
- this.ro = new ResizeObserver((entries) => {
855
- // Only trigger if size actually changed
856
- const entry = entries[0];
857
- if (entry?.contentRect) {
858
- this.requestFit();
859
- }
860
- });
861
- const parent = this.el.nativeElement.parentElement;
862
- if (parent) {
863
- this.ro.observe(parent);
864
- }
865
- }
866
- /**
867
- * Observe text content changes
868
- */
869
- observeText() {
870
- if (!('MutationObserver' in window))
871
- return;
872
- this.mo = new MutationObserver((mutations) => {
873
- // Check if text actually changed
874
- const hasTextChange = mutations.some((m) => m.type === 'characterData' ||
875
- (m.type === 'childList' &&
876
- (m.addedNodes.length > 0 || m.removedNodes.length > 0)));
877
- if (hasTextChange) {
878
- this.requestFit();
879
- }
880
- });
881
- this.mo.observe(this.el.nativeElement, {
882
- characterData: true,
883
- childList: true,
884
- subtree: true,
885
- });
886
- }
887
- /**
888
- * Cleanup resources
889
- */
890
- cleanup() {
891
- this.ro?.disconnect();
892
- this.mo?.disconnect();
893
- if (this.fitTimeout) {
894
- cancelAnimationFrame(this.fitTimeout);
895
- }
896
- // Clear canvas context
897
- this._ctx = undefined;
898
- }
899
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ResponsiveTextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
900
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.6", type: ResponsiveTextDirective, isStandalone: true, selector: "[responsiveText]", 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 });
901
- }
902
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ResponsiveTextDirective, decorators: [{
903
- type: Directive,
904
- args: [{
905
- selector: '[responsiveText]',
906
- standalone: true,
907
- host: {
908
- '[style.display]': '"block"',
909
- '[style.width]': '"100%"',
910
- '[style.white-space]': '"nowrap"',
911
- '[style.overflow]': '"visible"',
912
- },
913
- }]
914
- }] });
915
-
916
- // label-widget.component.ts
917
- class LabelWidgetComponent {
918
- static metadata = {
919
- widgetTypeid: '@default/label-widget',
920
- name: 'Label',
921
- description: 'A generic text label',
922
- svgIcon: svgIcon$1,
923
- };
924
- #sanitizer = inject(DomSanitizer);
925
- #dialog = inject(MatDialog);
926
- safeSvgIcon = this.#sanitizer.bypassSecurityTrustHtml(svgIcon$1);
927
- state = signal({
928
- label: '',
929
- fontSize: 16,
930
- alignment: 'center',
931
- fontWeight: 'normal',
932
- opacity: 1,
933
- hasBackground: true,
934
- responsive: false,
935
- minFontSize: 8, // Accessible minimum for responsive text
936
- maxFontSize: 64, // Practical maximum for widget display
937
- });
938
- dashboardSetState(state) {
939
- if (state) {
940
- this.state.update((current) => ({
941
- ...current,
942
- ...state,
943
- }));
944
- }
945
- }
946
- dashboardGetState() {
947
- return { ...this.state() };
948
- }
949
- dashboardEditState() {
950
- const dialogRef = this.#dialog.open(LabelStateDialogComponent, {
951
- data: this.dashboardGetState(),
952
- width: '400px',
953
- maxWidth: '90vw',
954
- disableClose: false,
955
- autoFocus: false,
956
- });
957
- dialogRef
958
- .afterClosed()
959
- .subscribe((result) => {
960
- if (result) {
961
- this.state.set(result);
962
- }
963
- });
964
- }
965
- get hasContent() {
966
- return !!this.state().label?.trim();
967
- }
968
- get label() {
969
- return this.state().label?.trim();
970
- }
971
- // Computed properties for responsive font size limits with fallbacks
972
- minFontSize = computed(() => this.state().minFontSize ?? 8);
973
- maxFontSize = computed(() => this.state().maxFontSize ?? 64);
974
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: LabelWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
975
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", 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\" responsiveText [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;transform-origin:center center}.svg-placeholder ::ng-deep svg{width:100%;height:100%;display:block;fill:var(--mat-sys-on-surface-variant, #6c757d);transition:fill .2s ease}.has-background .svg-placeholder ::ng-deep svg{fill:var(--mat-sys-on-surface, #1f1f1f)}.svg-wrapper:hover .svg-placeholder ::ng-deep svg{fill:var(--mat-sys-primary, #6750a4)}\n"], dependencies: [{ kind: "directive", type: ResponsiveTextDirective, selector: "[responsiveText]", inputs: ["minFontSize", "maxFontSize", "lineHeight", "observeMutations", "debounceMs"] }] });
976
- }
977
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: LabelWidgetComponent, decorators: [{
978
- type: Component,
979
- 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\" responsiveText [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;transform-origin:center center}.svg-placeholder ::ng-deep svg{width:100%;height:100%;display:block;fill:var(--mat-sys-on-surface-variant, #6c757d);transition:fill .2s ease}.has-background .svg-placeholder ::ng-deep svg{fill:var(--mat-sys-on-surface, #1f1f1f)}.svg-wrapper:hover .svg-placeholder ::ng-deep svg{fill:var(--mat-sys-primary, #6750a4)}\n"] }]
980
- }] });
981
-
982
- const svgIcon = `
983
- <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" preserveAspectRatio="xMidYMid meet">
984
- <use transform="matrix(-1,0,0,1,800,0)" href="#one-half" />
985
- <g id="one-half">
986
- <g id="one-fourth">
987
- <path d="m400 40v107" stroke-width="26.7" stroke="currentColor" />
988
- <g id="one-twelfth">
989
- <path
990
- d="m580 88.233-42.5 73.612"
991
- stroke-width="26.7"
992
- stroke="currentColor"
993
- />
994
- <g id="one-thirtieth">
995
- <path
996
- id="one-sixtieth"
997
- d="m437.63 41.974-3.6585 34.808"
998
- stroke-width="13.6"
999
- stroke="currentColor"
1000
- />
1001
- <use transform="rotate(6 400 400)" href="#one-sixtieth" />
1002
- </g>
1003
- <use transform="rotate(12 400 400)" href="#one-thirtieth" />
1004
- </g>
1005
- <use transform="rotate(30 400 400)" href="#one-twelfth" />
1006
- <use transform="rotate(60 400 400)" href="#one-twelfth" />
1007
- </g>
1008
- <use transform="rotate(90 400 400)" href="#one-fourth" />
1009
- </g>
1010
- <path
1011
- class="clock-hour-hand"
1012
- id="anim-clock-hour-hand"
1013
- d="m 381.925,476 h 36.15 l 5e-4,-300.03008 L 400,156.25 381.9245,175.96992 Z"
1014
- transform="rotate(110.2650694444, 400, 400)"
1015
- />
1016
- <path
1017
- class="clock-minute-hand"
1018
- id="anim-clock-minute-hand"
1019
- d="M 412.063,496.87456 H 387.937 L 385.249,65.68306 400,52.75 414.751,65.68306 Z"
1020
- transform="rotate(243.1808333333, 400, 400)"
1021
- />
1022
- </svg>
1023
- `;
1024
-
1025
- class ClockStateDialogComponent {
1026
- data = inject(MAT_DIALOG_DATA);
1027
- dialogRef = inject((MatDialogRef));
1028
- // State signals
1029
- mode = signal(this.data.mode ?? 'digital');
1030
- hasBackground = signal(this.data.hasBackground ?? true);
1031
- timeFormat = signal(this.data.timeFormat ?? '24h');
1032
- showSeconds = signal(this.data.showSeconds ?? true);
1033
- // Store original values for comparison
1034
- originalMode = this.data.mode ?? 'digital';
1035
- originalHasBackground = this.data.hasBackground ?? true;
1036
- originalTimeFormat = this.data.timeFormat ?? '24h';
1037
- originalShowSeconds = this.data.showSeconds ?? true;
1038
- // Computed values
1039
- hasChanged = computed(() => this.mode() !== this.originalMode ||
1040
- this.hasBackground() !== this.originalHasBackground ||
1041
- this.timeFormat() !== this.originalTimeFormat ||
1042
- this.showSeconds() !== this.originalShowSeconds);
1043
- onCancel() {
1044
- this.dialogRef.close();
1045
- }
1046
- save() {
1047
- this.dialogRef.close({
1048
- mode: this.mode(),
1049
- hasBackground: this.hasBackground(),
1050
- timeFormat: this.timeFormat(),
1051
- showSeconds: this.showSeconds(),
1052
- });
1053
- }
1054
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ClockStateDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1055
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: ClockStateDialogComponent, isStandalone: true, selector: "demo-clock-state-dialog", ngImport: i0, template: `
1056
- <h2 mat-dialog-title>Clock Settings</h2>
1057
- <mat-dialog-content>
1058
- <div class="mode-selection">
1059
- <label class="section-label">Display Mode</label>
1060
- <mat-radio-group
1061
- [value]="mode()"
1062
- (change)="mode.set($any($event.value))"
1063
- >
1064
- <mat-radio-button value="digital">Digital</mat-radio-button>
1065
- <mat-radio-button value="analog">Analog</mat-radio-button>
1066
- </mat-radio-group>
1067
- </div>
1068
-
1069
- <!-- Time Format (only for digital mode) -->
1070
- @if (mode() === 'digital') {
1071
- <div class="format-selection">
1072
- <label class="section-label">Time Format</label>
1073
- <mat-radio-group
1074
- [value]="timeFormat()"
1075
- (change)="timeFormat.set($any($event.value))"
1076
- >
1077
- <mat-radio-button value="24h">24 Hour (14:30:45)</mat-radio-button>
1078
- <mat-radio-button value="12h">12 Hour (2:30:45 PM)</mat-radio-button>
1079
- </mat-radio-group>
1080
- </div>
1081
- }
1082
-
1083
- <!-- Show Seconds Toggle (for both digital and analog modes) -->
1084
- <div class="toggle-section">
1085
- <mat-slide-toggle
1086
- [checked]="showSeconds()"
1087
- (change)="showSeconds.set($event.checked)">
1088
- Show Seconds
1089
- </mat-slide-toggle>
1090
- <span class="toggle-description">
1091
- @if (mode() === 'digital') {
1092
- Display seconds in the time
1093
- } @else {
1094
- Show the second hand on the clock
1095
- }
1096
- </span>
1097
- </div>
1098
-
1099
- <!-- Background Toggle -->
1100
- <div class="toggle-section">
1101
- <mat-slide-toggle
1102
- [checked]="hasBackground()"
1103
- (change)="hasBackground.set($event.checked)">
1104
- Background
1105
- </mat-slide-toggle>
1106
- <span class="toggle-description"
1107
- >Adds a background behind the clock</span
1108
- >
1109
- </div>
1110
- </mat-dialog-content>
1111
-
1112
- <mat-dialog-actions align="end">
1113
- <button mat-button (click)="onCancel()">Cancel</button>
1114
- <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
1115
- Save
1116
- </button>
1117
- </mat-dialog-actions>
1118
- `, 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"] }] });
1119
- }
1120
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ClockStateDialogComponent, decorators: [{
1121
- type: Component,
1122
- args: [{ selector: 'demo-clock-state-dialog', standalone: true, imports: [
1123
- CommonModule,
1124
- FormsModule,
1125
- MatDialogModule,
1126
- MatButtonModule,
1127
- MatRadioModule,
1128
- MatSlideToggleModule,
1129
- ], template: `
1130
- <h2 mat-dialog-title>Clock Settings</h2>
1131
- <mat-dialog-content>
1132
- <div class="mode-selection">
1133
- <label class="section-label">Display Mode</label>
1134
- <mat-radio-group
1135
- [value]="mode()"
1136
- (change)="mode.set($any($event.value))"
1137
- >
1138
- <mat-radio-button value="digital">Digital</mat-radio-button>
1139
- <mat-radio-button value="analog">Analog</mat-radio-button>
1140
- </mat-radio-group>
1141
- </div>
1142
-
1143
- <!-- Time Format (only for digital mode) -->
1144
- @if (mode() === 'digital') {
1145
- <div class="format-selection">
1146
- <label class="section-label">Time Format</label>
1147
- <mat-radio-group
1148
- [value]="timeFormat()"
1149
- (change)="timeFormat.set($any($event.value))"
1150
- >
1151
- <mat-radio-button value="24h">24 Hour (14:30:45)</mat-radio-button>
1152
- <mat-radio-button value="12h">12 Hour (2:30:45 PM)</mat-radio-button>
1153
- </mat-radio-group>
1154
- </div>
1155
- }
1156
-
1157
- <!-- Show Seconds Toggle (for both digital and analog modes) -->
1158
- <div class="toggle-section">
1159
- <mat-slide-toggle
1160
- [checked]="showSeconds()"
1161
- (change)="showSeconds.set($event.checked)">
1162
- Show Seconds
1163
- </mat-slide-toggle>
1164
- <span class="toggle-description">
1165
- @if (mode() === 'digital') {
1166
- Display seconds in the time
1167
- } @else {
1168
- Show the second hand on the clock
1169
- }
1170
- </span>
1171
- </div>
1172
-
1173
- <!-- Background Toggle -->
1174
- <div class="toggle-section">
1175
- <mat-slide-toggle
1176
- [checked]="hasBackground()"
1177
- (change)="hasBackground.set($event.checked)">
1178
- Background
1179
- </mat-slide-toggle>
1180
- <span class="toggle-description"
1181
- >Adds a background behind the clock</span
1182
- >
1183
- </div>
1184
- </mat-dialog-content>
1185
-
1186
- <mat-dialog-actions align="end">
1187
- <button mat-button (click)="onCancel()">Cancel</button>
1188
- <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
1189
- Save
1190
- </button>
1191
- </mat-dialog-actions>
1192
- `, 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"] }]
1193
- }] });
1194
-
1195
- class DigitalClockComponent {
1196
- #destroyRef = inject(DestroyRef);
1197
- // Inputs
1198
- timeFormat = input('24h');
1199
- showSeconds = input(true);
1200
- hasBackground = input(false);
1201
- // Time tracking
1202
- currentTime = signal(new Date());
1203
- formattedTime = computed(() => {
1204
- const time = this.currentTime();
1205
- const format = this.timeFormat();
1206
- const showSecs = this.showSeconds();
1207
- return this.#formatTime(time, format, showSecs);
1208
- });
1209
- #intervalId = null;
1210
- #formatTime(time, format, showSecs) {
1211
- let hours = time.getHours();
1212
- const minutes = time.getMinutes();
1213
- const seconds = time.getSeconds();
1214
- // Pad with leading zeros
1215
- const mm = minutes.toString().padStart(2, '0');
1216
- const ss = seconds.toString().padStart(2, '0');
1217
- if (format === '12h') {
1218
- // 12-hour format with AM/PM
1219
- const ampm = hours >= 12 ? 'PM' : 'AM';
1220
- hours = hours % 12;
1221
- if (hours === 0)
1222
- hours = 12; // Convert 0 to 12 for 12 AM/PM
1223
- const hh = hours.toString().padStart(2, '0');
1224
- return showSecs ? `${hh}:${mm}:${ss} ${ampm}` : `${hh}:${mm} ${ampm}`;
1225
- }
1226
- else {
1227
- // 24-hour format
1228
- const hh = hours.toString().padStart(2, '0');
1229
- return showSecs ? `${hh}:${mm}:${ss}` : `${hh}:${mm}`;
1230
- }
1231
- }
1232
- constructor() {
1233
- // Set up time update timer
1234
- this.#startTimer();
1235
- // Clean up timer on component destruction
1236
- this.#destroyRef.onDestroy(() => {
1237
- this.#stopTimer();
1238
- });
1239
- }
1240
- #startTimer() {
1241
- // Sync to the next second boundary for smooth start
1242
- const now = new Date();
1243
- const msUntilNextSecond = 1000 - now.getMilliseconds();
1244
- setTimeout(() => {
1245
- this.currentTime.set(new Date());
1246
- // Start the regular 1-second interval
1247
- this.#intervalId = window.setInterval(() => {
1248
- this.currentTime.set(new Date());
1249
- }, 1000);
1250
- }, msUntilNextSecond);
1251
- }
1252
- #stopTimer() {
1253
- if (this.#intervalId !== null) {
1254
- clearInterval(this.#intervalId);
1255
- this.#intervalId = null;
1256
- }
1257
- }
1258
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DigitalClockComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1259
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.6", type: DigitalClockComponent, isStandalone: true, selector: "ngx-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 });
1260
- }
1261
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DigitalClockComponent, decorators: [{
1262
- type: Component,
1263
- args: [{ selector: 'ngx-digital-clock', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1264
- '[class.has-background]': 'hasBackground()',
1265
- '[class.show-pm]': 'timeFormat() === "12h"',
1266
- '[class.show-seconds]': 'showSeconds()',
1267
- class: 'clock-widget digital',
1268
- }, 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"] }]
1269
- }], ctorParameters: () => [] });
1270
-
1271
- class AnalogClockComponent {
1272
- #destroyRef = inject(DestroyRef);
1273
- #renderer = inject(Renderer2);
1274
- // Inputs
1275
- hasBackground = input(false);
1276
- showSeconds = input(true);
1277
- // ViewChild references for clock hands
1278
- hourHand = viewChild('hourHand');
1279
- minuteHand = viewChild('minuteHand');
1280
- secondHand = viewChild('secondHand');
1281
- // Time tracking
1282
- currentTime = signal(new Date());
1283
- // Computed rotation signals
1284
- secondHandRotation = computed(() => {
1285
- const seconds = this.currentTime().getSeconds();
1286
- return seconds * 6; // 360° / 60s = 6° per second
1287
- });
1288
- minuteHandRotation = computed(() => {
1289
- const time = this.currentTime();
1290
- const minutes = time.getMinutes();
1291
- const seconds = time.getSeconds();
1292
- return minutes * 6 + seconds / 10; // Smooth minute hand movement
1293
- });
1294
- hourHandRotation = computed(() => {
1295
- const time = this.currentTime();
1296
- const hours = time.getHours() % 12;
1297
- const minutes = time.getMinutes();
1298
- const seconds = time.getSeconds();
1299
- return hours * 30 + minutes / 2 + seconds / 120; // Smooth hour hand movement
1300
- });
1301
- #intervalId = null;
1302
- constructor() {
1303
- // Set up time update timer
1304
- this.#startTimer();
1305
- // Clean up timer on component destruction
1306
- this.#destroyRef.onDestroy(() => {
1307
- this.#stopTimer();
1308
- });
1309
- // Update DOM when rotations change
1310
- effect(() => {
1311
- this.#updateClockHands();
1312
- });
1313
- }
1314
- #startTimer() {
1315
- // Sync to the next second boundary for smooth start
1316
- const now = new Date();
1317
- const msUntilNextSecond = 1000 - now.getMilliseconds();
1318
- setTimeout(() => {
1319
- this.currentTime.set(new Date());
1320
- // Start the regular 1-second interval
1321
- this.#intervalId = window.setInterval(() => {
1322
- this.currentTime.set(new Date());
1323
- }, 1000);
1324
- }, msUntilNextSecond);
1325
- }
1326
- #stopTimer() {
1327
- if (this.#intervalId !== null) {
1328
- clearInterval(this.#intervalId);
1329
- this.#intervalId = null;
1330
- }
1331
- }
1332
- #updateClockHands() {
1333
- const hourElement = this.hourHand()?.nativeElement;
1334
- const minuteElement = this.minuteHand()?.nativeElement;
1335
- const secondElement = this.secondHand()?.nativeElement;
1336
- if (hourElement) {
1337
- this.#renderer.setAttribute(hourElement, 'transform', `rotate(${this.hourHandRotation()}, 400, 400)`);
1338
- }
1339
- if (minuteElement) {
1340
- this.#renderer.setAttribute(minuteElement, 'transform', `rotate(${this.minuteHandRotation()}, 400, 400)`);
1341
- }
1342
- if (secondElement && this.showSeconds()) {
1343
- this.#renderer.setAttribute(secondElement, 'transform', `rotate(${this.secondHandRotation()}, 400, 400)`);
1344
- }
1345
- }
1346
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: AnalogClockComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1347
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "20.0.6", type: AnalogClockComponent, isStandalone: true, selector: "ngx-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 });
1348
- }
1349
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: AnalogClockComponent, decorators: [{
1350
- type: Component,
1351
- args: [{ selector: 'ngx-analog-clock', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1352
- '[class.has-background]': 'hasBackground()',
1353
- '[class.show-seconds]': 'showSeconds()',
1354
- 'class': 'clock-widget analog'
1355
- }, 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"] }]
1356
- }], ctorParameters: () => [] });
1357
-
1358
- // clock-widget.component.ts
1359
- class ClockWidgetComponent {
1360
- static metadata = {
1361
- widgetTypeid: '@default/clock-widget',
1362
- name: 'Clock',
1363
- description: 'Display time in analog or digital format',
1364
- svgIcon,
1365
- };
1366
- #sanitizer = inject(DomSanitizer);
1367
- #dialog = inject(MatDialog);
1368
- safeSvgIcon = this.#sanitizer.bypassSecurityTrustHtml(svgIcon);
1369
- state = signal({
1370
- mode: 'analog',
1371
- hasBackground: true,
1372
- timeFormat: '24h',
1373
- showSeconds: true,
1374
- });
1375
- constructor() {
1376
- // No timer logic needed - DigitalClock manages its own time
1377
- }
1378
- dashboardSetState(state) {
1379
- if (state) {
1380
- this.state.update((current) => ({
1381
- ...current,
1382
- ...state,
1383
- }));
1384
- }
1385
- }
1386
- dashboardGetState() {
1387
- return { ...this.state() };
1388
- }
1389
- dashboardEditState() {
1390
- const dialogRef = this.#dialog.open(ClockStateDialogComponent, {
1391
- data: this.dashboardGetState(),
1392
- width: '400px',
1393
- maxWidth: '90vw',
1394
- disableClose: false,
1395
- autoFocus: false,
1396
- });
1397
- dialogRef
1398
- .afterClosed()
1399
- .subscribe((result) => {
1400
- if (result) {
1401
- this.state.set(result);
1402
- }
1403
- });
1404
- }
1405
- get isAnalog() {
1406
- return this.state().mode === 'analog';
1407
- }
1408
- get isDigital() {
1409
- return this.state().mode === 'digital';
1410
- }
1411
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ClockWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1412
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: ClockWidgetComponent, isStandalone: true, selector: "ngx-dashboard-clock-widget", ngImport: i0, template: "@if (isDigital) {\r\n <ngx-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 <ngx-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: "ngx-digital-clock", inputs: ["timeFormat", "showSeconds", "hasBackground"] }, { kind: "component", type: AnalogClockComponent, selector: "ngx-analog-clock", inputs: ["hasBackground", "showSeconds"] }] });
1413
- }
1414
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ClockWidgetComponent, decorators: [{
1415
- type: Component,
1416
- args: [{ selector: 'ngx-dashboard-clock-widget', standalone: true, imports: [DigitalClockComponent, AnalogClockComponent], template: "@if (isDigital) {\r\n <ngx-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 <ngx-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"] }]
1417
- }], ctorParameters: () => [] });
1418
-
1419
- /*
1420
- * Public API Surface of ngx-dashboard-widgets
1421
- */
1422
-
1423
- /**
1424
- * Generated bundle index. Do not edit.
1425
- */
1426
-
1427
- export { ArrowWidgetComponent, ClockWidgetComponent, LabelWidgetComponent, ResponsiveTextDirective };
1428
- //# sourceMappingURL=dragonworks-ngx-dashboard-widgets.mjs.map