@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
@@ -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 968px = ĐÚNG tổng bề rộng tối thiểu dùng được của ba cụm:
41
- * facets 200 + list 320 + detail 400 + 2 gap (~48) = 968px
42
- * Dưới ngưỡng thì thật sự không đủ chỗ xếp dọc đúng, không phải lỗi.
43
- *
44
- * ⚠️ Đừng làm tròn lên 1000px: viewport 1280 (rất phổ biến) cho hàng đúng
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 04/09: mọi ngưỡng layout theo `md`,
64
+ * không còn `lg` 1080). Từ 0.7.85 khung 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 đúng, không phải lỗi.
47
68
  */
48
- @container fx-md (min-width: 968px) {
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: 320px;
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 400px;
73
- min-width: 400px;
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
+ }
@@ -1,40 +1,67 @@
1
- <!-- 3 cụm CHUNG chiều cao, mỗi cụm SCROLL RIÊNG (anh chốt 03/09).
2
- `items-stretch` + chiều cao đặt trên hàng (xem .fx-md-row trong CSS) để
3
- cả ba pane bằng nhau; mỗi pane flex-col + min-h-0 chỉ phần thân
4
- `overflow-auto flex-1` cuộn header/filter/paginator đứng yên.
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 gap-xl"
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 (V2 3-column lookup) wrapper always rendered so ng-content
15
- stays unconditional; hidden when the host projects nothing. -->
16
- <div
17
- class="fx-md-facets card-common bg-surface flex flex-col min-h-0"
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-1 min-h-0 overflow-auto pr-xs">
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
- </div>
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
- <div class="fx-md-list card-common flex flex-col min-h-0">
27
- <ng-content select="[mdHeader]"></ng-content>
28
- <ng-content select="[mdFilters]"></ng-content>
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 gap-normal overflow-auto pr-xs flex-1 min-h-0"
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
- <fx-ui-skeleton-list [rows]="table.pageSize"></fx-ui-skeleton-list>
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 rounded-lg border p-semi transition-colors"
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-border-default hover:border-border-interactive bg-bg-primary'
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
- <fx-ui-empty-state [message]="emptyMessage()"></fx-ui-empty-state>
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="mt-normal"
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 mt-normal">
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
- </div>
128
+ </section>
100
129
 
101
130
  <!-- Detail pane -->
102
- <div class="fx-md-detail w-full min-w-0 flex flex-col min-h-0">
103
- <div class="flex-1 min-h-0 overflow-auto">
104
- @if (table.selected) {
105
- @if (table.detailBusy) {
106
- <fx-ui-skeleton-detail [cards]="skeletonCards()"></fx-ui-skeleton-detail>
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
- <ng-container
109
- [ngTemplateOutlet]="detailTemplate()"
110
- [ngTemplateOutletContext]="{ $implicit: table.detail, selected: table.selected }"
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
- </div>
148
+ </section>
120
149
  </div>
@@ -1,12 +1,12 @@
1
1
  @if (variant() === 'dot') {
2
2
  <span
3
- class="inline-block w-[7px] h-[7px] rounded-full bg-warning/70 align-middle"
3
+ class="inline-block w-[7px] h-[7px] rounded-full bg-muted/60 align-middle"
4
4
  [attr.title]="tooltip"
5
5
  [attr.aria-label]="tooltip"
6
6
  ></span>
7
7
  } @else {
8
8
  <span
9
- class="inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap align-middle"
9
+ class="inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-border-subtle bg-neutral-soft text-muted txt-tag-label whitespace-nowrap align-middle"
10
10
  [attr.title]="tooltip"
11
11
  >
12
12
  <fx-ui-hero-icon icon="clock" [size]="11" class="flex" />
@@ -1,6 +1,6 @@
1
1
  <button
2
2
  type="button"
3
- class="card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all"
3
+ class="card-common bg-surface w-full flex-1 flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all"
4
4
  (click)="open.emit(card())"
5
5
  >
6
6
  <!-- Header: avatar + name/sub + score chip (design 12) -->
@@ -13,7 +13,10 @@
13
13
  >{{ card().avatar }}</span
14
14
  >
15
15
  }
16
- <div class="min-w-0">
16
+ <!-- min-h 46px = name (24) + mt-xs (2) + sub (20): reserves the two-line
17
+ header even when a card has no sub line, so trend/metric rows line
18
+ up across the grid row (measured 04/09). -->
19
+ <div class="min-w-0 min-h-[46px]">
17
20
  <div class="txt-heading-06 truncate text-left">{{ card().name }}</div>
18
21
  @if (card().sub; as sub) {
19
22
  <div class="flex items-center gap-xs txt-utility-description mt-xs">
@@ -88,8 +91,9 @@
88
91
  }
89
92
 
90
93
  @if (card().meta || card().openLabel) {
94
+ <!-- mt-auto: footers line up across a grid row whatever each card holds -->
91
95
  <div
92
- class="flex items-center justify-between w-full pt-normal border-t border-border-default"
96
+ class="flex items-center justify-between w-full pt-normal mt-auto border-t border-border-default"
93
97
  >
94
98
  <span class="txt-utility-description text-left">{{ card().meta }}</span>
95
99
  @if (card().openLabel) {
@@ -16,7 +16,7 @@
16
16
 
17
17
  /* Ngưỡng + bề rộng panes giữ y hệt master-detail để skeleton không nhảy hình
18
18
  khi dữ liệu về. */
19
- @container fx-rs (min-width: 968px) {
19
+ @container fx-rs (min-width: 720px) {
20
20
  .fx-rs-row {
21
21
  flex-direction: row;
22
22
  align-items: stretch;
@@ -1,10 +1,9 @@
1
- <div class="relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal">
2
- <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line
3
- instead of the label breaking mid-word ("SAMPLE / LAYOUT"). -->
4
- <div class="flex flex-wrap items-center gap-small mb-normal">
5
- <span class="txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap">{{ label() }}</span>
1
+ <div class="relative flex flex-col flex-1 min-h-0">
2
+ <!-- One quiet chip marks the block as sample data; the tooltip names the
3
+ missing field. flex-wrap so the chip drops a line in narrow rails. -->
4
+ <div class="flex flex-wrap items-center gap-small mb-small">
6
5
  <span
7
- class="inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap"
6
+ class="inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-border-subtle bg-neutral-soft text-muted txt-tag-label whitespace-nowrap"
8
7
  [attr.title]="tooltip"
9
8
  >
10
9
  <fx-ui-hero-icon icon="clock" [size]="11" class="flex" />
@@ -12,7 +11,7 @@
12
11
  </span>
13
12
  </div>
14
13
  <!-- Sample content is non-interactive: nothing here reflects real state. -->
15
- <div class="pointer-events-none select-none opacity-90">
14
+ <div class="pointer-events-none select-none opacity-90 flex-1 min-h-0 flex flex-col">
16
15
  <ng-content></ng-content>
17
16
  </div>
18
17
  </div>
@@ -1,4 +1,6 @@
1
- <div class="card-common">
1
+ <!-- flex column filling the host: cards in one grid row share a height and a
2
+ projected footer can sit at the bottom with `mt-auto` (design 01). -->
3
+ <div class="card-common bg-surface flex flex-col flex-1 min-h-0">
2
4
  @if (titleStyle() === 'eyebrow') {
3
5
  <div class="flex items-baseline justify-between gap-normal mb-large">
4
6
  <span class="txt-eyebrow">{{ title() }}</span>
@@ -1,5 +1,6 @@
1
1
  <div
2
- class="relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full"
2
+ class="relative inline-grid max-w-full grid-flow-col items-stretch p-[3px] bg-surface-sunken border border-border rounded-full"
3
+ [class]="gridClass()"
3
4
  role="group"
4
5
  [attr.aria-label]="ariaLabel()"
5
6
  >
@@ -13,13 +14,16 @@
13
14
  [style.transform]="thumbTransform()"
14
15
  ></span>
15
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) -->
16
19
  @for (option of options(); track option.value) {
17
20
  <button
21
+ #opt
18
22
  type="button"
19
23
  (click)="select(option)"
20
24
  [disabled]="option.disabled"
21
25
  [attr.aria-pressed]="option.value === value()"
22
- class="relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
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"
23
27
  [class]="optionClass(option)"
24
28
  >
25
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-0.5 rounded-full txt-utility-caption font-medium cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50"
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
- >{{ card.value }}</span
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
- >{{ card.value }}</span
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
- export { ChartComponent };
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. */