@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.
Files changed (27) hide show
  1. package/components.css +40 -1
  2. package/fesm2022/foxeltech-angular-ui-chart.mjs +127 -44
  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 +559 -42
  7. package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
  8. package/package.json +3 -2
  9. package/src/lib/styles/components.css +40 -1
  10. package/src/lib/styles/utilities.css +2 -1
  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/route-skeleton/route-skeleton.component.css +1 -1
  20. package/src/lib/ui/components/segmented/segmented.component.html +3 -1
  21. package/src/lib/ui/components/severity-badges/severity-badges.component.html +1 -1
  22. package/src/lib/ui/components/stat-cards/stat-cards.component.css +18 -0
  23. package/src/lib/ui/components/stat-cards/stat-cards.component.html +3 -3
  24. package/types/foxeltech-angular-ui-chart.d.ts +56 -22
  25. package/types/foxeltech-angular-ui-flow.d.ts +7 -0
  26. package/types/foxeltech-angular-ui.d.ts +309 -8
  27. package/utilities.css +2 -1
package/components.css CHANGED
@@ -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
- @apply rounded bg-track animate-pulse motion-reduce:animate-none;
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
  /* ============================================================================
@@ -41,6 +41,8 @@ class ChartComponent {
41
41
  * dom.". Chỉ dựng directive khi đã có options thật ⇒ đúng một lần init.
42
42
  */
43
43
  hasOptions = false;
44
+ /** Heights (%) of the pre-init placeholder bars — a fixed, calm silhouette. */
45
+ placeholderBars = [34, 52, 44, 66, 58, 78, 70, 88, 62, 74];
44
46
  labelFontSize = 12;
45
47
  chartInstance;
46
48
  resizeObserver;
@@ -66,6 +68,24 @@ class ChartComponent {
66
68
  pendingFrames = new Set();
67
69
  isMobile = false;
68
70
  containerWidth = 0;
71
+ containerHeight = 0;
72
+ /**
73
+ * Mốc (performance.now) mà animation "xuất hiện" của lần setOption gần nhất
74
+ * chắc chắn đã xong. ECharts mặc định animationDuration 1000ms; trong khoảng
75
+ * này KHÔNG được `resize()` — resize nhảy thẳng tới trạng thái cuối và giết
76
+ * animation (đo 05/09: chart PM bị cắt ở ~800ms khi thanh cuộn xuất hiện
77
+ * lúc lưới card nạp xong làm bề rộng đổi 720→710).
78
+ */
79
+ appearUntil = 0;
80
+ static APPEAR_MS = 1100;
81
+ deferTimer;
82
+ /**
83
+ * Thay đổi kích thước ghi nhận TRONG cửa sổ appear, chờ áp sau. `measure()`
84
+ * đã cập nhật containerWidth/Height ngay lúc đo, nên lần gọi hoãn không thể
85
+ * dựa vào "có đổi không" nữa — phải nhớ bằng cờ.
86
+ */
87
+ pendingResize = false;
88
+ pendingMobile = false;
69
89
  constructor(ref, cdr) {
70
90
  this.ref = ref;
71
91
  this.cdr = cdr;
@@ -76,24 +96,27 @@ class ChartComponent {
76
96
  this.intersectionObserver = new IntersectionObserver(([entry]) => {
77
97
  if (entry.isIntersecting && !this.canInit && !this.destroyed) {
78
98
  this.canInit = true;
79
- this.updateResponsiveState();
99
+ this.measure();
80
100
  this.rebuildChart();
81
101
  this.cdr.detectChanges();
82
102
  }
83
103
  }, { threshold: 0.1 });
84
104
  this.intersectionObserver.observe(this.ref.nativeElement);
85
- // Resize-safe (tabs, flex, async layout)
105
+ // Resize-safe (tabs, flex, async layout). Đây là ĐƯỜNG RESIZE DUY NHẤT:
106
+ // `autoResize` của NgxEchartsDirective đã tắt trong template — nó
107
+ // resize() mù sau throttle 100ms, kể cả về 0×H khi phần tử display:none
108
+ // (skeleton route ẩn nội dung cũ) rồi resize lại khi hiện → animation
109
+ // xuất hiện bị giết (đo 05/09 khi đổi project trên chip breadcrumb).
86
110
  this.resizeObserver = new ResizeObserver(() => {
87
111
  if (!this.canInit || this.destroyed)
88
112
  return;
89
- // forceMediaRecalc tự đo lại và chỉ dựng lại khi kích thước đổi.
90
113
  this.forceMediaRecalc();
91
114
  });
92
115
  this.resizeObserver.observe(this.ref.nativeElement);
93
116
  }
94
117
  ngOnChanges() {
95
118
  if (this.canInit && !this.destroyed) {
96
- this.updateResponsiveState();
119
+ this.measure();
97
120
  this.rebuildChart();
98
121
  }
99
122
  }
@@ -106,6 +129,9 @@ class ChartComponent {
106
129
  for (const id of this.pendingFrames)
107
130
  cancelAnimationFrame(id);
108
131
  this.pendingFrames.clear();
132
+ if (this.deferTimer !== undefined)
133
+ clearTimeout(this.deferTimer);
134
+ this.deferTimer = undefined;
109
135
  this.resizeObserver?.disconnect();
110
136
  this.intersectionObserver?.disconnect();
111
137
  this.resizeObserver = undefined;
@@ -209,63 +235,91 @@ class ChartComponent {
209
235
  chart.setOption(options, true);
210
236
  }
211
237
  }
238
+ // Lần vẽ đầu (directive setOption lúc init) cũng có animation xuất hiện.
239
+ this.markAppear();
212
240
  this.chartInit.emit(chart);
213
241
  this.schedule(() => this.forceMediaRecalc());
214
242
  }
215
243
  /* ================= CORE FIX ================= */
216
244
  /**
217
- * @returns true nếu trạng thái responsive THỰC SỰ đổi (nên dựng lại options).
218
- *
219
- * Trả về boolean điều then chốt cho animation lúc chart xuất hiện — xem
220
- * ghi chú ở `forceMediaRecalc`.
245
+ * Đo host ghi lại; trả về CÁI đổi so với lần đo trước.
246
+ * Khi bị ẩn (0×0) thì KHÔNG ghi — giữ kích thước hiển thị cuối, để lúc hiện
247
+ * lại cùng cỡ thì coi như không đổi (không resize, không cắt animation).
221
248
  */
222
- updateResponsiveState() {
223
- const { width } = this.ref.nativeElement.getBoundingClientRect();
224
- if (width <= 0)
225
- return false;
249
+ measure() {
250
+ const { width, height } = this.ref.nativeElement.getBoundingClientRect();
251
+ if (width <= 0 || height <= 0)
252
+ return { size: false, mobile: false };
226
253
  const isMobile = width < this.mobileWidth();
227
- const changed = width !== this.containerWidth || isMobile !== this.isMobile;
254
+ const size = width !== this.containerWidth || height !== this.containerHeight;
255
+ const mobile = isMobile !== this.isMobile;
228
256
  this.containerWidth = width;
257
+ this.containerHeight = height;
229
258
  this.isMobile = isMobile;
230
- return changed;
259
+ return { size, mobile };
260
+ }
261
+ /** Animation xuất hiện vừa bắt đầu — khoá resize tới khi nó xong. */
262
+ markAppear() {
263
+ this.appearUntil = performance.now() + ChartComponent.APPEAR_MS;
264
+ }
265
+ /** Pie/sunburst đặt legend + số giữa theo px của containerWidth → phải dựng lại. */
266
+ needsLayoutRebuild() {
267
+ const type = this.detectType();
268
+ return type === 'pie' || type === 'sunburst';
269
+ }
270
+ deferResize(ms) {
271
+ if (this.deferTimer !== undefined)
272
+ clearTimeout(this.deferTimer);
273
+ this.deferTimer = setTimeout(() => {
274
+ this.deferTimer = undefined;
275
+ if (!this.destroyed)
276
+ this.forceMediaRecalc();
277
+ }, ms);
231
278
  }
232
279
  /**
233
- * ⚠️ CHỈ dựng lại options khi kích thước thật sự đổi nếu không thì
234
- * **animation lúc chart xuất hiện bị giết**.
235
- *
236
- * Đường đi: `onChartInit` → schedule(forceMediaRecalc) → chạy ở frame KẾ
237
- * TIẾP. Nếu chỗ này vô điều kiện `rebuildChart()` (clear + setOption notMerge)
238
- * thì nó xoá sạch và vẽ lại từ đầu ~16ms sau lần vẽ đầu, trong khi animation
239
- * xuất hiện dài ~1000ms ⇒ người dùng thấy chart hiện ra đứng im.
240
- *
241
- * Đo được: lần xuất hiện đầu 1/12 frame khác nhau (đứng yên) trong khi vẽ lại
242
- * bằng `theme-changed` cho 9/12 → animation của ECharts vẫn bật, chỉ lần đầu
243
- * bị cắt.
244
- *
245
- * Trước Pha 0 lỗi này bị che: lần init đầu dùng `chartOptions = {}` nên
246
- * animation xuất hiện xảy ra ở lần cập nhật SAU, tức sau khi forceMediaRecalc
247
- * đã chạy xong. Cổng `hasOptions` đưa options thật về ngay lần init nên lộ ra.
280
+ * Phản ứng với kích thước host (RO, sau init, sau khi hiện lại).
248
281
  *
249
- * `resize()` thì luôn gọi được không cắt animation.
282
+ * Ba quy tắc, đều đo được (0.7.75 05/09):
283
+ * 1. **Ẩn (0×H) thì không làm gì** — không resize canvas về 0. Chart hiện
284
+ * lại cùng cỡ thì animation đã bắt đầu lúc ẩn (setOption khi resolver về,
285
+ * ngay trước NavigationEnd) chạy tiếp bình thường.
286
+ * 2. **`resize()` nhảy thẳng tới trạng thái cuối** ⇒ trong cửa sổ appear
287
+ * (`appearUntil`) thì HOÃN, đo lại và xử lý sau khi animation xong. Không
288
+ * dựng lại (clear + setOption) lúc đó vì đó là "vẽ 2 lần" theo cách khác.
289
+ * 3. Ngoài cửa sổ: `resize()` trước, rồi chỉ dựng lại khi preset đổi (mobile)
290
+ * hoặc loại chart đặt layout theo px (pie/sunburst). Thứ tự này quan
291
+ * trọng: resize SAU rebuild sẽ cắt ngay animation vừa bắt đầu.
250
292
  */
251
293
  forceMediaRecalc(retry = 0) {
252
294
  if (!this.liveChart)
253
295
  return;
254
296
  const { width, height } = this.ref.nativeElement.getBoundingClientRect();
255
- if ((width === 0 || height === 0) && retry < 10) {
256
- this.schedule(() => this.forceMediaRecalc(retry + 1));
297
+ if (width === 0 || height === 0) {
298
+ // Chưa layout xong (ngay sau init) thì thử lại vài frame; đang ẩn thì thôi.
299
+ if (retry < 10)
300
+ this.schedule(() => this.forceMediaRecalc(retry + 1));
257
301
  return;
258
302
  }
259
- // ⚠️ `resize()` cũng phải CÓ ĐIỀU KIỆN. ECharts nhảy thẳng tới trạng thái
260
- // cuối khi resize, nên gọi nó ở frame ngay sau init sẽ CẮT animation lúc
261
- // chart xuất hiện đo được: 3 lần đổi khung trong 200ms rồi đứng im, dù
262
- // đã đặt animationDuration 3000ms.
263
- // Không cần resize ở lần đầu: ECharts tự lấy kích thước khi init, và vòng
264
- // retry ở trên đã lo ca width/height = 0.
265
- if (!this.updateResponsiveState())
303
+ const change = this.measure();
304
+ const size = change.size || this.pendingResize;
305
+ const mobile = change.mobile || this.pendingMobile;
306
+ if (!size && !mobile)
266
307
  return;
267
- this.rebuildChart();
268
- this.liveChart?.resize();
308
+ const remaining = this.appearUntil - performance.now();
309
+ if (remaining > 0) {
310
+ this.pendingResize = size;
311
+ this.pendingMobile = mobile;
312
+ this.deferResize(remaining + 30);
313
+ return;
314
+ }
315
+ this.pendingResize = false;
316
+ this.pendingMobile = false;
317
+ const chart = this.liveChart;
318
+ if (!chart)
319
+ return;
320
+ chart.resize();
321
+ if (mobile || this.needsLayoutRebuild())
322
+ this.rebuildChart();
269
323
  }
270
324
  rebuildChart() {
271
325
  if (this.destroyed)
@@ -293,6 +347,7 @@ class ChartComponent {
293
347
  this.hasOptions = true;
294
348
  chart.clear();
295
349
  chart.setOption(options, true);
350
+ this.markAppear();
296
351
  }
297
352
  checkEmpty() {
298
353
  const type = this.detectType();
@@ -715,19 +770,47 @@ class ChartComponent {
715
770
  });
716
771
  }
717
772
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ChartComponent, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
718
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ChartComponent, isStandalone: true, selector: "fx-ui-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, palette: { classPropertyName: "palette", publicName: "palette", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, pieLegendAlign: { classPropertyName: "pieLegendAlign", publicName: "pieLegendAlign", isSignal: true, isRequired: false, transformFunction: null }, pieLegendPosition: { classPropertyName: "pieLegendPosition", publicName: "pieLegendPosition", isSignal: true, isRequired: false, transformFunction: null }, showPieLabel: { classPropertyName: "showPieLabel", publicName: "showPieLabel", isSignal: true, isRequired: false, transformFunction: null }, showPieLegend: { classPropertyName: "showPieLegend", publicName: "showPieLegend", isSignal: true, isRequired: false, transformFunction: null }, pieCenterLabel: { classPropertyName: "pieCenterLabel", publicName: "pieCenterLabel", isSignal: true, isRequired: false, transformFunction: null }, mobileWidth: { classPropertyName: "mobileWidth", publicName: "mobileWidth", isSignal: true, isRequired: false, transformFunction: null }, labelFontFamily: { classPropertyName: "labelFontFamily", publicName: "labelFontFamily", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { chartClick: "chartClick", chartInit: "chartInit" }, host: { listeners: { "window:theme-changed": "onThemeChange()" } }, providers: [provideEchartsCore({ echarts: () => import('echarts') })], usesOnChanges: true, ngImport: i0, template: "<div class=\"chart-wrapper\" [style.height]=\"height()\">\n @if (isEmpty) {\n <div class=\"chart-empty\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"chart-empty-icon\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z\" />\n </svg>\n <span class=\"chart-empty-text\">{{ emptyText() }}</span>\n </div>\n } @else if (hasOptions) {\n <div\n echarts\n [options]=\"chartOptions\"\n [loading]=\"loading()\"\n (chartInit)=\"onChartInit($event)\"\n (chartClick)=\"onChartClick($event)\"\n class=\"fx-chart\"\n [style.height]=\"height()\"\n ></div>\n }\n</div>\n", styles: [":host{display:block;width:100%}.chart-wrapper{position:relative;width:100%;height:100%}.chart-container{width:100%;height:100%}.chart-loader{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--bg-primary-hover)/.6);font-weight:500;font-size:1.1rem;z-index:2;border-radius:4px}.chart-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;color:rgba(var(--text-secondary)/1)}.chart-empty-icon{width:48px;height:48px;opacity:.4}.chart-empty-text{font-size:14px;font-weight:500}\n"], dependencies: [{ kind: "directive", type: NgxEchartsDirective, selector: "echarts, [echarts]", inputs: ["options", "theme", "initOpts", "merge", "autoResize", "loading", "loadingType", "loadingOpts"], outputs: ["chartInit", "optionsError", "chartClick", "chartDblClick", "chartMouseDown", "chartMouseMove", "chartMouseUp", "chartMouseOver", "chartMouseOut", "chartGlobalOut", "chartContextMenu", "chartHighlight", "chartDownplay", "chartSelectChanged", "chartLegendSelectChanged", "chartLegendSelected", "chartLegendUnselected", "chartLegendLegendSelectAll", "chartLegendLegendInverseSelect", "chartLegendScroll", "chartDataZoom", "chartDataRangeSelected", "chartGraphRoam", "chartGeoRoam", "chartTreeRoam", "chartTimelineChanged", "chartTimelinePlayChanged", "chartRestore", "chartDataViewChanged", "chartMagicTypeChanged", "chartGeoSelectChanged", "chartGeoSelected", "chartGeoUnselected", "chartAxisAreaSelected", "chartBrush", "chartBrushEnd", "chartBrushSelected", "chartGlobalCursorTaken", "chartRendered", "chartFinished"], exportAs: ["echarts"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
773
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ChartComponent, isStandalone: true, selector: "fx-ui-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, palette: { classPropertyName: "palette", publicName: "palette", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, pieLegendAlign: { classPropertyName: "pieLegendAlign", publicName: "pieLegendAlign", isSignal: true, isRequired: false, transformFunction: null }, pieLegendPosition: { classPropertyName: "pieLegendPosition", publicName: "pieLegendPosition", isSignal: true, isRequired: false, transformFunction: null }, showPieLabel: { classPropertyName: "showPieLabel", publicName: "showPieLabel", isSignal: true, isRequired: false, transformFunction: null }, showPieLegend: { classPropertyName: "showPieLegend", publicName: "showPieLegend", isSignal: true, isRequired: false, transformFunction: null }, pieCenterLabel: { classPropertyName: "pieCenterLabel", publicName: "pieCenterLabel", isSignal: true, isRequired: false, transformFunction: null }, mobileWidth: { classPropertyName: "mobileWidth", publicName: "mobileWidth", isSignal: true, isRequired: false, transformFunction: null }, labelFontFamily: { classPropertyName: "labelFontFamily", publicName: "labelFontFamily", isSignal: true, isRequired: false, transformFunction: null }, emptyText: { classPropertyName: "emptyText", publicName: "emptyText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { chartClick: "chartClick", chartInit: "chartInit" }, host: { listeners: { "window:theme-changed": "onThemeChange()" } }, providers: [provideEchartsCore({ echarts: () => import('echarts') })], usesOnChanges: true, ngImport: i0, template: "<div class=\"chart-wrapper\" [style.height]=\"height()\">\n @if (isEmpty) {\n <div class=\"chart-empty\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"chart-empty-icon\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z\" />\n </svg>\n <span class=\"chart-empty-text\">{{ emptyText() }}</span>\n </div>\n } @else if (hasOptions) {\n <div\n echarts\n [options]=\"chartOptions\"\n [autoResize]=\"false\"\n [loading]=\"loading()\"\n (chartInit)=\"onChartInit($event)\"\n (chartClick)=\"onChartClick($event)\"\n class=\"fx-chart\"\n [style.height]=\"height()\"\n ></div>\n } @else {\n <!-- Until echarts is loaded + initialised (or the element scrolls into view)\n the panel keeps the chart's footprint with a quiet pulsing sketch\n instead of an empty box (anh T\u00FA 04/09: \"the page is too blank\"). -->\n <div class=\"chart-placeholder\" aria-hidden=\"true\">\n @for (h of placeholderBars; track $index) {\n <span class=\"chart-placeholder__bar\" [style.height.%]=\"h\"></span>\n }\n </div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%}.chart-wrapper{position:relative;width:100%;height:100%}.chart-container{width:100%;height:100%}.chart-loader{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--bg-primary-hover)/.6);font-weight:500;font-size:1.1rem;z-index:2;border-radius:4px}.chart-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;color:rgba(var(--text-secondary)/1)}.chart-empty-icon{width:48px;height:48px;opacity:.4}.chart-empty-text{font-size:14px;font-weight:500}.chart-placeholder{position:absolute;inset:0;display:flex;align-items:flex-end;gap:6px;padding:12px 8px 22px;animation:chart-placeholder-pulse 1.6s ease-in-out infinite}.chart-placeholder__bar{flex:1;border-radius:3px 3px 0 0;background:rgb(var(--og-track))}@keyframes chart-placeholder-pulse{0%,to{opacity:1}50%{opacity:.45}}@media(prefers-reduced-motion:reduce){.chart-placeholder{animation:none}}\n"], dependencies: [{ kind: "directive", type: NgxEchartsDirective, selector: "echarts, [echarts]", inputs: ["options", "theme", "initOpts", "merge", "autoResize", "loading", "loadingType", "loadingOpts"], outputs: ["chartInit", "optionsError", "chartClick", "chartDblClick", "chartMouseDown", "chartMouseMove", "chartMouseUp", "chartMouseOver", "chartMouseOut", "chartGlobalOut", "chartContextMenu", "chartHighlight", "chartDownplay", "chartSelectChanged", "chartLegendSelectChanged", "chartLegendSelected", "chartLegendUnselected", "chartLegendLegendSelectAll", "chartLegendLegendInverseSelect", "chartLegendScroll", "chartDataZoom", "chartDataRangeSelected", "chartGraphRoam", "chartGeoRoam", "chartTreeRoam", "chartTimelineChanged", "chartTimelinePlayChanged", "chartRestore", "chartDataViewChanged", "chartMagicTypeChanged", "chartGeoSelectChanged", "chartGeoSelected", "chartGeoUnselected", "chartAxisAreaSelected", "chartBrush", "chartBrushEnd", "chartBrushSelected", "chartGlobalCursorTaken", "chartRendered", "chartFinished"], exportAs: ["echarts"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
719
774
  }
720
775
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ChartComponent, decorators: [{
721
776
  type: Component,
722
- args: [{ selector: 'fx-ui-chart', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgxEchartsDirective, InputComponent], providers: [provideEchartsCore({ echarts: () => import('echarts') })], template: "<div class=\"chart-wrapper\" [style.height]=\"height()\">\n @if (isEmpty) {\n <div class=\"chart-empty\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"chart-empty-icon\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z\" />\n </svg>\n <span class=\"chart-empty-text\">{{ emptyText() }}</span>\n </div>\n } @else if (hasOptions) {\n <div\n echarts\n [options]=\"chartOptions\"\n [loading]=\"loading()\"\n (chartInit)=\"onChartInit($event)\"\n (chartClick)=\"onChartClick($event)\"\n class=\"fx-chart\"\n [style.height]=\"height()\"\n ></div>\n }\n</div>\n", styles: [":host{display:block;width:100%}.chart-wrapper{position:relative;width:100%;height:100%}.chart-container{width:100%;height:100%}.chart-loader{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--bg-primary-hover)/.6);font-weight:500;font-size:1.1rem;z-index:2;border-radius:4px}.chart-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;color:rgba(var(--text-secondary)/1)}.chart-empty-icon{width:48px;height:48px;opacity:.4}.chart-empty-text{font-size:14px;font-weight:500}\n"] }]
777
+ args: [{ selector: 'fx-ui-chart', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgxEchartsDirective, InputComponent], providers: [provideEchartsCore({ echarts: () => import('echarts') })], template: "<div class=\"chart-wrapper\" [style.height]=\"height()\">\n @if (isEmpty) {\n <div class=\"chart-empty\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"chart-empty-icon\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z\" />\n </svg>\n <span class=\"chart-empty-text\">{{ emptyText() }}</span>\n </div>\n } @else if (hasOptions) {\n <div\n echarts\n [options]=\"chartOptions\"\n [autoResize]=\"false\"\n [loading]=\"loading()\"\n (chartInit)=\"onChartInit($event)\"\n (chartClick)=\"onChartClick($event)\"\n class=\"fx-chart\"\n [style.height]=\"height()\"\n ></div>\n } @else {\n <!-- Until echarts is loaded + initialised (or the element scrolls into view)\n the panel keeps the chart's footprint with a quiet pulsing sketch\n instead of an empty box (anh T\u00FA 04/09: \"the page is too blank\"). -->\n <div class=\"chart-placeholder\" aria-hidden=\"true\">\n @for (h of placeholderBars; track $index) {\n <span class=\"chart-placeholder__bar\" [style.height.%]=\"h\"></span>\n }\n </div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;width:100%}.chart-wrapper{position:relative;width:100%;height:100%}.chart-container{width:100%;height:100%}.chart-loader{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(var(--bg-primary-hover)/.6);font-weight:500;font-size:1.1rem;z-index:2;border-radius:4px}.chart-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;color:rgba(var(--text-secondary)/1)}.chart-empty-icon{width:48px;height:48px;opacity:.4}.chart-empty-text{font-size:14px;font-weight:500}.chart-placeholder{position:absolute;inset:0;display:flex;align-items:flex-end;gap:6px;padding:12px 8px 22px;animation:chart-placeholder-pulse 1.6s ease-in-out infinite}.chart-placeholder__bar{flex:1;border-radius:3px 3px 0 0;background:rgb(var(--og-track))}@keyframes chart-placeholder-pulse{0%,to{opacity:1}50%{opacity:.45}}@media(prefers-reduced-motion:reduce){.chart-placeholder{animation:none}}\n"] }]
723
778
  }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], palette: [{ type: i0.Input, args: [{ isSignal: true, alias: "palette", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], pieLegendAlign: [{ type: i0.Input, args: [{ isSignal: true, alias: "pieLegendAlign", required: false }] }], pieLegendPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "pieLegendPosition", required: false }] }], showPieLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPieLabel", required: false }] }], showPieLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPieLegend", required: false }] }], pieCenterLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pieCenterLabel", required: false }] }], mobileWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileWidth", required: false }] }], labelFontFamily: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelFontFamily", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], chartClick: [{ type: i0.Output, args: ["chartClick"] }], chartInit: [{ type: i0.Output, args: ["chartInit"] }], onThemeChange: [{
724
779
  type: HostListener,
725
780
  args: ['window:theme-changed']
726
781
  }] } });
727
782
 
783
+ let pending;
784
+ /**
785
+ * Warm the echarts chunk before any chart is on screen.
786
+ *
787
+ * `fx-ui-chart` loads echarts lazily (`provideEchartsCore({ echarts: () =>
788
+ * import('echarts') })`), so the FIRST chart of a session pays the chunk
789
+ * download right when the user is looking at an empty panel. Call this once
790
+ * from an idle callback when the app's env injector is created (e.g. a
791
+ * `provideEnvironmentInitializer` in the remote's root route) and the dashboard
792
+ * charts appear on the frame they mount. Same module URL as the directive's
793
+ * import ⇒ one shared instance, no double load.
794
+ */
795
+ function preloadEcharts() {
796
+ return (pending ??= import('echarts'));
797
+ }
798
+ /** Schedule `preloadEcharts()` when the main thread is idle (SSR/tests: no-op). */
799
+ function preloadEchartsWhenIdle(timeoutMs = 3000) {
800
+ if (typeof window === 'undefined')
801
+ return;
802
+ const w = window;
803
+ if (typeof w.requestIdleCallback === 'function') {
804
+ w.requestIdleCallback(() => void preloadEcharts(), { timeout: timeoutMs });
805
+ }
806
+ else {
807
+ setTimeout(() => void preloadEcharts(), 1500);
808
+ }
809
+ }
810
+
728
811
  /**
729
812
  * Generated bundle index. Do not edit.
730
813
  */
731
814
 
732
- export { ChartComponent };
815
+ export { ChartComponent, preloadEcharts, preloadEchartsWhenIdle };
733
816
  //# sourceMappingURL=foxeltech-angular-ui-chart.mjs.map