@foxeltech/angular-ui 0.7.83 → 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 +40 -1
- 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 +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 +18 -1
- package/types/foxeltech-angular-ui-flow.d.ts +7 -0
- package/types/foxeltech-angular-ui.d.ts +309 -8
- package/utilities.css +2 -1
|
@@ -1,40 +1,67 @@
|
|
|
1
|
-
<!--
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
⚠️ Hướng hàng + chiều cao do CONTAINER QUERY trong CSS quyết định (theo
|
|
6
|
-
bề rộng của chính hàng), KHÔNG phải `lg:flex-row`. Đừng thêm lại class
|
|
7
|
-
`lg:*` ở đây — `lg` trong lib là 1080px và viewport là proxy sai; xem
|
|
8
|
-
ghi chú đầu master-detail.component.css. -->
|
|
1
|
+
<!-- ONE frame, three columns (designs 11/14/17): facets rail · list · detail,
|
|
2
|
+
separated by hairlines, sharing one height; only each pane's body scrolls.
|
|
3
|
+
Row direction / height come from the CONTAINER QUERY in the CSS (the row's
|
|
4
|
+
own width), never from `lg:*` — see the note at the top of the .css. -->
|
|
9
5
|
<div
|
|
10
|
-
class="fx-md-row flex flex-col items-stretch
|
|
6
|
+
class="fx-md-row flex flex-col items-stretch"
|
|
7
|
+
[class.fx-md-row--rail-open]="railOpen()"
|
|
11
8
|
[style.--fx-md-list-width]="listWidth()"
|
|
12
9
|
[style.--fx-md-pane-h]="paneHeight()"
|
|
13
10
|
>
|
|
14
|
-
<!-- Facets rail (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
<!-- Facets rail — collapsible (» / «), collapsed = 40px strip with the label
|
|
12
|
+
set vertically. The [mdFacets] projection is unconditional (hasFacets is
|
|
13
|
+
detected by querying it) and only hidden while collapsed. -->
|
|
14
|
+
<aside
|
|
15
|
+
class="fx-md-facets flex flex-col min-h-0 min-w-0"
|
|
18
16
|
[class.hidden]="!hasFacets"
|
|
17
|
+
[class.fx-md-facets--open]="railOpen()"
|
|
19
18
|
>
|
|
20
|
-
<div class="flex-
|
|
19
|
+
<div class="fx-md-rail-head flex items-center gap-small px-normal py-semi border-b border-border-subtle shrink-0" [class.hidden]="!railOpen()">
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
class="flex items-center justify-center w-6 h-6 rounded-control text-muted hover:bg-bg-hover shrink-0"
|
|
23
|
+
[attr.aria-label]="collapseFacetsLabel()"
|
|
24
|
+
(click)="railOpen.set(false)"
|
|
25
|
+
>
|
|
26
|
+
<fx-ui-hero-icon icon="chevron-double-left" [size]="14" class="flex" />
|
|
27
|
+
</button>
|
|
28
|
+
<span class="txt-eyebrow truncate">{{ facetsLabel() }}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="flex-1 min-h-0 overflow-auto px-normal py-normal" [class.hidden]="!railOpen()">
|
|
21
31
|
<ng-content select="[mdFacets]"></ng-content>
|
|
22
32
|
</div>
|
|
23
|
-
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="fx-md-rail-toggle flex items-center gap-normal text-muted hover:bg-bg-hover transition-colors"
|
|
36
|
+
[class.hidden]="railOpen()"
|
|
37
|
+
[attr.aria-label]="expandFacetsLabel()"
|
|
38
|
+
[attr.aria-expanded]="false"
|
|
39
|
+
(click)="railOpen.set(true)"
|
|
40
|
+
>
|
|
41
|
+
<fx-ui-hero-icon icon="chevron-double-right" [size]="14" class="flex shrink-0" />
|
|
42
|
+
<span class="fx-md-rail-label txt-eyebrow whitespace-nowrap">{{ facetsLabel() }}</span>
|
|
43
|
+
</button>
|
|
44
|
+
</aside>
|
|
24
45
|
|
|
25
46
|
<!-- Master: list pane -->
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
47
|
+
<section class="fx-md-list flex flex-col min-h-0 min-w-0">
|
|
48
|
+
<div class="fx-md-list-head px-large pt-large pb-small border-b border-border-subtle shrink-0">
|
|
49
|
+
<ng-content select="[mdHeader]"></ng-content>
|
|
50
|
+
<ng-content select="[mdFilters]"></ng-content>
|
|
51
|
+
</div>
|
|
29
52
|
<div
|
|
30
|
-
class="flex flex-col
|
|
53
|
+
class="flex flex-col overflow-auto flex-1 min-h-0"
|
|
31
54
|
role="listbox"
|
|
32
55
|
[attr.aria-label]="listLabel() || null"
|
|
33
56
|
>
|
|
34
57
|
@if (table.loading) {
|
|
35
|
-
<
|
|
58
|
+
<div class="px-large py-normal">
|
|
59
|
+
<fx-ui-skeleton-list [rows]="table.pageSize"></fx-ui-skeleton-list>
|
|
60
|
+
</div>
|
|
36
61
|
} @else {
|
|
37
62
|
@for (row of table.dataSource.data; track table.idOf(row); let i = $index) {
|
|
63
|
+
<!-- Hairline rows; the selected one is tinted with an accent bar on
|
|
64
|
+
the left (design 11/14 selected finding / component). -->
|
|
38
65
|
<button
|
|
39
66
|
type="button"
|
|
40
67
|
role="option"
|
|
@@ -43,11 +70,11 @@
|
|
|
43
70
|
[tabindex]="isSelected(row) ? 0 : -1"
|
|
44
71
|
(click)="table.selectItem(row)"
|
|
45
72
|
(keydown)="onKeydown($event, i)"
|
|
46
|
-
class="text-left
|
|
73
|
+
class="text-left px-large py-semi border-l-2 border-b border-b-border-subtle transition-colors"
|
|
47
74
|
[ngClass]="
|
|
48
75
|
isSelected(row)
|
|
49
|
-
? 'border-accent bg-accent-soft/60'
|
|
50
|
-
: 'border-
|
|
76
|
+
? 'border-l-accent bg-accent-soft/60'
|
|
77
|
+
: 'border-l-transparent hover:bg-surface-sunken'
|
|
51
78
|
"
|
|
52
79
|
>
|
|
53
80
|
<ng-container
|
|
@@ -57,7 +84,9 @@
|
|
|
57
84
|
</button>
|
|
58
85
|
}
|
|
59
86
|
@if (!table.dataSource.data.length) {
|
|
60
|
-
<
|
|
87
|
+
<div class="px-large py-large">
|
|
88
|
+
<fx-ui-empty-state [message]="emptyMessage()"></fx-ui-empty-state>
|
|
89
|
+
</div>
|
|
61
90
|
}
|
|
62
91
|
}
|
|
63
92
|
</div>
|
|
@@ -65,14 +94,14 @@
|
|
|
65
94
|
whose own BaseTable subscription (running after this child's hook)
|
|
66
95
|
wires page events — a binding here would double-fetch every change. -->
|
|
67
96
|
<mat-paginator
|
|
68
|
-
class="
|
|
97
|
+
class="fx-md-paginator"
|
|
69
98
|
[class.fx-md-paginator-hidden]="paginationStyle() === 'prevnext'"
|
|
70
99
|
[length]="table.total"
|
|
71
100
|
[pageSize]="table.pageSize"
|
|
72
101
|
[pageSizeOptions]="pageSizeOptions()"
|
|
73
102
|
></mat-paginator>
|
|
74
103
|
@if (paginationStyle() === 'prevnext') {
|
|
75
|
-
<div class="flex items-center justify-between
|
|
104
|
+
<div class="flex items-center justify-between gap-normal px-large py-normal border-t border-border-subtle shrink-0">
|
|
76
105
|
<span class="txt-utility-description" style="font-family: var(--og-font-figure)">
|
|
77
106
|
{{ rangeText() }}
|
|
78
107
|
</span>
|
|
@@ -96,25 +125,25 @@
|
|
|
96
125
|
</div>
|
|
97
126
|
</div>
|
|
98
127
|
}
|
|
99
|
-
</
|
|
128
|
+
</section>
|
|
100
129
|
|
|
101
130
|
<!-- Detail pane -->
|
|
102
|
-
<
|
|
103
|
-
<div class="flex-1 min-h-0 overflow-auto">
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
131
|
+
<section class="fx-md-detail w-full min-w-0 flex flex-col min-h-0">
|
|
132
|
+
<div class="flex-1 min-h-0 overflow-auto px-large py-large">
|
|
133
|
+
@if (table.selected) {
|
|
134
|
+
@if (table.detailBusy) {
|
|
135
|
+
<fx-ui-skeleton-detail [cards]="skeletonCards()"></fx-ui-skeleton-detail>
|
|
136
|
+
} @else {
|
|
137
|
+
<ng-container
|
|
138
|
+
[ngTemplateOutlet]="detailTemplate()"
|
|
139
|
+
[ngTemplateOutletContext]="{ $implicit: table.detail, selected: table.selected }"
|
|
140
|
+
></ng-container>
|
|
141
|
+
}
|
|
107
142
|
} @else {
|
|
108
|
-
<
|
|
109
|
-
[
|
|
110
|
-
|
|
111
|
-
></ng-container>
|
|
143
|
+
<div class="flex items-center justify-center h-full min-h-[300px]">
|
|
144
|
+
<fx-ui-empty-state [message]="emptyMessage()"></fx-ui-empty-state>
|
|
145
|
+
</div>
|
|
112
146
|
}
|
|
113
|
-
} @else {
|
|
114
|
-
<div class="card-common flex items-center justify-center h-full min-h-[300px]">
|
|
115
|
-
<fx-ui-empty-state [message]="emptyMessage()"></fx-ui-empty-state>
|
|
116
|
-
</div>
|
|
117
|
-
}
|
|
118
147
|
</div>
|
|
119
|
-
</
|
|
148
|
+
</section>
|
|
120
149
|
</div>
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
[style.transform]="thumbTransform()"
|
|
15
15
|
></span>
|
|
16
16
|
}
|
|
17
|
+
<!-- option 30px tall (7px × 18px padding): the text-box trim below collapses the
|
|
18
|
+
glyph box to ~8px, so padding alone left the pill at 22px (anh Tú 04/09) -->
|
|
17
19
|
@for (option of options(); track option.value) {
|
|
18
20
|
<button
|
|
19
21
|
#opt
|
|
@@ -21,7 +23,7 @@
|
|
|
21
23
|
(click)="select(option)"
|
|
22
24
|
[disabled]="option.disabled"
|
|
23
25
|
[attr.aria-pressed]="option.value === value()"
|
|
24
|
-
class="relative z-[1] flex min-w-0 items-center justify-center gap-xs px-
|
|
26
|
+
class="relative z-[1] flex min-w-0 min-h-[30px] items-center justify-center gap-xs px-[18px] py-[7px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
25
27
|
[class]="optionClass(option)"
|
|
26
28
|
>
|
|
27
29
|
@if (option.icon) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
@for (badge of badges; track badge.key) {
|
|
3
3
|
<button
|
|
4
4
|
type="button"
|
|
5
|
-
class="px-2 py-
|
|
5
|
+
class="px-2 py-[3px] rounded-full txt-utility-caption font-medium cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50"
|
|
6
6
|
[ngClass]="badgeClass(badge)"
|
|
7
7
|
[attr.aria-label]="badge.key + ': ' + badge.label"
|
|
8
8
|
(click)="badgeClick.emit(badge.key)"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
container-type: inline-size;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/* Explicit `cols` only holds from md (720px) up; narrower containers fall back
|
|
7
|
+
to 2-up, then 1-up — figures stay readable on phones (anh Tú 04/09). The
|
|
8
|
+
inline style carries the wide layout, so the overrides need !important. */
|
|
9
|
+
@container (width < 720px) {
|
|
10
|
+
.fx-stat-grid {
|
|
11
|
+
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
@container (width < 360px) {
|
|
15
|
+
.fx-stat-grid {
|
|
16
|
+
grid-template-columns: minmax(0, 1fr) !important;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div class="grid gap-normal" [ngStyle]="{ 'grid-template-columns': gridColumns }">
|
|
1
|
+
<div class="fx-stat-grid grid gap-normal" [ngStyle]="{ 'grid-template-columns': gridColumns }">
|
|
2
2
|
@for (card of cards(); track card.label) {
|
|
3
3
|
@if (variant() === 'figure') {
|
|
4
4
|
<div class="card-common flex flex-col gap-small min-w-0">
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
class="txt-heading-03"
|
|
8
8
|
[ngClass]="numClass(card)"
|
|
9
9
|
style="font-family: var(--og-font-figure)"
|
|
10
|
-
|
|
10
|
+
><span [fxCountUp]="card.value"></span></span
|
|
11
11
|
>
|
|
12
12
|
@if (card.sub) {
|
|
13
13
|
<span class="txt-utility-caption" [ngClass]="subClass(card)">{{ card.sub }}</span>
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
class="txt-heading-04"
|
|
33
33
|
[ngClass]="numClass(card)"
|
|
34
34
|
style="font-family: var(--og-font-figure)"
|
|
35
|
-
|
|
35
|
+
><span [fxCountUp]="card.value"></span></span
|
|
36
36
|
>
|
|
37
37
|
@if (card.delta) {
|
|
38
38
|
<span class="text-xs font-semibold" [ngClass]="deltaClass(card)">
|
|
@@ -37,6 +37,8 @@ declare class ChartComponent implements OnChanges, AfterViewInit, OnDestroy {
|
|
|
37
37
|
* dom.". Chỉ dựng directive khi đã có options thật ⇒ đúng một lần init.
|
|
38
38
|
*/
|
|
39
39
|
hasOptions: boolean;
|
|
40
|
+
/** Heights (%) of the pre-init placeholder bars — a fixed, calm silhouette. */
|
|
41
|
+
readonly placeholderBars: number[];
|
|
40
42
|
labelFontSize: number;
|
|
41
43
|
private chartInstance?;
|
|
42
44
|
private resizeObserver?;
|
|
@@ -136,5 +138,20 @@ declare class ChartComponent implements OnChanges, AfterViewInit, OnDestroy {
|
|
|
136
138
|
static ɵcmp: _angular_core.ɵɵComponentDeclaration<ChartComponent, "fx-ui-chart", never, { "type": { "alias": "type"; "required": false; "isSignal": true; }; "data": { "alias": "data"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "palette": { "alias": "palette"; "required": false; "isSignal": true; }; "height": { "alias": "height"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "pieLegendAlign": { "alias": "pieLegendAlign"; "required": false; "isSignal": true; }; "pieLegendPosition": { "alias": "pieLegendPosition"; "required": false; "isSignal": true; }; "showPieLabel": { "alias": "showPieLabel"; "required": false; "isSignal": true; }; "showPieLegend": { "alias": "showPieLegend"; "required": false; "isSignal": true; }; "pieCenterLabel": { "alias": "pieCenterLabel"; "required": false; "isSignal": true; }; "mobileWidth": { "alias": "mobileWidth"; "required": false; "isSignal": true; }; "labelFontFamily": { "alias": "labelFontFamily"; "required": false; "isSignal": true; }; "emptyText": { "alias": "emptyText"; "required": false; "isSignal": true; }; }, { "chartClick": "chartClick"; "chartInit": "chartInit"; }, never, never, true, never>;
|
|
137
139
|
}
|
|
138
140
|
|
|
139
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Warm the echarts chunk before any chart is on screen.
|
|
143
|
+
*
|
|
144
|
+
* `fx-ui-chart` loads echarts lazily (`provideEchartsCore({ echarts: () =>
|
|
145
|
+
* import('echarts') })`), so the FIRST chart of a session pays the chunk
|
|
146
|
+
* download right when the user is looking at an empty panel. Call this once
|
|
147
|
+
* from an idle callback when the app's env injector is created (e.g. a
|
|
148
|
+
* `provideEnvironmentInitializer` in the remote's root route) and the dashboard
|
|
149
|
+
* charts appear on the frame they mount. Same module URL as the directive's
|
|
150
|
+
* import ⇒ one shared instance, no double load.
|
|
151
|
+
*/
|
|
152
|
+
declare function preloadEcharts(): Promise<unknown>;
|
|
153
|
+
/** Schedule `preloadEcharts()` when the main thread is idle (SSR/tests: no-op). */
|
|
154
|
+
declare function preloadEchartsWhenIdle(timeoutMs?: number): void;
|
|
155
|
+
|
|
156
|
+
export { ChartComponent, preloadEcharts, preloadEchartsWhenIdle };
|
|
140
157
|
export type { IChartType };
|
|
@@ -52,6 +52,13 @@ declare class FlowCanvasComponent {
|
|
|
52
52
|
/** Caller-translated hint shown when the graph is empty. */
|
|
53
53
|
readonly emptyText: _angular_core.InputSignal<string>;
|
|
54
54
|
readonly nodeClick: _angular_core.OutputEmitterRef<string>;
|
|
55
|
+
private readonly flow;
|
|
56
|
+
private readonly host;
|
|
57
|
+
private readonly destroyRef;
|
|
58
|
+
constructor();
|
|
59
|
+
private fitHandle;
|
|
60
|
+
/** Coalesces fit requests into one frame after vflow has measured its nodes. */
|
|
61
|
+
private scheduleFit;
|
|
55
62
|
/** Column index per node: pinned value, else longest path from a source. */
|
|
56
63
|
private readonly columns;
|
|
57
64
|
/** ngx-vflow nodes: html-template cards positioned by the derived layout. */
|