@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
package/package.json CHANGED
@@ -1,20 +1,18 @@
1
1
  {
2
2
  "name": "@foxeltech/angular-ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "peerDependencies": {
9
- "tailwindcss": "^3.0.0",
10
- "autoprefixer": "^10.0.0",
11
- "postcss": "^8.0.0",
12
- "@angular/common": "^20.3.0",
13
- "@angular/animations": "^20.3.0",
14
- "@angular/core": "^20.3.0",
15
- "@angular/forms": "^20.3.0",
16
- "@angular/material": "^20.2.7",
17
- "@angular/cdk": "^20.2.7",
9
+ "tailwindcss": "^4.0.0",
10
+ "@angular/common": "^20.3.0 || ^21.0.0",
11
+ "@angular/animations": "^20.3.0 || ^21.0.0",
12
+ "@angular/core": "^20.3.0 || ^21.0.0",
13
+ "@angular/forms": "^20.3.0 || ^21.0.0",
14
+ "@angular/material": "^20.2.7 || ^21.0.0",
15
+ "@angular/cdk": "^20.2.7 || ^21.0.0",
18
16
  "lodash": "^4.17.21",
19
17
  "@ngx-translate/core": "^14.0.0",
20
18
  "@ngx-translate/http-loader": "^7.0.0",
@@ -22,8 +20,8 @@
22
20
  },
23
21
  "dependencies": {
24
22
  "tslib": "^2.3.0",
25
- "@danielmoncada/angular-datetime-picker": "^20.0.0",
26
- "@danielmoncada/angular-datetime-picker-moment-adapter": "^7.0.1",
23
+ "@danielmoncada/angular-datetime-picker": "^21.0.0",
24
+ "@danielmoncada/angular-datetime-picker-moment-adapter": "^8.0.0",
27
25
  "echarts": "^6.0.0",
28
26
  "ngx-echarts": "^20.0.2",
29
27
  "d3": "^7.9.0",
@@ -33,18 +31,19 @@
33
31
  "@microsoft/signalr": "^10.0.0"
34
32
  },
35
33
  "exports": {
36
- "./tailwind-config": "./tailwind.config.js",
37
34
  "./tailwind.css": "./tailwind.css",
38
35
  "./theme.css": "./theme.css",
39
36
  "./components.css": "./components.css",
40
37
  "./dialogs.css": "./dialogs.css",
41
38
  "./fonts.css": "./fonts.css",
42
39
  "./animations.css": "./animations.css",
40
+ "./tokens.css": "./tokens.css",
41
+ "./utilities.css": "./utilities.css",
43
42
  "./package.json": {
44
43
  "default": "./package.json"
45
44
  },
46
45
  ".": {
47
- "types": "./index.d.ts",
46
+ "types": "./types/foxeltech-angular-ui.d.ts",
48
47
  "default": "./fesm2022/foxeltech-angular-ui.mjs"
49
48
  }
50
49
  },
@@ -52,5 +51,6 @@
52
51
  "*.css"
53
52
  ],
54
53
  "module": "fesm2022/foxeltech-angular-ui.mjs",
55
- "typings": "index.d.ts"
54
+ "typings": "types/foxeltech-angular-ui.d.ts",
55
+ "type": "module"
56
56
  }
@@ -296,9 +296,6 @@ input[type='password'] {
296
296
  .txt-card-info-description {
297
297
  @apply text-xs font-light text-text-secondary tracking-normal;
298
298
  }
299
- .txt-tag-common {
300
- @apply font-semibold text-xs;
301
- }
302
299
  .txt-tag-default {
303
300
  @apply txt-tag-common text-text-secondary;
304
301
  }
@@ -330,9 +327,6 @@ input[type='password'] {
330
327
  @apply font-mono text-sm text-text-primary tracking-normal;
331
328
  }
332
329
 
333
- .btn-common {
334
- @apply flex text-sm h-10 font-semibold px-xl py-normal items-center justify-center rounded-lg shadow-sm transition-all duration-200 disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed;
335
- }
336
330
  .btn-default {
337
331
  @apply btn-common bg-button-default hover:opacity-75 text-text-inverse;
338
332
  }
@@ -345,9 +339,6 @@ input[type='password'] {
345
339
  .btn-alternative {
346
340
  @apply btn-common hover:bg-bg-hover text-text-primary border border-border-default;
347
341
  }
348
- .btn-menu-common {
349
- @apply flex flex-row items-center px-semi;
350
- }
351
342
  .btn-menu-top {
352
343
  @apply btn-menu-common border-b border-border-default pt-small pb-semi;
353
344
  }
@@ -359,7 +350,7 @@ input[type='password'] {
359
350
  }
360
351
 
361
352
  .input-default {
362
- @apply block h-[36px] text-text-primary placeholder:text-text-placeholder bg-bg-primary w-full rounded-md border px-semi py-normal border-border-default focus:outline-none focus:border-2 focus:border-gradient-primary-start text-sm text-text-primary tracking-normal ring-0 transition-colors duration-500 ease-in-out;
353
+ @apply block h-[36px] text-text-primary placeholder:text-text-placeholder bg-bg-primary w-full rounded-md border px-semi py-normal border-border-default focus:outline-none focus:border-2 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;
363
354
  }
364
355
  .input-invalid {
365
356
  @apply border-bg-error focus:ring-bg-error focus:border-bg-error;
@@ -368,12 +359,9 @@ input[type='password'] {
368
359
  @apply focus:border-2 focus:border-border-hover2 focus:bg-bg-hover2;
369
360
  }
370
361
  .input-otp {
371
- @apply bg-bg-primary w-full rounded-md border px-semi py-normal border-border-default focus:outline-none focus:border-2 focus:border-gradient-primary-start text-text-primary ring-0 transition-colors duration-500 ease-in-out;
362
+ @apply bg-bg-primary w-full rounded-md border px-semi py-normal border-border-default focus:outline-none focus:border-2 focus:border-gradient-primary-start focus-visible:ring-2 focus-visible:ring-primary/50 text-text-primary ring-0 transition-colors duration-500 ease-in-out;
372
363
  }
373
364
 
374
- .card-common {
375
- @apply text-left px-xl py-large border border-border-default rounded-lg shadow-sm transition-colors duration-500 ease-in-out;
376
- }
377
365
  .card-information {
378
366
  @apply card-common hover:opacity-70 hover:shadow-md hover:border-border-selected;
379
367
  }
@@ -384,9 +372,6 @@ input[type='password'] {
384
372
  @apply flex items-center justify-between rounded-md px-semi py-normal bg-bg-primary mb-normal last:mb-0;
385
373
  }
386
374
 
387
- .tag-common {
388
- @apply flex min-w-7 px-[10px] py-[2px] leading-5 hover:opacity-70 items-center justify-center;
389
- }
390
375
  .tag-default {
391
376
  @apply tag-common bg-bg-hover;
392
377
  }
@@ -1,18 +1,21 @@
1
+ /* fonts.css is NOT imported here: Tailwind v4 inlines imports and rebases
2
+ font url()s against the entry file, which breaks webpack resolution.
3
+ Consumers add '@foxeltech/angular-ui/fonts.css' to their styles array. */
4
+ @import 'tailwindcss';
5
+ @import './tokens.css';
6
+ @import './utilities.css';
1
7
  @import './theme.css';
2
- @import './components.css';
3
- @import './dialogs.css';
4
- @import './fonts.css';
8
+ @import './components.css' layer(components);
9
+ @import './dialogs.css' layer(components);
5
10
  @import './animations.css';
6
11
  @import '@danielmoncada/angular-datetime-picker/assets/style/picker.min.css';
7
- @tailwind base;
8
- @tailwind components;
9
- @tailwind utilities;
10
12
 
11
- html, body {
13
+ html,
14
+ body {
12
15
  font-family: 'Roboto';
13
16
  margin: 0;
14
17
  padding: 0;
15
18
  height: 100%;
16
19
  width: 100%;
17
20
  background: rgb(var(--bg-primary));
18
- }
21
+ }
@@ -0,0 +1,131 @@
1
+ /*
2
+ * Tailwind v4 CSS-first theme for @foxeltech/angular-ui.
3
+ * Replaces the old JS preset (tailwind.config.js / lodash.merge sharing).
4
+ *
5
+ * Colour tokens point at the runtime RGB-triplet variables from theme.css
6
+ * (dark default on :root, light via .light_theme) so theme switching keeps
7
+ * working; opacity modifiers like bg-critical/10 are handled by v4's
8
+ * color-mix() and need no <alpha-value> plumbing.
9
+ */
10
+ /* `inline` is load-bearing: without it the rgb(var(--x)) values are baked
11
+ into :root once, so .light_theme's runtime triplet overrides never reach
12
+ the utilities. inline makes every utility carry rgb(var(--x)) itself and
13
+ resolve at the element that uses it — same behavior as v3. */
14
+ @theme inline {
15
+ /* Colours (names match the v3 preset so every existing class keeps working) */
16
+ --color-gradient-primary-start: rgb(var(--gradient-primary-start));
17
+ --color-gradient-primary-end: rgb(var(--gradient-primary-end));
18
+ --color-gradient-success-start: rgb(var(--gradient-success-start));
19
+ --color-gradient-success-end: rgb(var(--gradient-success-end));
20
+ --color-primary: rgb(var(--primary));
21
+ --color-success: rgb(var(--success));
22
+ --color-danger: rgb(var(--danger));
23
+ --color-warning: rgb(var(--warning));
24
+ --color-critical: rgb(var(--critical));
25
+ --color-discovery: rgb(var(--discovery));
26
+ --color-information: rgb(var(--information));
27
+ --color-bg-primary: rgb(var(--bg-primary));
28
+ --color-bg-canvas: rgb(var(--bg-canvas));
29
+ --color-bg-hover: rgb(var(--bg-primary-hover));
30
+ --color-bg-error: rgb(var(--bg-error-bold));
31
+ --color-bg-hover2: rgb(var(--bg-secondary-hover));
32
+ --color-text-primary: rgb(var(--text-primary));
33
+ --color-text-secondary: rgb(var(--text-secondary));
34
+ --color-text-placeholder: rgb(var(--text-placeholder));
35
+ --color-text-selected: rgb(var(--text-selected-primary));
36
+ --color-text-focus: rgb(var(--text-focus));
37
+ --color-text-inverse: rgb(var(--text-inverse));
38
+ --color-text-error: rgb(var(--text-error));
39
+ --color-text-link: rgb(var(--text-link-primary));
40
+ --color-border-default: rgb(var(--border-default));
41
+ --color-border-interactive: rgb(var(--border-interactive));
42
+ --color-border-selected: rgb(var(--border-selected));
43
+ --color-border-strong: rgb(var(--border-strong));
44
+ --color-border-hover2: rgb(var(--border-secondary-hover));
45
+ --color-button-default: rgb(var(--button-default));
46
+ --color-button-primary: rgb(var(--button-primary));
47
+
48
+ /* Breakpoints (v3 custom screens) */
49
+ --breakpoint-sm: 480px;
50
+ --breakpoint-md: 720px;
51
+ --breakpoint-lg: 1080px;
52
+ --breakpoint-xl: 1920px;
53
+
54
+ /* Shadows — same scale as the v3 preset, incl. the DEFAULT key so the bare
55
+ `shadow` utility keeps its v3 meaning (no shadow→shadow-sm rename sweep). */
56
+ --shadow-sm: 0 1px 2px 0 rgb(var(--shadow-color) / 0.05);
57
+ --shadow: 0 1px 3px 0 rgb(var(--shadow-color) / 0.1), 0 1px 2px -1px rgb(var(--shadow-color) / 0.1);
58
+ --shadow-md: 0 4px 6px -1px rgb(var(--shadow-color) / 0.1), 0 2px 4px -2px rgb(var(--shadow-color) / 0.1);
59
+ --shadow-lg: 0 10px 15px -3px rgb(var(--shadow-color) / 0.1), 0 4px 6px -4px rgb(var(--shadow-color) / 0.1);
60
+ --shadow-xl: 0 20px 25px -5px rgb(var(--shadow-color) / 0.1), 0 10px 10px -5px rgb(var(--shadow-color) / 0.1);
61
+ --shadow-2xl: 0 25px 50px -12px rgb(var(--shadow-color) / 0.25);
62
+ --inset-shadow-sm: inset 0 2px 4px 0 rgb(var(--shadow-color) / 0.05);
63
+
64
+ /* v3 default border radii, incl. DEFAULT so bare `rounded` keeps working */
65
+ --radius-sm: 0.125rem;
66
+ --radius: 0.25rem;
67
+ --radius-md: 0.375rem;
68
+ --radius-lg: 0.5rem;
69
+ --radius-xl: 0.75rem;
70
+ --radius-2xl: 1rem;
71
+ --radius-3xl: 1.5rem;
72
+
73
+ /* Named spacing scale (gap-small, p-semi, mb-large, …) */
74
+ --spacing-xs: 0.125rem;
75
+ --spacing-small: 0.25rem;
76
+ --spacing-normal: 0.5rem;
77
+ --spacing-semi: 0.75rem;
78
+ --spacing-large: 1rem;
79
+ --spacing-xl: 1.25rem;
80
+ --spacing-2xl: 1.5rem;
81
+
82
+ /* Font families */
83
+ --font-spmono: 'SpaceMonoRegular', monospace;
84
+ --font-dmsans: 'DMSans', sans-serif;
85
+ --font-orbitron: 'Orbitron', sans-serif;
86
+ --font-bevn: 'BeVietnamPro', sans-serif;
87
+ --font-roboto: 'Roboto', sans-serif;
88
+
89
+ /* Font-size overrides (v3 preset values) */
90
+ --text-xs: 0.75rem;
91
+ --text-sm: 0.875rem;
92
+ --text-base: 1rem;
93
+ --text-xl: 1.25rem;
94
+ --text-2xl: 1.625rem;
95
+ --text-3xl: 2rem;
96
+ --text-4xl: 2.25rem;
97
+ --text-5xl: 3rem;
98
+ }
99
+
100
+ /* v3 backgroundImage entries — v4 has no background-image theme namespace */
101
+ @utility bg-gradient-primary {
102
+ background-image: linear-gradient(
103
+ 135deg,
104
+ rgb(var(--gradient-primary-start)) 0%,
105
+ rgb(var(--gradient-primary-end)) 100%
106
+ );
107
+ }
108
+ @utility bg-gradient-success {
109
+ background-image: linear-gradient(
110
+ 135deg,
111
+ rgb(var(--gradient-success-start)) 0%,
112
+ rgb(var(--gradient-success-end)) 100%
113
+ );
114
+ }
115
+
116
+ /* v3 behavior compat (per the official upgrade guide) */
117
+ @layer base {
118
+ /* v4 defaults borders to currentColor; restore the theme default */
119
+ *,
120
+ ::after,
121
+ ::before,
122
+ ::backdrop,
123
+ ::file-selector-button {
124
+ border-color: rgb(var(--border-default));
125
+ }
126
+ /* v4 preflight switched buttons to cursor: default */
127
+ button:not(:disabled),
128
+ [role='button']:not(:disabled) {
129
+ cursor: pointer;
130
+ }
131
+ }
@@ -0,0 +1,24 @@
1
+ /*
2
+ * Shared composable utilities (v4 @utility) — kept OUT of components.css
3
+ * because that file is imported inside layer(components) and @utility must
4
+ * be top-level/unlayered. Usable both in markup and via @apply.
5
+ */
6
+ @utility txt-tag-common {
7
+ @apply font-semibold text-xs;
8
+ }
9
+
10
+ @utility btn-common {
11
+ @apply flex text-sm h-10 font-semibold px-xl py-normal items-center justify-center rounded-lg shadow-sm transition-all duration-200 disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed;
12
+ }
13
+
14
+ @utility btn-menu-common {
15
+ @apply flex flex-row items-center px-semi;
16
+ }
17
+
18
+ @utility card-common {
19
+ @apply text-left px-xl py-large border border-border-default rounded-lg shadow-sm transition-colors duration-500 ease-in-out;
20
+ }
21
+
22
+ @utility tag-common {
23
+ @apply flex min-w-7 px-[10px] py-[2px] leading-5 hover:opacity-70 items-center justify-center;
24
+ }
@@ -1,4 +1,7 @@
1
1
  <button
2
+ [attr.type]="type"
3
+ [attr.aria-label]="ariaLabel || null"
4
+ [attr.aria-busy]="loading || null"
2
5
  [class]="class"
3
6
  [ngClass]="{
4
7
  'btn-default': buttonVariant === 'default',
@@ -10,7 +13,7 @@
10
13
  (click)="onClick()"
11
14
  >
12
15
  @if (loading) {
13
- <div class="flex w-full items-center justify-center">
16
+ <div class="flex w-full items-center justify-center" aria-hidden="true">
14
17
  <svg
15
18
  [attr.fill]="
16
19
  buttonVariant !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'
@@ -3,16 +3,17 @@
3
3
  [class.opacity-50]="disabled"
4
4
  [class.cursor-not-allowed]="disabled"
5
5
  >
6
+ <!-- sr-only (not `hidden`): keeps the input focusable and in the a11y tree -->
6
7
  <input
7
8
  type="checkbox"
8
- class="hidden"
9
+ class="peer sr-only"
9
10
  [checked]="value"
10
11
  [disabled]="disabled"
11
12
  (change)="onInputChange($event)"
12
13
  (blur)="onTouched()"
13
14
  />
14
15
  <span
15
- class="relative w-4 h-4 border rounded-full transition-colors duration-150 shrink-0"
16
+ class="relative w-4 h-4 border rounded-full transition-colors duration-150 shrink-0 peer-focus-visible:ring-2 peer-focus-visible:ring-primary/50"
16
17
  [ngClass]="{
17
18
  'rounded-full': rounded,
18
19
  'rounded-sm': !rounded,
@@ -1,13 +1,16 @@
1
1
  <div class="flex flex-col mb-4 w-full text-left">
2
2
  @if (label) {
3
- <label class="txt-field-label">
3
+ <label class="txt-field-label" [attr.for]="fieldId">
4
4
  {{ label }}
5
- <span *ngIf="required" class="txt-required">*</span>
5
+ @if (required) {
6
+ <span class="txt-required" aria-hidden="true">*</span>
7
+ }
6
8
  </label>
7
9
  }
8
10
 
9
11
  <div class="relative">
10
12
  <input
13
+ [id]="fieldId"
11
14
  [owlDateTimeTrigger]="picker"
12
15
  [owlDateTime]="picker"
13
16
  [placeholder]="placeholder"
@@ -18,11 +21,11 @@
18
21
  [value]="value"
19
22
  readonly
20
23
  class="input-default pr-9"
21
- />
24
+ />
22
25
  <span
23
26
  [owlDateTimeTrigger]="picker"
24
27
  class="absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors"
25
- >
28
+ >
26
29
  <fx-ui-hero-icon icon="calendar" [size]="20" class="text-text-primary"></fx-ui-hero-icon>
27
30
  </span>
28
31
  </div>
@@ -32,5 +35,7 @@
32
35
  [pickerType]="type"
33
36
  (afterPickerClosed)="onValueChange($event)"
34
37
  ></owl-date-time>
35
- <div *ngIf="invalid" class="txt-invalid">{{ getErrorMessage(label) }}</div>
38
+ @if (invalid) {
39
+ <div class="txt-invalid">{{ getErrorMessage(label) }}</div>
40
+ }
36
41
  </div>
@@ -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
  // ─── Owl DateTime Picker Custom Styles ───────────────────────────────────────
2
5
 
3
6
  // Container
@@ -3,13 +3,14 @@
3
3
  href="/"
4
4
  class="h-[60px]"
5
5
  [ngClass]="{ 'flex items-center justify-center': !isExpanded, 'p-4': isExpanded }"
6
- >
6
+ >
7
7
  @if (logo) {
8
8
  <img
9
9
  [src]="logo"
10
+ alt="Home"
10
11
  class="h-[20px] w-auto transition-all duration-300 ease-in-out"
11
12
  [ngClass]="{ 'scale-90': !isExpanded, 'ml-5': isExpanded }"
12
- />
13
+ />
13
14
  } @else {
14
15
  <div class="h-[30px] w-auto transition-all duration-300 ease-in-out"></div>
15
16
  }
@@ -17,29 +18,37 @@
17
18
  <div class="drawer-container" [@drawerExpand]="getDrawerState()">
18
19
  <!-- Navigation Items -->
19
20
  <nav class="drawer-nav">
20
- <ng-container *ngFor="let item of items">
21
+ @for (item of items; track item) {
21
22
  <ng-container
22
23
  *ngTemplateOutlet="drawerItem; context: { $implicit: item, level: 0 }"
23
24
  ></ng-container>
24
- </ng-container>
25
+ }
25
26
  </nav>
26
27
 
27
28
  <!-- Version -->
28
- <div *ngIf="version" class="drawer-version" [matTooltip]="!isExpanded ? version : ''" matTooltipPosition="right">
29
- <span *ngIf="isExpanded">{{ version }}</span>
30
- <span *ngIf="!isExpanded" class="drawer-version-dot"></span>
31
- </div>
29
+ @if (version) {
30
+ <div class="drawer-version" [matTooltip]="!isExpanded ? version : ''" matTooltipPosition="right">
31
+ @if (isExpanded) {
32
+ <span>{{ version }}</span>
33
+ }
34
+ @if (!isExpanded) {
35
+ <span class="drawer-version-dot"></span>
36
+ }
37
+ </div>
38
+ }
32
39
  </div>
33
40
 
34
41
  <!-- Floating Toggle Button on right border -->
35
42
  <button
36
43
  type="button"
37
44
  class="toggle-btn"
45
+ [attr.aria-label]="isExpanded ? 'Collapse navigation' : 'Expand navigation'"
46
+ [attr.aria-expanded]="isExpanded"
38
47
  (click)="toggleDrawer()"
39
48
  [matTooltip]="isExpanded ? 'Collapse' : 'Expand'"
40
49
  matTooltipClass="text-text-primary text-xs font-semibold"
41
50
  matTooltipPosition="right"
42
- >
51
+ >
43
52
  <fx-ui-hero-icon
44
53
  [icon]="isExpanded ? 'chevron-left' : 'chevron-right'"
45
54
  [size]="16"
@@ -52,60 +61,68 @@
52
61
  <ng-template #drawerItem let-item let-level="level">
53
62
  <div class="drawer-item-wrapper">
54
63
  <!-- Item Button/Link -->
55
- <a
56
- *ngIf="item.route && !item.children"
57
- [routerLink]="item.route"
58
- class="drawer-item"
59
- [class.active]="isActiveRoute(item.route)"
60
- [class.has-children]="item.children && item.children.length > 0"
61
- [style.paddingLeft.px]="isExpanded ? 16 + level * 16 : 16"
62
- [matTooltip]="!isExpanded ? item.label : ''"
63
- matTooltipPosition="right"
64
- matTooltipClass="text-text-primary text-xs font-semibold"
65
- (click)="onItemClick(item, $event)"
66
- >
67
- <div class="item-content">
68
- <fx-ui-hero-icon [icon]="item.icon" [size]="20" class="item-icon"></fx-ui-hero-icon>
69
- <span *ngIf="isExpanded" class="item-label">{{ item.label }}</span>
70
- </div>
71
- </a>
64
+ @if (item.route && !item.children) {
65
+ <a
66
+ [routerLink]="item.route"
67
+ class="drawer-item"
68
+ [class.active]="isActiveRoute(item.route)"
69
+ [class.has-children]="item.children && item.children.length > 0"
70
+ [style.paddingLeft.px]="isExpanded ? 16 + level * 16 : 16"
71
+ [matTooltip]="!isExpanded ? item.label : ''"
72
+ matTooltipPosition="right"
73
+ matTooltipClass="text-text-primary text-xs font-semibold"
74
+ (click)="onItemClick(item, $event)"
75
+ >
76
+ <div class="item-content">
77
+ <fx-ui-hero-icon [icon]="item.icon" [size]="20" class="item-icon"></fx-ui-hero-icon>
78
+ @if (isExpanded) {
79
+ <span class="item-label">{{ item.label }}</span>
80
+ }
81
+ </div>
82
+ </a>
83
+ }
72
84
 
73
- <button
74
- *ngIf="!item.route || item.children"
75
- type="button"
76
- class="drawer-item"
77
- [class.active]="isActiveRoute(item.route)"
78
- [class.has-children]="item.children && item.children.length > 0"
79
- [style.paddingLeft.px]="isExpanded ? 16 + level * 16 : 16"
80
- [matTooltip]="!isExpanded ? item.label : ''"
81
- matTooltipPosition="right"
82
- matTooltipClass="text-text-primary text-sm font-semibold"
83
- (click)="onItemClick(item, $event)"
84
- >
85
- <div class="item-content">
86
- <fx-ui-hero-icon [icon]="item.icon" [size]="20" class="item-icon"></fx-ui-hero-icon>
87
- <span *ngIf="isExpanded" class="item-label">{{ item.label }}</span>
88
- <fx-ui-hero-icon
89
- *ngIf="isExpanded && item.children && item.children.length > 0"
90
- [icon]="'chevron-down'"
91
- [size]="16"
92
- class="chevron-icon"
93
- [class.rotated]="isAccordionExpanded(item.id)"
94
- ></fx-ui-hero-icon>
95
- </div>
96
- </button>
85
+ @if (!item.route || item.children) {
86
+ <button
87
+ type="button"
88
+ class="drawer-item"
89
+ [class.active]="isActiveRoute(item.route)"
90
+ [class.has-children]="item.children && item.children.length > 0"
91
+ [style.paddingLeft.px]="isExpanded ? 16 + level * 16 : 16"
92
+ [matTooltip]="!isExpanded ? item.label : ''"
93
+ matTooltipPosition="right"
94
+ matTooltipClass="text-text-primary text-sm font-semibold"
95
+ (click)="onItemClick(item, $event)"
96
+ >
97
+ <div class="item-content">
98
+ <fx-ui-hero-icon [icon]="item.icon" [size]="20" class="item-icon"></fx-ui-hero-icon>
99
+ @if (isExpanded) {
100
+ <span class="item-label">{{ item.label }}</span>
101
+ }
102
+ @if (isExpanded && item.children && item.children.length > 0) {
103
+ <fx-ui-hero-icon
104
+ [icon]="'chevron-down'"
105
+ [size]="16"
106
+ class="chevron-icon"
107
+ [class.rotated]="isAccordionExpanded(item.id)"
108
+ ></fx-ui-hero-icon>
109
+ }
110
+ </div>
111
+ </button>
112
+ }
97
113
 
98
114
  <!-- Accordion Children -->
99
- <div
100
- *ngIf="item.children && item.children.length > 0"
101
- class="accordion-children"
102
- [@accordionExpand]="getAccordionState(item.id)"
103
- >
104
- <ng-container *ngFor="let child of item.children">
105
- <ng-container
106
- *ngTemplateOutlet="drawerItem; context: { $implicit: child, level: level + 1 }"
107
- ></ng-container>
108
- </ng-container>
109
- </div>
115
+ @if (item.children && item.children.length > 0) {
116
+ <div
117
+ class="accordion-children"
118
+ [@accordionExpand]="getAccordionState(item.id)"
119
+ >
120
+ @for (child of item.children; track child) {
121
+ <ng-container
122
+ *ngTemplateOutlet="drawerItem; context: { $implicit: child, level: level + 1 }"
123
+ ></ng-container>
124
+ }
125
+ </div>
126
+ }
110
127
  </div>
111
128
  </ng-template>
@@ -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
  @apply block h-full;
3
6
  }
@@ -1,9 +1,9 @@
1
1
  <div class="mb-4 text-left w-full text-text-primary" [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,6 +22,7 @@
22
22
  <input
23
23
  appTrimOnBlur
24
24
  [appTrimOnBlurCollapse]="true"
25
+ [id]="fieldId"
25
26
  [type]="displayType"
26
27
  [placeholder]="placeholder"
27
28
  [disabled]="disabled"
@@ -30,6 +31,9 @@
30
31
  (blur)="onBlur()"
31
32
  (focus)="onFocus()"
32
33
  [attr.maxlength]="maxlength"
34
+ [attr.aria-invalid]="invalid || null"
35
+ [attr.aria-describedby]="invalid ? fieldId + '-error' : null"
36
+ [attr.aria-required]="required ? true : null"
33
37
  [class]="type === 'otp' ? 'input-otp' : 'input-default'"
34
38
  [class.input-invalid]="invalid"
35
39
  [class.pr-8]="type === 'password' || suffixIcon"
@@ -41,18 +45,26 @@
41
45
  class="absolute inset-y-0 right-2 flex items-center text-text-primary hover:text-text-secondary"
42
46
  (click)="onSuffixClick()"
43
47
  [disabled]="disabled"
48
+ [attr.aria-label]="
49
+ type === 'password'
50
+ ? (showPassword ? 'Hide password' : 'Show password')
51
+ : (suffixAriaLabel || null)
52
+ "
53
+ [attr.aria-pressed]="type === 'password' ? showPassword : null"
44
54
  >
45
55
  @if (type === 'password') {
46
56
  <fx-ui-hero-icon
47
57
  [icon]="showPassword ? 'eye-slash' : 'eye'"
48
58
  [size]="18"
49
59
  class="text-text-primary"
60
+ aria-hidden="true"
50
61
  ></fx-ui-hero-icon>
51
62
  } @else if (suffixIcon) {
52
63
  <fx-ui-hero-icon
53
64
  [icon]="suffixIcon"
54
65
  [size]="18"
55
66
  class="text-text-primary"
67
+ aria-hidden="true"
56
68
  ></fx-ui-hero-icon>
57
69
  }
58
70
  </button>
@@ -60,7 +72,7 @@
60
72
  </div>
61
73
 
62
74
  @if (invalid) {
63
- <div class="txt-invalid">
75
+ <div class="txt-invalid" role="alert" [id]="fieldId + '-error'">
64
76
  {{ getErrorMessage(label) }}
65
77
  </div>
66
78
  }