@foxeltech/angular-ui 0.7.83 → 0.7.103
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 +40 -1
- package/fesm2022/foxeltech-angular-ui-chart.mjs +127 -44
- 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 +559 -42
- package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
- package/package.json +3 -2
- package/src/lib/styles/components.css +40 -1
- package/src/lib/styles/utilities.css +2 -1
- 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/route-skeleton/route-skeleton.component.css +1 -1
- package/src/lib/ui/components/segmented/segmented.component.html +3 -1
- 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 +56 -22
- package/types/foxeltech-angular-ui-flow.d.ts +7 -0
- package/types/foxeltech-angular-ui.d.ts +309 -8
- package/utilities.css +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foxeltech/angular-ui",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.103",
|
|
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",
|
|
@@ -361,6 +361,14 @@ 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. */
|
|
367
|
+
.fx-btn--icon {
|
|
368
|
+
width: 2rem;
|
|
369
|
+
padding-left: 0;
|
|
370
|
+
padding-right: 0;
|
|
371
|
+
}
|
|
364
372
|
/* Nút hành động chính theo demo (`.bi-newbtn`): màu cố định, không phải accent. */
|
|
365
373
|
/* ---- Tông severity theo ĐÚNG mô hình demo (đo 03/09) -----------------
|
|
366
374
|
Demo KHÔNG tô badge bằng màu rực. Nó dùng token semantic, và bậc nặng
|
|
@@ -755,6 +763,17 @@ input[type='password'] {
|
|
|
755
763
|
max-height: calc(100vh - 315px);
|
|
756
764
|
overflow: auto;
|
|
757
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
|
+
}
|
|
758
777
|
.table-cell-padding {
|
|
759
778
|
padding-top: 8px !important;
|
|
760
779
|
padding-bottom: 8px !important;
|
|
@@ -790,8 +809,28 @@ input[type='password'] {
|
|
|
790
809
|
rgb(var(--gradient-primary-end))
|
|
791
810
|
) border-box;
|
|
792
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. */
|
|
793
816
|
.skeleton-load {
|
|
794
|
-
|
|
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); }
|
|
795
834
|
}
|
|
796
835
|
|
|
797
836
|
/* ============================================================================
|
|
@@ -40,5 +40,6 @@
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
@utility tag-common {
|
|
43
|
-
|
|
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;
|
|
44
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
|
+
}
|
|
@@ -28,6 +28,29 @@
|
|
|
28
28
|
container-name: fx-md;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* The frame — one card around all three panes (design 11/14/17); the panes
|
|
32
|
+
themselves are flat and separated by hairlines. Shadow = demo .og-panel. */
|
|
33
|
+
.fx-md-row {
|
|
34
|
+
background: rgb(var(--og-surface));
|
|
35
|
+
border: 1px solid rgb(var(--border-default));
|
|
36
|
+
border-radius: 0.5rem;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
box-shadow:
|
|
39
|
+
0 1px 2px rgb(0 0 0 / 0.04),
|
|
40
|
+
0 3px 8px rgb(0 0 0 / 0.05);
|
|
41
|
+
}
|
|
42
|
+
/* Stacked (narrow): hairline BELOW each pane; rail toggle is a horizontal strip. */
|
|
43
|
+
.fx-md-facets,
|
|
44
|
+
.fx-md-list {
|
|
45
|
+
border-bottom: 1px solid rgb(var(--og-border-subtle));
|
|
46
|
+
}
|
|
47
|
+
.fx-md-rail-toggle {
|
|
48
|
+
width: 100%;
|
|
49
|
+
padding: 10px 12px;
|
|
50
|
+
flex-direction: row;
|
|
51
|
+
justify-content: flex-start;
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
/* Mặc định: xếp dọc, mỗi cụm full width, chiều cao tự nhiên.
|
|
32
55
|
KHÔNG ép chiều cao khi xếp dọc — sẽ sinh ba hộp cuộn tí hon lồng trong
|
|
33
56
|
trang đang cuộn, tệ hơn hẳn. */
|
|
@@ -37,15 +60,13 @@
|
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
/*
|
|
40
|
-
* Ngưỡng
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* 987px — trượt ngưỡng 1000 chỉ vì 13px dù thừa sức xếp 3 cụm ở mức sàn
|
|
46
|
-
* (đo được: facets 200 / list 320 / detail 419).
|
|
63
|
+
* Ngưỡng 720px = `md` của lib (anh Tú 04/09: mọi ngưỡng layout theo `md`,
|
|
64
|
+
* không còn `lg` 1080). Từ 0.7.85 khung là MỘT mảnh, rail thu gọn còn 40px,
|
|
65
|
+
* nên 720 vẫn đủ: rail 40 + list ≥ 300 + detail ≥ 360. Rail mở ở bề rộng
|
|
66
|
+
* hẹp thì list nhường chỗ (min 260) — người dùng tự đóng rail nếu cần.
|
|
67
|
+
* Dưới 720 thật sự không đủ chỗ → xếp dọc là đúng, không phải lỗi.
|
|
47
68
|
*/
|
|
48
|
-
@container fx-md (min-width:
|
|
69
|
+
@container fx-md (min-width: 720px) {
|
|
49
70
|
.fx-md-row {
|
|
50
71
|
flex-direction: row;
|
|
51
72
|
height: var(--fx-md-pane-h, calc(100vh - 300px));
|
|
@@ -55,22 +76,47 @@
|
|
|
55
76
|
/* Panes CO GIÃN: facets/list nhường chỗ để cụm detail luôn có sàn 400px.
|
|
56
77
|
`--fx-md-list-width` (input `listWidth`) giờ là bề rộng MONG MUỐN kiêm
|
|
57
78
|
tối đa của cụm list, không còn là con số cứng. */
|
|
79
|
+
/* Hairline to the RIGHT of rail and list instead of below. */
|
|
80
|
+
.fx-md-facets,
|
|
81
|
+
.fx-md-list {
|
|
82
|
+
border-bottom: 0;
|
|
83
|
+
border-right: 1px solid rgb(var(--og-border-subtle));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Rail: 40px strip when collapsed (» + vertical label), 230px when open. */
|
|
58
87
|
.fx-md-facets {
|
|
88
|
+
flex: 0 0 40px;
|
|
89
|
+
width: 40px;
|
|
90
|
+
min-width: 40px;
|
|
91
|
+
max-width: 40px;
|
|
92
|
+
}
|
|
93
|
+
.fx-md-facets--open {
|
|
59
94
|
flex: 0 1 230px;
|
|
95
|
+
width: auto;
|
|
60
96
|
min-width: 200px;
|
|
61
97
|
max-width: 260px;
|
|
62
98
|
}
|
|
99
|
+
.fx-md-rail-toggle {
|
|
100
|
+
height: 100%;
|
|
101
|
+
padding: 12px 0;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
justify-content: flex-start;
|
|
104
|
+
}
|
|
105
|
+
.fx-md-rail-label {
|
|
106
|
+
writing-mode: vertical-rl;
|
|
107
|
+
transform: rotate(180deg);
|
|
108
|
+
}
|
|
63
109
|
|
|
64
110
|
.fx-md-list {
|
|
65
111
|
width: auto;
|
|
66
112
|
flex: 0 1 var(--fx-md-list-width, 420px);
|
|
67
|
-
min-width:
|
|
113
|
+
min-width: 260px;
|
|
68
114
|
max-width: var(--fx-md-list-width, 420px);
|
|
69
115
|
}
|
|
70
116
|
|
|
71
117
|
.fx-md-detail {
|
|
72
|
-
flex: 1 1
|
|
73
|
-
min-width:
|
|
118
|
+
flex: 1 1 360px;
|
|
119
|
+
min-width: 360px;
|
|
74
120
|
}
|
|
75
121
|
}
|
|
76
122
|
|
|
@@ -83,3 +129,9 @@
|
|
|
83
129
|
overflow: hidden;
|
|
84
130
|
clip-path: inset(50%);
|
|
85
131
|
}
|
|
132
|
+
|
|
133
|
+
/* Material paginator inside the list pane keeps the pane's horizontal rhythm. */
|
|
134
|
+
.fx-md-paginator {
|
|
135
|
+
padding: 0 12px;
|
|
136
|
+
border-top: 1px solid rgb(var(--og-border-subtle));
|
|
137
|
+
}
|