@edmicro/olkit 1.1.3 → 1.1.5

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, Service, input, signal, computed, effect, ChangeDetectionStrategy, Component, forwardRef, model, output, contentChildren, HostListener, DOCUMENT, Injectable, DestroyRef, viewChild, ElementRef, Renderer2, ApplicationRef, EnvironmentInjector, createComponent, Directive, ViewContainerRef, contentChild, ContentChild, ViewChildren, ViewChild } from '@angular/core';
2
+ import { inject, Service, input, signal, computed, effect, ChangeDetectionStrategy, Component, forwardRef, model, output, contentChildren, HostListener, DOCUMENT, Injectable, DestroyRef, viewChild, ElementRef, Renderer2, ApplicationRef, EnvironmentInjector, createComponent, Directive, contentChild, ViewContainerRef, ContentChild, ViewChildren, ViewChild } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { CommonModule, NgTemplateOutlet, NgStyle } from '@angular/common';
5
5
  import { DomSanitizer } from '@angular/platform-browser';
@@ -57,8 +57,8 @@ class OlIconService {
57
57
  // Vì vậy khi tên CÓ alias thì bỏ hẳn hai nhánh sau — alias là quyết định
58
58
  // cuối cùng, và đó là cách duy nhất đổi được icon mà không bị
59
59
  // `gen-registry-metadata.js` ghi đè (registry là file sinh tự động).
60
- const coAlias = resolvedName !== normName;
61
- const lazyGetter = coAlias ? registry[resolvedName] : registry[name] || registry[normName];
60
+ const hasAlias = resolvedName !== normName;
61
+ const lazyGetter = hasAlias ? registry[resolvedName] : registry[name] || registry[normName];
62
62
  if (lazyGetter) {
63
63
  try {
64
64
  const svg = lazyGetter();
@@ -1381,33 +1381,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
1381
1381
  type: Service
1382
1382
  }] });
1383
1383
 
1384
- /**
1385
- * Container CHUNG cho mọi lớp nổi, gắn thẳng vào `<body>`.
1386
- *
1387
- * VÌ SAO CẦN — đo trên `/modal` ngày 2026-09-17, dropdown đặt trong modal hỏng
1388
- * vì HAI lý do chồng nhau, và z-index chỉ chữa được một:
1389
- *
1390
- * 1. Khung modal có `z-index: 80` + `backdrop-filter: blur(4px)` nên nó TẠO
1391
- * STACKING CONTEXT. Dropdown nằm bên trong chỉ xếp lớp so với anh em
1392
- * CÙNG trong modal — đặt `z-index: 9999` cũng không thoát ra được.
1393
- * 2. Hộp thoại có `overflow: hidden` (giữ bo góc) nên phần dropdown tràn ra
1394
- * bị CẮT THẬT. Không z-index nào chữa được điều này.
1395
- *
1396
- * CÁCH BA THƯ VIỆN LỚN LÀM (đọc mã tải từ npm, không nói từ trí nhớ):
1397
- *
1398
- * · Angular CDK 22.1.7 — `document.body` (20 lượt gọi), một container chung
1399
- * `cdk-overlay-container`, `z-index: 1000`.
1400
- * · PrimeNG 22.1.1 — `appendTo` + `ZIndexUtils.generateZIndex` cấp z-index
1401
- * ĐỘNG theo ngăn xếp (menu 1000 · modal 1100), không phải số cố định.
1402
- * · ng-select 24 — `appendTo` + CDK Overlay, tức dùng lại cách của CDK.
1403
- *
1404
- * Cả ba cùng một nguyên lý: ĐẨY LỚP NỔI RA `<body>`. Không ai cố chữa bằng
1405
- * z-index đơn thuần.
1406
- *
1407
- * Ở đây lấy container chung của CDK cộng z-index động của PrimeNG — thang tĩnh
1408
- * không xử lý được ca modal chồng modal, mỗi lớp mở sau phải nằm trên lớp
1409
- * trước.
1410
- */
1411
1384
  class OlOverlayContainer {
1412
1385
  doc = inject(DOCUMENT);
1413
1386
  host = null;
@@ -1499,6 +1472,12 @@ class OlOverlayContainer {
1499
1472
  openLayers = [];
1500
1473
  /** Trình nghe Escape duy nhất, gắn khi có lớp đầu tiên mở. */
1501
1474
  escapeListener = null;
1475
+ /**
1476
+ * Trình nghe click duy nhất — cùng nguyên tắc với Escape.
1477
+ *
1478
+ * Bấm ra ngoài chỉ đóng lớp TRÊN CÙNG, lớp dưới chờ cú bấm sau.
1479
+ */
1480
+ clickListener = null;
1502
1481
  /**
1503
1482
  * Đăng ký một lớp nổi vừa mở. Trả hàm gỡ — gọi khi lớp đóng.
1504
1483
  *
@@ -1530,18 +1509,21 @@ class OlOverlayContainer {
1530
1509
  * Hàm gỡ **an toàn khi gọi nhiều lần** — `close()` thường chạy cả từ phím
1531
1510
  * Escape lẫn từ nút đóng, và cả hai đường đều gọi nó.
1532
1511
  */
1533
- register(close) {
1534
- this.openLayers.push(close);
1512
+ register(close, contains) {
1513
+ const layer = { close, contains };
1514
+ this.openLayers.push(layer);
1535
1515
  this.ensureEscapeListener();
1516
+ if (contains)
1517
+ this.ensureClickListener();
1536
1518
  let released = false;
1537
1519
  return () => {
1538
1520
  if (released)
1539
1521
  return;
1540
1522
  released = true;
1541
- const i = this.openLayers.indexOf(close);
1523
+ const i = this.openLayers.indexOf(layer);
1542
1524
  if (i !== -1)
1543
1525
  this.openLayers.splice(i, 1);
1544
- this.teardownEscapeListenerIfIdle();
1526
+ this.teardownListenersIfIdle();
1545
1527
  };
1546
1528
  }
1547
1529
  /**
@@ -1562,18 +1544,84 @@ class OlOverlayContainer {
1562
1544
  // CHỈ lớp trên cùng đóng. Lớp dưới chờ lần nhấn sau.
1563
1545
  event.preventDefault();
1564
1546
  event.stopPropagation();
1565
- top();
1547
+ top.close();
1566
1548
  };
1567
1549
  // `capture: true` để chạy TRƯỚC mọi `@HostListener` còn sót lại ở
1568
1550
  // component, nhờ đó `stopPropagation` ở trên mới chặn được chúng.
1569
1551
  this.doc.addEventListener('keydown', this.escapeListener, true);
1570
1552
  }
1571
- /** Gỡ trình nghe khi lớp cuối cùng đóng — không để nó nghe vô ích. */
1572
- teardownEscapeListenerIfIdle() {
1573
- if (this.openLayers.length > 0 || !this.escapeListener)
1553
+ /**
1554
+ * Trình nghe click duy nhất cho cả ngăn xếp.
1555
+ *
1556
+ * ## Vì sao không để mỗi component tự nghe
1557
+ *
1558
+ * Trước đây `ol-datepicker`, `ol-select`, `ol-dropdown`… mỗi cái một
1559
+ * `@HostListener('document:click')`. Chúng không biết nhau, nên một cú bấm
1560
+ * ra ngoài kích hoạt TẤT CẢ cùng lúc: mở datepicker trong modal rồi bấm nền
1561
+ * thì cả datepicker lẫn modal cùng đóng, người dùng mất hộp thoại đang điền
1562
+ * dở. Đo /modal ngày 2026-09-24: `dp=1, modal=true` -> sau MỘT cú bấm còn
1563
+ * `dp=0, modal=false`.
1564
+ *
1565
+ * Escape đã giải đúng từ trước (xem `ensureEscapeListener`); đây là áp cùng
1566
+ * nguyên tắc cho chuột.
1567
+ *
1568
+ * ## Chỉ lớp TRÊN CÙNG đóng
1569
+ *
1570
+ * Click nằm trong lớp trên cùng -> không đóng gì. Nằm ngoài -> đóng đúng lớp
1571
+ * đó, và `stopPropagation` chặn lớp dưới nhận cùng cú bấm ấy.
1572
+ *
1573
+ * Lớp không khai `contains` thì bị bỏ qua hoàn toàn — nó tự lo phần click
1574
+ * của mình, ngăn xếp không đoán thay.
1575
+ */
1576
+ ensureClickListener() {
1577
+ if (this.clickListener)
1578
+ return;
1579
+ this.clickListener = (event) => {
1580
+ // Duyệt từ trên xuống, dừng ở lớp đầu tiên có khai `contains`.
1581
+ for (let i = this.openLayers.length - 1; i >= 0; i--) {
1582
+ const layer = this.openLayers[i];
1583
+ if (!layer.contains)
1584
+ continue;
1585
+ // Bấm TRONG lớp trên cùng: không đóng gì cả.
1586
+ if (layer.contains(event.target))
1587
+ return;
1588
+ /**
1589
+ * Bấm ngoài: đóng đúng lớp này, và chặn lớp dưới nhận cùng cú bấm.
1590
+ *
1591
+ * Điều kiện chặn là "CÒN LỚP NÀO bên dưới", không phải "còn lớp nào
1592
+ * khai `contains`". Modal đóng qua `onBackdropClick` — sự kiện click
1593
+ * trên chính lớp nền, KHÔNG đi qua ngăn xếp này — nên nó không khai
1594
+ * `contains`. Lọc theo `contains` thì `hasLayerBelow` ra `false` và
1595
+ * modal vẫn đóng cùng lượt: đo /modal thấy bấm một cú là mất cả hai.
1596
+ *
1597
+ * Chặn vô điều kiện cũng sai: khi CHỈ có một lớp mở, nút mà người dùng
1598
+ * vừa bấm không nhận được sự kiện. Đo bằng bộ test thư viện: 1-2 ca đỏ
1599
+ * mỗi lượt, 4/4 lượt, trong khi mã gốc 3/3 lượt xanh.
1600
+ *
1601
+ * Nên: có lớp dưới thì chặn, không có thì để sự kiện đi tiếp.
1602
+ */
1603
+ if (i > 0)
1604
+ event.stopPropagation();
1605
+ layer.close();
1606
+ return;
1607
+ }
1608
+ };
1609
+ // `capture: true` để chạy TRƯỚC `@HostListener` của component, nhờ đó
1610
+ // `stopPropagation` mới chặn được chúng.
1611
+ this.doc.addEventListener('click', this.clickListener, true);
1612
+ }
1613
+ /** Gỡ hai trình nghe khi lớp cuối cùng đóng — không để chúng nghe vô ích. */
1614
+ teardownListenersIfIdle() {
1615
+ if (this.openLayers.length > 0)
1574
1616
  return;
1575
- this.doc.removeEventListener('keydown', this.escapeListener, true);
1576
- this.escapeListener = null;
1617
+ if (this.escapeListener) {
1618
+ this.doc.removeEventListener('keydown', this.escapeListener, true);
1619
+ this.escapeListener = null;
1620
+ }
1621
+ if (this.clickListener) {
1622
+ this.doc.removeEventListener('click', this.clickListener, true);
1623
+ this.clickListener = null;
1624
+ }
1577
1625
  }
1578
1626
  /**
1579
1627
  * @deprecated Dùng `attach()`. Giữ lại vì đã publish trong
@@ -1897,7 +1945,23 @@ class OlTooltip {
1897
1945
  * Computed tooltip classes based on theme
1898
1946
  */
1899
1947
  tooltipClasses = computed(() => {
1900
- const baseClasses = 'px-3 py-2 rounded-lg text-sm font-medium whitespace-nowrap z-tooltip max-w-xs';
1948
+ /*
1949
+ * KHÔNG `whitespace-nowrap` — nó mâu thuẫn với `max-w-xs` đứng ngay cạnh.
1950
+ *
1951
+ * Hai class này cùng lúc thì `nowrap` thắng: chữ không được phép xuống
1952
+ * dòng nên `max-width` chỉ bó cái HỘP lại, còn chữ cứ chạy thẳng ra ngoài.
1953
+ * Đo /tooltip ngày 2026-09-25 với nội dung 160 ký tự: hộp đúng 320px
1954
+ * nhưng `scrollWidth` lên **1070px** — chữ tràn khỏi nền tối ra cả hai
1955
+ * phía. Chụp ảnh thấy rõ.
1956
+ *
1957
+ * `break-words` (`overflow-wrap: break-word`) lo nốt từ đơn dài hơn 320px
1958
+ * — ví dụ một URL hay mã lớp không có dấu cách: không có nó thì từ đó vẫn
1959
+ * tràn dù đã bỏ `nowrap`.
1960
+ *
1961
+ * Tooltip ngắn KHÔNG bị ảnh hưởng: `max-w-xs` là giới hạn TRÊN, hộp vẫn
1962
+ * ôm sát nội dung khi chữ ngắn hơn.
1963
+ */
1964
+ const baseClasses = 'px-3 py-2 rounded-lg text-sm font-medium break-words z-tooltip max-w-xs';
1901
1965
  // Figma `ol-tooltip` chỉ định nghĩa nền tối; hai theme `light` và `brand`
1902
1966
  // là mở rộng NGOÀI Figma nên giữ bậc số cho tới khi thiết kế định nghĩa.
1903
1967
  //
@@ -2753,6 +2817,200 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
2753
2817
  }, template: "<!--\n `olTooltip` l\u00E0 `input.required<string>()` nh\u01B0ng directive t\u1EF1 b\u1ECF qua chu\u1ED7i\n R\u1ED6NG (xem `tooltipDisabled` v\u00E0 nh\u00E1nh `if (!content)` trong directive), n\u00EAn\n truy\u1EC1n `''` cho n\u00FAt c\u00F3 ch\u1EEF l\u00E0 an to\u00E0n \u2014 kh\u00F4ng c\u1EA7n `@if` b\u1ECDc ngo\u00E0i, v\u1ED1n s\u1EBD\n nh\u00E2n \u0111\u00F4i c\u1EA3 kh\u1ED1i template.\n-->\n<button\n [olTooltip]=\"tooltipText()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-expanded]=\"ariaExpanded() === null ? null : ariaExpanded()\"\n [attr.aria-controls]=\"ariaControls() || null\"\n [attr.aria-pressed]=\"ariaPressed() === null ? null : ariaPressed()\"\n [type]=\"type()\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"handleClick($event)\"\n [class]=\"buttonClasses()\"\n>\n @if (loading()) {\n <svg\n class=\"animate-spin shrink-0\"\n [style.width.px]=\"iconPx()\"\n [style.height.px]=\"iconPx()\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <ng-content select=\"[leftIcon]\" />\n <ng-content select=\"[logo]\" />\n <!--\n `iconOnly` \u1EA9n ch\u1EEF b\u1EB1ng `sr-only`, KH\u00D4NG b\u1EB1ng `@if`: Angular chi\u1EBFu n\u1ED9i dung\n \u0111\u00FAng m\u1ED9t l\u1EA7n, \u0111\u1EB7t `<ng-content />` trong c\u00E1c nh\u00E1nh lo\u1EA1i tr\u1EEB nhau l\u00E0 m\u1EA5t ru\u1ED9t\n \u1EDF m\u1ECDi nh\u00E1nh. Gi\u1EEF ch\u1EEF trong DOM c\u00F2n \u0111\u00FAng v\u1EC1 tr\u1EE3 n\u0103ng \u2014 n\u00FAt ch\u1EC9-icon ph\u1EA3i c\u00F3\n t\u00EAn \u0111\u1ECDc \u0111\u01B0\u1EE3c.\n -->\n <span data-noi-dung [class.sr-only]=\"iconOnly()\">\n <ng-content />\n </span>\n <ng-content select=\"[rightIcon]\" />\n</button>\n", styles: [".ol-btn-ico-14 ::ng-deep ol-icon,.ol-btn-ico-14 ::ng-deep ol-icon>span{width:14px!important;height:14px!important}.ol-btn-ico-16 ::ng-deep ol-icon,.ol-btn-ico-16 ::ng-deep ol-icon>span{width:16px!important;height:16px!important}.ol-btn-ico-20 ::ng-deep ol-icon,.ol-btn-ico-20 ::ng-deep ol-icon>span{width:20px!important;height:20px!important}[data-noi-dung].sr-only:has(ol-icon,svg,img){position:static;width:auto;height:auto;margin:0;padding:0;overflow:visible;clip:auto;clip-path:none;white-space:normal;font-size:0;line-height:0}.ol-btn-loading ::ng-deep ol-icon,.ol-btn-loading ::ng-deep>svg:not(.animate-spin),.ol-btn-loading ::ng-deep>img{display:none!important}.ol-btn-ico-14 ::ng-deep>svg:not(.animate-spin),.ol-btn-ico-14 ::ng-deep>img{width:14px;height:14px}.ol-btn-ico-16 ::ng-deep>svg:not(.animate-spin),.ol-btn-ico-16 ::ng-deep>img{width:16px;height:16px}.ol-btn-ico-20 ::ng-deep>svg:not(.animate-spin),.ol-btn-ico-20 ::ng-deep>img{width:20px;height:20px}\n"] }]
2754
2818
  }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], pill: [{ type: i0.Input, args: [{ isSignal: true, alias: "pill", required: false }] }], outline: [{ type: i0.Input, args: [{ isSignal: true, alias: "outline", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaExpanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaExpanded", required: false }] }], ariaControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaControls", required: false }] }], ariaPressed: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaPressed", required: false }] }], btnClick: [{ type: i0.Output, args: ["btnClick"] }] } });
2755
2819
 
2820
+ /**
2821
+ * Tiêu đề Modal — khớp Figma `ol-modal-header` (node 1149:71403).
2822
+ * Figma có 7 biến thể: Default, Heading & supporting text, Icon + Heading &
2823
+ * helper text, With Logo, Logo & icons, Avatar group, Breadcrumb.
2824
+ * Component lo phần khung dùng chung; nội dung giàu hơn lắp qua <ng-content>.
2825
+ */
2826
+ /*
2827
+ * GHÉP VỚI `ol-modal`: lắp thẳng vào, KHÔNG phải tắt gì cả.
2828
+ *
2829
+ * `ol-modal` dùng `contentChild(OlModalHeader)` để tự nhận ra có header lồng
2830
+ * vào rồi thôi không dựng phần đầu mặc định (xem `showDefaultHeader`).
2831
+ *
2832
+ * ```html
2833
+ * <ol-modal [(isOpen)]="open">
2834
+ * <ol-modal-header heading="Tiêu đề" (closeClick)="open.set(false)" />
2835
+ * …nội dung…
2836
+ * </ol-modal>
2837
+ * ```
2838
+ *
2839
+ * Trước đây phải tự tắt bằng `title=""` kèm `[dismissible]="false"`, quên một
2840
+ * trong hai là lồng HAI khung header chồng nhau và nút đóng hiện hai lần —
2841
+ * build vẫn xanh nên chỉ nhìn mới thấy.
2842
+ *
2843
+ * Nút đóng: header này có `closeClick` riêng, người dùng tự nối. `ol-modal`
2844
+ * không chen nút vào header lồng ngoài.
2845
+ */
2846
+ class OlModalHeader {
2847
+ /**
2848
+ * Bậc thẻ tiêu đề (h1–h6). Mặc định 3 — giữ nguyên bậc đang chạy.
2849
+ *
2850
+ * Khoá cứng một bậc thì nơi gọi không sửa được: đặt component này sau
2851
+ * `<h2>` của trang là nhảy bậc.
2852
+ */
2853
+ headingLevel = input(3, /* @ts-ignore */
2854
+ ...(ngDevMode ? [{ debugName: "headingLevel" }] : /* istanbul ignore next */ []));
2855
+ heading = input('', /* @ts-ignore */
2856
+ ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
2857
+ /**
2858
+ * Icon đầu tiêu đề. Rỗng thì KHÔNG dựng khung — không để lại thẻ trống.
2859
+ *
2860
+ * Figma `Type=Icon + Heading & helper text` (#1149:71412) vẽ khung 48px bo
2861
+ * 12px, icon 28px bên trong. Đó đúng là `ol-icon-shape` size `lg`, nên dùng
2862
+ * lại thay vì tự vẽ khung.
2863
+ *
2864
+ * `color="white"` chứ KHÔNG `gray`: Figma bind nền
2865
+ * `bg-neutral-primary-medium` (trắng) kèm viền `border-default-medium`, mà
2866
+ * trong `ol-icon-shape` chỉ nhánh `white` có cặp đó — `gray` ra nền xám đặc
2867
+ * và KHÔNG viền. Đo /modal lượt đầu: nền rgb(243,244,246), viền 0px.
2868
+ */
2869
+ icon = input('', /* @ts-ignore */
2870
+ ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
2871
+ /**
2872
+ * Mô tả phụ dưới tiêu đề.
2873
+ *
2874
+ * Tên `description` theo `ol-alert-dialog` — thứ dựng trên chính `ol-modal`
2875
+ * — và `ol-empty`. `helperText` mang nghĩa "chú thích dưới ô nhập" ở họ form
2876
+ * (16 component dùng), đặt cho tiêu đề hộp thoại là lệch nghĩa.
2877
+ */
2878
+ description = input('', /* @ts-ignore */
2879
+ ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
2880
+ /** @deprecated Dùng `description`. Giữ để không phá mã đang chạy. */
2881
+ supportingText = input('', /* @ts-ignore */
2882
+ ...(ngDevMode ? [{ debugName: "supportingText" }] : /* istanbul ignore next */ []));
2883
+ /** @deprecated Dùng `description`. Tên cũ của `ol-drawer-header`. */
2884
+ helperText = input('', /* @ts-ignore */
2885
+ ...(ngDevMode ? [{ debugName: "helperText" }] : /* istanbul ignore next */ []));
2886
+ /**
2887
+ * Cái nào được TRUYỀN thì thắng — không phụ thuộc giá trị.
2888
+ *
2889
+ * `ol-badge` từng hoà giải hai input cùng nghĩa bằng cách so với giá trị mặc
2890
+ * định (`theme` thắng trừ khi nó bằng `'gray'`), nên truyền cả hai thì một
2891
+ * cái bị nuốt im lặng. Ở đây chuỗi rỗng là "không truyền", nên chỉ cần lấy
2892
+ * cái đầu tiên khác rỗng.
2893
+ */
2894
+ descriptionText = computed(() => this.description() || this.supportingText() || this.helperText(), /* @ts-ignore */
2895
+ ...(ngDevMode ? [{ debugName: "descriptionText" }] : /* istanbul ignore next */ []));
2896
+ showClose = input(true, /* @ts-ignore */
2897
+ ...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
2898
+ closeClick = output();
2899
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
2900
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModalHeader, isStandalone: true, selector: "ol-modal-header", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, supportingText: { classPropertyName: "supportingText", publicName: "supportingText", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, showClose: { classPropertyName: "showClose", publicName: "showClose", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeClick: "closeClick" }, host: { classAttribute: "block" }, ngImport: i0, template: `
2901
+ <!--
2902
+ PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
2903
+ kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
2904
+ và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
2905
+ CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
2906
+ -->
2907
+ <div class="flex items-start justify-between gap-3 p-3">
2908
+ @if (icon()) {
2909
+ <ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
2910
+ }
2911
+ <div class="min-w-0 flex-1">
2912
+ @if (heading()) {
2913
+ @switch (headingLevel()) {
2914
+ @case (1) {
2915
+ <h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
2916
+ }
2917
+ @case (2) {
2918
+ <h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
2919
+ }
2920
+ @case (4) {
2921
+ <h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
2922
+ }
2923
+ @case (5) {
2924
+ <h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
2925
+ }
2926
+ @case (6) {
2927
+ <h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
2928
+ }
2929
+ @default {
2930
+ <h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
2931
+ }
2932
+ }
2933
+ }
2934
+ @if (descriptionText()) {
2935
+ <p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
2936
+ }
2937
+ <ng-content />
2938
+ </div>
2939
+ @if (showClose()) {
2940
+ <button
2941
+ type="button"
2942
+ (click)="closeClick.emit()"
2943
+ aria-label="Đóng hộp thoại"
2944
+ class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
2945
+ >
2946
+ <ol-icon name="close" size="sm" />
2947
+ </button>
2948
+ }
2949
+ </div>
2950
+ `, isInline: true, dependencies: [{ kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2951
+ }
2952
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, decorators: [{
2953
+ type: Component,
2954
+ args: [{
2955
+ selector: 'ol-modal-header',
2956
+ standalone: true,
2957
+ imports: [OlIcon, OlIconShape],
2958
+ template: `
2959
+ <!--
2960
+ PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
2961
+ kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
2962
+ và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
2963
+ CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
2964
+ -->
2965
+ <div class="flex items-start justify-between gap-3 p-3">
2966
+ @if (icon()) {
2967
+ <ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
2968
+ }
2969
+ <div class="min-w-0 flex-1">
2970
+ @if (heading()) {
2971
+ @switch (headingLevel()) {
2972
+ @case (1) {
2973
+ <h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
2974
+ }
2975
+ @case (2) {
2976
+ <h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
2977
+ }
2978
+ @case (4) {
2979
+ <h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
2980
+ }
2981
+ @case (5) {
2982
+ <h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
2983
+ }
2984
+ @case (6) {
2985
+ <h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
2986
+ }
2987
+ @default {
2988
+ <h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
2989
+ }
2990
+ }
2991
+ }
2992
+ @if (descriptionText()) {
2993
+ <p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
2994
+ }
2995
+ <ng-content />
2996
+ </div>
2997
+ @if (showClose()) {
2998
+ <button
2999
+ type="button"
3000
+ (click)="closeClick.emit()"
3001
+ aria-label="Đóng hộp thoại"
3002
+ class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
3003
+ >
3004
+ <ol-icon name="close" size="sm" />
3005
+ </button>
3006
+ }
3007
+ </div>
3008
+ `,
3009
+ changeDetection: ChangeDetectionStrategy.OnPush,
3010
+ host: { class: 'block' },
3011
+ }]
3012
+ }], propDecorators: { headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], supportingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "supportingText", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], showClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClose", required: false }] }], closeClick: [{ type: i0.Output, args: ["closeClick"] }] } });
3013
+
2756
3014
  class OlModal {
2757
3015
  focusTrap = inject(OlFocusTrapService);
2758
3016
  destroyRef = inject(DestroyRef);
@@ -2781,10 +3039,81 @@ class OlModal {
2781
3039
  ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
2782
3040
  title = input('', /* @ts-ignore */
2783
3041
  ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
3042
+ /**
3043
+ * Icon đứng trước tiêu đề ở phần đầu mặc định.
3044
+ *
3045
+ * Thêm 2026-09-24 cùng `description`. Trước đó `ol-modal` chỉ có `title`,
3046
+ * nên muốn có icon hay dòng mô tả là phải lắp `ol-modal-header` thủ công —
3047
+ * và lắp rồi thì `title` bị bỏ qua, phải khai lại qua `heading`. Đo trên
3048
+ * /modal: 13 chỗ dùng modal, chỗ nào cần icon đều lặp lại đúng khuôn đó.
3049
+ *
3050
+ * Vẫn dùng `ol-modal-header` được khi cần thứ phần đầu mặc định không có
3051
+ * (`supportingText`, `helperText`, nút tuỳ biến); lắp nó vào thì phần đầu
3052
+ * mặc định tự tắt, xem `showDefaultHeader`.
3053
+ */
3054
+ icon = input('', /* @ts-ignore */
3055
+ ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
3056
+ /** Dòng mô tả dưới tiêu đề ở phần đầu mặc định. Xem chú thích ở `icon`. */
3057
+ description = input('', /* @ts-ignore */
3058
+ ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
2784
3059
  size = input('base', /* @ts-ignore */
2785
3060
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3061
+ /**
3062
+ * Cho phép đóng hộp thoại bằng Escape hoặc bấm ra nền mờ.
3063
+ *
3064
+ * Tắt khi người dùng BUỘC phải chọn một phương án — xác nhận xoá, đồng ý
3065
+ * điều khoản. Không còn ẩn nút X nữa: xem `showClose`.
3066
+ */
2786
3067
  dismissible = input(true, /* @ts-ignore */
2787
3068
  ...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
3069
+ /**
3070
+ * Có vẽ nút X ở góc phải phần đầu không. Mặc định **theo `dismissible`**.
3071
+ *
3072
+ * TÁCH KHỎI `dismissible` (2026-09-24). Trước đây một input gánh BỐN việc —
3073
+ * hiện nút X, đóng bằng Escape, đóng khi bấm nền, và quyết định có dựng
3074
+ * phần đầu — nên `[dismissible]="false"` (chỉ định chặn bấm-ra-ngoài) kéo
3075
+ * theo MẤT nút X. Người dùng phải tự lắp `ol-modal-header` để lấy lại nút,
3076
+ * tức phải khai lại cả tiêu đề vốn đã truyền qua `title`.
3077
+ *
3078
+ * Nay ba trường hợp tách bạch:
3079
+ *
3080
+ * `[dismissible]="false"` chặn Escape + nền mờ, VẪN có nút X
3081
+ * `[dismissible]="false" [showClose]="false"` không lối thoát nào ngoài nút trong nội dung
3082
+ * `[showClose]="false"` giấu nút X, vẫn đóng được bằng Escape/nền
3083
+ *
3084
+ * Mặc định `true` — nút X LUÔN hiện trừ khi tắt tường minh.
3085
+ *
3086
+ * ĐÂY LÀ THAY ĐỔI PHÁ VỠ so với bản trước: `[dismissible]="false"` vốn ẩn
3087
+ * nút X, nay vẫn giữ. Nơi nào thật sự muốn không có lối thoát nào phải thêm
3088
+ * `[showClose]="false"`. Đổi vì hành vi cũ là LỖI chứ không phải lựa chọn:
3089
+ * người dùng chặn bấm-ra-ngoài rồi mất nút đóng, phải tự lắp
3090
+ * `ol-modal-header` để lấy lại — tức khai lại tiêu đề đã truyền qua `title`.
3091
+ */
3092
+ showClose = input(true, /* @ts-ignore */
3093
+ ...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
3094
+ /** Nút X có hiện không. Tách hẳn khỏi `dismissible` từ 2026-09-24. */
3095
+ closeVisible = computed(() => this.showClose(), /* @ts-ignore */
3096
+ ...(ngDevMode ? [{ debugName: "closeVisible" }] : /* istanbul ignore next */ []));
3097
+ /**
3098
+ * Có `<ol-modal-header>` lồng bên trong không.
3099
+ *
3100
+ * Dùng để KHÔNG dựng phần đầu mặc định khi người dùng đã tự lắp header
3101
+ * riêng. Trước đây phải tắt tay bằng `title=""` kèm `[dismissible]="false"`
3102
+ * — quên một trong hai là lồng HAI khung header chồng nhau, mà build vẫn
3103
+ * xanh nên chỉ nhìn mới thấy.
3104
+ *
3105
+ * `descendants: false` để chỉ bắt header là CON TRỰC TIẾP. Một `ol-modal`
3106
+ * lồng trong nội dung của modal này thì header của nó không tính.
3107
+ */
3108
+ projectedHeader = contentChild(OlModalHeader, { ...(ngDevMode ? { debugName: "projectedHeader" } : /* istanbul ignore next */ {}), descendants: false });
3109
+ /**
3110
+ * Có dựng phần đầu mặc định không.
3111
+ *
3112
+ * Người dùng lắp `ol-modal-header` riêng thì thôi — nếu không sẽ có hai
3113
+ * khung `p-3` chồng nhau, nút đóng hiện hai lần.
3114
+ */
3115
+ showDefaultHeader = computed(() => !this.projectedHeader() && (this.title() !== '' || this.closeVisible()), /* @ts-ignore */
3116
+ ...(ngDevMode ? [{ debugName: "showDefaultHeader" }] : /* istanbul ignore next */ []));
2788
3117
  backdrop = input('dynamic', /* @ts-ignore */
2789
3118
  ...(ngDevMode ? [{ debugName: "backdrop" }] : /* istanbul ignore next */ []));
2790
3119
  placement = input('center', /* @ts-ignore */
@@ -2891,12 +3220,12 @@ class OlModal {
2891
3220
  return this.isOpen();
2892
3221
  }
2893
3222
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModal, deps: [], target: i0.ɵɵFactoryTarget.Component });
2894
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModal, isStandalone: true, selector: "ol-modal", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, backdrop: { classPropertyName: "backdrop", publicName: "backdrop", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", opened: "opened", closed: "closed" }, viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isOpen()) {\n <div\n (click)=\"onBackdropClick($event)\"\n class=\"fixed inset-0 z-modal overflow-y-auto overflow-x-hidden flex w-full h-full bg-overlay-backdrop backdrop-blur-xs transition-opacity duration-200\"\n [class]=\"placementClasses()\"\n >\n <div\n #panel\n [attr.role]=\"role()\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title() || 'H\u1ED9p tho\u1EA1i'\"\n tabindex=\"-1\"\n class=\"relative w-full max-h-full transition-all duration-200 animate-in fade-in zoom-in-95\"\n [class]=\"sizeClasses()\"\n >\n <div\n class=\"relative bg-neutral-primary rounded-md shadow-2xl border border-default overflow-hidden\"\n >\n @if (title() || dismissible()) {\n <div class=\"flex items-center justify-between p-3\">\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"text-xl font-bold text-heading\">{{ title() }}</h1>\n }\n @case (2) {\n <h2 class=\"text-xl font-bold text-heading\">{{ title() }}</h2>\n }\n @case (4) {\n <h4 class=\"text-xl font-bold text-heading\">{{ title() }}</h4>\n }\n @case (5) {\n <h5 class=\"text-xl font-bold text-heading\">{{ title() }}</h5>\n }\n @case (6) {\n <h6 class=\"text-xl font-bold text-heading\">{{ title() }}</h6>\n }\n @default {\n <h3 class=\"text-xl font-bold text-heading\">{{ title() }}</h3>\n }\n }\n @if (dismissible()) {\n <button\n type=\"button\"\n (click)=\"hide()\"\n class=\"text-body-subtle hover:text-heading hover:bg-neutral-tertiary rounded-md text-sm w-9 h-9 ms-auto inline-flex justify-center items-center cursor-pointer transition-colors focus:outline-hidden focus:ring-2 focus:ring-brand-subtle\"\n aria-label=\"\u0110\u00F3ng h\u1ED9p tho\u1EA1i\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n }\n </div>\n }\n\n <!-- Modal Body Content -->\n <!--\n \u0110\u1EC7m 12px B\u1EB0NG header v\u00E0 footer \u2014 ch\u1EEF th\u00E2n b\u00E0i th\u1EB3ng h\u00E0ng v\u1EDBi ti\u00EAu \u0111\u1EC1.\n Tr\u01B0\u1EDBc \u0111\u00E2y `p-6` (24px) trong khi hai ph\u1EA7n kia `p-3`, n\u00EAn n\u1ED9i dung\n th\u1EE5t v\u00E0o s\u00E2u h\u01A1n ti\u00EAu \u0111\u1EC1 \u0111\u00FAng 12px, nh\u00ECn nh\u01B0 l\u1ED7i c\u0103n l\u1EC1.\n -->\n <div class=\"p-3 space-y-4 text-body leading-relaxed text-sm md:text-base\">\n <ng-content />\n </div>\n\n <!-- Modal Footer Actions Slot -->\n <div footer-frame class=\"p-3 flex items-center justify-end gap-3\">\n <ng-content select=\"[footer]\">\n <button\n type=\"button\"\n (click)=\"hide()\"\n class=\"px-4 py-2 text-sm font-medium rounded-md text-body bg-neutral-primary border border-default hover:bg-neutral-tertiary cursor-pointer transition-colors focus:outline-hidden focus:ring-2 focus:ring-brand-subtle\"\n >\n \u0110\u00F3ng\n </button>\n </ng-content>\n </div>\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3223
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModal, isStandalone: true, selector: "ol-modal", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, showClose: { classPropertyName: "showClose", publicName: "showClose", isSignal: true, isRequired: false, transformFunction: null }, backdrop: { classPropertyName: "backdrop", publicName: "backdrop", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", opened: "opened", closed: "closed" }, queries: [{ propertyName: "projectedHeader", first: true, predicate: OlModalHeader, isSignal: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isOpen()) {\n <div\n (click)=\"onBackdropClick($event)\"\n class=\"fixed inset-0 z-modal overflow-y-auto overflow-x-hidden flex w-full h-full bg-overlay-backdrop backdrop-blur-xs transition-opacity duration-200\"\n [class]=\"placementClasses()\"\n >\n <div\n #panel\n [attr.role]=\"role()\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title() || 'H\u1ED9p tho\u1EA1i'\"\n tabindex=\"-1\"\n class=\"relative w-full max-h-full transition-all duration-200 animate-in fade-in zoom-in-95\"\n [class]=\"sizeClasses()\"\n >\n <div\n class=\"relative bg-neutral-primary rounded-md shadow-2xl border border-default overflow-hidden\"\n >\n <!--\n Ph\u1EA7n \u0111\u1EA7u M\u1EB6C \u0110\u1ECANH \u2014 ch\u1EC9 d\u1EF1ng khi ng\u01B0\u1EDDi d\u00F9ng KH\u00D4NG t\u1EF1 l\u1EAFp\n `<ol-modal-header>`. Xem `showDefaultHeader` trong `.ts`: l\u1EAFp c\u1EA3 hai\n l\u00E0 hai khung `p-3` ch\u1ED3ng nhau v\u00E0 n\u00FAt \u0111\u00F3ng hi\u1EC7n hai l\u1EA7n.\n -->\n <!--\n Ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh D\u00D9NG L\u1EA0I `ol-modal-header` thay v\u00EC d\u1EF1ng l\u1EA1i s\u00E1u\n nh\u00E1nh `@switch` cho `headingLevel` c\u1ED9ng m\u1ED9t n\u00FAt \u0111\u00F3ng.\n\n B\u1EA3n c\u0169 ch\u00E9p khu\u00F4n: modal c\u00F3 `title` th\u00EC t\u1EF1 v\u1EBD `<h1..h6>` + n\u00FAt X, c\u00F2n\n ai c\u1EA7n `icon` hay `description` ph\u1EA3i l\u1EAFp `ol-modal-header` th\u1EE7 c\u00F4ng \u2014\n m\u00E0 l\u1EAFp r\u1ED3i th\u00EC `title` b\u1ECB b\u1ECF qua, ph\u1EA3i khai l\u1EA1i qua `heading`. Hai\n khu\u00F4n song song n\u00EAn s\u1EEDa m\u1ED9t b\u00EAn l\u00E0 l\u1EC7ch b\u00EAn kia.\n\n Nay `ol-modal` nh\u1EADn th\u1EB3ng `icon`/`description` v\u00E0 chuy\u1EC3n xu\u1ED1ng. L\u1EAFp\n `ol-modal-header` ri\u00EAng v\u1EABn \u0111\u01B0\u1EE3c khi c\u1EA7n th\u1EE9 ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh kh\u00F4ng\n c\u00F3 (`supportingText`, `helperText`) \u2014 l\u00FAc \u0111\u00F3 `showDefaultHeader` t\u1EF1\n t\u1EAFt, kh\u00F4ng c\u00F3 hai khung ch\u1ED3ng nhau.\n -->\n @if (showDefaultHeader()) {\n <ol-modal-header\n [heading]=\"title()\"\n [headingLevel]=\"headingLevel()\"\n [icon]=\"icon()\"\n [description]=\"description()\"\n [showClose]=\"closeVisible()\"\n (closeClick)=\"hide()\"\n />\n }\n\n <!-- Modal Body Content -->\n <!--\n \u0110\u1EC7m 12px B\u1EB0NG header v\u00E0 footer \u2014 ch\u1EEF th\u00E2n b\u00E0i th\u1EB3ng h\u00E0ng v\u1EDBi ti\u00EAu \u0111\u1EC1.\n Tr\u01B0\u1EDBc \u0111\u00E2y `p-6` (24px) trong khi hai ph\u1EA7n kia `p-3`, n\u00EAn n\u1ED9i dung\n th\u1EE5t v\u00E0o s\u00E2u h\u01A1n ti\u00EAu \u0111\u1EC1 \u0111\u00FAng 12px, nh\u00ECn nh\u01B0 l\u1ED7i c\u0103n l\u1EC1.\n -->\n <div class=\"p-3 space-y-4 text-body leading-relaxed text-sm md:text-base\">\n <ng-content />\n </div>\n\n <!--\n Modal Footer Actions Slot.\n\n `[&>[footer]]:*` \u00E1p th\u1EB3ng l\u00EAn PH\u1EA6N T\u1EEC \u0110\u01AF\u1EE2C CHI\u1EBEU V\u00C0O, kh\u00F4ng ch\u1EC9 l\u00EAn\n khung n\u00E0y. N\u01A1i d\u00F9ng g\u1EA7n nh\u01B0 lu\u00F4n b\u1ECDc n\u00FAt trong m\u1ED9t `<div footer>`, m\u00E0\n `gap-3` c\u1EE7a khung th\u00EC \u00E1p gi\u1EEFa khung v\u00E0 c\u00E1c con TR\u1EF0C TI\u1EBEP c\u1EE7a n\u00F3 \u2014\n t\u1EE9c gi\u1EEFa c\u00E1i `div` \u0111\u00F3 v\u00E0 anh em c\u1EE7a n\u00F3, KH\u00D4NG ph\u1EA3i gi\u1EEFa hai n\u00FAt b\u00EAn\n trong. \u0110o /modal ng\u00E0y 2026-09-24: modal \"Giao b\u00E0i t\u1EADp m\u1EDBi\" c\u00F3 hai n\u00FAt\n c\u00E1ch nhau \u0111\u00FAng **0px**, d\u00EDnh li\u1EC1n.\n\n Qu\u00E9t to\u00E0n repo l\u00FAc \u0111\u00F3: 13 ch\u1ED7 d\u00F9ng `<div footer>`, ch\u1EC9 1 ch\u1ED7 t\u1EF1 nh\u1EDB\n th\u00EAm `class=\"flex gap-2\"`; 0 ch\u1ED7 d\u00F9ng `ol-modal-footer`. B\u1EAFt n\u01A1i d\u00F9ng\n nh\u1EDB m\u1ED9t class m\u00E0 12/13 ch\u1ED7 qu\u00EAn th\u00EC \u0111\u00F3 l\u00E0 l\u1ED7i c\u1EE7a API, kh\u00F4ng ph\u1EA3i l\u1ED7i\n ng\u01B0\u1EDDi d\u00F9ng \u2014 n\u00EAn component t\u1EF1 lo.\n\n `justify-end` v\u00E0 `gap-3` gi\u1EEF nguy\u00EAn tr\u00EAn khung \u0111\u1EC3 nh\u00E1nh KH\u00D4NG b\u1ECDc\n `div` (chi\u1EBFu th\u1EB3ng nhi\u1EC1u `ol-button`) v\u1EABn \u0111\u00FAng.\n -->\n <div\n footer-frame\n class=\"p-3 flex items-center justify-end gap-3 [&>[footer]]:flex [&>[footer]]:flex-wrap [&>[footer]]:items-center [&>[footer]]:gap-3\"\n >\n <ng-content select=\"[footer]\">\n <button\n type=\"button\"\n (click)=\"hide()\"\n class=\"px-4 py-2 text-sm font-medium rounded-md text-body bg-neutral-primary border border-default hover:bg-neutral-tertiary cursor-pointer transition-colors focus:outline-hidden focus:ring-2 focus:ring-brand-subtle\"\n >\n \u0110\u00F3ng\n </button>\n </ng-content>\n </div>\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlModalHeader, selector: "ol-modal-header", inputs: ["headingLevel", "heading", "icon", "description", "supportingText", "helperText", "showClose"], outputs: ["closeClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2895
3224
  }
2896
3225
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModal, decorators: [{
2897
3226
  type: Component,
2898
- args: [{ selector: 'ol-modal', standalone: true, imports: [CommonModule, OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (isOpen()) {\n <div\n (click)=\"onBackdropClick($event)\"\n class=\"fixed inset-0 z-modal overflow-y-auto overflow-x-hidden flex w-full h-full bg-overlay-backdrop backdrop-blur-xs transition-opacity duration-200\"\n [class]=\"placementClasses()\"\n >\n <div\n #panel\n [attr.role]=\"role()\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title() || 'H\u1ED9p tho\u1EA1i'\"\n tabindex=\"-1\"\n class=\"relative w-full max-h-full transition-all duration-200 animate-in fade-in zoom-in-95\"\n [class]=\"sizeClasses()\"\n >\n <div\n class=\"relative bg-neutral-primary rounded-md shadow-2xl border border-default overflow-hidden\"\n >\n @if (title() || dismissible()) {\n <div class=\"flex items-center justify-between p-3\">\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"text-xl font-bold text-heading\">{{ title() }}</h1>\n }\n @case (2) {\n <h2 class=\"text-xl font-bold text-heading\">{{ title() }}</h2>\n }\n @case (4) {\n <h4 class=\"text-xl font-bold text-heading\">{{ title() }}</h4>\n }\n @case (5) {\n <h5 class=\"text-xl font-bold text-heading\">{{ title() }}</h5>\n }\n @case (6) {\n <h6 class=\"text-xl font-bold text-heading\">{{ title() }}</h6>\n }\n @default {\n <h3 class=\"text-xl font-bold text-heading\">{{ title() }}</h3>\n }\n }\n @if (dismissible()) {\n <button\n type=\"button\"\n (click)=\"hide()\"\n class=\"text-body-subtle hover:text-heading hover:bg-neutral-tertiary rounded-md text-sm w-9 h-9 ms-auto inline-flex justify-center items-center cursor-pointer transition-colors focus:outline-hidden focus:ring-2 focus:ring-brand-subtle\"\n aria-label=\"\u0110\u00F3ng h\u1ED9p tho\u1EA1i\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n }\n </div>\n }\n\n <!-- Modal Body Content -->\n <!--\n \u0110\u1EC7m 12px B\u1EB0NG header v\u00E0 footer \u2014 ch\u1EEF th\u00E2n b\u00E0i th\u1EB3ng h\u00E0ng v\u1EDBi ti\u00EAu \u0111\u1EC1.\n Tr\u01B0\u1EDBc \u0111\u00E2y `p-6` (24px) trong khi hai ph\u1EA7n kia `p-3`, n\u00EAn n\u1ED9i dung\n th\u1EE5t v\u00E0o s\u00E2u h\u01A1n ti\u00EAu \u0111\u1EC1 \u0111\u00FAng 12px, nh\u00ECn nh\u01B0 l\u1ED7i c\u0103n l\u1EC1.\n -->\n <div class=\"p-3 space-y-4 text-body leading-relaxed text-sm md:text-base\">\n <ng-content />\n </div>\n\n <!-- Modal Footer Actions Slot -->\n <div footer-frame class=\"p-3 flex items-center justify-end gap-3\">\n <ng-content select=\"[footer]\">\n <button\n type=\"button\"\n (click)=\"hide()\"\n class=\"px-4 py-2 text-sm font-medium rounded-md text-body bg-neutral-primary border border-default hover:bg-neutral-tertiary cursor-pointer transition-colors focus:outline-hidden focus:ring-2 focus:ring-brand-subtle\"\n >\n \u0110\u00F3ng\n </button>\n </ng-content>\n </div>\n </div>\n </div>\n </div>\n}\n" }]
2899
- }], ctorParameters: () => [], propDecorators: { panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }, { type: i0.Output, args: ["isOpenChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], backdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdrop", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
3227
+ args: [{ selector: 'ol-modal', standalone: true, imports: [CommonModule, OlModalHeader], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (isOpen()) {\n <div\n (click)=\"onBackdropClick($event)\"\n class=\"fixed inset-0 z-modal overflow-y-auto overflow-x-hidden flex w-full h-full bg-overlay-backdrop backdrop-blur-xs transition-opacity duration-200\"\n [class]=\"placementClasses()\"\n >\n <div\n #panel\n [attr.role]=\"role()\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title() || 'H\u1ED9p tho\u1EA1i'\"\n tabindex=\"-1\"\n class=\"relative w-full max-h-full transition-all duration-200 animate-in fade-in zoom-in-95\"\n [class]=\"sizeClasses()\"\n >\n <div\n class=\"relative bg-neutral-primary rounded-md shadow-2xl border border-default overflow-hidden\"\n >\n <!--\n Ph\u1EA7n \u0111\u1EA7u M\u1EB6C \u0110\u1ECANH \u2014 ch\u1EC9 d\u1EF1ng khi ng\u01B0\u1EDDi d\u00F9ng KH\u00D4NG t\u1EF1 l\u1EAFp\n `<ol-modal-header>`. Xem `showDefaultHeader` trong `.ts`: l\u1EAFp c\u1EA3 hai\n l\u00E0 hai khung `p-3` ch\u1ED3ng nhau v\u00E0 n\u00FAt \u0111\u00F3ng hi\u1EC7n hai l\u1EA7n.\n -->\n <!--\n Ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh D\u00D9NG L\u1EA0I `ol-modal-header` thay v\u00EC d\u1EF1ng l\u1EA1i s\u00E1u\n nh\u00E1nh `@switch` cho `headingLevel` c\u1ED9ng m\u1ED9t n\u00FAt \u0111\u00F3ng.\n\n B\u1EA3n c\u0169 ch\u00E9p khu\u00F4n: modal c\u00F3 `title` th\u00EC t\u1EF1 v\u1EBD `<h1..h6>` + n\u00FAt X, c\u00F2n\n ai c\u1EA7n `icon` hay `description` ph\u1EA3i l\u1EAFp `ol-modal-header` th\u1EE7 c\u00F4ng \u2014\n m\u00E0 l\u1EAFp r\u1ED3i th\u00EC `title` b\u1ECB b\u1ECF qua, ph\u1EA3i khai l\u1EA1i qua `heading`. Hai\n khu\u00F4n song song n\u00EAn s\u1EEDa m\u1ED9t b\u00EAn l\u00E0 l\u1EC7ch b\u00EAn kia.\n\n Nay `ol-modal` nh\u1EADn th\u1EB3ng `icon`/`description` v\u00E0 chuy\u1EC3n xu\u1ED1ng. L\u1EAFp\n `ol-modal-header` ri\u00EAng v\u1EABn \u0111\u01B0\u1EE3c khi c\u1EA7n th\u1EE9 ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh kh\u00F4ng\n c\u00F3 (`supportingText`, `helperText`) \u2014 l\u00FAc \u0111\u00F3 `showDefaultHeader` t\u1EF1\n t\u1EAFt, kh\u00F4ng c\u00F3 hai khung ch\u1ED3ng nhau.\n -->\n @if (showDefaultHeader()) {\n <ol-modal-header\n [heading]=\"title()\"\n [headingLevel]=\"headingLevel()\"\n [icon]=\"icon()\"\n [description]=\"description()\"\n [showClose]=\"closeVisible()\"\n (closeClick)=\"hide()\"\n />\n }\n\n <!-- Modal Body Content -->\n <!--\n \u0110\u1EC7m 12px B\u1EB0NG header v\u00E0 footer \u2014 ch\u1EEF th\u00E2n b\u00E0i th\u1EB3ng h\u00E0ng v\u1EDBi ti\u00EAu \u0111\u1EC1.\n Tr\u01B0\u1EDBc \u0111\u00E2y `p-6` (24px) trong khi hai ph\u1EA7n kia `p-3`, n\u00EAn n\u1ED9i dung\n th\u1EE5t v\u00E0o s\u00E2u h\u01A1n ti\u00EAu \u0111\u1EC1 \u0111\u00FAng 12px, nh\u00ECn nh\u01B0 l\u1ED7i c\u0103n l\u1EC1.\n -->\n <div class=\"p-3 space-y-4 text-body leading-relaxed text-sm md:text-base\">\n <ng-content />\n </div>\n\n <!--\n Modal Footer Actions Slot.\n\n `[&>[footer]]:*` \u00E1p th\u1EB3ng l\u00EAn PH\u1EA6N T\u1EEC \u0110\u01AF\u1EE2C CHI\u1EBEU V\u00C0O, kh\u00F4ng ch\u1EC9 l\u00EAn\n khung n\u00E0y. N\u01A1i d\u00F9ng g\u1EA7n nh\u01B0 lu\u00F4n b\u1ECDc n\u00FAt trong m\u1ED9t `<div footer>`, m\u00E0\n `gap-3` c\u1EE7a khung th\u00EC \u00E1p gi\u1EEFa khung v\u00E0 c\u00E1c con TR\u1EF0C TI\u1EBEP c\u1EE7a n\u00F3 \u2014\n t\u1EE9c gi\u1EEFa c\u00E1i `div` \u0111\u00F3 v\u00E0 anh em c\u1EE7a n\u00F3, KH\u00D4NG ph\u1EA3i gi\u1EEFa hai n\u00FAt b\u00EAn\n trong. \u0110o /modal ng\u00E0y 2026-09-24: modal \"Giao b\u00E0i t\u1EADp m\u1EDBi\" c\u00F3 hai n\u00FAt\n c\u00E1ch nhau \u0111\u00FAng **0px**, d\u00EDnh li\u1EC1n.\n\n Qu\u00E9t to\u00E0n repo l\u00FAc \u0111\u00F3: 13 ch\u1ED7 d\u00F9ng `<div footer>`, ch\u1EC9 1 ch\u1ED7 t\u1EF1 nh\u1EDB\n th\u00EAm `class=\"flex gap-2\"`; 0 ch\u1ED7 d\u00F9ng `ol-modal-footer`. B\u1EAFt n\u01A1i d\u00F9ng\n nh\u1EDB m\u1ED9t class m\u00E0 12/13 ch\u1ED7 qu\u00EAn th\u00EC \u0111\u00F3 l\u00E0 l\u1ED7i c\u1EE7a API, kh\u00F4ng ph\u1EA3i l\u1ED7i\n ng\u01B0\u1EDDi d\u00F9ng \u2014 n\u00EAn component t\u1EF1 lo.\n\n `justify-end` v\u00E0 `gap-3` gi\u1EEF nguy\u00EAn tr\u00EAn khung \u0111\u1EC3 nh\u00E1nh KH\u00D4NG b\u1ECDc\n `div` (chi\u1EBFu th\u1EB3ng nhi\u1EC1u `ol-button`) v\u1EABn \u0111\u00FAng.\n -->\n <div\n footer-frame\n class=\"p-3 flex items-center justify-end gap-3 [&>[footer]]:flex [&>[footer]]:flex-wrap [&>[footer]]:items-center [&>[footer]]:gap-3\"\n >\n <ng-content select=\"[footer]\">\n <button\n type=\"button\"\n (click)=\"hide()\"\n class=\"px-4 py-2 text-sm font-medium rounded-md text-body bg-neutral-primary border border-default hover:bg-neutral-tertiary cursor-pointer transition-colors focus:outline-hidden focus:ring-2 focus:ring-brand-subtle\"\n >\n \u0110\u00F3ng\n </button>\n </ng-content>\n </div>\n </div>\n </div>\n </div>\n}\n" }]
3228
+ }], ctorParameters: () => [], propDecorators: { panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }, { type: i0.Output, args: ["isOpenChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], showClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClose", required: false }] }], projectedHeader: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlModalHeader), { ...{ descendants: false }, isSignal: true }] }], backdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdrop", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
2900
3229
 
2901
3230
  /**
2902
3231
  * Hộp thoại bắt buộc trả lời: xoá, huỷ, rời trang khi chưa lưu.
@@ -3008,12 +3337,22 @@ class OlAlertDialog {
3008
3337
  }
3009
3338
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAlertDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
3010
3339
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAlertDialog, isStandalone: true, selector: "ol-alert-dialog", inputs: { isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, confirmText: { classPropertyName: "confirmText", publicName: "confirmText", isSignal: true, isRequired: false, transformFunction: null }, cancelText: { classPropertyName: "cancelText", publicName: "cancelText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", confirm: "confirm", cancel: "cancel", closed: "closed" }, ngImport: i0, template: `
3340
+ <!--
3341
+ showClose=false là BẮT BUỘC từ 2026-09-24: showClose đã TÁCH khỏi
3342
+ dismissible, nên chỉ tắt dismissible thì nút X VẪN hiện. Hộp thoại này
3343
+ buộc người dùng chọn một trong hai nút, không có lối thoát thứ ba.
3344
+
3345
+ Hai bẫy đã mắc khi viết chú thích này, ghi lại: chú thích đặt XEN GIỮA
3346
+ danh sách thuộc tính làm hỏng build với NG1002; và dấu nháy ngược trong
3347
+ chú thích đóng template literal sớm, lỗi hiện ở dòng chẳng liên quan.
3348
+ -->
3011
3349
  <ol-modal
3012
3350
  [isOpen]="isOpen()"
3013
3351
  (isOpenChange)="isOpen.set($event)"
3014
3352
  [role]="'alertdialog'"
3015
3353
  size="sm"
3016
3354
  [dismissible]="false"
3355
+ [showClose]="false"
3017
3356
  backdrop="static"
3018
3357
  (closed)="closed.emit()"
3019
3358
  >
@@ -3038,7 +3377,7 @@ class OlAlertDialog {
3038
3377
  </ol-button>
3039
3378
  </div>
3040
3379
  </ol-modal>
3041
- `, isInline: true, dependencies: [{ kind: "component", type: OlModal, selector: "ol-modal", inputs: ["headingLevel", "isOpen", "title", "size", "dismissible", "backdrop", "placement", "role"], outputs: ["isOpenChange", "opened", "closed"] }, { kind: "component", type: OlButton, selector: "ol-button", inputs: ["color", "size", "pill", "outline", "iconOnly", "loading", "disabled", "type", "ariaLabel", "ariaExpanded", "ariaControls", "ariaPressed"], outputs: ["btnClick"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3380
+ `, isInline: true, dependencies: [{ kind: "component", type: OlModal, selector: "ol-modal", inputs: ["headingLevel", "isOpen", "title", "icon", "description", "size", "dismissible", "showClose", "backdrop", "placement", "role"], outputs: ["isOpenChange", "opened", "closed"] }, { kind: "component", type: OlButton, selector: "ol-button", inputs: ["color", "size", "pill", "outline", "iconOnly", "loading", "disabled", "type", "ariaLabel", "ariaExpanded", "ariaControls", "ariaPressed"], outputs: ["btnClick"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3042
3381
  }
3043
3382
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAlertDialog, decorators: [{
3044
3383
  type: Component,
@@ -3047,12 +3386,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
3047
3386
  standalone: true,
3048
3387
  imports: [OlModal, OlButton, OlIconShape],
3049
3388
  template: `
3389
+ <!--
3390
+ showClose=false là BẮT BUỘC từ 2026-09-24: showClose đã TÁCH khỏi
3391
+ dismissible, nên chỉ tắt dismissible thì nút X VẪN hiện. Hộp thoại này
3392
+ buộc người dùng chọn một trong hai nút, không có lối thoát thứ ba.
3393
+
3394
+ Hai bẫy đã mắc khi viết chú thích này, ghi lại: chú thích đặt XEN GIỮA
3395
+ danh sách thuộc tính làm hỏng build với NG1002; và dấu nháy ngược trong
3396
+ chú thích đóng template literal sớm, lỗi hiện ở dòng chẳng liên quan.
3397
+ -->
3050
3398
  <ol-modal
3051
3399
  [isOpen]="isOpen()"
3052
3400
  (isOpenChange)="isOpen.set($event)"
3053
3401
  [role]="'alertdialog'"
3054
3402
  size="sm"
3055
3403
  [dismissible]="false"
3404
+ [showClose]="false"
3056
3405
  backdrop="static"
3057
3406
  (closed)="closed.emit()"
3058
3407
  >
@@ -3300,6 +3649,22 @@ class OlBadge {
3300
3649
  */
3301
3650
  color = input('gray', /* @ts-ignore */
3302
3651
  ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3652
+ /**
3653
+ * Có vẽ viền không. Tắt thì giữ nguyên nền và màu chữ của `color`.
3654
+ *
3655
+ * Viền là thuộc tính ĐỘC LẬP với màu, nên tách thành input riêng thay vì
3656
+ * thêm giá trị vào `BadgeColor`: làm thành màu thì muốn "brand không viền"
3657
+ * và "danger không viền" phải nhân đôi cả bảng 12 màu thành 24.
3658
+ *
3659
+ * Khác `color="ghost"`: ghost bỏ CẢ nền lẫn viền và ép chữ về `text-heading`
3660
+ * — nó là một màu. `[bordered]="false"` chỉ bỏ viền.
3661
+ *
3662
+ * Class `border` VẪN giữ (đổi thành `border-transparent`) chứ không bỏ hẳn:
3663
+ * badge tính chiều cao theo hộp viền, bỏ đi là badge không viền thấp hơn
3664
+ * badge thường đúng 2px khi đặt cạnh nhau. Cùng lý do với `ghost`.
3665
+ */
3666
+ bordered = input(true, /* @ts-ignore */
3667
+ ...(ngDevMode ? [{ debugName: "bordered" }] : /* istanbul ignore next */ []));
3303
3668
  /**
3304
3669
  * @deprecated Dùng `color`.
3305
3670
  *
@@ -3455,6 +3820,22 @@ class OlBadge {
3455
3820
  classes.push('bg-neutral-secondary', 'border-default-medium', 'text-heading');
3456
3821
  break;
3457
3822
  }
3823
+ /*
3824
+ * `[bordered]="false"` — bỏ viền, GIỮ nền và màu chữ.
3825
+ *
3826
+ * Phải LỌC class viền của màu ra rồi mới thêm `border-transparent`: hai
3827
+ * class viền cùng lúc thì trình duyệt chọn theo thứ tự luật trong file CSS
3828
+ * chứ không theo thứ tự đẩy vào mảng, nên kết quả đổi theo bản build.
3829
+ *
3830
+ * `border` (độ dày) vẫn giữ — xem chú thích ở input `bordered`.
3831
+ */
3832
+ if (!this.bordered()) {
3833
+ for (let i = classes.length - 1; i >= 0; i--) {
3834
+ if (classes[i].startsWith('border-'))
3835
+ classes.splice(i, 1);
3836
+ }
3837
+ classes.push('border-transparent');
3838
+ }
3458
3839
  return classes.join(' ');
3459
3840
  }, /* @ts-ignore */
3460
3841
  ...(ngDevMode ? [{ debugName: "badgeClasses" }] : /* istanbul ignore next */ []));
@@ -3488,7 +3869,7 @@ class OlBadge {
3488
3869
  this.dismiss.emit(event);
3489
3870
  }
3490
3871
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
3491
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBadge, isStandalone: true, selector: "ol-badge", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null }, leftIcon: { classPropertyName: "leftIcon", publicName: "leftIcon", isSignal: true, isRequired: false, transformFunction: null }, rightIcon: { classPropertyName: "rightIcon", publicName: "rightIcon", isSignal: true, isRequired: false, transformFunction: null }, avatar: { classPropertyName: "avatar", publicName: "avatar", isSignal: true, isRequired: false, transformFunction: null }, avatarText: { classPropertyName: "avatarText", publicName: "avatarText", isSignal: true, isRequired: false, transformFunction: null }, secondaryText: { classPropertyName: "secondaryText", publicName: "secondaryText", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, host: { classAttribute: "inline-flex align-middle max-w-full" }, ngImport: i0, template: "<!--\n `href` bi\u1EBFn nh\u00E3n th\u00E0nh li\u00EAn k\u1EBFt. Ru\u1ED9t ch\u1EC9 c\u00F3 M\u1ED8T `<ng-content />` n\u00EAn kh\u00F4ng\n \u0111\u01B0\u1EE3c nh\u00E2n \u0111\u00F4i cho hai nh\u00E1nh \u2014 Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung v\u00E0o m\u1ED9t ch\u1ED7, nh\u00E2n\n \u0111\u00F4i l\u00E0 m\u1EA5t ru\u1ED9t \u1EDF M\u1ECCI nh\u00E1nh m\u00E0 build v\u1EABn xanh.\n-->\n@if (href()) {\n <a [href]=\"href()\" [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <span [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n}\n\n<ng-template #ruot>\n <!-- 1. Dot indicator -->\n @if (dot() || type() === 'dot') {\n <span class=\"w-1.5 h-1.5 rounded-full shrink-0\" [class]=\"dotClasses()\"></span>\n }\n\n <!-- 2. Avatar -->\n @if (avatar() || avatarText() || type() === 'avatar') {\n <ol-avatar\n [src]=\"avatar() || ''\"\n [initials]=\"avatarText() || ''\"\n [size]=\"size() === 'lg' ? 'xs' : '2xs'\"\n class=\"-ms-1 rounded-full shrink-0 flex items-center justify-center overflow-hidden\"\n />\n }\n\n <!-- 3. Loading Spinner -->\n @if (loading() || type() === 'loader') {\n <svg\n class=\"animate-spin w-3 h-3 shrink-0 text-current\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <!-- 4. Left icon \u2014 nh\u01B0\u1EDDng ch\u1ED7 cho spinner khi \u0111ang loading -->\n @if (leftIcon() && !loading() && type() !== 'loader') {\n <ol-icon [name]=\"leftIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 5. Main content -->\n <span class=\"truncate min-w-0 max-w-full block\"><ng-content /></span>\n\n <!-- 6. Secondary text separator & content -->\n @if (secondaryText() || type() === 'secondary') {\n <span class=\"w-px h-3 bg-current opacity-30 shrink-0 mx-0.5\"></span>\n <span class=\"opacity-80 font-normal truncate max-w-full\">{{ secondaryText() }}</span>\n }\n\n <!-- 7. Right icon -->\n @if (rightIcon()) {\n <ol-icon [name]=\"rightIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 8. Dismiss / Remove button (Figma Close X) -->\n @if (removable()) {\n <button\n type=\"button\"\n (click)=\"onDismiss($event)\"\n class=\"inline-flex items-center justify-center -me-0.5 p-0.5 rounded-xs hover:bg-black/10 dark:hover:bg-white/10 cursor-pointer transition-colors\"\n aria-label=\"G\u1EE1 nh\u00E3n\"\n >\n <svg\n class=\"w-3 h-3 text-current\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3872
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBadge, isStandalone: true, selector: "ol-badge", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null }, leftIcon: { classPropertyName: "leftIcon", publicName: "leftIcon", isSignal: true, isRequired: false, transformFunction: null }, rightIcon: { classPropertyName: "rightIcon", publicName: "rightIcon", isSignal: true, isRequired: false, transformFunction: null }, avatar: { classPropertyName: "avatar", publicName: "avatar", isSignal: true, isRequired: false, transformFunction: null }, avatarText: { classPropertyName: "avatarText", publicName: "avatarText", isSignal: true, isRequired: false, transformFunction: null }, secondaryText: { classPropertyName: "secondaryText", publicName: "secondaryText", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, host: { classAttribute: "inline-flex align-middle max-w-full" }, ngImport: i0, template: "<!--\n `href` bi\u1EBFn nh\u00E3n th\u00E0nh li\u00EAn k\u1EBFt. Ru\u1ED9t ch\u1EC9 c\u00F3 M\u1ED8T `<ng-content />` n\u00EAn kh\u00F4ng\n \u0111\u01B0\u1EE3c nh\u00E2n \u0111\u00F4i cho hai nh\u00E1nh \u2014 Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung v\u00E0o m\u1ED9t ch\u1ED7, nh\u00E2n\n \u0111\u00F4i l\u00E0 m\u1EA5t ru\u1ED9t \u1EDF M\u1ECCI nh\u00E1nh m\u00E0 build v\u1EABn xanh.\n-->\n@if (href()) {\n <a [href]=\"href()\" [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <span [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n}\n\n<ng-template #ruot>\n <!-- 1. Dot indicator -->\n @if (dot() || type() === 'dot') {\n <span class=\"w-1.5 h-1.5 rounded-full shrink-0\" [class]=\"dotClasses()\"></span>\n }\n\n <!-- 2. Avatar -->\n @if (avatar() || avatarText() || type() === 'avatar') {\n <ol-avatar\n [src]=\"avatar() || ''\"\n [initials]=\"avatarText() || ''\"\n [size]=\"size() === 'lg' ? 'xs' : '2xs'\"\n class=\"-ms-1 rounded-full shrink-0 flex items-center justify-center overflow-hidden\"\n />\n }\n\n <!-- 3. Loading Spinner -->\n @if (loading() || type() === 'loader') {\n <svg\n class=\"animate-spin w-3 h-3 shrink-0 text-current\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <!-- 4. Left icon \u2014 nh\u01B0\u1EDDng ch\u1ED7 cho spinner khi \u0111ang loading -->\n @if (leftIcon() && !loading() && type() !== 'loader') {\n <ol-icon [name]=\"leftIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 5. Main content -->\n <span class=\"truncate min-w-0 max-w-full block\"><ng-content /></span>\n\n <!-- 6. Secondary text separator & content -->\n @if (secondaryText() || type() === 'secondary') {\n <span class=\"w-px h-3 bg-current opacity-30 shrink-0 mx-0.5\"></span>\n <span class=\"opacity-80 font-normal truncate max-w-full\">{{ secondaryText() }}</span>\n }\n\n <!-- 7. Right icon -->\n @if (rightIcon()) {\n <ol-icon [name]=\"rightIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 8. Dismiss / Remove button (Figma Close X) -->\n @if (removable()) {\n <button\n type=\"button\"\n (click)=\"onDismiss($event)\"\n class=\"inline-flex items-center justify-center -me-0.5 p-0.5 rounded-xs hover:bg-black/10 dark:hover:bg-white/10 cursor-pointer transition-colors\"\n aria-label=\"G\u1EE1 nh\u00E3n\"\n >\n <svg\n class=\"w-3 h-3 text-current\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3492
3873
  }
3493
3874
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBadge, decorators: [{
3494
3875
  type: Component,
@@ -3503,7 +3884,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
3503
3884
  // hay không có `min-w-0` badge đều dừng đúng 60px.
3504
3885
  class: 'inline-flex align-middle max-w-full',
3505
3886
  }, template: "<!--\n `href` bi\u1EBFn nh\u00E3n th\u00E0nh li\u00EAn k\u1EBFt. Ru\u1ED9t ch\u1EC9 c\u00F3 M\u1ED8T `<ng-content />` n\u00EAn kh\u00F4ng\n \u0111\u01B0\u1EE3c nh\u00E2n \u0111\u00F4i cho hai nh\u00E1nh \u2014 Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung v\u00E0o m\u1ED9t ch\u1ED7, nh\u00E2n\n \u0111\u00F4i l\u00E0 m\u1EA5t ru\u1ED9t \u1EDF M\u1ECCI nh\u00E1nh m\u00E0 build v\u1EABn xanh.\n-->\n@if (href()) {\n <a [href]=\"href()\" [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <span [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n}\n\n<ng-template #ruot>\n <!-- 1. Dot indicator -->\n @if (dot() || type() === 'dot') {\n <span class=\"w-1.5 h-1.5 rounded-full shrink-0\" [class]=\"dotClasses()\"></span>\n }\n\n <!-- 2. Avatar -->\n @if (avatar() || avatarText() || type() === 'avatar') {\n <ol-avatar\n [src]=\"avatar() || ''\"\n [initials]=\"avatarText() || ''\"\n [size]=\"size() === 'lg' ? 'xs' : '2xs'\"\n class=\"-ms-1 rounded-full shrink-0 flex items-center justify-center overflow-hidden\"\n />\n }\n\n <!-- 3. Loading Spinner -->\n @if (loading() || type() === 'loader') {\n <svg\n class=\"animate-spin w-3 h-3 shrink-0 text-current\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <!-- 4. Left icon \u2014 nh\u01B0\u1EDDng ch\u1ED7 cho spinner khi \u0111ang loading -->\n @if (leftIcon() && !loading() && type() !== 'loader') {\n <ol-icon [name]=\"leftIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 5. Main content -->\n <span class=\"truncate min-w-0 max-w-full block\"><ng-content /></span>\n\n <!-- 6. Secondary text separator & content -->\n @if (secondaryText() || type() === 'secondary') {\n <span class=\"w-px h-3 bg-current opacity-30 shrink-0 mx-0.5\"></span>\n <span class=\"opacity-80 font-normal truncate max-w-full\">{{ secondaryText() }}</span>\n }\n\n <!-- 7. Right icon -->\n @if (rightIcon()) {\n <ol-icon [name]=\"rightIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 8. Dismiss / Remove button (Figma Close X) -->\n @if (removable()) {\n <button\n type=\"button\"\n (click)=\"onDismiss($event)\"\n class=\"inline-flex items-center justify-center -me-0.5 p-0.5 rounded-xs hover:bg-black/10 dark:hover:bg-white/10 cursor-pointer transition-colors\"\n aria-label=\"G\u1EE1 nh\u00E3n\"\n >\n <svg\n class=\"w-3 h-3 text-current\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n</ng-template>\n" }]
3506
- }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], rightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightIcon", required: false }] }], avatar: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatar", required: false }] }], avatarText: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarText", required: false }] }], secondaryText: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondaryText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
3887
+ }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], rightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightIcon", required: false }] }], avatar: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatar", required: false }] }], avatarText: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarText", required: false }] }], secondaryText: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondaryText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
3507
3888
 
3508
3889
  /**
3509
3890
  * Figma `ol-banner` (#1146:21254) KHÔNG có trục màu — Type chỉ là bố cục
@@ -3734,6 +4115,31 @@ class OlBreadcrumbItem {
3734
4115
  */
3735
4116
  disabled = input(false, /* @ts-ignore */
3736
4117
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4118
+ /**
4119
+ * Cho phép bấm để chuyển trang hay không. Mặc định **bật** — giữ hành vi cũ.
4120
+ *
4121
+ * Tắt khi muốn chặng VẪN hiện như liên kết (chữ xanh khi rê chuột, Tab tới
4122
+ * được, trình đọc màn hình vẫn đọc là liên kết) nhưng KHÔNG tự điều hướng —
4123
+ * ví dụ cần hỏi xác nhận trước khi rời trang đang sửa dở, hoặc nơi dùng tự
4124
+ * xử lý điều hướng qua `(itemClick)`.
4125
+ *
4126
+ * Khác `disabled`: `disabled` bỏ HẲN thẻ `<a>`, chặng thành chữ xám không
4127
+ * bấm được và không Tab tới được. `[clickable]="false"` giữ nguyên vẻ ngoài
4128
+ * và trợ năng của liên kết, chỉ chặn hành vi điều hướng.
4129
+ *
4130
+ * Chặn bằng `preventDefault()` trong `(click)` chứ không bỏ `[routerLink]`:
4131
+ * bỏ binding thì `<a>` mất `href`, trình đọc màn hình không còn coi nó là
4132
+ * liên kết, và người dùng mất luôn khả năng xem đích đến ở thanh trạng thái.
4133
+ */
4134
+ clickable = input(true, /* @ts-ignore */
4135
+ ...(ngDevMode ? [{ debugName: "clickable" }] : /* istanbul ignore next */ []));
4136
+ /**
4137
+ * Phát khi người dùng bấm vào chặng — kể cả khi `[clickable]="false"`.
4138
+ *
4139
+ * Nhờ vậy nơi dùng chặn điều hướng mặc định rồi tự quyết: hỏi xác nhận, lưu
4140
+ * nháp, hay điều hướng bằng đường khác.
4141
+ */
4142
+ itemClick = output();
3737
4143
  parentBreadcrumb = inject(forwardRef(() => OlBreadcrumb), {
3738
4144
  optional: true,
3739
4145
  });
@@ -3750,13 +4156,56 @@ class OlBreadcrumbItem {
3750
4156
  */
3751
4157
  isLink = computed(() => !this.isCurrent() && !this.disabled() && (this.routerLink() !== null || this.href() !== null), /* @ts-ignore */
3752
4158
  ...(ngDevMode ? [{ debugName: "isLink" }] : /* istanbul ignore next */ []));
4159
+ /**
4160
+ * `href` tĩnh dựng từ `routerLink()`, dùng cho nhánh `clickable=false`.
4161
+ *
4162
+ * Giữ được thuộc tính `href` thật trên thẻ `<a>` nên trợ năng không đổi;
4163
+ * `preventDefault()` trong `onClick` lo phần chặn đi.
4164
+ */
4165
+ staticHref = computed(() => {
4166
+ const link = this.routerLink();
4167
+ if (link === null)
4168
+ return this.href() ?? '';
4169
+ return Array.isArray(link) ? link.join('/').replace(/\/{2,}/g, '/') : String(link);
4170
+ }, /* @ts-ignore */
4171
+ ...(ngDevMode ? [{ debugName: "staticHref" }] : /* istanbul ignore next */ []));
4172
+ /**
4173
+ * Chặn điều hướng khi `[clickable]="false"`, nhưng VẪN phát `itemClick` để
4174
+ * nơi dùng tự xử lý.
4175
+ *
4176
+ * `preventDefault()` chặn cả `routerLink` lẫn `href`: RouterLink bỏ qua sự
4177
+ * kiện đã bị chặn, còn trình duyệt không đi theo `href` nữa.
4178
+ */
4179
+ onClick(event) {
4180
+ if (!this.clickable())
4181
+ event.preventDefault();
4182
+ this.itemClick.emit(event);
4183
+ }
3753
4184
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBreadcrumbItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
3754
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBreadcrumbItem, isStandalone: true, selector: "ol-breadcrumb-item", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, showSeparator: { classPropertyName: "showSeparator", publicName: "showSeparator", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-template #contentTpl>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span>{{ label() }}</span>\n } @else {\n <span><ng-content /></span>\n }\n</ng-template>\n\n<li class=\"inline-flex items-center gap-1.5\">\n @if (isLink()) {\n @if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else {\n <a\n [href]=\"href()\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n }\n } @else {\n <!--\n Th\u1EE9 t\u1EF1 ba nh\u00E1nh l\u00E0 C\u1ED0 \u00DD: `disabled` \u0111\u1EE9ng TR\u01AF\u1EDAC `isCurrent`. M\u1EE5c cu\u1ED1i m\u00E0\n c\u0169ng b\u1ECB kho\u00E1 th\u00EC ph\u1EA3i hi\u1EC7n l\u00E0 kho\u00E1 \u2014 ng\u01B0\u1EE3c l\u1EA1i ng\u01B0\u1EDDi d\u00F9ng th\u1EA5y n\u00F3 y h\u1EC7t\n trang hi\u1EC7n t\u1EA1i v\u00E0 kh\u00F4ng hi\u1EC3u v\u00EC sao b\u1EA5m kh\u00F4ng \u0111\u01B0\u1EE3c.\n\n M\u00E0u theo chu\u1EA9n disabled c\u1EE7a th\u01B0 vi\u1EC7n (`bg-disabled` + `text-fg-disabled`,\n \u0111\u00FAng Figma State=Disabled, xem button.component.ts). KH\u00D4NG d\u00F9ng\n `opacity-50`: m\u1EABu \u0111\u00F3 ch\u1EC9 c\u00F2n 2 component d\u00F9ng, v\u00E0 th\u01B0 vi\u1EC7n \u0111ang c\u00F3 b\u1ED1n\n b\u1EADc opacity song song ch\u01B0a h\u1ED9i t\u1EE5.\n\n `dark:text-heading` cho m\u1EE5c cu\u1ED1i l\u00E0 NGO\u1EA0I L\u1EC6 C\u00D3 \u0110O, \u0111\u1EEBng g\u1EE1 v\u00EC t\u01B0\u1EDFng th\u1EEBa\n (token th\u01B0\u1EDDng t\u1EF1 l\u1EADt theo `.dark`). \u1EDE nh\u00E1nh t\u1ED1i `--text-body-subtle` b\u1ECB\n \u0111\u1EB7t B\u1EB0NG H\u1EC6T `--text-body` \u2014 c\u1EA3 hai \u0111\u1EC1u gray-300 \u2014 n\u00EAn m\u1EE5c cu\u1ED1i v\u00E0 m\u1EE5c\n th\u01B0\u1EDDng ra c\u00F9ng m\u1ED9t m\u00E0u, hai tr\u1EA1ng th\u00E1i kh\u00F4ng ph\u00E2n bi\u1EC7t \u0111\u01B0\u1EE3c. \u0110o\n /breadcrumb 2026-09-23: s\u00E1ng 107,114,128 vs 74,85,101 (kh\u00E1c); t\u1ED1i\n 209,213,219 vs 209,213,219 (TR\u00D9NG).\n\n Kh\u00F4ng s\u1EEDa token chung v\u00EC kh\u00F4ng b\u1EADc n\u00E0o tho\u1EA3: qu\u00E9t c\u1EA3 thang \u1EDF nh\u00E1nh t\u1ED1i,\n gray-400 ch\u1EC9 \u0111\u1EA1t 3,96 tr\u00EAn n\u1EC1n quaternary v\u00E0 gray-500 \u0111\u1EA1t 2,13 \u2014 \u0111\u1EC1u d\u01B0\u1EDBi\n AA 4,5; ch\u1EC9 gray-200 \u0111\u1EA1t (8,32) nh\u01B0ng n\u00F3 S\u00C1NG h\u01A1n body, t\u1EE9c \"subtle\" n\u1ED5i\n h\u01A1n \"body\", ng\u01B0\u1EE3c ng\u1EEF ngh\u0129a. M\u00E0 50 file \u0111ang d\u00F9ng token \u0111\u00F3.\n\n `text-heading` \u1EDF nh\u00E1nh t\u1ED1i l\u00E0 tr\u1EAFng: t\u00E1ch r\u00F5 kh\u1ECFi gray-300, v\u00E0 trang \u0111ang\n \u0111\u1EE9ng \u0111\u00E1ng n\u1ED5i h\u01A1n c\u00E1c c\u1EA5p cha. Ch\u1EBF \u0111\u1ED9 s\u00E1ng gi\u1EEF nguy\u00EAn #6B7280 \u0111\u00FAng Figma.\n -->\n <span\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\n [class]=\"\n disabled()\n ? 'font-medium text-fg-disabled cursor-not-allowed'\n : isCurrent()\n ? 'font-medium text-body-subtle dark:text-heading'\n : status() === 'hover'\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\n : 'font-medium text-body'\n \"\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </span>\n }\n\n @if (!isCurrent() && showSeparator()) {\n <!--\n D\u1EA5u ng\u0103n d\u00F9ng `text-body-subtle`, KH\u00D4NG `text-fg-disabled`.\n\n `text-fg-disabled` (#99A1AF) tr\u00EAn n\u1EC1n tr\u1EAFng ch\u1EC9 \u0111\u1EA1t **2,60** \u2014 c\u00F3\n `aria-hidden` n\u00EAn kh\u00F4ng vi ph\u1EA1m WCAG, nh\u01B0ng ng\u01B0\u1EDDi nh\u00ECn k\u00E9m kh\u00F3 th\u1EA5y ranh\n gi\u1EDBi gi\u1EEFa c\u00E1c c\u1EA5p. `text-body-subtle` (#6B7280) \u0111\u1EA1t 4,83 m\u00E0 v\u1EABn nh\u1EA1t h\u01A1n\n ch\u1EEF c\u1EE7a ch\u1EB7ng, n\u00EAn v\u1EABn ra \"d\u1EA5u ng\u0103n\" ch\u1EE9 kh\u00F4ng c\u1EA1nh tranh v\u1EDBi n\u1ED9i dung.\n\n KH\u00C1C d\u00F2ng tr\u00EAn: ch\u1EB7ng `disabled` gi\u1EEF `text-fg-disabled` \u2014 \u0111\u00F3 l\u00E0 chu\u1EA9n\n tr\u1EA1ng th\u00E1i kho\u00E1 c\u1EE7a th\u01B0 vi\u1EC7n, v\u00E0 WCAG mi\u1EC5n tr\u1EEB ph\u1EA7n t\u1EED kh\u00F4ng b\u1EA5m \u0111\u01B0\u1EE3c.\n -->\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\n @switch (effectiveSeparator()) {\n @case ('slash') {\n <span class=\"text-xs font-normal\">/</span>\n }\n @case ('arrow') {\n <span class=\"text-xs\">\u2192</span>\n }\n @case ('dot') {\n <span class=\"text-xs\">\u2022</span>\n }\n @case ('chevron') {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n @default {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n }\n </span>\n }\n</li>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4185
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBreadcrumbItem, isStandalone: true, selector: "ol-breadcrumb-item", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, showSeparator: { classPropertyName: "showSeparator", publicName: "showSeparator", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, host: { classAttribute: "inline-flex" }, ngImport: i0, template: "<ng-template #contentTpl>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span>{{ label() }}</span>\n } @else {\n <span><ng-content /></span>\n }\n</ng-template>\n\n<li class=\"inline-flex items-center gap-1.5\">\n @if (isLink()) {\n <!--\n `[clickable]=\"false\"` ph\u1EA3i B\u1ECE H\u1EB2N `[routerLink]`, kh\u00F4ng ch\u1EC9\n `preventDefault()` trong `(click)`.\n\n \u0110\u00E3 th\u1EED c\u00E1ch kia tr\u01B0\u1EDBc: RouterLink t\u1EF1 \u0111\u0103ng k\u00FD handler click c\u1EE7a n\u00F3 v\u00E0\n KH\u00D4NG ki\u1EC3m `event.defaultPrevented`, n\u00EAn n\u00F3 \u0111i\u1EC1u h\u01B0\u1EDBng b\u1EA5t k\u1EC3 m\u00ECnh \u0111\u00E3\n ch\u1EB7n hay ch\u01B0a \u2014 spec `breadcrumb-item-clickable.spec.ts` b\u1EAFt \u0111\u00FAng ch\u1ED7 n\u00E0y.\n\n Thay b\u1EB1ng `href` t\u0129nh d\u1EF1ng t\u1EEB ch\u00EDnh `routerLink()`: th\u1EBB v\u1EABn l\u00E0 `<a>` c\u00F3\n `href` th\u1EADt n\u00EAn tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc l\u00E0 li\u00EAn k\u1EBFt, v\u1EABn Tab t\u1EDBi \u0111\u01B0\u1EE3c,\n v\u1EABn xem \u0111\u01B0\u1EE3c \u0111\u00EDch \u0111\u1EBFn \u1EDF thanh tr\u1EA1ng th\u00E1i \u2014 ch\u1EC9 `preventDefault()` ch\u1EB7n\n tr\u00ECnh duy\u1EC7t \u0111i theo. \u0110\u00F3 l\u00E0 kh\u00E1c bi\u1EC7t v\u1EDBi `disabled`, v\u1ED1n b\u1ECF lu\u00F4n th\u1EBB `<a>`.\n -->\n @if (routerLink() && clickable()) {\n <a\n [routerLink]=\"routerLink()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else if (routerLink()) {\n <a\n [href]=\"staticHref()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else {\n <a\n [href]=\"href()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n }\n } @else {\n <!--\n Th\u1EE9 t\u1EF1 ba nh\u00E1nh l\u00E0 C\u1ED0 \u00DD: `disabled` \u0111\u1EE9ng TR\u01AF\u1EDAC `isCurrent`. M\u1EE5c cu\u1ED1i m\u00E0\n c\u0169ng b\u1ECB kho\u00E1 th\u00EC ph\u1EA3i hi\u1EC7n l\u00E0 kho\u00E1 \u2014 ng\u01B0\u1EE3c l\u1EA1i ng\u01B0\u1EDDi d\u00F9ng th\u1EA5y n\u00F3 y h\u1EC7t\n trang hi\u1EC7n t\u1EA1i v\u00E0 kh\u00F4ng hi\u1EC3u v\u00EC sao b\u1EA5m kh\u00F4ng \u0111\u01B0\u1EE3c.\n\n M\u00E0u theo chu\u1EA9n disabled c\u1EE7a th\u01B0 vi\u1EC7n (`bg-disabled` + `text-fg-disabled`,\n \u0111\u00FAng Figma State=Disabled, xem button.component.ts). KH\u00D4NG d\u00F9ng\n `opacity-50`: m\u1EABu \u0111\u00F3 ch\u1EC9 c\u00F2n 2 component d\u00F9ng, v\u00E0 th\u01B0 vi\u1EC7n \u0111ang c\u00F3 b\u1ED1n\n b\u1EADc opacity song song ch\u01B0a h\u1ED9i t\u1EE5.\n\n M\u1EE5c \u0110ANG \u0110\u1EE8NG d\u00F9ng `text-heading` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 L\u1EC6CH FIGMA C\u00D3 CH\u1EE6 \u00DD\n (2026-09-24, theo y\u00EAu c\u1EA7u ng\u01B0\u1EDDi d\u00F9ng \"cho m\u00E0u \u0111\u1EADm h\u01A1n c\u00E1c m\u00E0u kh\u00E1c\").\n\n Figma `ol-breadcrumb-item` #1148:31348 `Status=Active` bind\n `colors/text/text-body-subtle` = #6B7280. \u0110o /breadcrumb \u1EDF ch\u1EBF \u0111\u1ED9 S\u00C1NG\n th\u00EC n\u00F3 cho t\u01B0\u01A1ng ph\u1EA3n **4,83** trong khi c\u00E1c c\u1EA5p cha (`text-body`\n #4A5565) \u0111\u1EA1t **7,56** \u2014 t\u1EE9c trang \u0111ang \u0111\u1EE9ng NH\u1EA0T H\u01A0N c\u1EA5p cha n\u00F3, ng\u01B0\u1EE3c\n ng\u1EEF ngh\u0129a. Nay `text-heading`: s\u00E1ng #101828 (17,75) \u00B7 t\u1ED1i #FFFFFF\n (20,13), \u0111\u1EADm h\u01A1n h\u1EB3n v\u00E0 t\u1EF1 l\u1EADt theo `.dark`.\n\n B\u1EA3n tr\u01B0\u1EDBc ph\u1EA3i vi\u1EBFt `text-body-subtle dark:text-heading` v\u00EC \u1EDF nh\u00E1nh t\u1ED1i\n `--text-body-subtle` b\u1ECB \u0111\u1EB7t B\u1EB0NG H\u1EC6T `--text-body` (c\u00F9ng gray-300) n\u00EAn\n m\u1EE5c cu\u1ED1i v\u00E0 m\u1EE5c th\u01B0\u1EDDng tr\u00F9ng m\u00E0u. D\u00F9ng `text-heading` cho c\u1EA3 hai ch\u1EBF \u0111\u1ED9\n th\u00EC h\u1EBFt c\u1EA7n ngo\u1EA1i l\u1EC7 \u0111\u00F3.\n\n N\u1EE3: c\u1EADp nh\u1EADt Figma `Status=Active` sang `colors/text/text-heading`.\n -->\n <span\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\n [class]=\"\n disabled()\n ? 'font-medium text-fg-disabled cursor-not-allowed'\n : isCurrent()\n ? 'font-medium text-heading'\n : status() === 'hover'\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\n : 'font-medium text-body'\n \"\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </span>\n }\n\n @if (!isCurrent() && showSeparator()) {\n <!--\n D\u1EA5u ng\u0103n d\u00F9ng `text-body-subtle`, KH\u00D4NG `text-fg-disabled`.\n\n `text-fg-disabled` (#99A1AF) tr\u00EAn n\u1EC1n tr\u1EAFng ch\u1EC9 \u0111\u1EA1t **2,60** \u2014 c\u00F3\n `aria-hidden` n\u00EAn kh\u00F4ng vi ph\u1EA1m WCAG, nh\u01B0ng ng\u01B0\u1EDDi nh\u00ECn k\u00E9m kh\u00F3 th\u1EA5y ranh\n gi\u1EDBi gi\u1EEFa c\u00E1c c\u1EA5p. `text-body-subtle` (#6B7280) \u0111\u1EA1t 4,83 m\u00E0 v\u1EABn nh\u1EA1t h\u01A1n\n ch\u1EEF c\u1EE7a ch\u1EB7ng, n\u00EAn v\u1EABn ra \"d\u1EA5u ng\u0103n\" ch\u1EE9 kh\u00F4ng c\u1EA1nh tranh v\u1EDBi n\u1ED9i dung.\n\n KH\u00C1C d\u00F2ng tr\u00EAn: ch\u1EB7ng `disabled` gi\u1EEF `text-fg-disabled` \u2014 \u0111\u00F3 l\u00E0 chu\u1EA9n\n tr\u1EA1ng th\u00E1i kho\u00E1 c\u1EE7a th\u01B0 vi\u1EC7n, v\u00E0 WCAG mi\u1EC5n tr\u1EEB ph\u1EA7n t\u1EED kh\u00F4ng b\u1EA5m \u0111\u01B0\u1EE3c.\n -->\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\n @switch (effectiveSeparator()) {\n @case ('slash') {\n <span class=\"text-xs font-normal\">/</span>\n }\n @case ('arrow') {\n <span class=\"text-xs\">\u2192</span>\n }\n @case ('dot') {\n <span class=\"text-xs\">\u2022</span>\n }\n @case ('chevron') {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n @default {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n }\n </span>\n }\n</li>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3755
4186
  }
3756
4187
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBreadcrumbItem, decorators: [{
3757
4188
  type: Component,
3758
- args: [{ selector: 'ol-breadcrumb-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-template #contentTpl>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span>{{ label() }}</span>\n } @else {\n <span><ng-content /></span>\n }\n</ng-template>\n\n<li class=\"inline-flex items-center gap-1.5\">\n @if (isLink()) {\n @if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else {\n <a\n [href]=\"href()\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n }\n } @else {\n <!--\n Th\u1EE9 t\u1EF1 ba nh\u00E1nh l\u00E0 C\u1ED0 \u00DD: `disabled` \u0111\u1EE9ng TR\u01AF\u1EDAC `isCurrent`. M\u1EE5c cu\u1ED1i m\u00E0\n c\u0169ng b\u1ECB kho\u00E1 th\u00EC ph\u1EA3i hi\u1EC7n l\u00E0 kho\u00E1 \u2014 ng\u01B0\u1EE3c l\u1EA1i ng\u01B0\u1EDDi d\u00F9ng th\u1EA5y n\u00F3 y h\u1EC7t\n trang hi\u1EC7n t\u1EA1i v\u00E0 kh\u00F4ng hi\u1EC3u v\u00EC sao b\u1EA5m kh\u00F4ng \u0111\u01B0\u1EE3c.\n\n M\u00E0u theo chu\u1EA9n disabled c\u1EE7a th\u01B0 vi\u1EC7n (`bg-disabled` + `text-fg-disabled`,\n \u0111\u00FAng Figma State=Disabled, xem button.component.ts). KH\u00D4NG d\u00F9ng\n `opacity-50`: m\u1EABu \u0111\u00F3 ch\u1EC9 c\u00F2n 2 component d\u00F9ng, v\u00E0 th\u01B0 vi\u1EC7n \u0111ang c\u00F3 b\u1ED1n\n b\u1EADc opacity song song ch\u01B0a h\u1ED9i t\u1EE5.\n\n `dark:text-heading` cho m\u1EE5c cu\u1ED1i l\u00E0 NGO\u1EA0I L\u1EC6 C\u00D3 \u0110O, \u0111\u1EEBng g\u1EE1 v\u00EC t\u01B0\u1EDFng th\u1EEBa\n (token th\u01B0\u1EDDng t\u1EF1 l\u1EADt theo `.dark`). \u1EDE nh\u00E1nh t\u1ED1i `--text-body-subtle` b\u1ECB\n \u0111\u1EB7t B\u1EB0NG H\u1EC6T `--text-body` \u2014 c\u1EA3 hai \u0111\u1EC1u gray-300 \u2014 n\u00EAn m\u1EE5c cu\u1ED1i v\u00E0 m\u1EE5c\n th\u01B0\u1EDDng ra c\u00F9ng m\u1ED9t m\u00E0u, hai tr\u1EA1ng th\u00E1i kh\u00F4ng ph\u00E2n bi\u1EC7t \u0111\u01B0\u1EE3c. \u0110o\n /breadcrumb 2026-09-23: s\u00E1ng 107,114,128 vs 74,85,101 (kh\u00E1c); t\u1ED1i\n 209,213,219 vs 209,213,219 (TR\u00D9NG).\n\n Kh\u00F4ng s\u1EEDa token chung v\u00EC kh\u00F4ng b\u1EADc n\u00E0o tho\u1EA3: qu\u00E9t c\u1EA3 thang \u1EDF nh\u00E1nh t\u1ED1i,\n gray-400 ch\u1EC9 \u0111\u1EA1t 3,96 tr\u00EAn n\u1EC1n quaternary v\u00E0 gray-500 \u0111\u1EA1t 2,13 \u2014 \u0111\u1EC1u d\u01B0\u1EDBi\n AA 4,5; ch\u1EC9 gray-200 \u0111\u1EA1t (8,32) nh\u01B0ng n\u00F3 S\u00C1NG h\u01A1n body, t\u1EE9c \"subtle\" n\u1ED5i\n h\u01A1n \"body\", ng\u01B0\u1EE3c ng\u1EEF ngh\u0129a. M\u00E0 50 file \u0111ang d\u00F9ng token \u0111\u00F3.\n\n `text-heading` \u1EDF nh\u00E1nh t\u1ED1i l\u00E0 tr\u1EAFng: t\u00E1ch r\u00F5 kh\u1ECFi gray-300, v\u00E0 trang \u0111ang\n \u0111\u1EE9ng \u0111\u00E1ng n\u1ED5i h\u01A1n c\u00E1c c\u1EA5p cha. Ch\u1EBF \u0111\u1ED9 s\u00E1ng gi\u1EEF nguy\u00EAn #6B7280 \u0111\u00FAng Figma.\n -->\n <span\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\n [class]=\"\n disabled()\n ? 'font-medium text-fg-disabled cursor-not-allowed'\n : isCurrent()\n ? 'font-medium text-body-subtle dark:text-heading'\n : status() === 'hover'\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\n : 'font-medium text-body'\n \"\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </span>\n }\n\n @if (!isCurrent() && showSeparator()) {\n <!--\n D\u1EA5u ng\u0103n d\u00F9ng `text-body-subtle`, KH\u00D4NG `text-fg-disabled`.\n\n `text-fg-disabled` (#99A1AF) tr\u00EAn n\u1EC1n tr\u1EAFng ch\u1EC9 \u0111\u1EA1t **2,60** \u2014 c\u00F3\n `aria-hidden` n\u00EAn kh\u00F4ng vi ph\u1EA1m WCAG, nh\u01B0ng ng\u01B0\u1EDDi nh\u00ECn k\u00E9m kh\u00F3 th\u1EA5y ranh\n gi\u1EDBi gi\u1EEFa c\u00E1c c\u1EA5p. `text-body-subtle` (#6B7280) \u0111\u1EA1t 4,83 m\u00E0 v\u1EABn nh\u1EA1t h\u01A1n\n ch\u1EEF c\u1EE7a ch\u1EB7ng, n\u00EAn v\u1EABn ra \"d\u1EA5u ng\u0103n\" ch\u1EE9 kh\u00F4ng c\u1EA1nh tranh v\u1EDBi n\u1ED9i dung.\n\n KH\u00C1C d\u00F2ng tr\u00EAn: ch\u1EB7ng `disabled` gi\u1EEF `text-fg-disabled` \u2014 \u0111\u00F3 l\u00E0 chu\u1EA9n\n tr\u1EA1ng th\u00E1i kho\u00E1 c\u1EE7a th\u01B0 vi\u1EC7n, v\u00E0 WCAG mi\u1EC5n tr\u1EEB ph\u1EA7n t\u1EED kh\u00F4ng b\u1EA5m \u0111\u01B0\u1EE3c.\n -->\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\n @switch (effectiveSeparator()) {\n @case ('slash') {\n <span class=\"text-xs font-normal\">/</span>\n }\n @case ('arrow') {\n <span class=\"text-xs\">\u2192</span>\n }\n @case ('dot') {\n <span class=\"text-xs\">\u2022</span>\n }\n @case ('chevron') {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n @default {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n }\n </span>\n }\n</li>\n" }]
3759
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], current: [{ type: i0.Input, args: [{ isSignal: true, alias: "current", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], showSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeparator", required: false }] }], separator: [{ type: i0.Input, args: [{ isSignal: true, alias: "separator", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
4189
+ args: [{ selector: 'ol-breadcrumb-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], host: {
4190
+ /*
4191
+ * Thẻ tuỳ biến KHÔNG khai `display` thì mặc định là `inline`, và bản cũ
4192
+ * không khai `host` nào cả. Hậu quả: host chứa `<li>` inline-flex nên nó
4193
+ * tính chiều cao theo dòng chữ — cộng thêm khoảng trống dưới baseline cho
4194
+ * phần đuôi chữ. Mục CÓ ICON có `<svg>` 16px đẩy dòng cao hơn, nên host
4195
+ * của nó cao 23px trong khi mục không icon chỉ 20px.
4196
+ *
4197
+ * `<ol>` bên ngoài dùng `items-center`, mà căn giữa ba hộp CAO KHÁC NHAU
4198
+ * thì chữ không thể thẳng hàng: đo /breadcrumb 2026-09-24, chữ của mục có
4199
+ * icon nằm cao hơn hai mục sau đúng **1,5px** — thấy rõ bằng mắt trên
4200
+ * thanh "Trang chủ › Bài tập › Học sinh".
4201
+ *
4202
+ * `inline-flex` làm host thành hộp flex, chiều cao bám đúng nội dung
4203
+ * (20px) và không còn khoảng baseline. Ba host bằng nhau thì `items-center`
4204
+ * căn ra cùng một đường.
4205
+ */
4206
+ class: 'inline-flex',
4207
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-template #contentTpl>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span>{{ label() }}</span>\n } @else {\n <span><ng-content /></span>\n }\n</ng-template>\n\n<li class=\"inline-flex items-center gap-1.5\">\n @if (isLink()) {\n <!--\n `[clickable]=\"false\"` ph\u1EA3i B\u1ECE H\u1EB2N `[routerLink]`, kh\u00F4ng ch\u1EC9\n `preventDefault()` trong `(click)`.\n\n \u0110\u00E3 th\u1EED c\u00E1ch kia tr\u01B0\u1EDBc: RouterLink t\u1EF1 \u0111\u0103ng k\u00FD handler click c\u1EE7a n\u00F3 v\u00E0\n KH\u00D4NG ki\u1EC3m `event.defaultPrevented`, n\u00EAn n\u00F3 \u0111i\u1EC1u h\u01B0\u1EDBng b\u1EA5t k\u1EC3 m\u00ECnh \u0111\u00E3\n ch\u1EB7n hay ch\u01B0a \u2014 spec `breadcrumb-item-clickable.spec.ts` b\u1EAFt \u0111\u00FAng ch\u1ED7 n\u00E0y.\n\n Thay b\u1EB1ng `href` t\u0129nh d\u1EF1ng t\u1EEB ch\u00EDnh `routerLink()`: th\u1EBB v\u1EABn l\u00E0 `<a>` c\u00F3\n `href` th\u1EADt n\u00EAn tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc l\u00E0 li\u00EAn k\u1EBFt, v\u1EABn Tab t\u1EDBi \u0111\u01B0\u1EE3c,\n v\u1EABn xem \u0111\u01B0\u1EE3c \u0111\u00EDch \u0111\u1EBFn \u1EDF thanh tr\u1EA1ng th\u00E1i \u2014 ch\u1EC9 `preventDefault()` ch\u1EB7n\n tr\u00ECnh duy\u1EC7t \u0111i theo. \u0110\u00F3 l\u00E0 kh\u00E1c bi\u1EC7t v\u1EDBi `disabled`, v\u1ED1n b\u1ECF lu\u00F4n th\u1EBB `<a>`.\n -->\n @if (routerLink() && clickable()) {\n <a\n [routerLink]=\"routerLink()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else if (routerLink()) {\n <a\n [href]=\"staticHref()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else {\n <a\n [href]=\"href()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n }\n } @else {\n <!--\n Th\u1EE9 t\u1EF1 ba nh\u00E1nh l\u00E0 C\u1ED0 \u00DD: `disabled` \u0111\u1EE9ng TR\u01AF\u1EDAC `isCurrent`. M\u1EE5c cu\u1ED1i m\u00E0\n c\u0169ng b\u1ECB kho\u00E1 th\u00EC ph\u1EA3i hi\u1EC7n l\u00E0 kho\u00E1 \u2014 ng\u01B0\u1EE3c l\u1EA1i ng\u01B0\u1EDDi d\u00F9ng th\u1EA5y n\u00F3 y h\u1EC7t\n trang hi\u1EC7n t\u1EA1i v\u00E0 kh\u00F4ng hi\u1EC3u v\u00EC sao b\u1EA5m kh\u00F4ng \u0111\u01B0\u1EE3c.\n\n M\u00E0u theo chu\u1EA9n disabled c\u1EE7a th\u01B0 vi\u1EC7n (`bg-disabled` + `text-fg-disabled`,\n \u0111\u00FAng Figma State=Disabled, xem button.component.ts). KH\u00D4NG d\u00F9ng\n `opacity-50`: m\u1EABu \u0111\u00F3 ch\u1EC9 c\u00F2n 2 component d\u00F9ng, v\u00E0 th\u01B0 vi\u1EC7n \u0111ang c\u00F3 b\u1ED1n\n b\u1EADc opacity song song ch\u01B0a h\u1ED9i t\u1EE5.\n\n M\u1EE5c \u0110ANG \u0110\u1EE8NG d\u00F9ng `text-heading` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 L\u1EC6CH FIGMA C\u00D3 CH\u1EE6 \u00DD\n (2026-09-24, theo y\u00EAu c\u1EA7u ng\u01B0\u1EDDi d\u00F9ng \"cho m\u00E0u \u0111\u1EADm h\u01A1n c\u00E1c m\u00E0u kh\u00E1c\").\n\n Figma `ol-breadcrumb-item` #1148:31348 `Status=Active` bind\n `colors/text/text-body-subtle` = #6B7280. \u0110o /breadcrumb \u1EDF ch\u1EBF \u0111\u1ED9 S\u00C1NG\n th\u00EC n\u00F3 cho t\u01B0\u01A1ng ph\u1EA3n **4,83** trong khi c\u00E1c c\u1EA5p cha (`text-body`\n #4A5565) \u0111\u1EA1t **7,56** \u2014 t\u1EE9c trang \u0111ang \u0111\u1EE9ng NH\u1EA0T H\u01A0N c\u1EA5p cha n\u00F3, ng\u01B0\u1EE3c\n ng\u1EEF ngh\u0129a. Nay `text-heading`: s\u00E1ng #101828 (17,75) \u00B7 t\u1ED1i #FFFFFF\n (20,13), \u0111\u1EADm h\u01A1n h\u1EB3n v\u00E0 t\u1EF1 l\u1EADt theo `.dark`.\n\n B\u1EA3n tr\u01B0\u1EDBc ph\u1EA3i vi\u1EBFt `text-body-subtle dark:text-heading` v\u00EC \u1EDF nh\u00E1nh t\u1ED1i\n `--text-body-subtle` b\u1ECB \u0111\u1EB7t B\u1EB0NG H\u1EC6T `--text-body` (c\u00F9ng gray-300) n\u00EAn\n m\u1EE5c cu\u1ED1i v\u00E0 m\u1EE5c th\u01B0\u1EDDng tr\u00F9ng m\u00E0u. D\u00F9ng `text-heading` cho c\u1EA3 hai ch\u1EBF \u0111\u1ED9\n th\u00EC h\u1EBFt c\u1EA7n ngo\u1EA1i l\u1EC7 \u0111\u00F3.\n\n N\u1EE3: c\u1EADp nh\u1EADt Figma `Status=Active` sang `colors/text/text-heading`.\n -->\n <span\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\n [class]=\"\n disabled()\n ? 'font-medium text-fg-disabled cursor-not-allowed'\n : isCurrent()\n ? 'font-medium text-heading'\n : status() === 'hover'\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\n : 'font-medium text-body'\n \"\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </span>\n }\n\n @if (!isCurrent() && showSeparator()) {\n <!--\n D\u1EA5u ng\u0103n d\u00F9ng `text-body-subtle`, KH\u00D4NG `text-fg-disabled`.\n\n `text-fg-disabled` (#99A1AF) tr\u00EAn n\u1EC1n tr\u1EAFng ch\u1EC9 \u0111\u1EA1t **2,60** \u2014 c\u00F3\n `aria-hidden` n\u00EAn kh\u00F4ng vi ph\u1EA1m WCAG, nh\u01B0ng ng\u01B0\u1EDDi nh\u00ECn k\u00E9m kh\u00F3 th\u1EA5y ranh\n gi\u1EDBi gi\u1EEFa c\u00E1c c\u1EA5p. `text-body-subtle` (#6B7280) \u0111\u1EA1t 4,83 m\u00E0 v\u1EABn nh\u1EA1t h\u01A1n\n ch\u1EEF c\u1EE7a ch\u1EB7ng, n\u00EAn v\u1EABn ra \"d\u1EA5u ng\u0103n\" ch\u1EE9 kh\u00F4ng c\u1EA1nh tranh v\u1EDBi n\u1ED9i dung.\n\n KH\u00C1C d\u00F2ng tr\u00EAn: ch\u1EB7ng `disabled` gi\u1EEF `text-fg-disabled` \u2014 \u0111\u00F3 l\u00E0 chu\u1EA9n\n tr\u1EA1ng th\u00E1i kho\u00E1 c\u1EE7a th\u01B0 vi\u1EC7n, v\u00E0 WCAG mi\u1EC5n tr\u1EEB ph\u1EA7n t\u1EED kh\u00F4ng b\u1EA5m \u0111\u01B0\u1EE3c.\n -->\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\n @switch (effectiveSeparator()) {\n @case ('slash') {\n <span class=\"text-xs font-normal\">/</span>\n }\n @case ('arrow') {\n <span class=\"text-xs\">\u2192</span>\n }\n @case ('dot') {\n <span class=\"text-xs\">\u2022</span>\n }\n @case ('chevron') {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n @default {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n }\n </span>\n }\n</li>\n" }]
4208
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], current: [{ type: i0.Input, args: [{ isSignal: true, alias: "current", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], showSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeparator", required: false }] }], separator: [{ type: i0.Input, args: [{ isSignal: true, alias: "separator", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
3760
4209
 
3761
4210
  class OlDownloadAppButton {
3762
4211
  store = input('appstore', /* @ts-ignore */
@@ -5231,9 +5680,13 @@ class OlDatepickerPopup {
5231
5680
  /**
5232
5681
  * Trần chiều cao + cuộn CHỈ cho popup nổi.
5233
5682
  *
5234
- * Dùng `[class]` trả chuỗi chứ KHÔNG `[class.max-h-…]` với giá trị tuỳ ý: tên class
5235
- * chứa dấu chấm và ngoặc vuông nên Angular cắt hỏng tên khi dùng class
5683
+ * Dùng `[class]` trả chuỗi chứ KHÔNG `[class.max-h-…]` với giá trị tuỳ ý: tên
5684
+ * class chứa dấu chấm và ngoặc vuông nên Angular cắt hỏng tên khi dùng class
5236
5685
  * binding từng cái (`[class.h-1.5]` ra `h-1`).
5686
+ *
5687
+ * Dấu `…` ở trên là cố ý, không phải ba chấm ASCII: Tailwind quét file thô,
5688
+ * không phân biệt chú thích với mã, nên tên class đầy đủ viết trong chú thích
5689
+ * cũng bị sinh thành luật CSS và làm build cảnh báo `Unexpected token Delim`.
5237
5690
  */
5238
5691
  wrapperClasses = computed(() => this.inline() ? '' : 'max-h-[var(--ol-dp-max-height,85svh)] overflow-y-auto overscroll-contain', /* @ts-ignore */
5239
5692
  ...(ngDevMode ? [{ debugName: "wrapperClasses" }] : /* istanbul ignore next */ []));
@@ -5503,12 +5956,12 @@ class OlDatepicker {
5503
5956
  const el = this.panel()?.nativeElement;
5504
5957
  const raBody = this.appendTo() === 'body';
5505
5958
  if (!open || !el) {
5506
- this.donDepPanel();
5959
+ this.detachPanelNow();
5507
5960
  return;
5508
5961
  }
5509
5962
  // Vào ngăn xếp Escape khi mở. Đăng ký ở đây chứ không trong `openPopup`
5510
5963
  // để mọi đường mở popup đều đi qua — kể cả mở bằng bàn phím.
5511
- this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup());
5964
+ this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup(), (target) => this.ownsTarget(target));
5512
5965
  // `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
5513
5966
  if (raBody && !this.detachPanel) {
5514
5967
  this.detachPanel = this.overlayContainer.attach(el);
@@ -5692,7 +6145,7 @@ class OlDatepicker {
5692
6145
  this.state.clear();
5693
6146
  }
5694
6147
  /** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
5695
- donDepPanel() {
6148
+ detachPanelNow() {
5696
6149
  if (this.stopTrackingHeight) {
5697
6150
  this.stopTrackingHeight();
5698
6151
  this.stopTrackingHeight = null;
@@ -5705,37 +6158,67 @@ class OlDatepicker {
5705
6158
  this.releaseEscape?.();
5706
6159
  this.releaseEscape = null;
5707
6160
  }
5708
- onDocumentClick(event) {
5709
- if (this.inline())
5710
- return;
5711
- const path = event.composedPath();
6161
+ /**
6162
+ * Phần tử được bấm có thuộc datepicker này không.
6163
+ *
6164
+ * Ngăn xếp `OlOverlayContainer` gọi hàm này để biết một cú click là "trong"
6165
+ * hay "ngoài". Trước đây mỗi component tự nghe `document:click`, nên một cú
6166
+ * bấm ra ngoài đóng luôn MỌI lớp — mở datepicker trong modal rồi bấm nền là
6167
+ * mất cả hộp thoại đang điền dở. Đo /modal ngày 2026-09-24 xác nhận.
6168
+ *
6169
+ * ## Phải kiểm CẢ PANEL, không chỉ host
6170
+ *
6171
+ * Với `appendTo="body"` panel đã bị `overlayContainer.attach()` chuyển ra
6172
+ * `.ol-overlay-container` — không còn là con của host, nên click vào chính
6173
+ * popup bị coi là click ra ngoài. Đo /modal: bật `appendTo` rồi bấm một
6174
+ * ngày → popup ĐÓNG NGAY, range picker cần hai ngày nên người dùng không
6175
+ * chọn nổi ngày kết thúc.
6176
+ *
6177
+ * Tắt `appendTo` thì popup ở lại đúng — lỗi chỉ xuất hiện khi treo ra ngoài,
6178
+ * tức đúng ca sinh ra để dùng trong modal.
6179
+ */
6180
+ ownsTarget(target) {
6181
+ if (!(target instanceof Node))
6182
+ return false;
6183
+ const panelEl = this.panel()?.nativeElement;
6184
+ return this.hostEl.nativeElement.contains(target) || (!!panelEl && panelEl.contains(target));
6185
+ }
6186
+ /**
6187
+ * Đóng popup khi component bị huỷ.
6188
+ *
6189
+ * Bắt buộc với `appendTo="body"`: panel treo vào `<body>` chứ không phải cây
6190
+ * DOM của component, nên Angular huỷ component mà panel VẪN Ở LẠI. Chuyển
6191
+ * trang khi đang mở lịch là để lại một popup mồ côi không ai đóng được.
6192
+ */
6193
+ ngOnDestroy() {
6194
+ this.closePopup();
5712
6195
  /**
5713
- * Phải kiểm CẢ PANEL, không chỉ host.
6196
+ * Gọi THẲNG `detachPanelNow()`, không trông vào `effect`.
5714
6197
  *
5715
- * Với `appendTo="body"` panel đã bị `overlayContainer.attach()` chuyển ra
5716
- * `.ol-overlay-container` — không còn là con của host, nên click vào chính
5717
- * popup không nằm trong `composedPath` của host và bị coi là click ra
5718
- * ngoài. Đo /modal: bật `appendTo` rồi bấm một ngày → popup ĐÓNG NGAY,
5719
- * range picker cần hai ngày nên người dùng không chọn nổi ngày kết thúc.
6198
+ * Việc trả panel về chỗ cũ nằm trong `effect()` ở constructor. Angular
6199
+ * DỪNG mọi effect của component ngay khi huỷ, nên `closePopup()` ở trên
6200
+ * đặt `isOpen = false` mà effect không bao giờ chạy để dọn.
6201
+ *
6202
+ * Với `appendTo="body"` panel đã treo vào `.ol-overlay-container` — không
6203
+ * còn là con của component — nên Angular xoá cây DOM của component mà panel
6204
+ * VẪN Ở LẠI. Nó mất luôn liên kết với ô nhập nên Floating UI trả về toạ độ
6205
+ * `(0, 0)`: popup nhảy lên góc trái trên màn hình và không cách nào đóng
6206
+ * được vì component đã biến mất. Đúng hiện tượng anh Vinh mô tả.
5720
6207
  *
5721
- * Tắt `appendTo` thì popup ở lại đúng — lỗi chỉ xuất hiện khi treo ra
5722
- * ngoài, tức đúng ca sinh ra để dùng trong modal.
6208
+ * Chỉ "thỉnh thoảng" vì nó cần popup ĐANG MỞ lúc chuyển trang; đóng trước
6209
+ * rồi mới chuyển thì effect đã kịp dọn.
5723
6210
  */
5724
- const panelEl = this.panel()?.nativeElement;
5725
- const clickedInside = path.includes(this.hostEl.nativeElement) || (!!panelEl && path.includes(panelEl));
5726
- if (!clickedInside) {
5727
- this.closePopup();
5728
- }
6211
+ this.detachPanelNow();
5729
6212
  }
5730
6213
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDatepicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
5731
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDatepicker, isStandalone: true, selector: "ol-datepicker", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", dateChange: "dateChange", timestampChange: "timestampChange", isoChange: "isoChange", formattedChange: "formattedChange" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, properties: { "class.block": "!inline()", "class.w-full": "!inline()", "class.inline-block": "inline()", "class.z-dropdown": "isOpen()" }, classAttribute: "relative" }, providers: [
6214
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDatepicker, isStandalone: true, selector: "ol-datepicker", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", dateChange: "dateChange", timestampChange: "timestampChange", isoChange: "isoChange", formattedChange: "formattedChange" }, host: { properties: { "class.block": "!inline()", "class.w-full": "!inline()", "class.inline-block": "inline()", "class.z-dropdown": "isOpen()" }, classAttribute: "relative" }, providers: [
5732
6215
  OlDatepickerStateService,
5733
6216
  {
5734
6217
  provide: NG_VALUE_ACCESSOR,
5735
6218
  useExisting: forwardRef(() => OlDatepicker),
5736
6219
  multi: true,
5737
6220
  },
5738
- ], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-body\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <!-- Input Trigger Field -->\n <div class=\"relative w-full\">\n <!-- Calendar Icon Prefix -->\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n\n <input\n [id]=\"inputId()\"\n type=\"text\"\n [value]=\"inputValue()\"\n (input)=\"onInputChange($any($event.target).value)\"\n (click)=\"togglePopup($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [class]=\"inputSizeClasses()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:ring-brand-950 shadow-xs transition-colors cursor-pointer\"\n />\n\n <!-- Clear Button or Arrow Dropdown -->\n <div class=\"absolute inset-y-0 inset-e-0 flex items-center pe-3 gap-1\">\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearInput($event)\"\n class=\"text-body-subtle hover:text-heading p-0.5 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 shadow-2xl\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n </div>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlDatepickerPopup, selector: "ol-datepicker-popup", inputs: ["showFooter", "dualMonth", "presets", "inline"], outputs: ["valueChange", "clearClick", "applyClick", "cancelClick"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6221
+ ], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <!-- Input Trigger Field -->\n <div class=\"relative w-full\">\n <!-- Calendar Icon Prefix -->\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n\n <input\n [id]=\"inputId()\"\n type=\"text\"\n [value]=\"inputValue()\"\n (input)=\"onInputChange($any($event.target).value)\"\n (click)=\"togglePopup($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [class]=\"inputSizeClasses()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:ring-brand-950 shadow-xs transition-colors cursor-pointer\"\n />\n\n <!-- Clear Button or Arrow Dropdown -->\n <div class=\"absolute inset-y-0 inset-e-0 flex items-center pe-3 gap-1\">\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearInput($event)\"\n class=\"text-body-subtle hover:text-heading p-0.5 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <!--\n `rounded-base` PH\u1EA2I kh\u1EDBp bo g\u00F3c c\u1EE7a `ol-datepicker-popup` b\u00EAn trong.\n\n Khung n\u00E0y mang `shadow-2xl` nh\u01B0ng tr\u01B0\u1EDBc \u0111\u00E2y kh\u00F4ng c\u00F3 `rounded` n\u00E0o,\n n\u00EAn b\u00F3ng \u0111\u1ED5 v\u1EBD theo h\u00ECnh CH\u1EEE NH\u1EACT VU\u00D4NG trong khi popup b\u00EAn trong bo\n 12px \u2014 hai g\u00F3c d\u01B0\u1EDBi l\u1ED9 ra hai v\u1EC7t vu\u00F4ng x\u00E1m. \u0110o ng\u00E0y 2026-09-24: hai\n khung c\u00F9ng 294\u00D7374, khung ngo\u00E0i `radius: 0px`, khung trong\n `radius: 12px`.\n -->\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n </div>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlDatepickerPopup, selector: "ol-datepicker-popup", inputs: ["showFooter", "dualMonth", "presets", "inline"], outputs: ["valueChange", "clearClick", "applyClick", "cancelClick"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5739
6222
  }
5740
6223
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDatepicker, decorators: [{
5741
6224
  type: Component,
@@ -5775,11 +6258,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
5775
6258
  '[class.w-full]': '!inline()',
5776
6259
  '[class.inline-block]': 'inline()',
5777
6260
  '[class.z-dropdown]': 'isOpen()',
5778
- }, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-body\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <!-- Input Trigger Field -->\n <div class=\"relative w-full\">\n <!-- Calendar Icon Prefix -->\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n\n <input\n [id]=\"inputId()\"\n type=\"text\"\n [value]=\"inputValue()\"\n (input)=\"onInputChange($any($event.target).value)\"\n (click)=\"togglePopup($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [class]=\"inputSizeClasses()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:ring-brand-950 shadow-xs transition-colors cursor-pointer\"\n />\n\n <!-- Clear Button or Arrow Dropdown -->\n <div class=\"absolute inset-y-0 inset-e-0 flex items-center pe-3 gap-1\">\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearInput($event)\"\n class=\"text-body-subtle hover:text-heading p-0.5 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 shadow-2xl\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n </div>\n }\n </div>\n }\n</div>\n" }]
5779
- }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], daysDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "daysDisabled", required: false }] }], datesDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesDisabled", required: false }] }], datesEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesEnabled", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooter", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], dateChange: [{ type: i0.Output, args: ["dateChange"] }], timestampChange: [{ type: i0.Output, args: ["timestampChange"] }], isoChange: [{ type: i0.Output, args: ["isoChange"] }], formattedChange: [{ type: i0.Output, args: ["formattedChange"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], onDocumentClick: [{
5780
- type: HostListener,
5781
- args: ['document:click', ['$event']]
5782
- }] } });
6261
+ }, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <!-- Input Trigger Field -->\n <div class=\"relative w-full\">\n <!-- Calendar Icon Prefix -->\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n\n <input\n [id]=\"inputId()\"\n type=\"text\"\n [value]=\"inputValue()\"\n (input)=\"onInputChange($any($event.target).value)\"\n (click)=\"togglePopup($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [class]=\"inputSizeClasses()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:ring-brand-950 shadow-xs transition-colors cursor-pointer\"\n />\n\n <!-- Clear Button or Arrow Dropdown -->\n <div class=\"absolute inset-y-0 inset-e-0 flex items-center pe-3 gap-1\">\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearInput($event)\"\n class=\"text-body-subtle hover:text-heading p-0.5 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <!--\n `rounded-base` PH\u1EA2I kh\u1EDBp bo g\u00F3c c\u1EE7a `ol-datepicker-popup` b\u00EAn trong.\n\n Khung n\u00E0y mang `shadow-2xl` nh\u01B0ng tr\u01B0\u1EDBc \u0111\u00E2y kh\u00F4ng c\u00F3 `rounded` n\u00E0o,\n n\u00EAn b\u00F3ng \u0111\u1ED5 v\u1EBD theo h\u00ECnh CH\u1EEE NH\u1EACT VU\u00D4NG trong khi popup b\u00EAn trong bo\n 12px \u2014 hai g\u00F3c d\u01B0\u1EDBi l\u1ED9 ra hai v\u1EC7t vu\u00F4ng x\u00E1m. \u0110o ng\u00E0y 2026-09-24: hai\n khung c\u00F9ng 294\u00D7374, khung ngo\u00E0i `radius: 0px`, khung trong\n `radius: 12px`.\n -->\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n </div>\n }\n </div>\n }\n</div>\n" }]
6262
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], daysDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "daysDisabled", required: false }] }], datesDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesDisabled", required: false }] }], datesEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesEnabled", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooter", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], dateChange: [{ type: i0.Output, args: ["dateChange"] }], timestampChange: [{ type: i0.Output, args: ["timestampChange"] }], isoChange: [{ type: i0.Output, args: ["isoChange"] }], formattedChange: [{ type: i0.Output, args: ["formattedChange"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
5783
6263
 
5784
6264
  class OlDateRangePicker {
5785
6265
  state = inject(OlDatepickerStateService);
@@ -5896,11 +6376,11 @@ class OlDateRangePicker {
5896
6376
  const el = this.panel()?.nativeElement;
5897
6377
  const raBody = this.appendTo() === 'body';
5898
6378
  if (!open || !el) {
5899
- this.donDepPanel();
6379
+ this.detachPanelNow();
5900
6380
  return;
5901
6381
  }
5902
6382
  // Vào ngăn xếp Escape — chỉ lớp trên cùng đóng.
5903
- this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup());
6383
+ this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup(), (target) => this.ownsTarget(target));
5904
6384
  // `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
5905
6385
  if (raBody && !this.detachPanel) {
5906
6386
  this.detachPanel = this.overlayContainer.attach(el);
@@ -6146,7 +6626,7 @@ class OlDateRangePicker {
6146
6626
  this.state.clear();
6147
6627
  }
6148
6628
  /** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
6149
- donDepPanel() {
6629
+ detachPanelNow() {
6150
6630
  if (this.stopTrackingHeight) {
6151
6631
  this.stopTrackingHeight();
6152
6632
  this.stopTrackingHeight = null;
@@ -6159,37 +6639,58 @@ class OlDateRangePicker {
6159
6639
  this.releaseEscape?.();
6160
6640
  this.releaseEscape = null;
6161
6641
  }
6162
- onDocumentClick(event) {
6163
- if (this.inline())
6164
- return;
6165
- const path = event.composedPath();
6642
+ /**
6643
+ * Phần tử được bấm có thuộc bộ chọn khoảng này không.
6644
+ *
6645
+ * Xem chú thích cùng tên ở `ol-datepicker` — hai component dùng chung cơ
6646
+ * chế. Range picker còn nhạy hơn: nó cần HAI cú bấm (ngày đầu, ngày cuối),
6647
+ * nên popup đóng sớm là người dùng không chọn nổi khoảng.
6648
+ *
6649
+ * Phải kiểm cả panel vì với `appendTo="body"` nó đã bị chuyển ra
6650
+ * `.ol-overlay-container`, không còn là con của host.
6651
+ */
6652
+ ownsTarget(target) {
6653
+ if (!(target instanceof Node))
6654
+ return false;
6655
+ const panelEl = this.panel()?.nativeElement;
6656
+ return this.hostEl.nativeElement.contains(target) || (!!panelEl && panelEl.contains(target));
6657
+ }
6658
+ /**
6659
+ * Đóng popup khi component bị huỷ.
6660
+ *
6661
+ * Bắt buộc với `appendTo="body"`: panel treo vào `<body>` nên Angular huỷ
6662
+ * component mà panel VẪN Ở LẠI. Chuyển trang khi đang mở lịch là để lại một
6663
+ * popup mồ côi không ai đóng được.
6664
+ */
6665
+ ngOnDestroy() {
6666
+ this.closePopup();
6166
6667
  /**
6167
- * Phải kiểm CẢ PANEL, không chỉ host.
6668
+ * Gọi THẲNG `detachPanelNow()`, không trông vào `effect`.
6168
6669
  *
6169
- * Với `appendTo="body"` panel đã bị `overlayContainer.attach()` chuyển ra
6170
- * `.ol-overlay-container` — không còn là con của host, nên click vào chính
6171
- * popup không nằm trong `composedPath` của host và bị coi là click ra
6172
- * ngoài. Đo /modal: bật `appendTo` rồi bấm một ngày → popup ĐÓNG NGAY,
6173
- * range picker cần hai ngày nên người dùng không chọn nổi ngày kết thúc.
6670
+ * Việc trả panel về chỗ cũ nằm trong `effect()` ở constructor. Angular
6671
+ * DỪNG mọi effect của component ngay khi huỷ, nên `closePopup()` ở trên
6672
+ * đặt `isOpen = false` mà effect không bao giờ chạy để dọn.
6673
+ *
6674
+ * Với `appendTo="body"` panel đã treo vào `.ol-overlay-container` — không
6675
+ * còn là con của component — nên Angular xoá cây DOM của component mà panel
6676
+ * VẪN Ở LẠI. Nó mất luôn liên kết với ô nhập nên Floating UI trả về toạ độ
6677
+ * `(0, 0)`: popup nhảy lên góc trái trên màn hình và không cách nào đóng
6678
+ * được vì component đã biến mất. Đúng hiện tượng anh Vinh mô tả.
6174
6679
  *
6175
- * Tắt `appendTo` thì popup ở lại đúng — lỗi chỉ xuất hiện khi treo ra
6176
- * ngoài, tức đúng ca sinh ra để dùng trong modal.
6680
+ * Chỉ "thỉnh thoảng" vì nó cần popup ĐANG MỞ lúc chuyển trang; đóng trước
6681
+ * rồi mới chuyển thì effect đã kịp dọn.
6177
6682
  */
6178
- const panelEl = this.panel()?.nativeElement;
6179
- const clickedInside = path.includes(this.hostEl.nativeElement) || (!!panelEl && path.includes(panelEl));
6180
- if (!clickedInside) {
6181
- this.closePopup();
6182
- }
6683
+ this.detachPanelNow();
6183
6684
  }
6184
6685
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDateRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
6185
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDateRangePicker, isStandalone: true, selector: "ol-date-range-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, fromLabel: { classPropertyName: "fromLabel", publicName: "fromLabel", isSignal: true, isRequired: false, transformFunction: null }, toLabel: { classPropertyName: "toLabel", publicName: "toLabel", isSignal: true, isRequired: false, transformFunction: null }, showInfoIcon: { classPropertyName: "showInfoIcon", publicName: "showInfoIcon", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, dualMonth: { classPropertyName: "dualMonth", publicName: "dualMonth", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", rangeChange: "rangeChange" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, properties: { "class.z-dropdown": "isOpen()" }, classAttribute: "relative inline-block" }, providers: [
6686
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDateRangePicker, isStandalone: true, selector: "ol-date-range-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, fromLabel: { classPropertyName: "fromLabel", publicName: "fromLabel", isSignal: true, isRequired: false, transformFunction: null }, toLabel: { classPropertyName: "toLabel", publicName: "toLabel", isSignal: true, isRequired: false, transformFunction: null }, showInfoIcon: { classPropertyName: "showInfoIcon", publicName: "showInfoIcon", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, dualMonth: { classPropertyName: "dualMonth", publicName: "dualMonth", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", rangeChange: "rangeChange" }, host: { properties: { "class.z-dropdown": "isOpen()" }, classAttribute: "relative inline-block" }, providers: [
6186
6687
  OlDatepickerStateService,
6187
6688
  {
6188
6689
  provide: NG_VALUE_ACCESSOR,
6189
6690
  useExisting: forwardRef(() => OlDateRangePicker),
6190
6691
  multi: true,
6191
6692
  },
6192
- ], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-body select-none\">\n <span>{{ fromLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-3\"\n [class]=\"inputSizeClasses()\"\n />\n </div>\n </div>\n\n <!-- To Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-body select-none\">\n <span>{{ toLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-8\"\n [class]=\"inputSizeClasses()\"\n />\n\n <!-- Clear button on the right input -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"absolute inset-y-0 inset-e-0 flex items-center pe-2.5 text-body-subtle hover:text-heading transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n </div>\n } @else {\n <!-- Joined Single Container Variant -->\n @if (label()) {\n <label class=\"flex items-center gap-1 text-sm font-medium text-body\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <div\n class=\"flex items-center gap-2 bg-neutral-secondary-medium border border-default-medium rounded-md p-1 shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle focus-within:border-brand dark:focus-within:border-brand-subtle transition-all cursor-pointer\"\n [class]=\"joinedWrapperClasses()\"\n (click)=\"togglePopup($event)\"\n >\n <!-- Start Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Separator Arrow -->\n <span class=\"text-fg-disabled text-xs font-medium px-1 select-none\"> \u0111\u1EBFn </span>\n\n <!-- End Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Clear Button -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"text-body-subtle hover:text-heading p-1 mr-1 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n }\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 shadow-2xl w-max max-w-[calc(100vw-2rem)]\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n </div>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlDatepickerPopup, selector: "ol-datepicker-popup", inputs: ["showFooter", "dualMonth", "presets", "inline"], outputs: ["valueChange", "clearClick", "applyClick", "cancelClick"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6693
+ ], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ fromLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-3\"\n [class]=\"inputSizeClasses()\"\n />\n </div>\n </div>\n\n <!-- To Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ toLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-8\"\n [class]=\"inputSizeClasses()\"\n />\n\n <!-- Clear button on the right input -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"absolute inset-y-0 inset-e-0 flex items-center pe-2.5 text-body-subtle hover:text-heading transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n </div>\n } @else {\n <!-- Joined Single Container Variant -->\n @if (label()) {\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <div\n class=\"flex items-center gap-2 bg-neutral-secondary-medium border border-default-medium rounded-md p-1 shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle focus-within:border-brand dark:focus-within:border-brand-subtle transition-all cursor-pointer\"\n [class]=\"joinedWrapperClasses()\"\n (click)=\"togglePopup($event)\"\n >\n <!-- Start Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Separator Arrow -->\n <span class=\"text-fg-disabled text-xs font-medium px-1 select-none\"> \u0111\u1EBFn </span>\n\n <!-- End Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Clear Button -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"text-body-subtle hover:text-heading p-1 mr-1 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n }\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl w-max max-w-[calc(100vw-2rem)]\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n </div>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlDatepickerPopup, selector: "ol-datepicker-popup", inputs: ["showFooter", "dualMonth", "presets", "inline"], outputs: ["valueChange", "clearClick", "applyClick", "cancelClick"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6193
6694
  }
6194
6695
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDateRangePicker, decorators: [{
6195
6696
  type: Component,
@@ -6213,11 +6714,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
6213
6714
  */
6214
6715
  class: 'relative inline-block',
6215
6716
  '[class.z-dropdown]': 'isOpen()',
6216
- }, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-body select-none\">\n <span>{{ fromLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-3\"\n [class]=\"inputSizeClasses()\"\n />\n </div>\n </div>\n\n <!-- To Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-body select-none\">\n <span>{{ toLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-8\"\n [class]=\"inputSizeClasses()\"\n />\n\n <!-- Clear button on the right input -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"absolute inset-y-0 inset-e-0 flex items-center pe-2.5 text-body-subtle hover:text-heading transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n </div>\n } @else {\n <!-- Joined Single Container Variant -->\n @if (label()) {\n <label class=\"flex items-center gap-1 text-sm font-medium text-body\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <div\n class=\"flex items-center gap-2 bg-neutral-secondary-medium border border-default-medium rounded-md p-1 shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle focus-within:border-brand dark:focus-within:border-brand-subtle transition-all cursor-pointer\"\n [class]=\"joinedWrapperClasses()\"\n (click)=\"togglePopup($event)\"\n >\n <!-- Start Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Separator Arrow -->\n <span class=\"text-fg-disabled text-xs font-medium px-1 select-none\"> \u0111\u1EBFn </span>\n\n <!-- End Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Clear Button -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"text-body-subtle hover:text-heading p-1 mr-1 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n }\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 shadow-2xl w-max max-w-[calc(100vw-2rem)]\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n </div>\n }\n </div>\n }\n</div>\n" }]
6217
- }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], startPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPlaceholder", required: false }] }], endPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "endPlaceholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], fromLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fromLabel", required: false }] }], toLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toLabel", required: false }] }], showInfoIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showInfoIcon", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], dualMonth: [{ type: i0.Input, args: [{ isSignal: true, alias: "dualMonth", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], daysDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "daysDisabled", required: false }] }], datesDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesDisabled", required: false }] }], datesEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesEnabled", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooter", required: false }] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], onDocumentClick: [{
6218
- type: HostListener,
6219
- args: ['document:click', ['$event']]
6220
- }] } });
6717
+ }, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ fromLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-3\"\n [class]=\"inputSizeClasses()\"\n />\n </div>\n </div>\n\n <!-- To Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ toLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-8\"\n [class]=\"inputSizeClasses()\"\n />\n\n <!-- Clear button on the right input -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"absolute inset-y-0 inset-e-0 flex items-center pe-2.5 text-body-subtle hover:text-heading transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n </div>\n } @else {\n <!-- Joined Single Container Variant -->\n @if (label()) {\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <div\n class=\"flex items-center gap-2 bg-neutral-secondary-medium border border-default-medium rounded-md p-1 shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle focus-within:border-brand dark:focus-within:border-brand-subtle transition-all cursor-pointer\"\n [class]=\"joinedWrapperClasses()\"\n (click)=\"togglePopup($event)\"\n >\n <!-- Start Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Separator Arrow -->\n <span class=\"text-fg-disabled text-xs font-medium px-1 select-none\"> \u0111\u1EBFn </span>\n\n <!-- End Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Clear Button -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"text-body-subtle hover:text-heading p-1 mr-1 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n }\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl w-max max-w-[calc(100vw-2rem)]\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n </div>\n }\n </div>\n }\n</div>\n" }]
6718
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], startPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPlaceholder", required: false }] }], endPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "endPlaceholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], fromLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fromLabel", required: false }] }], toLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toLabel", required: false }] }], showInfoIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showInfoIcon", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], dualMonth: [{ type: i0.Input, args: [{ isSignal: true, alias: "dualMonth", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], daysDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "daysDisabled", required: false }] }], datesDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesDisabled", required: false }] }], datesEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesEnabled", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooter", required: false }] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
6221
6719
 
6222
6720
  class OlDatepickerDirective {
6223
6721
  el = inject((ElementRef));
@@ -7563,10 +8061,10 @@ class OlInput {
7563
8061
  * lệch 2px so với thiết kế.
7564
8062
  */
7565
8063
  paddingClasses = computed(() => {
7566
- const coIconTrai = this.leftIcon() && !this.addOnIcon();
7567
- const coIconPhai = this.rightIcon() || this.state() === 'error' || this.state() === 'success';
8064
+ const hasLeadingIcon = this.leftIcon() && !this.addOnIcon();
8065
+ const hasTrailingIcon = this.rightIcon() || this.state() === 'error' || this.state() === 'success';
7568
8066
  const classes = [];
7569
- if (!coIconTrai) {
8067
+ if (!hasLeadingIcon) {
7570
8068
  switch (this.size()) {
7571
8069
  case 'sm':
7572
8070
  classes.push('px-2.5');
@@ -7584,7 +8082,7 @@ class OlInput {
7584
8082
  else {
7585
8083
  classes.push('ps-2.5');
7586
8084
  }
7587
- if (coIconPhai)
8085
+ if (hasTrailingIcon)
7588
8086
  classes.push('pe-2.5');
7589
8087
  return classes.join(' ');
7590
8088
  }, /* @ts-ignore */
@@ -7610,7 +8108,7 @@ class OlInput {
7610
8108
  this.disabled.set(isDisabled);
7611
8109
  }
7612
8110
  /** Gọi từ template mỗi khi giá trị đổi, để form biết. */
7613
- phatThayDoi(val) {
8111
+ emitChange(val) {
7614
8112
  this.value.set(val);
7615
8113
  this.onChange(val);
7616
8114
  }
@@ -7638,7 +8136,7 @@ class OlInput {
7638
8136
  useExisting: forwardRef(() => OlInput),
7639
8137
  multi: true,
7640
8138
  },
7641
- ], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"phatThayDoi($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\n [class]=\"paddingClasses()\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n />\n\n <!--\n N\u00DAT XO\u00C1 \u2014 \u0111\u1EB7t TR\u01AF\u1EDAC icon tr\u1EA1ng th\u00E1i.\n\n Icon l\u1ED7i/th\u00E0nh c\u00F4ng ch\u1EC9 \u0111\u1EC3 \u0111\u1ECDc, n\u00FAt xo\u00E1 th\u00EC b\u1EA5m \u0111\u01B0\u1EE3c; \u0111\u1EB7t n\u00FAt sau th\u00EC \u1EDF\n tr\u1EA1ng th\u00E1i l\u1ED7i n\u00F3 bi\u1EBFn m\u1EA5t, \u0111\u00FAng l\u00FAc ng\u01B0\u1EDDi d\u00F9ng mu\u1ED1n xo\u00E1 \u0111i g\u00F5 l\u1EA1i.\n\n \u1EA8n khi `readonly` ho\u1EB7c `disabled`: \u00F4 kh\u00F4ng s\u1EEDa \u0111\u01B0\u1EE3c th\u00EC n\u00FAt xo\u00E1 l\u00E0 m\u1EDDi\n b\u1EA5m v\u00E0o ch\u1ED7 kh\u00F4ng l\u00E0m g\u00EC.\n -->\n @if (clearable() && value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n class=\"flex shrink-0 cursor-pointer items-center rounded-full p-0.5 pe-3 text-body-subtle transition-colors hover:text-heading\"\n [attr.aria-label]=\"clearAriaLabel()\"\n (click)=\"clearValue($event)\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n\n <!-- Right Icon / Status Icon -->\n @if (state() === 'error') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"sm\" />\n </div>\n } @else if (state() === 'success') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-success\">\n <ol-icon name=\"check-circle\" size=\"sm\" />\n </div>\n } @else if (rightIcon()) {\n <div class=\"flex items-center pe-3 text-body-subtle shrink-0\">\n <ng-content select=\"[rightIcon]\" />\n </div>\n }\n\n <!-- Suffix Add-on Text -->\n @if (suffixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-s border-default-medium select-none shrink-0\"\n >\n {{ suffixText() }}\n </span>\n }\n </div>\n\n <!-- Helper Text & Right Helper Action -->\n @if (helperText() || rightHelperText()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [id]=\"helperId()\"\n [attr.role]=\"state() === 'error' ? 'alert' : null\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger]=\"state() === 'error'\"\n [class.text-fg-success]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (rightHelperText()) {\n <span class=\"text-xs text-fg-brand hover:underline cursor-pointer ms-auto font-medium\">\n {{ rightHelperText() }}\n </span>\n }\n <ng-content select=\"[rightHelperAction]\" />\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8139
+ ], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\n [class]=\"paddingClasses()\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n />\n\n <!--\n N\u00DAT XO\u00C1 \u2014 \u0111\u1EB7t TR\u01AF\u1EDAC icon tr\u1EA1ng th\u00E1i.\n\n Icon l\u1ED7i/th\u00E0nh c\u00F4ng ch\u1EC9 \u0111\u1EC3 \u0111\u1ECDc, n\u00FAt xo\u00E1 th\u00EC b\u1EA5m \u0111\u01B0\u1EE3c; \u0111\u1EB7t n\u00FAt sau th\u00EC \u1EDF\n tr\u1EA1ng th\u00E1i l\u1ED7i n\u00F3 bi\u1EBFn m\u1EA5t, \u0111\u00FAng l\u00FAc ng\u01B0\u1EDDi d\u00F9ng mu\u1ED1n xo\u00E1 \u0111i g\u00F5 l\u1EA1i.\n\n \u1EA8n khi `readonly` ho\u1EB7c `disabled`: \u00F4 kh\u00F4ng s\u1EEDa \u0111\u01B0\u1EE3c th\u00EC n\u00FAt xo\u00E1 l\u00E0 m\u1EDDi\n b\u1EA5m v\u00E0o ch\u1ED7 kh\u00F4ng l\u00E0m g\u00EC.\n -->\n @if (clearable() && value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n class=\"flex shrink-0 cursor-pointer items-center rounded-full p-0.5 pe-3 text-body-subtle transition-colors hover:text-heading\"\n [attr.aria-label]=\"clearAriaLabel()\"\n (click)=\"clearValue($event)\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n\n <!-- Right Icon / Status Icon -->\n @if (state() === 'error') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"sm\" />\n </div>\n } @else if (state() === 'success') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-success\">\n <ol-icon name=\"check-circle\" size=\"sm\" />\n </div>\n } @else if (rightIcon()) {\n <div class=\"flex items-center pe-3 text-body-subtle shrink-0\">\n <ng-content select=\"[rightIcon]\" />\n </div>\n }\n\n <!-- Suffix Add-on Text -->\n @if (suffixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-s border-default-medium select-none shrink-0\"\n >\n {{ suffixText() }}\n </span>\n }\n </div>\n\n <!-- Helper Text & Right Helper Action -->\n @if (helperText() || rightHelperText()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [id]=\"helperId()\"\n [attr.role]=\"state() === 'error' ? 'alert' : null\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger]=\"state() === 'error'\"\n [class.text-fg-success]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (rightHelperText()) {\n <span class=\"text-xs text-fg-brand hover:underline cursor-pointer ms-auto font-medium\">\n {{ rightHelperText() }}\n </span>\n }\n <ng-content select=\"[rightHelperAction]\" />\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7642
8140
  }
7643
8141
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlInput, decorators: [{
7644
8142
  type: Component,
@@ -7654,7 +8152,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
7654
8152
  useExisting: forwardRef(() => OlInput),
7655
8153
  multi: true,
7656
8154
  },
7657
- ], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"phatThayDoi($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\n [class]=\"paddingClasses()\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n />\n\n <!--\n N\u00DAT XO\u00C1 \u2014 \u0111\u1EB7t TR\u01AF\u1EDAC icon tr\u1EA1ng th\u00E1i.\n\n Icon l\u1ED7i/th\u00E0nh c\u00F4ng ch\u1EC9 \u0111\u1EC3 \u0111\u1ECDc, n\u00FAt xo\u00E1 th\u00EC b\u1EA5m \u0111\u01B0\u1EE3c; \u0111\u1EB7t n\u00FAt sau th\u00EC \u1EDF\n tr\u1EA1ng th\u00E1i l\u1ED7i n\u00F3 bi\u1EBFn m\u1EA5t, \u0111\u00FAng l\u00FAc ng\u01B0\u1EDDi d\u00F9ng mu\u1ED1n xo\u00E1 \u0111i g\u00F5 l\u1EA1i.\n\n \u1EA8n khi `readonly` ho\u1EB7c `disabled`: \u00F4 kh\u00F4ng s\u1EEDa \u0111\u01B0\u1EE3c th\u00EC n\u00FAt xo\u00E1 l\u00E0 m\u1EDDi\n b\u1EA5m v\u00E0o ch\u1ED7 kh\u00F4ng l\u00E0m g\u00EC.\n -->\n @if (clearable() && value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n class=\"flex shrink-0 cursor-pointer items-center rounded-full p-0.5 pe-3 text-body-subtle transition-colors hover:text-heading\"\n [attr.aria-label]=\"clearAriaLabel()\"\n (click)=\"clearValue($event)\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n\n <!-- Right Icon / Status Icon -->\n @if (state() === 'error') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"sm\" />\n </div>\n } @else if (state() === 'success') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-success\">\n <ol-icon name=\"check-circle\" size=\"sm\" />\n </div>\n } @else if (rightIcon()) {\n <div class=\"flex items-center pe-3 text-body-subtle shrink-0\">\n <ng-content select=\"[rightIcon]\" />\n </div>\n }\n\n <!-- Suffix Add-on Text -->\n @if (suffixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-s border-default-medium select-none shrink-0\"\n >\n {{ suffixText() }}\n </span>\n }\n </div>\n\n <!-- Helper Text & Right Helper Action -->\n @if (helperText() || rightHelperText()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [id]=\"helperId()\"\n [attr.role]=\"state() === 'error' ? 'alert' : null\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger]=\"state() === 'error'\"\n [class.text-fg-success]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (rightHelperText()) {\n <span class=\"text-xs text-fg-brand hover:underline cursor-pointer ms-auto font-medium\">\n {{ rightHelperText() }}\n </span>\n }\n <ng-content select=\"[rightHelperAction]\" />\n </div>\n }\n</div>\n" }]
8155
+ ], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\n [class]=\"paddingClasses()\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n />\n\n <!--\n N\u00DAT XO\u00C1 \u2014 \u0111\u1EB7t TR\u01AF\u1EDAC icon tr\u1EA1ng th\u00E1i.\n\n Icon l\u1ED7i/th\u00E0nh c\u00F4ng ch\u1EC9 \u0111\u1EC3 \u0111\u1ECDc, n\u00FAt xo\u00E1 th\u00EC b\u1EA5m \u0111\u01B0\u1EE3c; \u0111\u1EB7t n\u00FAt sau th\u00EC \u1EDF\n tr\u1EA1ng th\u00E1i l\u1ED7i n\u00F3 bi\u1EBFn m\u1EA5t, \u0111\u00FAng l\u00FAc ng\u01B0\u1EDDi d\u00F9ng mu\u1ED1n xo\u00E1 \u0111i g\u00F5 l\u1EA1i.\n\n \u1EA8n khi `readonly` ho\u1EB7c `disabled`: \u00F4 kh\u00F4ng s\u1EEDa \u0111\u01B0\u1EE3c th\u00EC n\u00FAt xo\u00E1 l\u00E0 m\u1EDDi\n b\u1EA5m v\u00E0o ch\u1ED7 kh\u00F4ng l\u00E0m g\u00EC.\n -->\n @if (clearable() && value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n class=\"flex shrink-0 cursor-pointer items-center rounded-full p-0.5 pe-3 text-body-subtle transition-colors hover:text-heading\"\n [attr.aria-label]=\"clearAriaLabel()\"\n (click)=\"clearValue($event)\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n\n <!-- Right Icon / Status Icon -->\n @if (state() === 'error') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"sm\" />\n </div>\n } @else if (state() === 'success') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-success\">\n <ol-icon name=\"check-circle\" size=\"sm\" />\n </div>\n } @else if (rightIcon()) {\n <div class=\"flex items-center pe-3 text-body-subtle shrink-0\">\n <ng-content select=\"[rightIcon]\" />\n </div>\n }\n\n <!-- Suffix Add-on Text -->\n @if (suffixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-s border-default-medium select-none shrink-0\"\n >\n {{ suffixText() }}\n </span>\n }\n </div>\n\n <!-- Helper Text & Right Helper Action -->\n @if (helperText() || rightHelperText()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [id]=\"helperId()\"\n [attr.role]=\"state() === 'error' ? 'alert' : null\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger]=\"state() === 'error'\"\n [class.text-fg-success]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (rightHelperText()) {\n <span class=\"text-xs text-fg-brand hover:underline cursor-pointer ms-auto font-medium\">\n {{ rightHelperText() }}\n </span>\n }\n <ng-content select=\"[rightHelperAction]\" />\n </div>\n }\n</div>\n" }]
7658
8156
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], labelIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelIcon", required: false }] }], labelTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelTooltip", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], prefixText: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixText", required: false }] }], suffixText: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixText", required: false }] }], addOnIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOnIcon", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], rightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightIcon", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearAriaLabel", required: false }] }], cleared: [{ type: i0.Output, args: ["cleared"] }], rightHelperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightHelperText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }] } });
7659
8157
 
7660
8158
  let nextTextareaId = 0;
@@ -7738,7 +8236,7 @@ class OlTextarea {
7738
8236
  this.disabled.set(isDisabled);
7739
8237
  }
7740
8238
  /** Gọi từ template mỗi khi giá trị đổi, để form biết. */
7741
- phatThayDoi(val) {
8239
+ emitChange(val) {
7742
8240
  this.value.set(val);
7743
8241
  this.onChange(val);
7744
8242
  }
@@ -7753,7 +8251,7 @@ class OlTextarea {
7753
8251
  useExisting: forwardRef(() => OlTextarea),
7754
8252
  multi: true,
7755
8253
  },
7756
- ], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"phatThayDoi($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm rounded-base outline-none border font-normal shadow-xs\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n ></textarea>\n </div>\n } @else {\n <!--\n Figma Ready-to-use Comment / Reply Textarea Card (`Type=Chatroom`).\n\n `rounded-md` (12px) ch\u1EE9 kh\u00F4ng `rounded-lg` (16px): \u0111\u1ECDc Figma\n `ol-textarea` bi\u1EBFn th\u1EC3 `Type=Chatroom, State=Initial` ra **radius 12** \u2014\n c\u00F9ng b\u1EADc v\u1EDBi `ol-input` v\u00E0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD. \u0110o trang /forms\n 2026-09-17: `ol-textarea` l\u00E0 component DUY NH\u1EA4T trong h\u1ECD c\u00F3 hai gi\u00E1 tr\u1ECB\n bo g\u00F3c (12px v\u00E0 16px), l\u1EC7ch v\u1EDBi 9 component c\u00F2n l\u1EA1i.\n -->\n <div\n class=\"border border-default-medium rounded-md bg-neutral-primary overflow-hidden shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle dark:focus-within:ring-brand-950 focus-within:border-brand\"\n >\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() || 'Vi\u1EBFt b\u00ECnh lu\u1EADn ho\u1EB7c ph\u1EA3n h\u1ED3i...'\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"phatThayDoi($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm text-body bg-transparent border-0 outline-none resize-none placeholder:text-body-subtle\"\n ></textarea>\n <!-- Bottom Action Bar -->\n <div\n class=\"flex items-center justify-between px-3 py-2 border-t border-default-medium bg-neutral-secondary-medium\"\n >\n <div class=\"flex items-center gap-1 text-body-subtle\">\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"\u0110\u00EDnh k\u00E8m t\u1EC7p\"\n >\n <ol-icon name=\"link\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Ch\u00E8n v\u1ECB tr\u00ED/h\u00ECnh \u1EA3nh\"\n >\n <ol-icon name=\"photo\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Bi\u1EC3u t\u01B0\u1EE3ng c\u1EA3m x\u00FAc\"\n >\n <ol-icon name=\"mynaui:smile\" size=\"sm\" />\n </button>\n </div>\n <button\n type=\"button\"\n (click)=\"onSubmit()\"\n [disabled]=\"disabled() || !value().trim()\"\n class=\"inline-flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong disabled:opacity-50 disabled:cursor-not-allowed rounded-lg shadow-xs\"\n >\n <span>G\u1EEDi</span>\n <ol-icon name=\"arrow-right\" size=\"xs\" />\n </button>\n </div>\n </div>\n }\n @if (helperText() || showCharCount()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (showCharCount()) {\n <span class=\"text-body-subtle ms-auto\">\n {{ value().length }}{{ maxLength() ? '/' + maxLength() : '' }} k\u00FD t\u1EF1\n </span>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8254
+ ], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm rounded-base outline-none border font-normal shadow-xs\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n ></textarea>\n </div>\n } @else {\n <!--\n Figma Ready-to-use Comment / Reply Textarea Card (`Type=Chatroom`).\n\n `rounded-md` (12px) ch\u1EE9 kh\u00F4ng `rounded-lg` (16px): \u0111\u1ECDc Figma\n `ol-textarea` bi\u1EBFn th\u1EC3 `Type=Chatroom, State=Initial` ra **radius 12** \u2014\n c\u00F9ng b\u1EADc v\u1EDBi `ol-input` v\u00E0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD. \u0110o trang /forms\n 2026-09-17: `ol-textarea` l\u00E0 component DUY NH\u1EA4T trong h\u1ECD c\u00F3 hai gi\u00E1 tr\u1ECB\n bo g\u00F3c (12px v\u00E0 16px), l\u1EC7ch v\u1EDBi 9 component c\u00F2n l\u1EA1i.\n -->\n <div\n class=\"border border-default-medium rounded-md bg-neutral-primary overflow-hidden shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle dark:focus-within:ring-brand-950 focus-within:border-brand\"\n >\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() || 'Vi\u1EBFt b\u00ECnh lu\u1EADn ho\u1EB7c ph\u1EA3n h\u1ED3i...'\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm text-body bg-transparent border-0 outline-none resize-none placeholder:text-body-subtle\"\n ></textarea>\n <!-- Bottom Action Bar -->\n <div\n class=\"flex items-center justify-between px-3 py-2 border-t border-default-medium bg-neutral-secondary-medium\"\n >\n <div class=\"flex items-center gap-1 text-body-subtle\">\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"\u0110\u00EDnh k\u00E8m t\u1EC7p\"\n >\n <ol-icon name=\"link\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Ch\u00E8n v\u1ECB tr\u00ED/h\u00ECnh \u1EA3nh\"\n >\n <ol-icon name=\"photo\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Bi\u1EC3u t\u01B0\u1EE3ng c\u1EA3m x\u00FAc\"\n >\n <ol-icon name=\"mynaui:smile\" size=\"sm\" />\n </button>\n </div>\n <button\n type=\"button\"\n (click)=\"onSubmit()\"\n [disabled]=\"disabled() || !value().trim()\"\n class=\"inline-flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong disabled:opacity-50 disabled:cursor-not-allowed rounded-lg shadow-xs\"\n >\n <span>G\u1EEDi</span>\n <ol-icon name=\"arrow-right\" size=\"xs\" />\n </button>\n </div>\n </div>\n }\n @if (helperText() || showCharCount()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (showCharCount()) {\n <span class=\"text-body-subtle ms-auto\">\n {{ value().length }}{{ maxLength() ? '/' + maxLength() : '' }} k\u00FD t\u1EF1\n </span>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7757
8255
  }
7758
8256
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTextarea, decorators: [{
7759
8257
  type: Component,
@@ -7769,7 +8267,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
7769
8267
  useExisting: forwardRef(() => OlTextarea),
7770
8268
  multi: true,
7771
8269
  },
7772
- ], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"phatThayDoi($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm rounded-base outline-none border font-normal shadow-xs\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n ></textarea>\n </div>\n } @else {\n <!--\n Figma Ready-to-use Comment / Reply Textarea Card (`Type=Chatroom`).\n\n `rounded-md` (12px) ch\u1EE9 kh\u00F4ng `rounded-lg` (16px): \u0111\u1ECDc Figma\n `ol-textarea` bi\u1EBFn th\u1EC3 `Type=Chatroom, State=Initial` ra **radius 12** \u2014\n c\u00F9ng b\u1EADc v\u1EDBi `ol-input` v\u00E0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD. \u0110o trang /forms\n 2026-09-17: `ol-textarea` l\u00E0 component DUY NH\u1EA4T trong h\u1ECD c\u00F3 hai gi\u00E1 tr\u1ECB\n bo g\u00F3c (12px v\u00E0 16px), l\u1EC7ch v\u1EDBi 9 component c\u00F2n l\u1EA1i.\n -->\n <div\n class=\"border border-default-medium rounded-md bg-neutral-primary overflow-hidden shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle dark:focus-within:ring-brand-950 focus-within:border-brand\"\n >\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() || 'Vi\u1EBFt b\u00ECnh lu\u1EADn ho\u1EB7c ph\u1EA3n h\u1ED3i...'\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"phatThayDoi($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm text-body bg-transparent border-0 outline-none resize-none placeholder:text-body-subtle\"\n ></textarea>\n <!-- Bottom Action Bar -->\n <div\n class=\"flex items-center justify-between px-3 py-2 border-t border-default-medium bg-neutral-secondary-medium\"\n >\n <div class=\"flex items-center gap-1 text-body-subtle\">\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"\u0110\u00EDnh k\u00E8m t\u1EC7p\"\n >\n <ol-icon name=\"link\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Ch\u00E8n v\u1ECB tr\u00ED/h\u00ECnh \u1EA3nh\"\n >\n <ol-icon name=\"photo\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Bi\u1EC3u t\u01B0\u1EE3ng c\u1EA3m x\u00FAc\"\n >\n <ol-icon name=\"mynaui:smile\" size=\"sm\" />\n </button>\n </div>\n <button\n type=\"button\"\n (click)=\"onSubmit()\"\n [disabled]=\"disabled() || !value().trim()\"\n class=\"inline-flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong disabled:opacity-50 disabled:cursor-not-allowed rounded-lg shadow-xs\"\n >\n <span>G\u1EEDi</span>\n <ol-icon name=\"arrow-right\" size=\"xs\" />\n </button>\n </div>\n </div>\n }\n @if (helperText() || showCharCount()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (showCharCount()) {\n <span class=\"text-body-subtle ms-auto\">\n {{ value().length }}{{ maxLength() ? '/' + maxLength() : '' }} k\u00FD t\u1EF1\n </span>\n }\n </div>\n }\n</div>\n" }]
8270
+ ], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm rounded-base outline-none border font-normal shadow-xs\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n ></textarea>\n </div>\n } @else {\n <!--\n Figma Ready-to-use Comment / Reply Textarea Card (`Type=Chatroom`).\n\n `rounded-md` (12px) ch\u1EE9 kh\u00F4ng `rounded-lg` (16px): \u0111\u1ECDc Figma\n `ol-textarea` bi\u1EBFn th\u1EC3 `Type=Chatroom, State=Initial` ra **radius 12** \u2014\n c\u00F9ng b\u1EADc v\u1EDBi `ol-input` v\u00E0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD. \u0110o trang /forms\n 2026-09-17: `ol-textarea` l\u00E0 component DUY NH\u1EA4T trong h\u1ECD c\u00F3 hai gi\u00E1 tr\u1ECB\n bo g\u00F3c (12px v\u00E0 16px), l\u1EC7ch v\u1EDBi 9 component c\u00F2n l\u1EA1i.\n -->\n <div\n class=\"border border-default-medium rounded-md bg-neutral-primary overflow-hidden shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle dark:focus-within:ring-brand-950 focus-within:border-brand\"\n >\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() || 'Vi\u1EBFt b\u00ECnh lu\u1EADn ho\u1EB7c ph\u1EA3n h\u1ED3i...'\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm text-body bg-transparent border-0 outline-none resize-none placeholder:text-body-subtle\"\n ></textarea>\n <!-- Bottom Action Bar -->\n <div\n class=\"flex items-center justify-between px-3 py-2 border-t border-default-medium bg-neutral-secondary-medium\"\n >\n <div class=\"flex items-center gap-1 text-body-subtle\">\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"\u0110\u00EDnh k\u00E8m t\u1EC7p\"\n >\n <ol-icon name=\"link\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Ch\u00E8n v\u1ECB tr\u00ED/h\u00ECnh \u1EA3nh\"\n >\n <ol-icon name=\"photo\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Bi\u1EC3u t\u01B0\u1EE3ng c\u1EA3m x\u00FAc\"\n >\n <ol-icon name=\"mynaui:smile\" size=\"sm\" />\n </button>\n </div>\n <button\n type=\"button\"\n (click)=\"onSubmit()\"\n [disabled]=\"disabled() || !value().trim()\"\n class=\"inline-flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong disabled:opacity-50 disabled:cursor-not-allowed rounded-lg shadow-xs\"\n >\n <span>G\u1EEDi</span>\n <ol-icon name=\"arrow-right\" size=\"xs\" />\n </button>\n </div>\n </div>\n }\n @if (helperText() || showCharCount()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (showCharCount()) {\n <span class=\"text-body-subtle ms-auto\">\n {{ value().length }}{{ maxLength() ? '/' + maxLength() : '' }} k\u00FD t\u1EF1\n </span>\n }\n </div>\n }\n</div>\n" }]
7773
8271
  }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], showCharCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCharCount", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], commentBox: [{ type: i0.Input, args: [{ isSignal: true, alias: "commentBox", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], submitComment: [{ type: i0.Output, args: ["submitComment"] }] } });
7774
8272
 
7775
8273
  class OlSpinner {
@@ -8417,7 +8915,7 @@ class OlSelect {
8417
8915
  const el = this.panel()?.nativeElement;
8418
8916
  const raBody = this.appendTo() === 'body';
8419
8917
  if (!open || !el) {
8420
- this.donDepPanel();
8918
+ this.detachPanelNow();
8421
8919
  return;
8422
8920
  }
8423
8921
  /**
@@ -8451,11 +8949,11 @@ class OlSelect {
8451
8949
  this.destroyRef.onDestroy(() => {
8452
8950
  if (this.hengio)
8453
8951
  clearTimeout(this.hengio);
8454
- this.donDepPanel();
8952
+ this.detachPanelNow();
8455
8953
  });
8456
8954
  }
8457
8955
  /** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
8458
- donDepPanel() {
8956
+ detachPanelNow() {
8459
8957
  if (this.stopTracking) {
8460
8958
  this.stopTracking();
8461
8959
  this.stopTracking = null;
@@ -8864,13 +9362,13 @@ class OlSelect {
8864
9362
  this.searchQuery.set(val);
8865
9363
  this.activeIndex.set(0);
8866
9364
  this.scrollTop.set(0);
8867
- this.phatSearch(val);
9365
+ this.emitSearch(val);
8868
9366
  }
8869
9367
  /**
8870
9368
  * Phát `search` có GỠ NẢY. Gõ "nguyễn văn a" là 12 phím — không gỡ nảy thì
8871
9369
  * 12 lượt gọi API, mà chỉ lượt cuối có ích.
8872
9370
  */
8873
- phatSearch(val) {
9371
+ emitSearch(val) {
8874
9372
  if (this.hengio) {
8875
9373
  clearTimeout(this.hengio);
8876
9374
  this.hengio = null;
@@ -9104,7 +9602,7 @@ class OlSelect {
9104
9602
  useExisting: forwardRef(() => OlSelect),
9105
9603
  multi: true,
9106
9604
  },
9107
- ], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }, { propertyName: "labelTemplate", first: true, predicate: ["labelTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "listboxRef", first: true, predicate: ["listboxRef"], descendants: true, isSignal: true }, { propertyName: "oTimPanel", first: true, predicate: ["oTimPanel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9605
+ ], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }, { propertyName: "labelTemplate", first: true, predicate: ["labelTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "listboxRef", first: true, predicate: ["listboxRef"], descendants: true, isSignal: true }, { propertyName: "oTimPanel", first: true, predicate: ["oTimPanel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-heading]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9108
9606
  }
9109
9607
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSelect, decorators: [{
9110
9608
  type: Component,
@@ -9118,7 +9616,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
9118
9616
  useExisting: forwardRef(() => OlSelect),
9119
9617
  multi: true,
9120
9618
  },
9121
- ], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n" }]
9619
+ ], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-heading]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n" }]
9122
9620
  }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], labelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKey", required: false }] }], valueKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueKey", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], menuClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuClass", required: false }] }], matchTriggerWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "matchTriggerWidth", required: false }] }], maxSelectedItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelectedItems", required: false }] }], closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnSelect", required: false }] }], hideSelected: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideSelected", required: false }] }], allowCreate: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCreate", required: false }] }], createText: [{ type: i0.Input, args: [{ isSignal: true, alias: "createText", required: false }] }], groupSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupSeparator", required: false }] }], searchPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPosition", required: false }] }], searchIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchIcon", required: false }] }], searchPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPrefix", required: false }] }], triggerIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerIcon", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], remoteSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteSearch", required: false }] }], minTermLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minTermLength", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], typeToSearchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeToSearchText", required: false }] }], virtualScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScroll", required: false }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: false }] }], bufferAmount: [{ type: i0.Input, args: [{ isSignal: true, alias: "bufferAmount", required: false }] }], search: [{ type: i0.Output, args: ["search"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], added: [{ type: i0.Output, args: ["added"] }], removed: [{ type: i0.Output, args: ["removed"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], optionTemplate: [{
9123
9621
  type: ContentChild,
9124
9622
  args: ['optionTemplate']
@@ -10618,6 +11116,24 @@ class OlTimepicker {
10618
11116
  ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
10619
11117
  size = input('base', /* @ts-ignore */
10620
11118
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
11119
+ /**
11120
+ * Đệm ngang và cỡ chữ theo BẢNG CHUẨN họ form.
11121
+ *
11122
+ * Trước đây template ghim cứng `px-3.5` (14px cho mọi cỡ) và không đặt cỡ
11123
+ * chữ nào, nên nó thừa kế 16px mặc định. Đo trên /datepicker mục 8 ngày
11124
+ * 2026-09-24, đặt cạnh ba ô kia:
11125
+ *
11126
+ * sm/base timepicker chữ 16px · ba ô kia 14px
11127
+ * mọi cỡ timepicker đệm 14px · input 10/12/14/16px theo cỡ
11128
+ *
11129
+ * Gom vào MỘT chuỗi thay vì nhiều `[class.px-*]`: Angular cắt tên class ở
11130
+ * dấu chấm nên `[class.px-2.5]` render ra `px-2`, lệch 2px so với thiết kế.
11131
+ */
11132
+ fieldTextClasses = computed(() => {
11133
+ const size = this.size();
11134
+ return `${FIELD_PADDING_X[size]} ${FIELD_TEXT[size]}`;
11135
+ }, /* @ts-ignore */
11136
+ ...(ngDevMode ? [{ debugName: "fieldTextClasses" }] : /* istanbul ignore next */ []));
10621
11137
  state = input('default', /* @ts-ignore */
10622
11138
  ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
10623
11139
  /**
@@ -10778,7 +11294,7 @@ class OlTimepicker {
10778
11294
  const el = this.panel()?.nativeElement;
10779
11295
  const raBody = this.appendTo() === 'body';
10780
11296
  if (!open || !el) {
10781
- this.donDepPanel();
11297
+ this.detachPanelNow();
10782
11298
  return;
10783
11299
  }
10784
11300
  // `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
@@ -10801,7 +11317,7 @@ class OlTimepicker {
10801
11317
  });
10802
11318
  }
10803
11319
  /** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
10804
- donDepPanel() {
11320
+ detachPanelNow() {
10805
11321
  if (this.stopTracking) {
10806
11322
  this.stopTracking();
10807
11323
  this.stopTracking = null;
@@ -10966,7 +11482,7 @@ class OlTimepicker {
10966
11482
  useExisting: forwardRef(() => OlTimepicker),
10967
11483
  multi: true,
10968
11484
  },
10969
- ], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "hourAreaRef", first: true, predicate: ["hourArea"], descendants: true, isSignal: true }, { propertyName: "minuteAreaRef", first: true, predicate: ["minuteArea"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none px-3.5 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlScrollArea, selector: "ol-scroll-area", inputs: ["scrollbar", "axis", "smooth", "innerClass"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11485
+ ], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "hourAreaRef", first: true, predicate: ["hourArea"], descendants: true, isSignal: true }, { propertyName: "minuteAreaRef", first: true, predicate: ["minuteArea"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [ngClass]=\"fieldTextClasses()\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlScrollArea, selector: "ol-scroll-area", inputs: ["scrollbar", "axis", "smooth", "innerClass"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10970
11486
  }
10971
11487
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTimepicker, decorators: [{
10972
11488
  type: Component,
@@ -10982,7 +11498,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
10982
11498
  useExisting: forwardRef(() => OlTimepicker),
10983
11499
  multi: true,
10984
11500
  },
10985
- ], template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none px-3.5 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
11501
+ ], template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [ngClass]=\"fieldTextClasses()\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
10986
11502
  }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], errorText: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showPresets: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPresets", required: false }] }], addonType: [{ type: i0.Input, args: [{ isSignal: true, alias: "addonType", required: false }] }], addonText: [{ type: i0.Input, args: [{ isSignal: true, alias: "addonText", required: false }] }], addonIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addonIcon", required: false }] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], hourAreaRef: [{ type: i0.ViewChild, args: ['hourArea', { isSignal: true }] }], minuteAreaRef: [{ type: i0.ViewChild, args: ['minuteArea', { isSignal: true }] }], timeChange: [{ type: i0.Output, args: ["timeChange"] }], timestampChange: [{ type: i0.Output, args: ["timestampChange"] }], isoChange: [{ type: i0.Output, args: ["isoChange"] }], timeObjectChange: [{ type: i0.Output, args: ["timeObjectChange"] }], onDocumentClick: [{
10987
11503
  type: HostListener,
10988
11504
  args: ['document:click', ['$event']]
@@ -11019,8 +11535,32 @@ class OlAutocomplete {
11019
11535
  size = input('base', /* @ts-ignore */
11020
11536
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
11021
11537
  /** Cỡ đã quy bí danh: `'md'` -> `'base'`. Template so với cái này. */
11022
- resolvedSize = computed(() => (this.size() === 'md' ? 'base' : this.size()), /* @ts-ignore */
11538
+ resolvedSize = computed(() => {
11539
+ const size = this.size();
11540
+ // `md` là BÍ DANH của `base` — gập ở đây để kiểu trả về đúng `FieldSize`,
11541
+ // nhờ vậy tra bảng chuẩn không cần ép kiểu.
11542
+ return size === 'md' ? 'base' : size;
11543
+ }, /* @ts-ignore */
11023
11544
  ...(ngDevMode ? [{ debugName: "resolvedSize" }] : /* istanbul ignore next */ []));
11545
+ /**
11546
+ * Đệm ngang theo BẢNG CHUẨN họ form. Chỉ dùng khi KHÔNG `multiple` —
11547
+ * chế độ nhiều thẻ có đệm riêng (`px-1.5 py-1`) vì các thẻ tự mang đệm.
11548
+ *
11549
+ * Ba lỗi cũ gộp làm một chỗ này (đo /autocomplete 2026-09-24):
11550
+ * sm `[class.px-2.5]` -> Angular CẮT ở dấu chấm, render `px-2` = 8px
11551
+ * thay vì 10px. Đây là lý do phải gom vào chuỗi, không bind lẻ.
11552
+ * base dùng chung `px-3.5` với `lg` = 14px, bảng chuẩn quy định 12px.
11553
+ * xl có `px-4` nhưng nằm ở `[class.px-4]` rời, dễ lệch khỏi ba cỡ kia.
11554
+ */
11555
+ fieldPaddingClass = computed(() => FIELD_PADDING_X[this.resolvedSize()], /* @ts-ignore */
11556
+ ...(ngDevMode ? [{ debugName: "fieldPaddingClass" }] : /* istanbul ignore next */ []));
11557
+ /**
11558
+ * Cỡ chữ theo BẢNG CHUẨN. Trước đây `[class.text-base]` chỉ khai cho `lg`,
11559
+ * `xl` không có nhánh nào nên rơi về 16px MẶC ĐỊNH của trình duyệt — trùng
11560
+ * giá trị đúng một cách tình cờ, hỏng ngay khi cỡ gốc đổi.
11561
+ */
11562
+ fieldTextClass = computed(() => FIELD_TEXT[this.resolvedSize()], /* @ts-ignore */
11563
+ ...(ngDevMode ? [{ debugName: "fieldTextClass" }] : /* istanbul ignore next */ []));
11024
11564
  state = input('default', /* @ts-ignore */
11025
11565
  ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
11026
11566
  required = input(false, /* @ts-ignore */
@@ -11188,7 +11728,7 @@ class OlAutocomplete {
11188
11728
  const el = this.panel()?.nativeElement;
11189
11729
  const raBody = this.appendTo() === 'body';
11190
11730
  if (!open || !el) {
11191
- this.donDepPanel();
11731
+ this.detachPanelNow();
11192
11732
  return;
11193
11733
  }
11194
11734
  /**
@@ -11222,11 +11762,11 @@ class OlAutocomplete {
11222
11762
  this.destroyRef.onDestroy(() => {
11223
11763
  if (this.hengio)
11224
11764
  clearTimeout(this.hengio);
11225
- this.donDepPanel();
11765
+ this.detachPanelNow();
11226
11766
  });
11227
11767
  }
11228
11768
  /** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
11229
- donDepPanel() {
11769
+ detachPanelNow() {
11230
11770
  if (this.stopTracking) {
11231
11771
  this.stopTracking();
11232
11772
  this.stopTracking = null;
@@ -11394,7 +11934,7 @@ class OlAutocomplete {
11394
11934
  this.searchQuery.set(val);
11395
11935
  this.isOpen.set(true);
11396
11936
  this.activeIndex.set(0);
11397
- this.phatSearch(val);
11937
+ this.emitSearch(val);
11398
11938
  }
11399
11939
  /**
11400
11940
  * Phát `search` có GỠ NẢY. Gõ "nguyễn văn a" là 12 phím — không gỡ nảy thì
@@ -11406,7 +11946,7 @@ class OlAutocomplete {
11406
11946
  *
11407
11947
  * Dưới `minTermLength` thì KHÔNG phát: nơi gọi không phải tự kiểm lại.
11408
11948
  */
11409
- phatSearch(val) {
11949
+ emitSearch(val) {
11410
11950
  if (this.hengio) {
11411
11951
  clearTimeout(this.hengio);
11412
11952
  this.hengio = null;
@@ -11542,7 +12082,7 @@ class OlAutocomplete {
11542
12082
  useExisting: forwardRef(() => OlAutocomplete),
11543
12083
  multi: true,
11544
12084
  },
11545
- ], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.px-2.5]=\"!multiple() && resolvedSize() === 'sm'\"\n [class.px-4]=\"!multiple() && resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"{\n 'px-1.5 py-1': multiple(),\n 'px-3.5': !multiple() && (resolvedSize() === 'base' || resolvedSize() === 'lg'),\n }\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg'\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
12085
+ ], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"multiple() ? 'px-1.5 py-1' : fieldPaddingClass()\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [ngClass]=\"fieldTextClass()\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11546
12086
  }
11547
12087
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAutocomplete, decorators: [{
11548
12088
  type: Component,
@@ -11558,7 +12098,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
11558
12098
  useExisting: forwardRef(() => OlAutocomplete),
11559
12099
  multi: true,
11560
12100
  },
11561
- ], template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.px-2.5]=\"!multiple() && resolvedSize() === 'sm'\"\n [class.px-4]=\"!multiple() && resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"{\n 'px-1.5 py-1': multiple(),\n 'px-3.5': !multiple() && (resolvedSize() === 'base' || resolvedSize() === 'lg'),\n }\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg'\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
12101
+ ], template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"multiple() ? 'px-1.5 py-1' : fieldPaddingClass()\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [ngClass]=\"fieldTextClass()\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
11562
12102
  }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], labelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKey", required: false }] }], valueKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueKey", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], errorText: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], allowCreate: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCreate", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], menuClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuClass", required: false }] }], matchTriggerWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "matchTriggerWidth", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], remoteSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteSearch", required: false }] }], minTermLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minTermLength", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], typeToSearchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeToSearchText", required: false }] }], createOption: [{ type: i0.Output, args: ["createOption"] }], search: [{ type: i0.Output, args: ["search"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], added: [{ type: i0.Output, args: ["added"] }], removed: [{ type: i0.Output, args: ["removed"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], optionTemplate: [{
11563
12103
  type: ContentChild,
11564
12104
  args: ['optionTemplate']
@@ -11645,186 +12185,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
11645
12185
  args: [{ selector: 'ol-list-group-item', standalone: true, imports: [NgTemplateOutlet], host: { class: 'block w-full' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<li\n (click)=\"handleClick($event)\"\n class=\"w-full transition-colors\"\n [class]=\"textClasses()\"\n [class.border-b]=\"hasDivider()\"\n [class.border-default-strong]=\"hasDivider()\"\n>\n <!--\n `href` bi\u1EBFn m\u1EE5c th\u00E0nh li\u00EAn k\u1EBFt. Th\u1EBB `<a>` n\u1EB1m B\u00CAN TRONG `<li>`, kh\u00F4ng thay\n th\u1EBF n\u00F3 \u2014 `<a>` tr\u1EF1c ti\u1EBFp trong `<ul>` l\u00E0 HTML h\u1ECFng. \u0110\u1EC7m chuy\u1EC3n xu\u1ED1ng th\u1EBB\n trong c\u00F9ng \u0111\u1EC3 v\u00F9ng b\u1EA5m ph\u1EE7 tr\u1ECDn m\u1EE5c.\n -->\n @if (href() && !disabled()) {\n <a [href]=\"href()\" class=\"block w-full px-4 py-2\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n } @else {\n <span class=\"block w-full px-4 py-2\" [attr.aria-disabled]=\"disabled() ? 'true' : null\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n }\n</li>\n\n<ng-template #ruot><ng-content /></ng-template>\n" }]
11646
12186
  }], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
11647
12187
 
11648
- /**
11649
- * Tiêu đề Modal — khớp Figma `ol-modal-header` (node 1149:71403).
11650
- * Figma có 7 biến thể: Default, Heading & supporting text, Icon + Heading &
11651
- * helper text, With Logo, Logo & icons, Avatar group, Breadcrumb.
11652
- * Component lo phần khung dùng chung; nội dung giàu hơn lắp qua <ng-content>.
11653
- */
11654
- /*
11655
- * GHÉP VỚI `ol-modal`: phần đầu của `ol-modal` nằm trong `@if (title() ||
11656
- * dismissible())`, nên muốn dùng component này thì phải TẮT nó đi —
11657
- * `title=""` kèm `[dismissible]="false"` — rồi lắp vào `<ng-content />` trần
11658
- * của phần thân. Để nguyên `title` là lồng hai khung `p-5 border-b`.
11659
- */
11660
- class OlModalHeader {
11661
- /**
11662
- * Bậc thẻ tiêu đề (h1–h6). Mặc định 3 — giữ nguyên bậc đang chạy.
11663
- *
11664
- * Khoá cứng một bậc thì nơi gọi không sửa được: đặt component này sau
11665
- * `<h2>` của trang là nhảy bậc.
11666
- */
11667
- headingLevel = input(3, /* @ts-ignore */
11668
- ...(ngDevMode ? [{ debugName: "headingLevel" }] : /* istanbul ignore next */ []));
11669
- heading = input('', /* @ts-ignore */
11670
- ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
11671
- /**
11672
- * Icon đầu tiêu đề. Rỗng thì KHÔNG dựng khung — không để lại thẻ trống.
11673
- *
11674
- * Figma `Type=Icon + Heading & helper text` (#1149:71412) vẽ khung 48px bo
11675
- * 12px, icon 28px bên trong. Đó đúng là `ol-icon-shape` size `lg`, nên dùng
11676
- * lại thay vì tự vẽ khung.
11677
- *
11678
- * `color="white"` chứ KHÔNG `gray`: Figma bind nền
11679
- * `bg-neutral-primary-medium` (trắng) kèm viền `border-default-medium`, mà
11680
- * trong `ol-icon-shape` chỉ nhánh `white` có cặp đó — `gray` ra nền xám đặc
11681
- * và KHÔNG viền. Đo /modal lượt đầu: nền rgb(243,244,246), viền 0px.
11682
- */
11683
- icon = input('', /* @ts-ignore */
11684
- ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
11685
- /**
11686
- * Mô tả phụ dưới tiêu đề.
11687
- *
11688
- * Tên `description` theo `ol-alert-dialog` — thứ dựng trên chính `ol-modal`
11689
- * — và `ol-empty`. `helperText` mang nghĩa "chú thích dưới ô nhập" ở họ form
11690
- * (16 component dùng), đặt cho tiêu đề hộp thoại là lệch nghĩa.
11691
- */
11692
- description = input('', /* @ts-ignore */
11693
- ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
11694
- /** @deprecated Dùng `description`. Giữ để không phá mã đang chạy. */
11695
- supportingText = input('', /* @ts-ignore */
11696
- ...(ngDevMode ? [{ debugName: "supportingText" }] : /* istanbul ignore next */ []));
11697
- /** @deprecated Dùng `description`. Tên cũ của `ol-drawer-header`. */
11698
- helperText = input('', /* @ts-ignore */
11699
- ...(ngDevMode ? [{ debugName: "helperText" }] : /* istanbul ignore next */ []));
11700
- /**
11701
- * Cái nào được TRUYỀN thì thắng — không phụ thuộc giá trị.
11702
- *
11703
- * `ol-badge` từng hoà giải hai input cùng nghĩa bằng cách so với giá trị mặc
11704
- * định (`theme` thắng trừ khi nó bằng `'gray'`), nên truyền cả hai thì một
11705
- * cái bị nuốt im lặng. Ở đây chuỗi rỗng là "không truyền", nên chỉ cần lấy
11706
- * cái đầu tiên khác rỗng.
11707
- */
11708
- descriptionText = computed(() => this.description() || this.supportingText() || this.helperText(), /* @ts-ignore */
11709
- ...(ngDevMode ? [{ debugName: "descriptionText" }] : /* istanbul ignore next */ []));
11710
- showClose = input(true, /* @ts-ignore */
11711
- ...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
11712
- closeClick = output();
11713
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
11714
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModalHeader, isStandalone: true, selector: "ol-modal-header", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, supportingText: { classPropertyName: "supportingText", publicName: "supportingText", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, showClose: { classPropertyName: "showClose", publicName: "showClose", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeClick: "closeClick" }, host: { classAttribute: "block" }, ngImport: i0, template: `
11715
- <!--
11716
- PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
11717
- kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
11718
- và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
11719
- CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
11720
- -->
11721
- <div class="flex items-start justify-between gap-3 p-3">
11722
- @if (icon()) {
11723
- <ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
11724
- }
11725
- <div class="min-w-0 flex-1">
11726
- @if (heading()) {
11727
- @switch (headingLevel()) {
11728
- @case (1) {
11729
- <h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
11730
- }
11731
- @case (2) {
11732
- <h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
11733
- }
11734
- @case (4) {
11735
- <h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
11736
- }
11737
- @case (5) {
11738
- <h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
11739
- }
11740
- @case (6) {
11741
- <h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
11742
- }
11743
- @default {
11744
- <h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
11745
- }
11746
- }
11747
- }
11748
- @if (descriptionText()) {
11749
- <p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
11750
- }
11751
- <ng-content />
11752
- </div>
11753
- @if (showClose()) {
11754
- <button
11755
- type="button"
11756
- (click)="closeClick.emit()"
11757
- aria-label="Đóng hộp thoại"
11758
- class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
11759
- >
11760
- <ol-icon name="close" size="sm" />
11761
- </button>
11762
- }
11763
- </div>
11764
- `, isInline: true, dependencies: [{ kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11765
- }
11766
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, decorators: [{
11767
- type: Component,
11768
- args: [{
11769
- selector: 'ol-modal-header',
11770
- standalone: true,
11771
- imports: [OlIcon, OlIconShape],
11772
- template: `
11773
- <!--
11774
- PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
11775
- kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
11776
- và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
11777
- CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
11778
- -->
11779
- <div class="flex items-start justify-between gap-3 p-3">
11780
- @if (icon()) {
11781
- <ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
11782
- }
11783
- <div class="min-w-0 flex-1">
11784
- @if (heading()) {
11785
- @switch (headingLevel()) {
11786
- @case (1) {
11787
- <h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
11788
- }
11789
- @case (2) {
11790
- <h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
11791
- }
11792
- @case (4) {
11793
- <h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
11794
- }
11795
- @case (5) {
11796
- <h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
11797
- }
11798
- @case (6) {
11799
- <h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
11800
- }
11801
- @default {
11802
- <h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
11803
- }
11804
- }
11805
- }
11806
- @if (descriptionText()) {
11807
- <p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
11808
- }
11809
- <ng-content />
11810
- </div>
11811
- @if (showClose()) {
11812
- <button
11813
- type="button"
11814
- (click)="closeClick.emit()"
11815
- aria-label="Đóng hộp thoại"
11816
- class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
11817
- >
11818
- <ol-icon name="close" size="sm" />
11819
- </button>
11820
- }
11821
- </div>
11822
- `,
11823
- changeDetection: ChangeDetectionStrategy.OnPush,
11824
- host: { class: 'block' },
11825
- }]
11826
- }], propDecorators: { headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], supportingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "supportingText", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], showClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClose", required: false }] }], closeClick: [{ type: i0.Output, args: ["closeClick"] }] } });
11827
-
11828
12188
  /**
11829
12189
  * Dải nút hành động cuối Modal — khớp Figma `ol-modal-footer`.
11830
12190
  * Figma có 4 biến thể: Buttons, 3 buttons, Buttons & links, Pagination.
@@ -11988,15 +12348,36 @@ class OlNavbarItem {
11988
12348
  'inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-150 whitespace-nowrap shrink-0',
11989
12349
  /**
11990
12350
  * Hover CHỈ cho mục KHÔNG active — cùng lý do với `ol-sidebar-item`:
11991
- * `:hover` tính như một class nên hover (0,2,0) thắng `bg-brand` (0,1,0),
11992
- * di chuột vào mục đang mở là mất nền đặc. Xem chú thích đầy đủ ở đó.
12351
+ * `:hover` tính như một class nên hover (0,2,0) thắng nền active
12352
+ * (0,1,0), di chuột vào mục đang mở là mất trạng thái. Xem chú thích
12353
+ * đầy đủ ở đó.
12354
+ */
12355
+ 'text-body not-[.active]:hover:text-fg-brand not-[.active]:hover:bg-brand-softer',
12356
+ /**
12357
+ * Trạng thái active — BÁM THEO `ol-sidebar-item`, xem ba đời màu ở đó.
12358
+ *
12359
+ * Đồng bộ 2026-09-24. Trước đó navbar kẹt lại ở ĐỜI 2 (`bg-brand` đặc +
12360
+ * chữ trắng + gạch mép trái 4px): commit 89b2e49 ngày 21/09 đổi cả hai
12361
+ * component sang đời 2 cùng lúc, nhưng khi sidebar chuyển sang đời 3
12362
+ * (c978f10 + 224d048 ngày 23/09 — nền nhạt `bg-brand-soft`, gỡ gạch
12363
+ * mép) thì navbar không được đổi theo. Chú thích ở đây vẫn ghi "cùng
12364
+ * luật với ol-sidebar-item" trong khi hai bên đã khác hẳn.
12365
+ *
12366
+ * Gỡ luôn gạch mép trái, cùng lý do sidebar đã gỡ: nền đã đánh dấu đủ
12367
+ * rõ, gạch là dấu hiệu thứ hai cho cùng một trạng thái, và nó ăn 4px
12368
+ * đệm trái làm chữ mục active lệch so với các mục còn lại — ở navbar
12369
+ * xếp NGANG thì lệch này thấy rõ hơn sidebar xếp dọc.
12370
+ *
12371
+ * VẪN LỆCH FIGMA CÓ CHỦ Ý. Figma `ol-navbar-item` #6375:261 cho
12372
+ * `State=Active` chỉ đổi màu chữ (`text-body` -> `text-fg-brand`),
12373
+ * KHÔNG nền, KHÔNG viền — đọc biến bind ngày 2026-09-24. Đó chính là
12374
+ * phương án đã bị bác hai lần vì nổi bật chỉ 1.06 lần so với nền trang.
12375
+ * Nợ: cập nhật Figma cho khớp đời 3, cho cả sidebar lẫn navbar.
11993
12376
  */
11994
- 'text-body not-[.active]:hover:text-fg-brand not-[.active]:hover:bg-neutral-secondary-medium',
11995
- /** Cùng luật active với `ol-sidebar-item` — xem chú thích đầy đủ ở đó. */
11996
- '[&.active]:text-white [&.active]:bg-brand [&.active]:font-semibold [&.active]:border-l-4 [&.active]:border-white',
11997
- this.active() ? 'active text-white bg-brand font-semibold border-l-4 border-white' : '',
11998
- /** Hover của mục active: ĐẬM thêm một bậc, không nhạt đi. */
11999
- '[&.active]:hover:bg-brand-strong',
12377
+ '[&.active]:text-fg-brand [&.active]:bg-brand-soft [&.active]:font-semibold',
12378
+ this.active() ? 'active text-fg-brand bg-brand-soft font-semibold' : '',
12379
+ /** Hover của mục ĐANG active: ĐẬM thêm một bậc, không nhạt đi. */
12380
+ '[&.active]:hover:bg-brand-medium',
12000
12381
  this.disabled() ? 'opacity-50 pointer-events-none' : 'cursor-pointer',
12001
12382
  ]
12002
12383
  .filter(Boolean)
@@ -12004,11 +12385,11 @@ class OlNavbarItem {
12004
12385
  }, /* @ts-ignore */
12005
12386
  ...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
12006
12387
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlNavbarItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
12007
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlNavbarItem, isStandalone: true, selector: "ol-navbar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<a\n [routerLink]=\"routerLink() || undefined\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [href]=\"href() || undefined\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span class=\"whitespace-nowrap\">{{ label() }}</span>\n }\n <ng-content />\n</a>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
12388
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlNavbarItem, isStandalone: true, selector: "ol-navbar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n BA NH\u00C1NH, `routerLinkActive` CH\u1EC8 \u1EDF nh\u00E1nh c\u00F3 `routerLink`.\n\n B\u1EA3n c\u0169 d\u00F9ng M\u1ED8T `<a>` mang c\u1EA3 `[routerLink]`, `[routerLinkActive]` v\u00E0\n `[class]=\"itemClasses()\"`. Hai th\u1EE9 sau c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh `class`, v\u00E0\n `RouterLinkActive` ghi SAU n\u00EAn n\u00F3 th\u1EAFng: chu\u1ED7i `'active \u2026'` m\u00E0 `itemClasses()`\n th\u00EAm khi `[active]=\"true\"` b\u1ECB nu\u1ED1t s\u1EA1ch.\n\n T\u00E1ch nh\u00E1nh th\u00F4i KH\u00D4NG \u0111\u1EE7 \u2014 nh\u00E1nh `routerLink` v\u1EABn c\u00F3 c\u1EA3 hai directive tr\u00EAn\n c\u00F9ng th\u1EBB. N\u00EAn `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET: \u0111\u00E3 \u00E9p active\n b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh, hai b\u00EAn h\u1EBFt tranh nhau.\n\n H\u1ECFng im l\u1EB7ng \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (ch\u00FAng n\u1EB1m trong c\u00F9ng chu\u1ED7i), ch\u1EC9 ri\u00EAng\n ch\u1EEF `active` m\u1EA5t. H\u1EADu qu\u1EA3 ch\u1EC9 l\u1ED9 ra khi r\u00EA chu\u1ED9t: \u0111o /navbar 2026-09-24, m\u1EE5c\n \u0111ang m\u1EDF NH\u1EA0T \u0110I (#e3eaff -> #f5f8ff) v\u00EC `not-[.active]:hover:bg-brand-softer`\n v\u1EABn \u00E1p d\u1EE5ng, c\u00F2n `[&.active]:hover:bg-brand-medium` th\u00EC kh\u00F4ng. Sidebar c\u00F9ng\n b\u1ED9 class nh\u01B0ng \u0110\u1EACM L\u00CAN \u0111\u00FAng (#d2dcfa) v\u00EC n\u00F3 \u0111\u00E3 t\u00E1ch nh\u00E1nh t\u1EEB tr\u01B0\u1EDBc.\n\n Unit test KH\u00D4NG b\u1EAFt \u0111\u01B0\u1EE3c: `provideRouter([])` kh\u00F4ng c\u00F3 route n\u00E0o kh\u1EDBp n\u00EAn\n directive kh\u00F4ng ch\u1EA1m v\u00E0o `class`, test xanh trong khi tr\u00ECnh duy\u1EC7t h\u1ECFng.\n\n N\u1ED9i dung g\u00F3i trong `ng-template` v\u00E0 chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`: khai\n nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh lo\u1EA1i tr\u1EEB nhau \u2014\n th\u00EC Angular ch\u1EC9 chi\u1EBFu v\u00E0o M\u1ED8T ch\u1ED7 v\u00E0 th\u1EF1c t\u1EBF m\u1EA5t \u1EDF m\u1ECDi nh\u00E1nh.\n-->\n<ng-template #itemContent>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span class=\"whitespace-nowrap\">{{ label() }}</span>\n }\n <ng-content />\n</ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else {\n <a [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
12008
12389
  }
12009
12390
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlNavbarItem, decorators: [{
12010
12391
  type: Component,
12011
- args: [{ selector: 'ol-navbar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<a\n [routerLink]=\"routerLink() || undefined\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [href]=\"href() || undefined\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span class=\"whitespace-nowrap\">{{ label() }}</span>\n }\n <ng-content />\n</a>\n" }]
12392
+ args: [{ selector: 'ol-navbar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n BA NH\u00C1NH, `routerLinkActive` CH\u1EC8 \u1EDF nh\u00E1nh c\u00F3 `routerLink`.\n\n B\u1EA3n c\u0169 d\u00F9ng M\u1ED8T `<a>` mang c\u1EA3 `[routerLink]`, `[routerLinkActive]` v\u00E0\n `[class]=\"itemClasses()\"`. Hai th\u1EE9 sau c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh `class`, v\u00E0\n `RouterLinkActive` ghi SAU n\u00EAn n\u00F3 th\u1EAFng: chu\u1ED7i `'active \u2026'` m\u00E0 `itemClasses()`\n th\u00EAm khi `[active]=\"true\"` b\u1ECB nu\u1ED1t s\u1EA1ch.\n\n T\u00E1ch nh\u00E1nh th\u00F4i KH\u00D4NG \u0111\u1EE7 \u2014 nh\u00E1nh `routerLink` v\u1EABn c\u00F3 c\u1EA3 hai directive tr\u00EAn\n c\u00F9ng th\u1EBB. N\u00EAn `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET: \u0111\u00E3 \u00E9p active\n b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh, hai b\u00EAn h\u1EBFt tranh nhau.\n\n H\u1ECFng im l\u1EB7ng \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (ch\u00FAng n\u1EB1m trong c\u00F9ng chu\u1ED7i), ch\u1EC9 ri\u00EAng\n ch\u1EEF `active` m\u1EA5t. H\u1EADu qu\u1EA3 ch\u1EC9 l\u1ED9 ra khi r\u00EA chu\u1ED9t: \u0111o /navbar 2026-09-24, m\u1EE5c\n \u0111ang m\u1EDF NH\u1EA0T \u0110I (#e3eaff -> #f5f8ff) v\u00EC `not-[.active]:hover:bg-brand-softer`\n v\u1EABn \u00E1p d\u1EE5ng, c\u00F2n `[&.active]:hover:bg-brand-medium` th\u00EC kh\u00F4ng. Sidebar c\u00F9ng\n b\u1ED9 class nh\u01B0ng \u0110\u1EACM L\u00CAN \u0111\u00FAng (#d2dcfa) v\u00EC n\u00F3 \u0111\u00E3 t\u00E1ch nh\u00E1nh t\u1EEB tr\u01B0\u1EDBc.\n\n Unit test KH\u00D4NG b\u1EAFt \u0111\u01B0\u1EE3c: `provideRouter([])` kh\u00F4ng c\u00F3 route n\u00E0o kh\u1EDBp n\u00EAn\n directive kh\u00F4ng ch\u1EA1m v\u00E0o `class`, test xanh trong khi tr\u00ECnh duy\u1EC7t h\u1ECFng.\n\n N\u1ED9i dung g\u00F3i trong `ng-template` v\u00E0 chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`: khai\n nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh lo\u1EA1i tr\u1EEB nhau \u2014\n th\u00EC Angular ch\u1EC9 chi\u1EBFu v\u00E0o M\u1ED8T ch\u1ED7 v\u00E0 th\u1EF1c t\u1EBF m\u1EA5t \u1EDF m\u1ECDi nh\u00E1nh.\n-->\n<ng-template #itemContent>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span class=\"whitespace-nowrap\">{{ label() }}</span>\n }\n <ng-content />\n</ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else {\n <a [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n}\n" }]
12012
12393
  }], propDecorators: { routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], routerLinkActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActive", required: false }] }], routerLinkActiveOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActiveOptions", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
12013
12394
 
12014
12395
  class OlNavDropdown {
@@ -13180,7 +13561,7 @@ class OlAppNav {
13180
13561
  this.collapsed.update((v) => !v);
13181
13562
  }
13182
13563
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, deps: [], target: i0.ɵɵFactoryTarget.Component });
13183
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAppNav, isStandalone: true, selector: "ol-app-nav", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, logoSrc: { classPropertyName: "logoSrc", publicName: "logoSrc", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, showRail: { classPropertyName: "showRail", publicName: "showRail", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", itemClick: "itemClick", userClick: "userClick" }, host: { classAttribute: "block" }, ngImport: i0, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n class=\"relative flex h-full flex-col justify-between rounded-2xl pt-6 transition-[width] duration-200\"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 13px khi thu g\u1ECDn, KH\u00D4NG ph\u1EA3i 12px: `<ul>` l\u00E0 flex-col n\u00EAn `<li>`\n k\u00E9o gi\u00E3n m\u1EE5c con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-11` kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 13\u00D72 = 44,\n \u0111\u00FAng \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c. \u0110\u1EC7m 12px cho ra 46\u00D744, l\u1EC7ch m\u1ED9t chi\u1EC1u.\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex flex-col gap-7 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoSrc()) {\n <img [src]=\"logoSrc()\" alt=\"\u00D4n Luy\u1EC7n\" class=\"h-10 w-auto max-w-full object-contain\" />\n } @else if (collapsed()) {\n <!--\n Thu g\u1ECDn: ch\u1EC9 ch\u1EEF t\u1EAFt. Logo \u0111\u1EA7y \u0111\u1EE7 r\u1ED9ng 155px (Figma) n\u00EAn \u1EDF khung 70px\n n\u00F3 tr\u00E0n h\u1EB3n ra ngo\u00E0i \u2014 \u0111o \u0111\u01B0\u1EE3c tr\u00EAn tr\u00ECnh duy\u1EC7t, ch\u1EEF \"\u00D4nluy\u1EC7n\" b\u1ECB c\u1EAFt\n gi\u1EEFa ch\u1EEBng \u1EDF m\u00E9p tr\u00E1i.\n -->\n <span class=\"text-2xl font-bold text-warning\" aria-label=\"\u00D4n Luy\u1EC7n\">\u00D4L</span>\n } @else {\n <!-- D\u1EF1 ph\u00F2ng khi ch\u01B0a truy\u1EC1n \u1EA3nh: ch\u1EEF gi\u1EEF \u0111\u00FAng hai m\u00E0u c\u1EE7a logo -->\n <span class=\"whitespace-nowrap text-2xl font-bold text-white\">\n \u00D4n<span class=\"text-warning\">luy\u1EC7n</span>\n </span>\n }\n </div>\n\n <ul class=\"flex flex-col gap-2\">\n @for (item of mainItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex flex-col gap-[7px] overflow-hidden rounded-b-2xl pb-5\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ul class=\"flex flex-col gap-[7px]\">\n @for (item of footerItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n\n @if (config().user; as user) {\n <button\n type=\"button\"\n class=\"mt-[30px] flex cursor-pointer items-center rounded-md transition-colors hover:bg-[var(--bg-app-nav-hover,#5168ac)]\"\n [class]=\"collapsed() ? 'mx-auto justify-center p-1' : 'w-full gap-2 p-1 text-start'\"\n (click)=\"userClick.emit()\"\n >\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n\n <!--\n Kh\u1ED1i t\u00EAn v\u00E0 email ch\u1EC9 hi\u1EC7n khi m\u1EDF. D\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class:\n \u1EA9n b\u1EB1ng `hidden` th\u00EC tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc, v\u00E0 ch\u1EEF v\u1EABn chi\u1EBFm ch\u1ED7\n trong t\u00EDnh to\u00E1n b\u1ED1 c\u1EE5c \u1EDF b\u1EC1 r\u1ED9ng 70px.\n -->\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs\">{{ user.subtitle }}</span>\n }\n </span>\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" class=\"shrink-0 text-white\" />\n }\n </button>\n }\n </div>\n</nav>\n\n<!--\n M\u1ED9t m\u1EE5c menu \u2014 d\u00F9ng chung cho c\u1EA3 ph\u1EA7n tr\u00EAn l\u1EABn ph\u1EA7n d\u01B0\u1EDBi.\n\n Ba nh\u00E1nh v\u00EC ba lo\u1EA1i h\u00E0nh vi kh\u00E1c nhau, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 `route` \u2192 `<a routerLink>` \u0111\u1EC3 router lo \u0111i\u1EC1u h\u01B0\u1EDBng v\u00E0 tr\u1EA1ng th\u00E1i active\n kh\u00F4ng route \u2192 `<button>` ch\u1EC9 ph\u00E1t s\u1EF1 ki\u1EC7n\n disabled \u2192 v\u1EABn `<button>` nh\u01B0ng `pointer-events-none` (lo trong class)\n-->\n<ng-template #navItem let-item>\n @if (item.route) {\n <a\n [routerLink]=\"item.route\"\n routerLinkActive=\"ol-app-nav-item-active font-medium\"\n [class]=\"itemClasses(item)\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </button>\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c: icon 24px + nh\u00E3n + huy hi\u1EC7u. Nh\u00E3n \u1EA9n khi thu g\u1ECDn. -->\n<ng-template #itemBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n\n @if (!collapsed()) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "directive", type: OlTooltipDirective, selector: "[olTooltip]", inputs: ["olTooltip", "tooltipPosition", "tooltipTheme", "tooltipDelay", "tooltipDisabled", "tooltipArrow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13564
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAppNav, isStandalone: true, selector: "ol-app-nav", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, logoSrc: { classPropertyName: "logoSrc", publicName: "logoSrc", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, showRail: { classPropertyName: "showRail", publicName: "showRail", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", itemClick: "itemClick", userClick: "userClick" }, host: { classAttribute: "block" }, ngImport: i0, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n class=\"relative flex h-full flex-col justify-between rounded-2xl pt-6 transition-[width] duration-200\"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 13px khi thu g\u1ECDn, KH\u00D4NG ph\u1EA3i 12px: `<ul>` l\u00E0 flex-col n\u00EAn `<li>`\n k\u00E9o gi\u00E3n m\u1EE5c con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-11` kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 13\u00D72 = 44,\n \u0111\u00FAng \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c. \u0110\u1EC7m 12px cho ra 46\u00D744, l\u1EC7ch m\u1ED9t chi\u1EC1u.\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex flex-col gap-7 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoSrc()) {\n <img [src]=\"logoSrc()\" alt=\"\u00D4n Luy\u1EC7n\" class=\"h-10 w-auto max-w-full object-contain\" />\n } @else if (collapsed()) {\n <!--\n Thu g\u1ECDn: ch\u1EC9 ch\u1EEF t\u1EAFt. Logo \u0111\u1EA7y \u0111\u1EE7 r\u1ED9ng 155px (Figma) n\u00EAn \u1EDF khung 70px\n n\u00F3 tr\u00E0n h\u1EB3n ra ngo\u00E0i \u2014 \u0111o \u0111\u01B0\u1EE3c tr\u00EAn tr\u00ECnh duy\u1EC7t, ch\u1EEF \"\u00D4nluy\u1EC7n\" b\u1ECB c\u1EAFt\n gi\u1EEFa ch\u1EEBng \u1EDF m\u00E9p tr\u00E1i.\n -->\n <span class=\"text-2xl font-bold text-warning\" aria-label=\"\u00D4n Luy\u1EC7n\">\u00D4L</span>\n } @else {\n <!-- D\u1EF1 ph\u00F2ng khi ch\u01B0a truy\u1EC1n \u1EA3nh: ch\u1EEF gi\u1EEF \u0111\u00FAng hai m\u00E0u c\u1EE7a logo -->\n <span class=\"whitespace-nowrap text-2xl font-bold text-white\">\n \u00D4n<span class=\"text-warning\">luy\u1EC7n</span>\n </span>\n }\n </div>\n\n <ul class=\"flex flex-col gap-2\">\n @for (item of mainItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex flex-col gap-[7px] overflow-hidden rounded-b-2xl pb-5\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ul class=\"flex flex-col gap-[7px]\">\n @for (item of footerItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n\n @if (config().user; as user) {\n <button\n type=\"button\"\n class=\"mt-[30px] flex cursor-pointer items-center rounded-md transition-colors hover:bg-[var(--bg-app-nav-hover,#5168ac)]\"\n [class]=\"collapsed() ? 'mx-auto justify-center p-1' : 'w-full gap-2 p-1 text-start'\"\n (click)=\"userClick.emit()\"\n >\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n\n <!--\n Kh\u1ED1i t\u00EAn v\u00E0 email ch\u1EC9 hi\u1EC7n khi m\u1EDF. D\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class:\n \u1EA9n b\u1EB1ng `hidden` th\u00EC tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc, v\u00E0 ch\u1EEF v\u1EABn chi\u1EBFm ch\u1ED7\n trong t\u00EDnh to\u00E1n b\u1ED1 c\u1EE5c \u1EDF b\u1EC1 r\u1ED9ng 70px.\n -->\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs\">{{ user.subtitle }}</span>\n }\n </span>\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" class=\"shrink-0 text-white\" />\n }\n </button>\n }\n </div>\n</nav>\n\n<!--\n M\u1ED9t m\u1EE5c menu \u2014 d\u00F9ng chung cho c\u1EA3 ph\u1EA7n tr\u00EAn l\u1EABn ph\u1EA7n d\u01B0\u1EDBi.\n\n Ba nh\u00E1nh v\u00EC ba lo\u1EA1i h\u00E0nh vi kh\u00E1c nhau, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 `route` \u2192 `<a routerLink>` \u0111\u1EC3 router lo \u0111i\u1EC1u h\u01B0\u1EDBng v\u00E0 tr\u1EA1ng th\u00E1i active\n kh\u00F4ng route \u2192 `<button>` ch\u1EC9 ph\u00E1t s\u1EF1 ki\u1EC7n\n disabled \u2192 v\u1EABn `<button>` nh\u01B0ng `pointer-events-none` (lo trong class)\n-->\n<ng-template #navItem let-item>\n @if (item.route) {\n <a\n [routerLink]=\"item.route\"\n routerLinkActive=\"ol-app-nav-item-active font-medium\"\n [class]=\"itemClasses(item)\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </button>\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c: icon 24px + nh\u00E3n + huy hi\u1EC7u. Nh\u00E3n \u1EA9n khi thu g\u1ECDn. -->\n<ng-template #itemBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n\n @if (!collapsed()) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "directive", type: OlTooltipDirective, selector: "[olTooltip]", inputs: ["olTooltip", "tooltipPosition", "tooltipTheme", "tooltipDelay", "tooltipDisabled", "tooltipArrow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13184
13565
  }
13185
13566
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, decorators: [{
13186
13567
  type: Component,
@@ -13371,11 +13752,11 @@ class OlSidebarItem {
13371
13752
  }, /* @ts-ignore */
13372
13753
  ...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
13373
13754
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
13374
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarItem, isStandalone: true, selector: "ol-sidebar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13755
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarItem, isStandalone: true, selector: "ol-sidebar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!--\n `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET.\n\n `RouterLinkActive` v\u00E0 `[class]=\"itemClasses()\"` c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh\n `class`, directive ghi SAU n\u00EAn n\u00F3 th\u1EAFng: k\u1EBFt h\u1EE3p `routerLink` + `[active]`\n th\u00EC chu\u1ED7i `'active \u2026'` b\u1ECB nu\u1ED1t. H\u1ECFng IM L\u1EB6NG \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (c\u00F9ng\n chu\u1ED7i), ch\u1EC9 ri\u00EAng ch\u1EEF `active` m\u1EA5t, n\u00EAn `not-[.active]:hover:*` v\u1EABn \u00E1p d\u1EE5ng\n v\u00E0 m\u1EE5c \u0111ang m\u1EDF NH\u1EA0T \u0110I khi r\u00EA chu\u1ED9t thay v\u00EC \u0111\u1EADm l\u00EAn.\n\n \u0110\u00E3 \u00E9p active b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh \u2014 t\u1EAFt n\u00F3 \u0111i l\u00E0\n h\u1EBFt tranh nhau. B\u1EAFt \u0111\u01B0\u1EE3c khi \u0111\u1ED1i chi\u1EBFu v\u1EDBi `ol-navbar-item` ng\u00E0y 2026-09-24;\n trang docs kh\u00F4ng l\u1ED9 v\u00EC c\u00E1c m\u1EE5c active \u1EDF \u0111\u00E2y d\u00F9ng nh\u00E1nh `div`/`href`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13375
13756
  }
13376
13757
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarItem, decorators: [{
13377
13758
  type: Component,
13378
- args: [{ selector: 'ol-sidebar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon, OlBadge, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n" }]
13759
+ args: [{ selector: 'ol-sidebar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon, OlBadge, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!--\n `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET.\n\n `RouterLinkActive` v\u00E0 `[class]=\"itemClasses()\"` c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh\n `class`, directive ghi SAU n\u00EAn n\u00F3 th\u1EAFng: k\u1EBFt h\u1EE3p `routerLink` + `[active]`\n th\u00EC chu\u1ED7i `'active \u2026'` b\u1ECB nu\u1ED1t. H\u1ECFng IM L\u1EB6NG \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (c\u00F9ng\n chu\u1ED7i), ch\u1EC9 ri\u00EAng ch\u1EEF `active` m\u1EA5t, n\u00EAn `not-[.active]:hover:*` v\u1EABn \u00E1p d\u1EE5ng\n v\u00E0 m\u1EE5c \u0111ang m\u1EDF NH\u1EA0T \u0110I khi r\u00EA chu\u1ED9t thay v\u00EC \u0111\u1EADm l\u00EAn.\n\n \u0110\u00E3 \u00E9p active b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh \u2014 t\u1EAFt n\u00F3 \u0111i l\u00E0\n h\u1EBFt tranh nhau. B\u1EAFt \u0111\u01B0\u1EE3c khi \u0111\u1ED1i chi\u1EBFu v\u1EDBi `ol-navbar-item` ng\u00E0y 2026-09-24;\n trang docs kh\u00F4ng l\u1ED9 v\u00EC c\u00E1c m\u1EE5c active \u1EDF \u0111\u00E2y d\u00F9ng nh\u00E1nh `div`/`href`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n" }]
13379
13760
  }], propDecorators: { routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], routerLinkActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActive", required: false }] }], routerLinkActiveOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActiveOptions", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], badgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeColor", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }] } });
13380
13761
 
13381
13762
  class OlSidebarGroup {
@@ -13444,7 +13825,7 @@ class OlSidebarSubmenu {
13444
13825
  this.isOpen.update((v) => !v);
13445
13826
  }
13446
13827
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarSubmenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
13447
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarSubmenu, isStandalone: true, selector: "ol-sidebar-submenu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange" }, ngImport: i0, template: "<div>\n <button\n type=\"button\"\n (click)=\"toggle()\"\n [class]=\"triggerClasses()\"\n [attr.aria-expanded]=\"isOpen()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\">{{ label() }}</span>\n </div>\n\n <div class=\"flex items-center gap-2\">\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n <ol-icon\n name=\"chevron-right\"\n size=\"xs\"\n class=\"text-body-subtle transition-transform duration-200 shrink-0\"\n [class.rotate-90]=\"isOpen()\"\n ></ol-icon>\n </div>\n </button>\n\n <div\n class=\"pl-4 pr-1 py-1 space-y-1 border-l-2 border-default-subtle ml-5 my-1 transition-all duration-150\"\n [class.hidden]=\"!isOpen()\"\n >\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13828
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarSubmenu, isStandalone: true, selector: "ol-sidebar-submenu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange" }, ngImport: i0, template: "<div>\n <button\n type=\"button\"\n (click)=\"toggle()\"\n [class]=\"triggerClasses()\"\n [attr.aria-expanded]=\"isOpen()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\">{{ label() }}</span>\n </div>\n\n <div class=\"flex items-center gap-2\">\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n <ol-icon\n name=\"chevron-right\"\n size=\"xs\"\n class=\"text-body-subtle transition-transform duration-200 shrink-0\"\n [class.rotate-90]=\"isOpen()\"\n ></ol-icon>\n </div>\n </button>\n\n <div\n class=\"pl-4 pr-1 py-1 space-y-1 border-l-2 border-default-subtle ml-5 my-1 transition-all duration-150\"\n [class.hidden]=\"!isOpen()\"\n >\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13448
13829
  }
13449
13830
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarSubmenu, decorators: [{
13450
13831
  type: Component,
@@ -14409,7 +14790,7 @@ class OlTabs {
14409
14790
  }
14410
14791
  }
14411
14792
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
14412
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTabs, isStandalone: true, selector: "ol-tabs", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, mobile: { classPropertyName: "mobile", publicName: "mobile", isSignal: true, isRequired: false, transformFunction: null }, mobileDropdown: { classPropertyName: "mobileDropdown", publicName: "mobileDropdown", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, activeTab: { classPropertyName: "activeTab", publicName: "activeTab", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", activeTab: "activeTabChange", tabChange: "tabChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "items", predicate: OlTabItem, isSignal: true }], ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!-- 1. Mobile Dropdown View (Type=Default / Pills / Border bottom, Mobile=True - Figma #1149:80537, #1149:80539, #1149:80541) -->\n@if (isMobileDropdown()) {\n <div class=\"w-full max-w-md\">\n <div class=\"relative\">\n <!--\n `[ngModel]` ch\u1EE9 KH\u00D4NG ph\u1EA3i `[value]`: `[value]` tr\u00EAn <select> \u0111\u1EB7t thu\u1ED9c\n t\u00EDnh DOM NGAY, l\u00FAc c\u00E1c <option> c\u1EE7a @for ch\u01B0a t\u1ED3n t\u1EA1i, n\u00EAn tr\u00ECnh duy\u1EC7t\n ho\u00E0n nguy\u00EAn v\u1EC1 m\u1EE5c \u0111\u1EA7u. \u0110o 2026-09-17 v\u1EDBi `[selectedIndex]=\"2\"`: tab\n \u0111ang ch\u1ECDn l\u00E0 \"Ba\" m\u00E0 \u00F4 ch\u1ECDn hi\u1EC7n \"M\u1ED9t\". Build xanh, kh\u00F4ng c\u1EA3nh b\u00E1o.\n\n V\u00E0 `selectTab($event)` ch\u1EE9 kh\u00F4ng ph\u1EA3i `.selectedIndex` \u2014 c\u00E1i sau l\u00E0 v\u1ECB\n tr\u00ED trong danh s\u00E1ch DOM, tr\u00F9ng v\u1EDBi ch\u1EC9 s\u1ED1 tab ch\u1EC9 khi m\u1ECDi tab \u0111\u1EC1u \u0111\u01B0\u1EE3c\n v\u1EBD; l\u1ECDc b\u1EDBt m\u1ED9t tab l\u00E0 l\u1EC7ch ngay. `[value]` c\u1EE7a option m\u1EDBi l\u00E0 ngu\u1ED3n.\n -->\n <select\n [ngModel]=\"currentIndex()\"\n (ngModelChange)=\"selectTab(+$event)\"\n aria-label=\"Ch\u1ECDn tab\"\n class=\"w-full appearance-none rounded-lg border border-default-medium bg-neutral-primary py-2.5 pl-3.5 pr-10 text-sm font-medium text-heading shadow-xs focus:border-brand-light focus:outline-hidden focus:ring-1 focus:ring-brand-subtle cursor-pointer\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <option [value]=\"i\" [disabled]=\"item.disabled()\">\n {{ item.displayLabel() }}\n </option>\n }\n </select>\n <div\n class=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-body-subtle\"\n >\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" />\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div class=\"pt-4 w-full\">\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n} @else {\n <!-- 2. Standard / Desktop / Vertical Stack View -->\n <div\n [class]=\"\n isVertical()\n ? 'flex flex-col md:flex-row gap-6 items-start w-full'\n : 'flex w-full flex-col min-h-0'\n \"\n >\n <!-- Tab Headers Navigation -->\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs w-full max-w-md'\n : mobile() && variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs divide-y divide-default overflow-hidden w-full max-w-md'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'w-full md:w-64 shrink-0'\n : 'w-full md:w-44 shrink-0'\n : variant() === 'button' || variant() === 'boxed'\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs inline-flex max-w-full overflow-x-auto'\n : variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs inline-flex divide-x divide-default overflow-hidden max-w-full overflow-x-auto'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'border-b border-default w-full'\n : 'w-full'\n \"\n >\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'flex flex-col gap-1 w-full'\n : mobile() && variant() === 'button-group'\n ? 'flex flex-col w-full'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'flex flex-col gap-1 p-2 bg-neutral-primary rounded-md border border-default shadow-xs'\n : 'flex flex-col gap-2.5'\n : variant() === 'button' || variant() === 'boxed'\n ? 'flex items-center gap-1 ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'button-group'\n ? 'flex items-center ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'flex items-center gap-6 overflow-x-auto -mb-px ' +\n (fullWidth() ? 'justify-between' : '')\n : 'flex items-center gap-6 overflow-x-auto ' +\n (fullWidth() ? 'justify-between' : '')\n \"\n role=\"tablist\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [id]=\"item.tabId()\"\n [disabled]=\"item.disabled()\"\n [attr.aria-selected]=\"i === currentIndex()\"\n (click)=\"selectTab(i)\"\n [class]=\"\n 'inline-flex items-center font-medium transition-all duration-150 select-none whitespace-nowrap ' +\n sizeClasses() +\n ' ' +\n (fullWidth() || mobile() ? 'w-full justify-start ' : '') +\n (item.disabled() ? 'opacity-40 cursor-not-allowed ' : 'cursor-pointer ') +\n (mobile() && (variant() === 'button' || variant() === 'boxed')\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : mobile() && variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'default'\n ? i === currentIndex()\n ? 'text-fg-brand font-semibold'\n : 'text-body hover:text-heading'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? i === currentIndex()\n ? 'border-b-2 border-brand text-fg-brand font-semibold pb-3 -mb-px'\n : 'border-b-2 border-transparent text-body hover:text-heading hover:border-default-strong pb-3 -mb-px'\n : variant() === 'pills'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-full shadow-xs'\n : 'text-body hover:bg-neutral-tertiary rounded-full'\n : variant() === 'button' || variant() === 'boxed'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'vertical-pills' || variant() === 'vertical'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-md shadow-xs justify-start w-full'\n : 'text-body hover:bg-neutral-tertiary rounded-md justify-start w-full'\n : variant() === 'vertical-list'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-fg-brand font-semibold rounded-md justify-between w-full'\n : 'text-body hover:bg-neutral-secondary-medium rounded-md justify-between w-full'\n : '')\n \"\n >\n <!-- User select format with Avatar (Figma #1149:80606) -->\n @if (item.avatar()) {\n <div class=\"inline-flex items-center gap-3 min-w-0 text-left\">\n <img\n [src]=\"item.avatar()!\"\n alt=\"\"\n class=\"w-8 h-8 rounded-full object-cover shrink-0 border border-default\"\n />\n <div class=\"flex flex-col min-w-0\">\n <span class=\"truncate font-medium text-xs leading-tight text-heading\">{{\n item.displayLabel()\n }}</span>\n <!--\n \u0110\u1ECDc `displaySubtitle()` ch\u1EE9 kh\u00F4ng ph\u1EA3i `item.helper()`:\n `subtitle` v\u00E0 `helper` l\u00E0 b\u00ED danh c\u1EE7a nhau (ch\u00FA th\u00EDch ngay\n ch\u1ED7 khai trong tab-item), nh\u01B0ng render th\u1EB3ng `helper` th\u00EC\n \u0111\u1EB7t `subtitle=\"...\"` kh\u00F4ng hi\u1EC7n g\u00EC.\n -->\n @if (item.displaySubtitle()) {\n <span class=\"truncate text-[11px] text-body-subtle font-normal leading-tight\">{{\n item.displaySubtitle()\n }}</span>\n }\n </div>\n </div>\n } @else {\n <div class=\"inline-flex items-center gap-2 min-w-0\">\n @if (item.icon()) {\n <ol-icon [name]=\"item.icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n <span class=\"truncate\">{{ item.displayLabel() }}</span>\n </div>\n }\n\n @if (item.badge() !== null && item.badge() !== undefined && item.badge() !== '') {\n <ol-badge\n [color]=\"\n i === currentIndex() && (variant() === 'pills' || variant() === 'vertical-pills')\n ? 'white'\n : item.badgeColor()\n \"\n size=\"xs\"\n shape=\"pill\"\n class=\"ml-auto\"\n >\n {{ item.badge() }}\n </ol-badge>\n }\n\n @if (variant() === 'vertical-list' && !item.avatar()) {\n @if (i === currentIndex()) {\n <ol-icon\n name=\"check-circle\"\n size=\"xs\"\n class=\"text-fg-brand shrink-0 ml-auto\"\n ></ol-icon>\n } @else {\n <ol-icon\n name=\"alt-arrow-down\"\n size=\"xs\"\n class=\"text-body-subtle shrink-0 -rotate-90 ml-auto\"\n ></ol-icon>\n }\n }\n </button>\n }\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div\n [class]=\"isVertical() ? 'flex-1 min-w-0 w-full' : 'flex min-h-0 flex-1 flex-col pt-4 w-full'\"\n >\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14793
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTabs, isStandalone: true, selector: "ol-tabs", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, mobile: { classPropertyName: "mobile", publicName: "mobile", isSignal: true, isRequired: false, transformFunction: null }, mobileDropdown: { classPropertyName: "mobileDropdown", publicName: "mobileDropdown", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, activeTab: { classPropertyName: "activeTab", publicName: "activeTab", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", activeTab: "activeTabChange", tabChange: "tabChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "items", predicate: OlTabItem, isSignal: true }], ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!-- 1. Mobile Dropdown View (Type=Default / Pills / Border bottom, Mobile=True - Figma #1149:80537, #1149:80539, #1149:80541) -->\n@if (isMobileDropdown()) {\n <div class=\"w-full max-w-md\">\n <div class=\"relative\">\n <!--\n `[ngModel]` ch\u1EE9 KH\u00D4NG ph\u1EA3i `[value]`: `[value]` tr\u00EAn <select> \u0111\u1EB7t thu\u1ED9c\n t\u00EDnh DOM NGAY, l\u00FAc c\u00E1c <option> c\u1EE7a @for ch\u01B0a t\u1ED3n t\u1EA1i, n\u00EAn tr\u00ECnh duy\u1EC7t\n ho\u00E0n nguy\u00EAn v\u1EC1 m\u1EE5c \u0111\u1EA7u. \u0110o 2026-09-17 v\u1EDBi `[selectedIndex]=\"2\"`: tab\n \u0111ang ch\u1ECDn l\u00E0 \"Ba\" m\u00E0 \u00F4 ch\u1ECDn hi\u1EC7n \"M\u1ED9t\". Build xanh, kh\u00F4ng c\u1EA3nh b\u00E1o.\n\n V\u00E0 `selectTab($event)` ch\u1EE9 kh\u00F4ng ph\u1EA3i `.selectedIndex` \u2014 c\u00E1i sau l\u00E0 v\u1ECB\n tr\u00ED trong danh s\u00E1ch DOM, tr\u00F9ng v\u1EDBi ch\u1EC9 s\u1ED1 tab ch\u1EC9 khi m\u1ECDi tab \u0111\u1EC1u \u0111\u01B0\u1EE3c\n v\u1EBD; l\u1ECDc b\u1EDBt m\u1ED9t tab l\u00E0 l\u1EC7ch ngay. `[value]` c\u1EE7a option m\u1EDBi l\u00E0 ngu\u1ED3n.\n -->\n <select\n [ngModel]=\"currentIndex()\"\n (ngModelChange)=\"selectTab(+$event)\"\n aria-label=\"Ch\u1ECDn tab\"\n class=\"w-full appearance-none rounded-lg border border-default-medium bg-neutral-primary py-2.5 pl-3.5 pr-10 text-sm font-medium text-heading shadow-xs focus:border-brand-light focus:outline-hidden focus:ring-1 focus:ring-brand-subtle cursor-pointer\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <option [value]=\"i\" [disabled]=\"item.disabled()\">\n {{ item.displayLabel() }}\n </option>\n }\n </select>\n <div\n class=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-body-subtle\"\n >\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" />\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div class=\"pt-4 w-full\">\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n} @else {\n <!-- 2. Standard / Desktop / Vertical Stack View -->\n <div\n [class]=\"\n isVertical()\n ? 'flex flex-col md:flex-row gap-6 items-start w-full'\n : 'flex w-full flex-col min-h-0'\n \"\n >\n <!-- Tab Headers Navigation -->\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs w-full max-w-md'\n : mobile() && variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs divide-y divide-default overflow-hidden w-full max-w-md'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'w-full md:w-64 shrink-0'\n : 'w-full md:w-44 shrink-0'\n : variant() === 'button' || variant() === 'boxed'\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs inline-flex max-w-full overflow-x-auto'\n : variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs inline-flex divide-x divide-default overflow-hidden max-w-full overflow-x-auto'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'border-b border-default w-full'\n : 'w-full'\n \"\n >\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'flex flex-col gap-1 w-full'\n : mobile() && variant() === 'button-group'\n ? 'flex flex-col w-full'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'flex flex-col gap-1 p-2 bg-neutral-primary rounded-md border border-default shadow-xs'\n : 'flex flex-col gap-2.5'\n : variant() === 'button' || variant() === 'boxed'\n ? 'flex items-center gap-1 ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'button-group'\n ? 'flex items-center ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'flex items-center gap-6 overflow-x-auto -mb-px ' +\n (fullWidth() ? 'justify-between' : '')\n : 'flex items-center gap-6 overflow-x-auto ' +\n (fullWidth() ? 'justify-between' : '')\n \"\n role=\"tablist\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [id]=\"item.tabId()\"\n [disabled]=\"item.disabled()\"\n [attr.aria-selected]=\"i === currentIndex()\"\n (click)=\"selectTab(i)\"\n [class]=\"\n 'inline-flex items-center font-medium transition-all duration-150 select-none whitespace-nowrap ' +\n sizeClasses() +\n ' ' +\n (fullWidth() || mobile() ? 'w-full justify-start ' : '') +\n (item.disabled() ? 'opacity-40 cursor-not-allowed ' : 'cursor-pointer ') +\n (mobile() && (variant() === 'button' || variant() === 'boxed')\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : mobile() && variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'default'\n ? i === currentIndex()\n ? 'text-fg-brand font-semibold'\n : 'text-body hover:text-heading'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? i === currentIndex()\n ? 'border-b-2 border-brand text-fg-brand font-semibold pb-3 -mb-px'\n : 'border-b-2 border-transparent text-body hover:text-heading hover:border-default-strong pb-3 -mb-px'\n : variant() === 'pills'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-full shadow-xs'\n : 'text-body hover:bg-neutral-tertiary rounded-full'\n : variant() === 'button' || variant() === 'boxed'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'vertical-pills' || variant() === 'vertical'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-md shadow-xs justify-start w-full'\n : 'text-body hover:bg-neutral-tertiary rounded-md justify-start w-full'\n : variant() === 'vertical-list'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-fg-brand font-semibold rounded-md justify-between w-full'\n : 'text-body hover:bg-neutral-secondary-medium rounded-md justify-between w-full'\n : '')\n \"\n >\n <!-- User select format with Avatar (Figma #1149:80606) -->\n @if (item.avatar()) {\n <div class=\"inline-flex items-center gap-3 min-w-0 text-left\">\n <img\n [src]=\"item.avatar()!\"\n alt=\"\"\n class=\"w-8 h-8 rounded-full object-cover shrink-0 border border-default\"\n />\n <div class=\"flex flex-col min-w-0\">\n <span class=\"truncate font-medium text-xs leading-tight text-heading\">{{\n item.displayLabel()\n }}</span>\n <!--\n \u0110\u1ECDc `displaySubtitle()` ch\u1EE9 kh\u00F4ng ph\u1EA3i `item.helper()`:\n `subtitle` v\u00E0 `helper` l\u00E0 b\u00ED danh c\u1EE7a nhau (ch\u00FA th\u00EDch ngay\n ch\u1ED7 khai trong tab-item), nh\u01B0ng render th\u1EB3ng `helper` th\u00EC\n \u0111\u1EB7t `subtitle=\"...\"` kh\u00F4ng hi\u1EC7n g\u00EC.\n -->\n @if (item.displaySubtitle()) {\n <span class=\"truncate text-[11px] text-body-subtle font-normal leading-tight\">{{\n item.displaySubtitle()\n }}</span>\n }\n </div>\n </div>\n } @else {\n <div class=\"inline-flex items-center gap-2 min-w-0\">\n @if (item.icon()) {\n <ol-icon [name]=\"item.icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n <span class=\"truncate\">{{ item.displayLabel() }}</span>\n </div>\n }\n\n @if (item.badge() !== null && item.badge() !== undefined && item.badge() !== '') {\n <ol-badge\n [color]=\"\n i === currentIndex() && (variant() === 'pills' || variant() === 'vertical-pills')\n ? 'white'\n : item.badgeColor()\n \"\n size=\"xs\"\n shape=\"pill\"\n class=\"ml-auto\"\n >\n {{ item.badge() }}\n </ol-badge>\n }\n\n @if (variant() === 'vertical-list' && !item.avatar()) {\n @if (i === currentIndex()) {\n <ol-icon\n name=\"check-circle\"\n size=\"xs\"\n class=\"text-fg-brand shrink-0 ml-auto\"\n ></ol-icon>\n } @else {\n <ol-icon\n name=\"alt-arrow-down\"\n size=\"xs\"\n class=\"text-body-subtle shrink-0 -rotate-90 ml-auto\"\n ></ol-icon>\n }\n }\n </button>\n }\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div\n [class]=\"isVertical() ? 'flex-1 min-w-0 w-full' : 'flex min-h-0 flex-1 flex-col pt-4 w-full'\"\n >\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14413
14794
  }
14414
14795
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTabs, decorators: [{
14415
14796
  type: Component,
@@ -14795,7 +15176,7 @@ class OlBottomNavItem {
14795
15176
  }, /* @ts-ignore */
14796
15177
  ...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
14797
15178
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBottomNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
14798
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBottomNavItem, isStandalone: true, selector: "ol-bottom-nav-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isCenterAction: { classPropertyName: "isCenterAction", publicName: "isCenterAction", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba nh\u00E1nh routerLink / href / button ch\u1EC9 kh\u00E1c nhau \u1EDF TH\u1EBA B\u1ECCC; ru\u1ED9t y h\u1EC7t nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y ru\u1ED9t \u0111\u01B0\u1EE3c ch\u00E9p ba l\u1EA7n (105 d\u00F2ng) \u2014 s\u1EEDa gradient hay badge ph\u1EA3i s\u1EEDa\n \u0111\u1EE7 ba ch\u1ED7, qu\u00EAn m\u1ED9t l\u00E0 l\u1EC7ch \u00E2m th\u1EA7m. Gom v\u00E0o `ng-template`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n >\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <button type=\"button\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </button>\n}\n\n<ng-template #ruot>\n @if (isCenterAction()) {\n <!-- Gradient n\u00FAt h\u00E0nh \u0111\u1ED9ng gi\u1EEFa: `from-brand-700 to-brand-500` GI\u1EEE b\u1EADc s\u1ED1.\n `brand` l\u00E0 b\u1EA3ng Figma C\u00D3, v\u00E0 h\u1ECD `from-`/`to-` kh\u00F4ng c\u00F3 token ng\u1EEF ngh\u0129a\n n\u00E0o \u0111\u01B0\u1EE3c @theme emit (\u0111o 2026-09-15). -->\n <div\n class=\"w-12 h-12 rounded-full bg-linear-to-tr from-brand-700 to-brand-500 text-white shadow-lg shadow-md/30 flex items-center justify-center transform group-hover:scale-105 group-active:scale-95 transition-all\"\n >\n @if (icon()) {\n <ol-icon [name]=\"icon()\" size=\"lg\" color=\"white\"></ol-icon>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs font-semibold text-fg-brand mt-1 truncate\">{{ label() }}</span>\n }\n } @else {\n <div class=\"relative flex items-center justify-center\">\n @if (icon()) {\n <ol-icon\n [name]=\"icon()\"\n size=\"base\"\n class=\"transition-transform group-hover:scale-110\"\n ></ol-icon>\n }\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <span class=\"absolute -top-1.5 -right-2\">\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n </span>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs mt-1 truncate font-medium\">{{ label() }}</span>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
15179
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBottomNavItem, isStandalone: true, selector: "ol-bottom-nav-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isCenterAction: { classPropertyName: "isCenterAction", publicName: "isCenterAction", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba nh\u00E1nh routerLink / href / button ch\u1EC9 kh\u00E1c nhau \u1EDF TH\u1EBA B\u1ECCC; ru\u1ED9t y h\u1EC7t nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y ru\u1ED9t \u0111\u01B0\u1EE3c ch\u00E9p ba l\u1EA7n (105 d\u00F2ng) \u2014 s\u1EEDa gradient hay badge ph\u1EA3i s\u1EEDa\n \u0111\u1EE7 ba ch\u1ED7, qu\u00EAn m\u1ED9t l\u00E0 l\u1EC7ch \u00E2m th\u1EA7m. Gom v\u00E0o `ng-template`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n >\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <button type=\"button\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </button>\n}\n\n<ng-template #ruot>\n @if (isCenterAction()) {\n <!-- Gradient n\u00FAt h\u00E0nh \u0111\u1ED9ng gi\u1EEFa: `from-brand-700 to-brand-500` GI\u1EEE b\u1EADc s\u1ED1.\n `brand` l\u00E0 b\u1EA3ng Figma C\u00D3, v\u00E0 h\u1ECD `from-`/`to-` kh\u00F4ng c\u00F3 token ng\u1EEF ngh\u0129a\n n\u00E0o \u0111\u01B0\u1EE3c @theme emit (\u0111o 2026-09-15). -->\n <div\n class=\"w-12 h-12 rounded-full bg-linear-to-tr from-brand-700 to-brand-500 text-white shadow-lg shadow-md/30 flex items-center justify-center transform group-hover:scale-105 group-active:scale-95 transition-all\"\n >\n @if (icon()) {\n <ol-icon [name]=\"icon()\" size=\"lg\" color=\"white\"></ol-icon>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs font-semibold text-fg-brand mt-1 truncate\">{{ label() }}</span>\n }\n } @else {\n <div class=\"relative flex items-center justify-center\">\n @if (icon()) {\n <ol-icon\n [name]=\"icon()\"\n size=\"base\"\n class=\"transition-transform group-hover:scale-110\"\n ></ol-icon>\n }\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <span class=\"absolute -top-1.5 -right-2\">\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n </span>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs mt-1 truncate font-medium\">{{ label() }}</span>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14799
15180
  }
14800
15181
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBottomNavItem, decorators: [{
14801
15182
  type: Component,
@@ -14959,7 +15340,7 @@ class OlStepper {
14959
15340
  }
14960
15341
  }
14961
15342
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlStepper, deps: [], target: i0.ɵɵFactoryTarget.Component });
14962
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlStepper, isStandalone: true, selector: "ol-stepper", inputs: { currentStep: { classPropertyName: "currentStep", publicName: "currentStep", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, breakpoint: { classPropertyName: "breakpoint", publicName: "breakpoint", isSignal: true, isRequired: false, transformFunction: null }, linear: { classPropertyName: "linear", publicName: "linear", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentStep: "currentStepChange", stepChange: "stepChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "steps", predicate: OlStepItem, isSignal: true }], ngImport: i0, template: "<div [class]=\"isVertical() ? 'flex flex-col md:flex-row gap-8 w-full' : 'w-full'\">\n <!-- Stepper Header / Navigation Area -->\n <div [class]=\"isVertical() ? 'w-full md:w-80 shrink-0' : 'w-full mb-6'\">\n @if (isMobile()) {\n <!-- Mobile Layout -->\n <div class=\"p-4 rounded-md bg-neutral-tertiary/80 border border-default\">\n <div class=\"flex items-center justify-between mb-2\">\n <span class=\"text-xs font-semibold text-fg-brand uppercase tracking-wider\">\n B\u01B0\u1EDBc {{ currentStep() + 1 }} / {{ steps().length }}\n </span>\n @if (steps()[currentStep()]; as activeStep) {\n <span class=\"text-xs text-body-subtle font-medium\">\n {{ activeStep.title() }}\n </span>\n }\n </div>\n <!-- Progress Bar -->\n <div\n class=\"w-full bg-neutral-tertiary-medium h-2 rounded-full overflow-hidden flex gap-1 p-0.5\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div\n class=\"h-full flex-1 rounded-full transition-all duration-300\"\n [class.bg-brand]=\"i <= currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i > currentStep()\"\n ></div>\n }\n </div>\n </div>\n } @else if (variant() === 'cards') {\n <!-- Cards Variant (Vertical Stack) -->\n <div class=\"flex flex-col gap-3\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <!--\n `<button role=\"tab\">` ch\u1EE9 KH\u00D4NG ph\u1EA3i `<div (click)>`. Khung ngo\u00E0i\n khai `role=\"tablist\"` nh\u01B0ng \u0111o 2026-09-17 b\u00EAn trong c\u00F3 **0\n `role=\"tab\"` v\u00E0 0 ph\u1EA7n t\u1EED tab t\u1EDBi \u0111\u01B0\u1EE3c** \u2014 `tablist` r\u1ED7ng vai tr\u00F2,\n v\u00E0 b\u00E0n ph\u00EDm kh\u00F4ng ch\u1ECDn n\u1ED5i b\u01B0\u1EDBc n\u00E0o. Ba bi\u1EBFn th\u1EC3 stepper kh\u00E1c \u0111\u00E3\n d\u00F9ng `<button>` s\u1EB5n; ch\u1EC9 ri\u00EAng `cards` l\u1EC7ch.\n\n `w-full` v\u00EC button kh\u00F4ng t\u1EF1 gi\u00E3n nh\u01B0 div.\n -->\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"step.effectiveState() === 'active'\"\n [disabled]=\"step.effectiveState() === 'disabled'\"\n (click)=\"goToStep(i)\"\n [class]=\"\n 'w-full flex items-center justify-between p-4 rounded-md border transition-all duration-200 text-left ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-secondary-soft border-default text-fg-disabled'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer shadow-xs ring-2 ring-brand-subtle'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer hover:shadow-xs border-danger-subtle bg-danger-soft'\n : 'cursor-pointer hover:shadow-xs border-default-medium bg-neutral-secondary-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n <!-- Left Icon / Step Badge -->\n <div\n [class]=\"\n 'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 font-bold text-xs transition-colors ' +\n (step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'active'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body')\n \"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </div>\n\n <!-- Title & Subtitle -->\n <div class=\"min-w-0\">\n <div\n [class]=\"\n 'text-sm truncate ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand-strong '\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled truncate\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n\n <!-- Right Status Icon -->\n <div class=\"shrink-0 ml-3\">\n @if (step.effectiveState() === 'completed') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <ol-icon name=\"check-circle\" size=\"xs\" color=\"brand\"></ol-icon>\n <span class=\"hidden sm:inline\">Ho\u00E0n t\u1EA5t</span>\n </span>\n } @else if (step.effectiveState() === 'active') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <span class=\"w-2 h-2 rounded-full bg-brand animate-pulse\"></span>\n <span class=\"hidden sm:inline\">\u0110ang th\u1EF1c hi\u1EC7n</span>\n </span>\n } @else if (step.effectiveState() === 'error') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n <span class=\"hidden sm:inline\">L\u1ED7i</span>\n </span>\n } @else {\n <ol-icon name=\"chevron-right\" size=\"xs\" class=\"text-fg-disabled\"></ol-icon>\n }\n </div>\n </button>\n }\n </div>\n } @else if (isVertical()) {\n <!-- Vertical Variant -->\n <div class=\"flex flex-col relative\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-start gap-4 relative pb-8 last:pb-0\">\n <!-- Connecting Vertical Line -->\n @if (i < steps().length - 1) {\n <div\n class=\"absolute left-4.25 top-9 bottom-0 w-0.5 transition-colors duration-200\"\n [class.bg-brand]=\"i < currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i >= currentStep()\"\n ></div>\n }\n\n <!-- Step Circle / Icon Shape -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 z-10 ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-tertiary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'cursor-pointer bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n\n <!-- Step Title & Subtitle -->\n <div class=\"text-left pt-1 cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled mt-0.5\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- Horizontal Variants: Default | Only Icons | Icons & Text | With Dots -->\n <div\n class=\"flex items-center justify-between w-full relative\"\n role=\"tablist\"\n aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-center flex-1 last:flex-none relative\">\n <div class=\"flex items-center gap-3\">\n @if (variant() === 'with-dots') {\n <!-- With Dots Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'flex items-center justify-center transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'w-3.5 h-3.5 rounded-full opacity-50 bg-neutral-primary text-fg-disabled border-2 border-default-medium'\n : step.effectiveState() === 'completed'\n ? 'w-4 h-4 rounded-full bg-brand text-white'\n : step.effectiveState() === 'active'\n ? 'w-4 h-4 rounded-full bg-neutral-primary border-2 border-brand-light ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'w-4 h-4 rounded-full bg-danger-surface ring-4 ring-danger-subtle'\n : 'w-3.5 h-3.5 rounded-full bg-neutral-primary border-2 border-default-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" class=\"scale-75\"></ol-icon>\n }\n </button>\n } @else if (variant() === 'only-icons' || variant() === 'icons-text') {\n <!-- Icon Shape Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-10 h-10 rounded-full flex items-center justify-center font-semibold text-sm transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-tertiary text-fg-disabled'\n : step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body border border-default')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"sm\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"sm\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"sm\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n } @else {\n <!-- Default Variant (Numbered Circle) -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-primary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n }\n\n <!-- Text Title & Subtitle (unless only-icons) -->\n @if (variant() !== 'only-icons') {\n <div class=\"text-left cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled hidden md:block\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Connecting Line (Horizontal) -->\n @if (i < steps().length - 1) {\n <div class=\"flex-1 mx-4 h-0.5 bg-neutral-tertiary-medium relative overflow-hidden\">\n <div\n class=\"h-full bg-brand transition-all duration-300\"\n [class.w-full]=\"i < currentStep()\"\n [class.w-0]=\"i >= currentStep()\"\n ></div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Stepper Body Content Area -->\n <div [class]=\"isVertical() ? 'flex-1 min-w-0' : 'w-full'\">\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
15343
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlStepper, isStandalone: true, selector: "ol-stepper", inputs: { currentStep: { classPropertyName: "currentStep", publicName: "currentStep", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, breakpoint: { classPropertyName: "breakpoint", publicName: "breakpoint", isSignal: true, isRequired: false, transformFunction: null }, linear: { classPropertyName: "linear", publicName: "linear", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentStep: "currentStepChange", stepChange: "stepChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "steps", predicate: OlStepItem, isSignal: true }], ngImport: i0, template: "<div [class]=\"isVertical() ? 'flex flex-col md:flex-row gap-8 w-full' : 'w-full'\">\n <!-- Stepper Header / Navigation Area -->\n <div [class]=\"isVertical() ? 'w-full md:w-80 shrink-0' : 'w-full mb-6'\">\n @if (isMobile()) {\n <!-- Mobile Layout -->\n <div class=\"p-4 rounded-md bg-neutral-tertiary/80 border border-default\">\n <div class=\"flex items-center justify-between mb-2\">\n <span class=\"text-xs font-semibold text-fg-brand uppercase tracking-wider\">\n B\u01B0\u1EDBc {{ currentStep() + 1 }} / {{ steps().length }}\n </span>\n @if (steps()[currentStep()]; as activeStep) {\n <span class=\"text-xs text-body-subtle font-medium\">\n {{ activeStep.title() }}\n </span>\n }\n </div>\n <!-- Progress Bar -->\n <div\n class=\"w-full bg-neutral-tertiary-medium h-2 rounded-full overflow-hidden flex gap-1 p-0.5\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div\n class=\"h-full flex-1 rounded-full transition-all duration-300\"\n [class.bg-brand]=\"i <= currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i > currentStep()\"\n ></div>\n }\n </div>\n </div>\n } @else if (variant() === 'cards') {\n <!-- Cards Variant (Vertical Stack) -->\n <div class=\"flex flex-col gap-3\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <!--\n `<button role=\"tab\">` ch\u1EE9 KH\u00D4NG ph\u1EA3i `<div (click)>`. Khung ngo\u00E0i\n khai `role=\"tablist\"` nh\u01B0ng \u0111o 2026-09-17 b\u00EAn trong c\u00F3 **0\n `role=\"tab\"` v\u00E0 0 ph\u1EA7n t\u1EED tab t\u1EDBi \u0111\u01B0\u1EE3c** \u2014 `tablist` r\u1ED7ng vai tr\u00F2,\n v\u00E0 b\u00E0n ph\u00EDm kh\u00F4ng ch\u1ECDn n\u1ED5i b\u01B0\u1EDBc n\u00E0o. Ba bi\u1EBFn th\u1EC3 stepper kh\u00E1c \u0111\u00E3\n d\u00F9ng `<button>` s\u1EB5n; ch\u1EC9 ri\u00EAng `cards` l\u1EC7ch.\n\n `w-full` v\u00EC button kh\u00F4ng t\u1EF1 gi\u00E3n nh\u01B0 div.\n -->\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"step.effectiveState() === 'active'\"\n [disabled]=\"step.effectiveState() === 'disabled'\"\n (click)=\"goToStep(i)\"\n [class]=\"\n 'w-full flex items-center justify-between p-4 rounded-md border transition-all duration-200 text-left ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-secondary-soft border-default text-fg-disabled'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer shadow-xs ring-2 ring-brand-subtle'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer hover:shadow-xs border-danger-subtle bg-danger-soft'\n : 'cursor-pointer hover:shadow-xs border-default-medium bg-neutral-secondary-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n <!-- Left Icon / Step Badge -->\n <div\n [class]=\"\n 'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 font-bold text-xs transition-colors ' +\n (step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'active'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body')\n \"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </div>\n\n <!-- Title & Subtitle -->\n <div class=\"min-w-0\">\n <div\n [class]=\"\n 'text-sm truncate ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand-strong '\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled truncate\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n\n <!-- Right Status Icon -->\n <div class=\"shrink-0 ml-3\">\n @if (step.effectiveState() === 'completed') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <ol-icon name=\"check-circle\" size=\"xs\" color=\"brand\"></ol-icon>\n <span class=\"hidden sm:inline\">Ho\u00E0n t\u1EA5t</span>\n </span>\n } @else if (step.effectiveState() === 'active') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <span class=\"w-2 h-2 rounded-full bg-brand animate-pulse\"></span>\n <span class=\"hidden sm:inline\">\u0110ang th\u1EF1c hi\u1EC7n</span>\n </span>\n } @else if (step.effectiveState() === 'error') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n <span class=\"hidden sm:inline\">L\u1ED7i</span>\n </span>\n } @else {\n <ol-icon name=\"chevron-right\" size=\"xs\" class=\"text-fg-disabled\"></ol-icon>\n }\n </div>\n </button>\n }\n </div>\n } @else if (isVertical()) {\n <!-- Vertical Variant -->\n <div class=\"flex flex-col relative\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-start gap-4 relative pb-8 last:pb-0\">\n <!-- Connecting Vertical Line -->\n @if (i < steps().length - 1) {\n <div\n class=\"absolute left-4.25 top-9 bottom-0 w-0.5 transition-colors duration-200\"\n [class.bg-brand]=\"i < currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i >= currentStep()\"\n ></div>\n }\n\n <!-- Step Circle / Icon Shape -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 z-10 ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-tertiary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'cursor-pointer bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n\n <!-- Step Title & Subtitle -->\n <div class=\"text-left pt-1 cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled mt-0.5\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- Horizontal Variants: Default | Only Icons | Icons & Text | With Dots -->\n <div\n class=\"flex items-center justify-between w-full relative\"\n role=\"tablist\"\n aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-center flex-1 last:flex-none relative\">\n <div class=\"flex items-center gap-3\">\n @if (variant() === 'with-dots') {\n <!-- With Dots Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'flex items-center justify-center transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'w-3.5 h-3.5 rounded-full opacity-50 bg-neutral-primary text-fg-disabled border-2 border-default-medium'\n : step.effectiveState() === 'completed'\n ? 'w-4 h-4 rounded-full bg-brand text-white'\n : step.effectiveState() === 'active'\n ? 'w-4 h-4 rounded-full bg-neutral-primary border-2 border-brand-light ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'w-4 h-4 rounded-full bg-danger-surface ring-4 ring-danger-subtle'\n : 'w-3.5 h-3.5 rounded-full bg-neutral-primary border-2 border-default-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" class=\"scale-75\"></ol-icon>\n }\n </button>\n } @else if (variant() === 'only-icons' || variant() === 'icons-text') {\n <!-- Icon Shape Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-10 h-10 rounded-full flex items-center justify-center font-semibold text-sm transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-tertiary text-fg-disabled'\n : step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body border border-default')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"sm\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"sm\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"sm\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n } @else {\n <!-- Default Variant (Numbered Circle) -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-primary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n }\n\n <!-- Text Title & Subtitle (unless only-icons) -->\n @if (variant() !== 'only-icons') {\n <div class=\"text-left cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled hidden md:block\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Connecting Line (Horizontal) -->\n @if (i < steps().length - 1) {\n <div class=\"flex-1 mx-4 h-0.5 bg-neutral-tertiary-medium relative overflow-hidden\">\n <div\n class=\"h-full bg-brand transition-all duration-300\"\n [class.w-full]=\"i < currentStep()\"\n [class.w-0]=\"i >= currentStep()\"\n ></div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Stepper Body Content Area -->\n <div [class]=\"isVertical() ? 'flex-1 min-w-0' : 'w-full'\">\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14963
15344
  }
14964
15345
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlStepper, decorators: [{
14965
15346
  type: Component,
@@ -15041,15 +15422,50 @@ class OlTreeNodeItem {
15041
15422
  expanded: !!this.node().expanded,
15042
15423
  }), /* @ts-ignore */
15043
15424
  ...(ngDevMode ? [{ debugName: "tplContext" }] : /* istanbul ignore next */ []));
15425
+ /**
15426
+ * Chỉ `id` của nút, tách khỏi cả object.
15427
+ *
15428
+ * `effect` bên dưới đọc tín hiệu này thay vì `this.node()`: đọc cả object
15429
+ * thì effect chạy lại mỗi lần nơi dùng thay MẢNG gốc — kiểu tải lại danh
15430
+ * sách từ máy chủ — dù chính nút này không đổi gì. `computed` chỉ phát tín
15431
+ * hiệu khi GIÁ TRỊ đổi, mà `id` là chuỗi nên so sánh bằng `===`: tải lại
15432
+ * 100 nút với cùng id thì effect không chạy lần nào.
15433
+ */
15434
+ nodeId = computed(() => this.node().id, /* @ts-ignore */
15435
+ ...(ngDevMode ? [{ debugName: "nodeId" }] : /* istanbul ignore next */ []));
15044
15436
  constructor() {
15045
- // Điều hướng bằng phím chỉ đổi `focusedId`; phải gọi `.focus()` thật thì
15046
- // trình duyệt mới chuyển con trỏ và vẽ vòng focus.
15437
+ /*
15438
+ * Điều hướng bằng phím chỉ đổi `focusedId`; phải gọi `.focus()` thật thì
15439
+ * trình duyệt mới chuyển con trỏ.
15440
+ *
15441
+ * HAI ĐIỀU KIỆN CHẶN, cả hai đều có lý do đo được:
15442
+ *
15443
+ * 1. Đọc `nodeId()` chứ không `node()`. Đo bằng ca thử "tải lại toàn bộ
15444
+ * cây, id giữ nguyên": bản cũ effect chạy lại và CƯỚP focus khỏi phần
15445
+ * tử người dùng đang đứng (đo được focus nhảy từ nút ngoài cây vào một
15446
+ * `div` trong cây).
15447
+ *
15448
+ * 2. Chỉ lấy focus khi focus ĐANG Ở TRONG CÂY. Người dùng bấm ra ô tìm
15449
+ * kiếm rồi dữ liệu tự cập nhật thì con trỏ đang ở ô tìm — kéo nó về cây
15450
+ * là cướp thao tác đang làm dở. `focusedId` chỉ được đặt khi điều hướng
15451
+ * bằng phím, nhưng nó KHÔNG TỰ XOÁ khi người dùng rời cây.
15452
+ *
15453
+ * Hệ quả phụ, cũng là thứ người dùng báo: `.focus()` gọi BẰNG MÃ làm
15454
+ * `:focus-visible` khớp, nên viền brand 2px hiện ra sau mỗi lần cập nhật
15455
+ * dữ liệu — trông như "viền đen to" trên nền sẫm. Chặn effect chạy oan là
15456
+ * hết luôn viền oan.
15457
+ */
15047
15458
  effect(() => {
15048
- if (this.focusedId() !== this.node().id)
15459
+ if (this.focusedId() !== this.nodeId())
15049
15460
  return;
15050
15461
  const el = this.hang()?.nativeElement;
15051
- if (el && document.activeElement !== el)
15052
- el.focus();
15462
+ if (!el || document.activeElement === el)
15463
+ return;
15464
+ // Chỉ cướp focus khi nó vốn đã ở trong cây này.
15465
+ const tree = el.closest('[role="tree"]');
15466
+ if (tree && !tree.contains(document.activeElement))
15467
+ return;
15468
+ el.focus();
15053
15469
  });
15054
15470
  }
15055
15471
  /**
@@ -15094,7 +15510,7 @@ class OlTreeNodeItem {
15094
15510
  this.nodeCheck.emit({ node: this.node(), checked });
15095
15511
  }
15096
15512
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTreeNodeItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
15097
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTreeNodeItem, isStandalone: true, selector: "ol-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null }, actionsTemplate: { classPropertyName: "actionsTemplate", publicName: "actionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, checkable: { classPropertyName: "checkable", publicName: "checkable", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, toggleIcon: { classPropertyName: "toggleIcon", publicName: "toggleIcon", isSignal: true, isRequired: false, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, tabbableId: { classPropertyName: "tabbableId", publicName: "tabbableId", isSignal: true, isRequired: false, transformFunction: null }, focusedId: { classPropertyName: "focusedId", publicName: "focusedId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeClick: "nodeClick", nodeCheck: "nodeCheck", toggleExpand: "toggleExpand", nodeFocus: "nodeFocus" }, viewQueries: [{ propertyName: "hang", first: true, predicate: ["hang"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"select-none py-0.5\">\n <!-- Focus v\u1EBD VI\u1EC0N 2px b\u00EAn trong, kh\u00F4ng ph\u1EA3i ring: Figma `ol-tree`\n State=Focus \u0111o \u0111\u01B0\u1EE3c `strokeWeight: 2`, `strokeAlign: INSIDE`, m\u00E0u\n `border/border-brand` #2b4182. Ring v\u1EBD NGO\u00C0I h\u1ED9p n\u00EAn kh\u00E1c h\u00ECnh, v\u00E0 \u1EDF\n c\u00E2y l\u1ED3ng nhau n\u00F3 ch\u1ED3ng l\u00EAn node b\u00EAn c\u1EA1nh. -->\n <div\n #hang\n role=\"treeitem\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-level]=\"ariaLevel()\"\n [attr.aria-expanded]=\"ariaExpanded()\"\n [attr.aria-selected]=\"!!node().selected\"\n [attr.aria-disabled]=\"node().disabled ? true : null\"\n (focus)=\"nodeFocus.emit(node().id)\"\n class=\"group flex cursor-pointer items-center gap-2 rounded-lg border-2 border-transparent p-1 text-sm outline-hidden transition-colors focus:outline-hidden focus-visible:border-brand\"\n [class.bg-brand-softer]=\"node().selected\"\n [class.text-fg-brand]=\"node().selected\"\n [class.font-medium]=\"node().selected\"\n [class.text-heading]=\"!node().selected && !node().disabled\"\n [class.hover:bg-brand-softer]=\"!node().selected && !node().disabled\"\n [class.opacity-50]=\"node().disabled\"\n [class.cursor-not-allowed]=\"node().disabled\"\n (click)=\"handleClick()\"\n >\n <!-- Expand Toggle Button -->\n @if (node().children && node().children!.length > 0) {\n <!--\n N\u00FAt ch\u1EC9 c\u00F3 icon n\u00EAn PH\u1EA2I c\u00F3 t\u00EAn: \u0111o `/tree` ra 18 n\u00FAt g\u1EADp kh\u00F4ng t\u00EAn\n trong c\u00E2y tr\u1EE3 n\u0103ng. `aria-expanded` cho bi\u1EBFt tr\u1EA1ng th\u00E1i, `aria-label`\n cho bi\u1EBFt n\u00FAt n\u00E0y g\u1EADp/m\u1EDF NH\u00C1NH N\u00C0O \u2014 nhi\u1EC1u n\u00FAt gi\u1ED1ng h\u1EC7t nhau tr\u00EAn m\u1ED9t\n trang, ch\u1EC9 \"M\u1EDF r\u1ED9ng\" th\u00F4i th\u00EC v\u1EABn kh\u00F4ng ph\u00E2n bi\u1EC7t \u0111\u01B0\u1EE3c.\n -->\n <button\n type=\"button\"\n (click)=\"handleToggle($event)\"\n [attr.aria-expanded]=\"!!node().expanded\"\n [attr.aria-label]=\"(node().expanded ? 'Thu g\u1ECDn ' : 'M\u1EDF r\u1ED9ng ') + (node().label || 'nh\u00E1nh')\"\n class=\"w-5 h-5 flex items-center justify-center rounded text-body-subtle hover:text-heading transition-colors shrink-0\"\n >\n @if (toggleIcon() === 'plus-minus') {\n <span\n class=\"w-4 h-4 rounded border border-default-strong flex items-center justify-center text-xs font-mono font-bold leading-none text-body\"\n >\n {{ node().expanded ? '\u2212' : '+' }}\n </span>\n } @else {\n <ol-icon\n [name]=\"node().expanded ? 'alt-arrow-down' : 'alt-arrow-right'\"\n size=\"xs\"\n ></ol-icon>\n }\n </button>\n } @else {\n @if (!checkable()) {\n <span class=\"w-5 h-5 flex items-center justify-center shrink-0\">\n <span\n class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand transition-colors\"\n ></span>\n </span>\n }\n }\n <!-- Checkbox option (when checkable) -->\n @if (checkable()) {\n <div class=\"flex items-center justify-center shrink-0\" (click)=\"$event.stopPropagation()\">\n <ol-checkbox\n [standalone]=\"true\"\n [ariaLabel]=\"'Ch\u1ECDn ' + (node().label || 'nh\u00E1nh')\"\n [checked]=\"node().selected ?? false\"\n [indeterminate]=\"indeterminate()\"\n [disabled]=\"node().disabled ?? false\"\n (checkedChange)=\"handleCheck($event)\"\n size=\"base\"\n ></ol-checkbox>\n </div>\n }\n <!--\n Template tu\u1EF3 bi\u1EBFn CH\u1EC8 thay ph\u1EA7n n\u1ED9i dung (icon \u00B7 nh\u00E3n \u00B7 count \u00B7 badge).\n N\u00FAt m\u1EDF r\u1ED9ng, checkbox v\u00E0 th\u1EBB `role=\"treeitem\"` b\u1ECDc ngo\u00E0i v\u1EABn do lib gi\u1EEF \u2014\n cho ng\u01B0\u1EDDi d\u00F9ng v\u1EBD c\u1EA3 h\u00E0ng l\u00E0 m\u1EA5t ARIA \u0111\u00E3 l\u00E0m \u0111\u00FAng chu\u1EA9n \u1EDF BUG-017.\n -->\n @if (nodeTemplate()) {\n <ng-container *ngTemplateOutlet=\"nodeTemplate()!; context: tplContext()\" />\n } @else {\n <!-- Node Icon (if provided) -->\n @if (node().icon) {\n <ol-icon\n [name]=\"node().icon!\"\n size=\"xs\"\n [class.text-fg-brand]=\"node().selected\"\n [class.text-body-subtle]=\"!node().selected\"\n ></ol-icon>\n }\n <!-- Node Label -->\n <span class=\"truncate flex-1\">{{ node().label }}</span>\n <!-- Count or Badge -->\n @if (node().count !== undefined) {\n <span class=\"text-2xs text-body-subtle font-mono\"> ({{ node().count }}) </span>\n }\n @if (node().badge) {\n <ol-badge [color]=\"node().badgeColor || 'brand'\" size=\"xs\" shape=\"pill\">\n {{ node().badge }}\n </ol-badge>\n }\n }\n <!--\n H\u00E0nh \u0111\u1ED9ng cu\u1ED1i h\u00E0ng. \u0110\u1EB7t NGO\u00C0I c\u1EB7p @if/@else \u1EDF tr\u00EAn l\u00E0 c\u00F3 ch\u1EE7 \u00FD: n\u00FAt ph\u1EA3i\n hi\u1EC7n c\u1EA3 khi ng\u01B0\u1EDDi d\u00F9ng t\u1EF1 v\u1EBD n\u1ED9i dung b\u1EB1ng `nodeTemplate`. \u0110\u01B0a v\u00E0o trong\n nh\u00E1nh @else th\u00EC b\u1EADt `nodeTemplate` l\u00E0 n\u00FAt bi\u1EBFn m\u1EA5t \u2014 build v\u1EABn xanh.\n -->\n @if (actionsTemplate()) {\n <span class=\"ms-auto flex shrink-0 items-center gap-1\">\n <ng-container *ngTemplateOutlet=\"actionsTemplate()!; context: tplContext()\" />\n </span>\n }\n </div>\n <!-- Children Nodes (Indented 20px guide line + pl-5 child offset) -->\n @if (node().expanded && node().children && node().children!.length > 0) {\n <div role=\"group\" class=\"ml-5 mt-1 space-y-1 border-l border-default pl-5\">\n @for (child of node().children; track child.id) {\n <ol-tree-node\n [node]=\"child\"\n [nodeTemplate]=\"nodeTemplate()\"\n [actionsTemplate]=\"actionsTemplate()\"\n [checkable]=\"checkable()\"\n [selectionMode]=\"selectionMode()\"\n [toggleIcon]=\"toggleIcon()\"\n [level]=\"level() + 1\"\n [tabbableId]=\"tabbableId()\"\n [focusedId]=\"focusedId()\"\n (nodeFocus)=\"nodeFocus.emit($event)\"\n (nodeClick)=\"nodeClick.emit($event)\"\n (nodeCheck)=\"nodeCheck.emit($event)\"\n (toggleExpand)=\"toggleExpand.emit($event)\"\n ></ol-tree-node>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: OlTreeNodeItem, selector: "ol-tree-node", inputs: ["node", "nodeTemplate", "actionsTemplate", "checkable", "selectionMode", "toggleIcon", "level", "tabbableId", "focusedId"], outputs: ["nodeClick", "nodeCheck", "toggleExpand", "nodeFocus"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "component", type: OlCheckbox, selector: "ol-checkbox", inputs: ["checked", "label", "ariaLabel", "description", "disabled", "size", "indeterminate", "card", "id", "standalone"], outputs: ["checkedChange", "disabledChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
15513
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTreeNodeItem, isStandalone: true, selector: "ol-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null }, actionsTemplate: { classPropertyName: "actionsTemplate", publicName: "actionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, checkable: { classPropertyName: "checkable", publicName: "checkable", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, toggleIcon: { classPropertyName: "toggleIcon", publicName: "toggleIcon", isSignal: true, isRequired: false, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, tabbableId: { classPropertyName: "tabbableId", publicName: "tabbableId", isSignal: true, isRequired: false, transformFunction: null }, focusedId: { classPropertyName: "focusedId", publicName: "focusedId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeClick: "nodeClick", nodeCheck: "nodeCheck", toggleExpand: "toggleExpand", nodeFocus: "nodeFocus" }, viewQueries: [{ propertyName: "hang", first: true, predicate: ["hang"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"select-none py-0.5\">\n <!-- Focus v\u1EBD VI\u1EC0N 2px b\u00EAn trong, kh\u00F4ng ph\u1EA3i ring: Figma `ol-tree`\n State=Focus \u0111o \u0111\u01B0\u1EE3c `strokeWeight: 2`, `strokeAlign: INSIDE`, m\u00E0u\n `border/border-brand` #2b4182. Ring v\u1EBD NGO\u00C0I h\u1ED9p n\u00EAn kh\u00E1c h\u00ECnh, v\u00E0 \u1EDF\n c\u00E2y l\u1ED3ng nhau n\u00F3 ch\u1ED3ng l\u00EAn node b\u00EAn c\u1EA1nh. -->\n <div\n #hang\n role=\"treeitem\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-level]=\"ariaLevel()\"\n [attr.aria-expanded]=\"ariaExpanded()\"\n [attr.aria-selected]=\"!!node().selected\"\n [attr.aria-disabled]=\"node().disabled ? true : null\"\n (focus)=\"nodeFocus.emit(node().id)\"\n class=\"group flex cursor-pointer items-center gap-2 rounded-lg border-2 border-transparent p-1 text-sm outline-hidden transition-colors focus:outline-hidden focus-visible:border-brand\"\n [class.bg-brand-softer]=\"node().selected\"\n [class.text-fg-brand]=\"node().selected\"\n [class.font-medium]=\"node().selected\"\n [class.text-heading]=\"!node().selected && !node().disabled\"\n [class.hover:bg-brand-softer]=\"!node().selected && !node().disabled\"\n [class.opacity-50]=\"node().disabled\"\n [class.cursor-not-allowed]=\"node().disabled\"\n (click)=\"handleClick()\"\n >\n <!-- Expand Toggle Button -->\n @if (node().children && node().children!.length > 0) {\n <!--\n N\u00FAt ch\u1EC9 c\u00F3 icon n\u00EAn PH\u1EA2I c\u00F3 t\u00EAn: \u0111o `/tree` ra 18 n\u00FAt g\u1EADp kh\u00F4ng t\u00EAn\n trong c\u00E2y tr\u1EE3 n\u0103ng. `aria-expanded` cho bi\u1EBFt tr\u1EA1ng th\u00E1i, `aria-label`\n cho bi\u1EBFt n\u00FAt n\u00E0y g\u1EADp/m\u1EDF NH\u00C1NH N\u00C0O \u2014 nhi\u1EC1u n\u00FAt gi\u1ED1ng h\u1EC7t nhau tr\u00EAn m\u1ED9t\n trang, ch\u1EC9 \"M\u1EDF r\u1ED9ng\" th\u00F4i th\u00EC v\u1EABn kh\u00F4ng ph\u00E2n bi\u1EC7t \u0111\u01B0\u1EE3c.\n -->\n <button\n type=\"button\"\n (click)=\"handleToggle($event)\"\n [attr.aria-expanded]=\"!!node().expanded\"\n [attr.aria-label]=\"(node().expanded ? 'Thu g\u1ECDn ' : 'M\u1EDF r\u1ED9ng ') + (node().label || 'nh\u00E1nh')\"\n class=\"w-5 h-5 flex items-center justify-center rounded text-body-subtle hover:text-heading transition-colors shrink-0\"\n >\n @if (toggleIcon() === 'plus-minus') {\n <span\n class=\"w-4 h-4 rounded border border-default-strong flex items-center justify-center text-xs font-mono font-bold leading-none text-body\"\n >\n {{ node().expanded ? '\u2212' : '+' }}\n </span>\n } @else {\n <ol-icon\n [name]=\"node().expanded ? 'alt-arrow-down' : 'alt-arrow-right'\"\n size=\"xs\"\n ></ol-icon>\n }\n </button>\n } @else {\n @if (!checkable()) {\n <span class=\"w-5 h-5 flex items-center justify-center shrink-0\">\n <span\n class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand transition-colors\"\n ></span>\n </span>\n }\n }\n <!-- Checkbox option (when checkable) -->\n @if (checkable()) {\n <div class=\"flex items-center justify-center shrink-0\" (click)=\"$event.stopPropagation()\">\n <ol-checkbox\n [standalone]=\"true\"\n [ariaLabel]=\"'Ch\u1ECDn ' + (node().label || 'nh\u00E1nh')\"\n [checked]=\"node().selected ?? false\"\n [indeterminate]=\"indeterminate()\"\n [disabled]=\"node().disabled ?? false\"\n (checkedChange)=\"handleCheck($event)\"\n size=\"base\"\n ></ol-checkbox>\n </div>\n }\n <!--\n Template tu\u1EF3 bi\u1EBFn CH\u1EC8 thay ph\u1EA7n n\u1ED9i dung (icon \u00B7 nh\u00E3n \u00B7 count \u00B7 badge).\n N\u00FAt m\u1EDF r\u1ED9ng, checkbox v\u00E0 th\u1EBB `role=\"treeitem\"` b\u1ECDc ngo\u00E0i v\u1EABn do lib gi\u1EEF \u2014\n cho ng\u01B0\u1EDDi d\u00F9ng v\u1EBD c\u1EA3 h\u00E0ng l\u00E0 m\u1EA5t ARIA \u0111\u00E3 l\u00E0m \u0111\u00FAng chu\u1EA9n \u1EDF BUG-017.\n -->\n @if (nodeTemplate()) {\n <ng-container *ngTemplateOutlet=\"nodeTemplate()!; context: tplContext()\" />\n } @else {\n <!-- Node Icon (if provided) -->\n @if (node().icon) {\n <ol-icon\n [name]=\"node().icon!\"\n size=\"xs\"\n [class.text-fg-brand]=\"node().selected\"\n [class.text-body-subtle]=\"!node().selected\"\n ></ol-icon>\n }\n <!-- Node Label -->\n <span class=\"truncate flex-1\">{{ node().label }}</span>\n <!-- Count or Badge -->\n @if (node().count !== undefined) {\n <span class=\"text-2xs text-body-subtle font-mono\"> ({{ node().count }}) </span>\n }\n @if (node().badge) {\n <ol-badge [color]=\"node().badgeColor || 'brand'\" size=\"xs\" shape=\"pill\">\n {{ node().badge }}\n </ol-badge>\n }\n }\n <!--\n H\u00E0nh \u0111\u1ED9ng cu\u1ED1i h\u00E0ng. \u0110\u1EB7t NGO\u00C0I c\u1EB7p @if/@else \u1EDF tr\u00EAn l\u00E0 c\u00F3 ch\u1EE7 \u00FD: n\u00FAt ph\u1EA3i\n hi\u1EC7n c\u1EA3 khi ng\u01B0\u1EDDi d\u00F9ng t\u1EF1 v\u1EBD n\u1ED9i dung b\u1EB1ng `nodeTemplate`. \u0110\u01B0a v\u00E0o trong\n nh\u00E1nh @else th\u00EC b\u1EADt `nodeTemplate` l\u00E0 n\u00FAt bi\u1EBFn m\u1EA5t \u2014 build v\u1EABn xanh.\n -->\n @if (actionsTemplate()) {\n <span class=\"ms-auto flex shrink-0 items-center gap-1\">\n <ng-container *ngTemplateOutlet=\"actionsTemplate()!; context: tplContext()\" />\n </span>\n }\n </div>\n <!-- Children Nodes (Indented 20px guide line + pl-5 child offset) -->\n @if (node().expanded && node().children && node().children!.length > 0) {\n <div role=\"group\" class=\"ml-5 mt-1 space-y-1 border-l border-default pl-5\">\n @for (child of node().children; track child.id) {\n <ol-tree-node\n [node]=\"child\"\n [nodeTemplate]=\"nodeTemplate()\"\n [actionsTemplate]=\"actionsTemplate()\"\n [checkable]=\"checkable()\"\n [selectionMode]=\"selectionMode()\"\n [toggleIcon]=\"toggleIcon()\"\n [level]=\"level() + 1\"\n [tabbableId]=\"tabbableId()\"\n [focusedId]=\"focusedId()\"\n (nodeFocus)=\"nodeFocus.emit($event)\"\n (nodeClick)=\"nodeClick.emit($event)\"\n (nodeCheck)=\"nodeCheck.emit($event)\"\n (toggleExpand)=\"toggleExpand.emit($event)\"\n ></ol-tree-node>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: OlTreeNodeItem, selector: "ol-tree-node", inputs: ["node", "nodeTemplate", "actionsTemplate", "checkable", "selectionMode", "toggleIcon", "level", "tabbableId", "focusedId"], outputs: ["nodeClick", "nodeCheck", "toggleExpand", "nodeFocus"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "component", type: OlCheckbox, selector: "ol-checkbox", inputs: ["checked", "label", "ariaLabel", "description", "disabled", "size", "indeterminate", "card", "id", "standalone"], outputs: ["checkedChange", "disabledChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
15098
15514
  }
15099
15515
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTreeNodeItem, decorators: [{
15100
15516
  type: Component,
@@ -16076,11 +16492,11 @@ class OlChart {
16076
16492
  }, /* @ts-ignore */
16077
16493
  ...(ngDevMode ? [{ debugName: "horizontalBarsComputed" }] : /* istanbul ignore next */ []));
16078
16494
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
16079
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlChart, isStandalone: true, selector: "ol-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, categories: { classPropertyName: "categories", publicName: "categories", isSignal: true, isRequired: false, transformFunction: null }, series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: false, transformFunction: null }, dataPoints: { classPropertyName: "dataPoints", publicName: "dataPoints", isSignal: true, isRequired: false, transformFunction: null }, indicators: { classPropertyName: "indicators", publicName: "indicators", isSignal: true, isRequired: false, transformFunction: null }, scatterSeries: { classPropertyName: "scatterSeries", publicName: "scatterSeries", isSignal: true, isRequired: false, transformFunction: null }, heatmapData: { classPropertyName: "heatmapData", publicName: "heatmapData", isSignal: true, isRequired: false, transformFunction: null }, boxplotData: { classPropertyName: "boxplotData", publicName: "boxplotData", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, xAxisTitle: { classPropertyName: "xAxisTitle", publicName: "xAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, yAxisTitle: { classPropertyName: "yAxisTitle", publicName: "yAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 13/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n\n 38 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n text-gray-700 / dark:text-gray-300 s\u00E1ng \u0111\u1EADm, t\u1ED1i nh\u1EA1t\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
16495
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlChart, isStandalone: true, selector: "ol-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, categories: { classPropertyName: "categories", publicName: "categories", isSignal: true, isRequired: false, transformFunction: null }, series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: false, transformFunction: null }, dataPoints: { classPropertyName: "dataPoints", publicName: "dataPoints", isSignal: true, isRequired: false, transformFunction: null }, indicators: { classPropertyName: "indicators", publicName: "indicators", isSignal: true, isRequired: false, transformFunction: null }, scatterSeries: { classPropertyName: "scatterSeries", publicName: "scatterSeries", isSignal: true, isRequired: false, transformFunction: null }, heatmapData: { classPropertyName: "heatmapData", publicName: "heatmapData", isSignal: true, isRequired: false, transformFunction: null }, boxplotData: { classPropertyName: "boxplotData", publicName: "boxplotData", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, xAxisTitle: { classPropertyName: "xAxisTitle", publicName: "xAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, yAxisTitle: { classPropertyName: "yAxisTitle", publicName: "yAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 16/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n text-gray-700 dark:text-gray-300 -> text-body (3 l\u01B0\u1EE3t, 2026-09-24)\n\n Ba l\u01B0\u1EE3t cu\u1ED1i: ch\u00FA th\u00EDch c\u0169 x\u1EBFp ch\u00FAng v\u00E0o nh\u00F3m \"kh\u00F4ng token n\u00E0o kh\u1EDBp\", \u0111o l\u1EA1i\n th\u00EC `--text-body` \u0111\u1ED5i \u0110\u00DANG chi\u1EC1u #4a5565 -> #d1d5db, v\u00E0 gi\u00E1 tr\u1ECB t\u1ED1i TR\u00D9NG\n KH\u00CDT gray-300 c\u0169. S\u00E1ng nh\u1EA1t h\u01A1n m\u1ED9t b\u1EADc (#364153 -> #4a5565), t\u01B0\u01A1ng ph\u1EA3n\n tr\u00EAn n\u1EC1n tr\u1EAFng v\u1EABn 7,56 \u2014 d\u01B0 chu\u1EA9n AA 4,5.\n\n 35 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
16080
16496
  }
16081
16497
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlChart, decorators: [{
16082
16498
  type: Component,
16083
- args: [{ selector: 'ol-chart', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 13/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n\n 38 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n text-gray-700 / dark:text-gray-300 s\u00E1ng \u0111\u1EADm, t\u1ED1i nh\u1EA1t\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"] }]
16499
+ args: [{ selector: 'ol-chart', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 16/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n text-gray-700 dark:text-gray-300 -> text-body (3 l\u01B0\u1EE3t, 2026-09-24)\n\n Ba l\u01B0\u1EE3t cu\u1ED1i: ch\u00FA th\u00EDch c\u0169 x\u1EBFp ch\u00FAng v\u00E0o nh\u00F3m \"kh\u00F4ng token n\u00E0o kh\u1EDBp\", \u0111o l\u1EA1i\n th\u00EC `--text-body` \u0111\u1ED5i \u0110\u00DANG chi\u1EC1u #4a5565 -> #d1d5db, v\u00E0 gi\u00E1 tr\u1ECB t\u1ED1i TR\u00D9NG\n KH\u00CDT gray-300 c\u0169. S\u00E1ng nh\u1EA1t h\u01A1n m\u1ED9t b\u1EADc (#364153 -> #4a5565), t\u01B0\u01A1ng ph\u1EA3n\n tr\u00EAn n\u1EC1n tr\u1EAFng v\u1EABn 7,56 \u2014 d\u01B0 chu\u1EA9n AA 4,5.\n\n 35 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"] }]
16084
16500
  }], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], categories: [{ type: i0.Input, args: [{ isSignal: true, alias: "categories", required: false }] }], series: [{ type: i0.Input, args: [{ isSignal: true, alias: "series", required: false }] }], dataPoints: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataPoints", required: false }] }], indicators: [{ type: i0.Input, args: [{ isSignal: true, alias: "indicators", required: false }] }], scatterSeries: [{ type: i0.Input, args: [{ isSignal: true, alias: "scatterSeries", required: false }] }], heatmapData: [{ type: i0.Input, args: [{ isSignal: true, alias: "heatmapData", required: false }] }], boxplotData: [{ type: i0.Input, args: [{ isSignal: true, alias: "boxplotData", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], showGrid: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGrid", required: false }] }], xAxisTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "xAxisTitle", required: false }] }], yAxisTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "yAxisTitle", required: false }] }], unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: false }] }] } });
16085
16501
 
16086
16502
  /** Nhãn hiển thị + icon cho từng trạng thái. */
@@ -16235,11 +16651,11 @@ class OlJumbotron {
16235
16651
  }, /* @ts-ignore */
16236
16652
  ...(ngDevMode ? [{ debugName: "badgeClasses" }] : /* istanbul ignore next */ []));
16237
16653
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlJumbotron, deps: [], target: i0.ɵɵFactoryTarget.Component });
16238
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlJumbotron, isStandalone: true, selector: "ol-jumbotron", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, badgeText: { classPropertyName: "badgeText", publicName: "badgeText", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\u00F2n l\u1EA1i, \u0111o hex \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i r\u1ED3i m\u1EDBi quy\u1EBFt:\n dark:border-gray-800 #1e2939 = `border-default` -> \u0110\u00C3 THAY\n dark:bg-gray-900 #101828 kh\u00F4ng token n\u00E0o kh\u1EDBp -> gi\u1EEF\n dark:text-gray-300 #d1d5db kh\u00F4ng token n\u00E0o kh\u1EDBp -> gi\u1EEF\n-->\n<!-- Figma trang Jumbotron ch\u1EC9 \u0111\u1ECBnh ngh\u0129a bi\u1EBFn th\u1EC3 n\u1EC1n brand; c\u00E1c bi\u1EBFn th\u1EC3\n `gradient`, `dark`, `light` b\u00EAn d\u01B0\u1EDBi l\u00E0 m\u1EDF r\u1ED9ng NGO\u00C0I Figma n\u00EAn gi\u1EEF\n b\u1EADc s\u1ED1 cho t\u1EDBi khi thi\u1EBFt k\u1EBF \u0111\u1ECBnh ngh\u0129a. -->\n<div\n class=\"relative overflow-hidden rounded-3xl p-6 sm:p-8 md:p-12 border shadow-xl\"\n [class.bg-linear-to-tr]=\"variant() === 'gradient'\"\n [class.from-brand-950]=\"variant() === 'gradient'\"\n [class.via-brand-800]=\"variant() === 'gradient'\"\n [class.to-indigo-900]=\"variant() === 'gradient'\"\n [class.text-white]=\"variant() === 'gradient' || variant() === 'brand' || variant() === 'dark'\"\n [class.border-brand/50]=\"variant() === 'gradient'\"\n [class.bg-brand]=\"variant() === 'brand'\"\n [class.border-brand-subtle]=\"variant() === 'brand'\"\n [class.bg-gray-900]=\"variant() === 'dark'\"\n [class.border-gray-800]=\"variant() === 'dark'\"\n [class.bg-neutral-secondary]=\"variant() === 'light'\"\n [class.dark:bg-gray-900]=\"variant() === 'light'\"\n [class.border-default]=\"variant() === 'light'\"\n [class.dark:border-default]=\"variant() === 'light'\"\n [class.text-heading]=\"variant() === 'light'\"\n [class.dark:text-white]=\"variant() === 'light'\"\n>\n <div class=\"relative z-10 max-w-3xl space-y-4\">\n <!-- Optional Badge Slot or text -->\n @if (badgeText()) {\n <div [class]=\"badgeClasses()\">\n <ng-content select=\"[slot=badge-icon]\"></ng-content>\n <span>{{ badgeText() }}</span>\n </div>\n }\n\n <!-- Title -->\n @if (title()) {\n @switch (headingLevel()) {\n @case (2) {\n <h2 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h2>\n }\n @case (3) {\n <h3 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h3>\n }\n @case (4) {\n <h4 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h4>\n }\n @case (5) {\n <h5 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h5>\n }\n @case (6) {\n <h6 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h6>\n }\n @default {\n <h1 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h1>\n }\n }\n }\n\n <!--\n Subtitle / Description\n\n Bi\u1EBFn th\u1EC3 n\u1EC1n BRAND d\u00F9ng `text-fg-white` + `opacity-90`, KH\u00D4NG\n `text-fg-brand-soft`. T\u00E1ch hai class ch\u1EE9 kh\u00F4ng vi\u1EBFt `text-fg-white/90`:\n c\u00FA ph\u00E1p opacity k\u00E9p kh\u00F4ng sinh CSS \u1EDF d\u1EF1 \u00E1n n\u00E0y, class v\u1EABn n\u1EB1m trong\n `classList` m\u00E0 ph\u1EA7n t\u1EED m\u1EA5t m\u00E0u.\n\n `--text-brand-soft` l\u1EADt theo n\u1EC1n TRANG (brand-100 \u1EDF s\u00E1ng, brand-900 \u1EDF\n t\u1ED1i), nh\u01B0ng jumbotron t\u1EF1 t\u00F4 gradient brand \u0111\u1EADm \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 l\u1EADt\n xong th\u00E0nh xanh \u0111\u1EADm tr\u00EAn xanh \u0111\u1EADm. \u0110o /jumbotron: s\u00E1ng 1,20 \u00B7 t\u1ED1i 1,29,\n \u1EA3nh ch\u1EE5p cho th\u1EA5y d\u00F2ng m\u00F4 t\u1EA3 ch\u00ECm h\u1EB3n (BUG-035).\n\n N\u1EC1n gradient n\u00EAn m\u1ECDi ph\u00E9p \u0111o t\u1EF1 \u0111\u1ED9ng \u0111\u1EC1u sai \u2014 ph\u1EA3i x\u00E1c nh\u1EADn b\u1EB1ng \u1EA3nh.\n\n Bi\u1EBFn th\u1EC3 `light` gi\u1EEF nguy\u00EAn: n\u00F3 \u0111i theo n\u1EC1n trang n\u00EAn `dark:` l\u00E0 \u0111\u00FAng.\n -->\n @if (subtitle()) {\n <p\n class=\"text-sm md:text-base max-w-2xl\"\n [class.text-fg-white]=\"variant() !== 'light'\"\n [class.opacity-90]=\"variant() !== 'light'\"\n [class.text-body]=\"variant() === 'light'\"\n [class.dark:text-gray-300]=\"variant() === 'light'\"\n >\n {{ subtitle() }}\n </p>\n }\n\n <!-- Content / CTA Slot -->\n <div class=\"pt-2 flex flex-wrap items-center gap-3\">\n <ng-content select=\"[slot=actions]\"></ng-content>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
16654
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlJumbotron, isStandalone: true, selector: "ol-jumbotron", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, badgeText: { classPropertyName: "badgeText", publicName: "badgeText", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\u0169, \u0110\u00C3 G\u1EE0 H\u1EBET (2026-09-24). Ch\u00FA th\u00EDch tr\u01B0\u1EDBc \u0111\u00E2y ghi\n \"kh\u00F4ng token n\u00E0o kh\u1EDBp\" l\u00E0 SAI \u2014 ph\u00E9p \u0111o c\u0169 \u0111\u1ECDc hex t\u1EEB file CSS v\u00E0 l\u1EA5y nh\u1EA7m\n l\u1EA7n khai trong kh\u1ED1i `.dark`, n\u00EAn token n\u00E0o c\u0169ng ra gi\u00E1 tr\u1ECB ch\u1EBF \u0111\u1ED9 t\u1ED1i.\n\n \u0110o l\u1EA1i tr\u00EAn Chrome, \u0111\u1ECDc `getComputedStyle` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 (\u0111\u1ED1i ch\u1EE9ng d\u01B0\u01A1ng:\n `--text-heading` #101828 -> #ffffff), ba token n\u00E0y T\u1EF0 \u0110\u1ED4I theo `.dark`:\n\n --bg-secondary #f9fafb -> #030712\n --text-body #4a5565 -> #d1d5db (tr\u00F9ng kh\u00EDt gray-300 c\u0169)\n --bg-primary-soft #ffffff -> #101828 (tr\u00F9ng kh\u00EDt gray-900 c\u0169)\n\n N\u00EAn `dark:bg-gray-900` v\u00E0 `dark:text-gray-300` ch\u1EC9 \u0110\u00C8 L\u00CAN token \u0111\u00E3 \u0111\u00FAng:\n nh\u00ECn kh\u00F4ng kh\u00E1c g\u00EC, nh\u01B0ng token \u0111\u1ED5i l\u00E0 h\u1ECFng ngay m\u00E0 kh\u00F4ng ai th\u1EA5y.\n-->\n<!-- Figma trang Jumbotron ch\u1EC9 \u0111\u1ECBnh ngh\u0129a bi\u1EBFn th\u1EC3 n\u1EC1n brand; c\u00E1c bi\u1EBFn th\u1EC3\n `gradient`, `dark`, `light` b\u00EAn d\u01B0\u1EDBi l\u00E0 m\u1EDF r\u1ED9ng NGO\u00C0I Figma n\u00EAn gi\u1EEF\n b\u1EADc s\u1ED1 cho t\u1EDBi khi thi\u1EBFt k\u1EBF \u0111\u1ECBnh ngh\u0129a. -->\n<div\n class=\"relative overflow-hidden rounded-3xl p-6 sm:p-8 md:p-12 border shadow-xl\"\n [class.bg-linear-to-tr]=\"variant() === 'gradient'\"\n [class.from-brand-950]=\"variant() === 'gradient'\"\n [class.via-brand-800]=\"variant() === 'gradient'\"\n [class.to-indigo-900]=\"variant() === 'gradient'\"\n [class.text-white]=\"variant() === 'gradient' || variant() === 'brand' || variant() === 'dark'\"\n [class.border-brand/50]=\"variant() === 'gradient'\"\n [class.bg-brand]=\"variant() === 'brand'\"\n [class.border-brand-subtle]=\"variant() === 'brand'\"\n [class.bg-gray-900]=\"variant() === 'dark'\"\n [class.border-gray-800]=\"variant() === 'dark'\"\n [class.bg-neutral-secondary]=\"variant() === 'light'\"\n [class.border-default]=\"variant() === 'light'\"\n [class.dark:border-default]=\"variant() === 'light'\"\n [class.text-heading]=\"variant() === 'light'\"\n [class.dark:text-white]=\"variant() === 'light'\"\n>\n <div class=\"relative z-10 max-w-3xl space-y-4\">\n <!-- Optional Badge Slot or text -->\n @if (badgeText()) {\n <div [class]=\"badgeClasses()\">\n <ng-content select=\"[slot=badge-icon]\"></ng-content>\n <span>{{ badgeText() }}</span>\n </div>\n }\n\n <!-- Title -->\n @if (title()) {\n @switch (headingLevel()) {\n @case (2) {\n <h2 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h2>\n }\n @case (3) {\n <h3 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h3>\n }\n @case (4) {\n <h4 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h4>\n }\n @case (5) {\n <h5 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h5>\n }\n @case (6) {\n <h6 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h6>\n }\n @default {\n <h1 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h1>\n }\n }\n }\n\n <!--\n Subtitle / Description\n\n Bi\u1EBFn th\u1EC3 n\u1EC1n BRAND d\u00F9ng `text-fg-white` + `opacity-90`, KH\u00D4NG\n `text-fg-brand-soft`. T\u00E1ch hai class ch\u1EE9 kh\u00F4ng vi\u1EBFt `text-fg-white/90`:\n c\u00FA ph\u00E1p opacity k\u00E9p kh\u00F4ng sinh CSS \u1EDF d\u1EF1 \u00E1n n\u00E0y, class v\u1EABn n\u1EB1m trong\n `classList` m\u00E0 ph\u1EA7n t\u1EED m\u1EA5t m\u00E0u.\n\n `--text-brand-soft` l\u1EADt theo n\u1EC1n TRANG (brand-100 \u1EDF s\u00E1ng, brand-900 \u1EDF\n t\u1ED1i), nh\u01B0ng jumbotron t\u1EF1 t\u00F4 gradient brand \u0111\u1EADm \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 l\u1EADt\n xong th\u00E0nh xanh \u0111\u1EADm tr\u00EAn xanh \u0111\u1EADm. \u0110o /jumbotron: s\u00E1ng 1,20 \u00B7 t\u1ED1i 1,29,\n \u1EA3nh ch\u1EE5p cho th\u1EA5y d\u00F2ng m\u00F4 t\u1EA3 ch\u00ECm h\u1EB3n (BUG-035).\n\n N\u1EC1n gradient n\u00EAn m\u1ECDi ph\u00E9p \u0111o t\u1EF1 \u0111\u1ED9ng \u0111\u1EC1u sai \u2014 ph\u1EA3i x\u00E1c nh\u1EADn b\u1EB1ng \u1EA3nh.\n\n Bi\u1EBFn th\u1EC3 `light` gi\u1EEF nguy\u00EAn: n\u00F3 \u0111i theo n\u1EC1n trang n\u00EAn `dark:` l\u00E0 \u0111\u00FAng.\n -->\n @if (subtitle()) {\n <p\n class=\"text-sm md:text-base max-w-2xl\"\n [class.text-fg-white]=\"variant() !== 'light'\"\n [class.opacity-90]=\"variant() !== 'light'\"\n [class.text-body]=\"variant() === 'light'\"\n >\n {{ subtitle() }}\n </p>\n }\n\n <!-- Content / CTA Slot -->\n <div class=\"pt-2 flex flex-wrap items-center gap-3\">\n <ng-content select=\"[slot=actions]\"></ng-content>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
16239
16655
  }
16240
16656
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlJumbotron, decorators: [{
16241
16657
  type: Component,
16242
- args: [{ selector: 'ol-jumbotron', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\u00F2n l\u1EA1i, \u0111o hex \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i r\u1ED3i m\u1EDBi quy\u1EBFt:\n dark:border-gray-800 #1e2939 = `border-default` -> \u0110\u00C3 THAY\n dark:bg-gray-900 #101828 kh\u00F4ng token n\u00E0o kh\u1EDBp -> gi\u1EEF\n dark:text-gray-300 #d1d5db kh\u00F4ng token n\u00E0o kh\u1EDBp -> gi\u1EEF\n-->\n<!-- Figma trang Jumbotron ch\u1EC9 \u0111\u1ECBnh ngh\u0129a bi\u1EBFn th\u1EC3 n\u1EC1n brand; c\u00E1c bi\u1EBFn th\u1EC3\n `gradient`, `dark`, `light` b\u00EAn d\u01B0\u1EDBi l\u00E0 m\u1EDF r\u1ED9ng NGO\u00C0I Figma n\u00EAn gi\u1EEF\n b\u1EADc s\u1ED1 cho t\u1EDBi khi thi\u1EBFt k\u1EBF \u0111\u1ECBnh ngh\u0129a. -->\n<div\n class=\"relative overflow-hidden rounded-3xl p-6 sm:p-8 md:p-12 border shadow-xl\"\n [class.bg-linear-to-tr]=\"variant() === 'gradient'\"\n [class.from-brand-950]=\"variant() === 'gradient'\"\n [class.via-brand-800]=\"variant() === 'gradient'\"\n [class.to-indigo-900]=\"variant() === 'gradient'\"\n [class.text-white]=\"variant() === 'gradient' || variant() === 'brand' || variant() === 'dark'\"\n [class.border-brand/50]=\"variant() === 'gradient'\"\n [class.bg-brand]=\"variant() === 'brand'\"\n [class.border-brand-subtle]=\"variant() === 'brand'\"\n [class.bg-gray-900]=\"variant() === 'dark'\"\n [class.border-gray-800]=\"variant() === 'dark'\"\n [class.bg-neutral-secondary]=\"variant() === 'light'\"\n [class.dark:bg-gray-900]=\"variant() === 'light'\"\n [class.border-default]=\"variant() === 'light'\"\n [class.dark:border-default]=\"variant() === 'light'\"\n [class.text-heading]=\"variant() === 'light'\"\n [class.dark:text-white]=\"variant() === 'light'\"\n>\n <div class=\"relative z-10 max-w-3xl space-y-4\">\n <!-- Optional Badge Slot or text -->\n @if (badgeText()) {\n <div [class]=\"badgeClasses()\">\n <ng-content select=\"[slot=badge-icon]\"></ng-content>\n <span>{{ badgeText() }}</span>\n </div>\n }\n\n <!-- Title -->\n @if (title()) {\n @switch (headingLevel()) {\n @case (2) {\n <h2 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h2>\n }\n @case (3) {\n <h3 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h3>\n }\n @case (4) {\n <h4 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h4>\n }\n @case (5) {\n <h5 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h5>\n }\n @case (6) {\n <h6 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h6>\n }\n @default {\n <h1 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h1>\n }\n }\n }\n\n <!--\n Subtitle / Description\n\n Bi\u1EBFn th\u1EC3 n\u1EC1n BRAND d\u00F9ng `text-fg-white` + `opacity-90`, KH\u00D4NG\n `text-fg-brand-soft`. T\u00E1ch hai class ch\u1EE9 kh\u00F4ng vi\u1EBFt `text-fg-white/90`:\n c\u00FA ph\u00E1p opacity k\u00E9p kh\u00F4ng sinh CSS \u1EDF d\u1EF1 \u00E1n n\u00E0y, class v\u1EABn n\u1EB1m trong\n `classList` m\u00E0 ph\u1EA7n t\u1EED m\u1EA5t m\u00E0u.\n\n `--text-brand-soft` l\u1EADt theo n\u1EC1n TRANG (brand-100 \u1EDF s\u00E1ng, brand-900 \u1EDF\n t\u1ED1i), nh\u01B0ng jumbotron t\u1EF1 t\u00F4 gradient brand \u0111\u1EADm \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 l\u1EADt\n xong th\u00E0nh xanh \u0111\u1EADm tr\u00EAn xanh \u0111\u1EADm. \u0110o /jumbotron: s\u00E1ng 1,20 \u00B7 t\u1ED1i 1,29,\n \u1EA3nh ch\u1EE5p cho th\u1EA5y d\u00F2ng m\u00F4 t\u1EA3 ch\u00ECm h\u1EB3n (BUG-035).\n\n N\u1EC1n gradient n\u00EAn m\u1ECDi ph\u00E9p \u0111o t\u1EF1 \u0111\u1ED9ng \u0111\u1EC1u sai \u2014 ph\u1EA3i x\u00E1c nh\u1EADn b\u1EB1ng \u1EA3nh.\n\n Bi\u1EBFn th\u1EC3 `light` gi\u1EEF nguy\u00EAn: n\u00F3 \u0111i theo n\u1EC1n trang n\u00EAn `dark:` l\u00E0 \u0111\u00FAng.\n -->\n @if (subtitle()) {\n <p\n class=\"text-sm md:text-base max-w-2xl\"\n [class.text-fg-white]=\"variant() !== 'light'\"\n [class.opacity-90]=\"variant() !== 'light'\"\n [class.text-body]=\"variant() === 'light'\"\n [class.dark:text-gray-300]=\"variant() === 'light'\"\n >\n {{ subtitle() }}\n </p>\n }\n\n <!-- Content / CTA Slot -->\n <div class=\"pt-2 flex flex-wrap items-center gap-3\">\n <ng-content select=\"[slot=actions]\"></ng-content>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n" }]
16658
+ args: [{ selector: 'ol-jumbotron', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\u0169, \u0110\u00C3 G\u1EE0 H\u1EBET (2026-09-24). Ch\u00FA th\u00EDch tr\u01B0\u1EDBc \u0111\u00E2y ghi\n \"kh\u00F4ng token n\u00E0o kh\u1EDBp\" l\u00E0 SAI \u2014 ph\u00E9p \u0111o c\u0169 \u0111\u1ECDc hex t\u1EEB file CSS v\u00E0 l\u1EA5y nh\u1EA7m\n l\u1EA7n khai trong kh\u1ED1i `.dark`, n\u00EAn token n\u00E0o c\u0169ng ra gi\u00E1 tr\u1ECB ch\u1EBF \u0111\u1ED9 t\u1ED1i.\n\n \u0110o l\u1EA1i tr\u00EAn Chrome, \u0111\u1ECDc `getComputedStyle` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 (\u0111\u1ED1i ch\u1EE9ng d\u01B0\u01A1ng:\n `--text-heading` #101828 -> #ffffff), ba token n\u00E0y T\u1EF0 \u0110\u1ED4I theo `.dark`:\n\n --bg-secondary #f9fafb -> #030712\n --text-body #4a5565 -> #d1d5db (tr\u00F9ng kh\u00EDt gray-300 c\u0169)\n --bg-primary-soft #ffffff -> #101828 (tr\u00F9ng kh\u00EDt gray-900 c\u0169)\n\n N\u00EAn `dark:bg-gray-900` v\u00E0 `dark:text-gray-300` ch\u1EC9 \u0110\u00C8 L\u00CAN token \u0111\u00E3 \u0111\u00FAng:\n nh\u00ECn kh\u00F4ng kh\u00E1c g\u00EC, nh\u01B0ng token \u0111\u1ED5i l\u00E0 h\u1ECFng ngay m\u00E0 kh\u00F4ng ai th\u1EA5y.\n-->\n<!-- Figma trang Jumbotron ch\u1EC9 \u0111\u1ECBnh ngh\u0129a bi\u1EBFn th\u1EC3 n\u1EC1n brand; c\u00E1c bi\u1EBFn th\u1EC3\n `gradient`, `dark`, `light` b\u00EAn d\u01B0\u1EDBi l\u00E0 m\u1EDF r\u1ED9ng NGO\u00C0I Figma n\u00EAn gi\u1EEF\n b\u1EADc s\u1ED1 cho t\u1EDBi khi thi\u1EBFt k\u1EBF \u0111\u1ECBnh ngh\u0129a. -->\n<div\n class=\"relative overflow-hidden rounded-3xl p-6 sm:p-8 md:p-12 border shadow-xl\"\n [class.bg-linear-to-tr]=\"variant() === 'gradient'\"\n [class.from-brand-950]=\"variant() === 'gradient'\"\n [class.via-brand-800]=\"variant() === 'gradient'\"\n [class.to-indigo-900]=\"variant() === 'gradient'\"\n [class.text-white]=\"variant() === 'gradient' || variant() === 'brand' || variant() === 'dark'\"\n [class.border-brand/50]=\"variant() === 'gradient'\"\n [class.bg-brand]=\"variant() === 'brand'\"\n [class.border-brand-subtle]=\"variant() === 'brand'\"\n [class.bg-gray-900]=\"variant() === 'dark'\"\n [class.border-gray-800]=\"variant() === 'dark'\"\n [class.bg-neutral-secondary]=\"variant() === 'light'\"\n [class.border-default]=\"variant() === 'light'\"\n [class.dark:border-default]=\"variant() === 'light'\"\n [class.text-heading]=\"variant() === 'light'\"\n [class.dark:text-white]=\"variant() === 'light'\"\n>\n <div class=\"relative z-10 max-w-3xl space-y-4\">\n <!-- Optional Badge Slot or text -->\n @if (badgeText()) {\n <div [class]=\"badgeClasses()\">\n <ng-content select=\"[slot=badge-icon]\"></ng-content>\n <span>{{ badgeText() }}</span>\n </div>\n }\n\n <!-- Title -->\n @if (title()) {\n @switch (headingLevel()) {\n @case (2) {\n <h2 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h2>\n }\n @case (3) {\n <h3 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h3>\n }\n @case (4) {\n <h4 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h4>\n }\n @case (5) {\n <h5 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h5>\n }\n @case (6) {\n <h6 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h6>\n }\n @default {\n <h1 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h1>\n }\n }\n }\n\n <!--\n Subtitle / Description\n\n Bi\u1EBFn th\u1EC3 n\u1EC1n BRAND d\u00F9ng `text-fg-white` + `opacity-90`, KH\u00D4NG\n `text-fg-brand-soft`. T\u00E1ch hai class ch\u1EE9 kh\u00F4ng vi\u1EBFt `text-fg-white/90`:\n c\u00FA ph\u00E1p opacity k\u00E9p kh\u00F4ng sinh CSS \u1EDF d\u1EF1 \u00E1n n\u00E0y, class v\u1EABn n\u1EB1m trong\n `classList` m\u00E0 ph\u1EA7n t\u1EED m\u1EA5t m\u00E0u.\n\n `--text-brand-soft` l\u1EADt theo n\u1EC1n TRANG (brand-100 \u1EDF s\u00E1ng, brand-900 \u1EDF\n t\u1ED1i), nh\u01B0ng jumbotron t\u1EF1 t\u00F4 gradient brand \u0111\u1EADm \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 l\u1EADt\n xong th\u00E0nh xanh \u0111\u1EADm tr\u00EAn xanh \u0111\u1EADm. \u0110o /jumbotron: s\u00E1ng 1,20 \u00B7 t\u1ED1i 1,29,\n \u1EA3nh ch\u1EE5p cho th\u1EA5y d\u00F2ng m\u00F4 t\u1EA3 ch\u00ECm h\u1EB3n (BUG-035).\n\n N\u1EC1n gradient n\u00EAn m\u1ECDi ph\u00E9p \u0111o t\u1EF1 \u0111\u1ED9ng \u0111\u1EC1u sai \u2014 ph\u1EA3i x\u00E1c nh\u1EADn b\u1EB1ng \u1EA3nh.\n\n Bi\u1EBFn th\u1EC3 `light` gi\u1EEF nguy\u00EAn: n\u00F3 \u0111i theo n\u1EC1n trang n\u00EAn `dark:` l\u00E0 \u0111\u00FAng.\n -->\n @if (subtitle()) {\n <p\n class=\"text-sm md:text-base max-w-2xl\"\n [class.text-fg-white]=\"variant() !== 'light'\"\n [class.opacity-90]=\"variant() !== 'light'\"\n [class.text-body]=\"variant() === 'light'\"\n >\n {{ subtitle() }}\n </p>\n }\n\n <!-- Content / CTA Slot -->\n <div class=\"pt-2 flex flex-wrap items-center gap-3\">\n <ng-content select=\"[slot=actions]\"></ng-content>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n" }]
16243
16659
  }], propDecorators: { headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], badgeText: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeText", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
16244
16660
 
16245
16661
  /**
@@ -16331,5 +16747,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
16331
16747
  * Generated bundle index. Do not edit.
16332
16748
  */
16333
16749
 
16334
- export { CHAT_BUBBLE_STATUS, MONTHS_FULL_EN, MONTHS_FULL_VI, MONTHS_SHORT_VI, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppNav, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, OlEmpty, OlFileInput, OlFloatingLabel, OlFocusTrapService, OlFooter, OlGallery, OlGalleryItem, OlHelper, OlIcon, OlIconService, OlIconShape, OlInput, OlInputCounter, OlJumbotron, OlKbd, OlLabel, OlListGroup, OlListGroupItem, OlLoadingOverlay, OlModal, OlModalFooter, OlModalHeader, OlMonthView, OlNavDropdown, OlNavbar, OlNavbarItem, OlOtpInput, OlOverlayContainer, OlPagination, OlPopover, OlPopoverContent, OlPositioningService, OlProgress, OlProgressBarService, OlProgressBarTop, OlRadio, OlRange, OlRating, OlRichTextEditor, OlScrollArea, OlSelect, OlSeparator, OlSidebar, OlSidebarGroup, OlSidebarItem, OlSidebarSubmenu, OlSkeleton, OlSpeedDial, OlSpeedDialItem, OlSpinner, OlStepItem, OlStepper, OlTabItem, OlTable, OlTableLoading, OlTableRow, OlTabs, OlTagInput, OlTextarea, OlTimeline, OlTimelineItem, OlTimepicker, OlToast, OlToastContainer, OlToastService, OlToggle, OlToggleGroup, OlToggleInput, OlTooltip, OlTooltipDirective, OlTree, OlTreeNodeItem, OlYearView, WEEKDAYS_SHORT_EN, WEEKDAYS_SHORT_VI, boDau, cloneDate, endOfDay, formatDate, generateMonthMatrix, generateMonthsMatrix, generateYearsMatrix, isBetween, isDateDisabled, isSameDay, isSameMonth, isSameYear, isToday, parseDate, startOfDay, stripDiacritics };
16750
+ export { CHAT_BUBBLE_STATUS, FIELD_ASTERISK, FIELD_DISABLED, FIELD_HEIGHT, FIELD_HELPER, FIELD_HELPER_GAP, FIELD_LABEL, FIELD_LABEL_GAP, FIELD_MIN_HEIGHT, FIELD_PADDING_X, FIELD_RADIUS, FIELD_STATE, FIELD_TEXT, MONTHS_FULL_EN, MONTHS_FULL_VI, MONTHS_SHORT_VI, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppNav, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, OlEmpty, OlFileInput, OlFloatingLabel, OlFocusTrapService, OlFooter, OlGallery, OlGalleryItem, OlHelper, OlIcon, OlIconService, OlIconShape, OlInput, OlInputCounter, OlJumbotron, OlKbd, OlLabel, OlListGroup, OlListGroupItem, OlLoadingOverlay, OlModal, OlModalFooter, OlModalHeader, OlMonthView, OlNavDropdown, OlNavbar, OlNavbarItem, OlOtpInput, OlOverlayContainer, OlPagination, OlPopover, OlPopoverContent, OlPositioningService, OlProgress, OlProgressBarService, OlProgressBarTop, OlRadio, OlRange, OlRating, OlRichTextEditor, OlScrollArea, OlSelect, OlSeparator, OlSidebar, OlSidebarGroup, OlSidebarItem, OlSidebarSubmenu, OlSkeleton, OlSpeedDial, OlSpeedDialItem, OlSpinner, OlStepItem, OlStepper, OlTabItem, OlTable, OlTableLoading, OlTableRow, OlTabs, OlTagInput, OlTextarea, OlTimeline, OlTimelineItem, OlTimepicker, OlToast, OlToastContainer, OlToastService, OlToggle, OlToggleGroup, OlToggleInput, OlTooltip, OlTooltipDirective, OlTree, OlTreeNodeItem, OlYearView, WEEKDAYS_SHORT_EN, WEEKDAYS_SHORT_VI, boDau, cloneDate, endOfDay, fieldSizeClasses, formatDate, generateMonthMatrix, generateMonthsMatrix, generateYearsMatrix, isBetween, isDateDisabled, isSameDay, isSameMonth, isSameYear, isToday, parseDate, startOfDay, stripDiacritics };
16335
16751
  //# sourceMappingURL=edmicro-olkit.mjs.map