@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.
- package/components.css +66 -3
- package/fesm2022/foxeltech-angular-ui-chart.mjs +33 -3
- package/fesm2022/foxeltech-angular-ui-chart.mjs.map +1 -1
- package/fesm2022/foxeltech-angular-ui-flow.mjs +42 -5
- package/fesm2022/foxeltech-angular-ui-flow.mjs.map +1 -1
- package/fesm2022/foxeltech-angular-ui.mjs +641 -60
- package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
- package/package.json +3 -2
- package/src/lib/styles/components.css +66 -3
- package/src/lib/styles/utilities.css +8 -2
- package/src/lib/ui/components/breadcrumb/breadcrumb.component.css +262 -0
- package/src/lib/ui/components/breadcrumb/breadcrumb.component.html +91 -0
- package/src/lib/ui/components/button/button.component.html +1 -0
- package/src/lib/ui/components/filter-pills/filter-pills.component.html +1 -1
- package/src/lib/ui/components/load-status/load-status.component.css +96 -0
- package/src/lib/ui/components/load-status/load-status.component.html +20 -0
- package/src/lib/ui/components/master-detail/master-detail.component.css +63 -11
- package/src/lib/ui/components/master-detail/master-detail.component.html +71 -42
- package/src/lib/ui/components/pending-badge/pending-badge.component.html +2 -2
- package/src/lib/ui/components/project-card/project-card.component.html +7 -3
- package/src/lib/ui/components/route-skeleton/route-skeleton.component.css +1 -1
- package/src/lib/ui/components/sample-frame/sample-frame.component.html +6 -7
- package/src/lib/ui/components/section-card/section-card.component.html +3 -1
- package/src/lib/ui/components/segmented/segmented.component.html +6 -2
- package/src/lib/ui/components/severity-badges/severity-badges.component.html +1 -1
- package/src/lib/ui/components/stat-cards/stat-cards.component.css +18 -0
- package/src/lib/ui/components/stat-cards/stat-cards.component.html +3 -3
- package/types/foxeltech-angular-ui-chart.d.ts +18 -1
- package/types/foxeltech-angular-ui-flow.d.ts +7 -0
- package/types/foxeltech-angular-ui.d.ts +349 -14
- 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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
43
|
+
/* design .og-badge: 3px 10px (anh Tú 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>
|
|
@@ -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-
|
|
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
|
+
}
|