@foxeltech/angular-ui 0.7.108 → 0.7.131

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 (31) hide show
  1. package/components.css +24 -5
  2. package/fesm2022/foxeltech-angular-ui-chart.mjs +25 -1
  3. package/fesm2022/foxeltech-angular-ui-chart.mjs.map +1 -1
  4. package/fesm2022/foxeltech-angular-ui-flow.mjs +42 -11
  5. package/fesm2022/foxeltech-angular-ui-flow.mjs.map +1 -1
  6. package/fesm2022/foxeltech-angular-ui.mjs +102 -28
  7. package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
  8. package/package.json +3 -2
  9. package/src/lib/styles/components.css +24 -5
  10. package/src/lib/styles/tokens.css +3 -0
  11. package/src/lib/styles/utilities.css +9 -4
  12. package/src/lib/ui/components/bar-list/bar-list.component.html +19 -18
  13. package/src/lib/ui/components/bar-list/bar-list.component.scss +63 -0
  14. package/src/lib/ui/components/breadcrumb/breadcrumb.component.css +31 -0
  15. package/src/lib/ui/components/breadcrumb/breadcrumb.component.html +6 -1
  16. package/src/lib/ui/components/button/button.component.html +37 -225
  17. package/src/lib/ui/components/button/button.component.scss +30 -0
  18. package/src/lib/ui/components/drawer/drawer.component.html +32 -7
  19. package/src/lib/ui/components/drawer/drawer.component.scss +114 -17
  20. package/src/lib/ui/components/filter-pills/filter-pills.component.html +4 -1
  21. package/src/lib/ui/components/filter-pills/filter-pills.component.scss +23 -0
  22. package/src/lib/ui/components/master-detail/master-detail.component.css +36 -8
  23. package/src/lib/ui/components/master-detail/master-detail.component.html +4 -2
  24. package/src/lib/ui/components/section-card/section-card.component.html +10 -5
  25. package/src/lib/ui/components/section-card/section-card.component.scss +28 -0
  26. package/src/lib/ui/components/stat-cards/stat-cards.component.css +48 -0
  27. package/src/lib/ui/components/stat-cards/stat-cards.component.html +16 -7
  28. package/tokens.css +3 -0
  29. package/types/foxeltech-angular-ui-flow.d.ts +24 -2
  30. package/types/foxeltech-angular-ui.d.ts +92 -5
  31. package/utilities.css +9 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxeltech/angular-ui",
3
- "version": "0.7.108",
3
+ "version": "0.7.131",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -26,7 +26,8 @@
26
26
  "@microsoft/signalr": "^10.0.0",
27
27
  "d3-drag": "^3.0.0",
28
28
  "d3-selection": "^3.0.0",
29
- "d3-zoom": "^3.0.0"
29
+ "d3-zoom": "^3.0.0",
30
+ "lucide-angular": "^1.0.0"
30
31
  },
31
32
  "exports": {
32
33
  "./tailwind.css": "./tailwind.css",
@@ -361,11 +361,12 @@ input[type='password'] {
361
361
  inset 0 0 0 1px rgb(0 0 0 / 0.08),
362
362
  0 1px 2px rgb(0 0 0 / 0.12);
363
363
  }
364
- /* Icon-only button (no label): square h-8 × w-8, no side padding, so the
365
- glyph sits dead-centre (anh Tú 04/09). Consumers may still size it with
366
- utilities (`w-9 h-9`) those win in the utilities layer. */
364
+ /* Icon-only button (no label): square, no side padding, so the glyph sits
365
+ dead-centre (anh Tú 04/09). The width tracks `btn-common`'s height — 27px
366
+ since 17/09so it stays square; consumers may still size it with
367
+ utilities (`w-9 h-9`), which win in the utilities layer. */
367
368
  .fx-btn--icon {
368
- width: 2rem;
369
+ width: 27px;
369
370
  padding-left: 0;
370
371
  padding-right: 0;
371
372
  }
@@ -631,6 +632,10 @@ input[type='password'] {
631
632
  0 1px 2px rgb(0 0 0 / 0.15);
632
633
  }
633
634
  .btn-cta:hover {
635
+ /* The edge darkens a step past the fill — reference `.bi-newbtn:hover`
636
+ (measured 16/09). Without it the button only changed fill and shadow, so
637
+ the hover read softer than the design's. */
638
+ border-color: var(--color-cta-hover-border);
634
639
  box-shadow:
635
640
  inset 0 1px 0 rgb(255 255 255 / 0.18),
636
641
  0 2px 6px rgb(0 0 0 / 0.2);
@@ -971,7 +976,9 @@ input[type='password'] {
971
976
  font-size: var(--og-fs-xs);
972
977
  line-height: 1.5;
973
978
  white-space: nowrap;
974
- font-weight: 500;
979
+ /* 400, per the reference's risk badge ("High · 82", measured 16/09). At 500
980
+ it sat a step heavier than every other 11.5px label around it. */
981
+ font-weight: 400;
975
982
  }
976
983
  .fx-badge--negative {
977
984
  background: rgb(var(--og-negative-soft-bg));
@@ -1032,3 +1039,15 @@ input[type='password'] {
1032
1039
  .fx-statusdot--accent::before {
1033
1040
  background: rgb(var(--og-accent));
1034
1041
  }
1042
+
1043
+ /* A tooltip must never take the pointer. Material gives its overlay pane
1044
+ `pointer-events: auto`, and with the drawer COLLAPSED a nav item's tooltip
1045
+ opens at x≈70..110 while the collapse/expand button sits at 58..82 — the
1046
+ tooltip covered the button's right half and swallowed the click, which is
1047
+ why the toggle "sometimes" did nothing (MEASURED 16/09). Nothing in a
1048
+ tooltip is interactive, so this is safe everywhere. */
1049
+ .mat-mdc-tooltip-panel,
1050
+ .mat-mdc-tooltip,
1051
+ .mat-mdc-tooltip-panel > * {
1052
+ pointer-events: none !important;
1053
+ }
@@ -53,6 +53,9 @@
53
53
 
54
54
  --color-cta: #c2582a;
55
55
  --color-cta-hover: #b34e24;
56
+ /* Border of the CTA while hovered — the reference's `.bi-newbtn:hover`
57
+ darkens the edge one more step than the fill (16/09). */
58
+ --color-cta-hover-border: #a3461f;
56
59
 
57
60
  /* ---- Legacy colour names (@deprecated — remove after the V2 campaign;
58
61
  they resolve through the alias layer in theme.css, so every existing
@@ -8,10 +8,15 @@
8
8
  }
9
9
 
10
10
  @utility btn-common {
11
- /* Compact size (anh Tú 29/08): h-8 + 13px label the pre-V2 h-10 read too
12
- large against the V2 density. leading-none: arbitrary text-[13px] inherits
13
- lh 1.5 and Archivo's uneven leading floats the ink ~1px above center. */
14
- @apply flex text-[13px] leading-none h-8 font-semibold px-large py-small items-center justify-center rounded-lg shadow-sm transition-all duration-200 disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed;
11
+ /* The demo's `.og-button--sm` (anh Tú 17/09: compact như New Analysis)
12
+ 27px tall, 11.5px at weight 400, 12px sides, 7px radius. MEASURED off the
13
+ reference's own `.og-button--sm.bi-newbtn`, which is the topbar CTA: it
14
+ wore this shape through `!` overrides because the shared button was a size
15
+ above it. The h-8 / 13px / 600 this replaces was itself a step down from
16
+ pre-V2 h-10 (29/08); against V2 panel density it still read one notch
17
+ heavy. leading-none: an arbitrary font-size inherits lh 1.5 and Archivo's
18
+ uneven leading floats the ink ~1px above centre. */
19
+ @apply flex text-[length:var(--og-fs-xs)] leading-none h-[27px] font-normal px-[12px] py-[4px] items-center justify-center rounded-control shadow-sm transition-all duration-200 disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed;
15
20
  }
16
21
 
17
22
  @utility btn-menu-common {
@@ -1,29 +1,30 @@
1
- <div class="flex flex-col gap-small">
2
- @for (item of items(); track item.label) {
1
+ <!-- Ranked list, to the reference's "Top projects by risk" (measured in the
2
+ browser 16/09): the SCORE leads, right-aligned in a 70px column, then the
3
+ label in a 150px column, then a 3px track taking the rest. It used to be
4
+ label-left / score-right with a 5px full-width track underneath, which is a
5
+ different object — two lines per row instead of one. -->
6
+ <div class="fx-barlist flex flex-col">
7
+ @for (item of items(); track item.label; let first = $first) {
3
8
  <button
4
9
  type="button"
5
- class="text-left rounded-control px-normal py-small -mx-normal transition-colors"
10
+ class="fx-barlist__row text-left flex items-center"
6
11
  [class.hover:bg-surface-sunken]="interactive()"
7
12
  [class.cursor-default]="!interactive()"
8
13
  [tabindex]="interactive() ? 0 : -1"
9
14
  (click)="onSelect(item)"
10
15
  >
11
- <div class="flex items-baseline justify-between gap-normal">
12
- <span class="txt-body-medium-01 truncate">{{ item.label }}</span>
13
- <span class="text-sm font-semibold text-strong shrink-0" style="font-family: var(--og-font-figure)">
14
- {{ item.valueLabel ?? item.value }}
15
- </span>
16
- </div>
17
- @if (item.meta) {
18
- <div class="txt-utility-description truncate">{{ item.meta }}</div>
19
- }
20
- <div class="mt-xs h-[var(--og-track-h)] rounded-pill bg-track overflow-hidden">
21
- <div
22
- class="h-full rounded-pill transition-all duration-500"
23
- [ngClass]="barClass(item)"
16
+ <span class="fx-barlist__score shrink-0">{{ item.valueLabel ?? item.value }}</span>
17
+ <span class="fx-barlist__label shrink-0 truncate">{{ item.label }}</span>
18
+ <span class="fx-barlist__track flex-1">
19
+ <span
20
+ class="fx-barlist__fill block"
21
+ [class.fx-barlist__fill--lead]="first"
24
22
  [style.width.%]="widthOf(item)"
25
- ></div>
26
- </div>
23
+ ></span>
24
+ </span>
27
25
  </button>
26
+ @if (item.meta) {
27
+ <div class="fx-barlist__meta truncate">{{ item.meta }}</div>
28
+ }
28
29
  }
29
30
  </div>
@@ -0,0 +1,63 @@
1
+ @reference '../../../styles/tailwind.css';
2
+
3
+ /* Metrics from the reference (measured 16/09): rows 8px apart, each 18px tall
4
+ with a 10px gap between its three cells. */
5
+ .fx-barlist {
6
+ gap: 8px;
7
+ }
8
+
9
+ .fx-barlist__row {
10
+ gap: 10px;
11
+ min-height: 18px;
12
+ /* A right gutter so the bar stops short of the panel edge (anh Tú 16/09).
13
+ The reference's row is 387px wide and its 147px track happens to end level
14
+ with the card's padding; this panel is twice that, so a track that fills
15
+ the remainder runs right up to the edge and reads as an overflow. */
16
+ padding: 0 24px 0 0;
17
+ background: transparent;
18
+ border: 0;
19
+ }
20
+
21
+ /* Score first, right-aligned in its own column — that alignment is what makes
22
+ the numbers read as a ranked column rather than trailing labels. */
23
+ .fx-barlist__score {
24
+ width: 70px;
25
+ text-align: right;
26
+ font-family: var(--og-font-mono);
27
+ font-size: var(--og-fs-sm);
28
+ font-weight: 400;
29
+ color: rgb(var(--og-text));
30
+ }
31
+
32
+ .fx-barlist__label {
33
+ width: 150px;
34
+ font-size: var(--og-fs-sm);
35
+ font-weight: 400;
36
+ color: rgb(var(--og-text-muted));
37
+ }
38
+
39
+ /* A 3px hairline track, square ends — not the 5px pill it was. */
40
+ .fx-barlist__track {
41
+ height: 3px;
42
+ background: rgb(var(--og-track));
43
+ overflow: hidden;
44
+ }
45
+
46
+ /* Every bar is muted EXCEPT the leader, which takes the accent. The reference
47
+ ranks by one colour and highlights the top row; tone-per-row turned the
48
+ panel into a severity chart, which is the donut's job, not this one. */
49
+ .fx-barlist__fill {
50
+ height: 100%;
51
+ background: rgb(var(--og-text-muted));
52
+ transition: width 500ms ease;
53
+ }
54
+
55
+ .fx-barlist__fill--lead {
56
+ background: rgb(var(--og-accent));
57
+ }
58
+
59
+ .fx-barlist__meta {
60
+ font-size: var(--og-fs-2xs);
61
+ color: rgb(var(--og-text-faint));
62
+ padding-left: 80px;
63
+ }
@@ -260,3 +260,34 @@
260
260
  .text-faint { color: rgb(var(--og-text-faint)); }
261
261
  .text-strong { color: rgb(var(--og-text)); }
262
262
  .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
263
+
264
+ /* CTA crumb — the reference's "+ Create project" at the end of the Project
265
+ Management trail (measured 16/09: 24px tall, 4px 10px 4px 8px, gap 5,
266
+ 10.5px/600, radius 7, a light accent fill with an accent border).
267
+ The exact demo colours are #eaf1fa / #b9cde6 / #2a4f7c; the accent-soft
268
+ TOKENS are used instead so the pill follows the theme in all six — they sit
269
+ within ~3 of those values in light mode, and the border is mixed from the
270
+ accent rather than hard-coded because no token matches it. */
271
+ .fx-crumb-cta {
272
+ appearance: none;
273
+ display: inline-flex;
274
+ align-items: center;
275
+ gap: 5px;
276
+ min-width: 0;
277
+ padding: 4px 10px 4px 8px;
278
+ line-height: 1.5;
279
+ border: 1px solid color-mix(in srgb, rgb(var(--og-accent)) 35%, transparent);
280
+ border-radius: var(--og-radius-control);
281
+ background: rgb(var(--og-accent-soft-bg));
282
+ color: rgb(var(--og-accent-soft-fg));
283
+ font-size: var(--og-fs-2xs);
284
+ font-weight: 600;
285
+ white-space: nowrap;
286
+ cursor: pointer;
287
+ transition: background-color 130ms ease, border-color 130ms ease;
288
+ }
289
+
290
+ .fx-crumb-cta:hover {
291
+ background: color-mix(in srgb, rgb(var(--og-accent)) 16%, rgb(var(--og-accent-soft-bg)));
292
+ border-color: color-mix(in srgb, rgb(var(--og-accent)) 55%, transparent);
293
+ }
@@ -3,7 +3,12 @@
3
3
  @if (i > 0) {
4
4
  <span aria-hidden="true" class="fx-crumb-sep">/</span>
5
5
  }
6
- @if (isChip(crumb)) {
6
+ @if (crumb.cta) {
7
+ <button type="button" class="fx-crumb-cta" (click)="go($event, crumb.url || '')">
8
+ <fx-ui-hero-icon [icon]="crumb.icon || 'plus'" [size]="13" class="flex shrink-0" />
9
+ <span class="truncate">{{ crumb.label }}</span>
10
+ </button>
11
+ } @else if (isChip(crumb)) {
7
12
  <span class="relative inline-flex min-w-0">
8
13
  <button
9
14
  type="button"
@@ -14,229 +14,41 @@
14
14
  [disabled]="disabled() || loading()"
15
15
  (click)="onClick()"
16
16
  >
17
- @if (loading()) {
18
- <div class="flex w-full items-center justify-center" aria-hidden="true">
19
- <svg
20
- [attr.fill]="
21
- buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'
22
- "
23
- width="16"
24
- height="16"
25
- viewBox="0 0 24 24"
26
- xmlns="http://www.w3.org/2000/svg"
27
- >
28
- <circle cx="4" cy="12" r="0">
29
- <animate
30
- begin="0;spinner_z0Or.end"
31
- attributeName="r"
32
- calcMode="spline"
33
- dur="0.5s"
34
- keySplines=".36,.6,.31,1"
35
- values="0;3"
36
- fill="freeze"
37
- />
38
- <animate
39
- begin="spinner_OLMs.end"
40
- attributeName="cx"
41
- calcMode="spline"
42
- dur="0.5s"
43
- keySplines=".36,.6,.31,1"
44
- values="4;12"
45
- fill="freeze"
46
- />
47
- <animate
48
- begin="spinner_UHR2.end"
49
- attributeName="cx"
50
- calcMode="spline"
51
- dur="0.5s"
52
- keySplines=".36,.6,.31,1"
53
- values="12;20"
54
- fill="freeze"
55
- />
56
- <animate
57
- id="spinner_lo66"
58
- begin="spinner_Aguh.end"
59
- attributeName="r"
60
- calcMode="spline"
61
- dur="0.5s"
62
- keySplines=".36,.6,.31,1"
63
- values="3;0"
64
- fill="freeze"
65
- />
66
- <animate
67
- id="spinner_z0Or"
68
- begin="spinner_lo66.end"
69
- attributeName="cx"
70
- dur="0.001s"
71
- values="20;4"
72
- fill="freeze"
73
- />
74
- </circle>
75
- <circle cx="4" cy="12" r="3">
76
- <animate
77
- begin="0;spinner_z0Or.end"
78
- attributeName="cx"
79
- calcMode="spline"
80
- dur="0.5s"
81
- keySplines=".36,.6,.31,1"
82
- values="4;12"
83
- fill="freeze"
84
- />
85
- <animate
86
- begin="spinner_OLMs.end"
87
- attributeName="cx"
88
- calcMode="spline"
89
- dur="0.5s"
90
- keySplines=".36,.6,.31,1"
91
- values="12;20"
92
- fill="freeze"
93
- />
94
- <animate
95
- id="spinner_JsnR"
96
- begin="spinner_UHR2.end"
97
- attributeName="r"
98
- calcMode="spline"
99
- dur="0.5s"
100
- keySplines=".36,.6,.31,1"
101
- values="3;0"
102
- fill="freeze"
103
- />
104
- <animate
105
- id="spinner_Aguh"
106
- begin="spinner_JsnR.end"
107
- attributeName="cx"
108
- dur="0.001s"
109
- values="20;4"
110
- fill="freeze"
111
- />
112
- <animate
113
- begin="spinner_Aguh.end"
114
- attributeName="r"
115
- calcMode="spline"
116
- dur="0.5s"
117
- keySplines=".36,.6,.31,1"
118
- values="0;3"
119
- fill="freeze"
120
- />
121
- </circle>
122
- <circle cx="12" cy="12" r="3">
123
- <animate
124
- begin="0;spinner_z0Or.end"
125
- attributeName="cx"
126
- calcMode="spline"
127
- dur="0.5s"
128
- keySplines=".36,.6,.31,1"
129
- values="12;20"
130
- fill="freeze"
131
- />
132
- <animate
133
- id="spinner_hSjk"
134
- begin="spinner_OLMs.end"
135
- attributeName="r"
136
- calcMode="spline"
137
- dur="0.5s"
138
- keySplines=".36,.6,.31,1"
139
- values="3;0"
140
- fill="freeze"
141
- />
142
- <animate
143
- id="spinner_UHR2"
144
- begin="spinner_hSjk.end"
145
- attributeName="cx"
146
- dur="0.001s"
147
- values="20;4"
148
- fill="freeze"
149
- />
150
- <animate
151
- begin="spinner_UHR2.end"
152
- attributeName="r"
153
- calcMode="spline"
154
- dur="0.5s"
155
- keySplines=".36,.6,.31,1"
156
- values="0;3"
157
- fill="freeze"
158
- />
159
- <animate
160
- begin="spinner_Aguh.end"
161
- attributeName="cx"
162
- calcMode="spline"
163
- dur="0.5s"
164
- keySplines=".36,.6,.31,1"
165
- values="4;12"
166
- fill="freeze"
167
- />
168
- </circle>
169
- <circle cx="20" cy="12" r="3">
170
- <animate
171
- id="spinner_4v5M"
172
- begin="0;spinner_z0Or.end"
173
- attributeName="r"
174
- calcMode="spline"
175
- dur="0.5s"
176
- keySplines=".36,.6,.31,1"
177
- values="3;0"
178
- fill="freeze"
179
- />
180
- <animate
181
- id="spinner_OLMs"
182
- begin="spinner_4v5M.end"
183
- attributeName="cx"
184
- dur="0.001s"
185
- values="20;4"
186
- fill="freeze"
187
- />
188
- <animate
189
- begin="spinner_OLMs.end"
190
- attributeName="r"
191
- calcMode="spline"
192
- dur="0.5s"
193
- keySplines=".36,.6,.31,1"
194
- values="0;3"
195
- fill="freeze"
196
- />
197
- <animate
198
- begin="spinner_UHR2.end"
199
- attributeName="cx"
200
- calcMode="spline"
201
- dur="0.5s"
202
- keySplines=".36,.6,.31,1"
203
- values="4;12"
204
- fill="freeze"
205
- />
206
- <animate
207
- begin="spinner_Aguh.end"
208
- attributeName="cx"
209
- calcMode="spline"
210
- dur="0.5s"
211
- keySplines=".36,.6,.31,1"
212
- values="12;20"
213
- fill="freeze"
214
- />
215
- </circle>
216
- </svg>
217
- </div>
218
- } @else {
219
- <ng-container>
220
- <div class="flex items-center justify-center gap-small">
221
- @if (icon) {
222
- <fx-ui-hero-icon
223
- [icon]="icon"
224
- [size]="16"
225
- class="flex"
226
- [ngClass]="{
227
- 'text-text-inverse': buttonVariant() === 'default',
228
- 'text-text-primary': buttonVariant() === 'alternative',
229
- 'text-accent-contrast': buttonVariant() === 'primary',
230
- 'text-surface': buttonVariant() === 'success',
231
- 'text-white': buttonVariant() === 'cta',
232
- }"
233
- ></fx-ui-hero-icon>
234
- }
235
- @if (label) {
236
- <!-- text-box: optical centering — Archivo's leading floats ink up otherwise -->
237
- <div class="[text-box:trim-both_cap_alphabetic]">{{ label }}</div>
238
- }
239
- </div>
240
- </ng-container>
241
- }
17
+ <!-- Loading keeps the LABEL, as the sign-in button does: a button that
18
+ swaps its text for a spinner collapses to a stub, and the row jumps.
19
+ The ring is that button's `.li-spin`, in the foreground the variant
20
+ already uses for its icon so it reads on a coloured ground. -->
21
+ <!-- 6px, the reference button's own gap; `gap-small` is 4px. -->
22
+ <div class="flex items-center justify-center gap-[6px]">
23
+ @if (loading()) {
24
+ <span
25
+ class="fx-btn__spin"
26
+ aria-hidden="true"
27
+ [ngClass]="{
28
+ 'text-text-inverse': buttonVariant() === 'default',
29
+ 'text-text-primary': buttonVariant() === 'alternative',
30
+ 'text-accent-contrast': buttonVariant() === 'primary',
31
+ 'text-surface': buttonVariant() === 'success',
32
+ 'text-white': buttonVariant() === 'cta',
33
+ }"
34
+ ></span>
35
+ } @else if (icon) {
36
+ <fx-ui-hero-icon
37
+ [icon]="icon"
38
+ [size]="13"
39
+ class="flex"
40
+ [ngClass]="{
41
+ 'text-text-inverse': buttonVariant() === 'default',
42
+ 'text-text-primary': buttonVariant() === 'alternative',
43
+ 'text-accent-contrast': buttonVariant() === 'primary',
44
+ 'text-surface': buttonVariant() === 'success',
45
+ 'text-white': buttonVariant() === 'cta',
46
+ }"
47
+ ></fx-ui-hero-icon>
48
+ }
49
+ @if (label) {
50
+ <!-- text-box: optical centering — Archivo's leading floats ink up otherwise -->
51
+ <div class="[text-box:trim-both_cap_alphabetic]">{{ label }}</div>
52
+ }
53
+ </div>
242
54
  </button>
@@ -0,0 +1,30 @@
1
+
2
+
3
+ /* Loading ring — the sign-in button's `.li-spin`, sized to sit beside a label.
4
+ `currentColor` so the variant's own foreground class colours it. */
5
+ .fx-btn__spin {
6
+ display: inline-block;
7
+ /* 12px, matching the button's 13px icon — 14 crowded the 27px box. */
8
+ width: 12px;
9
+ height: 12px;
10
+ flex: 0 0 auto;
11
+ border-radius: 50%;
12
+ border: 2px solid currentColor;
13
+ border-top-color: transparent;
14
+ opacity: 0.9;
15
+ animation: fx-btn-spin 0.8s linear infinite;
16
+ }
17
+
18
+ @keyframes fx-btn-spin {
19
+ to {
20
+ transform: rotate(360deg);
21
+ }
22
+ }
23
+
24
+ /* A ring that has stopped still marks the button as busy, and `aria-busy`
25
+ carries the state regardless. */
26
+ @media (prefers-reduced-motion: reduce) {
27
+ .fx-btn__spin {
28
+ animation: none;
29
+ }
30
+ }
@@ -1,10 +1,15 @@
1
1
  <div class="drawer-host-wrapper">
2
2
  <a href="/" class="drawer-brand" [class.is-collapsed]="!isExpanded">
3
- @if (logo) {
4
- <img [src]="logo" alt="Home" class="drawer-brand-mark" />
5
- } @else {
6
- <div class="drawer-brand-mark"></div>
7
- }
3
+ <!-- Logo sits in its own sunken tile (reference `.sb-logo`, measured 16/09):
4
+ the tile is what carries the hover border, so the mark keeps its own
5
+ size and nothing shifts by a pixel when that border appears. -->
6
+ <span class="drawer-brand-logo">
7
+ @if (logo) {
8
+ <img [src]="logo" alt="Home" class="drawer-brand-mark" />
9
+ } @else {
10
+ <span class="drawer-brand-mark"></span>
11
+ }
12
+ </span>
8
13
  @if (isExpanded && (logoText || logoSubtext)) {
9
14
  <span class="drawer-brand-copy">
10
15
  @if (logoText) {
@@ -87,6 +92,7 @@
87
92
  <a
88
93
  [routerLink]="item.route"
89
94
  class="drawer-item"
95
+ [class.is-collapsed]="!isExpanded"
90
96
  [class.active]="isActiveRoute(item.route)"
91
97
  [class.has-children]="item.children && item.children.length > 0"
92
98
  [style.paddingLeft.px]="isExpanded ? 8 + level * 16 : 16"
@@ -96,7 +102,16 @@
96
102
  (click)="onItemClick(item, $event)"
97
103
  >
98
104
  <div class="item-content">
99
- <fx-ui-hero-icon [icon]="item.icon" [size]="20" class="item-icon"></fx-ui-hero-icon>
105
+ @if (item.iconData) {
106
+ <lucide-icon
107
+ [img]="item.iconData"
108
+ [size]="isExpanded ? 13 : 18"
109
+ class="item-icon"
110
+ [class.item-icon--solid]="item.iconSolid"
111
+ />
112
+ } @else {
113
+ <fx-ui-hero-icon [icon]="item.icon" [size]="isExpanded ? 13 : 18" class="item-icon"></fx-ui-hero-icon>
114
+ }
100
115
  @if (isExpanded) {
101
116
  <span class="item-label">{{ item.label }}</span>
102
117
  }
@@ -108,6 +123,7 @@
108
123
  <button
109
124
  type="button"
110
125
  class="drawer-item"
126
+ [class.is-collapsed]="!isExpanded"
111
127
  [class.active]="isActiveRoute(item.route)"
112
128
  [class.has-children]="item.children && item.children.length > 0"
113
129
  [style.paddingLeft.px]="isExpanded ? 8 + level * 16 : 16"
@@ -117,7 +133,16 @@
117
133
  (click)="onItemClick(item, $event)"
118
134
  >
119
135
  <div class="item-content">
120
- <fx-ui-hero-icon [icon]="item.icon" [size]="20" class="item-icon"></fx-ui-hero-icon>
136
+ @if (item.iconData) {
137
+ <lucide-icon
138
+ [img]="item.iconData"
139
+ [size]="isExpanded ? 13 : 18"
140
+ class="item-icon"
141
+ [class.item-icon--solid]="item.iconSolid"
142
+ />
143
+ } @else {
144
+ <fx-ui-hero-icon [icon]="item.icon" [size]="isExpanded ? 13 : 18" class="item-icon"></fx-ui-hero-icon>
145
+ }
121
146
  @if (isExpanded) {
122
147
  <span class="item-label">{{ item.label }}</span>
123
148
  }