@foxeltech/angular-ui 0.7.75 → 0.7.101

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 +66 -3
  2. package/fesm2022/foxeltech-angular-ui-chart.mjs +33 -3
  3. package/fesm2022/foxeltech-angular-ui-chart.mjs.map +1 -1
  4. package/fesm2022/foxeltech-angular-ui-flow.mjs +42 -5
  5. package/fesm2022/foxeltech-angular-ui-flow.mjs.map +1 -1
  6. package/fesm2022/foxeltech-angular-ui.mjs +641 -60
  7. package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
  8. package/package.json +3 -2
  9. package/src/lib/styles/components.css +66 -3
  10. package/src/lib/styles/utilities.css +8 -2
  11. package/src/lib/ui/components/breadcrumb/breadcrumb.component.css +262 -0
  12. package/src/lib/ui/components/breadcrumb/breadcrumb.component.html +91 -0
  13. package/src/lib/ui/components/button/button.component.html +1 -0
  14. package/src/lib/ui/components/filter-pills/filter-pills.component.html +1 -1
  15. package/src/lib/ui/components/load-status/load-status.component.css +96 -0
  16. package/src/lib/ui/components/load-status/load-status.component.html +20 -0
  17. package/src/lib/ui/components/master-detail/master-detail.component.css +63 -11
  18. package/src/lib/ui/components/master-detail/master-detail.component.html +71 -42
  19. package/src/lib/ui/components/pending-badge/pending-badge.component.html +2 -2
  20. package/src/lib/ui/components/project-card/project-card.component.html +7 -3
  21. package/src/lib/ui/components/route-skeleton/route-skeleton.component.css +1 -1
  22. package/src/lib/ui/components/sample-frame/sample-frame.component.html +6 -7
  23. package/src/lib/ui/components/section-card/section-card.component.html +3 -1
  24. package/src/lib/ui/components/segmented/segmented.component.html +6 -2
  25. package/src/lib/ui/components/severity-badges/severity-badges.component.html +1 -1
  26. package/src/lib/ui/components/stat-cards/stat-cards.component.css +18 -0
  27. package/src/lib/ui/components/stat-cards/stat-cards.component.html +3 -3
  28. package/types/foxeltech-angular-ui-chart.d.ts +18 -1
  29. package/types/foxeltech-angular-ui-flow.d.ts +7 -0
  30. package/types/foxeltech-angular-ui.d.ts +349 -14
  31. package/utilities.css +8 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxeltech/angular-ui",
3
- "version": "0.7.75",
3
+ "version": "0.7.101",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -14,7 +14,8 @@
14
14
  "@angular/material": "^20.2.7 || ^21.0.0",
15
15
  "@angular/cdk": "^20.2.7 || ^21.0.0",
16
16
  "@ngx-translate/core": "^14.0.0",
17
- "@ngx-translate/http-loader": "^7.0.0"
17
+ "@ngx-translate/http-loader": "^7.0.0",
18
+ "gsap": "^3.12.0"
18
19
  },
19
20
  "dependencies": {
20
21
  "tslib": "^2.3.0",
@@ -339,14 +339,35 @@ input[type='password'] {
339
339
  @apply font-mono text-sm text-text-primary tracking-normal;
340
340
  }
341
341
 
342
+ /* ---- Nút: viền hairline + bóng mờ ở MỌI biến thể (anh Tú 04/09) --------
343
+ Demo `.og-button`: border `--og-rule`, hover `0 2px 7px rgba(0,0,0,.12)`;
344
+ `.bi-newbtn`: viền tối hơn nền 1 bậc + `0 1px 2px rgba(0,0,0,.15)`. Nút nền
345
+ đặc dùng viền inset tối 8% để không phải khai màu viền riêng cho gradient. */
342
346
  .btn-default {
343
347
  @apply btn-common bg-button-default hover:opacity-75 text-text-inverse;
348
+ box-shadow:
349
+ inset 0 0 0 1px rgb(0 0 0 / 0.08),
350
+ 0 1px 2px rgb(0 0 0 / 0.12);
344
351
  }
345
352
  .btn-primary {
346
353
  @apply btn-common bg-gradient-primary hover:opacity-75 text-text-focus;
354
+ box-shadow:
355
+ inset 0 0 0 1px rgb(0 0 0 / 0.08),
356
+ 0 1px 2px rgb(0 0 0 / 0.12);
347
357
  }
348
358
  .btn-success {
349
359
  @apply btn-common bg-gradient-success hover:opacity-75 text-surface;
360
+ box-shadow:
361
+ inset 0 0 0 1px rgb(0 0 0 / 0.08),
362
+ 0 1px 2px rgb(0 0 0 / 0.12);
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. */
367
+ .fx-btn--icon {
368
+ width: 2rem;
369
+ padding-left: 0;
370
+ padding-right: 0;
350
371
  }
351
372
  /* Nút hành động chính theo demo (`.bi-newbtn`): màu cố định, không phải accent. */
352
373
  /* ---- Tông severity theo ĐÚNG mô hình demo (đo 03/09) -----------------
@@ -604,13 +625,24 @@ input[type='password'] {
604
625
  }
605
626
 
606
627
  .btn-cta {
607
- @apply btn-common bg-cta hover:bg-cta-hover text-white;
628
+ @apply btn-common bg-cta hover:bg-cta-hover text-white border border-cta-hover;
608
629
  box-shadow:
609
630
  inset 0 1px 0 rgb(255 255 255 / 0.18),
610
631
  0 1px 2px rgb(0 0 0 / 0.15);
611
632
  }
633
+ .btn-cta:hover {
634
+ box-shadow:
635
+ inset 0 1px 0 rgb(255 255 255 / 0.18),
636
+ 0 2px 6px rgb(0 0 0 / 0.2);
637
+ }
638
+ /* Outline = `.og-button--outline`: nền surface, viền --og-border, bóng 1px;
639
+ hover nổi lên 0 2px 7px như demo. */
612
640
  .btn-alternative {
613
- @apply btn-common hover:bg-bg-hover text-text-primary border border-border-default;
641
+ @apply btn-common bg-surface hover:bg-bg-hover text-text-primary border border-border-default;
642
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
643
+ }
644
+ .btn-alternative:hover {
645
+ box-shadow: 0 2px 7px rgb(0 0 0 / 0.12);
614
646
  }
615
647
  .btn-menu-top {
616
648
  @apply btn-menu-common border-b border-border-default pt-small pb-semi;
@@ -731,6 +763,17 @@ input[type='password'] {
731
763
  max-height: calc(100vh - 315px);
732
764
  overflow: auto;
733
765
  }
766
+ /* Phones: a data table must not squeeze its columns to a few px per cell —
767
+ give it a floor and let .table-page-container scroll it sideways
768
+ (anh Tú 04/09 mobile audit: Name column collapsed to one character/line). */
769
+ @media (max-width: 719px) {
770
+ /* !important like the other .mat-mdc-table overrides above: this file is
771
+ imported into a cascade @layer, so an unlayered Material rule
772
+ (`.mat-mdc-table{min-width:100%}`) beats it whatever the specificity. */
773
+ .mat-mdc-table {
774
+ min-width: 640px !important;
775
+ }
776
+ }
734
777
  .table-cell-padding {
735
778
  padding-top: 8px !important;
736
779
  padding-bottom: 8px !important;
@@ -766,8 +809,28 @@ input[type='password'] {
766
809
  rgb(var(--gradient-primary-end))
767
810
  ) border-box;
768
811
  }
812
+ /* Skeleton block = the design's `.bi-sk` shimmer ("Loading States", 04/09):
813
+ text at 7% → 13% → 7% sweeping left→right over 1.15 s, 6px radius. One
814
+ rule for every skeleton in the product — route skeleton, table cells,
815
+ dashboard cards — so the boot splash, route and in-panel states match. */
769
816
  .skeleton-load {
770
- @apply rounded bg-track animate-pulse motion-reduce:animate-none;
817
+ display: block;
818
+ border-radius: 6px;
819
+ background: linear-gradient(
820
+ 90deg,
821
+ rgb(var(--og-text) / 0.07) 0px,
822
+ rgb(var(--og-text) / 0.13) 90px,
823
+ rgb(var(--og-text) / 0.07) 180px
824
+ );
825
+ background-size: 400px 100%;
826
+ animation: fx-shimmer 1.15s linear infinite;
827
+ }
828
+ @keyframes fx-shimmer {
829
+ 0% { background-position: -260px 0; }
830
+ 100% { background-position: 260px 0; }
831
+ }
832
+ @media (prefers-reduced-motion: reduce) {
833
+ .skeleton-load { animation: none; background: rgb(var(--og-text) / 0.08); }
771
834
  }
772
835
 
773
836
  /* ============================================================================
@@ -31,9 +31,15 @@
31
31
  }
32
32
 
33
33
  @utility card-common {
34
- @apply text-left px-xl py-large border border-border-default rounded-lg shadow-sm transition-colors duration-500 ease-in-out;
34
+ @apply text-left px-xl py-large border border-border-default rounded-lg transition-colors duration-500 ease-in-out;
35
+ /* Demo `.og-panel` / `.og-statgrid`: hairline border + two soft layers.
36
+ Replaces Tailwind shadow-sm, which reads harder at the edges. */
37
+ box-shadow:
38
+ 0 1px 2px rgb(0 0 0 / 0.04),
39
+ 0 3px 8px rgb(0 0 0 / 0.05);
35
40
  }
36
41
 
37
42
  @utility tag-common {
38
- @apply flex min-w-7 px-[10px] py-[2px] leading-5 hover:opacity-70 items-center justify-center;
43
+ /* design .og-badge: 3px 10px (anh 04/09: pills read too thin at 2px) */
44
+ @apply flex min-w-7 px-[10px] py-[3px] leading-5 hover:opacity-70 items-center justify-center;
39
45
  }
@@ -0,0 +1,262 @@
1
+ /* Demo topbar breadcrumb, measured 04/09 (getComputedStyle on the preview):
2
+ nav gap 7px · plain crumb 12.5px/500 muted · chip 11.5px/600, 1px border,
3
+ radius-control, 0 1px 2px .04 shadow, padding 4px 12px · listbox 220px min,
4
+ radius-card, 0 10px 28px .14 shadow. Plain CSS on tokens so the component
5
+ does not depend on the consuming app's Tailwind scan. */
6
+ :host {
7
+ display: block;
8
+ min-width: 0;
9
+ }
10
+
11
+ .fx-crumbs {
12
+ display: flex;
13
+ align-items: center;
14
+ gap: 7px;
15
+ min-width: 0;
16
+ white-space: nowrap;
17
+ }
18
+
19
+ .fx-crumb-sep {
20
+ flex: 0 0 auto;
21
+ padding: 0 2px;
22
+ font-size: 13px;
23
+ color: rgb(var(--og-text-faint));
24
+ }
25
+
26
+ .fx-crumb-link {
27
+ flex: 0 0 auto;
28
+ max-width: 260px;
29
+ overflow: hidden;
30
+ text-overflow: ellipsis;
31
+ font-size: var(--og-fs-sm);
32
+ font-weight: 500;
33
+ letter-spacing: 0.005em;
34
+ color: rgb(var(--og-text-muted));
35
+ text-decoration: none;
36
+ transition: color 0.15s;
37
+ }
38
+ .fx-crumb-link:hover {
39
+ color: rgb(var(--og-text));
40
+ }
41
+
42
+ .fx-crumb-current {
43
+ min-width: 0;
44
+ overflow: hidden;
45
+ text-overflow: ellipsis;
46
+ font-size: var(--og-fs-sm);
47
+ font-weight: 600;
48
+ color: rgb(var(--og-text));
49
+ }
50
+
51
+ .fx-crumb-chip {
52
+ appearance: none;
53
+ display: inline-flex;
54
+ align-items: center;
55
+ gap: 8px;
56
+ min-width: 0;
57
+ max-width: 320px;
58
+ padding: 4px 12px;
59
+ line-height: 1.5;
60
+ border: 1px solid rgb(var(--og-border));
61
+ border-radius: var(--og-radius-control);
62
+ background: rgb(var(--og-surface));
63
+ font-size: var(--og-fs-xs);
64
+ font-weight: 600;
65
+ color: rgb(var(--og-text));
66
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.04);
67
+ white-space: nowrap;
68
+ cursor: pointer;
69
+ transition: box-shadow 0.12s, border-color 0.12s;
70
+ }
71
+ .fx-crumb-chip:hover,
72
+ .fx-crumb-chip[aria-expanded='true'] {
73
+ box-shadow: 0 2px 7px rgb(0 0 0 / 0.1);
74
+ }
75
+ .fx-crumb-chip--mono {
76
+ font-family: var(--og-font-mono);
77
+ }
78
+
79
+ /* ---- switcher panel ---------------------------------------------------- */
80
+ .fx-crumb-panel {
81
+ position: absolute;
82
+ top: calc(100% + 8px);
83
+ left: 0;
84
+ z-index: 60;
85
+ min-width: 240px;
86
+ max-width: min(380px, 90vw);
87
+ display: flex;
88
+ flex-direction: column;
89
+ padding: 6px;
90
+ background: rgb(var(--og-surface));
91
+ border: 1px solid rgb(var(--og-border));
92
+ border-radius: var(--og-radius-card);
93
+ box-shadow: 0 10px 28px rgb(0 0 0 / 0.14);
94
+ text-align: left;
95
+ white-space: nowrap;
96
+ animation: fx-crumb-panel-in 0.16s ease-out;
97
+ }
98
+ @keyframes fx-crumb-panel-in {
99
+ from {
100
+ opacity: 0;
101
+ transform: translateY(-4px);
102
+ }
103
+ to {
104
+ opacity: 1;
105
+ transform: none;
106
+ }
107
+ }
108
+ @media (prefers-reduced-motion: reduce) {
109
+ .fx-crumb-panel {
110
+ animation: none;
111
+ }
112
+ }
113
+
114
+ .fx-crumb-panel__title {
115
+ padding: 6px 12px 8px;
116
+ margin-bottom: 4px;
117
+ font-size: var(--og-fs-2xs);
118
+ letter-spacing: 0.02em;
119
+ color: rgb(var(--og-text-faint));
120
+ border-bottom: 1px solid rgb(var(--og-border-subtle));
121
+ }
122
+
123
+ .fx-crumb-panel__filter {
124
+ margin: 2px 6px 6px;
125
+ padding: 6px 10px;
126
+ border: 1px solid rgb(var(--og-border));
127
+ border-radius: var(--og-radius-control);
128
+ background: rgb(var(--og-surface-sunken));
129
+ color: rgb(var(--og-text));
130
+ font-family: var(--og-font-body);
131
+ font-size: var(--og-fs-xs);
132
+ outline: none;
133
+ }
134
+ .fx-crumb-panel__filter:focus {
135
+ border-color: rgb(var(--og-accent));
136
+ }
137
+
138
+ .fx-crumb-panel__opt {
139
+ appearance: none;
140
+ width: 100%;
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 8px;
144
+ padding: 8px 12px;
145
+ border: 0;
146
+ border-radius: var(--og-radius-control);
147
+ background: transparent;
148
+ font-family: var(--og-font-body);
149
+ font-size: var(--og-fs-sm);
150
+ font-weight: 400;
151
+ color: rgb(var(--og-text-muted));
152
+ text-align: left;
153
+ cursor: pointer;
154
+ transition: background 0.12s;
155
+ }
156
+ .fx-crumb-panel--mono .fx-crumb-panel__opt {
157
+ font-family: var(--og-font-mono);
158
+ font-size: var(--og-fs-xs);
159
+ }
160
+ .fx-crumb-panel__opt:hover {
161
+ background: rgb(var(--og-surface-sunken));
162
+ }
163
+ .fx-crumb-panel__opt--on {
164
+ font-weight: 600;
165
+ color: rgb(var(--og-text));
166
+ }
167
+ .fx-crumb-panel__meta {
168
+ margin-left: auto;
169
+ font-family: var(--og-font-mono);
170
+ font-size: var(--og-fs-2xs);
171
+ color: rgb(var(--og-text-faint));
172
+ }
173
+ .fx-crumb-panel__opt > fx-ui-hero-icon:last-child {
174
+ margin-left: auto;
175
+ }
176
+ .fx-crumb-panel__opt > .fx-crumb-panel__meta + fx-ui-hero-icon {
177
+ margin-left: 0;
178
+ }
179
+
180
+ .fx-crumb-panel__empty {
181
+ padding: 8px 12px;
182
+ font-size: var(--og-fs-xs);
183
+ color: rgb(var(--og-text-faint));
184
+ }
185
+
186
+ .fx-crumb-panel__skeleton {
187
+ display: flex;
188
+ flex-direction: column;
189
+ gap: 8px;
190
+ padding: 8px 12px;
191
+ }
192
+ .fx-crumb-panel__skeleton span {
193
+ display: block;
194
+ height: 10px;
195
+ border-radius: 4px;
196
+ background: rgb(var(--og-border-subtle));
197
+ animation: fx-crumb-pulse 1.2s ease-in-out infinite;
198
+ }
199
+ .fx-crumb-panel__skeleton span:nth-child(1) { width: 62%; }
200
+ .fx-crumb-panel__skeleton span:nth-child(2) { width: 48%; animation-delay: 0.15s; }
201
+ .fx-crumb-panel__skeleton span:nth-child(3) { width: 55%; animation-delay: 0.3s; }
202
+ @keyframes fx-crumb-pulse {
203
+ 0%, 100% { opacity: 1; }
204
+ 50% { opacity: 0.45; }
205
+ }
206
+
207
+ .fx-crumb-panel__rule {
208
+ margin: 4px 0;
209
+ border-top: 1px solid rgb(var(--og-border-subtle));
210
+ }
211
+
212
+ .fx-crumb-panel__more {
213
+ appearance: none;
214
+ width: 100%;
215
+ display: flex;
216
+ align-items: center;
217
+ gap: 8px;
218
+ padding: 7px 12px;
219
+ border: 0;
220
+ border-radius: var(--og-radius-control);
221
+ background: transparent;
222
+ font-family: var(--og-font-body);
223
+ font-size: var(--og-fs-xs);
224
+ color: rgb(var(--og-text-muted));
225
+ text-align: left;
226
+ cursor: pointer;
227
+ }
228
+ .fx-crumb-panel__more:hover {
229
+ background: rgb(var(--og-surface-sunken));
230
+ color: rgb(var(--og-text));
231
+ }
232
+
233
+ .fx-crumb-panel__create {
234
+ appearance: none;
235
+ width: calc(100% - 12px);
236
+ margin: 4px 6px 2px;
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 8px;
240
+ padding: 9px 12px;
241
+ border: 0;
242
+ border-radius: var(--og-radius-control);
243
+ background: rgb(var(--og-accent-soft-bg));
244
+ color: rgb(var(--og-accent-soft-fg));
245
+ font-family: var(--og-font-body);
246
+ font-size: var(--og-fs-sm);
247
+ font-weight: 600;
248
+ text-align: left;
249
+ cursor: pointer;
250
+ transition: background 0.15s, color 0.15s;
251
+ }
252
+ .fx-crumb-panel__create:hover {
253
+ background: rgb(var(--og-accent));
254
+ color: rgb(var(--og-accent-contrast));
255
+ }
256
+
257
+ /* Text utilities used in the template — kept local so the chip renders the
258
+ same in every consumer regardless of its Tailwind scan. */
259
+ .text-accent { color: rgb(var(--og-accent)); }
260
+ .text-faint { color: rgb(var(--og-text-faint)); }
261
+ .text-strong { color: rgb(var(--og-text)); }
262
+ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
@@ -0,0 +1,91 @@
1
+ <nav aria-label="Breadcrumb" class="fx-crumbs">
2
+ @for (crumb of crumbs(); track $index; let last = $last; let i = $index) {
3
+ @if (i > 0) {
4
+ <span aria-hidden="true" class="fx-crumb-sep">/</span>
5
+ }
6
+ @if (isChip(crumb)) {
7
+ <span class="relative inline-flex min-w-0">
8
+ <button
9
+ type="button"
10
+ class="fx-crumb-chip"
11
+ [class.fx-crumb-chip--mono]="crumb.mono"
12
+ [attr.aria-haspopup]="crumb.switcher ? 'listbox' : null"
13
+ [attr.aria-expanded]="crumb.switcher ? openIndex() === i : null"
14
+ [attr.title]="crumb.switcher?.title || crumb.label"
15
+ (click)="onChipClick(crumb, i, $event)"
16
+ >
17
+ @if (crumb.icon) {
18
+ <fx-ui-hero-icon [icon]="crumb.icon" [size]="13" class="flex shrink-0 text-accent" />
19
+ }
20
+ <span class="truncate">{{ crumb.label }}</span>
21
+ @if (crumb.switcher) {
22
+ <fx-ui-hero-icon icon="chevron-down" [size]="10" class="flex shrink-0 text-faint" />
23
+ }
24
+ </button>
25
+
26
+ @if (crumb.switcher; as sw) {
27
+ @if (openIndex() === i) {
28
+ <div role="listbox" class="fx-crumb-panel" [class.fx-crumb-panel--mono]="sw.mono">
29
+ <div class="fx-crumb-panel__title">{{ sw.title }}</div>
30
+ @if (sw.filterPlaceholder) {
31
+ <input
32
+ type="search"
33
+ class="fx-crumb-panel__filter"
34
+ [placeholder]="sw.filterPlaceholder"
35
+ [attr.aria-label]="sw.filterPlaceholder"
36
+ [value]="keyword()"
37
+ (input)="onFilter(crumb, $any($event.target).value)"
38
+ (click)="$event.stopPropagation()"
39
+ />
40
+ }
41
+ @if (loading() && options().length === 0) {
42
+ <div class="fx-crumb-panel__skeleton" aria-busy="true">
43
+ <span></span><span></span><span></span>
44
+ </div>
45
+ } @else if (options().length === 0) {
46
+ <div class="fx-crumb-panel__empty">{{ sw.emptyLabel || '—' }}</div>
47
+ }
48
+ @for (opt of options(); track opt.value) {
49
+ <button
50
+ type="button"
51
+ role="option"
52
+ class="fx-crumb-panel__opt"
53
+ [class.fx-crumb-panel__opt--on]="opt.value === sw.selected"
54
+ [attr.aria-selected]="opt.value === sw.selected"
55
+ (click)="pick(opt, $event)"
56
+ >
57
+ <span class="truncate">{{ opt.label }}</span>
58
+ @if (opt.meta) {
59
+ <span class="fx-crumb-panel__meta">{{ opt.meta }}</span>
60
+ }
61
+ @if (opt.value === sw.selected) {
62
+ <fx-ui-hero-icon icon="check" [size]="13" class="flex shrink-0 text-strong" />
63
+ }
64
+ </button>
65
+ }
66
+ @if (sw.viewAll || sw.create) {
67
+ <div class="fx-crumb-panel__rule"></div>
68
+ }
69
+ @if (sw.viewAll; as more) {
70
+ <button type="button" class="fx-crumb-panel__more" (click)="go($event, more.url)">
71
+ <fx-ui-hero-icon icon="ellipsis-horizontal" [size]="13" class="flex shrink-0 text-faint" />
72
+ {{ more.label }}
73
+ </button>
74
+ }
75
+ @if (sw.create; as create) {
76
+ <button type="button" class="fx-crumb-panel__create" (click)="go($event, create.url)">
77
+ <fx-ui-hero-icon icon="plus" [size]="14" class="flex shrink-0" />
78
+ {{ create.label }}
79
+ </button>
80
+ }
81
+ </div>
82
+ }
83
+ }
84
+ </span>
85
+ } @else if (!last && crumb.url) {
86
+ <a class="fx-crumb-link" [href]="crumb.url" (click)="go($event, crumb.url)">{{ crumb.label }}</a>
87
+ } @else {
88
+ <span class="fx-crumb-current">{{ crumb.label }}</span>
89
+ }
90
+ }
91
+ </nav>
@@ -4,6 +4,7 @@
4
4
  [attr.aria-busy]="loading() || null"
5
5
  [class]="class()"
6
6
  [ngClass]="{
7
+ 'fx-btn--icon': !!icon && !label,
7
8
  'btn-default': buttonVariant() === 'default',
8
9
  'btn-primary': buttonVariant() === 'primary',
9
10
  'btn-success': buttonVariant() === 'success',
@@ -3,7 +3,7 @@
3
3
  <button
4
4
  type="button"
5
5
  [attr.aria-pressed]="isActive(option)"
6
- class="flex items-center gap-small px-semi py-xs rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors"
6
+ class="flex items-center gap-small px-[11px] py-[4px] rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors"
7
7
  [ngClass]="pillClass(option)"
8
8
  (click)="valueChange.emit(option.value)"
9
9
  >
@@ -0,0 +1,96 @@
1
+ :host {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 8px;
5
+ min-width: 0;
6
+ }
7
+
8
+ .fx-load-status {
9
+ display: inline-flex;
10
+ align-items: center;
11
+ gap: 7px;
12
+ min-width: 0;
13
+ font-family: var(--og-font-mono);
14
+ font-size: var(--og-fs-2xs);
15
+ color: rgb(var(--og-text-muted));
16
+ white-space: nowrap;
17
+ }
18
+ .fx-load-status--idle {
19
+ color: rgb(var(--og-text-faint));
20
+ }
21
+ .fx-load-dot {
22
+ color: rgb(var(--og-positive));
23
+ }
24
+ .truncate {
25
+ overflow: hidden;
26
+ text-overflow: ellipsis;
27
+ white-space: nowrap;
28
+ min-width: 0;
29
+ }
30
+
31
+ .fx-load-spinner {
32
+ display: inline-block;
33
+ flex: none;
34
+ width: 11px;
35
+ height: 11px;
36
+ border-radius: 50%;
37
+ border: 2px solid rgb(var(--og-text) / 0.14);
38
+ border-top-color: rgb(var(--og-accent));
39
+ animation: fx-load-spin 0.8s linear infinite;
40
+ }
41
+ @keyframes fx-load-spin {
42
+ to {
43
+ transform: rotate(360deg);
44
+ }
45
+ }
46
+
47
+ .fx-load-reload {
48
+ appearance: none;
49
+ display: inline-flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ width: 28px;
53
+ height: 28px;
54
+ padding: 0;
55
+ border: 0;
56
+ border-radius: var(--og-radius-control);
57
+ background: transparent;
58
+ color: rgb(var(--og-text-muted));
59
+ cursor: pointer;
60
+ transition: background 0.12s, color 0.12s;
61
+ }
62
+ .fx-load-reload:hover {
63
+ background: rgb(var(--og-surface-sunken));
64
+ color: rgb(var(--og-text));
65
+ }
66
+
67
+ /* 2px indeterminate bar along the top edge of the positioned ancestor */
68
+ .fx-load-bar {
69
+ position: absolute;
70
+ left: 0;
71
+ right: 0;
72
+ top: 0;
73
+ height: 2px;
74
+ overflow: hidden;
75
+ background: rgb(var(--og-accent) / 0.18);
76
+ pointer-events: none;
77
+ }
78
+ .fx-load-bar__run {
79
+ width: 38%;
80
+ height: 100%;
81
+ border-radius: 2px;
82
+ background: rgb(var(--og-accent));
83
+ animation: fx-load-indet 1.15s cubic-bezier(0.4, 0, 0.2, 1) infinite;
84
+ }
85
+ @keyframes fx-load-indet {
86
+ 0% { transform: translateX(-100%); }
87
+ 60% { transform: translateX(160%); }
88
+ 100% { transform: translateX(160%); }
89
+ }
90
+
91
+ @media (prefers-reduced-motion: reduce) {
92
+ .fx-load-spinner,
93
+ .fx-load-bar__run {
94
+ animation: none;
95
+ }
96
+ }
@@ -0,0 +1,20 @@
1
+ @if (bar() && status.loading()) {
2
+ <div class="fx-load-bar" aria-hidden="true"><div class="fx-load-bar__run"></div></div>
3
+ }
4
+ <div class="fx-load-status" role="status" aria-live="polite" [class.fx-load-status--idle]="!status.loading()">
5
+ @if (status.loading()) {
6
+ <span class="fx-load-spinner" aria-hidden="true"></span>
7
+ <span class="truncate">{{ loadingText() }}</span>
8
+ } @else {
9
+ <span class="fx-load-dot" aria-hidden="true">●</span>
10
+ <span class="truncate">{{ idleText() }}</span>
11
+ }
12
+ </div>
13
+ @if (showReload()) {
14
+ <button type="button" class="fx-load-reload" [attr.aria-label]="reloadLabel()" [title]="reloadLabel()" (click)="onReload()">
15
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
16
+ <path d="M21 12a9 9 0 1 1-3.1-6.8" />
17
+ <path d="M21 3v6h-6" />
18
+ </svg>
19
+ </button>
20
+ }