@dragonworks/ngx-dashboard-widgets 20.0.4 → 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 -1255
  33. package/fesm2022/dragonworks-ngx-dashboard-widgets.mjs.map +0 -1
  34. package/index.d.ts +0 -147
@@ -1,1255 +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
- // Store original values for comparison
255
- originalLabel = this.data.label ?? '';
256
- originalFontSize = this.data.fontSize ?? 16;
257
- originalAlignment = this.data.alignment ?? 'center';
258
- originalFontWeight = this.data.fontWeight ?? 'normal';
259
- originalOpacity = this.data.opacity ?? 1;
260
- originalHasBackground = this.data.hasBackground ?? true;
261
- originalResponsive = this.data.responsive ?? false;
262
- // Computed values
263
- hasChanged = computed(() => this.label() !== this.originalLabel ||
264
- this.fontSize() !== this.originalFontSize ||
265
- this.alignment() !== this.originalAlignment ||
266
- this.fontWeight() !== this.originalFontWeight ||
267
- this.opacity() !== this.originalOpacity ||
268
- this.hasBackground() !== this.originalHasBackground ||
269
- this.responsive() !== this.originalResponsive);
270
- formatOpacity(value) {
271
- return Math.round(value * 100);
272
- }
273
- formatOpacitySlider = (value) => {
274
- return `${Math.round(value * 100)}%`;
275
- };
276
- onBackgroundToggle(hasWhiteBackground) {
277
- this.hasBackground.set(hasWhiteBackground);
278
- this.transparentBackground.set(!hasWhiteBackground);
279
- }
280
- onCancel() {
281
- this.dialogRef.close();
282
- }
283
- save() {
284
- this.dialogRef.close({
285
- label: this.label(),
286
- fontSize: this.fontSize(),
287
- alignment: this.alignment(),
288
- fontWeight: this.fontWeight(),
289
- opacity: this.opacity(),
290
- hasBackground: this.hasBackground(),
291
- responsive: this.responsive(),
292
- });
293
- }
294
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: LabelStateDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
295
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.0.6", type: LabelStateDialogComponent, isStandalone: true, selector: "lib-label-state-dialog", ngImport: i0, template: `
296
- <h2 mat-dialog-title>Label Settings</h2>
297
- <mat-dialog-content>
298
- <mat-form-field appearance="outline" class="label-text-field">
299
- <mat-label>Label Text</mat-label>
300
- <input
301
- matInput
302
- type="text"
303
- [value]="label()"
304
- (input)="label.set($any($event.target).value)"
305
- placeholder="Enter your label text..."
306
- />
307
- </mat-form-field>
308
-
309
- <!-- Responsive Text Toggle -->
310
- <div class="toggle-section">
311
- <mat-slide-toggle
312
- [checked]="responsive()"
313
- (change)="responsive.set($event.checked)">
314
- Responsive Text
315
- </mat-slide-toggle>
316
- <span class="toggle-description"
317
- >Automatically adjust text size to fit the widget</span
318
- >
319
- </div>
320
-
321
- <div class="row-layout">
322
- <mat-form-field appearance="outline">
323
- <mat-label>Font Size (px)</mat-label>
324
- <input
325
- matInput
326
- type="number"
327
- [value]="fontSize()"
328
- (input)="fontSize.set(+$any($event.target).value)"
329
- [disabled]="responsive()"
330
- min="8"
331
- max="48"
332
- placeholder="16"
333
- />
334
- </mat-form-field>
335
-
336
- <mat-form-field appearance="outline">
337
- <mat-label>Alignment</mat-label>
338
- <mat-select
339
- [value]="alignment()"
340
- (selectionChange)="alignment.set($any($event.value))"
341
- >
342
- <mat-option value="left">Left</mat-option>
343
- <mat-option value="center">Center</mat-option>
344
- <mat-option value="right">Right</mat-option>
345
- </mat-select>
346
- </mat-form-field>
347
- </div>
348
-
349
- <mat-form-field appearance="outline">
350
- <mat-label>Font Weight</mat-label>
351
- <mat-select
352
- [value]="fontWeight()"
353
- (selectionChange)="fontWeight.set($any($event.value))"
354
- >
355
- <mat-option value="normal">Normal</mat-option>
356
- <mat-option value="bold">Bold</mat-option>
357
- </mat-select>
358
- </mat-form-field>
359
-
360
- <!-- Opacity Slider -->
361
- <div class="slider-section">
362
- <div class="slider-label">Opacity: {{ formatOpacity(opacity()) }}%</div>
363
- <mat-slider [min]="0.1" [max]="1" [step]="0.1">
364
- <input matSliderThumb [(ngModel)]="opacity" />
365
- </mat-slider>
366
- </div>
367
-
368
- <!-- Background Toggle -->
369
- <div class="toggle-section">
370
- <mat-slide-toggle
371
- [checked]="!transparentBackground()"
372
- (change)="onBackgroundToggle($event.checked)">
373
- Background
374
- </mat-slide-toggle>
375
- <span class="toggle-description"
376
- >Adds a background behind the text</span
377
- >
378
- </div>
379
- </mat-dialog-content>
380
-
381
- <mat-dialog-actions align="end">
382
- <button mat-button (click)="onCancel()">Cancel</button>
383
- <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
384
- Save
385
- </button>
386
- </mat-dialog-actions>
387
- `, 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}\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: 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"] }] });
388
- }
389
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: LabelStateDialogComponent, decorators: [{
390
- type: Component,
391
- args: [{ selector: 'lib-label-state-dialog', standalone: true, imports: [
392
- CommonModule,
393
- FormsModule,
394
- MatDialogModule,
395
- MatButtonModule,
396
- MatFormFieldModule,
397
- MatInputModule,
398
- MatSelectModule,
399
- MatSliderModule,
400
- MatSlideToggleModule, // Add this import
401
- ], template: `
402
- <h2 mat-dialog-title>Label Settings</h2>
403
- <mat-dialog-content>
404
- <mat-form-field appearance="outline" class="label-text-field">
405
- <mat-label>Label Text</mat-label>
406
- <input
407
- matInput
408
- type="text"
409
- [value]="label()"
410
- (input)="label.set($any($event.target).value)"
411
- placeholder="Enter your label text..."
412
- />
413
- </mat-form-field>
414
-
415
- <!-- Responsive Text Toggle -->
416
- <div class="toggle-section">
417
- <mat-slide-toggle
418
- [checked]="responsive()"
419
- (change)="responsive.set($event.checked)">
420
- Responsive Text
421
- </mat-slide-toggle>
422
- <span class="toggle-description"
423
- >Automatically adjust text size to fit the widget</span
424
- >
425
- </div>
426
-
427
- <div class="row-layout">
428
- <mat-form-field appearance="outline">
429
- <mat-label>Font Size (px)</mat-label>
430
- <input
431
- matInput
432
- type="number"
433
- [value]="fontSize()"
434
- (input)="fontSize.set(+$any($event.target).value)"
435
- [disabled]="responsive()"
436
- min="8"
437
- max="48"
438
- placeholder="16"
439
- />
440
- </mat-form-field>
441
-
442
- <mat-form-field appearance="outline">
443
- <mat-label>Alignment</mat-label>
444
- <mat-select
445
- [value]="alignment()"
446
- (selectionChange)="alignment.set($any($event.value))"
447
- >
448
- <mat-option value="left">Left</mat-option>
449
- <mat-option value="center">Center</mat-option>
450
- <mat-option value="right">Right</mat-option>
451
- </mat-select>
452
- </mat-form-field>
453
- </div>
454
-
455
- <mat-form-field appearance="outline">
456
- <mat-label>Font Weight</mat-label>
457
- <mat-select
458
- [value]="fontWeight()"
459
- (selectionChange)="fontWeight.set($any($event.value))"
460
- >
461
- <mat-option value="normal">Normal</mat-option>
462
- <mat-option value="bold">Bold</mat-option>
463
- </mat-select>
464
- </mat-form-field>
465
-
466
- <!-- Opacity Slider -->
467
- <div class="slider-section">
468
- <div class="slider-label">Opacity: {{ formatOpacity(opacity()) }}%</div>
469
- <mat-slider [min]="0.1" [max]="1" [step]="0.1">
470
- <input matSliderThumb [(ngModel)]="opacity" />
471
- </mat-slider>
472
- </div>
473
-
474
- <!-- Background Toggle -->
475
- <div class="toggle-section">
476
- <mat-slide-toggle
477
- [checked]="!transparentBackground()"
478
- (change)="onBackgroundToggle($event.checked)">
479
- Background
480
- </mat-slide-toggle>
481
- <span class="toggle-description"
482
- >Adds a background behind the text</span
483
- >
484
- </div>
485
- </mat-dialog-content>
486
-
487
- <mat-dialog-actions align="end">
488
- <button mat-button (click)="onCancel()">Cancel</button>
489
- <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
490
- Save
491
- </button>
492
- </mat-dialog-actions>
493
- `, 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}\n"] }]
494
- }] });
495
-
496
- /**
497
- * Directive that automatically adjusts font size to fit text within its parent container.
498
- * Uses canvas-based measurement for performance and DOM verification for accuracy.
499
- *
500
- * @example
501
- * <div class="container">
502
- * <span responsiveText [min]="12" [max]="72">Dynamic text here</span>
503
- * </div>
504
- */
505
- class ResponsiveTextDirective {
506
- /* ───────────────────────── Inputs with transforms ─────────────── */
507
- /** Minimum font-size in pixels (accessibility floor) */
508
- min = input(8, { transform: numberAttribute });
509
- /** Maximum font-size in pixels (layout ceiling) */
510
- max = input(512, { transform: numberAttribute });
511
- /**
512
- * Line-height: pass a multiplier (e.g. 1.1) or absolute px value.
513
- * For single-line text a multiplier < 10 is treated as unitless.
514
- */
515
- lineHeight = input(1.1, { transform: numberAttribute });
516
- /** Whether to observe text mutations after first render */
517
- observeMutations = input(true, { transform: booleanAttribute });
518
- /** Debounce delay in ms for resize/mutation callbacks */
519
- debounceMs = input(16, { transform: numberAttribute });
520
- /* ───────────────────────── Private state ───────────────────────── */
521
- el = inject(ElementRef);
522
- zone = inject(NgZone);
523
- platformId = inject(PLATFORM_ID);
524
- destroyRef = inject(DestroyRef);
525
- // Canvas context - lazy initialization
526
- _ctx;
527
- get ctx() {
528
- if (!this._ctx) {
529
- const canvas = document.createElement('canvas');
530
- this._ctx = canvas.getContext('2d', {
531
- willReadFrequently: true,
532
- alpha: false,
533
- });
534
- }
535
- return this._ctx;
536
- }
537
- ro;
538
- mo;
539
- fitTimeout;
540
- // Cache for performance
541
- lastText = '';
542
- lastMaxW = 0;
543
- lastMaxH = 0;
544
- lastFontSize = 0;
545
- /* ───────────────────────── Lifecycle ──────────────────────────── */
546
- ngAfterViewInit() {
547
- if (!isPlatformBrowser(this.platformId))
548
- return;
549
- // Set initial styles
550
- const span = this.el.nativeElement;
551
- span.style.transition = 'font-size 0.1s ease-out';
552
- // All observer callbacks run outside Angular's zone
553
- this.zone.runOutsideAngular(() => {
554
- this.fit();
555
- this.observeResize();
556
- if (this.observeMutations()) {
557
- this.observeText();
558
- }
559
- });
560
- }
561
- ngOnDestroy() {
562
- this.cleanup();
563
- }
564
- /* ───────────────────── Core fitting logic ───────────────────── */
565
- /**
566
- * Debounced fit handler to prevent excessive recalculations
567
- */
568
- requestFit = () => {
569
- if (this.fitTimeout) {
570
- cancelAnimationFrame(this.fitTimeout);
571
- }
572
- this.fitTimeout = requestAnimationFrame(() => {
573
- this.fit();
574
- });
575
- };
576
- /**
577
- * Recalculate & apply the ideal font-size
578
- */
579
- fit = () => {
580
- const span = this.el.nativeElement;
581
- const parent = span.parentElement;
582
- if (!parent)
583
- return;
584
- const text = span.textContent?.trim() || '';
585
- if (!text) {
586
- span.style.fontSize = `${this.min()}px`;
587
- return;
588
- }
589
- // Get available space
590
- const { maxW, maxH } = this.getAvailableSpace(parent);
591
- // Check cache to avoid redundant calculations
592
- if (text === this.lastText &&
593
- maxW === this.lastMaxW &&
594
- maxH === this.lastMaxH &&
595
- this.lastFontSize > 0) {
596
- return;
597
- }
598
- // Calculate ideal font size
599
- const ideal = this.calcFit(text, maxW, maxH);
600
- span.style.fontSize = `${ideal}px`;
601
- // DOM verification pass
602
- this.verifyFit(span, maxW, maxH, ideal);
603
- // Update cache
604
- this.lastText = text;
605
- this.lastMaxW = maxW;
606
- this.lastMaxH = maxH;
607
- this.lastFontSize = parseFloat(span.style.fontSize);
608
- };
609
- /**
610
- * Calculate available space accounting for padding and borders
611
- */
612
- getAvailableSpace(parent) {
613
- const cs = getComputedStyle(parent);
614
- const maxW = parent.clientWidth -
615
- parseFloat(cs.paddingLeft) -
616
- parseFloat(cs.paddingRight);
617
- const maxH = parent.clientHeight -
618
- parseFloat(cs.paddingTop) -
619
- parseFloat(cs.paddingBottom);
620
- return { maxW: Math.max(0, maxW), maxH: Math.max(0, maxH) };
621
- }
622
- /**
623
- * DOM-based verification to handle sub-pixel discrepancies
624
- */
625
- verifyFit(span, maxW, maxH, ideal) {
626
- // Use requestAnimationFrame to ensure layout is complete
627
- requestAnimationFrame(() => {
628
- if (span.scrollWidth > maxW || span.scrollHeight > maxH) {
629
- let safe = ideal;
630
- while (safe > this.min() &&
631
- (span.scrollWidth > maxW || span.scrollHeight > maxH)) {
632
- safe -= 0.5; // Finer adjustments
633
- span.style.fontSize = `${safe}px`;
634
- }
635
- }
636
- });
637
- }
638
- /* ───────────────────── Binary search algorithm ────────────────── */
639
- /**
640
- * Binary search for optimal font size using canvas measurements
641
- */
642
- calcFit(text, maxW, maxH, precision = 0.1) {
643
- if (maxW <= 0 || maxH <= 0)
644
- return this.min();
645
- const computedStyle = getComputedStyle(this.el.nativeElement);
646
- const fontFamily = computedStyle.fontFamily || 'sans-serif';
647
- const fontWeight = computedStyle.fontWeight || '400';
648
- let lo = this.min();
649
- let hi = this.max();
650
- let bestFit = this.min();
651
- while (hi - lo > precision) {
652
- const mid = (hi + lo) / 2;
653
- this.ctx.font = `${fontWeight} ${mid}px ${fontFamily}`;
654
- const metrics = this.ctx.measureText(text);
655
- const width = metrics.width;
656
- // Calculate height based on available metrics
657
- const height = this.calculateTextHeight(metrics, mid);
658
- if (width <= maxW && height <= maxH) {
659
- bestFit = mid;
660
- lo = mid;
661
- }
662
- else {
663
- hi = mid;
664
- }
665
- }
666
- return Math.floor(bestFit * 100) / 100;
667
- }
668
- /**
669
- * Calculate text height from metrics
670
- */
671
- calculateTextHeight(metrics, fontSize) {
672
- // Use font bounding box metrics if available
673
- if (metrics.fontBoundingBoxAscent && metrics.fontBoundingBoxDescent) {
674
- return metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
675
- }
676
- // Fallback to actual bounding box
677
- if (metrics.actualBoundingBoxAscent && metrics.actualBoundingBoxDescent) {
678
- return metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
679
- }
680
- // Final fallback using line height
681
- return this.lineHeight() < 10
682
- ? fontSize * this.lineHeight()
683
- : this.lineHeight();
684
- }
685
- /* ───────────────────────── Observers ─────────────────────────── */
686
- /**
687
- * Observe parent container resizes
688
- */
689
- observeResize() {
690
- if (!('ResizeObserver' in window))
691
- return;
692
- this.ro = new ResizeObserver((entries) => {
693
- // Only trigger if size actually changed
694
- const entry = entries[0];
695
- if (entry?.contentRect) {
696
- this.requestFit();
697
- }
698
- });
699
- const parent = this.el.nativeElement.parentElement;
700
- if (parent) {
701
- this.ro.observe(parent);
702
- }
703
- }
704
- /**
705
- * Observe text content changes
706
- */
707
- observeText() {
708
- if (!('MutationObserver' in window))
709
- return;
710
- this.mo = new MutationObserver((mutations) => {
711
- // Check if text actually changed
712
- const hasTextChange = mutations.some((m) => m.type === 'characterData' ||
713
- (m.type === 'childList' &&
714
- (m.addedNodes.length > 0 || m.removedNodes.length > 0)));
715
- if (hasTextChange) {
716
- this.requestFit();
717
- }
718
- });
719
- this.mo.observe(this.el.nativeElement, {
720
- characterData: true,
721
- childList: true,
722
- subtree: true,
723
- });
724
- }
725
- /**
726
- * Cleanup resources
727
- */
728
- cleanup() {
729
- this.ro?.disconnect();
730
- this.mo?.disconnect();
731
- if (this.fitTimeout) {
732
- cancelAnimationFrame(this.fitTimeout);
733
- }
734
- // Clear canvas context
735
- this._ctx = undefined;
736
- }
737
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ResponsiveTextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
738
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.0.6", type: ResponsiveTextDirective, isStandalone: true, selector: "[responsiveText]", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", 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": "\"hidden\"", "style.text-overflow": "\"ellipsis\"" } }, ngImport: i0 });
739
- }
740
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ResponsiveTextDirective, decorators: [{
741
- type: Directive,
742
- args: [{
743
- selector: '[responsiveText]',
744
- standalone: true,
745
- host: {
746
- '[style.display]': '"block"',
747
- '[style.width]': '"100%"',
748
- '[style.white-space]': '"nowrap"',
749
- '[style.overflow]': '"hidden"',
750
- '[style.text-overflow]': '"ellipsis"',
751
- },
752
- }]
753
- }] });
754
-
755
- // label-widget.component.ts
756
- class LabelWidgetComponent {
757
- static metadata = {
758
- widgetTypeid: '@default/label-widget',
759
- name: 'Label',
760
- description: 'A generic text label',
761
- svgIcon: svgIcon$1,
762
- };
763
- #sanitizer = inject(DomSanitizer);
764
- #dialog = inject(MatDialog);
765
- safeSvgIcon = this.#sanitizer.bypassSecurityTrustHtml(svgIcon$1);
766
- state = signal({
767
- label: '',
768
- fontSize: 16,
769
- alignment: 'center',
770
- fontWeight: 'normal',
771
- opacity: 1,
772
- hasBackground: true,
773
- responsive: false,
774
- });
775
- dashboardSetState(state) {
776
- if (state) {
777
- this.state.update((current) => ({
778
- ...current,
779
- ...state,
780
- }));
781
- }
782
- }
783
- dashboardGetState() {
784
- return { ...this.state() };
785
- }
786
- dashboardEditState() {
787
- const dialogRef = this.#dialog.open(LabelStateDialogComponent, {
788
- data: this.dashboardGetState(),
789
- width: '400px',
790
- maxWidth: '90vw',
791
- disableClose: false,
792
- autoFocus: false,
793
- });
794
- dialogRef
795
- .afterClosed()
796
- .subscribe((result) => {
797
- if (result) {
798
- this.state.set(result);
799
- }
800
- });
801
- }
802
- get hasContent() {
803
- return !!this.state().label?.trim();
804
- }
805
- get label() {
806
- return this.state().label?.trim();
807
- }
808
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: LabelWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
809
- 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>{{ 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: ["min", "max", "lineHeight", "observeMutations", "debounceMs"] }] });
810
- }
811
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: LabelWidgetComponent, decorators: [{
812
- type: Component,
813
- 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>{{ 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"] }]
814
- }] });
815
-
816
- // clock-widget.metadata.ts
817
- const svgIcon = `
818
- <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 800 800">
819
- <circle cx="400" cy="400" r="400" fill="#fff"/>
820
- <use transform="matrix(-1,0,0,1,800,0)" xlink:href="#one-half"/>
821
- <g id="one-half">
822
- <g id="one-fourth">
823
- <path d="m400 40v107" stroke="#000" stroke-width="26.7"/>
824
- <g id="one-twelfth">
825
- <path d="m580 88.233-42.5 73.612" stroke="#000" stroke-width="26.7"/>
826
- <g id="one-thirtieth">
827
- <path id="one-sixtieth" d="m437.63 41.974-3.6585 34.808" stroke="#000" stroke-width="13.6" />
828
- <use transform="rotate(6 400 400)" xlink:href="#one-sixtieth"/>
829
- </g>
830
- <use transform="rotate(12 400 400)" xlink:href="#one-thirtieth"/>
831
- </g>
832
- <use transform="rotate(30 400 400)" xlink:href="#one-twelfth"/>
833
- <use transform="rotate(60 400 400)" xlink:href="#one-twelfth"/>
834
- </g>
835
- <use transform="rotate(90 400 400)" xlink:href="#one-fourth"/>
836
- </g>
837
- <path class="clock-hour-hand" id="anim-clock-hour-hand" d="m 381.925,476 h 36.15 l 5e-4,-300.03008 L 400,156.25 381.9245,175.96992 Z" transform="rotate(110.2650694444, 400, 400)" />
838
- <path class="clock-minute-hand" id="anim-clock-minute-hand" d="M 412.063,496.87456 H 387.937 L 385.249,65.68306 400,52.75 414.751,65.68306 Z" transform="rotate(243.1808333333, 400, 400)" />
839
- </svg>
840
- `;
841
-
842
- class ClockStateDialogComponent {
843
- data = inject(MAT_DIALOG_DATA);
844
- dialogRef = inject((MatDialogRef));
845
- // State signals
846
- mode = signal(this.data.mode ?? 'digital');
847
- hasBackground = signal(this.data.hasBackground ?? true);
848
- timeFormat = signal(this.data.timeFormat ?? '24h');
849
- showSeconds = signal(this.data.showSeconds ?? true);
850
- // Store original values for comparison
851
- originalMode = this.data.mode ?? 'digital';
852
- originalHasBackground = this.data.hasBackground ?? true;
853
- originalTimeFormat = this.data.timeFormat ?? '24h';
854
- originalShowSeconds = this.data.showSeconds ?? true;
855
- // Computed values
856
- hasChanged = computed(() => this.mode() !== this.originalMode ||
857
- this.hasBackground() !== this.originalHasBackground ||
858
- this.timeFormat() !== this.originalTimeFormat ||
859
- this.showSeconds() !== this.originalShowSeconds);
860
- onCancel() {
861
- this.dialogRef.close();
862
- }
863
- save() {
864
- this.dialogRef.close({
865
- mode: this.mode(),
866
- hasBackground: this.hasBackground(),
867
- timeFormat: this.timeFormat(),
868
- showSeconds: this.showSeconds(),
869
- });
870
- }
871
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ClockStateDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
872
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.6", type: ClockStateDialogComponent, isStandalone: true, selector: "demo-clock-state-dialog", ngImport: i0, template: `
873
- <h2 mat-dialog-title>Clock Settings</h2>
874
- <mat-dialog-content>
875
- <div class="mode-selection">
876
- <label class="section-label">Display Mode</label>
877
- <mat-radio-group
878
- [value]="mode()"
879
- (change)="mode.set($any($event.value))"
880
- >
881
- <mat-radio-button value="digital">Digital</mat-radio-button>
882
- <mat-radio-button value="analog">Analog</mat-radio-button>
883
- </mat-radio-group>
884
- </div>
885
-
886
- <!-- Time Format (only for digital mode) -->
887
- @if (mode() === 'digital') {
888
- <div class="format-selection">
889
- <label class="section-label">Time Format</label>
890
- <mat-radio-group
891
- [value]="timeFormat()"
892
- (change)="timeFormat.set($any($event.value))"
893
- >
894
- <mat-radio-button value="24h">24 Hour (14:30:45)</mat-radio-button>
895
- <mat-radio-button value="12h">12 Hour (2:30:45 PM)</mat-radio-button>
896
- </mat-radio-group>
897
- </div>
898
- }
899
-
900
- <!-- Show Seconds Toggle (for both digital and analog modes) -->
901
- <div class="toggle-section">
902
- <mat-slide-toggle
903
- [checked]="showSeconds()"
904
- (change)="showSeconds.set($event.checked)">
905
- Show Seconds
906
- </mat-slide-toggle>
907
- <span class="toggle-description">
908
- @if (mode() === 'digital') {
909
- Display seconds in the time
910
- } @else {
911
- Show the second hand on the clock
912
- }
913
- </span>
914
- </div>
915
-
916
- <!-- Background Toggle -->
917
- <div class="toggle-section">
918
- <mat-slide-toggle
919
- [checked]="hasBackground()"
920
- (change)="hasBackground.set($event.checked)">
921
- Background
922
- </mat-slide-toggle>
923
- <span class="toggle-description"
924
- >Adds a background behind the clock</span
925
- >
926
- </div>
927
- </mat-dialog-content>
928
-
929
- <mat-dialog-actions align="end">
930
- <button mat-button (click)="onCancel()">Cancel</button>
931
- <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
932
- Save
933
- </button>
934
- </mat-dialog-actions>
935
- `, 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"] }] });
936
- }
937
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ClockStateDialogComponent, decorators: [{
938
- type: Component,
939
- args: [{ selector: 'demo-clock-state-dialog', standalone: true, imports: [
940
- CommonModule,
941
- FormsModule,
942
- MatDialogModule,
943
- MatButtonModule,
944
- MatRadioModule,
945
- MatSlideToggleModule,
946
- ], template: `
947
- <h2 mat-dialog-title>Clock Settings</h2>
948
- <mat-dialog-content>
949
- <div class="mode-selection">
950
- <label class="section-label">Display Mode</label>
951
- <mat-radio-group
952
- [value]="mode()"
953
- (change)="mode.set($any($event.value))"
954
- >
955
- <mat-radio-button value="digital">Digital</mat-radio-button>
956
- <mat-radio-button value="analog">Analog</mat-radio-button>
957
- </mat-radio-group>
958
- </div>
959
-
960
- <!-- Time Format (only for digital mode) -->
961
- @if (mode() === 'digital') {
962
- <div class="format-selection">
963
- <label class="section-label">Time Format</label>
964
- <mat-radio-group
965
- [value]="timeFormat()"
966
- (change)="timeFormat.set($any($event.value))"
967
- >
968
- <mat-radio-button value="24h">24 Hour (14:30:45)</mat-radio-button>
969
- <mat-radio-button value="12h">12 Hour (2:30:45 PM)</mat-radio-button>
970
- </mat-radio-group>
971
- </div>
972
- }
973
-
974
- <!-- Show Seconds Toggle (for both digital and analog modes) -->
975
- <div class="toggle-section">
976
- <mat-slide-toggle
977
- [checked]="showSeconds()"
978
- (change)="showSeconds.set($event.checked)">
979
- Show Seconds
980
- </mat-slide-toggle>
981
- <span class="toggle-description">
982
- @if (mode() === 'digital') {
983
- Display seconds in the time
984
- } @else {
985
- Show the second hand on the clock
986
- }
987
- </span>
988
- </div>
989
-
990
- <!-- Background Toggle -->
991
- <div class="toggle-section">
992
- <mat-slide-toggle
993
- [checked]="hasBackground()"
994
- (change)="hasBackground.set($event.checked)">
995
- Background
996
- </mat-slide-toggle>
997
- <span class="toggle-description"
998
- >Adds a background behind the clock</span
999
- >
1000
- </div>
1001
- </mat-dialog-content>
1002
-
1003
- <mat-dialog-actions align="end">
1004
- <button mat-button (click)="onCancel()">Cancel</button>
1005
- <button mat-flat-button (click)="save()" [disabled]="!hasChanged()">
1006
- Save
1007
- </button>
1008
- </mat-dialog-actions>
1009
- `, 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"] }]
1010
- }] });
1011
-
1012
- class DigitalClockComponent {
1013
- #destroyRef = inject(DestroyRef);
1014
- // Inputs
1015
- timeFormat = input('24h');
1016
- showSeconds = input(true);
1017
- hasBackground = input(false);
1018
- // Time tracking
1019
- currentTime = signal(new Date());
1020
- formattedTime = computed(() => {
1021
- const time = this.currentTime();
1022
- const format = this.timeFormat();
1023
- const showSecs = this.showSeconds();
1024
- return this.#formatTime(time, format, showSecs);
1025
- });
1026
- #intervalId = null;
1027
- #formatTime(time, format, showSecs) {
1028
- let hours = time.getHours();
1029
- const minutes = time.getMinutes();
1030
- const seconds = time.getSeconds();
1031
- // Pad with leading zeros
1032
- const mm = minutes.toString().padStart(2, '0');
1033
- const ss = seconds.toString().padStart(2, '0');
1034
- if (format === '12h') {
1035
- // 12-hour format with AM/PM
1036
- const ampm = hours >= 12 ? 'PM' : 'AM';
1037
- hours = hours % 12;
1038
- if (hours === 0)
1039
- hours = 12; // Convert 0 to 12 for 12 AM/PM
1040
- const hh = hours.toString().padStart(2, '0');
1041
- if (showSecs) {
1042
- return `${hh}:${mm}:${ss} ${ampm}`;
1043
- }
1044
- else {
1045
- return `${hh}:${mm} ${ampm}`;
1046
- }
1047
- }
1048
- else {
1049
- // 24-hour format
1050
- const hh = hours.toString().padStart(2, '0');
1051
- if (showSecs) {
1052
- return `${hh}:${mm}:${ss}`;
1053
- }
1054
- else {
1055
- return `${hh}:${mm}`;
1056
- }
1057
- }
1058
- }
1059
- constructor() {
1060
- // Set up time update timer
1061
- this.#startTimer();
1062
- // Clean up timer on component destruction
1063
- this.#destroyRef.onDestroy(() => {
1064
- this.#stopTimer();
1065
- });
1066
- }
1067
- #startTimer() {
1068
- // Sync to the next second boundary for smooth start
1069
- const now = new Date();
1070
- const msUntilNextSecond = 1000 - now.getMilliseconds();
1071
- setTimeout(() => {
1072
- this.currentTime.set(new Date());
1073
- // Start the regular 1-second interval
1074
- this.#intervalId = window.setInterval(() => {
1075
- this.currentTime.set(new Date());
1076
- }, 1000);
1077
- }, msUntilNextSecond);
1078
- }
1079
- #stopTimer() {
1080
- if (this.#intervalId !== null) {
1081
- clearInterval(this.#intervalId);
1082
- this.#intervalId = null;
1083
- }
1084
- }
1085
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DigitalClockComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1086
- 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 class=\"digital-time\">{{ formattedTime() }}</div>", 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(12px,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(12px,min(15cqw,50cqh),200px)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1087
- }
1088
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: DigitalClockComponent, decorators: [{
1089
- type: Component,
1090
- args: [{ selector: 'ngx-digital-clock', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1091
- '[class.has-background]': 'hasBackground()',
1092
- '[class.show-pm]': 'timeFormat() === "12h"',
1093
- '[class.show-seconds]': 'showSeconds()',
1094
- 'class': 'clock-widget digital'
1095
- }, template: "<div class=\"digital-time\">{{ formattedTime() }}</div>", 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(12px,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(12px,min(15cqw,50cqh),200px)}\n"] }]
1096
- }], ctorParameters: () => [] });
1097
-
1098
- class AnalogClockComponent {
1099
- #destroyRef = inject(DestroyRef);
1100
- #renderer = inject(Renderer2);
1101
- // Inputs
1102
- hasBackground = input(false);
1103
- showSeconds = input(true);
1104
- // ViewChild references for clock hands
1105
- hourHand = viewChild('hourHand');
1106
- minuteHand = viewChild('minuteHand');
1107
- secondHand = viewChild('secondHand');
1108
- // Time tracking
1109
- currentTime = signal(new Date());
1110
- // Computed rotation signals
1111
- secondHandRotation = computed(() => {
1112
- const seconds = this.currentTime().getSeconds();
1113
- return seconds * 6; // 360° / 60s = 6° per second
1114
- });
1115
- minuteHandRotation = computed(() => {
1116
- const time = this.currentTime();
1117
- const minutes = time.getMinutes();
1118
- const seconds = time.getSeconds();
1119
- return minutes * 6 + seconds / 10; // Smooth minute hand movement
1120
- });
1121
- hourHandRotation = computed(() => {
1122
- const time = this.currentTime();
1123
- const hours = time.getHours() % 12;
1124
- const minutes = time.getMinutes();
1125
- const seconds = time.getSeconds();
1126
- return hours * 30 + minutes / 2 + seconds / 120; // Smooth hour hand movement
1127
- });
1128
- #intervalId = null;
1129
- constructor() {
1130
- // Set up time update timer
1131
- this.#startTimer();
1132
- // Clean up timer on component destruction
1133
- this.#destroyRef.onDestroy(() => {
1134
- this.#stopTimer();
1135
- });
1136
- // Update DOM when rotations change
1137
- effect(() => {
1138
- this.#updateClockHands();
1139
- });
1140
- }
1141
- #startTimer() {
1142
- // Sync to the next second boundary for smooth start
1143
- const now = new Date();
1144
- const msUntilNextSecond = 1000 - now.getMilliseconds();
1145
- setTimeout(() => {
1146
- this.currentTime.set(new Date());
1147
- // Start the regular 1-second interval
1148
- this.#intervalId = window.setInterval(() => {
1149
- this.currentTime.set(new Date());
1150
- }, 1000);
1151
- }, msUntilNextSecond);
1152
- }
1153
- #stopTimer() {
1154
- if (this.#intervalId !== null) {
1155
- clearInterval(this.#intervalId);
1156
- this.#intervalId = null;
1157
- }
1158
- }
1159
- #updateClockHands() {
1160
- const hourElement = this.hourHand()?.nativeElement;
1161
- const minuteElement = this.minuteHand()?.nativeElement;
1162
- const secondElement = this.secondHand()?.nativeElement;
1163
- if (hourElement) {
1164
- this.#renderer.setAttribute(hourElement, 'transform', `rotate(${this.hourHandRotation()}, 400, 400)`);
1165
- }
1166
- if (minuteElement) {
1167
- this.#renderer.setAttribute(minuteElement, 'transform', `rotate(${this.minuteHandRotation()}, 400, 400)`);
1168
- }
1169
- if (secondElement && this.showSeconds()) {
1170
- this.#renderer.setAttribute(secondElement, 'transform', `rotate(${this.secondHandRotation()}, 400, 400)`);
1171
- }
1172
- }
1173
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: AnalogClockComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1174
- 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 xmlns:xlink=\"http://www.w3.org/1999/xlink\"\r\n viewBox=\"0 0 800 800\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n >\r\n <!-- <circle cx=\"400\" cy=\"400\" r=\"400\" fill=\"#fff\"/> -->\r\n <use transform=\"matrix(-1,0,0,1,800,0)\" xlink:href=\"#one-half\" />\r\n <g id=\"one-half\">\r\n <g id=\"one-fourth\">\r\n <path d=\"m400 40v107\" stroke=\"#000\" stroke-width=\"26.7\" />\r\n <g id=\"one-twelfth\">\r\n <path\r\n d=\"m580 88.233-42.5 73.612\"\r\n stroke=\"#000\"\r\n stroke-width=\"26.7\"\r\n />\r\n <g id=\"one-thirtieth\">\r\n <path\r\n id=\"one-sixtieth\"\r\n d=\"m437.63 41.974-3.6585 34.808\"\r\n stroke=\"#000\"\r\n stroke-width=\"13.6\"\r\n />\r\n <use transform=\"rotate(6 400 400)\" xlink:href=\"#one-sixtieth\" />\r\n </g>\r\n <use transform=\"rotate(12 400 400)\" xlink:href=\"#one-thirtieth\" />\r\n </g>\r\n <use transform=\"rotate(30 400 400)\" xlink:href=\"#one-twelfth\" />\r\n <use transform=\"rotate(60 400 400)\" xlink:href=\"#one-twelfth\" />\r\n </g>\r\n <use transform=\"rotate(90 400 400)\" xlink:href=\"#one-fourth\" />\r\n </g>\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 fill=\"#a40000\"\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.clock-widget.analog{container-type:size;container-name:analog-clock}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1175
- }
1176
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: AnalogClockComponent, decorators: [{
1177
- type: Component,
1178
- args: [{ selector: 'ngx-analog-clock', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
1179
- '[class.has-background]': 'hasBackground()',
1180
- '[class.show-seconds]': 'showSeconds()',
1181
- 'class': 'clock-widget analog'
1182
- }, 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 xmlns:xlink=\"http://www.w3.org/1999/xlink\"\r\n viewBox=\"0 0 800 800\"\r\n preserveAspectRatio=\"xMidYMid meet\"\r\n >\r\n <!-- <circle cx=\"400\" cy=\"400\" r=\"400\" fill=\"#fff\"/> -->\r\n <use transform=\"matrix(-1,0,0,1,800,0)\" xlink:href=\"#one-half\" />\r\n <g id=\"one-half\">\r\n <g id=\"one-fourth\">\r\n <path d=\"m400 40v107\" stroke=\"#000\" stroke-width=\"26.7\" />\r\n <g id=\"one-twelfth\">\r\n <path\r\n d=\"m580 88.233-42.5 73.612\"\r\n stroke=\"#000\"\r\n stroke-width=\"26.7\"\r\n />\r\n <g id=\"one-thirtieth\">\r\n <path\r\n id=\"one-sixtieth\"\r\n d=\"m437.63 41.974-3.6585 34.808\"\r\n stroke=\"#000\"\r\n stroke-width=\"13.6\"\r\n />\r\n <use transform=\"rotate(6 400 400)\" xlink:href=\"#one-sixtieth\" />\r\n </g>\r\n <use transform=\"rotate(12 400 400)\" xlink:href=\"#one-thirtieth\" />\r\n </g>\r\n <use transform=\"rotate(30 400 400)\" xlink:href=\"#one-twelfth\" />\r\n <use transform=\"rotate(60 400 400)\" xlink:href=\"#one-twelfth\" />\r\n </g>\r\n <use transform=\"rotate(90 400 400)\" xlink:href=\"#one-fourth\" />\r\n </g>\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 fill=\"#a40000\"\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.clock-widget.analog{container-type:size;container-name:analog-clock}\n"] }]
1183
- }], ctorParameters: () => [] });
1184
-
1185
- // clock-widget.component.ts
1186
- class ClockWidgetComponent {
1187
- static metadata = {
1188
- widgetTypeid: '@default/clock-widget',
1189
- name: 'Clock',
1190
- description: 'Display time in analog or digital format',
1191
- svgIcon,
1192
- };
1193
- #sanitizer = inject(DomSanitizer);
1194
- #dialog = inject(MatDialog);
1195
- safeSvgIcon = this.#sanitizer.bypassSecurityTrustHtml(svgIcon);
1196
- state = signal({
1197
- mode: 'analog',
1198
- hasBackground: true,
1199
- timeFormat: '24h',
1200
- showSeconds: true,
1201
- });
1202
- constructor() {
1203
- // No timer logic needed - DigitalClock manages its own time
1204
- }
1205
- dashboardSetState(state) {
1206
- if (state) {
1207
- this.state.update((current) => ({
1208
- ...current,
1209
- ...state,
1210
- }));
1211
- }
1212
- }
1213
- dashboardGetState() {
1214
- return { ...this.state() };
1215
- }
1216
- dashboardEditState() {
1217
- const dialogRef = this.#dialog.open(ClockStateDialogComponent, {
1218
- data: this.dashboardGetState(),
1219
- width: '400px',
1220
- maxWidth: '90vw',
1221
- disableClose: false,
1222
- autoFocus: false,
1223
- });
1224
- dialogRef
1225
- .afterClosed()
1226
- .subscribe((result) => {
1227
- if (result) {
1228
- this.state.set(result);
1229
- }
1230
- });
1231
- }
1232
- get isAnalog() {
1233
- return this.state().mode === 'analog';
1234
- }
1235
- get isDigital() {
1236
- return this.state().mode === 'digital';
1237
- }
1238
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ClockWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1239
- 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;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: "component", type: DigitalClockComponent, selector: "ngx-digital-clock", inputs: ["timeFormat", "showSeconds", "hasBackground"] }, { kind: "component", type: AnalogClockComponent, selector: "ngx-analog-clock", inputs: ["hasBackground", "showSeconds"] }] });
1240
- }
1241
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.6", ngImport: i0, type: ClockWidgetComponent, decorators: [{
1242
- type: Component,
1243
- 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;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"] }]
1244
- }], ctorParameters: () => [] });
1245
-
1246
- /*
1247
- * Public API Surface of ngx-dashboard-widgets
1248
- */
1249
-
1250
- /**
1251
- * Generated bundle index. Do not edit.
1252
- */
1253
-
1254
- export { ArrowWidgetComponent, ClockWidgetComponent, LabelWidgetComponent, ResponsiveTextDirective };
1255
- //# sourceMappingURL=dragonworks-ngx-dashboard-widgets.mjs.map