@edmicro/olkit 1.1.3 → 1.1.4

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)
1574
1578
  return;
1575
- this.doc.removeEventListener('keydown', this.escapeListener, true);
1576
- this.escapeListener = null;
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)
1616
+ return;
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
@@ -2753,6 +2801,200 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
2753
2801
  }, 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
2802
  }], 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
2803
 
2804
+ /**
2805
+ * Tiêu đề Modal — khớp Figma `ol-modal-header` (node 1149:71403).
2806
+ * Figma có 7 biến thể: Default, Heading & supporting text, Icon + Heading &
2807
+ * helper text, With Logo, Logo & icons, Avatar group, Breadcrumb.
2808
+ * Component lo phần khung dùng chung; nội dung giàu hơn lắp qua <ng-content>.
2809
+ */
2810
+ /*
2811
+ * GHÉP VỚI `ol-modal`: lắp thẳng vào, KHÔNG phải tắt gì cả.
2812
+ *
2813
+ * `ol-modal` dùng `contentChild(OlModalHeader)` để tự nhận ra có header lồng
2814
+ * vào rồi thôi không dựng phần đầu mặc định (xem `showDefaultHeader`).
2815
+ *
2816
+ * ```html
2817
+ * <ol-modal [(isOpen)]="open">
2818
+ * <ol-modal-header heading="Tiêu đề" (closeClick)="open.set(false)" />
2819
+ * …nội dung…
2820
+ * </ol-modal>
2821
+ * ```
2822
+ *
2823
+ * Trước đây phải tự tắt bằng `title=""` kèm `[dismissible]="false"`, quên một
2824
+ * trong hai là lồng HAI khung header chồng nhau và nút đóng hiện hai lần —
2825
+ * build vẫn xanh nên chỉ nhìn mới thấy.
2826
+ *
2827
+ * Nút đóng: header này có `closeClick` riêng, người dùng tự nối. `ol-modal`
2828
+ * không chen nút vào header lồng ngoài.
2829
+ */
2830
+ class OlModalHeader {
2831
+ /**
2832
+ * Bậc thẻ tiêu đề (h1–h6). Mặc định 3 — giữ nguyên bậc đang chạy.
2833
+ *
2834
+ * Khoá cứng một bậc thì nơi gọi không sửa được: đặt component này sau
2835
+ * `<h2>` của trang là nhảy bậc.
2836
+ */
2837
+ headingLevel = input(3, /* @ts-ignore */
2838
+ ...(ngDevMode ? [{ debugName: "headingLevel" }] : /* istanbul ignore next */ []));
2839
+ heading = input('', /* @ts-ignore */
2840
+ ...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
2841
+ /**
2842
+ * Icon đầu tiêu đề. Rỗng thì KHÔNG dựng khung — không để lại thẻ trống.
2843
+ *
2844
+ * Figma `Type=Icon + Heading & helper text` (#1149:71412) vẽ khung 48px bo
2845
+ * 12px, icon 28px bên trong. Đó đúng là `ol-icon-shape` size `lg`, nên dùng
2846
+ * lại thay vì tự vẽ khung.
2847
+ *
2848
+ * `color="white"` chứ KHÔNG `gray`: Figma bind nền
2849
+ * `bg-neutral-primary-medium` (trắng) kèm viền `border-default-medium`, mà
2850
+ * trong `ol-icon-shape` chỉ nhánh `white` có cặp đó — `gray` ra nền xám đặc
2851
+ * và KHÔNG viền. Đo /modal lượt đầu: nền rgb(243,244,246), viền 0px.
2852
+ */
2853
+ icon = input('', /* @ts-ignore */
2854
+ ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
2855
+ /**
2856
+ * Mô tả phụ dưới tiêu đề.
2857
+ *
2858
+ * Tên `description` theo `ol-alert-dialog` — thứ dựng trên chính `ol-modal`
2859
+ * — và `ol-empty`. `helperText` mang nghĩa "chú thích dưới ô nhập" ở họ form
2860
+ * (16 component dùng), đặt cho tiêu đề hộp thoại là lệch nghĩa.
2861
+ */
2862
+ description = input('', /* @ts-ignore */
2863
+ ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
2864
+ /** @deprecated Dùng `description`. Giữ để không phá mã đang chạy. */
2865
+ supportingText = input('', /* @ts-ignore */
2866
+ ...(ngDevMode ? [{ debugName: "supportingText" }] : /* istanbul ignore next */ []));
2867
+ /** @deprecated Dùng `description`. Tên cũ của `ol-drawer-header`. */
2868
+ helperText = input('', /* @ts-ignore */
2869
+ ...(ngDevMode ? [{ debugName: "helperText" }] : /* istanbul ignore next */ []));
2870
+ /**
2871
+ * Cái nào được TRUYỀN thì thắng — không phụ thuộc giá trị.
2872
+ *
2873
+ * `ol-badge` từng hoà giải hai input cùng nghĩa bằng cách so với giá trị mặc
2874
+ * định (`theme` thắng trừ khi nó bằng `'gray'`), nên truyền cả hai thì một
2875
+ * 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
2876
+ * cái đầu tiên khác rỗng.
2877
+ */
2878
+ descriptionText = computed(() => this.description() || this.supportingText() || this.helperText(), /* @ts-ignore */
2879
+ ...(ngDevMode ? [{ debugName: "descriptionText" }] : /* istanbul ignore next */ []));
2880
+ showClose = input(true, /* @ts-ignore */
2881
+ ...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
2882
+ closeClick = output();
2883
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
2884
+ 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: `
2885
+ <!--
2886
+ PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
2887
+ kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
2888
+ và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
2889
+ CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
2890
+ -->
2891
+ <div class="flex items-start justify-between gap-3 p-3">
2892
+ @if (icon()) {
2893
+ <ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
2894
+ }
2895
+ <div class="min-w-0 flex-1">
2896
+ @if (heading()) {
2897
+ @switch (headingLevel()) {
2898
+ @case (1) {
2899
+ <h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
2900
+ }
2901
+ @case (2) {
2902
+ <h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
2903
+ }
2904
+ @case (4) {
2905
+ <h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
2906
+ }
2907
+ @case (5) {
2908
+ <h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
2909
+ }
2910
+ @case (6) {
2911
+ <h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
2912
+ }
2913
+ @default {
2914
+ <h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
2915
+ }
2916
+ }
2917
+ }
2918
+ @if (descriptionText()) {
2919
+ <p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
2920
+ }
2921
+ <ng-content />
2922
+ </div>
2923
+ @if (showClose()) {
2924
+ <button
2925
+ type="button"
2926
+ (click)="closeClick.emit()"
2927
+ aria-label="Đóng hộp thoại"
2928
+ 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"
2929
+ >
2930
+ <ol-icon name="close" size="sm" />
2931
+ </button>
2932
+ }
2933
+ </div>
2934
+ `, 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 });
2935
+ }
2936
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, decorators: [{
2937
+ type: Component,
2938
+ args: [{
2939
+ selector: 'ol-modal-header',
2940
+ standalone: true,
2941
+ imports: [OlIcon, OlIconShape],
2942
+ template: `
2943
+ <!--
2944
+ PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
2945
+ kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
2946
+ và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
2947
+ CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
2948
+ -->
2949
+ <div class="flex items-start justify-between gap-3 p-3">
2950
+ @if (icon()) {
2951
+ <ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
2952
+ }
2953
+ <div class="min-w-0 flex-1">
2954
+ @if (heading()) {
2955
+ @switch (headingLevel()) {
2956
+ @case (1) {
2957
+ <h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
2958
+ }
2959
+ @case (2) {
2960
+ <h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
2961
+ }
2962
+ @case (4) {
2963
+ <h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
2964
+ }
2965
+ @case (5) {
2966
+ <h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
2967
+ }
2968
+ @case (6) {
2969
+ <h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
2970
+ }
2971
+ @default {
2972
+ <h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
2973
+ }
2974
+ }
2975
+ }
2976
+ @if (descriptionText()) {
2977
+ <p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
2978
+ }
2979
+ <ng-content />
2980
+ </div>
2981
+ @if (showClose()) {
2982
+ <button
2983
+ type="button"
2984
+ (click)="closeClick.emit()"
2985
+ aria-label="Đóng hộp thoại"
2986
+ 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"
2987
+ >
2988
+ <ol-icon name="close" size="sm" />
2989
+ </button>
2990
+ }
2991
+ </div>
2992
+ `,
2993
+ changeDetection: ChangeDetectionStrategy.OnPush,
2994
+ host: { class: 'block' },
2995
+ }]
2996
+ }], 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"] }] } });
2997
+
2756
2998
  class OlModal {
2757
2999
  focusTrap = inject(OlFocusTrapService);
2758
3000
  destroyRef = inject(DestroyRef);
@@ -2781,10 +3023,81 @@ class OlModal {
2781
3023
  ...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
2782
3024
  title = input('', /* @ts-ignore */
2783
3025
  ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
3026
+ /**
3027
+ * Icon đứng trước tiêu đề ở phần đầu mặc định.
3028
+ *
3029
+ * Thêm 2026-09-24 cùng `description`. Trước đó `ol-modal` chỉ có `title`,
3030
+ * nên muốn có icon hay dòng mô tả là phải lắp `ol-modal-header` thủ công —
3031
+ * và lắp rồi thì `title` bị bỏ qua, phải khai lại qua `heading`. Đo trên
3032
+ * /modal: 13 chỗ dùng modal, chỗ nào cần icon đều lặp lại đúng khuôn đó.
3033
+ *
3034
+ * Vẫn dùng `ol-modal-header` được khi cần thứ phần đầu mặc định không có
3035
+ * (`supportingText`, `helperText`, nút tuỳ biến); lắp nó vào thì phần đầu
3036
+ * mặc định tự tắt, xem `showDefaultHeader`.
3037
+ */
3038
+ icon = input('', /* @ts-ignore */
3039
+ ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
3040
+ /** Dòng mô tả dưới tiêu đề ở phần đầu mặc định. Xem chú thích ở `icon`. */
3041
+ description = input('', /* @ts-ignore */
3042
+ ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
2784
3043
  size = input('base', /* @ts-ignore */
2785
3044
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3045
+ /**
3046
+ * Cho phép đóng hộp thoại bằng Escape hoặc bấm ra nền mờ.
3047
+ *
3048
+ * Tắt khi người dùng BUỘC phải chọn một phương án — xác nhận xoá, đồng ý
3049
+ * điều khoản. Không còn ẩn nút X nữa: xem `showClose`.
3050
+ */
2786
3051
  dismissible = input(true, /* @ts-ignore */
2787
3052
  ...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
3053
+ /**
3054
+ * Có vẽ nút X ở góc phải phần đầu không. Mặc định **theo `dismissible`**.
3055
+ *
3056
+ * TÁCH KHỎI `dismissible` (2026-09-24). Trước đây một input gánh BỐN việc —
3057
+ * hiện nút X, đóng bằng Escape, đóng khi bấm nền, và quyết định có dựng
3058
+ * phần đầu — nên `[dismissible]="false"` (chỉ định chặn bấm-ra-ngoài) kéo
3059
+ * theo MẤT nút X. Người dùng phải tự lắp `ol-modal-header` để lấy lại nút,
3060
+ * tức phải khai lại cả tiêu đề vốn đã truyền qua `title`.
3061
+ *
3062
+ * Nay ba trường hợp tách bạch:
3063
+ *
3064
+ * `[dismissible]="false"` chặn Escape + nền mờ, VẪN có nút X
3065
+ * `[dismissible]="false" [showClose]="false"` không lối thoát nào ngoài nút trong nội dung
3066
+ * `[showClose]="false"` giấu nút X, vẫn đóng được bằng Escape/nền
3067
+ *
3068
+ * Mặc định `true` — nút X LUÔN hiện trừ khi tắt tường minh.
3069
+ *
3070
+ * ĐÂY LÀ THAY ĐỔI PHÁ VỠ so với bản trước: `[dismissible]="false"` vốn ẩn
3071
+ * 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
3072
+ * `[showClose]="false"`. Đổi vì hành vi cũ là LỖI chứ không phải lựa chọn:
3073
+ * người dùng chặn bấm-ra-ngoài rồi mất nút đóng, phải tự lắp
3074
+ * `ol-modal-header` để lấy lại — tức khai lại tiêu đề đã truyền qua `title`.
3075
+ */
3076
+ showClose = input(true, /* @ts-ignore */
3077
+ ...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
3078
+ /** Nút X có hiện không. Tách hẳn khỏi `dismissible` từ 2026-09-24. */
3079
+ closeVisible = computed(() => this.showClose(), /* @ts-ignore */
3080
+ ...(ngDevMode ? [{ debugName: "closeVisible" }] : /* istanbul ignore next */ []));
3081
+ /**
3082
+ * Có `<ol-modal-header>` lồng bên trong không.
3083
+ *
3084
+ * Dùng để KHÔNG dựng phần đầu mặc định khi người dùng đã tự lắp header
3085
+ * riêng. Trước đây phải tắt tay bằng `title=""` kèm `[dismissible]="false"`
3086
+ * — quên một trong hai là lồng HAI khung header chồng nhau, mà build vẫn
3087
+ * xanh nên chỉ nhìn mới thấy.
3088
+ *
3089
+ * `descendants: false` để chỉ bắt header là CON TRỰC TIẾP. Một `ol-modal`
3090
+ * lồng trong nội dung của modal này thì header của nó không tính.
3091
+ */
3092
+ projectedHeader = contentChild(OlModalHeader, { ...(ngDevMode ? { debugName: "projectedHeader" } : /* istanbul ignore next */ {}), descendants: false });
3093
+ /**
3094
+ * Có dựng phần đầu mặc định không.
3095
+ *
3096
+ * Người dùng lắp `ol-modal-header` riêng thì thôi — nếu không sẽ có hai
3097
+ * khung `p-3` chồng nhau, nút đóng hiện hai lần.
3098
+ */
3099
+ showDefaultHeader = computed(() => !this.projectedHeader() && (this.title() !== '' || this.closeVisible()), /* @ts-ignore */
3100
+ ...(ngDevMode ? [{ debugName: "showDefaultHeader" }] : /* istanbul ignore next */ []));
2788
3101
  backdrop = input('dynamic', /* @ts-ignore */
2789
3102
  ...(ngDevMode ? [{ debugName: "backdrop" }] : /* istanbul ignore next */ []));
2790
3103
  placement = input('center', /* @ts-ignore */
@@ -2891,12 +3204,12 @@ class OlModal {
2891
3204
  return this.isOpen();
2892
3205
  }
2893
3206
  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 });
3207
+ 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
3208
  }
2896
3209
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModal, decorators: [{
2897
3210
  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"] }] } });
3211
+ 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" }]
3212
+ }], 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
3213
 
2901
3214
  /**
2902
3215
  * Hộp thoại bắt buộc trả lời: xoá, huỷ, rời trang khi chưa lưu.
@@ -3008,12 +3321,22 @@ class OlAlertDialog {
3008
3321
  }
3009
3322
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAlertDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
3010
3323
  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: `
3324
+ <!--
3325
+ showClose=false là BẮT BUỘC từ 2026-09-24: showClose đã TÁCH khỏi
3326
+ dismissible, nên chỉ tắt dismissible thì nút X VẪN hiện. Hộp thoại này
3327
+ buộc người dùng chọn một trong hai nút, không có lối thoát thứ ba.
3328
+
3329
+ Hai bẫy đã mắc khi viết chú thích này, ghi lại: chú thích đặt XEN GIỮA
3330
+ danh sách thuộc tính làm hỏng build với NG1002; và dấu nháy ngược trong
3331
+ chú thích đóng template literal sớm, lỗi hiện ở dòng chẳng liên quan.
3332
+ -->
3011
3333
  <ol-modal
3012
3334
  [isOpen]="isOpen()"
3013
3335
  (isOpenChange)="isOpen.set($event)"
3014
3336
  [role]="'alertdialog'"
3015
3337
  size="sm"
3016
3338
  [dismissible]="false"
3339
+ [showClose]="false"
3017
3340
  backdrop="static"
3018
3341
  (closed)="closed.emit()"
3019
3342
  >
@@ -3038,7 +3361,7 @@ class OlAlertDialog {
3038
3361
  </ol-button>
3039
3362
  </div>
3040
3363
  </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 });
3364
+ `, 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
3365
  }
3043
3366
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAlertDialog, decorators: [{
3044
3367
  type: Component,
@@ -3047,12 +3370,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
3047
3370
  standalone: true,
3048
3371
  imports: [OlModal, OlButton, OlIconShape],
3049
3372
  template: `
3373
+ <!--
3374
+ showClose=false là BẮT BUỘC từ 2026-09-24: showClose đã TÁCH khỏi
3375
+ dismissible, nên chỉ tắt dismissible thì nút X VẪN hiện. Hộp thoại này
3376
+ buộc người dùng chọn một trong hai nút, không có lối thoát thứ ba.
3377
+
3378
+ Hai bẫy đã mắc khi viết chú thích này, ghi lại: chú thích đặt XEN GIỮA
3379
+ danh sách thuộc tính làm hỏng build với NG1002; và dấu nháy ngược trong
3380
+ chú thích đóng template literal sớm, lỗi hiện ở dòng chẳng liên quan.
3381
+ -->
3050
3382
  <ol-modal
3051
3383
  [isOpen]="isOpen()"
3052
3384
  (isOpenChange)="isOpen.set($event)"
3053
3385
  [role]="'alertdialog'"
3054
3386
  size="sm"
3055
3387
  [dismissible]="false"
3388
+ [showClose]="false"
3056
3389
  backdrop="static"
3057
3390
  (closed)="closed.emit()"
3058
3391
  >
@@ -3300,6 +3633,22 @@ class OlBadge {
3300
3633
  */
3301
3634
  color = input('gray', /* @ts-ignore */
3302
3635
  ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
3636
+ /**
3637
+ * Có vẽ viền không. Tắt thì giữ nguyên nền và màu chữ của `color`.
3638
+ *
3639
+ * 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ì
3640
+ * thêm giá trị vào `BadgeColor`: làm thành màu thì muốn "brand không viền"
3641
+ * và "danger không viền" phải nhân đôi cả bảng 12 màu thành 24.
3642
+ *
3643
+ * Khác `color="ghost"`: ghost bỏ CẢ nền lẫn viền và ép chữ về `text-heading`
3644
+ * — nó là một màu. `[bordered]="false"` chỉ bỏ viền.
3645
+ *
3646
+ * Class `border` VẪN giữ (đổi thành `border-transparent`) chứ không bỏ hẳn:
3647
+ * badge tính chiều cao theo hộp viền, bỏ đi là badge không viền thấp hơn
3648
+ * badge thường đúng 2px khi đặt cạnh nhau. Cùng lý do với `ghost`.
3649
+ */
3650
+ bordered = input(true, /* @ts-ignore */
3651
+ ...(ngDevMode ? [{ debugName: "bordered" }] : /* istanbul ignore next */ []));
3303
3652
  /**
3304
3653
  * @deprecated Dùng `color`.
3305
3654
  *
@@ -3455,6 +3804,22 @@ class OlBadge {
3455
3804
  classes.push('bg-neutral-secondary', 'border-default-medium', 'text-heading');
3456
3805
  break;
3457
3806
  }
3807
+ /*
3808
+ * `[bordered]="false"` — bỏ viền, GIỮ nền và màu chữ.
3809
+ *
3810
+ * Phải LỌC class viền của màu ra rồi mới thêm `border-transparent`: hai
3811
+ * class viền cùng lúc thì trình duyệt chọn theo thứ tự luật trong file CSS
3812
+ * chứ không theo thứ tự đẩy vào mảng, nên kết quả đổi theo bản build.
3813
+ *
3814
+ * `border` (độ dày) vẫn giữ — xem chú thích ở input `bordered`.
3815
+ */
3816
+ if (!this.bordered()) {
3817
+ for (let i = classes.length - 1; i >= 0; i--) {
3818
+ if (classes[i].startsWith('border-'))
3819
+ classes.splice(i, 1);
3820
+ }
3821
+ classes.push('border-transparent');
3822
+ }
3458
3823
  return classes.join(' ');
3459
3824
  }, /* @ts-ignore */
3460
3825
  ...(ngDevMode ? [{ debugName: "badgeClasses" }] : /* istanbul ignore next */ []));
@@ -3488,7 +3853,7 @@ class OlBadge {
3488
3853
  this.dismiss.emit(event);
3489
3854
  }
3490
3855
  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 });
3856
+ 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
3857
  }
3493
3858
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBadge, decorators: [{
3494
3859
  type: Component,
@@ -3503,7 +3868,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
3503
3868
  // hay không có `min-w-0` badge đều dừng đúng 60px.
3504
3869
  class: 'inline-flex align-middle max-w-full',
3505
3870
  }, 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"] }] } });
3871
+ }], 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
3872
 
3508
3873
  /**
3509
3874
  * Figma `ol-banner` (#1146:21254) KHÔNG có trục màu — Type chỉ là bố cục
@@ -3734,6 +4099,31 @@ class OlBreadcrumbItem {
3734
4099
  */
3735
4100
  disabled = input(false, /* @ts-ignore */
3736
4101
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
4102
+ /**
4103
+ * Cho phép bấm để chuyển trang hay không. Mặc định **bật** — giữ hành vi cũ.
4104
+ *
4105
+ * 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
4106
+ * đượ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 —
4107
+ * 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ự
4108
+ * xử lý điều hướng qua `(itemClick)`.
4109
+ *
4110
+ * Khác `disabled`: `disabled` bỏ HẲN thẻ `<a>`, chặng thành chữ xám không
4111
+ * bấm được và không Tab tới được. `[clickable]="false"` giữ nguyên vẻ ngoài
4112
+ * và trợ năng của liên kết, chỉ chặn hành vi điều hướng.
4113
+ *
4114
+ * Chặn bằng `preventDefault()` trong `(click)` chứ không bỏ `[routerLink]`:
4115
+ * bỏ binding thì `<a>` mất `href`, trình đọc màn hình không còn coi nó là
4116
+ * liên kết, và người dùng mất luôn khả năng xem đích đến ở thanh trạng thái.
4117
+ */
4118
+ clickable = input(true, /* @ts-ignore */
4119
+ ...(ngDevMode ? [{ debugName: "clickable" }] : /* istanbul ignore next */ []));
4120
+ /**
4121
+ * Phát khi người dùng bấm vào chặng — kể cả khi `[clickable]="false"`.
4122
+ *
4123
+ * 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
4124
+ * nháp, hay điều hướng bằng đường khác.
4125
+ */
4126
+ itemClick = output();
3737
4127
  parentBreadcrumb = inject(forwardRef(() => OlBreadcrumb), {
3738
4128
  optional: true,
3739
4129
  });
@@ -3750,13 +4140,56 @@ class OlBreadcrumbItem {
3750
4140
  */
3751
4141
  isLink = computed(() => !this.isCurrent() && !this.disabled() && (this.routerLink() !== null || this.href() !== null), /* @ts-ignore */
3752
4142
  ...(ngDevMode ? [{ debugName: "isLink" }] : /* istanbul ignore next */ []));
4143
+ /**
4144
+ * `href` tĩnh dựng từ `routerLink()`, dùng cho nhánh `clickable=false`.
4145
+ *
4146
+ * Giữ được thuộc tính `href` thật trên thẻ `<a>` nên trợ năng không đổi;
4147
+ * `preventDefault()` trong `onClick` lo phần chặn đi.
4148
+ */
4149
+ staticHref = computed(() => {
4150
+ const link = this.routerLink();
4151
+ if (link === null)
4152
+ return this.href() ?? '';
4153
+ return Array.isArray(link) ? link.join('/').replace(/\/{2,}/g, '/') : String(link);
4154
+ }, /* @ts-ignore */
4155
+ ...(ngDevMode ? [{ debugName: "staticHref" }] : /* istanbul ignore next */ []));
4156
+ /**
4157
+ * Chặn điều hướng khi `[clickable]="false"`, nhưng VẪN phát `itemClick` để
4158
+ * nơi dùng tự xử lý.
4159
+ *
4160
+ * `preventDefault()` chặn cả `routerLink` lẫn `href`: RouterLink bỏ qua sự
4161
+ * kiện đã bị chặn, còn trình duyệt không đi theo `href` nữa.
4162
+ */
4163
+ onClick(event) {
4164
+ if (!this.clickable())
4165
+ event.preventDefault();
4166
+ this.itemClick.emit(event);
4167
+ }
3753
4168
  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 });
4169
+ 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
4170
  }
3756
4171
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBreadcrumbItem, decorators: [{
3757
4172
  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 }] }] } });
4173
+ args: [{ selector: 'ol-breadcrumb-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], host: {
4174
+ /*
4175
+ * Thẻ tuỳ biến KHÔNG khai `display` thì mặc định là `inline`, và bản cũ
4176
+ * không khai `host` nào cả. Hậu quả: host chứa `<li>` inline-flex nên nó
4177
+ * tính chiều cao theo dòng chữ — cộng thêm khoảng trống dưới baseline cho
4178
+ * phần đuôi chữ. Mục CÓ ICON có `<svg>` 16px đẩy dòng cao hơn, nên host
4179
+ * của nó cao 23px trong khi mục không icon chỉ 20px.
4180
+ *
4181
+ * `<ol>` bên ngoài dùng `items-center`, mà căn giữa ba hộp CAO KHÁC NHAU
4182
+ * thì chữ không thể thẳng hàng: đo /breadcrumb 2026-09-24, chữ của mục có
4183
+ * icon nằm cao hơn hai mục sau đúng **1,5px** — thấy rõ bằng mắt trên
4184
+ * thanh "Trang chủ › Bài tập › Học sinh".
4185
+ *
4186
+ * `inline-flex` làm host thành hộp flex, chiều cao bám đúng nội dung
4187
+ * (20px) và không còn khoảng baseline. Ba host bằng nhau thì `items-center`
4188
+ * căn ra cùng một đường.
4189
+ */
4190
+ class: 'inline-flex',
4191
+ }, 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" }]
4192
+ }], 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
4193
 
3761
4194
  class OlDownloadAppButton {
3762
4195
  store = input('appstore', /* @ts-ignore */
@@ -5503,12 +5936,12 @@ class OlDatepicker {
5503
5936
  const el = this.panel()?.nativeElement;
5504
5937
  const raBody = this.appendTo() === 'body';
5505
5938
  if (!open || !el) {
5506
- this.donDepPanel();
5939
+ this.detachPanelNow();
5507
5940
  return;
5508
5941
  }
5509
5942
  // Vào ngăn xếp Escape khi mở. Đăng ký ở đây chứ không trong `openPopup`
5510
5943
  // để 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());
5944
+ this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup(), (target) => this.ownsTarget(target));
5512
5945
  // `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
5513
5946
  if (raBody && !this.detachPanel) {
5514
5947
  this.detachPanel = this.overlayContainer.attach(el);
@@ -5692,7 +6125,7 @@ class OlDatepicker {
5692
6125
  this.state.clear();
5693
6126
  }
5694
6127
  /** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
5695
- donDepPanel() {
6128
+ detachPanelNow() {
5696
6129
  if (this.stopTrackingHeight) {
5697
6130
  this.stopTrackingHeight();
5698
6131
  this.stopTrackingHeight = null;
@@ -5705,37 +6138,67 @@ class OlDatepicker {
5705
6138
  this.releaseEscape?.();
5706
6139
  this.releaseEscape = null;
5707
6140
  }
5708
- onDocumentClick(event) {
5709
- if (this.inline())
5710
- return;
5711
- const path = event.composedPath();
6141
+ /**
6142
+ * Phần tử được bấm có thuộc datepicker này không.
6143
+ *
6144
+ * Ngăn xếp `OlOverlayContainer` gọi hàm này để biết một cú click là "trong"
6145
+ * hay "ngoài". Trước đây mỗi component tự nghe `document:click`, nên một cú
6146
+ * bấm ra ngoài đóng luôn MỌI lớp — mở datepicker trong modal rồi bấm nền là
6147
+ * mất cả hộp thoại đang điền dở. Đo /modal ngày 2026-09-24 xác nhận.
6148
+ *
6149
+ * ## Phải kiểm CẢ PANEL, không chỉ host
6150
+ *
6151
+ * Với `appendTo="body"` panel đã bị `overlayContainer.attach()` chuyển ra
6152
+ * `.ol-overlay-container` — không còn là con của host, nên click vào chính
6153
+ * popup bị coi là click ra ngoài. Đo /modal: bật `appendTo` rồi bấm một
6154
+ * ngày → popup ĐÓNG NGAY, range picker cần hai ngày nên người dùng không
6155
+ * chọn nổi ngày kết thúc.
6156
+ *
6157
+ * Tắt `appendTo` thì popup ở lại đúng — lỗi chỉ xuất hiện khi treo ra ngoài,
6158
+ * tức đúng ca sinh ra để dùng trong modal.
6159
+ */
6160
+ ownsTarget(target) {
6161
+ if (!(target instanceof Node))
6162
+ return false;
6163
+ const panelEl = this.panel()?.nativeElement;
6164
+ return this.hostEl.nativeElement.contains(target) || (!!panelEl && panelEl.contains(target));
6165
+ }
6166
+ /**
6167
+ * Đóng popup khi component bị huỷ.
6168
+ *
6169
+ * Bắt buộc với `appendTo="body"`: panel treo vào `<body>` chứ không phải cây
6170
+ * DOM của component, nên Angular huỷ component mà panel VẪN Ở LẠI. Chuyển
6171
+ * trang khi đang mở lịch là để lại một popup mồ côi không ai đóng được.
6172
+ */
6173
+ ngOnDestroy() {
6174
+ this.closePopup();
5712
6175
  /**
5713
- * Phải kiểm CẢ PANEL, không chỉ host.
6176
+ * Gọi THẲNG `detachPanelNow()`, không trông vào `effect`.
5714
6177
  *
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.
6178
+ * Việc trả panel về chỗ cũ nằm trong `effect()` ở constructor. Angular
6179
+ * DỪNG mọi effect của component ngay khi huỷ, nên `closePopup()` ở trên
6180
+ * đặt `isOpen = false` mà effect không bao giờ chạy để dọn.
6181
+ *
6182
+ * Với `appendTo="body"` panel đã treo vào `.ol-overlay-container` — không
6183
+ * còn là con của component — nên Angular xoá cây DOM của component mà panel
6184
+ * VẪN Ở LẠI. Nó mất luôn liên kết với ô nhập nên Floating UI trả về toạ độ
6185
+ * `(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
6186
+ * được vì component đã biến mất. Đúng hiện tượng anh Vinh mô tả.
5720
6187
  *
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.
6188
+ * Chỉ "thỉnh thoảng" vì nó cần popup ĐANG MỞ lúc chuyển trang; đóng trước
6189
+ * rồi mới chuyển thì effect đã kịp dọn.
5723
6190
  */
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
- }
6191
+ this.detachPanelNow();
5729
6192
  }
5730
6193
  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: [
6194
+ 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
6195
  OlDatepickerStateService,
5733
6196
  {
5734
6197
  provide: NG_VALUE_ACCESSOR,
5735
6198
  useExisting: forwardRef(() => OlDatepicker),
5736
6199
  multi: true,
5737
6200
  },
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 });
6201
+ ], 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
6202
  }
5740
6203
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDatepicker, decorators: [{
5741
6204
  type: Component,
@@ -5775,11 +6238,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
5775
6238
  '[class.w-full]': '!inline()',
5776
6239
  '[class.inline-block]': 'inline()',
5777
6240
  '[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
- }] } });
6241
+ }, 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" }]
6242
+ }], 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
6243
 
5784
6244
  class OlDateRangePicker {
5785
6245
  state = inject(OlDatepickerStateService);
@@ -5896,11 +6356,11 @@ class OlDateRangePicker {
5896
6356
  const el = this.panel()?.nativeElement;
5897
6357
  const raBody = this.appendTo() === 'body';
5898
6358
  if (!open || !el) {
5899
- this.donDepPanel();
6359
+ this.detachPanelNow();
5900
6360
  return;
5901
6361
  }
5902
6362
  // Vào ngăn xếp Escape — chỉ lớp trên cùng đóng.
5903
- this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup());
6363
+ this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup(), (target) => this.ownsTarget(target));
5904
6364
  // `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
5905
6365
  if (raBody && !this.detachPanel) {
5906
6366
  this.detachPanel = this.overlayContainer.attach(el);
@@ -6146,7 +6606,7 @@ class OlDateRangePicker {
6146
6606
  this.state.clear();
6147
6607
  }
6148
6608
  /** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
6149
- donDepPanel() {
6609
+ detachPanelNow() {
6150
6610
  if (this.stopTrackingHeight) {
6151
6611
  this.stopTrackingHeight();
6152
6612
  this.stopTrackingHeight = null;
@@ -6159,37 +6619,58 @@ class OlDateRangePicker {
6159
6619
  this.releaseEscape?.();
6160
6620
  this.releaseEscape = null;
6161
6621
  }
6162
- onDocumentClick(event) {
6163
- if (this.inline())
6164
- return;
6165
- const path = event.composedPath();
6622
+ /**
6623
+ * Phần tử được bấm có thuộc bộ chọn khoảng này không.
6624
+ *
6625
+ * Xem chú thích cùng tên ở `ol-datepicker` — hai component dùng chung cơ
6626
+ * chế. Range picker còn nhạy hơn: nó cần HAI cú bấm (ngày đầu, ngày cuối),
6627
+ * nên popup đóng sớm là người dùng không chọn nổi khoảng.
6628
+ *
6629
+ * Phải kiểm cả panel vì với `appendTo="body"` nó đã bị chuyển ra
6630
+ * `.ol-overlay-container`, không còn là con của host.
6631
+ */
6632
+ ownsTarget(target) {
6633
+ if (!(target instanceof Node))
6634
+ return false;
6635
+ const panelEl = this.panel()?.nativeElement;
6636
+ return this.hostEl.nativeElement.contains(target) || (!!panelEl && panelEl.contains(target));
6637
+ }
6638
+ /**
6639
+ * Đóng popup khi component bị huỷ.
6640
+ *
6641
+ * Bắt buộc với `appendTo="body"`: panel treo vào `<body>` nên Angular huỷ
6642
+ * component mà panel VẪN Ở LẠI. Chuyển trang khi đang mở lịch là để lại một
6643
+ * popup mồ côi không ai đóng được.
6644
+ */
6645
+ ngOnDestroy() {
6646
+ this.closePopup();
6166
6647
  /**
6167
- * Phải kiểm CẢ PANEL, không chỉ host.
6648
+ * Gọi THẲNG `detachPanelNow()`, không trông vào `effect`.
6168
6649
  *
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.
6650
+ * Việc trả panel về chỗ cũ nằm trong `effect()` ở constructor. Angular
6651
+ * DỪNG mọi effect của component ngay khi huỷ, nên `closePopup()` ở trên
6652
+ * đặt `isOpen = false` mà effect không bao giờ chạy để dọn.
6653
+ *
6654
+ * Với `appendTo="body"` panel đã treo vào `.ol-overlay-container` — không
6655
+ * còn là con của component — nên Angular xoá cây DOM của component mà panel
6656
+ * VẪN Ở LẠI. Nó mất luôn liên kết với ô nhập nên Floating UI trả về toạ độ
6657
+ * `(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
6658
+ * được vì component đã biến mất. Đúng hiện tượng anh Vinh mô tả.
6174
6659
  *
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.
6660
+ * Chỉ "thỉnh thoảng" vì nó cần popup ĐANG MỞ lúc chuyển trang; đóng trước
6661
+ * rồi mới chuyển thì effect đã kịp dọn.
6177
6662
  */
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
- }
6663
+ this.detachPanelNow();
6183
6664
  }
6184
6665
  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: [
6666
+ 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
6667
  OlDatepickerStateService,
6187
6668
  {
6188
6669
  provide: NG_VALUE_ACCESSOR,
6189
6670
  useExisting: forwardRef(() => OlDateRangePicker),
6190
6671
  multi: true,
6191
6672
  },
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 });
6673
+ ], 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
6674
  }
6194
6675
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDateRangePicker, decorators: [{
6195
6676
  type: Component,
@@ -6213,11 +6694,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
6213
6694
  */
6214
6695
  class: 'relative inline-block',
6215
6696
  '[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
- }] } });
6697
+ }, 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" }]
6698
+ }], 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
6699
 
6222
6700
  class OlDatepickerDirective {
6223
6701
  el = inject((ElementRef));
@@ -7563,10 +8041,10 @@ class OlInput {
7563
8041
  * lệch 2px so với thiết kế.
7564
8042
  */
7565
8043
  paddingClasses = computed(() => {
7566
- const coIconTrai = this.leftIcon() && !this.addOnIcon();
7567
- const coIconPhai = this.rightIcon() || this.state() === 'error' || this.state() === 'success';
8044
+ const hasLeadingIcon = this.leftIcon() && !this.addOnIcon();
8045
+ const hasTrailingIcon = this.rightIcon() || this.state() === 'error' || this.state() === 'success';
7568
8046
  const classes = [];
7569
- if (!coIconTrai) {
8047
+ if (!hasLeadingIcon) {
7570
8048
  switch (this.size()) {
7571
8049
  case 'sm':
7572
8050
  classes.push('px-2.5');
@@ -7584,7 +8062,7 @@ class OlInput {
7584
8062
  else {
7585
8063
  classes.push('ps-2.5');
7586
8064
  }
7587
- if (coIconPhai)
8065
+ if (hasTrailingIcon)
7588
8066
  classes.push('pe-2.5');
7589
8067
  return classes.join(' ');
7590
8068
  }, /* @ts-ignore */
@@ -7610,7 +8088,7 @@ class OlInput {
7610
8088
  this.disabled.set(isDisabled);
7611
8089
  }
7612
8090
  /** Gọi từ template mỗi khi giá trị đổi, để form biết. */
7613
- phatThayDoi(val) {
8091
+ emitChange(val) {
7614
8092
  this.value.set(val);
7615
8093
  this.onChange(val);
7616
8094
  }
@@ -7638,7 +8116,7 @@ class OlInput {
7638
8116
  useExisting: forwardRef(() => OlInput),
7639
8117
  multi: true,
7640
8118
  },
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 });
8119
+ ], 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
8120
  }
7643
8121
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlInput, decorators: [{
7644
8122
  type: Component,
@@ -7654,7 +8132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
7654
8132
  useExisting: forwardRef(() => OlInput),
7655
8133
  multi: true,
7656
8134
  },
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" }]
8135
+ ], 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
8136
  }], 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
8137
 
7660
8138
  let nextTextareaId = 0;
@@ -7738,7 +8216,7 @@ class OlTextarea {
7738
8216
  this.disabled.set(isDisabled);
7739
8217
  }
7740
8218
  /** Gọi từ template mỗi khi giá trị đổi, để form biết. */
7741
- phatThayDoi(val) {
8219
+ emitChange(val) {
7742
8220
  this.value.set(val);
7743
8221
  this.onChange(val);
7744
8222
  }
@@ -7753,7 +8231,7 @@ class OlTextarea {
7753
8231
  useExisting: forwardRef(() => OlTextarea),
7754
8232
  multi: true,
7755
8233
  },
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 });
8234
+ ], 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
8235
  }
7758
8236
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTextarea, decorators: [{
7759
8237
  type: Component,
@@ -7769,7 +8247,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
7769
8247
  useExisting: forwardRef(() => OlTextarea),
7770
8248
  multi: true,
7771
8249
  },
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" }]
8250
+ ], 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
8251
  }], 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
8252
 
7775
8253
  class OlSpinner {
@@ -8417,7 +8895,7 @@ class OlSelect {
8417
8895
  const el = this.panel()?.nativeElement;
8418
8896
  const raBody = this.appendTo() === 'body';
8419
8897
  if (!open || !el) {
8420
- this.donDepPanel();
8898
+ this.detachPanelNow();
8421
8899
  return;
8422
8900
  }
8423
8901
  /**
@@ -8451,11 +8929,11 @@ class OlSelect {
8451
8929
  this.destroyRef.onDestroy(() => {
8452
8930
  if (this.hengio)
8453
8931
  clearTimeout(this.hengio);
8454
- this.donDepPanel();
8932
+ this.detachPanelNow();
8455
8933
  });
8456
8934
  }
8457
8935
  /** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
8458
- donDepPanel() {
8936
+ detachPanelNow() {
8459
8937
  if (this.stopTracking) {
8460
8938
  this.stopTracking();
8461
8939
  this.stopTracking = null;
@@ -8864,13 +9342,13 @@ class OlSelect {
8864
9342
  this.searchQuery.set(val);
8865
9343
  this.activeIndex.set(0);
8866
9344
  this.scrollTop.set(0);
8867
- this.phatSearch(val);
9345
+ this.emitSearch(val);
8868
9346
  }
8869
9347
  /**
8870
9348
  * Phát `search` có GỠ NẢY. Gõ "nguyễn văn a" là 12 phím — không gỡ nảy thì
8871
9349
  * 12 lượt gọi API, mà chỉ lượt cuối có ích.
8872
9350
  */
8873
- phatSearch(val) {
9351
+ emitSearch(val) {
8874
9352
  if (this.hengio) {
8875
9353
  clearTimeout(this.hengio);
8876
9354
  this.hengio = null;
@@ -9104,7 +9582,7 @@ class OlSelect {
9104
9582
  useExisting: forwardRef(() => OlSelect),
9105
9583
  multi: true,
9106
9584
  },
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 });
9585
+ ], 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
9586
  }
9109
9587
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSelect, decorators: [{
9110
9588
  type: Component,
@@ -9118,7 +9596,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
9118
9596
  useExisting: forwardRef(() => OlSelect),
9119
9597
  multi: true,
9120
9598
  },
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" }]
9599
+ ], 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
9600
  }], 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
9601
  type: ContentChild,
9124
9602
  args: ['optionTemplate']
@@ -10618,6 +11096,24 @@ class OlTimepicker {
10618
11096
  ...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
10619
11097
  size = input('base', /* @ts-ignore */
10620
11098
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
11099
+ /**
11100
+ * Đệm ngang và cỡ chữ theo BẢNG CHUẨN họ form.
11101
+ *
11102
+ * Trước đây template ghim cứng `px-3.5` (14px cho mọi cỡ) và không đặt cỡ
11103
+ * chữ nào, nên nó thừa kế 16px mặc định. Đo trên /datepicker mục 8 ngày
11104
+ * 2026-09-24, đặt cạnh ba ô kia:
11105
+ *
11106
+ * sm/base timepicker chữ 16px · ba ô kia 14px
11107
+ * mọi cỡ timepicker đệm 14px · input 10/12/14/16px theo cỡ
11108
+ *
11109
+ * Gom vào MỘT chuỗi thay vì nhiều `[class.px-*]`: Angular cắt tên class ở
11110
+ * dấu chấm nên `[class.px-2.5]` render ra `px-2`, lệch 2px so với thiết kế.
11111
+ */
11112
+ fieldTextClasses = computed(() => {
11113
+ const size = this.size();
11114
+ return `${FIELD_PADDING_X[size]} ${FIELD_TEXT[size]}`;
11115
+ }, /* @ts-ignore */
11116
+ ...(ngDevMode ? [{ debugName: "fieldTextClasses" }] : /* istanbul ignore next */ []));
10621
11117
  state = input('default', /* @ts-ignore */
10622
11118
  ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
10623
11119
  /**
@@ -10778,7 +11274,7 @@ class OlTimepicker {
10778
11274
  const el = this.panel()?.nativeElement;
10779
11275
  const raBody = this.appendTo() === 'body';
10780
11276
  if (!open || !el) {
10781
- this.donDepPanel();
11277
+ this.detachPanelNow();
10782
11278
  return;
10783
11279
  }
10784
11280
  // `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
@@ -10801,7 +11297,7 @@ class OlTimepicker {
10801
11297
  });
10802
11298
  }
10803
11299
  /** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
10804
- donDepPanel() {
11300
+ detachPanelNow() {
10805
11301
  if (this.stopTracking) {
10806
11302
  this.stopTracking();
10807
11303
  this.stopTracking = null;
@@ -10966,7 +11462,7 @@ class OlTimepicker {
10966
11462
  useExisting: forwardRef(() => OlTimepicker),
10967
11463
  multi: true,
10968
11464
  },
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 });
11465
+ ], 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
11466
  }
10971
11467
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTimepicker, decorators: [{
10972
11468
  type: Component,
@@ -10982,7 +11478,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
10982
11478
  useExisting: forwardRef(() => OlTimepicker),
10983
11479
  multi: true,
10984
11480
  },
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" }]
11481
+ ], 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
11482
  }], 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
11483
  type: HostListener,
10988
11484
  args: ['document:click', ['$event']]
@@ -11019,8 +11515,32 @@ class OlAutocomplete {
11019
11515
  size = input('base', /* @ts-ignore */
11020
11516
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
11021
11517
  /** 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 */
11518
+ resolvedSize = computed(() => {
11519
+ const size = this.size();
11520
+ // `md` là BÍ DANH của `base` — gập ở đây để kiểu trả về đúng `FieldSize`,
11521
+ // nhờ vậy tra bảng chuẩn không cần ép kiểu.
11522
+ return size === 'md' ? 'base' : size;
11523
+ }, /* @ts-ignore */
11023
11524
  ...(ngDevMode ? [{ debugName: "resolvedSize" }] : /* istanbul ignore next */ []));
11525
+ /**
11526
+ * Đệm ngang theo BẢNG CHUẨN họ form. Chỉ dùng khi KHÔNG `multiple` —
11527
+ * chế độ nhiều thẻ có đệm riêng (`px-1.5 py-1`) vì các thẻ tự mang đệm.
11528
+ *
11529
+ * Ba lỗi cũ gộp làm một chỗ này (đo /autocomplete 2026-09-24):
11530
+ * sm `[class.px-2.5]` -> Angular CẮT ở dấu chấm, render `px-2` = 8px
11531
+ * thay vì 10px. Đây là lý do phải gom vào chuỗi, không bind lẻ.
11532
+ * base dùng chung `px-3.5` với `lg` = 14px, bảng chuẩn quy định 12px.
11533
+ * xl có `px-4` nhưng nằm ở `[class.px-4]` rời, dễ lệch khỏi ba cỡ kia.
11534
+ */
11535
+ fieldPaddingClass = computed(() => FIELD_PADDING_X[this.resolvedSize()], /* @ts-ignore */
11536
+ ...(ngDevMode ? [{ debugName: "fieldPaddingClass" }] : /* istanbul ignore next */ []));
11537
+ /**
11538
+ * Cỡ chữ theo BẢNG CHUẨN. Trước đây `[class.text-base]` chỉ khai cho `lg`,
11539
+ * `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
11540
+ * giá trị đúng một cách tình cờ, hỏng ngay khi cỡ gốc đổi.
11541
+ */
11542
+ fieldTextClass = computed(() => FIELD_TEXT[this.resolvedSize()], /* @ts-ignore */
11543
+ ...(ngDevMode ? [{ debugName: "fieldTextClass" }] : /* istanbul ignore next */ []));
11024
11544
  state = input('default', /* @ts-ignore */
11025
11545
  ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
11026
11546
  required = input(false, /* @ts-ignore */
@@ -11188,7 +11708,7 @@ class OlAutocomplete {
11188
11708
  const el = this.panel()?.nativeElement;
11189
11709
  const raBody = this.appendTo() === 'body';
11190
11710
  if (!open || !el) {
11191
- this.donDepPanel();
11711
+ this.detachPanelNow();
11192
11712
  return;
11193
11713
  }
11194
11714
  /**
@@ -11222,11 +11742,11 @@ class OlAutocomplete {
11222
11742
  this.destroyRef.onDestroy(() => {
11223
11743
  if (this.hengio)
11224
11744
  clearTimeout(this.hengio);
11225
- this.donDepPanel();
11745
+ this.detachPanelNow();
11226
11746
  });
11227
11747
  }
11228
11748
  /** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
11229
- donDepPanel() {
11749
+ detachPanelNow() {
11230
11750
  if (this.stopTracking) {
11231
11751
  this.stopTracking();
11232
11752
  this.stopTracking = null;
@@ -11394,7 +11914,7 @@ class OlAutocomplete {
11394
11914
  this.searchQuery.set(val);
11395
11915
  this.isOpen.set(true);
11396
11916
  this.activeIndex.set(0);
11397
- this.phatSearch(val);
11917
+ this.emitSearch(val);
11398
11918
  }
11399
11919
  /**
11400
11920
  * 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 +11926,7 @@ class OlAutocomplete {
11406
11926
  *
11407
11927
  * Dưới `minTermLength` thì KHÔNG phát: nơi gọi không phải tự kiểm lại.
11408
11928
  */
11409
- phatSearch(val) {
11929
+ emitSearch(val) {
11410
11930
  if (this.hengio) {
11411
11931
  clearTimeout(this.hengio);
11412
11932
  this.hengio = null;
@@ -11542,7 +12062,7 @@ class OlAutocomplete {
11542
12062
  useExisting: forwardRef(() => OlAutocomplete),
11543
12063
  multi: true,
11544
12064
  },
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 });
12065
+ ], 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
12066
  }
11547
12067
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAutocomplete, decorators: [{
11548
12068
  type: Component,
@@ -11558,7 +12078,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
11558
12078
  useExisting: forwardRef(() => OlAutocomplete),
11559
12079
  multi: true,
11560
12080
  },
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" }]
12081
+ ], 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
12082
  }], 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
12083
  type: ContentChild,
11564
12084
  args: ['optionTemplate']
@@ -11645,186 +12165,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
11645
12165
  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
12166
  }], 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
12167
 
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
12168
  /**
11829
12169
  * Dải nút hành động cuối Modal — khớp Figma `ol-modal-footer`.
11830
12170
  * Figma có 4 biến thể: Buttons, 3 buttons, Buttons & links, Pagination.
@@ -11988,15 +12328,36 @@ class OlNavbarItem {
11988
12328
  '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
12329
  /**
11990
12330
  * 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 đủ ở đó.
12331
+ * `:hover` tính như một class nên hover (0,2,0) thắng nền active
12332
+ * (0,1,0), di chuột vào mục đang mở là mất trạng thái. Xem chú thích
12333
+ * đầy đủ ở đó.
12334
+ */
12335
+ 'text-body not-[.active]:hover:text-fg-brand not-[.active]:hover:bg-brand-softer',
12336
+ /**
12337
+ * Trạng thái active — BÁM THEO `ol-sidebar-item`, xem ba đời màu ở đó.
12338
+ *
12339
+ * Đồng bộ 2026-09-24. Trước đó navbar kẹt lại ở ĐỜI 2 (`bg-brand` đặc +
12340
+ * chữ trắng + gạch mép trái 4px): commit 89b2e49 ngày 21/09 đổi cả hai
12341
+ * component sang đời 2 cùng lúc, nhưng khi sidebar chuyển sang đời 3
12342
+ * (c978f10 + 224d048 ngày 23/09 — nền nhạt `bg-brand-soft`, gỡ gạch
12343
+ * mép) thì navbar không được đổi theo. Chú thích ở đây vẫn ghi "cùng
12344
+ * luật với ol-sidebar-item" trong khi hai bên đã khác hẳn.
12345
+ *
12346
+ * Gỡ luôn gạch mép trái, cùng lý do sidebar đã gỡ: nền đã đánh dấu đủ
12347
+ * rõ, gạch là dấu hiệu thứ hai cho cùng một trạng thái, và nó ăn 4px
12348
+ * đệm trái làm chữ mục active lệch so với các mục còn lại — ở navbar
12349
+ * xếp NGANG thì lệch này thấy rõ hơn sidebar xếp dọc.
12350
+ *
12351
+ * VẪN LỆCH FIGMA CÓ CHỦ Ý. Figma `ol-navbar-item` #6375:261 cho
12352
+ * `State=Active` chỉ đổi màu chữ (`text-body` -> `text-fg-brand`),
12353
+ * KHÔNG nền, KHÔNG viền — đọc biến bind ngày 2026-09-24. Đó chính là
12354
+ * 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.
12355
+ * Nợ: cập nhật Figma cho khớp đời 3, cho cả sidebar lẫn navbar.
11993
12356
  */
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',
12357
+ '[&.active]:text-fg-brand [&.active]:bg-brand-soft [&.active]:font-semibold',
12358
+ this.active() ? 'active text-fg-brand bg-brand-soft font-semibold' : '',
12359
+ /** Hover của mục ĐANG active: ĐẬM thêm một bậc, không nhạt đi. */
12360
+ '[&.active]:hover:bg-brand-medium',
12000
12361
  this.disabled() ? 'opacity-50 pointer-events-none' : 'cursor-pointer',
12001
12362
  ]
12002
12363
  .filter(Boolean)
@@ -12004,11 +12365,11 @@ class OlNavbarItem {
12004
12365
  }, /* @ts-ignore */
12005
12366
  ...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
12006
12367
  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 });
12368
+ 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
12369
  }
12009
12370
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlNavbarItem, decorators: [{
12010
12371
  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" }]
12372
+ 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
12373
  }], 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
12374
 
12014
12375
  class OlNavDropdown {
@@ -13180,7 +13541,7 @@ class OlAppNav {
13180
13541
  this.collapsed.update((v) => !v);
13181
13542
  }
13182
13543
  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 });
13544
+ 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
13545
  }
13185
13546
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, decorators: [{
13186
13547
  type: Component,
@@ -13371,11 +13732,11 @@ class OlSidebarItem {
13371
13732
  }, /* @ts-ignore */
13372
13733
  ...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
13373
13734
  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 });
13735
+ 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
13736
  }
13376
13737
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarItem, decorators: [{
13377
13738
  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" }]
13739
+ 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
13740
  }], 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
13741
 
13381
13742
  class OlSidebarGroup {
@@ -13444,7 +13805,7 @@ class OlSidebarSubmenu {
13444
13805
  this.isOpen.update((v) => !v);
13445
13806
  }
13446
13807
  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 });
13808
+ 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
13809
  }
13449
13810
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarSubmenu, decorators: [{
13450
13811
  type: Component,
@@ -14409,7 +14770,7 @@ class OlTabs {
14409
14770
  }
14410
14771
  }
14411
14772
  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 });
14773
+ 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
14774
  }
14414
14775
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTabs, decorators: [{
14415
14776
  type: Component,
@@ -14795,7 +15156,7 @@ class OlBottomNavItem {
14795
15156
  }, /* @ts-ignore */
14796
15157
  ...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
14797
15158
  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 });
15159
+ 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
15160
  }
14800
15161
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBottomNavItem, decorators: [{
14801
15162
  type: Component,
@@ -14959,7 +15320,7 @@ class OlStepper {
14959
15320
  }
14960
15321
  }
14961
15322
  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 });
15323
+ 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
15324
  }
14964
15325
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlStepper, decorators: [{
14965
15326
  type: Component,
@@ -15041,15 +15402,50 @@ class OlTreeNodeItem {
15041
15402
  expanded: !!this.node().expanded,
15042
15403
  }), /* @ts-ignore */
15043
15404
  ...(ngDevMode ? [{ debugName: "tplContext" }] : /* istanbul ignore next */ []));
15405
+ /**
15406
+ * Chỉ `id` của nút, tách khỏi cả object.
15407
+ *
15408
+ * `effect` bên dưới đọc tín hiệu này thay vì `this.node()`: đọc cả object
15409
+ * 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
15410
+ * sách từ máy chủ — dù chính nút này không đổi gì. `computed` chỉ phát tín
15411
+ * hiệu khi GIÁ TRỊ đổi, mà `id` là chuỗi nên so sánh bằng `===`: tải lại
15412
+ * 100 nút với cùng id thì effect không chạy lần nào.
15413
+ */
15414
+ nodeId = computed(() => this.node().id, /* @ts-ignore */
15415
+ ...(ngDevMode ? [{ debugName: "nodeId" }] : /* istanbul ignore next */ []));
15044
15416
  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.
15417
+ /*
15418
+ * Điều hướng bằng phím chỉ đổi `focusedId`; phải gọi `.focus()` thật thì
15419
+ * trình duyệt mới chuyển con trỏ.
15420
+ *
15421
+ * HAI ĐIỀU KIỆN CHẶN, cả hai đều có lý do đo được:
15422
+ *
15423
+ * 1. Đọc `nodeId()` chứ không `node()`. Đo bằng ca thử "tải lại toàn bộ
15424
+ * cây, id giữ nguyên": bản cũ effect chạy lại và CƯỚP focus khỏi phần
15425
+ * tử người dùng đang đứng (đo được focus nhảy từ nút ngoài cây vào một
15426
+ * `div` trong cây).
15427
+ *
15428
+ * 2. Chỉ lấy focus khi focus ĐANG Ở TRONG CÂY. Người dùng bấm ra ô tìm
15429
+ * kiếm rồi dữ liệu tự cập nhật thì con trỏ đang ở ô tìm — kéo nó về cây
15430
+ * là cướp thao tác đang làm dở. `focusedId` chỉ được đặt khi điều hướng
15431
+ * bằng phím, nhưng nó KHÔNG TỰ XOÁ khi người dùng rời cây.
15432
+ *
15433
+ * Hệ quả phụ, cũng là thứ người dùng báo: `.focus()` gọi BẰNG MÃ làm
15434
+ * `:focus-visible` khớp, nên viền brand 2px hiện ra sau mỗi lần cập nhật
15435
+ * dữ liệu — trông như "viền đen to" trên nền sẫm. Chặn effect chạy oan là
15436
+ * hết luôn viền oan.
15437
+ */
15047
15438
  effect(() => {
15048
- if (this.focusedId() !== this.node().id)
15439
+ if (this.focusedId() !== this.nodeId())
15049
15440
  return;
15050
15441
  const el = this.hang()?.nativeElement;
15051
- if (el && document.activeElement !== el)
15052
- el.focus();
15442
+ if (!el || document.activeElement === el)
15443
+ return;
15444
+ // Chỉ cướp focus khi nó vốn đã ở trong cây này.
15445
+ const tree = el.closest('[role="tree"]');
15446
+ if (tree && !tree.contains(document.activeElement))
15447
+ return;
15448
+ el.focus();
15053
15449
  });
15054
15450
  }
15055
15451
  /**
@@ -15094,7 +15490,7 @@ class OlTreeNodeItem {
15094
15490
  this.nodeCheck.emit({ node: this.node(), checked });
15095
15491
  }
15096
15492
  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 });
15493
+ 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
15494
  }
15099
15495
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTreeNodeItem, decorators: [{
15100
15496
  type: Component,
@@ -16076,11 +16472,11 @@ class OlChart {
16076
16472
  }, /* @ts-ignore */
16077
16473
  ...(ngDevMode ? [{ debugName: "horizontalBarsComputed" }] : /* istanbul ignore next */ []));
16078
16474
  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 });
16475
+ 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
16476
  }
16081
16477
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlChart, decorators: [{
16082
16478
  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"] }]
16479
+ 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
16480
  }], 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
16481
 
16086
16482
  /** Nhãn hiển thị + icon cho từng trạng thái. */
@@ -16235,11 +16631,11 @@ class OlJumbotron {
16235
16631
  }, /* @ts-ignore */
16236
16632
  ...(ngDevMode ? [{ debugName: "badgeClasses" }] : /* istanbul ignore next */ []));
16237
16633
  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 });
16634
+ 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
16635
  }
16240
16636
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlJumbotron, decorators: [{
16241
16637
  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" }]
16638
+ 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
16639
  }], 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
16640
 
16245
16641
  /**
@@ -16331,5 +16727,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
16331
16727
  * Generated bundle index. Do not edit.
16332
16728
  */
16333
16729
 
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 };
16730
+ 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
16731
  //# sourceMappingURL=edmicro-olkit.mjs.map