@foxeltech/angular-ui 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/components.css +2 -17
  2. package/fesm2022/foxeltech-angular-ui.mjs +509 -243
  3. package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
  4. package/package.json +15 -15
  5. package/src/lib/styles/components.css +2 -17
  6. package/src/lib/styles/tailwind.css +11 -8
  7. package/src/lib/styles/tokens.css +131 -0
  8. package/src/lib/styles/utilities.css +24 -0
  9. package/src/lib/ui/components/button/button.component.html +4 -1
  10. package/src/lib/ui/components/checkbox/checkbox.component.html +3 -2
  11. package/src/lib/ui/components/datetime-picker/datetime-picker.component.html +10 -5
  12. package/src/lib/ui/components/datetime-picker/datetime-picker.component.scss +3 -0
  13. package/src/lib/ui/components/drawer/drawer.component.html +78 -61
  14. package/src/lib/ui/components/drawer/drawer.component.scss +3 -0
  15. package/src/lib/ui/components/input/input.component.html +15 -3
  16. package/src/lib/ui/components/kanban-board/kanban-board.component.scss +3 -0
  17. package/src/lib/ui/components/loading-panel/loading-panel.component.html +53 -53
  18. package/src/lib/ui/components/master-detail/master-detail.component.css +11 -0
  19. package/src/lib/ui/components/master-detail/master-detail.component.html +70 -0
  20. package/src/lib/ui/components/meter/meter.component.html +8 -1
  21. package/src/lib/ui/components/radio-button/radio-button.component.html +33 -27
  22. package/src/lib/ui/components/radio-button-toggle/radio-button-toggle.component.html +8 -3
  23. package/src/lib/ui/components/rich-text-area/rich-text-area.component.scss +3 -0
  24. package/src/lib/ui/components/search-bar/search-bar.component.html +11 -10
  25. package/src/lib/ui/components/select/select.component.html +17 -10
  26. package/src/lib/ui/components/severity-badges/severity-badges.component.html +5 -4
  27. package/src/lib/ui/components/skeleton-list/skeleton-list.component.html +2 -2
  28. package/src/lib/ui/components/slider/slider.component.html +20 -15
  29. package/src/lib/ui/components/switch/switch.component.html +11 -6
  30. package/src/lib/ui/components/tab-group/tab-group.component.html +44 -27
  31. package/src/lib/ui/components/tab-group/tab-group.component.scss +3 -0
  32. package/src/lib/ui/components/tag/tag.component.html +6 -1
  33. package/src/lib/ui/components/toast/toast.component.html +38 -35
  34. package/src/lib/ui/components/tree-diagram/tree-diagram.component.html +21 -19
  35. package/src/lib/ui/dialogs/confirmation/confirmation.component.html +1 -1
  36. package/tailwind.css +11 -8
  37. package/tokens.css +131 -0
  38. package/{index.d.ts → types/foxeltech-angular-ui.d.ts} +123 -34
  39. package/utilities.css +24 -0
  40. package/tailwind-config.d.ts +0 -4
  41. package/tailwind.config.js +0 -113
@@ -1,3 +1,6 @@
1
+ /* Tailwind v4: component styles compile in isolation — @reference pulls in the theme for @apply without re-emitting it. */
2
+ @reference '../../../styles/tailwind.css';
3
+
1
4
  .kanban-board {
2
5
  @apply w-full h-full overflow-x-auto;
3
6
  }
@@ -1,57 +1,57 @@
1
1
  <!-- Full-screen glass overlay -->
2
- <div
3
- *ngIf="show"
4
- class="fixed inset-0 z-50 flex items-center justify-center"
5
- aria-hidden="false"
6
- role="alert"
7
- aria-busy="true"
8
- >
9
- <div class="absolute inset-0 bg-bg-primary/30 backdrop-blur-xl" aria-hidden="true"></div>
10
-
2
+ @if (show) {
11
3
  <div
12
- class="relative z-10 flex flex-col items-center gap-4 p-6 rounded-2xl shadow-xl bg-bg-primary/70 border border-border-strong backdrop-bg-primary/70"
13
- style="min-width: 220px; max-width: 90%"
14
- >
15
- <svg fill="url(#spinner-gradient)" width="50" height="50" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
16
- <defs>
17
- <linearGradient id="spinner-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
18
- <stop offset="0%" style="stop-color: rgb(var(--gradient-primary-start)); stop-opacity: 1" />
19
- <stop offset="100%" style="stop-color: rgb(var(--gradient-primary-end)); stop-opacity: 1" />
20
- </linearGradient>
21
- </defs>
22
- <circle cx="4" cy="12" r="0">
23
- <animate begin="0;spinner_z0Or.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3" fill="freeze"/>
24
- <animate begin="spinner_OLMs.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12" fill="freeze"/>
25
- <animate begin="spinner_UHR2.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20" fill="freeze"/>
26
- <animate id="spinner_lo66" begin="spinner_Aguh.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0" fill="freeze"/>
27
- <animate id="spinner_z0Or" begin="spinner_lo66.end" attributeName="cx" dur="0.001s" values="20;4" fill="freeze"/>
28
- </circle>
29
- <circle cx="4" cy="12" r="3">
30
- <animate begin="0;spinner_z0Or.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12" fill="freeze"/>
31
- <animate begin="spinner_OLMs.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20" fill="freeze"/>
32
- <animate id="spinner_JsnR" begin="spinner_UHR2.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0" fill="freeze"/>
33
- <animate id="spinner_Aguh" begin="spinner_JsnR.end" attributeName="cx" dur="0.001s" values="20;4" fill="freeze"/>
34
- <animate begin="spinner_Aguh.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3" fill="freeze"/>
35
- </circle>
36
- <circle cx="12" cy="12" r="3">
37
- <animate begin="0;spinner_z0Or.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20" fill="freeze"/>
38
- <animate id="spinner_hSjk" begin="spinner_OLMs.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0" fill="freeze"/>
39
- <animate id="spinner_UHR2" begin="spinner_hSjk.end" attributeName="cx" dur="0.001s" values="20;4" fill="freeze"/>
40
- <animate begin="spinner_UHR2.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3" fill="freeze"/>
41
- <animate begin="spinner_Aguh.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12" fill="freeze"/>
42
- </circle>
43
- <circle cx="20" cy="12" r="3">
44
- <animate id="spinner_4v5M" begin="0;spinner_z0Or.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0" fill="freeze"/>
45
- <animate id="spinner_OLMs" begin="spinner_4v5M.end" attributeName="cx" dur="0.001s" values="20;4" fill="freeze"/>
46
- <animate begin="spinner_OLMs.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3" fill="freeze"/>
47
- <animate begin="spinner_UHR2.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12" fill="freeze"/>
48
- <animate begin="spinner_Aguh.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20" fill="freeze"/>
49
- </circle>
50
- </svg>
51
-
52
-
53
- <div *ngIf="message" class="text-sm text-text-primary text-center">
54
- {{ message }}
4
+ class="fixed inset-0 z-50 flex items-center justify-center"
5
+ aria-hidden="false"
6
+ role="alert"
7
+ aria-busy="true"
8
+ >
9
+ <div class="absolute inset-0 bg-bg-primary/30 backdrop-blur-xl" aria-hidden="true"></div>
10
+ <div
11
+ class="relative z-10 flex flex-col items-center gap-4 p-6 rounded-2xl shadow-xl bg-bg-primary/70 border border-border-strong backdrop-bg-primary/70"
12
+ style="min-width: 220px; max-width: 90%"
13
+ >
14
+ <svg fill="url(#spinner-gradient)" width="50" height="50" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
15
+ <defs>
16
+ <linearGradient id="spinner-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
17
+ <stop offset="0%" style="stop-color: rgb(var(--gradient-primary-start)); stop-opacity: 1" />
18
+ <stop offset="100%" style="stop-color: rgb(var(--gradient-primary-end)); stop-opacity: 1" />
19
+ </linearGradient>
20
+ </defs>
21
+ <circle cx="4" cy="12" r="0">
22
+ <animate begin="0;spinner_z0Or.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3" fill="freeze"/>
23
+ <animate begin="spinner_OLMs.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12" fill="freeze"/>
24
+ <animate begin="spinner_UHR2.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20" fill="freeze"/>
25
+ <animate id="spinner_lo66" begin="spinner_Aguh.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0" fill="freeze"/>
26
+ <animate id="spinner_z0Or" begin="spinner_lo66.end" attributeName="cx" dur="0.001s" values="20;4" fill="freeze"/>
27
+ </circle>
28
+ <circle cx="4" cy="12" r="3">
29
+ <animate begin="0;spinner_z0Or.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12" fill="freeze"/>
30
+ <animate begin="spinner_OLMs.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20" fill="freeze"/>
31
+ <animate id="spinner_JsnR" begin="spinner_UHR2.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0" fill="freeze"/>
32
+ <animate id="spinner_Aguh" begin="spinner_JsnR.end" attributeName="cx" dur="0.001s" values="20;4" fill="freeze"/>
33
+ <animate begin="spinner_Aguh.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3" fill="freeze"/>
34
+ </circle>
35
+ <circle cx="12" cy="12" r="3">
36
+ <animate begin="0;spinner_z0Or.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20" fill="freeze"/>
37
+ <animate id="spinner_hSjk" begin="spinner_OLMs.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0" fill="freeze"/>
38
+ <animate id="spinner_UHR2" begin="spinner_hSjk.end" attributeName="cx" dur="0.001s" values="20;4" fill="freeze"/>
39
+ <animate begin="spinner_UHR2.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3" fill="freeze"/>
40
+ <animate begin="spinner_Aguh.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12" fill="freeze"/>
41
+ </circle>
42
+ <circle cx="20" cy="12" r="3">
43
+ <animate id="spinner_4v5M" begin="0;spinner_z0Or.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="3;0" fill="freeze"/>
44
+ <animate id="spinner_OLMs" begin="spinner_4v5M.end" attributeName="cx" dur="0.001s" values="20;4" fill="freeze"/>
45
+ <animate begin="spinner_OLMs.end" attributeName="r" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="0;3" fill="freeze"/>
46
+ <animate begin="spinner_UHR2.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="4;12" fill="freeze"/>
47
+ <animate begin="spinner_Aguh.end" attributeName="cx" calcMode="spline" dur="0.5s" keySplines=".36,.6,.31,1" values="12;20" fill="freeze"/>
48
+ </circle>
49
+ </svg>
50
+ @if (message) {
51
+ <div class="text-sm text-text-primary text-center">
52
+ {{ message }}
53
+ </div>
54
+ }
55
55
  </div>
56
56
  </div>
57
- </div>
57
+ }
@@ -0,0 +1,11 @@
1
+ /* Full-width when stacked; fixed width beside the detail pane on lg+ (1080px). */
2
+ .fx-md-list {
3
+ width: 100%;
4
+ }
5
+
6
+ @media (min-width: 1080px) {
7
+ .fx-md-list {
8
+ width: var(--fx-md-list-width, 420px);
9
+ flex-shrink: 0;
10
+ }
11
+ }
@@ -0,0 +1,70 @@
1
+ <div class="flex flex-col lg:flex-row items-start gap-xl" [style.--fx-md-list-width]="listWidth">
2
+ <!-- Master: list pane -->
3
+ <div class="fx-md-list card-common">
4
+ <ng-content select="[mdHeader]"></ng-content>
5
+ <ng-content select="[mdFilters]"></ng-content>
6
+ <div
7
+ class="flex flex-col gap-normal overflow-auto pr-xs"
8
+ [style.maxHeight]="maxListHeight"
9
+ role="listbox"
10
+ [attr.aria-label]="listLabel || null"
11
+ >
12
+ @if (table.loading) {
13
+ <fx-ui-skeleton-list [rows]="table.pageSize"></fx-ui-skeleton-list>
14
+ } @else {
15
+ @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {
16
+ <button
17
+ type="button"
18
+ role="option"
19
+ [id]="optionId(i)"
20
+ [attr.aria-selected]="isSelected(row)"
21
+ [tabindex]="isSelected(row) ? 0 : -1"
22
+ (click)="table.selectItem(row)"
23
+ (keydown)="onKeydown($event, i)"
24
+ class="text-left rounded-lg border p-semi transition-colors"
25
+ [ngClass]="
26
+ isSelected(row)
27
+ ? 'border-gradient-primary-start bg-gradient-primary-start/5'
28
+ : 'border-border-default hover:border-border-interactive bg-bg-primary'
29
+ "
30
+ >
31
+ <ng-container
32
+ [ngTemplateOutlet]="rowTemplate"
33
+ [ngTemplateOutletContext]="{ $implicit: row, selected: isSelected(row) }"
34
+ ></ng-container>
35
+ </button>
36
+ }
37
+ @if (!table.dataSource.data.length) {
38
+ <fx-ui-empty-state [message]="emptyMessage"></fx-ui-empty-state>
39
+ }
40
+ }
41
+ </div>
42
+ <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,
43
+ whose own BaseTable subscription (running after this child's hook)
44
+ wires page events — a binding here would double-fetch every change. -->
45
+ <mat-paginator
46
+ class="mt-normal"
47
+ [length]="table.total"
48
+ [pageSize]="table.pageSize"
49
+ [pageSizeOptions]="pageSizeOptions"
50
+ ></mat-paginator>
51
+ </div>
52
+
53
+ <!-- Detail pane -->
54
+ <div class="w-full lg:flex-1 min-w-0">
55
+ @if (table.selected) {
56
+ @if (table.detailBusy) {
57
+ <fx-ui-skeleton-detail [cards]="skeletonCards"></fx-ui-skeleton-detail>
58
+ } @else {
59
+ <ng-container
60
+ [ngTemplateOutlet]="detailTemplate"
61
+ [ngTemplateOutletContext]="{ $implicit: table.detail, selected: table.selected }"
62
+ ></ng-container>
63
+ }
64
+ } @else {
65
+ <div class="card-common flex items-center justify-center h-[300px]">
66
+ <fx-ui-empty-state [message]="emptyMessage"></fx-ui-empty-state>
67
+ </div>
68
+ }
69
+ </div>
70
+ </div>
@@ -9,7 +9,14 @@
9
9
  }
10
10
  </div>
11
11
  }
12
- <div class="h-2 rounded-full bg-bg-hover overflow-hidden">
12
+ <div
13
+ class="h-2 rounded-full bg-bg-hover overflow-hidden"
14
+ role="progressbar"
15
+ [attr.aria-valuemin]="0"
16
+ [attr.aria-valuemax]="max"
17
+ [attr.aria-valuenow]="value"
18
+ [attr.aria-label]="label || null"
19
+ >
13
20
  <div class="h-full rounded-full" [ngClass]="fillClass" [style.width.%]="percent"></div>
14
21
  </div>
15
22
  </div>
@@ -1,9 +1,15 @@
1
- <div class="w-full rounded-lg overflow-hidden" [class]="class" role="radiogroup">
1
+ <div
2
+ class="w-full rounded-lg overflow-hidden"
3
+ [class]="class"
4
+ role="radiogroup"
5
+ [attr.aria-labelledby]="label ? groupId + '-label' : null"
6
+ >
7
+ <!-- A radiogroup has no single control a <label> could point at — name it via aria-labelledby. -->
2
8
  @if (label) {
3
- <label class="txt-field-label">
9
+ <span class="txt-field-label block" [id]="groupId + '-label'">
4
10
  {{ label }}
5
11
  @if (required) {
6
- <span class="txt-required">*</span>
12
+ <span class="txt-required" aria-hidden="true">*</span>
7
13
  }
8
14
  @if (tooltip) {
9
15
  <fx-ui-hero-icon
@@ -15,36 +21,36 @@
15
21
  matTooltipClass="text-text-primary text-xs font-semibold"
16
22
  ></fx-ui-hero-icon>
17
23
  }
18
- </label>
24
+ </span>
19
25
  }
20
26
 
21
- <button
22
- *ngFor="let opt of options"
23
- type="button"
24
- role="radio"
25
- [attr.aria-checked]="value === opt.value"
26
- (click)="onSelectValue(opt.value)"
27
- [disabled]="disabled"
28
- [class.text-gray-900]="value !== opt.value"
29
- class="flex-1 px-normal w-full flex items-center gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed"
30
- [matTooltip]="opt.tooltip || ''"
31
- matTooltipPosition="right"
32
- matTooltipClass="text-text-primary text-xs font-semibold"
33
- >
34
- <span
27
+ @for (opt of options; track opt) {
28
+ <button
29
+ type="button"
30
+ role="radio"
31
+ [attr.aria-checked]="value === opt.value"
32
+ (click)="onSelectValue(opt.value)"
33
+ [disabled]="disabled"
34
+ [class.text-gray-900]="value !== opt.value"
35
+ class="flex-1 px-normal w-full flex items-center gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed"
36
+ [matTooltip]="opt.tooltip || ''"
37
+ matTooltipPosition="right"
38
+ matTooltipClass="text-text-primary text-xs font-semibold"
39
+ >
40
+ <span
35
41
  [ngClass]="{
36
42
  'radio-checked': value === opt.value,
37
43
  'radio-uncheck': value !== opt.value,
38
44
  }"
39
- class="radio-container"
40
- >
41
- @if (value === opt.value) {
42
- <span class="radio-dot"></span>
43
- }
44
- </span>
45
-
46
- <span class="txt-default mb-0">{{ opt.label }}</span>
47
- </button>
45
+ class="radio-container"
46
+ >
47
+ @if (value === opt.value) {
48
+ <span class="radio-dot"></span>
49
+ }
50
+ </span>
51
+ <span class="txt-default mb-0">{{ opt.label }}</span>
52
+ </button>
53
+ }
48
54
  @if (invalid) {
49
55
  <div class="txt-invalid">
50
56
  {{ getErrorMessage(label) }}
@@ -1,10 +1,15 @@
1
1
  <div class="flex flex-col text-left mb-4 w-full" [class]="class">
2
- @if(label){<label class="txt-field-label">
2
+ <!-- A radiogroup has no single control a <label> could point at — name it via aria-labelledby. -->
3
+ @if(label){<span class="txt-field-label" [id]="groupId + '-label'">
3
4
  {{ label }}
4
- @if(required) {<span class="txt-required">*</span>} </label
5
+ @if(required) {<span class="txt-required" aria-hidden="true">*</span>} </span
5
6
  >}
6
7
 
7
- <div class="inline-flex w-full rounded-lg overflow-hidden" role="radiogroup">
8
+ <div
9
+ class="inline-flex w-full rounded-lg overflow-hidden"
10
+ role="radiogroup"
11
+ [attr.aria-labelledby]="label ? groupId + '-label' : null"
12
+ >
8
13
  @for(opt of options; track opt){<button
9
14
  type="button"
10
15
  role="radio"
@@ -1,3 +1,6 @@
1
+ /* Tailwind v4: component styles compile in isolation — @reference pulls in the theme for @apply without re-emitting it. */
2
+ @reference '../../../styles/tailwind.css';
3
+
1
4
  :host {
2
5
  display: block;
3
6
  }
@@ -8,15 +8,16 @@
8
8
  [value]="value"
9
9
  (input)="onValueChange($any($event.target).value)"
10
10
  (keydown.enter)="onEnterKey()"
11
- />
11
+ />
12
12
 
13
- <button
14
- *ngIf="value && !disabled"
15
- type="button"
16
- class="absolute inset-y-0 right-2 flex items-center text-text-primary hover:text-text-secondary"
17
- (click)="clear()"
18
- aria-label="Clear"
19
- >
20
- <i class="material-symbols-outlined text-lg select-none">close</i>
21
- </button>
13
+ @if (value && !disabled) {
14
+ <button
15
+ type="button"
16
+ class="absolute inset-y-0 right-2 flex items-center text-text-primary hover:text-text-secondary"
17
+ (click)="clear()"
18
+ aria-label="Clear"
19
+ >
20
+ <i class="material-symbols-outlined text-lg select-none">close</i>
21
+ </button>
22
+ }
22
23
  </div>
@@ -1,9 +1,9 @@
1
1
  <div class="mb-4 text-left w-full" [class]="class">
2
2
  @if (label) {
3
- <label class="block txt-field-label">
3
+ <label class="block txt-field-label" [attr.for]="fieldId">
4
4
  {{ label }}
5
5
  @if (required) {
6
- <span class="txt-required">*</span>
6
+ <span class="txt-required" aria-hidden="true">*</span>
7
7
  }
8
8
  @if (tooltip) {
9
9
  <fx-ui-hero-icon
@@ -22,8 +22,12 @@
22
22
  [attr.invalid]="invalid || null"
23
23
  class="fx-select-field w-full shadow-sm"
24
24
  appearance="fill"
25
- >
25
+ >
26
26
  <mat-select
27
+ [id]="fieldId"
28
+ [required]="!!required"
29
+ [attr.aria-invalid]="invalid || null"
30
+ [attr.aria-describedby]="invalid ? fieldId + '-error' : null"
27
31
  [placeholder]="placeholder"
28
32
  [disabled]="disabled"
29
33
  [multiple]="multiple"
@@ -31,27 +35,30 @@
31
35
  (selectionChange)="onSelectionChange($event.value)"
32
36
  (openedChange)="onOpened($event)"
33
37
  (blur)="onTouched()"
34
- >
38
+ >
35
39
  @if (enableSearch) {
36
40
  <div class="px-semi w-full">
37
41
  <input
38
42
  type="text"
43
+ aria-label="Search"
39
44
  [placeholder]="'Search'"
40
45
  [(ngModel)]="searchTerm"
41
46
  (ngModelChange)="onSearchChange($event)"
42
47
  (click)="$event.stopPropagation()"
43
- class="text-text-primary placeholder:text-text-placeholder w-full bg-bg-primary border-b px-semi py-normal border-border-default focus:outline-none focus:border-gradient-primary-start text-sm text-text-primary tracking-normal ring-0 transition-colors duration-500 ease-in-out"
44
- />
48
+ class="text-text-primary placeholder:text-text-placeholder w-full bg-bg-primary border-b px-semi py-normal border-border-default focus:outline-none focus:border-gradient-primary-start focus-visible:ring-2 focus-visible:ring-primary/50 text-sm text-text-primary tracking-normal ring-0 transition-colors duration-500 ease-in-out"
49
+ />
45
50
  </div>
46
51
  }
47
- <mat-option *ngFor="let option of filteredOptions" [value]="option.value">
48
- <span class="txt-default">{{ option.label }}</span>
49
- </mat-option>
52
+ @for (option of filteredOptions; track option) {
53
+ <mat-option [value]="option.value">
54
+ <span class="txt-default">{{ option.label }}</span>
55
+ </mat-option>
56
+ }
50
57
  </mat-select>
51
58
  </mat-form-field>
52
59
 
53
60
  @if (invalid) {
54
- <div class="txt-invalid text-left">
61
+ <div class="txt-invalid text-left" role="alert" [id]="fieldId + '-error'">
55
62
  {{ getErrorMessage(label) }}
56
63
  </div>
57
64
  }
@@ -1,10 +1,11 @@
1
1
  <div class="flex items-center gap-small">
2
2
  @for (badge of badges; track badge.key) {
3
- <span
4
- class="px-2 py-0.5 rounded-full txt-utility-caption font-medium cursor-pointer"
3
+ <button
4
+ type="button"
5
+ class="px-2 py-0.5 rounded-full txt-utility-caption font-medium cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50"
5
6
  [ngClass]="badgeClass(badge)"
7
+ [attr.aria-label]="badge.key + ': ' + badge.label"
6
8
  (click)="badgeClick.emit(badge.key)"
7
- >{{ badge.label }}</span
8
- >
9
+ >{{ badge.label }}</button>
9
10
  }
10
11
  </div>
@@ -1,8 +1,8 @@
1
1
  <div class="flex flex-col gap-normal animate-pulse">
2
2
  @for (r of range(rows); track r) {
3
- <div class="rounded-normal border border-border-default p-semi">
3
+ <div class="rounded-lg border border-border-default p-semi">
4
4
  <div class="flex items-center gap-normal mb-small">
5
- <div class="w-8 h-8 rounded-normal bg-bg-hover shrink-0"></div>
5
+ <div class="w-8 h-8 rounded-lg bg-bg-hover shrink-0"></div>
6
6
  <div class="h-4 flex-1 rounded bg-bg-hover"></div>
7
7
  <div class="h-5 w-12 rounded-full bg-bg-hover shrink-0"></div>
8
8
  </div>
@@ -3,34 +3,38 @@
3
3
  [class.cursor-not-allowed]="disabled"
4
4
  [class.opacity-60]="disabled"
5
5
  role="group"
6
- >
6
+ >
7
+ <!-- Track click is a pointer shortcut; the thumb button below owns keyboard/focus. -->
8
+ <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->
7
9
  <div
8
10
  #track
9
11
  class="relative touch-none py-4"
10
12
  (click)="onTrackClick($event)"
11
13
  [attr.aria-disabled]="disabled"
12
14
  [attr.aria-orientation]="vertical ? 'vertical' : 'horizontal'"
13
- >
15
+ >
14
16
  <div
15
17
  class="relative flex items-center"
16
18
  [ngClass]="{ 'flex-col h-48': vertical }"
17
- >
19
+ >
18
20
  <div
19
21
  class="relative flex-1 rounded-full bg-gray-600 h-2"
20
22
  [ngClass]="{ 'w-1 rotate-0 h-full w-auto bg-border-gray-700': vertical }"
21
- >
22
- <div
23
- class="absolute left-0 top-0 h-full rounded-full bg-primary"
24
- *ngIf="!vertical"
25
- [style.width.%]="percent"
26
- ></div>
23
+ >
24
+ @if (!vertical) {
25
+ <div
26
+ class="absolute left-0 top-0 h-full rounded-full bg-primary"
27
+ [style.width.%]="percent"
28
+ ></div>
29
+ }
27
30
 
28
- <div
29
- class="absolute bottom-0 left-0 w-full rounded-full bg-primary"
30
- *ngIf="vertical"
31
- [style.height.%]="percent"
32
- style="transform-origin: bottom;"
33
- ></div>
31
+ @if (vertical) {
32
+ <div
33
+ class="absolute bottom-0 left-0 w-full rounded-full bg-primary"
34
+ [style.height.%]="percent"
35
+ style="transform-origin: bottom;"
36
+ ></div>
37
+ }
34
38
 
35
39
  <button
36
40
  #thumb
@@ -38,6 +42,7 @@
38
42
  class="absolute -mt-1 -ml-1.5 w-4 h-4 rounded-full shadow focus:ring-primary bg-primary border-2 border-text-primary"
39
43
  [ngStyle]="thumbStyle()"
40
44
  role="slider"
45
+ aria-label="Slider"
41
46
  [attr.aria-valuemin]="min"
42
47
  [attr.aria-valuemax]="max"
43
48
  [attr.aria-valuenow]="value"
@@ -1,5 +1,10 @@
1
- <div
2
- class="relative flex items-center w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer"
1
+ <button
2
+ type="button"
3
+ role="switch"
4
+ [attr.aria-checked]="!!value"
5
+ [attr.aria-label]="ariaLabel || null"
6
+ [disabled]="disabled"
7
+ class="relative flex items-center w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-primary/50"
3
8
  [ngClass]="{
4
9
  'bg-success': value && !disabled,
5
10
  'bg-button-default': !value && !disabled,
@@ -8,14 +13,14 @@
8
13
  (click)="toggle()"
9
14
  >
10
15
  @if(value) {
11
- <i style="font-size: 20px;" class="material-symbols-outlined absolute left-0.5 top-0 text-text-inverse">check</i>
12
-
16
+ <i style="font-size: 20px;" class="material-symbols-outlined absolute left-0.5 top-0 text-text-inverse" aria-hidden="true">check</i>
17
+
13
18
  }
14
19
  @if(!value) {
15
- <i style="font-size: 20px;" class="material-symbols-outlined absolute right-0.5 top-0 text-text-inverse">close</i>
20
+ <i style="font-size: 20px;" class="material-symbols-outlined absolute right-0.5 top-0 text-text-inverse" aria-hidden="true">close</i>
16
21
  }
17
22
  <div
18
23
  class="absolute top-0.5 left-0.5 w-4 h-4 bg-text-inverse rounded-full shadow-md transform transition-transform duration-300"
19
24
  [ngClass]="{ 'translate-x-5': value }"
20
25
  ></div>
21
- </div>
26
+ </button>