@foxeltech/angular-ui 0.7.106 → 0.7.124

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.
@@ -14,16 +14,24 @@
14
14
  hàng brand nằm ngoài container, nếu tô ở container thì 60px trên cùng
15
15
  lộ nền của trang. Sáng: drawer xám, nội dung trắng; tối: drawer tối hơn
16
16
  surface — đúng quy ước sidebar của cả hai chiều. */
17
- @apply relative h-full border-r border-border-default flex flex-col bg-page;
17
+ /* 0.7.107 (anh 15/09, reference HTML "Binary Intelligence Dashboard"):
18
+ the demo's `.og-sidebar` is `--og-surface-sunken` (#fbfbfa on porcelain)
19
+ with a `--og-border-subtle` right edge — not the page's `--og-page-bg`
20
+ (#f2f2f1). Was overridden from platform-host's styles.scss with doubled
21
+ selectors; it lives here now so all five apps agree. */
22
+ @apply relative h-full flex flex-col;
23
+ background: rgb(var(--og-surface-sunken));
24
+ border-right: 1px solid rgb(var(--og-border-subtle));
18
25
  }
19
26
 
20
27
  /* Brand row — phải thẳng hàng với icon của nav item.
21
- Icon nav nằm ở 24px tính từ mép drawer: 8px của .drawer-container (px-2)
22
- + 16px padding-left inline của .drawer-item. Brand nằm NGOÀI container
23
- nên phải tự cộng đủ 24px (trước đây p-4 + ml-5 = 36px → lệch 12px). */
28
+ Icon nav nằm ở 16px tính từ mép drawer (0.7.107, theo reference: 8px của
29
+ .drawer-container (px-2) + 8px padding-left inline của .drawer-item
30
+ trước đó 16px, tức 24px tổng). Brand nằm NGOÀI container nên phải tự
31
+ cộng đủ 16px. */
24
32
  .drawer-brand {
25
- @apply h-[60px] shrink-0 flex items-center gap-3 no-underline overflow-hidden;
26
- padding-left: 24px;
33
+ @apply h-[60px] shrink-0 flex items-center gap-[10px] no-underline overflow-hidden;
34
+ padding-left: 16px;
27
35
  padding-right: 8px;
28
36
  transition: padding 300ms cubic-bezier(0.4, 0, 0.2, 1);
29
37
  /* max-width là BẮT BUỘC: .drawer-host-wrapper là flex column có width auto
@@ -43,8 +51,38 @@
43
51
  }
44
52
  }
45
53
 
54
+ /* Sunken tile behind the mark (reference `.sb-logo`, measured 16/09: 6px pad,
55
+ radius-control, --og-surface-sunken). The border is declared TRANSPARENT and
56
+ only takes a colour on hover — declaring it here rather than adding one on
57
+ :hover is what keeps the row from shifting 1px when the pointer arrives. */
58
+ .drawer-brand-logo {
59
+ @apply inline-flex items-center justify-center shrink-0;
60
+ padding: 6px;
61
+ border-radius: var(--og-radius-control);
62
+ background: rgb(var(--og-surface-sunken));
63
+ border: 1px solid transparent;
64
+ transition:
65
+ padding 220ms cubic-bezier(0.4, 0, 0.2, 1),
66
+ border-color 150ms ease;
67
+ }
68
+
69
+ /* The whole brand row is one link, so the tile lights up for a hover anywhere
70
+ on it — the reference's `.sb-logo:hover` sits on a non-clickable span, where
71
+ pointer-over-the-mark is the only hover there is. */
72
+ .drawer-brand:hover .drawer-brand-logo {
73
+ border-color: rgb(var(--og-border));
74
+ }
75
+
76
+ .drawer-brand.is-collapsed .drawer-brand-logo {
77
+ padding: 4px;
78
+ }
79
+
46
80
  .drawer-brand-mark {
47
- @apply h-[20px] w-auto shrink-0;
81
+ @apply h-[21px] w-auto shrink-0;
82
+ }
83
+
84
+ .drawer-brand.is-collapsed .drawer-brand-mark {
85
+ @apply h-[19px];
48
86
  }
49
87
 
50
88
  /* Cột chữ: title + subtitle xếp dọc. min-w-0 để truncate hoạt động khi
@@ -55,21 +93,26 @@
55
93
 
56
94
  /* Wordmark: dùng CHỮ THẬT chứ không phải ảnh text_logo.png — ảnh đó là
57
95
  navy đặc nên chìm hẳn trên theme dark; chữ ăn --og-text nên đúng cả 6. */
96
+ /* Reference: --og-fs-lg / weight 500 / -0.015em / line-height 1 (measured 16/09).
97
+ It was 600 at -0.01em, which read a notch heavier and wider than the design. */
58
98
  .drawer-brand-text {
59
- @apply truncate text-[15px] font-semibold text-text-primary select-none;
99
+ @apply truncate text-[length:var(--og-fs-lg)] font-medium text-text-primary select-none;
60
100
  font-family: var(--og-font-display);
61
- letter-spacing: -0.01em;
101
+ letter-spacing: -0.015em;
102
+ line-height: 1;
62
103
  }
63
104
 
64
- /* Subtitle: 11px muted, cùng cỡ với .drawer-version caption
65
- ADMINISTRATION nên không tự sinh ra một bậc typography mới. */
66
- /* line-clamp-2 chứ không phải truncate: tagline dài (VD "Binary Exploit
67
- Intelligence Platform") không lọt một dòng trong cột chữ ~188px của drawer
68
- 260px, truncate sẽ cắt mất chữ. Hai dòng vẫn nằm gọn trong 60px của
69
- .drawer-brand (18.75 + 1 + 27.5 = 47px). */
105
+ /* Subtitle: 9px / weight 400 / +0.02em / line-height 1.2, --og-text-muted
106
+ (reference, measured 16/09).
107
+ MỘT DÒNG truncate chứ không còn line-clamp-2: 11px thì "Binary Exploit
108
+ Intelligence Platform" không lọt cột chữ ~188px nên phải cho xuống hai dòng;
109
+ 9px lọt (~157px), một dòng mới đúng bản gốc. Đổi cỡ mà quên đổi chỗ
110
+ này thì subtitle lại tự ngắt dòng. */
70
111
  .drawer-brand-subtext {
71
- @apply line-clamp-2 text-[11px] text-text-placeholder select-none mt-[1px];
72
- letter-spacing: var(--og-tracking-label);
112
+ @apply truncate text-[9px] font-normal select-none mt-[2px];
113
+ color: rgb(var(--og-text-muted));
114
+ letter-spacing: 0.02em;
115
+ line-height: 1.2;
73
116
  }
74
117
 
75
118
  .drawer-container {
@@ -82,14 +125,30 @@
82
125
  .toggle-btn {
83
126
  @apply absolute top-1/2 -translate-y-1/2 z-50 flex items-center justify-center w-6 h-6 rounded-full bg-bg-primary border border-border-default cursor-pointer shadow-sm;
84
127
  right: -12px; /* half of button width to straddle the border */
85
- transition: all 200ms ease;
128
+ transition: background-color 200ms ease, box-shadow 200ms ease;
129
+
130
+ /* An invisible 40px hit area over the 24px circle. The visible button is a
131
+ circle whose CORNERS are dead, and half of it hangs over the content pane,
132
+ so the reliable target was ~20px across — small enough that an off-centre
133
+ press missed (anh Tú: "đôi lúc không hoạt động"). The look does not
134
+ change; only what the pointer can hit. */
135
+ &::before {
136
+ content: '';
137
+ position: absolute;
138
+ inset: -8px;
139
+ border-radius: 9999px;
140
+ }
86
141
 
87
142
  &:hover {
88
143
  @apply bg-bg-hover shadow-md;
89
144
  }
90
145
 
146
+ /* NO scale on :active. Shrinking the button under a pressed pointer moves its
147
+ edge inward mid-press; a press that started near the rim then released
148
+ outside the element, and the browser fires no `click` at all for a
149
+ mousedown/mouseup pair on different targets. Press feedback is the fill. */
91
150
  &:active {
92
- transform: translateY(-50%) scale(0.9);
151
+ @apply bg-bg-hover;
93
152
  }
94
153
 
95
154
  &:focus-visible {
@@ -138,14 +197,25 @@
138
197
  }
139
198
 
140
199
  .drawer-item {
141
- @apply flex items-center mt-1 w-full min-h-8 py-2 px-large border-none bg-transparent cursor-pointer no-underline text-text-primary relative rounded;
142
- transition: all 200ms ease;
143
-
144
- /* Hover = glass mờ hơn: chỉ translucent, chưa viền/shadow, để không
145
- nhầm với hàng đang chọn. */
200
+ /* Row geometry to the reference (0.7.107): 6px 8px padding, 30px min
201
+ height, 12.5px, 7px control radius, 1px between rows, text-body at rest.
202
+ The container's px-2 supplies the 8px inset from the drawer's edge, so
203
+ the row itself carries no horizontal margin the reference's rows sit
204
+ 8px in, not 16. */
205
+ @apply flex items-center w-full border-none bg-transparent cursor-pointer no-underline relative;
206
+ margin-top: 1px;
207
+ min-height: 30px;
208
+ padding: 6px 8px;
209
+ font-size: var(--og-fs-sm);
210
+ color: rgb(var(--og-text-body));
211
+ border-radius: var(--og-radius-control);
212
+ transition: background-color 130ms ease, color 130ms ease;
213
+
214
+ /* Hover: an 8% tint of the text colour — the reference does not move or
215
+ scale the row. */
146
216
  &:hover {
147
- @apply text-text-primary scale-95;
148
- background: rgb(var(--og-surface-raised) / 0.5);
217
+ color: rgb(var(--og-text));
218
+ background: color-mix(in srgb, rgb(var(--og-text)) 8%, transparent);
149
219
  }
150
220
 
151
221
  /* Hàng đang chọn = GLASS, KHÔNG dùng màu primary (anh Tú chốt 31/08):
@@ -158,25 +228,24 @@
158
228
  khai border-none, thêm border thật sẽ đẩy layout 1px.
159
229
  - backdrop-blur chỉ có tác dụng khi nền sau có hoạ tiết/gradient; nền
160
230
  phẳng thì hiệu ứng đến từ độ trong + viền + shadow. */
231
+ /* Active (0.7.107, reference `.og-sidebar__item.is-active`): a 6% tint of
232
+ the text colour with a 7% inset hairline, weight 500. Replaces the glass
233
+ tile (surface-raised/0.8 + blur + shadow) of 31/08 — the reference's
234
+ selected row is a flat tint, and the tint is token-driven so charcoal
235
+ gets a light-on-dark ring where porcelain gets dark-on-light.
236
+ Viền vẽ bằng inset box-shadow chứ không phải `border`: .drawer-item
237
+ khai border-none, thêm border thật sẽ đẩy layout 1px. */
161
238
  &.active {
162
- @apply text-text-primary font-medium;
163
- background: rgb(var(--og-surface-raised) / 0.8);
164
- backdrop-filter: blur(12px);
165
- /* shadow viết literal chứ không dùng var(--shadow-sm): biến đó do @theme
166
- của Tailwind sinh, không chắc có mặt trong scope của component SCSS. */
167
- box-shadow:
168
- inset 0 0 0 1px rgb(var(--og-border)),
169
- 0 1px 2px 0 rgb(var(--shadow-color) / 0.05);
239
+ color: rgb(var(--og-text));
240
+ font-weight: 500;
241
+ background: color-mix(in srgb, rgb(var(--og-text)) 6%, transparent);
242
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, rgb(var(--og-text)) 7%, transparent);
170
243
 
171
244
  &:hover {
172
- background: rgb(var(--og-surface-raised) / 0.95);
245
+ background: color-mix(in srgb, rgb(var(--og-text)) 9%, transparent);
173
246
  }
174
247
  }
175
248
 
176
- &:active {
177
- transform: scale(0.98);
178
- }
179
-
180
249
  &:focus-visible {
181
250
  @apply outline-2 outline-primary outline-offset-2;
182
251
  }
@@ -185,17 +254,65 @@
185
254
  @apply outline-none;
186
255
  }
187
256
 
257
+ /* 9px icon-to-label, per the reference's nav row (measured 16/09). It was
258
+ `gap-3` = 12px. */
188
259
  .item-content {
189
- @apply flex items-center gap-3 w-full overflow-hidden;
260
+ @apply flex items-center gap-[9px] w-full overflow-hidden;
261
+ }
262
+
263
+ /* 18px, in an 18px box — the reference's glyph is 12.5px inside an 18px span.
264
+ At 20px the icon alone made the row 32px tall, so the `min-height: 30px` on
265
+ .drawer-item was never the height that actually shipped. The gap lives on
266
+ .item-content; do NOT add a margin here as well or the two compound. */
267
+ /* Collapsed the label is gone and the icon is the only thing in the row, so it
268
+ carries the row on its own and goes back up to 18px (anh Tú 16/09). Expanded
269
+ it stays at the reference's 13px mark. */
270
+ .drawer-item.is-collapsed .item-icon {
271
+ width: 18px;
272
+ height: 18px;
273
+ margin-right: 0;
274
+ }
275
+
276
+ /* A solid mark: lucide draws outlines, and filling the shape turns the diamond
277
+ into the reference's filled lozenge.
278
+ The descendant selector is REQUIRED: lucide renders an <svg> INSIDE the
279
+ element carrying `.item-icon`, and it is that inner svg that holds
280
+ `fill="none"` — styling only the outer one left the mark hollow (MEASURED
281
+ 16/09). Paths are named too, for the nesting lucide may skip. */
282
+ .item-icon--solid {
283
+ fill: currentColor;
284
+ }
285
+
286
+ /* ::ng-deep is REQUIRED for the descendants, not a shortcut: lucide builds its
287
+ <svg> (and the `fill="none"` on it) at runtime with Renderer2, so those nodes
288
+ never carry this component's [_ngcontent] attribute and a scoped selector
289
+ matches nothing. The host element above is in the template and does get it —
290
+ which is why the outer fill applied while the mark stayed hollow. */
291
+ :host ::ng-deep .item-icon--solid svg,
292
+ :host ::ng-deep .item-icon--solid path,
293
+ :host ::ng-deep .item-icon--solid polygon {
294
+ fill: currentColor;
190
295
  }
191
296
 
192
297
  .item-icon {
193
298
  @apply flex-shrink-0;
194
299
  color: inherit;
300
+ /* The reference's mark is a 12.5px glyph sitting in an 18px slot, so the
301
+ ICON is 13px and the 5px below restores the slot: 13 + 5 + the container's
302
+ 9px gap = 27px from the icon's left edge to the label, exactly as in the
303
+ design. Sizing the icon itself 18px drew it half again too large. */
304
+ width: 13px;
305
+ height: 13px;
306
+ margin-right: 5px;
195
307
  }
196
308
 
197
309
  .item-label {
198
- @apply flex-1 whitespace-nowrap overflow-hidden text-ellipsis text-[13px] leading-5 text-left;
310
+ /* Size comes from .drawer-item (12.5px, 0.7.107) — no second scale here.
311
+ Leading is 18px, not `leading-5`'s 20px: the label is the tallest thing in
312
+ the row, so 20 + 6 + 6 padding made every row 32px and the declared
313
+ `min-height: 30px` never applied. The reference's row is 30. */
314
+ @apply flex-1 whitespace-nowrap overflow-hidden text-ellipsis leading-[18px] text-left;
315
+ font-size: inherit;
199
316
  }
200
317
 
201
318
  .chevron-icon {
@@ -3,10 +3,13 @@
3
3
  <button
4
4
  type="button"
5
5
  [attr.aria-pressed]="isActive(option)"
6
- class="flex items-center gap-small px-[11px] py-[4px] rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors"
6
+ class="fx-pill flex items-center rounded-full border cursor-pointer transition-colors"
7
7
  [ngClass]="pillClass(option)"
8
8
  (click)="valueChange.emit(option.value)"
9
9
  >
10
+ @if (option.icon) {
11
+ <lucide-icon [img]="option.icon" [size]="14" class="flex shrink-0" />
12
+ }
10
13
  {{ option.label }}
11
14
  @if (option.count !== null && option.count !== undefined) {
12
15
  <span [ngClass]="countClass(option)" style="font-family: var(--og-font-figure)">{{ option.count }}</span>
@@ -0,0 +1,23 @@
1
+ @reference '../../../styles/tailwind.css';
2
+
3
+ /* Pill metrics to the reference (measured in the browser 16/09): 29px tall,
4
+ 6px/12px padding, --og-fs-xs at weight 600, 7px between label and count.
5
+ It was 26px at 4px/11px, 12px/700 and a 4px gap — a step smaller, a step
6
+ heavier and a step tighter than the design at every point. */
7
+ .fx-pill {
8
+ padding: 6px 12px;
9
+ gap: 7px;
10
+ font-size: var(--og-fs-xs);
11
+ font-weight: 600;
12
+ /* 15px, not a ratio: 6+6 padding + 2 border + 15 = the reference's 29px.
13
+ `1.4` on 11.5px renders 16.1 and the pill came out 30. */
14
+ line-height: 15px;
15
+ }
16
+
17
+ /* Hover lift, matching the reference's `.bi-filter-pill:hover` — the same
18
+ 0 2px 7px rgba(0,0,0,.12) every other button in that design gets
19
+ (`.og-button:hover`, which `.btn-alternative` already carries here).
20
+ Measured 16/09; the pill had no hover affordance at all before. */
21
+ .fx-pill:hover {
22
+ box-shadow: 0 2px 7px rgb(0 0 0 / 0.12);
23
+ }
@@ -1,20 +1,25 @@
1
1
  <!-- flex column filling the host: cards in one grid row share a height and a
2
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">
3
+ <!-- The card itself carries NO padding: the reference pads the header and the
4
+ body separately (10px 12px 4px / 10px 12px, measured 16/09), which is what
5
+ lets the header rule sit flush and the body start tight under it. -->
6
+ <div class="card-common fx-section-card bg-surface flex flex-col flex-1 min-h-0">
4
7
  @if (titleStyle() === 'eyebrow') {
5
- <div class="flex items-baseline justify-between gap-normal mb-large">
8
+ <div class="fx-section-card__head flex items-baseline justify-between gap-normal">
6
9
  <span class="txt-eyebrow">{{ title() }}</span>
7
10
  @if (aside()) {
8
- <span class="txt-utility-description text-right">{{ aside() }}</span>
11
+ <span class="fx-section-card__aside text-right">{{ aside() }}</span>
9
12
  }
10
13
  </div>
11
14
  } @else {
12
- <div class="flex items-center gap-normal mb-large">
15
+ <div class="fx-section-card__head flex items-center gap-normal">
13
16
  @if (icon) {
14
17
  <fx-ui-hero-icon [ngClass]="iconClass" [icon]="icon" [size]="18"></fx-ui-hero-icon>
15
18
  }
16
19
  <span class="txt-heading-06">{{ title() }}</span>
17
20
  </div>
18
21
  }
19
- <ng-content></ng-content>
22
+ <div class="fx-section-card__body flex flex-col flex-1 min-h-0">
23
+ <ng-content></ng-content>
24
+ </div>
20
25
  </div>
@@ -0,0 +1,28 @@
1
+ @reference '../../../styles/tailwind.css';
2
+
3
+ /* Card padding, to the reference (measured in the browser 16/09).
4
+ `card-common` pads 20px/16px uniformly, which the design never does: there
5
+ the card has none, the header carries 10px 12px 4px and the body 10px 12px.
6
+ The uniform 16/20 is what made every panel on the page read roomier than the
7
+ design and pushed each section's first row 6px further from its title. */
8
+ .fx-section-card {
9
+ padding: 0;
10
+ }
11
+
12
+ .fx-section-card__head {
13
+ padding: 10px 12px 4px;
14
+ }
15
+
16
+ .fx-section-card__body {
17
+ padding: 10px 12px;
18
+ }
19
+
20
+ /* Section caption ("ranked by risk · across all projects"): 11.5px/400 in
21
+ --og-text-faint. `txt-utility-description` is 12px/300 in --og-text-secondary
22
+ — a size too large, a weight too light and a shade too dark. */
23
+ .fx-section-card__aside {
24
+ font-size: var(--og-fs-xs);
25
+ font-weight: 400;
26
+ line-height: 1.4;
27
+ color: rgb(var(--og-text-faint));
28
+ }
@@ -16,3 +16,51 @@
16
16
  grid-template-columns: minmax(0, 1fr) !important;
17
17
  }
18
18
  }
19
+
20
+ /* The ruled KPI strip, to the reference (`.og-statgrid--ruled`, measured in the
21
+ browser 16/09): one bordered, rounded, softly-shadowed panel with NO gap,
22
+ its cells padded 10px 12px and separated by a hairline on every cell but the
23
+ last. Same treatment the Exploit Intelligence page already hand-rolled. */
24
+ .fx-stat-grid--ruled {
25
+ gap: 0;
26
+ border: 1px solid rgb(var(--og-border));
27
+ border-radius: var(--og-radius-control);
28
+ background: rgb(var(--og-surface));
29
+ box-shadow:
30
+ 0 1px 2px rgb(0 0 0 / 0.04),
31
+ 0 3px 8px rgb(0 0 0 / 0.05);
32
+ overflow: hidden;
33
+ }
34
+
35
+ .fx-stat-grid--ruled .fx-stat-cell {
36
+ padding: 10px 12px;
37
+ border-right: 1px solid rgb(var(--og-border));
38
+ }
39
+
40
+ .fx-stat-grid--ruled .fx-stat-cell--last {
41
+ border-right: 0;
42
+ }
43
+
44
+ /* Cell type: 10.5px faint label, a 32px mono figure 6px below it, an 11.5px
45
+ muted sub 4px below that. */
46
+ .fx-stat-cell__label {
47
+ font-size: var(--og-fs-2xs);
48
+ font-weight: 400;
49
+ color: rgb(var(--og-text-faint));
50
+ }
51
+
52
+ .fx-stat-cell__figure {
53
+ font-family: var(--og-font-figure);
54
+ font-size: 32px;
55
+ font-weight: 400;
56
+ line-height: 1.1;
57
+ color: rgb(var(--og-text));
58
+ margin-top: 6px;
59
+ }
60
+
61
+ .fx-stat-cell__sub {
62
+ font-size: var(--og-fs-xs);
63
+ font-weight: 400;
64
+ color: rgb(var(--og-text-muted));
65
+ margin-top: 4px;
66
+ }
@@ -1,16 +1,25 @@
1
- <div class="fx-stat-grid grid gap-normal" [ngStyle]="{ 'grid-template-columns': gridColumns }">
2
- @for (card of cards(); track card.label) {
1
+ <!-- variant="figure" is ONE ruled panel, not N cards: the reference draws the
2
+ KPI strip as a single bordered box divided by hairlines (`.og-statgrid
3
+ --ruled`, measured 16/09). It was four separate `card-common` boxes with an
4
+ 8px gap, which read as four objects where the design has one. The other
5
+ variant keeps its own cards. -->
6
+ <div
7
+ class="fx-stat-grid grid"
8
+ [class.fx-stat-grid--ruled]="variant() === 'figure'"
9
+ [class.gap-normal]="variant() !== 'figure'"
10
+ [ngStyle]="{ 'grid-template-columns': gridColumns }"
11
+ >
12
+ @for (card of cards(); track card.label; let last = $last) {
3
13
  @if (variant() === 'figure') {
4
- <div class="card-common flex flex-col gap-small min-w-0">
5
- <span class="txt-utility-caption text-text-secondary truncate">{{ card.label }}</span>
14
+ <div class="fx-stat-cell flex flex-col min-w-0" [class.fx-stat-cell--last]="last">
15
+ <span class="fx-stat-cell__label truncate">{{ card.label }}</span>
6
16
  <span
7
- class="txt-heading-03"
17
+ class="fx-stat-cell__figure"
8
18
  [ngClass]="numClass(card)"
9
- style="font-family: var(--og-font-figure)"
10
19
  ><span [fxCountUp]="card.value"></span></span
11
20
  >
12
21
  @if (card.sub) {
13
- <span class="txt-utility-caption" [ngClass]="subClass(card)">{{ card.sub }}</span>
22
+ <span class="fx-stat-cell__sub" [ngClass]="subClass(card)">{{ card.sub }}</span>
14
23
  }
15
24
  @if (card.spark?.length) {
16
25
  <fx-ui-sparkbar class="mt-xs" [values]="card.spark!" [height]="16" [tone]="card.tone ?? null" />
package/tokens.css CHANGED
@@ -53,6 +53,9 @@
53
53
 
54
54
  --color-cta: #c2582a;
55
55
  --color-cta-hover: #b34e24;
56
+ /* Border of the CTA while hovered — the reference's `.bi-newbtn:hover`
57
+ darkens the edge one more step than the fill (16/09). */
58
+ --color-cta-hover-border: #a3461f;
56
59
 
57
60
  /* ---- Legacy colour names (@deprecated — remove after the V2 campaign;
58
61
  they resolve through the alias layer in theme.css, so every existing
@@ -26,6 +26,14 @@ declare class ChartComponent implements OnChanges, AfterViewInit, OnDestroy {
26
26
  * rather than an annotation.
27
27
  */
28
28
  readonly pieCenterValueSize: _angular_core.InputSignal<number>;
29
+ /**
30
+ * The donut's [inner, outer] radius — ECharts' own vocabulary, so either a
31
+ * percentage of the shorter side or a pixel count. The default is what the
32
+ * ring has always been. The reference dashboard draws a 140px ring with a
33
+ * 22px stroke, i.e. `[48, 70]` at `height="140px"` — the panel that wants it
34
+ * says so; the other pies keep their proportions.
35
+ */
36
+ readonly pieRadius: _angular_core.InputSignal<[string | number, string | number]>;
29
37
  readonly mobileWidth: _angular_core.InputSignal<number>;
30
38
  /** Override font for canvas text; empty = follow the theme's --og-font-figure. */
31
39
  readonly labelFontFamily: _angular_core.InputSignal<string>;
@@ -89,6 +97,8 @@ declare class ChartComponent implements OnChanges, AfterViewInit, OnDestroy {
89
97
  */
90
98
  private appearUntil;
91
99
  private static readonly APPEAR_MS;
100
+ /** Quiet time a host size has to hold before a resize is acted on. */
101
+ private static readonly RESIZE_SETTLE_MS;
92
102
  private deferTimer?;
93
103
  /**
94
104
  * Thay đổi kích thước ghi nhận TRONG cửa sổ appear, chờ áp sau. `measure()`
@@ -135,7 +145,13 @@ declare class ChartComponent implements OnChanges, AfterViewInit, OnDestroy {
135
145
  private measure;
136
146
  /** Animation xuất hiện vừa bắt đầu — khoá resize tới khi nó xong. */
137
147
  private markAppear;
138
- /** Pie/sunburst đặt legend + số giữa theo px của containerWidth → phải dựng lại. */
148
+ /**
149
+ * Sunburst, and a pie WITH its built-in legend, place things in px of
150
+ * containerWidth → must rebuild on resize. A legend-less pie centres its
151
+ * ring at 50%/50% and its total in a `left: 'center' / top: 'middle'`
152
+ * graphic group, so `resize()` alone re-centres it — no rebuild, no
153
+ * second appear animation (15/09, donut on sidebar toggle).
154
+ */
139
155
  private needsLayoutRebuild;
140
156
  private deferResize;
141
157
  /**
@@ -153,6 +169,10 @@ declare class ChartComponent implements OnChanges, AfterViewInit, OnDestroy {
153
169
  * trọng: resize SAU rebuild sẽ cắt ngay animation vừa bắt đầu.
154
170
  */
155
171
  private forceMediaRecalc;
172
+ /**
173
+ * @param silent redraw for a new size only — same data, no appear animation,
174
+ * and the appear window is not re-armed.
175
+ */
156
176
  private rebuildChart;
157
177
  private checkEmpty;
158
178
  private registry;
@@ -195,7 +215,7 @@ declare class ChartComponent implements OnChanges, AfterViewInit, OnDestroy {
195
215
  private resolvedFontFamily;
196
216
  private resolvePalette;
197
217
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<ChartComponent, never>;
198
- 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; }; "pieCenterValueSize": { "alias": "pieCenterValueSize"; "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; }; "showPointLabels": { "alias": "showPointLabels"; "required": false; "isSignal": true; }; }, { "chartClick": "chartClick"; "chartInit": "chartInit"; }, never, never, true, never>;
218
+ 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; }; "pieCenterValueSize": { "alias": "pieCenterValueSize"; "required": false; "isSignal": true; }; "pieRadius": { "alias": "pieRadius"; "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; }; "showPointLabels": { "alias": "showPointLabels"; "required": false; "isSignal": true; }; }, { "chartClick": "chartClick"; "chartInit": "chartInit"; }, never, never, true, never>;
199
219
  }
200
220
 
201
221
  /**