@edmicro/olkit 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/LICENSES/FLOWBITE-MIT.txt +21 -0
- package/LICENSES/LICENSES/MYNAUI-MIT.txt +21 -0
- package/LICENSES/LICENSES/SOLAR-CC-BY-4.0.txt +49 -0
- package/README.md +57 -28
- package/fesm2022/edmicro-olkit.mjs +759 -343
- package/fesm2022/edmicro-olkit.mjs.map +1 -1
- package/olkit.css +1 -1
- package/package.json +6 -2
- package/types/edmicro-olkit.d.ts +388 -50
- package/types/edmicro-olkit.d.ts.map +1 -1
- package/version.txt +2 -2
|
@@ -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,
|
|
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
|
|
61
|
-
const lazyGetter =
|
|
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
|
-
|
|
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(
|
|
1523
|
+
const i = this.openLayers.indexOf(layer);
|
|
1542
1524
|
if (i !== -1)
|
|
1543
1525
|
this.openLayers.splice(i, 1);
|
|
1544
|
-
this.
|
|
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
|
-
/**
|
|
1572
|
-
|
|
1573
|
-
|
|
1553
|
+
/**
|
|
1554
|
+
* Trình nghe click duy nhất cho cả ngăn xếp.
|
|
1555
|
+
*
|
|
1556
|
+
* ## Vì sao không để mỗi component tự nghe
|
|
1557
|
+
*
|
|
1558
|
+
* Trước đây `ol-datepicker`, `ol-select`, `ol-dropdown`… mỗi cái một
|
|
1559
|
+
* `@HostListener('document:click')`. Chúng không biết nhau, nên một cú bấm
|
|
1560
|
+
* ra ngoài kích hoạt TẤT CẢ cùng lúc: mở datepicker trong modal rồi bấm nền
|
|
1561
|
+
* thì cả datepicker lẫn modal cùng đóng, người dùng mất hộp thoại đang điền
|
|
1562
|
+
* dở. Đo /modal ngày 2026-09-24: `dp=1, modal=true` -> sau MỘT cú bấm còn
|
|
1563
|
+
* `dp=0, modal=false`.
|
|
1564
|
+
*
|
|
1565
|
+
* Escape đã giải đúng từ trước (xem `ensureEscapeListener`); đây là áp cùng
|
|
1566
|
+
* nguyên tắc cho chuột.
|
|
1567
|
+
*
|
|
1568
|
+
* ## Chỉ lớp TRÊN CÙNG đóng
|
|
1569
|
+
*
|
|
1570
|
+
* Click nằm trong lớp trên cùng -> không đóng gì. Nằm ngoài -> đóng đúng lớp
|
|
1571
|
+
* đó, và `stopPropagation` chặn lớp dưới nhận cùng cú bấm ấy.
|
|
1572
|
+
*
|
|
1573
|
+
* Lớp không khai `contains` thì bị bỏ qua hoàn toàn — nó tự lo phần click
|
|
1574
|
+
* của mình, ngăn xếp không đoán thay.
|
|
1575
|
+
*/
|
|
1576
|
+
ensureClickListener() {
|
|
1577
|
+
if (this.clickListener)
|
|
1578
|
+
return;
|
|
1579
|
+
this.clickListener = (event) => {
|
|
1580
|
+
// Duyệt từ trên xuống, dừng ở lớp đầu tiên có khai `contains`.
|
|
1581
|
+
for (let i = this.openLayers.length - 1; i >= 0; i--) {
|
|
1582
|
+
const layer = this.openLayers[i];
|
|
1583
|
+
if (!layer.contains)
|
|
1584
|
+
continue;
|
|
1585
|
+
// Bấm TRONG lớp trên cùng: không đóng gì cả.
|
|
1586
|
+
if (layer.contains(event.target))
|
|
1587
|
+
return;
|
|
1588
|
+
/**
|
|
1589
|
+
* Bấm ngoài: đóng đúng lớp này, và chặn lớp dưới nhận cùng cú bấm.
|
|
1590
|
+
*
|
|
1591
|
+
* Điều kiện chặn là "CÒN LỚP NÀO bên dưới", không phải "còn lớp nào
|
|
1592
|
+
* khai `contains`". Modal đóng qua `onBackdropClick` — sự kiện click
|
|
1593
|
+
* trên chính lớp nền, KHÔNG đi qua ngăn xếp này — nên nó không khai
|
|
1594
|
+
* `contains`. Lọc theo `contains` thì `hasLayerBelow` ra `false` và
|
|
1595
|
+
* modal vẫn đóng cùng lượt: đo /modal thấy bấm một cú là mất cả hai.
|
|
1596
|
+
*
|
|
1597
|
+
* Chặn vô điều kiện cũng sai: khi CHỈ có một lớp mở, nút mà người dùng
|
|
1598
|
+
* vừa bấm không nhận được sự kiện. Đo bằng bộ test thư viện: 1-2 ca đỏ
|
|
1599
|
+
* mỗi lượt, 4/4 lượt, trong khi mã gốc 3/3 lượt xanh.
|
|
1600
|
+
*
|
|
1601
|
+
* Nên: có lớp dưới thì chặn, không có thì để sự kiện đi tiếp.
|
|
1602
|
+
*/
|
|
1603
|
+
if (i > 0)
|
|
1604
|
+
event.stopPropagation();
|
|
1605
|
+
layer.close();
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
// `capture: true` để chạy TRƯỚC `@HostListener` của component, nhờ đó
|
|
1610
|
+
// `stopPropagation` mới chặn được chúng.
|
|
1611
|
+
this.doc.addEventListener('click', this.clickListener, true);
|
|
1612
|
+
}
|
|
1613
|
+
/** Gỡ hai trình nghe khi lớp cuối cùng đóng — không để chúng nghe vô ích. */
|
|
1614
|
+
teardownListenersIfIdle() {
|
|
1615
|
+
if (this.openLayers.length > 0)
|
|
1574
1616
|
return;
|
|
1575
|
-
|
|
1576
|
-
|
|
1617
|
+
if (this.escapeListener) {
|
|
1618
|
+
this.doc.removeEventListener('keydown', this.escapeListener, true);
|
|
1619
|
+
this.escapeListener = null;
|
|
1620
|
+
}
|
|
1621
|
+
if (this.clickListener) {
|
|
1622
|
+
this.doc.removeEventListener('click', this.clickListener, true);
|
|
1623
|
+
this.clickListener = null;
|
|
1624
|
+
}
|
|
1577
1625
|
}
|
|
1578
1626
|
/**
|
|
1579
1627
|
* @deprecated Dùng `attach()`. Giữ lại vì đã publish trong
|
|
@@ -1897,7 +1945,23 @@ class OlTooltip {
|
|
|
1897
1945
|
* Computed tooltip classes based on theme
|
|
1898
1946
|
*/
|
|
1899
1947
|
tooltipClasses = computed(() => {
|
|
1900
|
-
|
|
1948
|
+
/*
|
|
1949
|
+
* KHÔNG `whitespace-nowrap` — nó mâu thuẫn với `max-w-xs` đứng ngay cạnh.
|
|
1950
|
+
*
|
|
1951
|
+
* Hai class này cùng lúc thì `nowrap` thắng: chữ không được phép xuống
|
|
1952
|
+
* dòng nên `max-width` chỉ bó cái HỘP lại, còn chữ cứ chạy thẳng ra ngoài.
|
|
1953
|
+
* Đo /tooltip ngày 2026-09-25 với nội dung 160 ký tự: hộp đúng 320px
|
|
1954
|
+
* nhưng `scrollWidth` lên **1070px** — chữ tràn khỏi nền tối ra cả hai
|
|
1955
|
+
* phía. Chụp ảnh thấy rõ.
|
|
1956
|
+
*
|
|
1957
|
+
* `break-words` (`overflow-wrap: break-word`) lo nốt từ đơn dài hơn 320px
|
|
1958
|
+
* — ví dụ một URL hay mã lớp không có dấu cách: không có nó thì từ đó vẫn
|
|
1959
|
+
* tràn dù đã bỏ `nowrap`.
|
|
1960
|
+
*
|
|
1961
|
+
* Tooltip ngắn KHÔNG bị ảnh hưởng: `max-w-xs` là giới hạn TRÊN, hộp vẫn
|
|
1962
|
+
* ôm sát nội dung khi chữ ngắn hơn.
|
|
1963
|
+
*/
|
|
1964
|
+
const baseClasses = 'px-3 py-2 rounded-lg text-sm font-medium break-words z-tooltip max-w-xs';
|
|
1901
1965
|
// Figma `ol-tooltip` chỉ định nghĩa nền tối; hai theme `light` và `brand`
|
|
1902
1966
|
// là mở rộng NGOÀI Figma nên giữ bậc số cho tới khi thiết kế định nghĩa.
|
|
1903
1967
|
//
|
|
@@ -2753,6 +2817,200 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
2753
2817
|
}, template: "<!--\n `olTooltip` l\u00E0 `input.required<string>()` nh\u01B0ng directive t\u1EF1 b\u1ECF qua chu\u1ED7i\n R\u1ED6NG (xem `tooltipDisabled` v\u00E0 nh\u00E1nh `if (!content)` trong directive), n\u00EAn\n truy\u1EC1n `''` cho n\u00FAt c\u00F3 ch\u1EEF l\u00E0 an to\u00E0n \u2014 kh\u00F4ng c\u1EA7n `@if` b\u1ECDc ngo\u00E0i, v\u1ED1n s\u1EBD\n nh\u00E2n \u0111\u00F4i c\u1EA3 kh\u1ED1i template.\n-->\n<button\n [olTooltip]=\"tooltipText()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-expanded]=\"ariaExpanded() === null ? null : ariaExpanded()\"\n [attr.aria-controls]=\"ariaControls() || null\"\n [attr.aria-pressed]=\"ariaPressed() === null ? null : ariaPressed()\"\n [type]=\"type()\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"handleClick($event)\"\n [class]=\"buttonClasses()\"\n>\n @if (loading()) {\n <svg\n class=\"animate-spin shrink-0\"\n [style.width.px]=\"iconPx()\"\n [style.height.px]=\"iconPx()\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <ng-content select=\"[leftIcon]\" />\n <ng-content select=\"[logo]\" />\n <!--\n `iconOnly` \u1EA9n ch\u1EEF b\u1EB1ng `sr-only`, KH\u00D4NG b\u1EB1ng `@if`: Angular chi\u1EBFu n\u1ED9i dung\n \u0111\u00FAng m\u1ED9t l\u1EA7n, \u0111\u1EB7t `<ng-content />` trong c\u00E1c nh\u00E1nh lo\u1EA1i tr\u1EEB nhau l\u00E0 m\u1EA5t ru\u1ED9t\n \u1EDF m\u1ECDi nh\u00E1nh. Gi\u1EEF ch\u1EEF trong DOM c\u00F2n \u0111\u00FAng v\u1EC1 tr\u1EE3 n\u0103ng \u2014 n\u00FAt ch\u1EC9-icon ph\u1EA3i c\u00F3\n t\u00EAn \u0111\u1ECDc \u0111\u01B0\u1EE3c.\n -->\n <span data-noi-dung [class.sr-only]=\"iconOnly()\">\n <ng-content />\n </span>\n <ng-content select=\"[rightIcon]\" />\n</button>\n", styles: [".ol-btn-ico-14 ::ng-deep ol-icon,.ol-btn-ico-14 ::ng-deep ol-icon>span{width:14px!important;height:14px!important}.ol-btn-ico-16 ::ng-deep ol-icon,.ol-btn-ico-16 ::ng-deep ol-icon>span{width:16px!important;height:16px!important}.ol-btn-ico-20 ::ng-deep ol-icon,.ol-btn-ico-20 ::ng-deep ol-icon>span{width:20px!important;height:20px!important}[data-noi-dung].sr-only:has(ol-icon,svg,img){position:static;width:auto;height:auto;margin:0;padding:0;overflow:visible;clip:auto;clip-path:none;white-space:normal;font-size:0;line-height:0}.ol-btn-loading ::ng-deep ol-icon,.ol-btn-loading ::ng-deep>svg:not(.animate-spin),.ol-btn-loading ::ng-deep>img{display:none!important}.ol-btn-ico-14 ::ng-deep>svg:not(.animate-spin),.ol-btn-ico-14 ::ng-deep>img{width:14px;height:14px}.ol-btn-ico-16 ::ng-deep>svg:not(.animate-spin),.ol-btn-ico-16 ::ng-deep>img{width:16px;height:16px}.ol-btn-ico-20 ::ng-deep>svg:not(.animate-spin),.ol-btn-ico-20 ::ng-deep>img{width:20px;height:20px}\n"] }]
|
|
2754
2818
|
}], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], pill: [{ type: i0.Input, args: [{ isSignal: true, alias: "pill", required: false }] }], outline: [{ type: i0.Input, args: [{ isSignal: true, alias: "outline", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaExpanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaExpanded", required: false }] }], ariaControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaControls", required: false }] }], ariaPressed: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaPressed", required: false }] }], btnClick: [{ type: i0.Output, args: ["btnClick"] }] } });
|
|
2755
2819
|
|
|
2820
|
+
/**
|
|
2821
|
+
* Tiêu đề Modal — khớp Figma `ol-modal-header` (node 1149:71403).
|
|
2822
|
+
* Figma có 7 biến thể: Default, Heading & supporting text, Icon + Heading &
|
|
2823
|
+
* helper text, With Logo, Logo & icons, Avatar group, Breadcrumb.
|
|
2824
|
+
* Component lo phần khung dùng chung; nội dung giàu hơn lắp qua <ng-content>.
|
|
2825
|
+
*/
|
|
2826
|
+
/*
|
|
2827
|
+
* GHÉP VỚI `ol-modal`: lắp thẳng vào, KHÔNG phải tắt gì cả.
|
|
2828
|
+
*
|
|
2829
|
+
* `ol-modal` dùng `contentChild(OlModalHeader)` để tự nhận ra có header lồng
|
|
2830
|
+
* vào rồi thôi không dựng phần đầu mặc định (xem `showDefaultHeader`).
|
|
2831
|
+
*
|
|
2832
|
+
* ```html
|
|
2833
|
+
* <ol-modal [(isOpen)]="open">
|
|
2834
|
+
* <ol-modal-header heading="Tiêu đề" (closeClick)="open.set(false)" />
|
|
2835
|
+
* …nội dung…
|
|
2836
|
+
* </ol-modal>
|
|
2837
|
+
* ```
|
|
2838
|
+
*
|
|
2839
|
+
* Trước đây phải tự tắt bằng `title=""` kèm `[dismissible]="false"`, quên một
|
|
2840
|
+
* trong hai là lồng HAI khung header chồng nhau và nút đóng hiện hai lần —
|
|
2841
|
+
* build vẫn xanh nên chỉ nhìn mới thấy.
|
|
2842
|
+
*
|
|
2843
|
+
* Nút đóng: header này có `closeClick` riêng, người dùng tự nối. `ol-modal`
|
|
2844
|
+
* không chen nút vào header lồng ngoài.
|
|
2845
|
+
*/
|
|
2846
|
+
class OlModalHeader {
|
|
2847
|
+
/**
|
|
2848
|
+
* Bậc thẻ tiêu đề (h1–h6). Mặc định 3 — giữ nguyên bậc đang chạy.
|
|
2849
|
+
*
|
|
2850
|
+
* Khoá cứng một bậc thì nơi gọi không sửa được: đặt component này sau
|
|
2851
|
+
* `<h2>` của trang là nhảy bậc.
|
|
2852
|
+
*/
|
|
2853
|
+
headingLevel = input(3, /* @ts-ignore */
|
|
2854
|
+
...(ngDevMode ? [{ debugName: "headingLevel" }] : /* istanbul ignore next */ []));
|
|
2855
|
+
heading = input('', /* @ts-ignore */
|
|
2856
|
+
...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
2857
|
+
/**
|
|
2858
|
+
* Icon đầu tiêu đề. Rỗng thì KHÔNG dựng khung — không để lại thẻ trống.
|
|
2859
|
+
*
|
|
2860
|
+
* Figma `Type=Icon + Heading & helper text` (#1149:71412) vẽ khung 48px bo
|
|
2861
|
+
* 12px, icon 28px bên trong. Đó đúng là `ol-icon-shape` size `lg`, nên dùng
|
|
2862
|
+
* lại thay vì tự vẽ khung.
|
|
2863
|
+
*
|
|
2864
|
+
* `color="white"` chứ KHÔNG `gray`: Figma bind nền
|
|
2865
|
+
* `bg-neutral-primary-medium` (trắng) kèm viền `border-default-medium`, mà
|
|
2866
|
+
* trong `ol-icon-shape` chỉ nhánh `white` có cặp đó — `gray` ra nền xám đặc
|
|
2867
|
+
* và KHÔNG viền. Đo /modal lượt đầu: nền rgb(243,244,246), viền 0px.
|
|
2868
|
+
*/
|
|
2869
|
+
icon = input('', /* @ts-ignore */
|
|
2870
|
+
...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
2871
|
+
/**
|
|
2872
|
+
* Mô tả phụ dưới tiêu đề.
|
|
2873
|
+
*
|
|
2874
|
+
* Tên `description` theo `ol-alert-dialog` — thứ dựng trên chính `ol-modal`
|
|
2875
|
+
* — và `ol-empty`. `helperText` mang nghĩa "chú thích dưới ô nhập" ở họ form
|
|
2876
|
+
* (16 component dùng), đặt cho tiêu đề hộp thoại là lệch nghĩa.
|
|
2877
|
+
*/
|
|
2878
|
+
description = input('', /* @ts-ignore */
|
|
2879
|
+
...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
|
|
2880
|
+
/** @deprecated Dùng `description`. Giữ để không phá mã đang chạy. */
|
|
2881
|
+
supportingText = input('', /* @ts-ignore */
|
|
2882
|
+
...(ngDevMode ? [{ debugName: "supportingText" }] : /* istanbul ignore next */ []));
|
|
2883
|
+
/** @deprecated Dùng `description`. Tên cũ của `ol-drawer-header`. */
|
|
2884
|
+
helperText = input('', /* @ts-ignore */
|
|
2885
|
+
...(ngDevMode ? [{ debugName: "helperText" }] : /* istanbul ignore next */ []));
|
|
2886
|
+
/**
|
|
2887
|
+
* Cái nào được TRUYỀN thì thắng — không phụ thuộc giá trị.
|
|
2888
|
+
*
|
|
2889
|
+
* `ol-badge` từng hoà giải hai input cùng nghĩa bằng cách so với giá trị mặc
|
|
2890
|
+
* định (`theme` thắng trừ khi nó bằng `'gray'`), nên truyền cả hai thì một
|
|
2891
|
+
* cái bị nuốt im lặng. Ở đây chuỗi rỗng là "không truyền", nên chỉ cần lấy
|
|
2892
|
+
* cái đầu tiên khác rỗng.
|
|
2893
|
+
*/
|
|
2894
|
+
descriptionText = computed(() => this.description() || this.supportingText() || this.helperText(), /* @ts-ignore */
|
|
2895
|
+
...(ngDevMode ? [{ debugName: "descriptionText" }] : /* istanbul ignore next */ []));
|
|
2896
|
+
showClose = input(true, /* @ts-ignore */
|
|
2897
|
+
...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
|
|
2898
|
+
closeClick = output();
|
|
2899
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2900
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModalHeader, isStandalone: true, selector: "ol-modal-header", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, supportingText: { classPropertyName: "supportingText", publicName: "supportingText", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, showClose: { classPropertyName: "showClose", publicName: "showClose", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeClick: "closeClick" }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
2901
|
+
<!--
|
|
2902
|
+
PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
|
|
2903
|
+
kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
|
|
2904
|
+
và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
|
|
2905
|
+
CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
|
|
2906
|
+
-->
|
|
2907
|
+
<div class="flex items-start justify-between gap-3 p-3">
|
|
2908
|
+
@if (icon()) {
|
|
2909
|
+
<ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
|
|
2910
|
+
}
|
|
2911
|
+
<div class="min-w-0 flex-1">
|
|
2912
|
+
@if (heading()) {
|
|
2913
|
+
@switch (headingLevel()) {
|
|
2914
|
+
@case (1) {
|
|
2915
|
+
<h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
|
|
2916
|
+
}
|
|
2917
|
+
@case (2) {
|
|
2918
|
+
<h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
|
|
2919
|
+
}
|
|
2920
|
+
@case (4) {
|
|
2921
|
+
<h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
|
|
2922
|
+
}
|
|
2923
|
+
@case (5) {
|
|
2924
|
+
<h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
|
|
2925
|
+
}
|
|
2926
|
+
@case (6) {
|
|
2927
|
+
<h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
|
|
2928
|
+
}
|
|
2929
|
+
@default {
|
|
2930
|
+
<h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
@if (descriptionText()) {
|
|
2935
|
+
<p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
|
|
2936
|
+
}
|
|
2937
|
+
<ng-content />
|
|
2938
|
+
</div>
|
|
2939
|
+
@if (showClose()) {
|
|
2940
|
+
<button
|
|
2941
|
+
type="button"
|
|
2942
|
+
(click)="closeClick.emit()"
|
|
2943
|
+
aria-label="Đóng hộp thoại"
|
|
2944
|
+
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
|
|
2945
|
+
>
|
|
2946
|
+
<ol-icon name="close" size="sm" />
|
|
2947
|
+
</button>
|
|
2948
|
+
}
|
|
2949
|
+
</div>
|
|
2950
|
+
`, isInline: true, dependencies: [{ kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2951
|
+
}
|
|
2952
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, decorators: [{
|
|
2953
|
+
type: Component,
|
|
2954
|
+
args: [{
|
|
2955
|
+
selector: 'ol-modal-header',
|
|
2956
|
+
standalone: true,
|
|
2957
|
+
imports: [OlIcon, OlIconShape],
|
|
2958
|
+
template: `
|
|
2959
|
+
<!--
|
|
2960
|
+
PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
|
|
2961
|
+
kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
|
|
2962
|
+
và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
|
|
2963
|
+
CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
|
|
2964
|
+
-->
|
|
2965
|
+
<div class="flex items-start justify-between gap-3 p-3">
|
|
2966
|
+
@if (icon()) {
|
|
2967
|
+
<ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
|
|
2968
|
+
}
|
|
2969
|
+
<div class="min-w-0 flex-1">
|
|
2970
|
+
@if (heading()) {
|
|
2971
|
+
@switch (headingLevel()) {
|
|
2972
|
+
@case (1) {
|
|
2973
|
+
<h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
|
|
2974
|
+
}
|
|
2975
|
+
@case (2) {
|
|
2976
|
+
<h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
|
|
2977
|
+
}
|
|
2978
|
+
@case (4) {
|
|
2979
|
+
<h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
|
|
2980
|
+
}
|
|
2981
|
+
@case (5) {
|
|
2982
|
+
<h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
|
|
2983
|
+
}
|
|
2984
|
+
@case (6) {
|
|
2985
|
+
<h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
|
|
2986
|
+
}
|
|
2987
|
+
@default {
|
|
2988
|
+
<h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
@if (descriptionText()) {
|
|
2993
|
+
<p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
|
|
2994
|
+
}
|
|
2995
|
+
<ng-content />
|
|
2996
|
+
</div>
|
|
2997
|
+
@if (showClose()) {
|
|
2998
|
+
<button
|
|
2999
|
+
type="button"
|
|
3000
|
+
(click)="closeClick.emit()"
|
|
3001
|
+
aria-label="Đóng hộp thoại"
|
|
3002
|
+
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
|
|
3003
|
+
>
|
|
3004
|
+
<ol-icon name="close" size="sm" />
|
|
3005
|
+
</button>
|
|
3006
|
+
}
|
|
3007
|
+
</div>
|
|
3008
|
+
`,
|
|
3009
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3010
|
+
host: { class: 'block' },
|
|
3011
|
+
}]
|
|
3012
|
+
}], propDecorators: { headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], supportingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "supportingText", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], showClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClose", required: false }] }], closeClick: [{ type: i0.Output, args: ["closeClick"] }] } });
|
|
3013
|
+
|
|
2756
3014
|
class OlModal {
|
|
2757
3015
|
focusTrap = inject(OlFocusTrapService);
|
|
2758
3016
|
destroyRef = inject(DestroyRef);
|
|
@@ -2781,10 +3039,81 @@ class OlModal {
|
|
|
2781
3039
|
...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
2782
3040
|
title = input('', /* @ts-ignore */
|
|
2783
3041
|
...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
3042
|
+
/**
|
|
3043
|
+
* Icon đứng trước tiêu đề ở phần đầu mặc định.
|
|
3044
|
+
*
|
|
3045
|
+
* Thêm 2026-09-24 cùng `description`. Trước đó `ol-modal` chỉ có `title`,
|
|
3046
|
+
* nên muốn có icon hay dòng mô tả là phải lắp `ol-modal-header` thủ công —
|
|
3047
|
+
* và lắp rồi thì `title` bị bỏ qua, phải khai lại qua `heading`. Đo trên
|
|
3048
|
+
* /modal: 13 chỗ dùng modal, chỗ nào cần icon đều lặp lại đúng khuôn đó.
|
|
3049
|
+
*
|
|
3050
|
+
* Vẫn dùng `ol-modal-header` được khi cần thứ phần đầu mặc định không có
|
|
3051
|
+
* (`supportingText`, `helperText`, nút tuỳ biến); lắp nó vào thì phần đầu
|
|
3052
|
+
* mặc định tự tắt, xem `showDefaultHeader`.
|
|
3053
|
+
*/
|
|
3054
|
+
icon = input('', /* @ts-ignore */
|
|
3055
|
+
...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
3056
|
+
/** Dòng mô tả dưới tiêu đề ở phần đầu mặc định. Xem chú thích ở `icon`. */
|
|
3057
|
+
description = input('', /* @ts-ignore */
|
|
3058
|
+
...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
|
|
2784
3059
|
size = input('base', /* @ts-ignore */
|
|
2785
3060
|
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
3061
|
+
/**
|
|
3062
|
+
* Cho phép đóng hộp thoại bằng Escape hoặc bấm ra nền mờ.
|
|
3063
|
+
*
|
|
3064
|
+
* Tắt khi người dùng BUỘC phải chọn một phương án — xác nhận xoá, đồng ý
|
|
3065
|
+
* điều khoản. Không còn ẩn nút X nữa: xem `showClose`.
|
|
3066
|
+
*/
|
|
2786
3067
|
dismissible = input(true, /* @ts-ignore */
|
|
2787
3068
|
...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
|
|
3069
|
+
/**
|
|
3070
|
+
* Có vẽ nút X ở góc phải phần đầu không. Mặc định **theo `dismissible`**.
|
|
3071
|
+
*
|
|
3072
|
+
* TÁCH KHỎI `dismissible` (2026-09-24). Trước đây một input gánh BỐN việc —
|
|
3073
|
+
* hiện nút X, đóng bằng Escape, đóng khi bấm nền, và quyết định có dựng
|
|
3074
|
+
* phần đầu — nên `[dismissible]="false"` (chỉ định chặn bấm-ra-ngoài) kéo
|
|
3075
|
+
* theo MẤT nút X. Người dùng phải tự lắp `ol-modal-header` để lấy lại nút,
|
|
3076
|
+
* tức phải khai lại cả tiêu đề vốn đã truyền qua `title`.
|
|
3077
|
+
*
|
|
3078
|
+
* Nay ba trường hợp tách bạch:
|
|
3079
|
+
*
|
|
3080
|
+
* `[dismissible]="false"` chặn Escape + nền mờ, VẪN có nút X
|
|
3081
|
+
* `[dismissible]="false" [showClose]="false"` không lối thoát nào ngoài nút trong nội dung
|
|
3082
|
+
* `[showClose]="false"` giấu nút X, vẫn đóng được bằng Escape/nền
|
|
3083
|
+
*
|
|
3084
|
+
* Mặc định `true` — nút X LUÔN hiện trừ khi tắt tường minh.
|
|
3085
|
+
*
|
|
3086
|
+
* ĐÂY LÀ THAY ĐỔI PHÁ VỠ so với bản trước: `[dismissible]="false"` vốn ẩn
|
|
3087
|
+
* nút X, nay vẫn giữ. Nơi nào thật sự muốn không có lối thoát nào phải thêm
|
|
3088
|
+
* `[showClose]="false"`. Đổi vì hành vi cũ là LỖI chứ không phải lựa chọn:
|
|
3089
|
+
* người dùng chặn bấm-ra-ngoài rồi mất nút đóng, phải tự lắp
|
|
3090
|
+
* `ol-modal-header` để lấy lại — tức khai lại tiêu đề đã truyền qua `title`.
|
|
3091
|
+
*/
|
|
3092
|
+
showClose = input(true, /* @ts-ignore */
|
|
3093
|
+
...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
|
|
3094
|
+
/** Nút X có hiện không. Tách hẳn khỏi `dismissible` từ 2026-09-24. */
|
|
3095
|
+
closeVisible = computed(() => this.showClose(), /* @ts-ignore */
|
|
3096
|
+
...(ngDevMode ? [{ debugName: "closeVisible" }] : /* istanbul ignore next */ []));
|
|
3097
|
+
/**
|
|
3098
|
+
* Có `<ol-modal-header>` lồng bên trong không.
|
|
3099
|
+
*
|
|
3100
|
+
* Dùng để KHÔNG dựng phần đầu mặc định khi người dùng đã tự lắp header
|
|
3101
|
+
* riêng. Trước đây phải tắt tay bằng `title=""` kèm `[dismissible]="false"`
|
|
3102
|
+
* — quên một trong hai là lồng HAI khung header chồng nhau, mà build vẫn
|
|
3103
|
+
* xanh nên chỉ nhìn mới thấy.
|
|
3104
|
+
*
|
|
3105
|
+
* `descendants: false` để chỉ bắt header là CON TRỰC TIẾP. Một `ol-modal`
|
|
3106
|
+
* lồng trong nội dung của modal này thì header của nó không tính.
|
|
3107
|
+
*/
|
|
3108
|
+
projectedHeader = contentChild(OlModalHeader, { ...(ngDevMode ? { debugName: "projectedHeader" } : /* istanbul ignore next */ {}), descendants: false });
|
|
3109
|
+
/**
|
|
3110
|
+
* Có dựng phần đầu mặc định không.
|
|
3111
|
+
*
|
|
3112
|
+
* Người dùng lắp `ol-modal-header` riêng thì thôi — nếu không sẽ có hai
|
|
3113
|
+
* khung `p-3` chồng nhau, nút đóng hiện hai lần.
|
|
3114
|
+
*/
|
|
3115
|
+
showDefaultHeader = computed(() => !this.projectedHeader() && (this.title() !== '' || this.closeVisible()), /* @ts-ignore */
|
|
3116
|
+
...(ngDevMode ? [{ debugName: "showDefaultHeader" }] : /* istanbul ignore next */ []));
|
|
2788
3117
|
backdrop = input('dynamic', /* @ts-ignore */
|
|
2789
3118
|
...(ngDevMode ? [{ debugName: "backdrop" }] : /* istanbul ignore next */ []));
|
|
2790
3119
|
placement = input('center', /* @ts-ignore */
|
|
@@ -2891,12 +3220,12 @@ class OlModal {
|
|
|
2891
3220
|
return this.isOpen();
|
|
2892
3221
|
}
|
|
2893
3222
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModal, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2894
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModal, isStandalone: true, selector: "ol-modal", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, backdrop: { classPropertyName: "backdrop", publicName: "backdrop", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", opened: "opened", closed: "closed" }, viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isOpen()) {\n <div\n (click)=\"onBackdropClick($event)\"\n class=\"fixed inset-0 z-modal overflow-y-auto overflow-x-hidden flex w-full h-full bg-overlay-backdrop backdrop-blur-xs transition-opacity duration-200\"\n [class]=\"placementClasses()\"\n >\n <div\n #panel\n [attr.role]=\"role()\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title() || 'H\u1ED9p tho\u1EA1i'\"\n tabindex=\"-1\"\n class=\"relative w-full max-h-full transition-all duration-200 animate-in fade-in zoom-in-95\"\n [class]=\"sizeClasses()\"\n >\n <div\n class=\"relative bg-neutral-primary rounded-md shadow-2xl border border-default overflow-hidden\"\n >\n
|
|
3223
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModal, isStandalone: true, selector: "ol-modal", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, showClose: { classPropertyName: "showClose", publicName: "showClose", isSignal: true, isRequired: false, transformFunction: null }, backdrop: { classPropertyName: "backdrop", publicName: "backdrop", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", opened: "opened", closed: "closed" }, queries: [{ propertyName: "projectedHeader", first: true, predicate: OlModalHeader, isSignal: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isOpen()) {\n <div\n (click)=\"onBackdropClick($event)\"\n class=\"fixed inset-0 z-modal overflow-y-auto overflow-x-hidden flex w-full h-full bg-overlay-backdrop backdrop-blur-xs transition-opacity duration-200\"\n [class]=\"placementClasses()\"\n >\n <div\n #panel\n [attr.role]=\"role()\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title() || 'H\u1ED9p tho\u1EA1i'\"\n tabindex=\"-1\"\n class=\"relative w-full max-h-full transition-all duration-200 animate-in fade-in zoom-in-95\"\n [class]=\"sizeClasses()\"\n >\n <div\n class=\"relative bg-neutral-primary rounded-md shadow-2xl border border-default overflow-hidden\"\n >\n <!--\n Ph\u1EA7n \u0111\u1EA7u M\u1EB6C \u0110\u1ECANH \u2014 ch\u1EC9 d\u1EF1ng khi ng\u01B0\u1EDDi d\u00F9ng KH\u00D4NG t\u1EF1 l\u1EAFp\n `<ol-modal-header>`. Xem `showDefaultHeader` trong `.ts`: l\u1EAFp c\u1EA3 hai\n l\u00E0 hai khung `p-3` ch\u1ED3ng nhau v\u00E0 n\u00FAt \u0111\u00F3ng hi\u1EC7n hai l\u1EA7n.\n -->\n <!--\n Ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh D\u00D9NG L\u1EA0I `ol-modal-header` thay v\u00EC d\u1EF1ng l\u1EA1i s\u00E1u\n nh\u00E1nh `@switch` cho `headingLevel` c\u1ED9ng m\u1ED9t n\u00FAt \u0111\u00F3ng.\n\n B\u1EA3n c\u0169 ch\u00E9p khu\u00F4n: modal c\u00F3 `title` th\u00EC t\u1EF1 v\u1EBD `<h1..h6>` + n\u00FAt X, c\u00F2n\n ai c\u1EA7n `icon` hay `description` ph\u1EA3i l\u1EAFp `ol-modal-header` th\u1EE7 c\u00F4ng \u2014\n m\u00E0 l\u1EAFp r\u1ED3i th\u00EC `title` b\u1ECB b\u1ECF qua, ph\u1EA3i khai l\u1EA1i qua `heading`. Hai\n khu\u00F4n song song n\u00EAn s\u1EEDa m\u1ED9t b\u00EAn l\u00E0 l\u1EC7ch b\u00EAn kia.\n\n Nay `ol-modal` nh\u1EADn th\u1EB3ng `icon`/`description` v\u00E0 chuy\u1EC3n xu\u1ED1ng. L\u1EAFp\n `ol-modal-header` ri\u00EAng v\u1EABn \u0111\u01B0\u1EE3c khi c\u1EA7n th\u1EE9 ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh kh\u00F4ng\n c\u00F3 (`supportingText`, `helperText`) \u2014 l\u00FAc \u0111\u00F3 `showDefaultHeader` t\u1EF1\n t\u1EAFt, kh\u00F4ng c\u00F3 hai khung ch\u1ED3ng nhau.\n -->\n @if (showDefaultHeader()) {\n <ol-modal-header\n [heading]=\"title()\"\n [headingLevel]=\"headingLevel()\"\n [icon]=\"icon()\"\n [description]=\"description()\"\n [showClose]=\"closeVisible()\"\n (closeClick)=\"hide()\"\n />\n }\n\n <!-- Modal Body Content -->\n <!--\n \u0110\u1EC7m 12px B\u1EB0NG header v\u00E0 footer \u2014 ch\u1EEF th\u00E2n b\u00E0i th\u1EB3ng h\u00E0ng v\u1EDBi ti\u00EAu \u0111\u1EC1.\n Tr\u01B0\u1EDBc \u0111\u00E2y `p-6` (24px) trong khi hai ph\u1EA7n kia `p-3`, n\u00EAn n\u1ED9i dung\n th\u1EE5t v\u00E0o s\u00E2u h\u01A1n ti\u00EAu \u0111\u1EC1 \u0111\u00FAng 12px, nh\u00ECn nh\u01B0 l\u1ED7i c\u0103n l\u1EC1.\n -->\n <div class=\"p-3 space-y-4 text-body leading-relaxed text-sm md:text-base\">\n <ng-content />\n </div>\n\n <!--\n Modal Footer Actions Slot.\n\n `[&>[footer]]:*` \u00E1p th\u1EB3ng l\u00EAn PH\u1EA6N T\u1EEC \u0110\u01AF\u1EE2C CHI\u1EBEU V\u00C0O, kh\u00F4ng ch\u1EC9 l\u00EAn\n khung n\u00E0y. N\u01A1i d\u00F9ng g\u1EA7n nh\u01B0 lu\u00F4n b\u1ECDc n\u00FAt trong m\u1ED9t `<div footer>`, m\u00E0\n `gap-3` c\u1EE7a khung th\u00EC \u00E1p gi\u1EEFa khung v\u00E0 c\u00E1c con TR\u1EF0C TI\u1EBEP c\u1EE7a n\u00F3 \u2014\n t\u1EE9c gi\u1EEFa c\u00E1i `div` \u0111\u00F3 v\u00E0 anh em c\u1EE7a n\u00F3, KH\u00D4NG ph\u1EA3i gi\u1EEFa hai n\u00FAt b\u00EAn\n trong. \u0110o /modal ng\u00E0y 2026-09-24: modal \"Giao b\u00E0i t\u1EADp m\u1EDBi\" c\u00F3 hai n\u00FAt\n c\u00E1ch nhau \u0111\u00FAng **0px**, d\u00EDnh li\u1EC1n.\n\n Qu\u00E9t to\u00E0n repo l\u00FAc \u0111\u00F3: 13 ch\u1ED7 d\u00F9ng `<div footer>`, ch\u1EC9 1 ch\u1ED7 t\u1EF1 nh\u1EDB\n th\u00EAm `class=\"flex gap-2\"`; 0 ch\u1ED7 d\u00F9ng `ol-modal-footer`. B\u1EAFt n\u01A1i d\u00F9ng\n nh\u1EDB m\u1ED9t class m\u00E0 12/13 ch\u1ED7 qu\u00EAn th\u00EC \u0111\u00F3 l\u00E0 l\u1ED7i c\u1EE7a API, kh\u00F4ng ph\u1EA3i l\u1ED7i\n ng\u01B0\u1EDDi d\u00F9ng \u2014 n\u00EAn component t\u1EF1 lo.\n\n `justify-end` v\u00E0 `gap-3` gi\u1EEF nguy\u00EAn tr\u00EAn khung \u0111\u1EC3 nh\u00E1nh KH\u00D4NG b\u1ECDc\n `div` (chi\u1EBFu th\u1EB3ng nhi\u1EC1u `ol-button`) v\u1EABn \u0111\u00FAng.\n -->\n <div\n footer-frame\n class=\"p-3 flex items-center justify-end gap-3 [&>[footer]]:flex [&>[footer]]:flex-wrap [&>[footer]]:items-center [&>[footer]]:gap-3\"\n >\n <ng-content select=\"[footer]\">\n <button\n type=\"button\"\n (click)=\"hide()\"\n class=\"px-4 py-2 text-sm font-medium rounded-md text-body bg-neutral-primary border border-default hover:bg-neutral-tertiary cursor-pointer transition-colors focus:outline-hidden focus:ring-2 focus:ring-brand-subtle\"\n >\n \u0110\u00F3ng\n </button>\n </ng-content>\n </div>\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlModalHeader, selector: "ol-modal-header", inputs: ["headingLevel", "heading", "icon", "description", "supportingText", "helperText", "showClose"], outputs: ["closeClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2895
3224
|
}
|
|
2896
3225
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModal, decorators: [{
|
|
2897
3226
|
type: Component,
|
|
2898
|
-
args: [{ selector: 'ol-modal', standalone: true, imports: [CommonModule,
|
|
2899
|
-
}], ctorParameters: () => [], propDecorators: { panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }, { type: i0.Output, args: ["isOpenChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], backdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdrop", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
3227
|
+
args: [{ selector: 'ol-modal', standalone: true, imports: [CommonModule, OlModalHeader], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (isOpen()) {\n <div\n (click)=\"onBackdropClick($event)\"\n class=\"fixed inset-0 z-modal overflow-y-auto overflow-x-hidden flex w-full h-full bg-overlay-backdrop backdrop-blur-xs transition-opacity duration-200\"\n [class]=\"placementClasses()\"\n >\n <div\n #panel\n [attr.role]=\"role()\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title() || 'H\u1ED9p tho\u1EA1i'\"\n tabindex=\"-1\"\n class=\"relative w-full max-h-full transition-all duration-200 animate-in fade-in zoom-in-95\"\n [class]=\"sizeClasses()\"\n >\n <div\n class=\"relative bg-neutral-primary rounded-md shadow-2xl border border-default overflow-hidden\"\n >\n <!--\n Ph\u1EA7n \u0111\u1EA7u M\u1EB6C \u0110\u1ECANH \u2014 ch\u1EC9 d\u1EF1ng khi ng\u01B0\u1EDDi d\u00F9ng KH\u00D4NG t\u1EF1 l\u1EAFp\n `<ol-modal-header>`. Xem `showDefaultHeader` trong `.ts`: l\u1EAFp c\u1EA3 hai\n l\u00E0 hai khung `p-3` ch\u1ED3ng nhau v\u00E0 n\u00FAt \u0111\u00F3ng hi\u1EC7n hai l\u1EA7n.\n -->\n <!--\n Ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh D\u00D9NG L\u1EA0I `ol-modal-header` thay v\u00EC d\u1EF1ng l\u1EA1i s\u00E1u\n nh\u00E1nh `@switch` cho `headingLevel` c\u1ED9ng m\u1ED9t n\u00FAt \u0111\u00F3ng.\n\n B\u1EA3n c\u0169 ch\u00E9p khu\u00F4n: modal c\u00F3 `title` th\u00EC t\u1EF1 v\u1EBD `<h1..h6>` + n\u00FAt X, c\u00F2n\n ai c\u1EA7n `icon` hay `description` ph\u1EA3i l\u1EAFp `ol-modal-header` th\u1EE7 c\u00F4ng \u2014\n m\u00E0 l\u1EAFp r\u1ED3i th\u00EC `title` b\u1ECB b\u1ECF qua, ph\u1EA3i khai l\u1EA1i qua `heading`. Hai\n khu\u00F4n song song n\u00EAn s\u1EEDa m\u1ED9t b\u00EAn l\u00E0 l\u1EC7ch b\u00EAn kia.\n\n Nay `ol-modal` nh\u1EADn th\u1EB3ng `icon`/`description` v\u00E0 chuy\u1EC3n xu\u1ED1ng. L\u1EAFp\n `ol-modal-header` ri\u00EAng v\u1EABn \u0111\u01B0\u1EE3c khi c\u1EA7n th\u1EE9 ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh kh\u00F4ng\n c\u00F3 (`supportingText`, `helperText`) \u2014 l\u00FAc \u0111\u00F3 `showDefaultHeader` t\u1EF1\n t\u1EAFt, kh\u00F4ng c\u00F3 hai khung ch\u1ED3ng nhau.\n -->\n @if (showDefaultHeader()) {\n <ol-modal-header\n [heading]=\"title()\"\n [headingLevel]=\"headingLevel()\"\n [icon]=\"icon()\"\n [description]=\"description()\"\n [showClose]=\"closeVisible()\"\n (closeClick)=\"hide()\"\n />\n }\n\n <!-- Modal Body Content -->\n <!--\n \u0110\u1EC7m 12px B\u1EB0NG header v\u00E0 footer \u2014 ch\u1EEF th\u00E2n b\u00E0i th\u1EB3ng h\u00E0ng v\u1EDBi ti\u00EAu \u0111\u1EC1.\n Tr\u01B0\u1EDBc \u0111\u00E2y `p-6` (24px) trong khi hai ph\u1EA7n kia `p-3`, n\u00EAn n\u1ED9i dung\n th\u1EE5t v\u00E0o s\u00E2u h\u01A1n ti\u00EAu \u0111\u1EC1 \u0111\u00FAng 12px, nh\u00ECn nh\u01B0 l\u1ED7i c\u0103n l\u1EC1.\n -->\n <div class=\"p-3 space-y-4 text-body leading-relaxed text-sm md:text-base\">\n <ng-content />\n </div>\n\n <!--\n Modal Footer Actions Slot.\n\n `[&>[footer]]:*` \u00E1p th\u1EB3ng l\u00EAn PH\u1EA6N T\u1EEC \u0110\u01AF\u1EE2C CHI\u1EBEU V\u00C0O, kh\u00F4ng ch\u1EC9 l\u00EAn\n khung n\u00E0y. N\u01A1i d\u00F9ng g\u1EA7n nh\u01B0 lu\u00F4n b\u1ECDc n\u00FAt trong m\u1ED9t `<div footer>`, m\u00E0\n `gap-3` c\u1EE7a khung th\u00EC \u00E1p gi\u1EEFa khung v\u00E0 c\u00E1c con TR\u1EF0C TI\u1EBEP c\u1EE7a n\u00F3 \u2014\n t\u1EE9c gi\u1EEFa c\u00E1i `div` \u0111\u00F3 v\u00E0 anh em c\u1EE7a n\u00F3, KH\u00D4NG ph\u1EA3i gi\u1EEFa hai n\u00FAt b\u00EAn\n trong. \u0110o /modal ng\u00E0y 2026-09-24: modal \"Giao b\u00E0i t\u1EADp m\u1EDBi\" c\u00F3 hai n\u00FAt\n c\u00E1ch nhau \u0111\u00FAng **0px**, d\u00EDnh li\u1EC1n.\n\n Qu\u00E9t to\u00E0n repo l\u00FAc \u0111\u00F3: 13 ch\u1ED7 d\u00F9ng `<div footer>`, ch\u1EC9 1 ch\u1ED7 t\u1EF1 nh\u1EDB\n th\u00EAm `class=\"flex gap-2\"`; 0 ch\u1ED7 d\u00F9ng `ol-modal-footer`. B\u1EAFt n\u01A1i d\u00F9ng\n nh\u1EDB m\u1ED9t class m\u00E0 12/13 ch\u1ED7 qu\u00EAn th\u00EC \u0111\u00F3 l\u00E0 l\u1ED7i c\u1EE7a API, kh\u00F4ng ph\u1EA3i l\u1ED7i\n ng\u01B0\u1EDDi d\u00F9ng \u2014 n\u00EAn component t\u1EF1 lo.\n\n `justify-end` v\u00E0 `gap-3` gi\u1EEF nguy\u00EAn tr\u00EAn khung \u0111\u1EC3 nh\u00E1nh KH\u00D4NG b\u1ECDc\n `div` (chi\u1EBFu th\u1EB3ng nhi\u1EC1u `ol-button`) v\u1EABn \u0111\u00FAng.\n -->\n <div\n footer-frame\n class=\"p-3 flex items-center justify-end gap-3 [&>[footer]]:flex [&>[footer]]:flex-wrap [&>[footer]]:items-center [&>[footer]]:gap-3\"\n >\n <ng-content select=\"[footer]\">\n <button\n type=\"button\"\n (click)=\"hide()\"\n class=\"px-4 py-2 text-sm font-medium rounded-md text-body bg-neutral-primary border border-default hover:bg-neutral-tertiary cursor-pointer transition-colors focus:outline-hidden focus:ring-2 focus:ring-brand-subtle\"\n >\n \u0110\u00F3ng\n </button>\n </ng-content>\n </div>\n </div>\n </div>\n </div>\n}\n" }]
|
|
3228
|
+
}], ctorParameters: () => [], propDecorators: { panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }, { type: i0.Output, args: ["isOpenChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], showClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClose", required: false }] }], projectedHeader: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlModalHeader), { ...{ descendants: false }, isSignal: true }] }], backdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdrop", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
2900
3229
|
|
|
2901
3230
|
/**
|
|
2902
3231
|
* Hộp thoại bắt buộc trả lời: xoá, huỷ, rời trang khi chưa lưu.
|
|
@@ -3008,12 +3337,22 @@ class OlAlertDialog {
|
|
|
3008
3337
|
}
|
|
3009
3338
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAlertDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3010
3339
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAlertDialog, isStandalone: true, selector: "ol-alert-dialog", inputs: { isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, confirmText: { classPropertyName: "confirmText", publicName: "confirmText", isSignal: true, isRequired: false, transformFunction: null }, cancelText: { classPropertyName: "cancelText", publicName: "cancelText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", confirm: "confirm", cancel: "cancel", closed: "closed" }, ngImport: i0, template: `
|
|
3340
|
+
<!--
|
|
3341
|
+
showClose=false là BẮT BUỘC từ 2026-09-24: showClose đã TÁCH khỏi
|
|
3342
|
+
dismissible, nên chỉ tắt dismissible thì nút X VẪN hiện. Hộp thoại này
|
|
3343
|
+
buộc người dùng chọn một trong hai nút, không có lối thoát thứ ba.
|
|
3344
|
+
|
|
3345
|
+
Hai bẫy đã mắc khi viết chú thích này, ghi lại: chú thích đặt XEN GIỮA
|
|
3346
|
+
danh sách thuộc tính làm hỏng build với NG1002; và dấu nháy ngược trong
|
|
3347
|
+
chú thích đóng template literal sớm, lỗi hiện ở dòng chẳng liên quan.
|
|
3348
|
+
-->
|
|
3011
3349
|
<ol-modal
|
|
3012
3350
|
[isOpen]="isOpen()"
|
|
3013
3351
|
(isOpenChange)="isOpen.set($event)"
|
|
3014
3352
|
[role]="'alertdialog'"
|
|
3015
3353
|
size="sm"
|
|
3016
3354
|
[dismissible]="false"
|
|
3355
|
+
[showClose]="false"
|
|
3017
3356
|
backdrop="static"
|
|
3018
3357
|
(closed)="closed.emit()"
|
|
3019
3358
|
>
|
|
@@ -3038,7 +3377,7 @@ class OlAlertDialog {
|
|
|
3038
3377
|
</ol-button>
|
|
3039
3378
|
</div>
|
|
3040
3379
|
</ol-modal>
|
|
3041
|
-
`, isInline: true, dependencies: [{ kind: "component", type: OlModal, selector: "ol-modal", inputs: ["headingLevel", "isOpen", "title", "size", "dismissible", "backdrop", "placement", "role"], outputs: ["isOpenChange", "opened", "closed"] }, { kind: "component", type: OlButton, selector: "ol-button", inputs: ["color", "size", "pill", "outline", "iconOnly", "loading", "disabled", "type", "ariaLabel", "ariaExpanded", "ariaControls", "ariaPressed"], outputs: ["btnClick"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3380
|
+
`, isInline: true, dependencies: [{ kind: "component", type: OlModal, selector: "ol-modal", inputs: ["headingLevel", "isOpen", "title", "icon", "description", "size", "dismissible", "showClose", "backdrop", "placement", "role"], outputs: ["isOpenChange", "opened", "closed"] }, { kind: "component", type: OlButton, selector: "ol-button", inputs: ["color", "size", "pill", "outline", "iconOnly", "loading", "disabled", "type", "ariaLabel", "ariaExpanded", "ariaControls", "ariaPressed"], outputs: ["btnClick"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3042
3381
|
}
|
|
3043
3382
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAlertDialog, decorators: [{
|
|
3044
3383
|
type: Component,
|
|
@@ -3047,12 +3386,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
3047
3386
|
standalone: true,
|
|
3048
3387
|
imports: [OlModal, OlButton, OlIconShape],
|
|
3049
3388
|
template: `
|
|
3389
|
+
<!--
|
|
3390
|
+
showClose=false là BẮT BUỘC từ 2026-09-24: showClose đã TÁCH khỏi
|
|
3391
|
+
dismissible, nên chỉ tắt dismissible thì nút X VẪN hiện. Hộp thoại này
|
|
3392
|
+
buộc người dùng chọn một trong hai nút, không có lối thoát thứ ba.
|
|
3393
|
+
|
|
3394
|
+
Hai bẫy đã mắc khi viết chú thích này, ghi lại: chú thích đặt XEN GIỮA
|
|
3395
|
+
danh sách thuộc tính làm hỏng build với NG1002; và dấu nháy ngược trong
|
|
3396
|
+
chú thích đóng template literal sớm, lỗi hiện ở dòng chẳng liên quan.
|
|
3397
|
+
-->
|
|
3050
3398
|
<ol-modal
|
|
3051
3399
|
[isOpen]="isOpen()"
|
|
3052
3400
|
(isOpenChange)="isOpen.set($event)"
|
|
3053
3401
|
[role]="'alertdialog'"
|
|
3054
3402
|
size="sm"
|
|
3055
3403
|
[dismissible]="false"
|
|
3404
|
+
[showClose]="false"
|
|
3056
3405
|
backdrop="static"
|
|
3057
3406
|
(closed)="closed.emit()"
|
|
3058
3407
|
>
|
|
@@ -3300,6 +3649,22 @@ class OlBadge {
|
|
|
3300
3649
|
*/
|
|
3301
3650
|
color = input('gray', /* @ts-ignore */
|
|
3302
3651
|
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
3652
|
+
/**
|
|
3653
|
+
* Có vẽ viền không. Tắt thì giữ nguyên nền và màu chữ của `color`.
|
|
3654
|
+
*
|
|
3655
|
+
* Viền là thuộc tính ĐỘC LẬP với màu, nên tách thành input riêng thay vì
|
|
3656
|
+
* thêm giá trị vào `BadgeColor`: làm thành màu thì muốn "brand không viền"
|
|
3657
|
+
* và "danger không viền" phải nhân đôi cả bảng 12 màu thành 24.
|
|
3658
|
+
*
|
|
3659
|
+
* Khác `color="ghost"`: ghost bỏ CẢ nền lẫn viền và ép chữ về `text-heading`
|
|
3660
|
+
* — nó là một màu. `[bordered]="false"` chỉ bỏ viền.
|
|
3661
|
+
*
|
|
3662
|
+
* Class `border` VẪN giữ (đổi thành `border-transparent`) chứ không bỏ hẳn:
|
|
3663
|
+
* badge tính chiều cao theo hộp viền, bỏ đi là badge không viền thấp hơn
|
|
3664
|
+
* badge thường đúng 2px khi đặt cạnh nhau. Cùng lý do với `ghost`.
|
|
3665
|
+
*/
|
|
3666
|
+
bordered = input(true, /* @ts-ignore */
|
|
3667
|
+
...(ngDevMode ? [{ debugName: "bordered" }] : /* istanbul ignore next */ []));
|
|
3303
3668
|
/**
|
|
3304
3669
|
* @deprecated Dùng `color`.
|
|
3305
3670
|
*
|
|
@@ -3455,6 +3820,22 @@ class OlBadge {
|
|
|
3455
3820
|
classes.push('bg-neutral-secondary', 'border-default-medium', 'text-heading');
|
|
3456
3821
|
break;
|
|
3457
3822
|
}
|
|
3823
|
+
/*
|
|
3824
|
+
* `[bordered]="false"` — bỏ viền, GIỮ nền và màu chữ.
|
|
3825
|
+
*
|
|
3826
|
+
* Phải LỌC class viền của màu ra rồi mới thêm `border-transparent`: hai
|
|
3827
|
+
* class viền cùng lúc thì trình duyệt chọn theo thứ tự luật trong file CSS
|
|
3828
|
+
* chứ không theo thứ tự đẩy vào mảng, nên kết quả đổi theo bản build.
|
|
3829
|
+
*
|
|
3830
|
+
* `border` (độ dày) vẫn giữ — xem chú thích ở input `bordered`.
|
|
3831
|
+
*/
|
|
3832
|
+
if (!this.bordered()) {
|
|
3833
|
+
for (let i = classes.length - 1; i >= 0; i--) {
|
|
3834
|
+
if (classes[i].startsWith('border-'))
|
|
3835
|
+
classes.splice(i, 1);
|
|
3836
|
+
}
|
|
3837
|
+
classes.push('border-transparent');
|
|
3838
|
+
}
|
|
3458
3839
|
return classes.join(' ');
|
|
3459
3840
|
}, /* @ts-ignore */
|
|
3460
3841
|
...(ngDevMode ? [{ debugName: "badgeClasses" }] : /* istanbul ignore next */ []));
|
|
@@ -3488,7 +3869,7 @@ class OlBadge {
|
|
|
3488
3869
|
this.dismiss.emit(event);
|
|
3489
3870
|
}
|
|
3490
3871
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3491
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBadge, isStandalone: true, selector: "ol-badge", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null }, leftIcon: { classPropertyName: "leftIcon", publicName: "leftIcon", isSignal: true, isRequired: false, transformFunction: null }, rightIcon: { classPropertyName: "rightIcon", publicName: "rightIcon", isSignal: true, isRequired: false, transformFunction: null }, avatar: { classPropertyName: "avatar", publicName: "avatar", isSignal: true, isRequired: false, transformFunction: null }, avatarText: { classPropertyName: "avatarText", publicName: "avatarText", isSignal: true, isRequired: false, transformFunction: null }, secondaryText: { classPropertyName: "secondaryText", publicName: "secondaryText", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, host: { classAttribute: "inline-flex align-middle max-w-full" }, ngImport: i0, template: "<!--\n `href` bi\u1EBFn nh\u00E3n th\u00E0nh li\u00EAn k\u1EBFt. Ru\u1ED9t ch\u1EC9 c\u00F3 M\u1ED8T `<ng-content />` n\u00EAn kh\u00F4ng\n \u0111\u01B0\u1EE3c nh\u00E2n \u0111\u00F4i cho hai nh\u00E1nh \u2014 Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung v\u00E0o m\u1ED9t ch\u1ED7, nh\u00E2n\n \u0111\u00F4i l\u00E0 m\u1EA5t ru\u1ED9t \u1EDF M\u1ECCI nh\u00E1nh m\u00E0 build v\u1EABn xanh.\n-->\n@if (href()) {\n <a [href]=\"href()\" [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <span [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n}\n\n<ng-template #ruot>\n <!-- 1. Dot indicator -->\n @if (dot() || type() === 'dot') {\n <span class=\"w-1.5 h-1.5 rounded-full shrink-0\" [class]=\"dotClasses()\"></span>\n }\n\n <!-- 2. Avatar -->\n @if (avatar() || avatarText() || type() === 'avatar') {\n <ol-avatar\n [src]=\"avatar() || ''\"\n [initials]=\"avatarText() || ''\"\n [size]=\"size() === 'lg' ? 'xs' : '2xs'\"\n class=\"-ms-1 rounded-full shrink-0 flex items-center justify-center overflow-hidden\"\n />\n }\n\n <!-- 3. Loading Spinner -->\n @if (loading() || type() === 'loader') {\n <svg\n class=\"animate-spin w-3 h-3 shrink-0 text-current\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <!-- 4. Left icon \u2014 nh\u01B0\u1EDDng ch\u1ED7 cho spinner khi \u0111ang loading -->\n @if (leftIcon() && !loading() && type() !== 'loader') {\n <ol-icon [name]=\"leftIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 5. Main content -->\n <span class=\"truncate min-w-0 max-w-full block\"><ng-content /></span>\n\n <!-- 6. Secondary text separator & content -->\n @if (secondaryText() || type() === 'secondary') {\n <span class=\"w-px h-3 bg-current opacity-30 shrink-0 mx-0.5\"></span>\n <span class=\"opacity-80 font-normal truncate max-w-full\">{{ secondaryText() }}</span>\n }\n\n <!-- 7. Right icon -->\n @if (rightIcon()) {\n <ol-icon [name]=\"rightIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 8. Dismiss / Remove button (Figma Close X) -->\n @if (removable()) {\n <button\n type=\"button\"\n (click)=\"onDismiss($event)\"\n class=\"inline-flex items-center justify-center -me-0.5 p-0.5 rounded-xs hover:bg-black/10 dark:hover:bg-white/10 cursor-pointer transition-colors\"\n aria-label=\"G\u1EE1 nh\u00E3n\"\n >\n <svg\n class=\"w-3 h-3 text-current\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3872
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBadge, isStandalone: true, selector: "ol-badge", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null }, leftIcon: { classPropertyName: "leftIcon", publicName: "leftIcon", isSignal: true, isRequired: false, transformFunction: null }, rightIcon: { classPropertyName: "rightIcon", publicName: "rightIcon", isSignal: true, isRequired: false, transformFunction: null }, avatar: { classPropertyName: "avatar", publicName: "avatar", isSignal: true, isRequired: false, transformFunction: null }, avatarText: { classPropertyName: "avatarText", publicName: "avatarText", isSignal: true, isRequired: false, transformFunction: null }, secondaryText: { classPropertyName: "secondaryText", publicName: "secondaryText", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, host: { classAttribute: "inline-flex align-middle max-w-full" }, ngImport: i0, template: "<!--\n `href` bi\u1EBFn nh\u00E3n th\u00E0nh li\u00EAn k\u1EBFt. Ru\u1ED9t ch\u1EC9 c\u00F3 M\u1ED8T `<ng-content />` n\u00EAn kh\u00F4ng\n \u0111\u01B0\u1EE3c nh\u00E2n \u0111\u00F4i cho hai nh\u00E1nh \u2014 Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung v\u00E0o m\u1ED9t ch\u1ED7, nh\u00E2n\n \u0111\u00F4i l\u00E0 m\u1EA5t ru\u1ED9t \u1EDF M\u1ECCI nh\u00E1nh m\u00E0 build v\u1EABn xanh.\n-->\n@if (href()) {\n <a [href]=\"href()\" [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <span [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n}\n\n<ng-template #ruot>\n <!-- 1. Dot indicator -->\n @if (dot() || type() === 'dot') {\n <span class=\"w-1.5 h-1.5 rounded-full shrink-0\" [class]=\"dotClasses()\"></span>\n }\n\n <!-- 2. Avatar -->\n @if (avatar() || avatarText() || type() === 'avatar') {\n <ol-avatar\n [src]=\"avatar() || ''\"\n [initials]=\"avatarText() || ''\"\n [size]=\"size() === 'lg' ? 'xs' : '2xs'\"\n class=\"-ms-1 rounded-full shrink-0 flex items-center justify-center overflow-hidden\"\n />\n }\n\n <!-- 3. Loading Spinner -->\n @if (loading() || type() === 'loader') {\n <svg\n class=\"animate-spin w-3 h-3 shrink-0 text-current\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <!-- 4. Left icon \u2014 nh\u01B0\u1EDDng ch\u1ED7 cho spinner khi \u0111ang loading -->\n @if (leftIcon() && !loading() && type() !== 'loader') {\n <ol-icon [name]=\"leftIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 5. Main content -->\n <span class=\"truncate min-w-0 max-w-full block\"><ng-content /></span>\n\n <!-- 6. Secondary text separator & content -->\n @if (secondaryText() || type() === 'secondary') {\n <span class=\"w-px h-3 bg-current opacity-30 shrink-0 mx-0.5\"></span>\n <span class=\"opacity-80 font-normal truncate max-w-full\">{{ secondaryText() }}</span>\n }\n\n <!-- 7. Right icon -->\n @if (rightIcon()) {\n <ol-icon [name]=\"rightIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 8. Dismiss / Remove button (Figma Close X) -->\n @if (removable()) {\n <button\n type=\"button\"\n (click)=\"onDismiss($event)\"\n class=\"inline-flex items-center justify-center -me-0.5 p-0.5 rounded-xs hover:bg-black/10 dark:hover:bg-white/10 cursor-pointer transition-colors\"\n aria-label=\"G\u1EE1 nh\u00E3n\"\n >\n <svg\n class=\"w-3 h-3 text-current\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3492
3873
|
}
|
|
3493
3874
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBadge, decorators: [{
|
|
3494
3875
|
type: Component,
|
|
@@ -3503,7 +3884,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
3503
3884
|
// hay không có `min-w-0` badge đều dừng đúng 60px.
|
|
3504
3885
|
class: 'inline-flex align-middle max-w-full',
|
|
3505
3886
|
}, template: "<!--\n `href` bi\u1EBFn nh\u00E3n th\u00E0nh li\u00EAn k\u1EBFt. Ru\u1ED9t ch\u1EC9 c\u00F3 M\u1ED8T `<ng-content />` n\u00EAn kh\u00F4ng\n \u0111\u01B0\u1EE3c nh\u00E2n \u0111\u00F4i cho hai nh\u00E1nh \u2014 Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung v\u00E0o m\u1ED9t ch\u1ED7, nh\u00E2n\n \u0111\u00F4i l\u00E0 m\u1EA5t ru\u1ED9t \u1EDF M\u1ECCI nh\u00E1nh m\u00E0 build v\u1EABn xanh.\n-->\n@if (href()) {\n <a [href]=\"href()\" [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <span [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n}\n\n<ng-template #ruot>\n <!-- 1. Dot indicator -->\n @if (dot() || type() === 'dot') {\n <span class=\"w-1.5 h-1.5 rounded-full shrink-0\" [class]=\"dotClasses()\"></span>\n }\n\n <!-- 2. Avatar -->\n @if (avatar() || avatarText() || type() === 'avatar') {\n <ol-avatar\n [src]=\"avatar() || ''\"\n [initials]=\"avatarText() || ''\"\n [size]=\"size() === 'lg' ? 'xs' : '2xs'\"\n class=\"-ms-1 rounded-full shrink-0 flex items-center justify-center overflow-hidden\"\n />\n }\n\n <!-- 3. Loading Spinner -->\n @if (loading() || type() === 'loader') {\n <svg\n class=\"animate-spin w-3 h-3 shrink-0 text-current\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <!-- 4. Left icon \u2014 nh\u01B0\u1EDDng ch\u1ED7 cho spinner khi \u0111ang loading -->\n @if (leftIcon() && !loading() && type() !== 'loader') {\n <ol-icon [name]=\"leftIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 5. Main content -->\n <span class=\"truncate min-w-0 max-w-full block\"><ng-content /></span>\n\n <!-- 6. Secondary text separator & content -->\n @if (secondaryText() || type() === 'secondary') {\n <span class=\"w-px h-3 bg-current opacity-30 shrink-0 mx-0.5\"></span>\n <span class=\"opacity-80 font-normal truncate max-w-full\">{{ secondaryText() }}</span>\n }\n\n <!-- 7. Right icon -->\n @if (rightIcon()) {\n <ol-icon [name]=\"rightIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 8. Dismiss / Remove button (Figma Close X) -->\n @if (removable()) {\n <button\n type=\"button\"\n (click)=\"onDismiss($event)\"\n class=\"inline-flex items-center justify-center -me-0.5 p-0.5 rounded-xs hover:bg-black/10 dark:hover:bg-white/10 cursor-pointer transition-colors\"\n aria-label=\"G\u1EE1 nh\u00E3n\"\n >\n <svg\n class=\"w-3 h-3 text-current\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n</ng-template>\n" }]
|
|
3506
|
-
}], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], rightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightIcon", required: false }] }], avatar: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatar", required: false }] }], avatarText: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarText", required: false }] }], secondaryText: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondaryText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
|
|
3887
|
+
}], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], rightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightIcon", required: false }] }], avatar: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatar", required: false }] }], avatarText: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarText", required: false }] }], secondaryText: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondaryText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
|
|
3507
3888
|
|
|
3508
3889
|
/**
|
|
3509
3890
|
* Figma `ol-banner` (#1146:21254) KHÔNG có trục màu — Type chỉ là bố cục
|
|
@@ -3734,6 +4115,31 @@ class OlBreadcrumbItem {
|
|
|
3734
4115
|
*/
|
|
3735
4116
|
disabled = input(false, /* @ts-ignore */
|
|
3736
4117
|
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4118
|
+
/**
|
|
4119
|
+
* Cho phép bấm để chuyển trang hay không. Mặc định **bật** — giữ hành vi cũ.
|
|
4120
|
+
*
|
|
4121
|
+
* Tắt khi muốn chặng VẪN hiện như liên kết (chữ xanh khi rê chuột, Tab tới
|
|
4122
|
+
* được, trình đọc màn hình vẫn đọc là liên kết) nhưng KHÔNG tự điều hướng —
|
|
4123
|
+
* ví dụ cần hỏi xác nhận trước khi rời trang đang sửa dở, hoặc nơi dùng tự
|
|
4124
|
+
* xử lý điều hướng qua `(itemClick)`.
|
|
4125
|
+
*
|
|
4126
|
+
* Khác `disabled`: `disabled` bỏ HẲN thẻ `<a>`, chặng thành chữ xám không
|
|
4127
|
+
* bấm được và không Tab tới được. `[clickable]="false"` giữ nguyên vẻ ngoài
|
|
4128
|
+
* và trợ năng của liên kết, chỉ chặn hành vi điều hướng.
|
|
4129
|
+
*
|
|
4130
|
+
* Chặn bằng `preventDefault()` trong `(click)` chứ không bỏ `[routerLink]`:
|
|
4131
|
+
* bỏ binding thì `<a>` mất `href`, trình đọc màn hình không còn coi nó là
|
|
4132
|
+
* liên kết, và người dùng mất luôn khả năng xem đích đến ở thanh trạng thái.
|
|
4133
|
+
*/
|
|
4134
|
+
clickable = input(true, /* @ts-ignore */
|
|
4135
|
+
...(ngDevMode ? [{ debugName: "clickable" }] : /* istanbul ignore next */ []));
|
|
4136
|
+
/**
|
|
4137
|
+
* Phát khi người dùng bấm vào chặng — kể cả khi `[clickable]="false"`.
|
|
4138
|
+
*
|
|
4139
|
+
* Nhờ vậy nơi dùng chặn điều hướng mặc định rồi tự quyết: hỏi xác nhận, lưu
|
|
4140
|
+
* nháp, hay điều hướng bằng đường khác.
|
|
4141
|
+
*/
|
|
4142
|
+
itemClick = output();
|
|
3737
4143
|
parentBreadcrumb = inject(forwardRef(() => OlBreadcrumb), {
|
|
3738
4144
|
optional: true,
|
|
3739
4145
|
});
|
|
@@ -3750,13 +4156,56 @@ class OlBreadcrumbItem {
|
|
|
3750
4156
|
*/
|
|
3751
4157
|
isLink = computed(() => !this.isCurrent() && !this.disabled() && (this.routerLink() !== null || this.href() !== null), /* @ts-ignore */
|
|
3752
4158
|
...(ngDevMode ? [{ debugName: "isLink" }] : /* istanbul ignore next */ []));
|
|
4159
|
+
/**
|
|
4160
|
+
* `href` tĩnh dựng từ `routerLink()`, dùng cho nhánh `clickable=false`.
|
|
4161
|
+
*
|
|
4162
|
+
* Giữ được thuộc tính `href` thật trên thẻ `<a>` nên trợ năng không đổi;
|
|
4163
|
+
* `preventDefault()` trong `onClick` lo phần chặn đi.
|
|
4164
|
+
*/
|
|
4165
|
+
staticHref = computed(() => {
|
|
4166
|
+
const link = this.routerLink();
|
|
4167
|
+
if (link === null)
|
|
4168
|
+
return this.href() ?? '';
|
|
4169
|
+
return Array.isArray(link) ? link.join('/').replace(/\/{2,}/g, '/') : String(link);
|
|
4170
|
+
}, /* @ts-ignore */
|
|
4171
|
+
...(ngDevMode ? [{ debugName: "staticHref" }] : /* istanbul ignore next */ []));
|
|
4172
|
+
/**
|
|
4173
|
+
* Chặn điều hướng khi `[clickable]="false"`, nhưng VẪN phát `itemClick` để
|
|
4174
|
+
* nơi dùng tự xử lý.
|
|
4175
|
+
*
|
|
4176
|
+
* `preventDefault()` chặn cả `routerLink` lẫn `href`: RouterLink bỏ qua sự
|
|
4177
|
+
* kiện đã bị chặn, còn trình duyệt không đi theo `href` nữa.
|
|
4178
|
+
*/
|
|
4179
|
+
onClick(event) {
|
|
4180
|
+
if (!this.clickable())
|
|
4181
|
+
event.preventDefault();
|
|
4182
|
+
this.itemClick.emit(event);
|
|
4183
|
+
}
|
|
3753
4184
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBreadcrumbItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3754
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBreadcrumbItem, isStandalone: true, selector: "ol-breadcrumb-item", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, showSeparator: { classPropertyName: "showSeparator", publicName: "showSeparator", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-template #contentTpl>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span>{{ label() }}</span>\n } @else {\n <span><ng-content /></span>\n }\n</ng-template>\n\n<li class=\"inline-flex items-center gap-1.5\">\n @if (isLink()) {\n @if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else {\n <a\n [href]=\"href()\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n }\n } @else {\n <!--\n Th\u1EE9 t\u1EF1 ba nh\u00E1nh l\u00E0 C\u1ED0 \u00DD: `disabled` \u0111\u1EE9ng TR\u01AF\u1EDAC `isCurrent`. M\u1EE5c cu\u1ED1i m\u00E0\n c\u0169ng b\u1ECB kho\u00E1 th\u00EC ph\u1EA3i hi\u1EC7n l\u00E0 kho\u00E1 \u2014 ng\u01B0\u1EE3c l\u1EA1i ng\u01B0\u1EDDi d\u00F9ng th\u1EA5y n\u00F3 y h\u1EC7t\n trang hi\u1EC7n t\u1EA1i v\u00E0 kh\u00F4ng hi\u1EC3u v\u00EC sao b\u1EA5m kh\u00F4ng \u0111\u01B0\u1EE3c.\n\n M\u00E0u theo chu\u1EA9n disabled c\u1EE7a th\u01B0 vi\u1EC7n (`bg-disabled` + `text-fg-disabled`,\n \u0111\u00FAng Figma State=Disabled, xem button.component.ts). KH\u00D4NG d\u00F9ng\n `opacity-50`: m\u1EABu \u0111\u00F3 ch\u1EC9 c\u00F2n 2 component d\u00F9ng, v\u00E0 th\u01B0 vi\u1EC7n \u0111ang c\u00F3 b\u1ED1n\n b\u1EADc opacity song song ch\u01B0a h\u1ED9i t\u1EE5.\n\n
|
|
4185
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBreadcrumbItem, isStandalone: true, selector: "ol-breadcrumb-item", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, showSeparator: { classPropertyName: "showSeparator", publicName: "showSeparator", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, host: { classAttribute: "inline-flex" }, ngImport: i0, template: "<ng-template #contentTpl>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span>{{ label() }}</span>\n } @else {\n <span><ng-content /></span>\n }\n</ng-template>\n\n<li class=\"inline-flex items-center gap-1.5\">\n @if (isLink()) {\n <!--\n `[clickable]=\"false\"` ph\u1EA3i B\u1ECE H\u1EB2N `[routerLink]`, kh\u00F4ng ch\u1EC9\n `preventDefault()` trong `(click)`.\n\n \u0110\u00E3 th\u1EED c\u00E1ch kia tr\u01B0\u1EDBc: RouterLink t\u1EF1 \u0111\u0103ng k\u00FD handler click c\u1EE7a n\u00F3 v\u00E0\n KH\u00D4NG ki\u1EC3m `event.defaultPrevented`, n\u00EAn n\u00F3 \u0111i\u1EC1u h\u01B0\u1EDBng b\u1EA5t k\u1EC3 m\u00ECnh \u0111\u00E3\n ch\u1EB7n hay ch\u01B0a \u2014 spec `breadcrumb-item-clickable.spec.ts` b\u1EAFt \u0111\u00FAng ch\u1ED7 n\u00E0y.\n\n Thay b\u1EB1ng `href` t\u0129nh d\u1EF1ng t\u1EEB ch\u00EDnh `routerLink()`: th\u1EBB v\u1EABn l\u00E0 `<a>` c\u00F3\n `href` th\u1EADt n\u00EAn tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc l\u00E0 li\u00EAn k\u1EBFt, v\u1EABn Tab t\u1EDBi \u0111\u01B0\u1EE3c,\n v\u1EABn xem \u0111\u01B0\u1EE3c \u0111\u00EDch \u0111\u1EBFn \u1EDF thanh tr\u1EA1ng th\u00E1i \u2014 ch\u1EC9 `preventDefault()` ch\u1EB7n\n tr\u00ECnh duy\u1EC7t \u0111i theo. \u0110\u00F3 l\u00E0 kh\u00E1c bi\u1EC7t v\u1EDBi `disabled`, v\u1ED1n b\u1ECF lu\u00F4n th\u1EBB `<a>`.\n -->\n @if (routerLink() && clickable()) {\n <a\n [routerLink]=\"routerLink()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else if (routerLink()) {\n <a\n [href]=\"staticHref()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else {\n <a\n [href]=\"href()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n }\n } @else {\n <!--\n Th\u1EE9 t\u1EF1 ba nh\u00E1nh l\u00E0 C\u1ED0 \u00DD: `disabled` \u0111\u1EE9ng TR\u01AF\u1EDAC `isCurrent`. M\u1EE5c cu\u1ED1i m\u00E0\n c\u0169ng b\u1ECB kho\u00E1 th\u00EC ph\u1EA3i hi\u1EC7n l\u00E0 kho\u00E1 \u2014 ng\u01B0\u1EE3c l\u1EA1i ng\u01B0\u1EDDi d\u00F9ng th\u1EA5y n\u00F3 y h\u1EC7t\n trang hi\u1EC7n t\u1EA1i v\u00E0 kh\u00F4ng hi\u1EC3u v\u00EC sao b\u1EA5m kh\u00F4ng \u0111\u01B0\u1EE3c.\n\n M\u00E0u theo chu\u1EA9n disabled c\u1EE7a th\u01B0 vi\u1EC7n (`bg-disabled` + `text-fg-disabled`,\n \u0111\u00FAng Figma State=Disabled, xem button.component.ts). KH\u00D4NG d\u00F9ng\n `opacity-50`: m\u1EABu \u0111\u00F3 ch\u1EC9 c\u00F2n 2 component d\u00F9ng, v\u00E0 th\u01B0 vi\u1EC7n \u0111ang c\u00F3 b\u1ED1n\n b\u1EADc opacity song song ch\u01B0a h\u1ED9i t\u1EE5.\n\n M\u1EE5c \u0110ANG \u0110\u1EE8NG d\u00F9ng `text-heading` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 L\u1EC6CH FIGMA C\u00D3 CH\u1EE6 \u00DD\n (2026-09-24, theo y\u00EAu c\u1EA7u ng\u01B0\u1EDDi d\u00F9ng \"cho m\u00E0u \u0111\u1EADm h\u01A1n c\u00E1c m\u00E0u kh\u00E1c\").\n\n Figma `ol-breadcrumb-item` #1148:31348 `Status=Active` bind\n `colors/text/text-body-subtle` = #6B7280. \u0110o /breadcrumb \u1EDF ch\u1EBF \u0111\u1ED9 S\u00C1NG\n th\u00EC n\u00F3 cho t\u01B0\u01A1ng ph\u1EA3n **4,83** trong khi c\u00E1c c\u1EA5p cha (`text-body`\n #4A5565) \u0111\u1EA1t **7,56** \u2014 t\u1EE9c trang \u0111ang \u0111\u1EE9ng NH\u1EA0T H\u01A0N c\u1EA5p cha n\u00F3, ng\u01B0\u1EE3c\n ng\u1EEF ngh\u0129a. Nay `text-heading`: s\u00E1ng #101828 (17,75) \u00B7 t\u1ED1i #FFFFFF\n (20,13), \u0111\u1EADm h\u01A1n h\u1EB3n v\u00E0 t\u1EF1 l\u1EADt theo `.dark`.\n\n B\u1EA3n tr\u01B0\u1EDBc ph\u1EA3i vi\u1EBFt `text-body-subtle dark:text-heading` v\u00EC \u1EDF nh\u00E1nh t\u1ED1i\n `--text-body-subtle` b\u1ECB \u0111\u1EB7t B\u1EB0NG H\u1EC6T `--text-body` (c\u00F9ng gray-300) n\u00EAn\n m\u1EE5c cu\u1ED1i v\u00E0 m\u1EE5c th\u01B0\u1EDDng tr\u00F9ng m\u00E0u. D\u00F9ng `text-heading` cho c\u1EA3 hai ch\u1EBF \u0111\u1ED9\n th\u00EC h\u1EBFt c\u1EA7n ngo\u1EA1i l\u1EC7 \u0111\u00F3.\n\n N\u1EE3: c\u1EADp nh\u1EADt Figma `Status=Active` sang `colors/text/text-heading`.\n -->\n <span\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\n [class]=\"\n disabled()\n ? 'font-medium text-fg-disabled cursor-not-allowed'\n : isCurrent()\n ? 'font-medium text-heading'\n : status() === 'hover'\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\n : 'font-medium text-body'\n \"\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </span>\n }\n\n @if (!isCurrent() && showSeparator()) {\n <!--\n D\u1EA5u ng\u0103n d\u00F9ng `text-body-subtle`, KH\u00D4NG `text-fg-disabled`.\n\n `text-fg-disabled` (#99A1AF) tr\u00EAn n\u1EC1n tr\u1EAFng ch\u1EC9 \u0111\u1EA1t **2,60** \u2014 c\u00F3\n `aria-hidden` n\u00EAn kh\u00F4ng vi ph\u1EA1m WCAG, nh\u01B0ng ng\u01B0\u1EDDi nh\u00ECn k\u00E9m kh\u00F3 th\u1EA5y ranh\n gi\u1EDBi gi\u1EEFa c\u00E1c c\u1EA5p. `text-body-subtle` (#6B7280) \u0111\u1EA1t 4,83 m\u00E0 v\u1EABn nh\u1EA1t h\u01A1n\n ch\u1EEF c\u1EE7a ch\u1EB7ng, n\u00EAn v\u1EABn ra \"d\u1EA5u ng\u0103n\" ch\u1EE9 kh\u00F4ng c\u1EA1nh tranh v\u1EDBi n\u1ED9i dung.\n\n KH\u00C1C d\u00F2ng tr\u00EAn: ch\u1EB7ng `disabled` gi\u1EEF `text-fg-disabled` \u2014 \u0111\u00F3 l\u00E0 chu\u1EA9n\n tr\u1EA1ng th\u00E1i kho\u00E1 c\u1EE7a th\u01B0 vi\u1EC7n, v\u00E0 WCAG mi\u1EC5n tr\u1EEB ph\u1EA7n t\u1EED kh\u00F4ng b\u1EA5m \u0111\u01B0\u1EE3c.\n -->\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\n @switch (effectiveSeparator()) {\n @case ('slash') {\n <span class=\"text-xs font-normal\">/</span>\n }\n @case ('arrow') {\n <span class=\"text-xs\">\u2192</span>\n }\n @case ('dot') {\n <span class=\"text-xs\">\u2022</span>\n }\n @case ('chevron') {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n @default {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n }\n </span>\n }\n</li>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3755
4186
|
}
|
|
3756
4187
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBreadcrumbItem, decorators: [{
|
|
3757
4188
|
type: Component,
|
|
3758
|
-
args: [{ selector: 'ol-breadcrumb-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon],
|
|
3759
|
-
|
|
4189
|
+
args: [{ selector: 'ol-breadcrumb-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], host: {
|
|
4190
|
+
/*
|
|
4191
|
+
* Thẻ tuỳ biến KHÔNG khai `display` thì mặc định là `inline`, và bản cũ
|
|
4192
|
+
* không khai `host` nào cả. Hậu quả: host chứa `<li>` inline-flex nên nó
|
|
4193
|
+
* tính chiều cao theo dòng chữ — cộng thêm khoảng trống dưới baseline cho
|
|
4194
|
+
* phần đuôi chữ. Mục CÓ ICON có `<svg>` 16px đẩy dòng cao hơn, nên host
|
|
4195
|
+
* của nó cao 23px trong khi mục không icon chỉ 20px.
|
|
4196
|
+
*
|
|
4197
|
+
* `<ol>` bên ngoài dùng `items-center`, mà căn giữa ba hộp CAO KHÁC NHAU
|
|
4198
|
+
* thì chữ không thể thẳng hàng: đo /breadcrumb 2026-09-24, chữ của mục có
|
|
4199
|
+
* icon nằm cao hơn hai mục sau đúng **1,5px** — thấy rõ bằng mắt trên
|
|
4200
|
+
* thanh "Trang chủ › Bài tập › Học sinh".
|
|
4201
|
+
*
|
|
4202
|
+
* `inline-flex` làm host thành hộp flex, chiều cao bám đúng nội dung
|
|
4203
|
+
* (20px) và không còn khoảng baseline. Ba host bằng nhau thì `items-center`
|
|
4204
|
+
* căn ra cùng một đường.
|
|
4205
|
+
*/
|
|
4206
|
+
class: 'inline-flex',
|
|
4207
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-template #contentTpl>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span>{{ label() }}</span>\n } @else {\n <span><ng-content /></span>\n }\n</ng-template>\n\n<li class=\"inline-flex items-center gap-1.5\">\n @if (isLink()) {\n <!--\n `[clickable]=\"false\"` ph\u1EA3i B\u1ECE H\u1EB2N `[routerLink]`, kh\u00F4ng ch\u1EC9\n `preventDefault()` trong `(click)`.\n\n \u0110\u00E3 th\u1EED c\u00E1ch kia tr\u01B0\u1EDBc: RouterLink t\u1EF1 \u0111\u0103ng k\u00FD handler click c\u1EE7a n\u00F3 v\u00E0\n KH\u00D4NG ki\u1EC3m `event.defaultPrevented`, n\u00EAn n\u00F3 \u0111i\u1EC1u h\u01B0\u1EDBng b\u1EA5t k\u1EC3 m\u00ECnh \u0111\u00E3\n ch\u1EB7n hay ch\u01B0a \u2014 spec `breadcrumb-item-clickable.spec.ts` b\u1EAFt \u0111\u00FAng ch\u1ED7 n\u00E0y.\n\n Thay b\u1EB1ng `href` t\u0129nh d\u1EF1ng t\u1EEB ch\u00EDnh `routerLink()`: th\u1EBB v\u1EABn l\u00E0 `<a>` c\u00F3\n `href` th\u1EADt n\u00EAn tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc l\u00E0 li\u00EAn k\u1EBFt, v\u1EABn Tab t\u1EDBi \u0111\u01B0\u1EE3c,\n v\u1EABn xem \u0111\u01B0\u1EE3c \u0111\u00EDch \u0111\u1EBFn \u1EDF thanh tr\u1EA1ng th\u00E1i \u2014 ch\u1EC9 `preventDefault()` ch\u1EB7n\n tr\u00ECnh duy\u1EC7t \u0111i theo. \u0110\u00F3 l\u00E0 kh\u00E1c bi\u1EC7t v\u1EDBi `disabled`, v\u1ED1n b\u1ECF lu\u00F4n th\u1EBB `<a>`.\n -->\n @if (routerLink() && clickable()) {\n <a\n [routerLink]=\"routerLink()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else if (routerLink()) {\n <a\n [href]=\"staticHref()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else {\n <a\n [href]=\"href()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n }\n } @else {\n <!--\n Th\u1EE9 t\u1EF1 ba nh\u00E1nh l\u00E0 C\u1ED0 \u00DD: `disabled` \u0111\u1EE9ng TR\u01AF\u1EDAC `isCurrent`. M\u1EE5c cu\u1ED1i m\u00E0\n c\u0169ng b\u1ECB kho\u00E1 th\u00EC ph\u1EA3i hi\u1EC7n l\u00E0 kho\u00E1 \u2014 ng\u01B0\u1EE3c l\u1EA1i ng\u01B0\u1EDDi d\u00F9ng th\u1EA5y n\u00F3 y h\u1EC7t\n trang hi\u1EC7n t\u1EA1i v\u00E0 kh\u00F4ng hi\u1EC3u v\u00EC sao b\u1EA5m kh\u00F4ng \u0111\u01B0\u1EE3c.\n\n M\u00E0u theo chu\u1EA9n disabled c\u1EE7a th\u01B0 vi\u1EC7n (`bg-disabled` + `text-fg-disabled`,\n \u0111\u00FAng Figma State=Disabled, xem button.component.ts). KH\u00D4NG d\u00F9ng\n `opacity-50`: m\u1EABu \u0111\u00F3 ch\u1EC9 c\u00F2n 2 component d\u00F9ng, v\u00E0 th\u01B0 vi\u1EC7n \u0111ang c\u00F3 b\u1ED1n\n b\u1EADc opacity song song ch\u01B0a h\u1ED9i t\u1EE5.\n\n M\u1EE5c \u0110ANG \u0110\u1EE8NG d\u00F9ng `text-heading` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 L\u1EC6CH FIGMA C\u00D3 CH\u1EE6 \u00DD\n (2026-09-24, theo y\u00EAu c\u1EA7u ng\u01B0\u1EDDi d\u00F9ng \"cho m\u00E0u \u0111\u1EADm h\u01A1n c\u00E1c m\u00E0u kh\u00E1c\").\n\n Figma `ol-breadcrumb-item` #1148:31348 `Status=Active` bind\n `colors/text/text-body-subtle` = #6B7280. \u0110o /breadcrumb \u1EDF ch\u1EBF \u0111\u1ED9 S\u00C1NG\n th\u00EC n\u00F3 cho t\u01B0\u01A1ng ph\u1EA3n **4,83** trong khi c\u00E1c c\u1EA5p cha (`text-body`\n #4A5565) \u0111\u1EA1t **7,56** \u2014 t\u1EE9c trang \u0111ang \u0111\u1EE9ng NH\u1EA0T H\u01A0N c\u1EA5p cha n\u00F3, ng\u01B0\u1EE3c\n ng\u1EEF ngh\u0129a. Nay `text-heading`: s\u00E1ng #101828 (17,75) \u00B7 t\u1ED1i #FFFFFF\n (20,13), \u0111\u1EADm h\u01A1n h\u1EB3n v\u00E0 t\u1EF1 l\u1EADt theo `.dark`.\n\n B\u1EA3n tr\u01B0\u1EDBc ph\u1EA3i vi\u1EBFt `text-body-subtle dark:text-heading` v\u00EC \u1EDF nh\u00E1nh t\u1ED1i\n `--text-body-subtle` b\u1ECB \u0111\u1EB7t B\u1EB0NG H\u1EC6T `--text-body` (c\u00F9ng gray-300) n\u00EAn\n m\u1EE5c cu\u1ED1i v\u00E0 m\u1EE5c th\u01B0\u1EDDng tr\u00F9ng m\u00E0u. D\u00F9ng `text-heading` cho c\u1EA3 hai ch\u1EBF \u0111\u1ED9\n th\u00EC h\u1EBFt c\u1EA7n ngo\u1EA1i l\u1EC7 \u0111\u00F3.\n\n N\u1EE3: c\u1EADp nh\u1EADt Figma `Status=Active` sang `colors/text/text-heading`.\n -->\n <span\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\n [class]=\"\n disabled()\n ? 'font-medium text-fg-disabled cursor-not-allowed'\n : isCurrent()\n ? 'font-medium text-heading'\n : status() === 'hover'\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\n : 'font-medium text-body'\n \"\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </span>\n }\n\n @if (!isCurrent() && showSeparator()) {\n <!--\n D\u1EA5u ng\u0103n d\u00F9ng `text-body-subtle`, KH\u00D4NG `text-fg-disabled`.\n\n `text-fg-disabled` (#99A1AF) tr\u00EAn n\u1EC1n tr\u1EAFng ch\u1EC9 \u0111\u1EA1t **2,60** \u2014 c\u00F3\n `aria-hidden` n\u00EAn kh\u00F4ng vi ph\u1EA1m WCAG, nh\u01B0ng ng\u01B0\u1EDDi nh\u00ECn k\u00E9m kh\u00F3 th\u1EA5y ranh\n gi\u1EDBi gi\u1EEFa c\u00E1c c\u1EA5p. `text-body-subtle` (#6B7280) \u0111\u1EA1t 4,83 m\u00E0 v\u1EABn nh\u1EA1t h\u01A1n\n ch\u1EEF c\u1EE7a ch\u1EB7ng, n\u00EAn v\u1EABn ra \"d\u1EA5u ng\u0103n\" ch\u1EE9 kh\u00F4ng c\u1EA1nh tranh v\u1EDBi n\u1ED9i dung.\n\n KH\u00C1C d\u00F2ng tr\u00EAn: ch\u1EB7ng `disabled` gi\u1EEF `text-fg-disabled` \u2014 \u0111\u00F3 l\u00E0 chu\u1EA9n\n tr\u1EA1ng th\u00E1i kho\u00E1 c\u1EE7a th\u01B0 vi\u1EC7n, v\u00E0 WCAG mi\u1EC5n tr\u1EEB ph\u1EA7n t\u1EED kh\u00F4ng b\u1EA5m \u0111\u01B0\u1EE3c.\n -->\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\n @switch (effectiveSeparator()) {\n @case ('slash') {\n <span class=\"text-xs font-normal\">/</span>\n }\n @case ('arrow') {\n <span class=\"text-xs\">\u2192</span>\n }\n @case ('dot') {\n <span class=\"text-xs\">\u2022</span>\n }\n @case ('chevron') {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n @default {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n }\n </span>\n }\n</li>\n" }]
|
|
4208
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], current: [{ type: i0.Input, args: [{ isSignal: true, alias: "current", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], showSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeparator", required: false }] }], separator: [{ type: i0.Input, args: [{ isSignal: true, alias: "separator", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
|
|
3760
4209
|
|
|
3761
4210
|
class OlDownloadAppButton {
|
|
3762
4211
|
store = input('appstore', /* @ts-ignore */
|
|
@@ -5231,9 +5680,13 @@ class OlDatepickerPopup {
|
|
|
5231
5680
|
/**
|
|
5232
5681
|
* Trần chiều cao + cuộn CHỈ cho popup nổi.
|
|
5233
5682
|
*
|
|
5234
|
-
* Dùng `[class]` trả chuỗi chứ KHÔNG `[class.max-h-…]` với giá trị tuỳ ý: tên
|
|
5235
|
-
* chứa dấu chấm và ngoặc vuông nên Angular cắt hỏng tên khi dùng class
|
|
5683
|
+
* Dùng `[class]` trả chuỗi chứ KHÔNG `[class.max-h-…]` với giá trị tuỳ ý: tên
|
|
5684
|
+
* class chứa dấu chấm và ngoặc vuông nên Angular cắt hỏng tên khi dùng class
|
|
5236
5685
|
* binding từng cái (`[class.h-1.5]` ra `h-1`).
|
|
5686
|
+
*
|
|
5687
|
+
* Dấu `…` ở trên là cố ý, không phải ba chấm ASCII: Tailwind quét file thô,
|
|
5688
|
+
* không phân biệt chú thích với mã, nên tên class đầy đủ viết trong chú thích
|
|
5689
|
+
* cũng bị sinh thành luật CSS và làm build cảnh báo `Unexpected token Delim`.
|
|
5237
5690
|
*/
|
|
5238
5691
|
wrapperClasses = computed(() => this.inline() ? '' : 'max-h-[var(--ol-dp-max-height,85svh)] overflow-y-auto overscroll-contain', /* @ts-ignore */
|
|
5239
5692
|
...(ngDevMode ? [{ debugName: "wrapperClasses" }] : /* istanbul ignore next */ []));
|
|
@@ -5503,12 +5956,12 @@ class OlDatepicker {
|
|
|
5503
5956
|
const el = this.panel()?.nativeElement;
|
|
5504
5957
|
const raBody = this.appendTo() === 'body';
|
|
5505
5958
|
if (!open || !el) {
|
|
5506
|
-
this.
|
|
5959
|
+
this.detachPanelNow();
|
|
5507
5960
|
return;
|
|
5508
5961
|
}
|
|
5509
5962
|
// Vào ngăn xếp Escape khi mở. Đăng ký ở đây chứ không trong `openPopup`
|
|
5510
5963
|
// để mọi đường mở popup đều đi qua — kể cả mở bằng bàn phím.
|
|
5511
|
-
this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup());
|
|
5964
|
+
this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup(), (target) => this.ownsTarget(target));
|
|
5512
5965
|
// `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
|
|
5513
5966
|
if (raBody && !this.detachPanel) {
|
|
5514
5967
|
this.detachPanel = this.overlayContainer.attach(el);
|
|
@@ -5692,7 +6145,7 @@ class OlDatepicker {
|
|
|
5692
6145
|
this.state.clear();
|
|
5693
6146
|
}
|
|
5694
6147
|
/** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
|
|
5695
|
-
|
|
6148
|
+
detachPanelNow() {
|
|
5696
6149
|
if (this.stopTrackingHeight) {
|
|
5697
6150
|
this.stopTrackingHeight();
|
|
5698
6151
|
this.stopTrackingHeight = null;
|
|
@@ -5705,37 +6158,67 @@ class OlDatepicker {
|
|
|
5705
6158
|
this.releaseEscape?.();
|
|
5706
6159
|
this.releaseEscape = null;
|
|
5707
6160
|
}
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
6161
|
+
/**
|
|
6162
|
+
* Phần tử được bấm có thuộc datepicker này không.
|
|
6163
|
+
*
|
|
6164
|
+
* Ngăn xếp `OlOverlayContainer` gọi hàm này để biết một cú click là "trong"
|
|
6165
|
+
* hay "ngoài". Trước đây mỗi component tự nghe `document:click`, nên một cú
|
|
6166
|
+
* bấm ra ngoài đóng luôn MỌI lớp — mở datepicker trong modal rồi bấm nền là
|
|
6167
|
+
* mất cả hộp thoại đang điền dở. Đo /modal ngày 2026-09-24 xác nhận.
|
|
6168
|
+
*
|
|
6169
|
+
* ## Phải kiểm CẢ PANEL, không chỉ host
|
|
6170
|
+
*
|
|
6171
|
+
* Với `appendTo="body"` panel đã bị `overlayContainer.attach()` chuyển ra
|
|
6172
|
+
* `.ol-overlay-container` — không còn là con của host, nên click vào chính
|
|
6173
|
+
* popup bị coi là click ra ngoài. Đo /modal: bật `appendTo` rồi bấm một
|
|
6174
|
+
* ngày → popup ĐÓNG NGAY, range picker cần hai ngày nên người dùng không
|
|
6175
|
+
* chọn nổi ngày kết thúc.
|
|
6176
|
+
*
|
|
6177
|
+
* Tắt `appendTo` thì popup ở lại đúng — lỗi chỉ xuất hiện khi treo ra ngoài,
|
|
6178
|
+
* tức đúng ca sinh ra để dùng trong modal.
|
|
6179
|
+
*/
|
|
6180
|
+
ownsTarget(target) {
|
|
6181
|
+
if (!(target instanceof Node))
|
|
6182
|
+
return false;
|
|
6183
|
+
const panelEl = this.panel()?.nativeElement;
|
|
6184
|
+
return this.hostEl.nativeElement.contains(target) || (!!panelEl && panelEl.contains(target));
|
|
6185
|
+
}
|
|
6186
|
+
/**
|
|
6187
|
+
* Đóng popup khi component bị huỷ.
|
|
6188
|
+
*
|
|
6189
|
+
* Bắt buộc với `appendTo="body"`: panel treo vào `<body>` chứ không phải cây
|
|
6190
|
+
* DOM của component, nên Angular huỷ component mà panel VẪN Ở LẠI. Chuyển
|
|
6191
|
+
* trang khi đang mở lịch là để lại một popup mồ côi không ai đóng được.
|
|
6192
|
+
*/
|
|
6193
|
+
ngOnDestroy() {
|
|
6194
|
+
this.closePopup();
|
|
5712
6195
|
/**
|
|
5713
|
-
*
|
|
6196
|
+
* Gọi THẲNG `detachPanelNow()`, không trông vào `effect`.
|
|
5714
6197
|
*
|
|
5715
|
-
*
|
|
5716
|
-
*
|
|
5717
|
-
*
|
|
5718
|
-
*
|
|
5719
|
-
*
|
|
6198
|
+
* Việc trả panel về chỗ cũ nằm trong `effect()` ở constructor. Angular
|
|
6199
|
+
* DỪNG mọi effect của component ngay khi huỷ, nên `closePopup()` ở trên
|
|
6200
|
+
* đặt `isOpen = false` mà effect không bao giờ chạy để dọn.
|
|
6201
|
+
*
|
|
6202
|
+
* Với `appendTo="body"` panel đã treo vào `.ol-overlay-container` — không
|
|
6203
|
+
* còn là con của component — nên Angular xoá cây DOM của component mà panel
|
|
6204
|
+
* VẪN Ở LẠI. Nó mất luôn liên kết với ô nhập nên Floating UI trả về toạ độ
|
|
6205
|
+
* `(0, 0)`: popup nhảy lên góc trái trên màn hình và không cách nào đóng
|
|
6206
|
+
* được vì component đã biến mất. Đúng hiện tượng anh Vinh mô tả.
|
|
5720
6207
|
*
|
|
5721
|
-
*
|
|
5722
|
-
*
|
|
6208
|
+
* Chỉ "thỉnh thoảng" vì nó cần popup ĐANG MỞ lúc chuyển trang; đóng trước
|
|
6209
|
+
* rồi mới chuyển thì effect đã kịp dọn.
|
|
5723
6210
|
*/
|
|
5724
|
-
|
|
5725
|
-
const clickedInside = path.includes(this.hostEl.nativeElement) || (!!panelEl && path.includes(panelEl));
|
|
5726
|
-
if (!clickedInside) {
|
|
5727
|
-
this.closePopup();
|
|
5728
|
-
}
|
|
6211
|
+
this.detachPanelNow();
|
|
5729
6212
|
}
|
|
5730
6213
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDatepicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5731
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDatepicker, isStandalone: true, selector: "ol-datepicker", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", dateChange: "dateChange", timestampChange: "timestampChange", isoChange: "isoChange", formattedChange: "formattedChange" }, host: {
|
|
6214
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDatepicker, isStandalone: true, selector: "ol-datepicker", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", dateChange: "dateChange", timestampChange: "timestampChange", isoChange: "isoChange", formattedChange: "formattedChange" }, host: { properties: { "class.block": "!inline()", "class.w-full": "!inline()", "class.inline-block": "inline()", "class.z-dropdown": "isOpen()" }, classAttribute: "relative" }, providers: [
|
|
5732
6215
|
OlDatepickerStateService,
|
|
5733
6216
|
{
|
|
5734
6217
|
provide: NG_VALUE_ACCESSOR,
|
|
5735
6218
|
useExisting: forwardRef(() => OlDatepicker),
|
|
5736
6219
|
multi: true,
|
|
5737
6220
|
},
|
|
5738
|
-
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-
|
|
6221
|
+
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <!-- Input Trigger Field -->\n <div class=\"relative w-full\">\n <!-- Calendar Icon Prefix -->\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n\n <input\n [id]=\"inputId()\"\n type=\"text\"\n [value]=\"inputValue()\"\n (input)=\"onInputChange($any($event.target).value)\"\n (click)=\"togglePopup($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [class]=\"inputSizeClasses()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:ring-brand-950 shadow-xs transition-colors cursor-pointer\"\n />\n\n <!-- Clear Button or Arrow Dropdown -->\n <div class=\"absolute inset-y-0 inset-e-0 flex items-center pe-3 gap-1\">\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearInput($event)\"\n class=\"text-body-subtle hover:text-heading p-0.5 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <!--\n `rounded-base` PH\u1EA2I kh\u1EDBp bo g\u00F3c c\u1EE7a `ol-datepicker-popup` b\u00EAn trong.\n\n Khung n\u00E0y mang `shadow-2xl` nh\u01B0ng tr\u01B0\u1EDBc \u0111\u00E2y kh\u00F4ng c\u00F3 `rounded` n\u00E0o,\n n\u00EAn b\u00F3ng \u0111\u1ED5 v\u1EBD theo h\u00ECnh CH\u1EEE NH\u1EACT VU\u00D4NG trong khi popup b\u00EAn trong bo\n 12px \u2014 hai g\u00F3c d\u01B0\u1EDBi l\u1ED9 ra hai v\u1EC7t vu\u00F4ng x\u00E1m. \u0110o ng\u00E0y 2026-09-24: hai\n khung c\u00F9ng 294\u00D7374, khung ngo\u00E0i `radius: 0px`, khung trong\n `radius: 12px`.\n -->\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n </div>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlDatepickerPopup, selector: "ol-datepicker-popup", inputs: ["showFooter", "dualMonth", "presets", "inline"], outputs: ["valueChange", "clearClick", "applyClick", "cancelClick"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5739
6222
|
}
|
|
5740
6223
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDatepicker, decorators: [{
|
|
5741
6224
|
type: Component,
|
|
@@ -5775,11 +6258,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
5775
6258
|
'[class.w-full]': '!inline()',
|
|
5776
6259
|
'[class.inline-block]': 'inline()',
|
|
5777
6260
|
'[class.z-dropdown]': 'isOpen()',
|
|
5778
|
-
}, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-
|
|
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 }] }]
|
|
5780
|
-
type: HostListener,
|
|
5781
|
-
args: ['document:click', ['$event']]
|
|
5782
|
-
}] } });
|
|
6261
|
+
}, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <!-- Input Trigger Field -->\n <div class=\"relative w-full\">\n <!-- Calendar Icon Prefix -->\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n\n <input\n [id]=\"inputId()\"\n type=\"text\"\n [value]=\"inputValue()\"\n (input)=\"onInputChange($any($event.target).value)\"\n (click)=\"togglePopup($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [class]=\"inputSizeClasses()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:ring-brand-950 shadow-xs transition-colors cursor-pointer\"\n />\n\n <!-- Clear Button or Arrow Dropdown -->\n <div class=\"absolute inset-y-0 inset-e-0 flex items-center pe-3 gap-1\">\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearInput($event)\"\n class=\"text-body-subtle hover:text-heading p-0.5 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <!--\n `rounded-base` PH\u1EA2I kh\u1EDBp bo g\u00F3c c\u1EE7a `ol-datepicker-popup` b\u00EAn trong.\n\n Khung n\u00E0y mang `shadow-2xl` nh\u01B0ng tr\u01B0\u1EDBc \u0111\u00E2y kh\u00F4ng c\u00F3 `rounded` n\u00E0o,\n n\u00EAn b\u00F3ng \u0111\u1ED5 v\u1EBD theo h\u00ECnh CH\u1EEE NH\u1EACT VU\u00D4NG trong khi popup b\u00EAn trong bo\n 12px \u2014 hai g\u00F3c d\u01B0\u1EDBi l\u1ED9 ra hai v\u1EC7t vu\u00F4ng x\u00E1m. \u0110o ng\u00E0y 2026-09-24: hai\n khung c\u00F9ng 294\u00D7374, khung ngo\u00E0i `radius: 0px`, khung trong\n `radius: 12px`.\n -->\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n </div>\n }\n </div>\n }\n</div>\n" }]
|
|
6262
|
+
}], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], daysDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "daysDisabled", required: false }] }], datesDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesDisabled", required: false }] }], datesEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesEnabled", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooter", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], dateChange: [{ type: i0.Output, args: ["dateChange"] }], timestampChange: [{ type: i0.Output, args: ["timestampChange"] }], isoChange: [{ type: i0.Output, args: ["isoChange"] }], formattedChange: [{ type: i0.Output, args: ["formattedChange"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
|
|
5783
6263
|
|
|
5784
6264
|
class OlDateRangePicker {
|
|
5785
6265
|
state = inject(OlDatepickerStateService);
|
|
@@ -5896,11 +6376,11 @@ class OlDateRangePicker {
|
|
|
5896
6376
|
const el = this.panel()?.nativeElement;
|
|
5897
6377
|
const raBody = this.appendTo() === 'body';
|
|
5898
6378
|
if (!open || !el) {
|
|
5899
|
-
this.
|
|
6379
|
+
this.detachPanelNow();
|
|
5900
6380
|
return;
|
|
5901
6381
|
}
|
|
5902
6382
|
// Vào ngăn xếp Escape — chỉ lớp trên cùng đóng.
|
|
5903
|
-
this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup());
|
|
6383
|
+
this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup(), (target) => this.ownsTarget(target));
|
|
5904
6384
|
// `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
|
|
5905
6385
|
if (raBody && !this.detachPanel) {
|
|
5906
6386
|
this.detachPanel = this.overlayContainer.attach(el);
|
|
@@ -6146,7 +6626,7 @@ class OlDateRangePicker {
|
|
|
6146
6626
|
this.state.clear();
|
|
6147
6627
|
}
|
|
6148
6628
|
/** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
|
|
6149
|
-
|
|
6629
|
+
detachPanelNow() {
|
|
6150
6630
|
if (this.stopTrackingHeight) {
|
|
6151
6631
|
this.stopTrackingHeight();
|
|
6152
6632
|
this.stopTrackingHeight = null;
|
|
@@ -6159,37 +6639,58 @@ class OlDateRangePicker {
|
|
|
6159
6639
|
this.releaseEscape?.();
|
|
6160
6640
|
this.releaseEscape = null;
|
|
6161
6641
|
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6642
|
+
/**
|
|
6643
|
+
* Phần tử được bấm có thuộc bộ chọn khoảng này không.
|
|
6644
|
+
*
|
|
6645
|
+
* Xem chú thích cùng tên ở `ol-datepicker` — hai component dùng chung cơ
|
|
6646
|
+
* chế. Range picker còn nhạy hơn: nó cần HAI cú bấm (ngày đầu, ngày cuối),
|
|
6647
|
+
* nên popup đóng sớm là người dùng không chọn nổi khoảng.
|
|
6648
|
+
*
|
|
6649
|
+
* Phải kiểm cả panel vì với `appendTo="body"` nó đã bị chuyển ra
|
|
6650
|
+
* `.ol-overlay-container`, không còn là con của host.
|
|
6651
|
+
*/
|
|
6652
|
+
ownsTarget(target) {
|
|
6653
|
+
if (!(target instanceof Node))
|
|
6654
|
+
return false;
|
|
6655
|
+
const panelEl = this.panel()?.nativeElement;
|
|
6656
|
+
return this.hostEl.nativeElement.contains(target) || (!!panelEl && panelEl.contains(target));
|
|
6657
|
+
}
|
|
6658
|
+
/**
|
|
6659
|
+
* Đóng popup khi component bị huỷ.
|
|
6660
|
+
*
|
|
6661
|
+
* Bắt buộc với `appendTo="body"`: panel treo vào `<body>` nên Angular huỷ
|
|
6662
|
+
* component mà panel VẪN Ở LẠI. Chuyển trang khi đang mở lịch là để lại một
|
|
6663
|
+
* popup mồ côi không ai đóng được.
|
|
6664
|
+
*/
|
|
6665
|
+
ngOnDestroy() {
|
|
6666
|
+
this.closePopup();
|
|
6166
6667
|
/**
|
|
6167
|
-
*
|
|
6668
|
+
* Gọi THẲNG `detachPanelNow()`, không trông vào `effect`.
|
|
6168
6669
|
*
|
|
6169
|
-
*
|
|
6170
|
-
*
|
|
6171
|
-
*
|
|
6172
|
-
*
|
|
6173
|
-
*
|
|
6670
|
+
* Việc trả panel về chỗ cũ nằm trong `effect()` ở constructor. Angular
|
|
6671
|
+
* DỪNG mọi effect của component ngay khi huỷ, nên `closePopup()` ở trên
|
|
6672
|
+
* đặt `isOpen = false` mà effect không bao giờ chạy để dọn.
|
|
6673
|
+
*
|
|
6674
|
+
* Với `appendTo="body"` panel đã treo vào `.ol-overlay-container` — không
|
|
6675
|
+
* còn là con của component — nên Angular xoá cây DOM của component mà panel
|
|
6676
|
+
* VẪN Ở LẠI. Nó mất luôn liên kết với ô nhập nên Floating UI trả về toạ độ
|
|
6677
|
+
* `(0, 0)`: popup nhảy lên góc trái trên màn hình và không cách nào đóng
|
|
6678
|
+
* được vì component đã biến mất. Đúng hiện tượng anh Vinh mô tả.
|
|
6174
6679
|
*
|
|
6175
|
-
*
|
|
6176
|
-
*
|
|
6680
|
+
* Chỉ "thỉnh thoảng" vì nó cần popup ĐANG MỞ lúc chuyển trang; đóng trước
|
|
6681
|
+
* rồi mới chuyển thì effect đã kịp dọn.
|
|
6177
6682
|
*/
|
|
6178
|
-
|
|
6179
|
-
const clickedInside = path.includes(this.hostEl.nativeElement) || (!!panelEl && path.includes(panelEl));
|
|
6180
|
-
if (!clickedInside) {
|
|
6181
|
-
this.closePopup();
|
|
6182
|
-
}
|
|
6683
|
+
this.detachPanelNow();
|
|
6183
6684
|
}
|
|
6184
6685
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDateRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6185
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDateRangePicker, isStandalone: true, selector: "ol-date-range-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, fromLabel: { classPropertyName: "fromLabel", publicName: "fromLabel", isSignal: true, isRequired: false, transformFunction: null }, toLabel: { classPropertyName: "toLabel", publicName: "toLabel", isSignal: true, isRequired: false, transformFunction: null }, showInfoIcon: { classPropertyName: "showInfoIcon", publicName: "showInfoIcon", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, dualMonth: { classPropertyName: "dualMonth", publicName: "dualMonth", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", rangeChange: "rangeChange" }, host: {
|
|
6686
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDateRangePicker, isStandalone: true, selector: "ol-date-range-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, fromLabel: { classPropertyName: "fromLabel", publicName: "fromLabel", isSignal: true, isRequired: false, transformFunction: null }, toLabel: { classPropertyName: "toLabel", publicName: "toLabel", isSignal: true, isRequired: false, transformFunction: null }, showInfoIcon: { classPropertyName: "showInfoIcon", publicName: "showInfoIcon", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, dualMonth: { classPropertyName: "dualMonth", publicName: "dualMonth", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", rangeChange: "rangeChange" }, host: { properties: { "class.z-dropdown": "isOpen()" }, classAttribute: "relative inline-block" }, providers: [
|
|
6186
6687
|
OlDatepickerStateService,
|
|
6187
6688
|
{
|
|
6188
6689
|
provide: NG_VALUE_ACCESSOR,
|
|
6189
6690
|
useExisting: forwardRef(() => OlDateRangePicker),
|
|
6190
6691
|
multi: true,
|
|
6191
6692
|
},
|
|
6192
|
-
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-
|
|
6693
|
+
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ fromLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-3\"\n [class]=\"inputSizeClasses()\"\n />\n </div>\n </div>\n\n <!-- To Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ toLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-8\"\n [class]=\"inputSizeClasses()\"\n />\n\n <!-- Clear button on the right input -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"absolute inset-y-0 inset-e-0 flex items-center pe-2.5 text-body-subtle hover:text-heading transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n </div>\n } @else {\n <!-- Joined Single Container Variant -->\n @if (label()) {\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <div\n class=\"flex items-center gap-2 bg-neutral-secondary-medium border border-default-medium rounded-md p-1 shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle focus-within:border-brand dark:focus-within:border-brand-subtle transition-all cursor-pointer\"\n [class]=\"joinedWrapperClasses()\"\n (click)=\"togglePopup($event)\"\n >\n <!-- Start Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Separator Arrow -->\n <span class=\"text-fg-disabled text-xs font-medium px-1 select-none\"> \u0111\u1EBFn </span>\n\n <!-- End Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Clear Button -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"text-body-subtle hover:text-heading p-1 mr-1 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n }\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl w-max max-w-[calc(100vw-2rem)]\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n </div>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlDatepickerPopup, selector: "ol-datepicker-popup", inputs: ["showFooter", "dualMonth", "presets", "inline"], outputs: ["valueChange", "clearClick", "applyClick", "cancelClick"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6193
6694
|
}
|
|
6194
6695
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDateRangePicker, decorators: [{
|
|
6195
6696
|
type: Component,
|
|
@@ -6213,11 +6714,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
6213
6714
|
*/
|
|
6214
6715
|
class: 'relative inline-block',
|
|
6215
6716
|
'[class.z-dropdown]': 'isOpen()',
|
|
6216
|
-
}, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-
|
|
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 }] }]
|
|
6218
|
-
type: HostListener,
|
|
6219
|
-
args: ['document:click', ['$event']]
|
|
6220
|
-
}] } });
|
|
6717
|
+
}, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ fromLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-3\"\n [class]=\"inputSizeClasses()\"\n />\n </div>\n </div>\n\n <!-- To Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ toLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-8\"\n [class]=\"inputSizeClasses()\"\n />\n\n <!-- Clear button on the right input -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"absolute inset-y-0 inset-e-0 flex items-center pe-2.5 text-body-subtle hover:text-heading transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n </div>\n } @else {\n <!-- Joined Single Container Variant -->\n @if (label()) {\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <div\n class=\"flex items-center gap-2 bg-neutral-secondary-medium border border-default-medium rounded-md p-1 shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle focus-within:border-brand dark:focus-within:border-brand-subtle transition-all cursor-pointer\"\n [class]=\"joinedWrapperClasses()\"\n (click)=\"togglePopup($event)\"\n >\n <!-- Start Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Separator Arrow -->\n <span class=\"text-fg-disabled text-xs font-medium px-1 select-none\"> \u0111\u1EBFn </span>\n\n <!-- End Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Clear Button -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"text-body-subtle hover:text-heading p-1 mr-1 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n }\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl w-max max-w-[calc(100vw-2rem)]\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n </div>\n }\n </div>\n }\n</div>\n" }]
|
|
6718
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], startPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPlaceholder", required: false }] }], endPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "endPlaceholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], fromLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fromLabel", required: false }] }], toLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toLabel", required: false }] }], showInfoIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showInfoIcon", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], dualMonth: [{ type: i0.Input, args: [{ isSignal: true, alias: "dualMonth", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], daysDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "daysDisabled", required: false }] }], datesDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesDisabled", required: false }] }], datesEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesEnabled", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooter", required: false }] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
|
|
6221
6719
|
|
|
6222
6720
|
class OlDatepickerDirective {
|
|
6223
6721
|
el = inject((ElementRef));
|
|
@@ -7563,10 +8061,10 @@ class OlInput {
|
|
|
7563
8061
|
* lệch 2px so với thiết kế.
|
|
7564
8062
|
*/
|
|
7565
8063
|
paddingClasses = computed(() => {
|
|
7566
|
-
const
|
|
7567
|
-
const
|
|
8064
|
+
const hasLeadingIcon = this.leftIcon() && !this.addOnIcon();
|
|
8065
|
+
const hasTrailingIcon = this.rightIcon() || this.state() === 'error' || this.state() === 'success';
|
|
7568
8066
|
const classes = [];
|
|
7569
|
-
if (!
|
|
8067
|
+
if (!hasLeadingIcon) {
|
|
7570
8068
|
switch (this.size()) {
|
|
7571
8069
|
case 'sm':
|
|
7572
8070
|
classes.push('px-2.5');
|
|
@@ -7584,7 +8082,7 @@ class OlInput {
|
|
|
7584
8082
|
else {
|
|
7585
8083
|
classes.push('ps-2.5');
|
|
7586
8084
|
}
|
|
7587
|
-
if (
|
|
8085
|
+
if (hasTrailingIcon)
|
|
7588
8086
|
classes.push('pe-2.5');
|
|
7589
8087
|
return classes.join(' ');
|
|
7590
8088
|
}, /* @ts-ignore */
|
|
@@ -7610,7 +8108,7 @@ class OlInput {
|
|
|
7610
8108
|
this.disabled.set(isDisabled);
|
|
7611
8109
|
}
|
|
7612
8110
|
/** Gọi từ template mỗi khi giá trị đổi, để form biết. */
|
|
7613
|
-
|
|
8111
|
+
emitChange(val) {
|
|
7614
8112
|
this.value.set(val);
|
|
7615
8113
|
this.onChange(val);
|
|
7616
8114
|
}
|
|
@@ -7638,7 +8136,7 @@ class OlInput {
|
|
|
7638
8136
|
useExisting: forwardRef(() => OlInput),
|
|
7639
8137
|
multi: true,
|
|
7640
8138
|
},
|
|
7641
|
-
], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"
|
|
8139
|
+
], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\n [class]=\"paddingClasses()\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n />\n\n <!--\n N\u00DAT XO\u00C1 \u2014 \u0111\u1EB7t TR\u01AF\u1EDAC icon tr\u1EA1ng th\u00E1i.\n\n Icon l\u1ED7i/th\u00E0nh c\u00F4ng ch\u1EC9 \u0111\u1EC3 \u0111\u1ECDc, n\u00FAt xo\u00E1 th\u00EC b\u1EA5m \u0111\u01B0\u1EE3c; \u0111\u1EB7t n\u00FAt sau th\u00EC \u1EDF\n tr\u1EA1ng th\u00E1i l\u1ED7i n\u00F3 bi\u1EBFn m\u1EA5t, \u0111\u00FAng l\u00FAc ng\u01B0\u1EDDi d\u00F9ng mu\u1ED1n xo\u00E1 \u0111i g\u00F5 l\u1EA1i.\n\n \u1EA8n khi `readonly` ho\u1EB7c `disabled`: \u00F4 kh\u00F4ng s\u1EEDa \u0111\u01B0\u1EE3c th\u00EC n\u00FAt xo\u00E1 l\u00E0 m\u1EDDi\n b\u1EA5m v\u00E0o ch\u1ED7 kh\u00F4ng l\u00E0m g\u00EC.\n -->\n @if (clearable() && value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n class=\"flex shrink-0 cursor-pointer items-center rounded-full p-0.5 pe-3 text-body-subtle transition-colors hover:text-heading\"\n [attr.aria-label]=\"clearAriaLabel()\"\n (click)=\"clearValue($event)\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n\n <!-- Right Icon / Status Icon -->\n @if (state() === 'error') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"sm\" />\n </div>\n } @else if (state() === 'success') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-success\">\n <ol-icon name=\"check-circle\" size=\"sm\" />\n </div>\n } @else if (rightIcon()) {\n <div class=\"flex items-center pe-3 text-body-subtle shrink-0\">\n <ng-content select=\"[rightIcon]\" />\n </div>\n }\n\n <!-- Suffix Add-on Text -->\n @if (suffixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-s border-default-medium select-none shrink-0\"\n >\n {{ suffixText() }}\n </span>\n }\n </div>\n\n <!-- Helper Text & Right Helper Action -->\n @if (helperText() || rightHelperText()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [id]=\"helperId()\"\n [attr.role]=\"state() === 'error' ? 'alert' : null\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger]=\"state() === 'error'\"\n [class.text-fg-success]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (rightHelperText()) {\n <span class=\"text-xs text-fg-brand hover:underline cursor-pointer ms-auto font-medium\">\n {{ rightHelperText() }}\n </span>\n }\n <ng-content select=\"[rightHelperAction]\" />\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7642
8140
|
}
|
|
7643
8141
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlInput, decorators: [{
|
|
7644
8142
|
type: Component,
|
|
@@ -7654,7 +8152,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
7654
8152
|
useExisting: forwardRef(() => OlInput),
|
|
7655
8153
|
multi: true,
|
|
7656
8154
|
},
|
|
7657
|
-
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"
|
|
8155
|
+
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\n [class]=\"paddingClasses()\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n />\n\n <!--\n N\u00DAT XO\u00C1 \u2014 \u0111\u1EB7t TR\u01AF\u1EDAC icon tr\u1EA1ng th\u00E1i.\n\n Icon l\u1ED7i/th\u00E0nh c\u00F4ng ch\u1EC9 \u0111\u1EC3 \u0111\u1ECDc, n\u00FAt xo\u00E1 th\u00EC b\u1EA5m \u0111\u01B0\u1EE3c; \u0111\u1EB7t n\u00FAt sau th\u00EC \u1EDF\n tr\u1EA1ng th\u00E1i l\u1ED7i n\u00F3 bi\u1EBFn m\u1EA5t, \u0111\u00FAng l\u00FAc ng\u01B0\u1EDDi d\u00F9ng mu\u1ED1n xo\u00E1 \u0111i g\u00F5 l\u1EA1i.\n\n \u1EA8n khi `readonly` ho\u1EB7c `disabled`: \u00F4 kh\u00F4ng s\u1EEDa \u0111\u01B0\u1EE3c th\u00EC n\u00FAt xo\u00E1 l\u00E0 m\u1EDDi\n b\u1EA5m v\u00E0o ch\u1ED7 kh\u00F4ng l\u00E0m g\u00EC.\n -->\n @if (clearable() && value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n class=\"flex shrink-0 cursor-pointer items-center rounded-full p-0.5 pe-3 text-body-subtle transition-colors hover:text-heading\"\n [attr.aria-label]=\"clearAriaLabel()\"\n (click)=\"clearValue($event)\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n\n <!-- Right Icon / Status Icon -->\n @if (state() === 'error') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"sm\" />\n </div>\n } @else if (state() === 'success') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-success\">\n <ol-icon name=\"check-circle\" size=\"sm\" />\n </div>\n } @else if (rightIcon()) {\n <div class=\"flex items-center pe-3 text-body-subtle shrink-0\">\n <ng-content select=\"[rightIcon]\" />\n </div>\n }\n\n <!-- Suffix Add-on Text -->\n @if (suffixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-s border-default-medium select-none shrink-0\"\n >\n {{ suffixText() }}\n </span>\n }\n </div>\n\n <!-- Helper Text & Right Helper Action -->\n @if (helperText() || rightHelperText()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [id]=\"helperId()\"\n [attr.role]=\"state() === 'error' ? 'alert' : null\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger]=\"state() === 'error'\"\n [class.text-fg-success]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (rightHelperText()) {\n <span class=\"text-xs text-fg-brand hover:underline cursor-pointer ms-auto font-medium\">\n {{ rightHelperText() }}\n </span>\n }\n <ng-content select=\"[rightHelperAction]\" />\n </div>\n }\n</div>\n" }]
|
|
7658
8156
|
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], labelIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelIcon", required: false }] }], labelTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelTooltip", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], prefixText: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixText", required: false }] }], suffixText: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixText", required: false }] }], addOnIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOnIcon", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], rightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightIcon", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearAriaLabel", required: false }] }], cleared: [{ type: i0.Output, args: ["cleared"] }], rightHelperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightHelperText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }] } });
|
|
7659
8157
|
|
|
7660
8158
|
let nextTextareaId = 0;
|
|
@@ -7738,7 +8236,7 @@ class OlTextarea {
|
|
|
7738
8236
|
this.disabled.set(isDisabled);
|
|
7739
8237
|
}
|
|
7740
8238
|
/** Gọi từ template mỗi khi giá trị đổi, để form biết. */
|
|
7741
|
-
|
|
8239
|
+
emitChange(val) {
|
|
7742
8240
|
this.value.set(val);
|
|
7743
8241
|
this.onChange(val);
|
|
7744
8242
|
}
|
|
@@ -7753,7 +8251,7 @@ class OlTextarea {
|
|
|
7753
8251
|
useExisting: forwardRef(() => OlTextarea),
|
|
7754
8252
|
multi: true,
|
|
7755
8253
|
},
|
|
7756
|
-
], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"
|
|
8254
|
+
], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm rounded-base outline-none border font-normal shadow-xs\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n ></textarea>\n </div>\n } @else {\n <!--\n Figma Ready-to-use Comment / Reply Textarea Card (`Type=Chatroom`).\n\n `rounded-md` (12px) ch\u1EE9 kh\u00F4ng `rounded-lg` (16px): \u0111\u1ECDc Figma\n `ol-textarea` bi\u1EBFn th\u1EC3 `Type=Chatroom, State=Initial` ra **radius 12** \u2014\n c\u00F9ng b\u1EADc v\u1EDBi `ol-input` v\u00E0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD. \u0110o trang /forms\n 2026-09-17: `ol-textarea` l\u00E0 component DUY NH\u1EA4T trong h\u1ECD c\u00F3 hai gi\u00E1 tr\u1ECB\n bo g\u00F3c (12px v\u00E0 16px), l\u1EC7ch v\u1EDBi 9 component c\u00F2n l\u1EA1i.\n -->\n <div\n class=\"border border-default-medium rounded-md bg-neutral-primary overflow-hidden shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle dark:focus-within:ring-brand-950 focus-within:border-brand\"\n >\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() || 'Vi\u1EBFt b\u00ECnh lu\u1EADn ho\u1EB7c ph\u1EA3n h\u1ED3i...'\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm text-body bg-transparent border-0 outline-none resize-none placeholder:text-body-subtle\"\n ></textarea>\n <!-- Bottom Action Bar -->\n <div\n class=\"flex items-center justify-between px-3 py-2 border-t border-default-medium bg-neutral-secondary-medium\"\n >\n <div class=\"flex items-center gap-1 text-body-subtle\">\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"\u0110\u00EDnh k\u00E8m t\u1EC7p\"\n >\n <ol-icon name=\"link\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Ch\u00E8n v\u1ECB tr\u00ED/h\u00ECnh \u1EA3nh\"\n >\n <ol-icon name=\"photo\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Bi\u1EC3u t\u01B0\u1EE3ng c\u1EA3m x\u00FAc\"\n >\n <ol-icon name=\"mynaui:smile\" size=\"sm\" />\n </button>\n </div>\n <button\n type=\"button\"\n (click)=\"onSubmit()\"\n [disabled]=\"disabled() || !value().trim()\"\n class=\"inline-flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong disabled:opacity-50 disabled:cursor-not-allowed rounded-lg shadow-xs\"\n >\n <span>G\u1EEDi</span>\n <ol-icon name=\"arrow-right\" size=\"xs\" />\n </button>\n </div>\n </div>\n }\n @if (helperText() || showCharCount()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (showCharCount()) {\n <span class=\"text-body-subtle ms-auto\">\n {{ value().length }}{{ maxLength() ? '/' + maxLength() : '' }} k\u00FD t\u1EF1\n </span>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7757
8255
|
}
|
|
7758
8256
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTextarea, decorators: [{
|
|
7759
8257
|
type: Component,
|
|
@@ -7769,7 +8267,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
7769
8267
|
useExisting: forwardRef(() => OlTextarea),
|
|
7770
8268
|
multi: true,
|
|
7771
8269
|
},
|
|
7772
|
-
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"
|
|
8270
|
+
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm rounded-base outline-none border font-normal shadow-xs\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n ></textarea>\n </div>\n } @else {\n <!--\n Figma Ready-to-use Comment / Reply Textarea Card (`Type=Chatroom`).\n\n `rounded-md` (12px) ch\u1EE9 kh\u00F4ng `rounded-lg` (16px): \u0111\u1ECDc Figma\n `ol-textarea` bi\u1EBFn th\u1EC3 `Type=Chatroom, State=Initial` ra **radius 12** \u2014\n c\u00F9ng b\u1EADc v\u1EDBi `ol-input` v\u00E0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD. \u0110o trang /forms\n 2026-09-17: `ol-textarea` l\u00E0 component DUY NH\u1EA4T trong h\u1ECD c\u00F3 hai gi\u00E1 tr\u1ECB\n bo g\u00F3c (12px v\u00E0 16px), l\u1EC7ch v\u1EDBi 9 component c\u00F2n l\u1EA1i.\n -->\n <div\n class=\"border border-default-medium rounded-md bg-neutral-primary overflow-hidden shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle dark:focus-within:ring-brand-950 focus-within:border-brand\"\n >\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() || 'Vi\u1EBFt b\u00ECnh lu\u1EADn ho\u1EB7c ph\u1EA3n h\u1ED3i...'\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm text-body bg-transparent border-0 outline-none resize-none placeholder:text-body-subtle\"\n ></textarea>\n <!-- Bottom Action Bar -->\n <div\n class=\"flex items-center justify-between px-3 py-2 border-t border-default-medium bg-neutral-secondary-medium\"\n >\n <div class=\"flex items-center gap-1 text-body-subtle\">\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"\u0110\u00EDnh k\u00E8m t\u1EC7p\"\n >\n <ol-icon name=\"link\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Ch\u00E8n v\u1ECB tr\u00ED/h\u00ECnh \u1EA3nh\"\n >\n <ol-icon name=\"photo\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Bi\u1EC3u t\u01B0\u1EE3ng c\u1EA3m x\u00FAc\"\n >\n <ol-icon name=\"mynaui:smile\" size=\"sm\" />\n </button>\n </div>\n <button\n type=\"button\"\n (click)=\"onSubmit()\"\n [disabled]=\"disabled() || !value().trim()\"\n class=\"inline-flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong disabled:opacity-50 disabled:cursor-not-allowed rounded-lg shadow-xs\"\n >\n <span>G\u1EEDi</span>\n <ol-icon name=\"arrow-right\" size=\"xs\" />\n </button>\n </div>\n </div>\n }\n @if (helperText() || showCharCount()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (showCharCount()) {\n <span class=\"text-body-subtle ms-auto\">\n {{ value().length }}{{ maxLength() ? '/' + maxLength() : '' }} k\u00FD t\u1EF1\n </span>\n }\n </div>\n }\n</div>\n" }]
|
|
7773
8271
|
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], showCharCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCharCount", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], commentBox: [{ type: i0.Input, args: [{ isSignal: true, alias: "commentBox", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], submitComment: [{ type: i0.Output, args: ["submitComment"] }] } });
|
|
7774
8272
|
|
|
7775
8273
|
class OlSpinner {
|
|
@@ -8417,7 +8915,7 @@ class OlSelect {
|
|
|
8417
8915
|
const el = this.panel()?.nativeElement;
|
|
8418
8916
|
const raBody = this.appendTo() === 'body';
|
|
8419
8917
|
if (!open || !el) {
|
|
8420
|
-
this.
|
|
8918
|
+
this.detachPanelNow();
|
|
8421
8919
|
return;
|
|
8422
8920
|
}
|
|
8423
8921
|
/**
|
|
@@ -8451,11 +8949,11 @@ class OlSelect {
|
|
|
8451
8949
|
this.destroyRef.onDestroy(() => {
|
|
8452
8950
|
if (this.hengio)
|
|
8453
8951
|
clearTimeout(this.hengio);
|
|
8454
|
-
this.
|
|
8952
|
+
this.detachPanelNow();
|
|
8455
8953
|
});
|
|
8456
8954
|
}
|
|
8457
8955
|
/** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
|
|
8458
|
-
|
|
8956
|
+
detachPanelNow() {
|
|
8459
8957
|
if (this.stopTracking) {
|
|
8460
8958
|
this.stopTracking();
|
|
8461
8959
|
this.stopTracking = null;
|
|
@@ -8864,13 +9362,13 @@ class OlSelect {
|
|
|
8864
9362
|
this.searchQuery.set(val);
|
|
8865
9363
|
this.activeIndex.set(0);
|
|
8866
9364
|
this.scrollTop.set(0);
|
|
8867
|
-
this.
|
|
9365
|
+
this.emitSearch(val);
|
|
8868
9366
|
}
|
|
8869
9367
|
/**
|
|
8870
9368
|
* Phát `search` có GỠ NẢY. Gõ "nguyễn văn a" là 12 phím — không gỡ nảy thì
|
|
8871
9369
|
* 12 lượt gọi API, mà chỉ lượt cuối có ích.
|
|
8872
9370
|
*/
|
|
8873
|
-
|
|
9371
|
+
emitSearch(val) {
|
|
8874
9372
|
if (this.hengio) {
|
|
8875
9373
|
clearTimeout(this.hengio);
|
|
8876
9374
|
this.hengio = null;
|
|
@@ -9104,7 +9602,7 @@ class OlSelect {
|
|
|
9104
9602
|
useExisting: forwardRef(() => OlSelect),
|
|
9105
9603
|
multi: true,
|
|
9106
9604
|
},
|
|
9107
|
-
], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }, { propertyName: "labelTemplate", first: true, predicate: ["labelTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "listboxRef", first: true, predicate: ["listboxRef"], descendants: true, isSignal: true }, { propertyName: "oTimPanel", first: true, predicate: ["oTimPanel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9605
|
+
], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }, { propertyName: "labelTemplate", first: true, predicate: ["labelTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "listboxRef", first: true, predicate: ["listboxRef"], descendants: true, isSignal: true }, { propertyName: "oTimPanel", first: true, predicate: ["oTimPanel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-heading]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9108
9606
|
}
|
|
9109
9607
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSelect, decorators: [{
|
|
9110
9608
|
type: Component,
|
|
@@ -9118,7 +9616,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
9118
9616
|
useExisting: forwardRef(() => OlSelect),
|
|
9119
9617
|
multi: true,
|
|
9120
9618
|
},
|
|
9121
|
-
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n" }]
|
|
9619
|
+
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-heading]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n" }]
|
|
9122
9620
|
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], labelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKey", required: false }] }], valueKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueKey", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], menuClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuClass", required: false }] }], matchTriggerWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "matchTriggerWidth", required: false }] }], maxSelectedItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelectedItems", required: false }] }], closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnSelect", required: false }] }], hideSelected: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideSelected", required: false }] }], allowCreate: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCreate", required: false }] }], createText: [{ type: i0.Input, args: [{ isSignal: true, alias: "createText", required: false }] }], groupSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupSeparator", required: false }] }], searchPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPosition", required: false }] }], searchIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchIcon", required: false }] }], searchPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPrefix", required: false }] }], triggerIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerIcon", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], remoteSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteSearch", required: false }] }], minTermLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minTermLength", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], typeToSearchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeToSearchText", required: false }] }], virtualScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScroll", required: false }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: false }] }], bufferAmount: [{ type: i0.Input, args: [{ isSignal: true, alias: "bufferAmount", required: false }] }], search: [{ type: i0.Output, args: ["search"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], added: [{ type: i0.Output, args: ["added"] }], removed: [{ type: i0.Output, args: ["removed"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], optionTemplate: [{
|
|
9123
9621
|
type: ContentChild,
|
|
9124
9622
|
args: ['optionTemplate']
|
|
@@ -10618,6 +11116,24 @@ class OlTimepicker {
|
|
|
10618
11116
|
...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
|
|
10619
11117
|
size = input('base', /* @ts-ignore */
|
|
10620
11118
|
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
11119
|
+
/**
|
|
11120
|
+
* Đệm ngang và cỡ chữ theo BẢNG CHUẨN họ form.
|
|
11121
|
+
*
|
|
11122
|
+
* Trước đây template ghim cứng `px-3.5` (14px cho mọi cỡ) và không đặt cỡ
|
|
11123
|
+
* chữ nào, nên nó thừa kế 16px mặc định. Đo trên /datepicker mục 8 ngày
|
|
11124
|
+
* 2026-09-24, đặt cạnh ba ô kia:
|
|
11125
|
+
*
|
|
11126
|
+
* sm/base timepicker chữ 16px · ba ô kia 14px
|
|
11127
|
+
* mọi cỡ timepicker đệm 14px · input 10/12/14/16px theo cỡ
|
|
11128
|
+
*
|
|
11129
|
+
* Gom vào MỘT chuỗi thay vì nhiều `[class.px-*]`: Angular cắt tên class ở
|
|
11130
|
+
* dấu chấm nên `[class.px-2.5]` render ra `px-2`, lệch 2px so với thiết kế.
|
|
11131
|
+
*/
|
|
11132
|
+
fieldTextClasses = computed(() => {
|
|
11133
|
+
const size = this.size();
|
|
11134
|
+
return `${FIELD_PADDING_X[size]} ${FIELD_TEXT[size]}`;
|
|
11135
|
+
}, /* @ts-ignore */
|
|
11136
|
+
...(ngDevMode ? [{ debugName: "fieldTextClasses" }] : /* istanbul ignore next */ []));
|
|
10621
11137
|
state = input('default', /* @ts-ignore */
|
|
10622
11138
|
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
10623
11139
|
/**
|
|
@@ -10778,7 +11294,7 @@ class OlTimepicker {
|
|
|
10778
11294
|
const el = this.panel()?.nativeElement;
|
|
10779
11295
|
const raBody = this.appendTo() === 'body';
|
|
10780
11296
|
if (!open || !el) {
|
|
10781
|
-
this.
|
|
11297
|
+
this.detachPanelNow();
|
|
10782
11298
|
return;
|
|
10783
11299
|
}
|
|
10784
11300
|
// `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
|
|
@@ -10801,7 +11317,7 @@ class OlTimepicker {
|
|
|
10801
11317
|
});
|
|
10802
11318
|
}
|
|
10803
11319
|
/** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
|
|
10804
|
-
|
|
11320
|
+
detachPanelNow() {
|
|
10805
11321
|
if (this.stopTracking) {
|
|
10806
11322
|
this.stopTracking();
|
|
10807
11323
|
this.stopTracking = null;
|
|
@@ -10966,7 +11482,7 @@ class OlTimepicker {
|
|
|
10966
11482
|
useExisting: forwardRef(() => OlTimepicker),
|
|
10967
11483
|
multi: true,
|
|
10968
11484
|
},
|
|
10969
|
-
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "hourAreaRef", first: true, predicate: ["hourArea"], descendants: true, isSignal: true }, { propertyName: "minuteAreaRef", first: true, predicate: ["minuteArea"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none px-3.5 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlScrollArea, selector: "ol-scroll-area", inputs: ["scrollbar", "axis", "smooth", "innerClass"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11485
|
+
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "hourAreaRef", first: true, predicate: ["hourArea"], descendants: true, isSignal: true }, { propertyName: "minuteAreaRef", first: true, predicate: ["minuteArea"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [ngClass]=\"fieldTextClasses()\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlScrollArea, selector: "ol-scroll-area", inputs: ["scrollbar", "axis", "smooth", "innerClass"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10970
11486
|
}
|
|
10971
11487
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTimepicker, decorators: [{
|
|
10972
11488
|
type: Component,
|
|
@@ -10982,7 +11498,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
10982
11498
|
useExisting: forwardRef(() => OlTimepicker),
|
|
10983
11499
|
multi: true,
|
|
10984
11500
|
},
|
|
10985
|
-
], template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none px-3.5 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
|
|
11501
|
+
], template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [ngClass]=\"fieldTextClasses()\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
|
|
10986
11502
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], errorText: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showPresets: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPresets", required: false }] }], addonType: [{ type: i0.Input, args: [{ isSignal: true, alias: "addonType", required: false }] }], addonText: [{ type: i0.Input, args: [{ isSignal: true, alias: "addonText", required: false }] }], addonIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addonIcon", required: false }] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], hourAreaRef: [{ type: i0.ViewChild, args: ['hourArea', { isSignal: true }] }], minuteAreaRef: [{ type: i0.ViewChild, args: ['minuteArea', { isSignal: true }] }], timeChange: [{ type: i0.Output, args: ["timeChange"] }], timestampChange: [{ type: i0.Output, args: ["timestampChange"] }], isoChange: [{ type: i0.Output, args: ["isoChange"] }], timeObjectChange: [{ type: i0.Output, args: ["timeObjectChange"] }], onDocumentClick: [{
|
|
10987
11503
|
type: HostListener,
|
|
10988
11504
|
args: ['document:click', ['$event']]
|
|
@@ -11019,8 +11535,32 @@ class OlAutocomplete {
|
|
|
11019
11535
|
size = input('base', /* @ts-ignore */
|
|
11020
11536
|
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
11021
11537
|
/** Cỡ đã quy bí danh: `'md'` -> `'base'`. Template so với cái này. */
|
|
11022
|
-
resolvedSize = computed(() =>
|
|
11538
|
+
resolvedSize = computed(() => {
|
|
11539
|
+
const size = this.size();
|
|
11540
|
+
// `md` là BÍ DANH của `base` — gập ở đây để kiểu trả về đúng `FieldSize`,
|
|
11541
|
+
// nhờ vậy tra bảng chuẩn không cần ép kiểu.
|
|
11542
|
+
return size === 'md' ? 'base' : size;
|
|
11543
|
+
}, /* @ts-ignore */
|
|
11023
11544
|
...(ngDevMode ? [{ debugName: "resolvedSize" }] : /* istanbul ignore next */ []));
|
|
11545
|
+
/**
|
|
11546
|
+
* Đệm ngang theo BẢNG CHUẨN họ form. Chỉ dùng khi KHÔNG `multiple` —
|
|
11547
|
+
* chế độ nhiều thẻ có đệm riêng (`px-1.5 py-1`) vì các thẻ tự mang đệm.
|
|
11548
|
+
*
|
|
11549
|
+
* Ba lỗi cũ gộp làm một chỗ này (đo /autocomplete 2026-09-24):
|
|
11550
|
+
* sm `[class.px-2.5]` -> Angular CẮT ở dấu chấm, render `px-2` = 8px
|
|
11551
|
+
* thay vì 10px. Đây là lý do phải gom vào chuỗi, không bind lẻ.
|
|
11552
|
+
* base dùng chung `px-3.5` với `lg` = 14px, bảng chuẩn quy định 12px.
|
|
11553
|
+
* xl có `px-4` nhưng nằm ở `[class.px-4]` rời, dễ lệch khỏi ba cỡ kia.
|
|
11554
|
+
*/
|
|
11555
|
+
fieldPaddingClass = computed(() => FIELD_PADDING_X[this.resolvedSize()], /* @ts-ignore */
|
|
11556
|
+
...(ngDevMode ? [{ debugName: "fieldPaddingClass" }] : /* istanbul ignore next */ []));
|
|
11557
|
+
/**
|
|
11558
|
+
* Cỡ chữ theo BẢNG CHUẨN. Trước đây `[class.text-base]` chỉ khai cho `lg`,
|
|
11559
|
+
* `xl` không có nhánh nào nên rơi về 16px MẶC ĐỊNH của trình duyệt — trùng
|
|
11560
|
+
* giá trị đúng một cách tình cờ, hỏng ngay khi cỡ gốc đổi.
|
|
11561
|
+
*/
|
|
11562
|
+
fieldTextClass = computed(() => FIELD_TEXT[this.resolvedSize()], /* @ts-ignore */
|
|
11563
|
+
...(ngDevMode ? [{ debugName: "fieldTextClass" }] : /* istanbul ignore next */ []));
|
|
11024
11564
|
state = input('default', /* @ts-ignore */
|
|
11025
11565
|
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
11026
11566
|
required = input(false, /* @ts-ignore */
|
|
@@ -11188,7 +11728,7 @@ class OlAutocomplete {
|
|
|
11188
11728
|
const el = this.panel()?.nativeElement;
|
|
11189
11729
|
const raBody = this.appendTo() === 'body';
|
|
11190
11730
|
if (!open || !el) {
|
|
11191
|
-
this.
|
|
11731
|
+
this.detachPanelNow();
|
|
11192
11732
|
return;
|
|
11193
11733
|
}
|
|
11194
11734
|
/**
|
|
@@ -11222,11 +11762,11 @@ class OlAutocomplete {
|
|
|
11222
11762
|
this.destroyRef.onDestroy(() => {
|
|
11223
11763
|
if (this.hengio)
|
|
11224
11764
|
clearTimeout(this.hengio);
|
|
11225
|
-
this.
|
|
11765
|
+
this.detachPanelNow();
|
|
11226
11766
|
});
|
|
11227
11767
|
}
|
|
11228
11768
|
/** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
|
|
11229
|
-
|
|
11769
|
+
detachPanelNow() {
|
|
11230
11770
|
if (this.stopTracking) {
|
|
11231
11771
|
this.stopTracking();
|
|
11232
11772
|
this.stopTracking = null;
|
|
@@ -11394,7 +11934,7 @@ class OlAutocomplete {
|
|
|
11394
11934
|
this.searchQuery.set(val);
|
|
11395
11935
|
this.isOpen.set(true);
|
|
11396
11936
|
this.activeIndex.set(0);
|
|
11397
|
-
this.
|
|
11937
|
+
this.emitSearch(val);
|
|
11398
11938
|
}
|
|
11399
11939
|
/**
|
|
11400
11940
|
* Phát `search` có GỠ NẢY. Gõ "nguyễn văn a" là 12 phím — không gỡ nảy thì
|
|
@@ -11406,7 +11946,7 @@ class OlAutocomplete {
|
|
|
11406
11946
|
*
|
|
11407
11947
|
* Dưới `minTermLength` thì KHÔNG phát: nơi gọi không phải tự kiểm lại.
|
|
11408
11948
|
*/
|
|
11409
|
-
|
|
11949
|
+
emitSearch(val) {
|
|
11410
11950
|
if (this.hengio) {
|
|
11411
11951
|
clearTimeout(this.hengio);
|
|
11412
11952
|
this.hengio = null;
|
|
@@ -11542,7 +12082,7 @@ class OlAutocomplete {
|
|
|
11542
12082
|
useExisting: forwardRef(() => OlAutocomplete),
|
|
11543
12083
|
multi: true,
|
|
11544
12084
|
},
|
|
11545
|
-
], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.px-2.5]=\"!multiple() && resolvedSize() === 'sm'\"\n [class.px-4]=\"!multiple() && resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"{\n 'px-1.5 py-1': multiple(),\n 'px-3.5': !multiple() && (resolvedSize() === 'base' || resolvedSize() === 'lg'),\n }\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg'\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12085
|
+
], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"multiple() ? 'px-1.5 py-1' : fieldPaddingClass()\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [ngClass]=\"fieldTextClass()\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11546
12086
|
}
|
|
11547
12087
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAutocomplete, decorators: [{
|
|
11548
12088
|
type: Component,
|
|
@@ -11558,7 +12098,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
11558
12098
|
useExisting: forwardRef(() => OlAutocomplete),
|
|
11559
12099
|
multi: true,
|
|
11560
12100
|
},
|
|
11561
|
-
], template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.px-2.5]=\"!multiple() && resolvedSize() === 'sm'\"\n [class.px-4]=\"!multiple() && resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"{\n 'px-1.5 py-1': multiple(),\n 'px-3.5': !multiple() && (resolvedSize() === 'base' || resolvedSize() === 'lg'),\n }\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg'\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
|
|
12101
|
+
], template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"multiple() ? 'px-1.5 py-1' : fieldPaddingClass()\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [ngClass]=\"fieldTextClass()\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
|
|
11562
12102
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], labelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKey", required: false }] }], valueKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueKey", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], errorText: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], allowCreate: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCreate", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], menuClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuClass", required: false }] }], matchTriggerWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "matchTriggerWidth", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], remoteSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteSearch", required: false }] }], minTermLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minTermLength", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], typeToSearchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeToSearchText", required: false }] }], createOption: [{ type: i0.Output, args: ["createOption"] }], search: [{ type: i0.Output, args: ["search"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], added: [{ type: i0.Output, args: ["added"] }], removed: [{ type: i0.Output, args: ["removed"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], optionTemplate: [{
|
|
11563
12103
|
type: ContentChild,
|
|
11564
12104
|
args: ['optionTemplate']
|
|
@@ -11645,186 +12185,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
11645
12185
|
args: [{ selector: 'ol-list-group-item', standalone: true, imports: [NgTemplateOutlet], host: { class: 'block w-full' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<li\n (click)=\"handleClick($event)\"\n class=\"w-full transition-colors\"\n [class]=\"textClasses()\"\n [class.border-b]=\"hasDivider()\"\n [class.border-default-strong]=\"hasDivider()\"\n>\n <!--\n `href` bi\u1EBFn m\u1EE5c th\u00E0nh li\u00EAn k\u1EBFt. Th\u1EBB `<a>` n\u1EB1m B\u00CAN TRONG `<li>`, kh\u00F4ng thay\n th\u1EBF n\u00F3 \u2014 `<a>` tr\u1EF1c ti\u1EBFp trong `<ul>` l\u00E0 HTML h\u1ECFng. \u0110\u1EC7m chuy\u1EC3n xu\u1ED1ng th\u1EBB\n trong c\u00F9ng \u0111\u1EC3 v\u00F9ng b\u1EA5m ph\u1EE7 tr\u1ECDn m\u1EE5c.\n -->\n @if (href() && !disabled()) {\n <a [href]=\"href()\" class=\"block w-full px-4 py-2\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n } @else {\n <span class=\"block w-full px-4 py-2\" [attr.aria-disabled]=\"disabled() ? 'true' : null\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n }\n</li>\n\n<ng-template #ruot><ng-content /></ng-template>\n" }]
|
|
11646
12186
|
}], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
|
|
11647
12187
|
|
|
11648
|
-
/**
|
|
11649
|
-
* Tiêu đề Modal — khớp Figma `ol-modal-header` (node 1149:71403).
|
|
11650
|
-
* Figma có 7 biến thể: Default, Heading & supporting text, Icon + Heading &
|
|
11651
|
-
* helper text, With Logo, Logo & icons, Avatar group, Breadcrumb.
|
|
11652
|
-
* Component lo phần khung dùng chung; nội dung giàu hơn lắp qua <ng-content>.
|
|
11653
|
-
*/
|
|
11654
|
-
/*
|
|
11655
|
-
* GHÉP VỚI `ol-modal`: phần đầu của `ol-modal` nằm trong `@if (title() ||
|
|
11656
|
-
* dismissible())`, nên muốn dùng component này thì phải TẮT nó đi —
|
|
11657
|
-
* `title=""` kèm `[dismissible]="false"` — rồi lắp vào `<ng-content />` trần
|
|
11658
|
-
* của phần thân. Để nguyên `title` là lồng hai khung `p-5 border-b`.
|
|
11659
|
-
*/
|
|
11660
|
-
class OlModalHeader {
|
|
11661
|
-
/**
|
|
11662
|
-
* Bậc thẻ tiêu đề (h1–h6). Mặc định 3 — giữ nguyên bậc đang chạy.
|
|
11663
|
-
*
|
|
11664
|
-
* Khoá cứng một bậc thì nơi gọi không sửa được: đặt component này sau
|
|
11665
|
-
* `<h2>` của trang là nhảy bậc.
|
|
11666
|
-
*/
|
|
11667
|
-
headingLevel = input(3, /* @ts-ignore */
|
|
11668
|
-
...(ngDevMode ? [{ debugName: "headingLevel" }] : /* istanbul ignore next */ []));
|
|
11669
|
-
heading = input('', /* @ts-ignore */
|
|
11670
|
-
...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
11671
|
-
/**
|
|
11672
|
-
* Icon đầu tiêu đề. Rỗng thì KHÔNG dựng khung — không để lại thẻ trống.
|
|
11673
|
-
*
|
|
11674
|
-
* Figma `Type=Icon + Heading & helper text` (#1149:71412) vẽ khung 48px bo
|
|
11675
|
-
* 12px, icon 28px bên trong. Đó đúng là `ol-icon-shape` size `lg`, nên dùng
|
|
11676
|
-
* lại thay vì tự vẽ khung.
|
|
11677
|
-
*
|
|
11678
|
-
* `color="white"` chứ KHÔNG `gray`: Figma bind nền
|
|
11679
|
-
* `bg-neutral-primary-medium` (trắng) kèm viền `border-default-medium`, mà
|
|
11680
|
-
* trong `ol-icon-shape` chỉ nhánh `white` có cặp đó — `gray` ra nền xám đặc
|
|
11681
|
-
* và KHÔNG viền. Đo /modal lượt đầu: nền rgb(243,244,246), viền 0px.
|
|
11682
|
-
*/
|
|
11683
|
-
icon = input('', /* @ts-ignore */
|
|
11684
|
-
...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
11685
|
-
/**
|
|
11686
|
-
* Mô tả phụ dưới tiêu đề.
|
|
11687
|
-
*
|
|
11688
|
-
* Tên `description` theo `ol-alert-dialog` — thứ dựng trên chính `ol-modal`
|
|
11689
|
-
* — và `ol-empty`. `helperText` mang nghĩa "chú thích dưới ô nhập" ở họ form
|
|
11690
|
-
* (16 component dùng), đặt cho tiêu đề hộp thoại là lệch nghĩa.
|
|
11691
|
-
*/
|
|
11692
|
-
description = input('', /* @ts-ignore */
|
|
11693
|
-
...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
|
|
11694
|
-
/** @deprecated Dùng `description`. Giữ để không phá mã đang chạy. */
|
|
11695
|
-
supportingText = input('', /* @ts-ignore */
|
|
11696
|
-
...(ngDevMode ? [{ debugName: "supportingText" }] : /* istanbul ignore next */ []));
|
|
11697
|
-
/** @deprecated Dùng `description`. Tên cũ của `ol-drawer-header`. */
|
|
11698
|
-
helperText = input('', /* @ts-ignore */
|
|
11699
|
-
...(ngDevMode ? [{ debugName: "helperText" }] : /* istanbul ignore next */ []));
|
|
11700
|
-
/**
|
|
11701
|
-
* Cái nào được TRUYỀN thì thắng — không phụ thuộc giá trị.
|
|
11702
|
-
*
|
|
11703
|
-
* `ol-badge` từng hoà giải hai input cùng nghĩa bằng cách so với giá trị mặc
|
|
11704
|
-
* định (`theme` thắng trừ khi nó bằng `'gray'`), nên truyền cả hai thì một
|
|
11705
|
-
* cái bị nuốt im lặng. Ở đây chuỗi rỗng là "không truyền", nên chỉ cần lấy
|
|
11706
|
-
* cái đầu tiên khác rỗng.
|
|
11707
|
-
*/
|
|
11708
|
-
descriptionText = computed(() => this.description() || this.supportingText() || this.helperText(), /* @ts-ignore */
|
|
11709
|
-
...(ngDevMode ? [{ debugName: "descriptionText" }] : /* istanbul ignore next */ []));
|
|
11710
|
-
showClose = input(true, /* @ts-ignore */
|
|
11711
|
-
...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
|
|
11712
|
-
closeClick = output();
|
|
11713
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11714
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModalHeader, isStandalone: true, selector: "ol-modal-header", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, supportingText: { classPropertyName: "supportingText", publicName: "supportingText", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, showClose: { classPropertyName: "showClose", publicName: "showClose", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeClick: "closeClick" }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
11715
|
-
<!--
|
|
11716
|
-
PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
|
|
11717
|
-
kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
|
|
11718
|
-
và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
|
|
11719
|
-
CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
|
|
11720
|
-
-->
|
|
11721
|
-
<div class="flex items-start justify-between gap-3 p-3">
|
|
11722
|
-
@if (icon()) {
|
|
11723
|
-
<ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
|
|
11724
|
-
}
|
|
11725
|
-
<div class="min-w-0 flex-1">
|
|
11726
|
-
@if (heading()) {
|
|
11727
|
-
@switch (headingLevel()) {
|
|
11728
|
-
@case (1) {
|
|
11729
|
-
<h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
|
|
11730
|
-
}
|
|
11731
|
-
@case (2) {
|
|
11732
|
-
<h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
|
|
11733
|
-
}
|
|
11734
|
-
@case (4) {
|
|
11735
|
-
<h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
|
|
11736
|
-
}
|
|
11737
|
-
@case (5) {
|
|
11738
|
-
<h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
|
|
11739
|
-
}
|
|
11740
|
-
@case (6) {
|
|
11741
|
-
<h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
|
|
11742
|
-
}
|
|
11743
|
-
@default {
|
|
11744
|
-
<h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
|
|
11745
|
-
}
|
|
11746
|
-
}
|
|
11747
|
-
}
|
|
11748
|
-
@if (descriptionText()) {
|
|
11749
|
-
<p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
|
|
11750
|
-
}
|
|
11751
|
-
<ng-content />
|
|
11752
|
-
</div>
|
|
11753
|
-
@if (showClose()) {
|
|
11754
|
-
<button
|
|
11755
|
-
type="button"
|
|
11756
|
-
(click)="closeClick.emit()"
|
|
11757
|
-
aria-label="Đóng hộp thoại"
|
|
11758
|
-
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
|
|
11759
|
-
>
|
|
11760
|
-
<ol-icon name="close" size="sm" />
|
|
11761
|
-
</button>
|
|
11762
|
-
}
|
|
11763
|
-
</div>
|
|
11764
|
-
`, isInline: true, dependencies: [{ kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11765
|
-
}
|
|
11766
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, decorators: [{
|
|
11767
|
-
type: Component,
|
|
11768
|
-
args: [{
|
|
11769
|
-
selector: 'ol-modal-header',
|
|
11770
|
-
standalone: true,
|
|
11771
|
-
imports: [OlIcon, OlIconShape],
|
|
11772
|
-
template: `
|
|
11773
|
-
<!--
|
|
11774
|
-
PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
|
|
11775
|
-
kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
|
|
11776
|
-
và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
|
|
11777
|
-
CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
|
|
11778
|
-
-->
|
|
11779
|
-
<div class="flex items-start justify-between gap-3 p-3">
|
|
11780
|
-
@if (icon()) {
|
|
11781
|
-
<ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
|
|
11782
|
-
}
|
|
11783
|
-
<div class="min-w-0 flex-1">
|
|
11784
|
-
@if (heading()) {
|
|
11785
|
-
@switch (headingLevel()) {
|
|
11786
|
-
@case (1) {
|
|
11787
|
-
<h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
|
|
11788
|
-
}
|
|
11789
|
-
@case (2) {
|
|
11790
|
-
<h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
|
|
11791
|
-
}
|
|
11792
|
-
@case (4) {
|
|
11793
|
-
<h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
|
|
11794
|
-
}
|
|
11795
|
-
@case (5) {
|
|
11796
|
-
<h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
|
|
11797
|
-
}
|
|
11798
|
-
@case (6) {
|
|
11799
|
-
<h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
|
|
11800
|
-
}
|
|
11801
|
-
@default {
|
|
11802
|
-
<h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
|
|
11803
|
-
}
|
|
11804
|
-
}
|
|
11805
|
-
}
|
|
11806
|
-
@if (descriptionText()) {
|
|
11807
|
-
<p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
|
|
11808
|
-
}
|
|
11809
|
-
<ng-content />
|
|
11810
|
-
</div>
|
|
11811
|
-
@if (showClose()) {
|
|
11812
|
-
<button
|
|
11813
|
-
type="button"
|
|
11814
|
-
(click)="closeClick.emit()"
|
|
11815
|
-
aria-label="Đóng hộp thoại"
|
|
11816
|
-
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
|
|
11817
|
-
>
|
|
11818
|
-
<ol-icon name="close" size="sm" />
|
|
11819
|
-
</button>
|
|
11820
|
-
}
|
|
11821
|
-
</div>
|
|
11822
|
-
`,
|
|
11823
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
11824
|
-
host: { class: 'block' },
|
|
11825
|
-
}]
|
|
11826
|
-
}], propDecorators: { headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], supportingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "supportingText", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], showClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClose", required: false }] }], closeClick: [{ type: i0.Output, args: ["closeClick"] }] } });
|
|
11827
|
-
|
|
11828
12188
|
/**
|
|
11829
12189
|
* Dải nút hành động cuối Modal — khớp Figma `ol-modal-footer`.
|
|
11830
12190
|
* Figma có 4 biến thể: Buttons, 3 buttons, Buttons & links, Pagination.
|
|
@@ -11988,15 +12348,36 @@ class OlNavbarItem {
|
|
|
11988
12348
|
'inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-150 whitespace-nowrap shrink-0',
|
|
11989
12349
|
/**
|
|
11990
12350
|
* Hover CHỈ cho mục KHÔNG active — cùng lý do với `ol-sidebar-item`:
|
|
11991
|
-
* `:hover` tính như một class nên hover (0,2,0) thắng
|
|
11992
|
-
* di chuột vào mục đang mở là mất
|
|
12351
|
+
* `:hover` tính như một class nên hover (0,2,0) thắng nền active
|
|
12352
|
+
* (0,1,0), di chuột vào mục đang mở là mất trạng thái. Xem chú thích
|
|
12353
|
+
* đầy đủ ở đó.
|
|
12354
|
+
*/
|
|
12355
|
+
'text-body not-[.active]:hover:text-fg-brand not-[.active]:hover:bg-brand-softer',
|
|
12356
|
+
/**
|
|
12357
|
+
* Trạng thái active — BÁM THEO `ol-sidebar-item`, xem ba đời màu ở đó.
|
|
12358
|
+
*
|
|
12359
|
+
* Đồng bộ 2026-09-24. Trước đó navbar kẹt lại ở ĐỜI 2 (`bg-brand` đặc +
|
|
12360
|
+
* chữ trắng + gạch mép trái 4px): commit 89b2e49 ngày 21/09 đổi cả hai
|
|
12361
|
+
* component sang đời 2 cùng lúc, nhưng khi sidebar chuyển sang đời 3
|
|
12362
|
+
* (c978f10 + 224d048 ngày 23/09 — nền nhạt `bg-brand-soft`, gỡ gạch
|
|
12363
|
+
* mép) thì navbar không được đổi theo. Chú thích ở đây vẫn ghi "cùng
|
|
12364
|
+
* luật với ol-sidebar-item" trong khi hai bên đã khác hẳn.
|
|
12365
|
+
*
|
|
12366
|
+
* Gỡ luôn gạch mép trái, cùng lý do sidebar đã gỡ: nền đã đánh dấu đủ
|
|
12367
|
+
* rõ, gạch là dấu hiệu thứ hai cho cùng một trạng thái, và nó ăn 4px
|
|
12368
|
+
* đệm trái làm chữ mục active lệch so với các mục còn lại — ở navbar
|
|
12369
|
+
* xếp NGANG thì lệch này thấy rõ hơn sidebar xếp dọc.
|
|
12370
|
+
*
|
|
12371
|
+
* VẪN LỆCH FIGMA CÓ CHỦ Ý. Figma `ol-navbar-item` #6375:261 cho
|
|
12372
|
+
* `State=Active` chỉ đổi màu chữ (`text-body` -> `text-fg-brand`),
|
|
12373
|
+
* KHÔNG nền, KHÔNG viền — đọc biến bind ngày 2026-09-24. Đó chính là
|
|
12374
|
+
* phương án đã bị bác hai lần vì nổi bật chỉ 1.06 lần so với nền trang.
|
|
12375
|
+
* Nợ: cập nhật Figma cho khớp đời 3, cho cả sidebar lẫn navbar.
|
|
11993
12376
|
*/
|
|
11994
|
-
'
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
/** Hover của mục active: ĐẬM thêm một bậc, không nhạt đi. */
|
|
11999
|
-
'[&.active]:hover:bg-brand-strong',
|
|
12377
|
+
'[&.active]:text-fg-brand [&.active]:bg-brand-soft [&.active]:font-semibold',
|
|
12378
|
+
this.active() ? 'active text-fg-brand bg-brand-soft font-semibold' : '',
|
|
12379
|
+
/** Hover của mục ĐANG active: ĐẬM thêm một bậc, không nhạt đi. */
|
|
12380
|
+
'[&.active]:hover:bg-brand-medium',
|
|
12000
12381
|
this.disabled() ? 'opacity-50 pointer-events-none' : 'cursor-pointer',
|
|
12001
12382
|
]
|
|
12002
12383
|
.filter(Boolean)
|
|
@@ -12004,11 +12385,11 @@ class OlNavbarItem {
|
|
|
12004
12385
|
}, /* @ts-ignore */
|
|
12005
12386
|
...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
|
|
12006
12387
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlNavbarItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
12007
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlNavbarItem, isStandalone: true, selector: "ol-navbar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "
|
|
12388
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlNavbarItem, isStandalone: true, selector: "ol-navbar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n BA NH\u00C1NH, `routerLinkActive` CH\u1EC8 \u1EDF nh\u00E1nh c\u00F3 `routerLink`.\n\n B\u1EA3n c\u0169 d\u00F9ng M\u1ED8T `<a>` mang c\u1EA3 `[routerLink]`, `[routerLinkActive]` v\u00E0\n `[class]=\"itemClasses()\"`. Hai th\u1EE9 sau c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh `class`, v\u00E0\n `RouterLinkActive` ghi SAU n\u00EAn n\u00F3 th\u1EAFng: chu\u1ED7i `'active \u2026'` m\u00E0 `itemClasses()`\n th\u00EAm khi `[active]=\"true\"` b\u1ECB nu\u1ED1t s\u1EA1ch.\n\n T\u00E1ch nh\u00E1nh th\u00F4i KH\u00D4NG \u0111\u1EE7 \u2014 nh\u00E1nh `routerLink` v\u1EABn c\u00F3 c\u1EA3 hai directive tr\u00EAn\n c\u00F9ng th\u1EBB. N\u00EAn `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET: \u0111\u00E3 \u00E9p active\n b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh, hai b\u00EAn h\u1EBFt tranh nhau.\n\n H\u1ECFng im l\u1EB7ng \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (ch\u00FAng n\u1EB1m trong c\u00F9ng chu\u1ED7i), ch\u1EC9 ri\u00EAng\n ch\u1EEF `active` m\u1EA5t. H\u1EADu qu\u1EA3 ch\u1EC9 l\u1ED9 ra khi r\u00EA chu\u1ED9t: \u0111o /navbar 2026-09-24, m\u1EE5c\n \u0111ang m\u1EDF NH\u1EA0T \u0110I (#e3eaff -> #f5f8ff) v\u00EC `not-[.active]:hover:bg-brand-softer`\n v\u1EABn \u00E1p d\u1EE5ng, c\u00F2n `[&.active]:hover:bg-brand-medium` th\u00EC kh\u00F4ng. Sidebar c\u00F9ng\n b\u1ED9 class nh\u01B0ng \u0110\u1EACM L\u00CAN \u0111\u00FAng (#d2dcfa) v\u00EC n\u00F3 \u0111\u00E3 t\u00E1ch nh\u00E1nh t\u1EEB tr\u01B0\u1EDBc.\n\n Unit test KH\u00D4NG b\u1EAFt \u0111\u01B0\u1EE3c: `provideRouter([])` kh\u00F4ng c\u00F3 route n\u00E0o kh\u1EDBp n\u00EAn\n directive kh\u00F4ng ch\u1EA1m v\u00E0o `class`, test xanh trong khi tr\u00ECnh duy\u1EC7t h\u1ECFng.\n\n N\u1ED9i dung g\u00F3i trong `ng-template` v\u00E0 chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`: khai\n nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh lo\u1EA1i tr\u1EEB nhau \u2014\n th\u00EC Angular ch\u1EC9 chi\u1EBFu v\u00E0o M\u1ED8T ch\u1ED7 v\u00E0 th\u1EF1c t\u1EBF m\u1EA5t \u1EDF m\u1ECDi nh\u00E1nh.\n-->\n<ng-template #itemContent>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span class=\"whitespace-nowrap\">{{ label() }}</span>\n }\n <ng-content />\n</ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else {\n <a [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12008
12389
|
}
|
|
12009
12390
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlNavbarItem, decorators: [{
|
|
12010
12391
|
type: Component,
|
|
12011
|
-
args: [{ selector: 'ol-navbar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "
|
|
12392
|
+
args: [{ selector: 'ol-navbar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n BA NH\u00C1NH, `routerLinkActive` CH\u1EC8 \u1EDF nh\u00E1nh c\u00F3 `routerLink`.\n\n B\u1EA3n c\u0169 d\u00F9ng M\u1ED8T `<a>` mang c\u1EA3 `[routerLink]`, `[routerLinkActive]` v\u00E0\n `[class]=\"itemClasses()\"`. Hai th\u1EE9 sau c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh `class`, v\u00E0\n `RouterLinkActive` ghi SAU n\u00EAn n\u00F3 th\u1EAFng: chu\u1ED7i `'active \u2026'` m\u00E0 `itemClasses()`\n th\u00EAm khi `[active]=\"true\"` b\u1ECB nu\u1ED1t s\u1EA1ch.\n\n T\u00E1ch nh\u00E1nh th\u00F4i KH\u00D4NG \u0111\u1EE7 \u2014 nh\u00E1nh `routerLink` v\u1EABn c\u00F3 c\u1EA3 hai directive tr\u00EAn\n c\u00F9ng th\u1EBB. N\u00EAn `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET: \u0111\u00E3 \u00E9p active\n b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh, hai b\u00EAn h\u1EBFt tranh nhau.\n\n H\u1ECFng im l\u1EB7ng \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (ch\u00FAng n\u1EB1m trong c\u00F9ng chu\u1ED7i), ch\u1EC9 ri\u00EAng\n ch\u1EEF `active` m\u1EA5t. H\u1EADu qu\u1EA3 ch\u1EC9 l\u1ED9 ra khi r\u00EA chu\u1ED9t: \u0111o /navbar 2026-09-24, m\u1EE5c\n \u0111ang m\u1EDF NH\u1EA0T \u0110I (#e3eaff -> #f5f8ff) v\u00EC `not-[.active]:hover:bg-brand-softer`\n v\u1EABn \u00E1p d\u1EE5ng, c\u00F2n `[&.active]:hover:bg-brand-medium` th\u00EC kh\u00F4ng. Sidebar c\u00F9ng\n b\u1ED9 class nh\u01B0ng \u0110\u1EACM L\u00CAN \u0111\u00FAng (#d2dcfa) v\u00EC n\u00F3 \u0111\u00E3 t\u00E1ch nh\u00E1nh t\u1EEB tr\u01B0\u1EDBc.\n\n Unit test KH\u00D4NG b\u1EAFt \u0111\u01B0\u1EE3c: `provideRouter([])` kh\u00F4ng c\u00F3 route n\u00E0o kh\u1EDBp n\u00EAn\n directive kh\u00F4ng ch\u1EA1m v\u00E0o `class`, test xanh trong khi tr\u00ECnh duy\u1EC7t h\u1ECFng.\n\n N\u1ED9i dung g\u00F3i trong `ng-template` v\u00E0 chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`: khai\n nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh lo\u1EA1i tr\u1EEB nhau \u2014\n th\u00EC Angular ch\u1EC9 chi\u1EBFu v\u00E0o M\u1ED8T ch\u1ED7 v\u00E0 th\u1EF1c t\u1EBF m\u1EA5t \u1EDF m\u1ECDi nh\u00E1nh.\n-->\n<ng-template #itemContent>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span class=\"whitespace-nowrap\">{{ label() }}</span>\n }\n <ng-content />\n</ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else {\n <a [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n}\n" }]
|
|
12012
12393
|
}], propDecorators: { routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], routerLinkActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActive", required: false }] }], routerLinkActiveOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActiveOptions", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
|
|
12013
12394
|
|
|
12014
12395
|
class OlNavDropdown {
|
|
@@ -13180,7 +13561,7 @@ class OlAppNav {
|
|
|
13180
13561
|
this.collapsed.update((v) => !v);
|
|
13181
13562
|
}
|
|
13182
13563
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13183
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAppNav, isStandalone: true, selector: "ol-app-nav", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, logoSrc: { classPropertyName: "logoSrc", publicName: "logoSrc", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, showRail: { classPropertyName: "showRail", publicName: "showRail", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", itemClick: "itemClick", userClick: "userClick" }, host: { classAttribute: "block" }, ngImport: i0, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n class=\"relative flex h-full flex-col justify-between rounded-2xl pt-6 transition-[width] duration-200\"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 13px khi thu g\u1ECDn, KH\u00D4NG ph\u1EA3i 12px: `<ul>` l\u00E0 flex-col n\u00EAn `<li>`\n k\u00E9o gi\u00E3n m\u1EE5c con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-11` kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 13\u00D72 = 44,\n \u0111\u00FAng \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c. \u0110\u1EC7m 12px cho ra 46\u00D744, l\u1EC7ch m\u1ED9t chi\u1EC1u.\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex flex-col gap-7 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoSrc()) {\n <img [src]=\"logoSrc()\" alt=\"\u00D4n Luy\u1EC7n\" class=\"h-10 w-auto max-w-full object-contain\" />\n } @else if (collapsed()) {\n <!--\n Thu g\u1ECDn: ch\u1EC9 ch\u1EEF t\u1EAFt. Logo \u0111\u1EA7y \u0111\u1EE7 r\u1ED9ng 155px (Figma) n\u00EAn \u1EDF khung 70px\n n\u00F3 tr\u00E0n h\u1EB3n ra ngo\u00E0i \u2014 \u0111o \u0111\u01B0\u1EE3c tr\u00EAn tr\u00ECnh duy\u1EC7t, ch\u1EEF \"\u00D4nluy\u1EC7n\" b\u1ECB c\u1EAFt\n gi\u1EEFa ch\u1EEBng \u1EDF m\u00E9p tr\u00E1i.\n -->\n <span class=\"text-2xl font-bold text-warning\" aria-label=\"\u00D4n Luy\u1EC7n\">\u00D4L</span>\n } @else {\n <!-- D\u1EF1 ph\u00F2ng khi ch\u01B0a truy\u1EC1n \u1EA3nh: ch\u1EEF gi\u1EEF \u0111\u00FAng hai m\u00E0u c\u1EE7a logo -->\n <span class=\"whitespace-nowrap text-2xl font-bold text-white\">\n \u00D4n<span class=\"text-warning\">luy\u1EC7n</span>\n </span>\n }\n </div>\n\n <ul class=\"flex flex-col gap-2\">\n @for (item of mainItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex flex-col gap-[7px] overflow-hidden rounded-b-2xl pb-5\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ul class=\"flex flex-col gap-[7px]\">\n @for (item of footerItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n\n @if (config().user; as user) {\n <button\n type=\"button\"\n class=\"mt-[30px] flex cursor-pointer items-center rounded-md transition-colors hover:bg-[var(--bg-app-nav-hover,#5168ac)]\"\n [class]=\"collapsed() ? 'mx-auto justify-center p-1' : 'w-full gap-2 p-1 text-start'\"\n (click)=\"userClick.emit()\"\n >\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n\n <!--\n Kh\u1ED1i t\u00EAn v\u00E0 email ch\u1EC9 hi\u1EC7n khi m\u1EDF. D\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class:\n \u1EA9n b\u1EB1ng `hidden` th\u00EC tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc, v\u00E0 ch\u1EEF v\u1EABn chi\u1EBFm ch\u1ED7\n trong t\u00EDnh to\u00E1n b\u1ED1 c\u1EE5c \u1EDF b\u1EC1 r\u1ED9ng 70px.\n -->\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs\">{{ user.subtitle }}</span>\n }\n </span>\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" class=\"shrink-0 text-white\" />\n }\n </button>\n }\n </div>\n</nav>\n\n<!--\n M\u1ED9t m\u1EE5c menu \u2014 d\u00F9ng chung cho c\u1EA3 ph\u1EA7n tr\u00EAn l\u1EABn ph\u1EA7n d\u01B0\u1EDBi.\n\n Ba nh\u00E1nh v\u00EC ba lo\u1EA1i h\u00E0nh vi kh\u00E1c nhau, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 `route` \u2192 `<a routerLink>` \u0111\u1EC3 router lo \u0111i\u1EC1u h\u01B0\u1EDBng v\u00E0 tr\u1EA1ng th\u00E1i active\n kh\u00F4ng route \u2192 `<button>` ch\u1EC9 ph\u00E1t s\u1EF1 ki\u1EC7n\n disabled \u2192 v\u1EABn `<button>` nh\u01B0ng `pointer-events-none` (lo trong class)\n-->\n<ng-template #navItem let-item>\n @if (item.route) {\n <a\n [routerLink]=\"item.route\"\n routerLinkActive=\"ol-app-nav-item-active font-medium\"\n [class]=\"itemClasses(item)\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </button>\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c: icon 24px + nh\u00E3n + huy hi\u1EC7u. Nh\u00E3n \u1EA9n khi thu g\u1ECDn. -->\n<ng-template #itemBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n\n @if (!collapsed()) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "directive", type: OlTooltipDirective, selector: "[olTooltip]", inputs: ["olTooltip", "tooltipPosition", "tooltipTheme", "tooltipDelay", "tooltipDisabled", "tooltipArrow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13564
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAppNav, isStandalone: true, selector: "ol-app-nav", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, logoSrc: { classPropertyName: "logoSrc", publicName: "logoSrc", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, showRail: { classPropertyName: "showRail", publicName: "showRail", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", itemClick: "itemClick", userClick: "userClick" }, host: { classAttribute: "block" }, ngImport: i0, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n class=\"relative flex h-full flex-col justify-between rounded-2xl pt-6 transition-[width] duration-200\"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 13px khi thu g\u1ECDn, KH\u00D4NG ph\u1EA3i 12px: `<ul>` l\u00E0 flex-col n\u00EAn `<li>`\n k\u00E9o gi\u00E3n m\u1EE5c con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-11` kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 13\u00D72 = 44,\n \u0111\u00FAng \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c. \u0110\u1EC7m 12px cho ra 46\u00D744, l\u1EC7ch m\u1ED9t chi\u1EC1u.\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex flex-col gap-7 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoSrc()) {\n <img [src]=\"logoSrc()\" alt=\"\u00D4n Luy\u1EC7n\" class=\"h-10 w-auto max-w-full object-contain\" />\n } @else if (collapsed()) {\n <!--\n Thu g\u1ECDn: ch\u1EC9 ch\u1EEF t\u1EAFt. Logo \u0111\u1EA7y \u0111\u1EE7 r\u1ED9ng 155px (Figma) n\u00EAn \u1EDF khung 70px\n n\u00F3 tr\u00E0n h\u1EB3n ra ngo\u00E0i \u2014 \u0111o \u0111\u01B0\u1EE3c tr\u00EAn tr\u00ECnh duy\u1EC7t, ch\u1EEF \"\u00D4nluy\u1EC7n\" b\u1ECB c\u1EAFt\n gi\u1EEFa ch\u1EEBng \u1EDF m\u00E9p tr\u00E1i.\n -->\n <span class=\"text-2xl font-bold text-warning\" aria-label=\"\u00D4n Luy\u1EC7n\">\u00D4L</span>\n } @else {\n <!-- D\u1EF1 ph\u00F2ng khi ch\u01B0a truy\u1EC1n \u1EA3nh: ch\u1EEF gi\u1EEF \u0111\u00FAng hai m\u00E0u c\u1EE7a logo -->\n <span class=\"whitespace-nowrap text-2xl font-bold text-white\">\n \u00D4n<span class=\"text-warning\">luy\u1EC7n</span>\n </span>\n }\n </div>\n\n <ul class=\"flex flex-col gap-2\">\n @for (item of mainItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex flex-col gap-[7px] overflow-hidden rounded-b-2xl pb-5\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ul class=\"flex flex-col gap-[7px]\">\n @for (item of footerItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n\n @if (config().user; as user) {\n <button\n type=\"button\"\n class=\"mt-[30px] flex cursor-pointer items-center rounded-md transition-colors hover:bg-[var(--bg-app-nav-hover,#5168ac)]\"\n [class]=\"collapsed() ? 'mx-auto justify-center p-1' : 'w-full gap-2 p-1 text-start'\"\n (click)=\"userClick.emit()\"\n >\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n\n <!--\n Kh\u1ED1i t\u00EAn v\u00E0 email ch\u1EC9 hi\u1EC7n khi m\u1EDF. D\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class:\n \u1EA9n b\u1EB1ng `hidden` th\u00EC tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc, v\u00E0 ch\u1EEF v\u1EABn chi\u1EBFm ch\u1ED7\n trong t\u00EDnh to\u00E1n b\u1ED1 c\u1EE5c \u1EDF b\u1EC1 r\u1ED9ng 70px.\n -->\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs\">{{ user.subtitle }}</span>\n }\n </span>\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" class=\"shrink-0 text-white\" />\n }\n </button>\n }\n </div>\n</nav>\n\n<!--\n M\u1ED9t m\u1EE5c menu \u2014 d\u00F9ng chung cho c\u1EA3 ph\u1EA7n tr\u00EAn l\u1EABn ph\u1EA7n d\u01B0\u1EDBi.\n\n Ba nh\u00E1nh v\u00EC ba lo\u1EA1i h\u00E0nh vi kh\u00E1c nhau, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 `route` \u2192 `<a routerLink>` \u0111\u1EC3 router lo \u0111i\u1EC1u h\u01B0\u1EDBng v\u00E0 tr\u1EA1ng th\u00E1i active\n kh\u00F4ng route \u2192 `<button>` ch\u1EC9 ph\u00E1t s\u1EF1 ki\u1EC7n\n disabled \u2192 v\u1EABn `<button>` nh\u01B0ng `pointer-events-none` (lo trong class)\n-->\n<ng-template #navItem let-item>\n @if (item.route) {\n <a\n [routerLink]=\"item.route\"\n routerLinkActive=\"ol-app-nav-item-active font-medium\"\n [class]=\"itemClasses(item)\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </button>\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c: icon 24px + nh\u00E3n + huy hi\u1EC7u. Nh\u00E3n \u1EA9n khi thu g\u1ECDn. -->\n<ng-template #itemBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n\n @if (!collapsed()) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "directive", type: OlTooltipDirective, selector: "[olTooltip]", inputs: ["olTooltip", "tooltipPosition", "tooltipTheme", "tooltipDelay", "tooltipDisabled", "tooltipArrow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13184
13565
|
}
|
|
13185
13566
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, decorators: [{
|
|
13186
13567
|
type: Component,
|
|
@@ -13371,11 +13752,11 @@ class OlSidebarItem {
|
|
|
13371
13752
|
}, /* @ts-ignore */
|
|
13372
13753
|
...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
|
|
13373
13754
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13374
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarItem, isStandalone: true, selector: "ol-sidebar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13755
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarItem, isStandalone: true, selector: "ol-sidebar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!--\n `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET.\n\n `RouterLinkActive` v\u00E0 `[class]=\"itemClasses()\"` c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh\n `class`, directive ghi SAU n\u00EAn n\u00F3 th\u1EAFng: k\u1EBFt h\u1EE3p `routerLink` + `[active]`\n th\u00EC chu\u1ED7i `'active \u2026'` b\u1ECB nu\u1ED1t. H\u1ECFng IM L\u1EB6NG \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (c\u00F9ng\n chu\u1ED7i), ch\u1EC9 ri\u00EAng ch\u1EEF `active` m\u1EA5t, n\u00EAn `not-[.active]:hover:*` v\u1EABn \u00E1p d\u1EE5ng\n v\u00E0 m\u1EE5c \u0111ang m\u1EDF NH\u1EA0T \u0110I khi r\u00EA chu\u1ED9t thay v\u00EC \u0111\u1EADm l\u00EAn.\n\n \u0110\u00E3 \u00E9p active b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh \u2014 t\u1EAFt n\u00F3 \u0111i l\u00E0\n h\u1EBFt tranh nhau. B\u1EAFt \u0111\u01B0\u1EE3c khi \u0111\u1ED1i chi\u1EBFu v\u1EDBi `ol-navbar-item` ng\u00E0y 2026-09-24;\n trang docs kh\u00F4ng l\u1ED9 v\u00EC c\u00E1c m\u1EE5c active \u1EDF \u0111\u00E2y d\u00F9ng nh\u00E1nh `div`/`href`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13375
13756
|
}
|
|
13376
13757
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarItem, decorators: [{
|
|
13377
13758
|
type: Component,
|
|
13378
|
-
args: [{ selector: 'ol-sidebar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon, OlBadge, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n" }]
|
|
13759
|
+
args: [{ selector: 'ol-sidebar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon, OlBadge, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!--\n `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET.\n\n `RouterLinkActive` v\u00E0 `[class]=\"itemClasses()\"` c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh\n `class`, directive ghi SAU n\u00EAn n\u00F3 th\u1EAFng: k\u1EBFt h\u1EE3p `routerLink` + `[active]`\n th\u00EC chu\u1ED7i `'active \u2026'` b\u1ECB nu\u1ED1t. H\u1ECFng IM L\u1EB6NG \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (c\u00F9ng\n chu\u1ED7i), ch\u1EC9 ri\u00EAng ch\u1EEF `active` m\u1EA5t, n\u00EAn `not-[.active]:hover:*` v\u1EABn \u00E1p d\u1EE5ng\n v\u00E0 m\u1EE5c \u0111ang m\u1EDF NH\u1EA0T \u0110I khi r\u00EA chu\u1ED9t thay v\u00EC \u0111\u1EADm l\u00EAn.\n\n \u0110\u00E3 \u00E9p active b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh \u2014 t\u1EAFt n\u00F3 \u0111i l\u00E0\n h\u1EBFt tranh nhau. B\u1EAFt \u0111\u01B0\u1EE3c khi \u0111\u1ED1i chi\u1EBFu v\u1EDBi `ol-navbar-item` ng\u00E0y 2026-09-24;\n trang docs kh\u00F4ng l\u1ED9 v\u00EC c\u00E1c m\u1EE5c active \u1EDF \u0111\u00E2y d\u00F9ng nh\u00E1nh `div`/`href`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n" }]
|
|
13379
13760
|
}], propDecorators: { routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], routerLinkActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActive", required: false }] }], routerLinkActiveOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActiveOptions", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], badgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeColor", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }] } });
|
|
13380
13761
|
|
|
13381
13762
|
class OlSidebarGroup {
|
|
@@ -13444,7 +13825,7 @@ class OlSidebarSubmenu {
|
|
|
13444
13825
|
this.isOpen.update((v) => !v);
|
|
13445
13826
|
}
|
|
13446
13827
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarSubmenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13447
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarSubmenu, isStandalone: true, selector: "ol-sidebar-submenu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange" }, ngImport: i0, template: "<div>\n <button\n type=\"button\"\n (click)=\"toggle()\"\n [class]=\"triggerClasses()\"\n [attr.aria-expanded]=\"isOpen()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\">{{ label() }}</span>\n </div>\n\n <div class=\"flex items-center gap-2\">\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n <ol-icon\n name=\"chevron-right\"\n size=\"xs\"\n class=\"text-body-subtle transition-transform duration-200 shrink-0\"\n [class.rotate-90]=\"isOpen()\"\n ></ol-icon>\n </div>\n </button>\n\n <div\n class=\"pl-4 pr-1 py-1 space-y-1 border-l-2 border-default-subtle ml-5 my-1 transition-all duration-150\"\n [class.hidden]=\"!isOpen()\"\n >\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13828
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarSubmenu, isStandalone: true, selector: "ol-sidebar-submenu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange" }, ngImport: i0, template: "<div>\n <button\n type=\"button\"\n (click)=\"toggle()\"\n [class]=\"triggerClasses()\"\n [attr.aria-expanded]=\"isOpen()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\">{{ label() }}</span>\n </div>\n\n <div class=\"flex items-center gap-2\">\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n <ol-icon\n name=\"chevron-right\"\n size=\"xs\"\n class=\"text-body-subtle transition-transform duration-200 shrink-0\"\n [class.rotate-90]=\"isOpen()\"\n ></ol-icon>\n </div>\n </button>\n\n <div\n class=\"pl-4 pr-1 py-1 space-y-1 border-l-2 border-default-subtle ml-5 my-1 transition-all duration-150\"\n [class.hidden]=\"!isOpen()\"\n >\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13448
13829
|
}
|
|
13449
13830
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarSubmenu, decorators: [{
|
|
13450
13831
|
type: Component,
|
|
@@ -14409,7 +14790,7 @@ class OlTabs {
|
|
|
14409
14790
|
}
|
|
14410
14791
|
}
|
|
14411
14792
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14412
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTabs, isStandalone: true, selector: "ol-tabs", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, mobile: { classPropertyName: "mobile", publicName: "mobile", isSignal: true, isRequired: false, transformFunction: null }, mobileDropdown: { classPropertyName: "mobileDropdown", publicName: "mobileDropdown", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, activeTab: { classPropertyName: "activeTab", publicName: "activeTab", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", activeTab: "activeTabChange", tabChange: "tabChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "items", predicate: OlTabItem, isSignal: true }], ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!-- 1. Mobile Dropdown View (Type=Default / Pills / Border bottom, Mobile=True - Figma #1149:80537, #1149:80539, #1149:80541) -->\n@if (isMobileDropdown()) {\n <div class=\"w-full max-w-md\">\n <div class=\"relative\">\n <!--\n `[ngModel]` ch\u1EE9 KH\u00D4NG ph\u1EA3i `[value]`: `[value]` tr\u00EAn <select> \u0111\u1EB7t thu\u1ED9c\n t\u00EDnh DOM NGAY, l\u00FAc c\u00E1c <option> c\u1EE7a @for ch\u01B0a t\u1ED3n t\u1EA1i, n\u00EAn tr\u00ECnh duy\u1EC7t\n ho\u00E0n nguy\u00EAn v\u1EC1 m\u1EE5c \u0111\u1EA7u. \u0110o 2026-09-17 v\u1EDBi `[selectedIndex]=\"2\"`: tab\n \u0111ang ch\u1ECDn l\u00E0 \"Ba\" m\u00E0 \u00F4 ch\u1ECDn hi\u1EC7n \"M\u1ED9t\". Build xanh, kh\u00F4ng c\u1EA3nh b\u00E1o.\n\n V\u00E0 `selectTab($event)` ch\u1EE9 kh\u00F4ng ph\u1EA3i `.selectedIndex` \u2014 c\u00E1i sau l\u00E0 v\u1ECB\n tr\u00ED trong danh s\u00E1ch DOM, tr\u00F9ng v\u1EDBi ch\u1EC9 s\u1ED1 tab ch\u1EC9 khi m\u1ECDi tab \u0111\u1EC1u \u0111\u01B0\u1EE3c\n v\u1EBD; l\u1ECDc b\u1EDBt m\u1ED9t tab l\u00E0 l\u1EC7ch ngay. `[value]` c\u1EE7a option m\u1EDBi l\u00E0 ngu\u1ED3n.\n -->\n <select\n [ngModel]=\"currentIndex()\"\n (ngModelChange)=\"selectTab(+$event)\"\n aria-label=\"Ch\u1ECDn tab\"\n class=\"w-full appearance-none rounded-lg border border-default-medium bg-neutral-primary py-2.5 pl-3.5 pr-10 text-sm font-medium text-heading shadow-xs focus:border-brand-light focus:outline-hidden focus:ring-1 focus:ring-brand-subtle cursor-pointer\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <option [value]=\"i\" [disabled]=\"item.disabled()\">\n {{ item.displayLabel() }}\n </option>\n }\n </select>\n <div\n class=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-body-subtle\"\n >\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" />\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div class=\"pt-4 w-full\">\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n} @else {\n <!-- 2. Standard / Desktop / Vertical Stack View -->\n <div\n [class]=\"\n isVertical()\n ? 'flex flex-col md:flex-row gap-6 items-start w-full'\n : 'flex w-full flex-col min-h-0'\n \"\n >\n <!-- Tab Headers Navigation -->\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs w-full max-w-md'\n : mobile() && variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs divide-y divide-default overflow-hidden w-full max-w-md'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'w-full md:w-64 shrink-0'\n : 'w-full md:w-44 shrink-0'\n : variant() === 'button' || variant() === 'boxed'\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs inline-flex max-w-full overflow-x-auto'\n : variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs inline-flex divide-x divide-default overflow-hidden max-w-full overflow-x-auto'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'border-b border-default w-full'\n : 'w-full'\n \"\n >\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'flex flex-col gap-1 w-full'\n : mobile() && variant() === 'button-group'\n ? 'flex flex-col w-full'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'flex flex-col gap-1 p-2 bg-neutral-primary rounded-md border border-default shadow-xs'\n : 'flex flex-col gap-2.5'\n : variant() === 'button' || variant() === 'boxed'\n ? 'flex items-center gap-1 ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'button-group'\n ? 'flex items-center ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'flex items-center gap-6 overflow-x-auto -mb-px ' +\n (fullWidth() ? 'justify-between' : '')\n : 'flex items-center gap-6 overflow-x-auto ' +\n (fullWidth() ? 'justify-between' : '')\n \"\n role=\"tablist\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [id]=\"item.tabId()\"\n [disabled]=\"item.disabled()\"\n [attr.aria-selected]=\"i === currentIndex()\"\n (click)=\"selectTab(i)\"\n [class]=\"\n 'inline-flex items-center font-medium transition-all duration-150 select-none whitespace-nowrap ' +\n sizeClasses() +\n ' ' +\n (fullWidth() || mobile() ? 'w-full justify-start ' : '') +\n (item.disabled() ? 'opacity-40 cursor-not-allowed ' : 'cursor-pointer ') +\n (mobile() && (variant() === 'button' || variant() === 'boxed')\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : mobile() && variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'default'\n ? i === currentIndex()\n ? 'text-fg-brand font-semibold'\n : 'text-body hover:text-heading'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? i === currentIndex()\n ? 'border-b-2 border-brand text-fg-brand font-semibold pb-3 -mb-px'\n : 'border-b-2 border-transparent text-body hover:text-heading hover:border-default-strong pb-3 -mb-px'\n : variant() === 'pills'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-full shadow-xs'\n : 'text-body hover:bg-neutral-tertiary rounded-full'\n : variant() === 'button' || variant() === 'boxed'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'vertical-pills' || variant() === 'vertical'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-md shadow-xs justify-start w-full'\n : 'text-body hover:bg-neutral-tertiary rounded-md justify-start w-full'\n : variant() === 'vertical-list'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-fg-brand font-semibold rounded-md justify-between w-full'\n : 'text-body hover:bg-neutral-secondary-medium rounded-md justify-between w-full'\n : '')\n \"\n >\n <!-- User select format with Avatar (Figma #1149:80606) -->\n @if (item.avatar()) {\n <div class=\"inline-flex items-center gap-3 min-w-0 text-left\">\n <img\n [src]=\"item.avatar()!\"\n alt=\"\"\n class=\"w-8 h-8 rounded-full object-cover shrink-0 border border-default\"\n />\n <div class=\"flex flex-col min-w-0\">\n <span class=\"truncate font-medium text-xs leading-tight text-heading\">{{\n item.displayLabel()\n }}</span>\n <!--\n \u0110\u1ECDc `displaySubtitle()` ch\u1EE9 kh\u00F4ng ph\u1EA3i `item.helper()`:\n `subtitle` v\u00E0 `helper` l\u00E0 b\u00ED danh c\u1EE7a nhau (ch\u00FA th\u00EDch ngay\n ch\u1ED7 khai trong tab-item), nh\u01B0ng render th\u1EB3ng `helper` th\u00EC\n \u0111\u1EB7t `subtitle=\"...\"` kh\u00F4ng hi\u1EC7n g\u00EC.\n -->\n @if (item.displaySubtitle()) {\n <span class=\"truncate text-[11px] text-body-subtle font-normal leading-tight\">{{\n item.displaySubtitle()\n }}</span>\n }\n </div>\n </div>\n } @else {\n <div class=\"inline-flex items-center gap-2 min-w-0\">\n @if (item.icon()) {\n <ol-icon [name]=\"item.icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n <span class=\"truncate\">{{ item.displayLabel() }}</span>\n </div>\n }\n\n @if (item.badge() !== null && item.badge() !== undefined && item.badge() !== '') {\n <ol-badge\n [color]=\"\n i === currentIndex() && (variant() === 'pills' || variant() === 'vertical-pills')\n ? 'white'\n : item.badgeColor()\n \"\n size=\"xs\"\n shape=\"pill\"\n class=\"ml-auto\"\n >\n {{ item.badge() }}\n </ol-badge>\n }\n\n @if (variant() === 'vertical-list' && !item.avatar()) {\n @if (i === currentIndex()) {\n <ol-icon\n name=\"check-circle\"\n size=\"xs\"\n class=\"text-fg-brand shrink-0 ml-auto\"\n ></ol-icon>\n } @else {\n <ol-icon\n name=\"alt-arrow-down\"\n size=\"xs\"\n class=\"text-body-subtle shrink-0 -rotate-90 ml-auto\"\n ></ol-icon>\n }\n }\n </button>\n }\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div\n [class]=\"isVertical() ? 'flex-1 min-w-0 w-full' : 'flex min-h-0 flex-1 flex-col pt-4 w-full'\"\n >\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14793
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTabs, isStandalone: true, selector: "ol-tabs", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, mobile: { classPropertyName: "mobile", publicName: "mobile", isSignal: true, isRequired: false, transformFunction: null }, mobileDropdown: { classPropertyName: "mobileDropdown", publicName: "mobileDropdown", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, activeTab: { classPropertyName: "activeTab", publicName: "activeTab", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", activeTab: "activeTabChange", tabChange: "tabChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "items", predicate: OlTabItem, isSignal: true }], ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!-- 1. Mobile Dropdown View (Type=Default / Pills / Border bottom, Mobile=True - Figma #1149:80537, #1149:80539, #1149:80541) -->\n@if (isMobileDropdown()) {\n <div class=\"w-full max-w-md\">\n <div class=\"relative\">\n <!--\n `[ngModel]` ch\u1EE9 KH\u00D4NG ph\u1EA3i `[value]`: `[value]` tr\u00EAn <select> \u0111\u1EB7t thu\u1ED9c\n t\u00EDnh DOM NGAY, l\u00FAc c\u00E1c <option> c\u1EE7a @for ch\u01B0a t\u1ED3n t\u1EA1i, n\u00EAn tr\u00ECnh duy\u1EC7t\n ho\u00E0n nguy\u00EAn v\u1EC1 m\u1EE5c \u0111\u1EA7u. \u0110o 2026-09-17 v\u1EDBi `[selectedIndex]=\"2\"`: tab\n \u0111ang ch\u1ECDn l\u00E0 \"Ba\" m\u00E0 \u00F4 ch\u1ECDn hi\u1EC7n \"M\u1ED9t\". Build xanh, kh\u00F4ng c\u1EA3nh b\u00E1o.\n\n V\u00E0 `selectTab($event)` ch\u1EE9 kh\u00F4ng ph\u1EA3i `.selectedIndex` \u2014 c\u00E1i sau l\u00E0 v\u1ECB\n tr\u00ED trong danh s\u00E1ch DOM, tr\u00F9ng v\u1EDBi ch\u1EC9 s\u1ED1 tab ch\u1EC9 khi m\u1ECDi tab \u0111\u1EC1u \u0111\u01B0\u1EE3c\n v\u1EBD; l\u1ECDc b\u1EDBt m\u1ED9t tab l\u00E0 l\u1EC7ch ngay. `[value]` c\u1EE7a option m\u1EDBi l\u00E0 ngu\u1ED3n.\n -->\n <select\n [ngModel]=\"currentIndex()\"\n (ngModelChange)=\"selectTab(+$event)\"\n aria-label=\"Ch\u1ECDn tab\"\n class=\"w-full appearance-none rounded-lg border border-default-medium bg-neutral-primary py-2.5 pl-3.5 pr-10 text-sm font-medium text-heading shadow-xs focus:border-brand-light focus:outline-hidden focus:ring-1 focus:ring-brand-subtle cursor-pointer\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <option [value]=\"i\" [disabled]=\"item.disabled()\">\n {{ item.displayLabel() }}\n </option>\n }\n </select>\n <div\n class=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-body-subtle\"\n >\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" />\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div class=\"pt-4 w-full\">\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n} @else {\n <!-- 2. Standard / Desktop / Vertical Stack View -->\n <div\n [class]=\"\n isVertical()\n ? 'flex flex-col md:flex-row gap-6 items-start w-full'\n : 'flex w-full flex-col min-h-0'\n \"\n >\n <!-- Tab Headers Navigation -->\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs w-full max-w-md'\n : mobile() && variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs divide-y divide-default overflow-hidden w-full max-w-md'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'w-full md:w-64 shrink-0'\n : 'w-full md:w-44 shrink-0'\n : variant() === 'button' || variant() === 'boxed'\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs inline-flex max-w-full overflow-x-auto'\n : variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs inline-flex divide-x divide-default overflow-hidden max-w-full overflow-x-auto'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'border-b border-default w-full'\n : 'w-full'\n \"\n >\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'flex flex-col gap-1 w-full'\n : mobile() && variant() === 'button-group'\n ? 'flex flex-col w-full'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'flex flex-col gap-1 p-2 bg-neutral-primary rounded-md border border-default shadow-xs'\n : 'flex flex-col gap-2.5'\n : variant() === 'button' || variant() === 'boxed'\n ? 'flex items-center gap-1 ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'button-group'\n ? 'flex items-center ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'flex items-center gap-6 overflow-x-auto -mb-px ' +\n (fullWidth() ? 'justify-between' : '')\n : 'flex items-center gap-6 overflow-x-auto ' +\n (fullWidth() ? 'justify-between' : '')\n \"\n role=\"tablist\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [id]=\"item.tabId()\"\n [disabled]=\"item.disabled()\"\n [attr.aria-selected]=\"i === currentIndex()\"\n (click)=\"selectTab(i)\"\n [class]=\"\n 'inline-flex items-center font-medium transition-all duration-150 select-none whitespace-nowrap ' +\n sizeClasses() +\n ' ' +\n (fullWidth() || mobile() ? 'w-full justify-start ' : '') +\n (item.disabled() ? 'opacity-40 cursor-not-allowed ' : 'cursor-pointer ') +\n (mobile() && (variant() === 'button' || variant() === 'boxed')\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : mobile() && variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'default'\n ? i === currentIndex()\n ? 'text-fg-brand font-semibold'\n : 'text-body hover:text-heading'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? i === currentIndex()\n ? 'border-b-2 border-brand text-fg-brand font-semibold pb-3 -mb-px'\n : 'border-b-2 border-transparent text-body hover:text-heading hover:border-default-strong pb-3 -mb-px'\n : variant() === 'pills'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-full shadow-xs'\n : 'text-body hover:bg-neutral-tertiary rounded-full'\n : variant() === 'button' || variant() === 'boxed'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'vertical-pills' || variant() === 'vertical'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-md shadow-xs justify-start w-full'\n : 'text-body hover:bg-neutral-tertiary rounded-md justify-start w-full'\n : variant() === 'vertical-list'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-fg-brand font-semibold rounded-md justify-between w-full'\n : 'text-body hover:bg-neutral-secondary-medium rounded-md justify-between w-full'\n : '')\n \"\n >\n <!-- User select format with Avatar (Figma #1149:80606) -->\n @if (item.avatar()) {\n <div class=\"inline-flex items-center gap-3 min-w-0 text-left\">\n <img\n [src]=\"item.avatar()!\"\n alt=\"\"\n class=\"w-8 h-8 rounded-full object-cover shrink-0 border border-default\"\n />\n <div class=\"flex flex-col min-w-0\">\n <span class=\"truncate font-medium text-xs leading-tight text-heading\">{{\n item.displayLabel()\n }}</span>\n <!--\n \u0110\u1ECDc `displaySubtitle()` ch\u1EE9 kh\u00F4ng ph\u1EA3i `item.helper()`:\n `subtitle` v\u00E0 `helper` l\u00E0 b\u00ED danh c\u1EE7a nhau (ch\u00FA th\u00EDch ngay\n ch\u1ED7 khai trong tab-item), nh\u01B0ng render th\u1EB3ng `helper` th\u00EC\n \u0111\u1EB7t `subtitle=\"...\"` kh\u00F4ng hi\u1EC7n g\u00EC.\n -->\n @if (item.displaySubtitle()) {\n <span class=\"truncate text-[11px] text-body-subtle font-normal leading-tight\">{{\n item.displaySubtitle()\n }}</span>\n }\n </div>\n </div>\n } @else {\n <div class=\"inline-flex items-center gap-2 min-w-0\">\n @if (item.icon()) {\n <ol-icon [name]=\"item.icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n <span class=\"truncate\">{{ item.displayLabel() }}</span>\n </div>\n }\n\n @if (item.badge() !== null && item.badge() !== undefined && item.badge() !== '') {\n <ol-badge\n [color]=\"\n i === currentIndex() && (variant() === 'pills' || variant() === 'vertical-pills')\n ? 'white'\n : item.badgeColor()\n \"\n size=\"xs\"\n shape=\"pill\"\n class=\"ml-auto\"\n >\n {{ item.badge() }}\n </ol-badge>\n }\n\n @if (variant() === 'vertical-list' && !item.avatar()) {\n @if (i === currentIndex()) {\n <ol-icon\n name=\"check-circle\"\n size=\"xs\"\n class=\"text-fg-brand shrink-0 ml-auto\"\n ></ol-icon>\n } @else {\n <ol-icon\n name=\"alt-arrow-down\"\n size=\"xs\"\n class=\"text-body-subtle shrink-0 -rotate-90 ml-auto\"\n ></ol-icon>\n }\n }\n </button>\n }\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div\n [class]=\"isVertical() ? 'flex-1 min-w-0 w-full' : 'flex min-h-0 flex-1 flex-col pt-4 w-full'\"\n >\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14413
14794
|
}
|
|
14414
14795
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTabs, decorators: [{
|
|
14415
14796
|
type: Component,
|
|
@@ -14795,7 +15176,7 @@ class OlBottomNavItem {
|
|
|
14795
15176
|
}, /* @ts-ignore */
|
|
14796
15177
|
...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
|
|
14797
15178
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBottomNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14798
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBottomNavItem, isStandalone: true, selector: "ol-bottom-nav-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isCenterAction: { classPropertyName: "isCenterAction", publicName: "isCenterAction", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba nh\u00E1nh routerLink / href / button ch\u1EC9 kh\u00E1c nhau \u1EDF TH\u1EBA B\u1ECCC; ru\u1ED9t y h\u1EC7t nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y ru\u1ED9t \u0111\u01B0\u1EE3c ch\u00E9p ba l\u1EA7n (105 d\u00F2ng) \u2014 s\u1EEDa gradient hay badge ph\u1EA3i s\u1EEDa\n \u0111\u1EE7 ba ch\u1ED7, qu\u00EAn m\u1ED9t l\u00E0 l\u1EC7ch \u00E2m th\u1EA7m. Gom v\u00E0o `ng-template`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n >\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <button type=\"button\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </button>\n}\n\n<ng-template #ruot>\n @if (isCenterAction()) {\n <!-- Gradient n\u00FAt h\u00E0nh \u0111\u1ED9ng gi\u1EEFa: `from-brand-700 to-brand-500` GI\u1EEE b\u1EADc s\u1ED1.\n `brand` l\u00E0 b\u1EA3ng Figma C\u00D3, v\u00E0 h\u1ECD `from-`/`to-` kh\u00F4ng c\u00F3 token ng\u1EEF ngh\u0129a\n n\u00E0o \u0111\u01B0\u1EE3c @theme emit (\u0111o 2026-09-15). -->\n <div\n class=\"w-12 h-12 rounded-full bg-linear-to-tr from-brand-700 to-brand-500 text-white shadow-lg shadow-md/30 flex items-center justify-center transform group-hover:scale-105 group-active:scale-95 transition-all\"\n >\n @if (icon()) {\n <ol-icon [name]=\"icon()\" size=\"lg\" color=\"white\"></ol-icon>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs font-semibold text-fg-brand mt-1 truncate\">{{ label() }}</span>\n }\n } @else {\n <div class=\"relative flex items-center justify-center\">\n @if (icon()) {\n <ol-icon\n [name]=\"icon()\"\n size=\"base\"\n class=\"transition-transform group-hover:scale-110\"\n ></ol-icon>\n }\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <span class=\"absolute -top-1.5 -right-2\">\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n </span>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs mt-1 truncate font-medium\">{{ label() }}</span>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15179
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBottomNavItem, isStandalone: true, selector: "ol-bottom-nav-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isCenterAction: { classPropertyName: "isCenterAction", publicName: "isCenterAction", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba nh\u00E1nh routerLink / href / button ch\u1EC9 kh\u00E1c nhau \u1EDF TH\u1EBA B\u1ECCC; ru\u1ED9t y h\u1EC7t nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y ru\u1ED9t \u0111\u01B0\u1EE3c ch\u00E9p ba l\u1EA7n (105 d\u00F2ng) \u2014 s\u1EEDa gradient hay badge ph\u1EA3i s\u1EEDa\n \u0111\u1EE7 ba ch\u1ED7, qu\u00EAn m\u1ED9t l\u00E0 l\u1EC7ch \u00E2m th\u1EA7m. Gom v\u00E0o `ng-template`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n >\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <button type=\"button\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </button>\n}\n\n<ng-template #ruot>\n @if (isCenterAction()) {\n <!-- Gradient n\u00FAt h\u00E0nh \u0111\u1ED9ng gi\u1EEFa: `from-brand-700 to-brand-500` GI\u1EEE b\u1EADc s\u1ED1.\n `brand` l\u00E0 b\u1EA3ng Figma C\u00D3, v\u00E0 h\u1ECD `from-`/`to-` kh\u00F4ng c\u00F3 token ng\u1EEF ngh\u0129a\n n\u00E0o \u0111\u01B0\u1EE3c @theme emit (\u0111o 2026-09-15). -->\n <div\n class=\"w-12 h-12 rounded-full bg-linear-to-tr from-brand-700 to-brand-500 text-white shadow-lg shadow-md/30 flex items-center justify-center transform group-hover:scale-105 group-active:scale-95 transition-all\"\n >\n @if (icon()) {\n <ol-icon [name]=\"icon()\" size=\"lg\" color=\"white\"></ol-icon>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs font-semibold text-fg-brand mt-1 truncate\">{{ label() }}</span>\n }\n } @else {\n <div class=\"relative flex items-center justify-center\">\n @if (icon()) {\n <ol-icon\n [name]=\"icon()\"\n size=\"base\"\n class=\"transition-transform group-hover:scale-110\"\n ></ol-icon>\n }\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <span class=\"absolute -top-1.5 -right-2\">\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n </span>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs mt-1 truncate font-medium\">{{ label() }}</span>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14799
15180
|
}
|
|
14800
15181
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBottomNavItem, decorators: [{
|
|
14801
15182
|
type: Component,
|
|
@@ -14959,7 +15340,7 @@ class OlStepper {
|
|
|
14959
15340
|
}
|
|
14960
15341
|
}
|
|
14961
15342
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlStepper, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14962
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlStepper, isStandalone: true, selector: "ol-stepper", inputs: { currentStep: { classPropertyName: "currentStep", publicName: "currentStep", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, breakpoint: { classPropertyName: "breakpoint", publicName: "breakpoint", isSignal: true, isRequired: false, transformFunction: null }, linear: { classPropertyName: "linear", publicName: "linear", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentStep: "currentStepChange", stepChange: "stepChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "steps", predicate: OlStepItem, isSignal: true }], ngImport: i0, template: "<div [class]=\"isVertical() ? 'flex flex-col md:flex-row gap-8 w-full' : 'w-full'\">\n <!-- Stepper Header / Navigation Area -->\n <div [class]=\"isVertical() ? 'w-full md:w-80 shrink-0' : 'w-full mb-6'\">\n @if (isMobile()) {\n <!-- Mobile Layout -->\n <div class=\"p-4 rounded-md bg-neutral-tertiary/80 border border-default\">\n <div class=\"flex items-center justify-between mb-2\">\n <span class=\"text-xs font-semibold text-fg-brand uppercase tracking-wider\">\n B\u01B0\u1EDBc {{ currentStep() + 1 }} / {{ steps().length }}\n </span>\n @if (steps()[currentStep()]; as activeStep) {\n <span class=\"text-xs text-body-subtle font-medium\">\n {{ activeStep.title() }}\n </span>\n }\n </div>\n <!-- Progress Bar -->\n <div\n class=\"w-full bg-neutral-tertiary-medium h-2 rounded-full overflow-hidden flex gap-1 p-0.5\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div\n class=\"h-full flex-1 rounded-full transition-all duration-300\"\n [class.bg-brand]=\"i <= currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i > currentStep()\"\n ></div>\n }\n </div>\n </div>\n } @else if (variant() === 'cards') {\n <!-- Cards Variant (Vertical Stack) -->\n <div class=\"flex flex-col gap-3\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <!--\n `<button role=\"tab\">` ch\u1EE9 KH\u00D4NG ph\u1EA3i `<div (click)>`. Khung ngo\u00E0i\n khai `role=\"tablist\"` nh\u01B0ng \u0111o 2026-09-17 b\u00EAn trong c\u00F3 **0\n `role=\"tab\"` v\u00E0 0 ph\u1EA7n t\u1EED tab t\u1EDBi \u0111\u01B0\u1EE3c** \u2014 `tablist` r\u1ED7ng vai tr\u00F2,\n v\u00E0 b\u00E0n ph\u00EDm kh\u00F4ng ch\u1ECDn n\u1ED5i b\u01B0\u1EDBc n\u00E0o. Ba bi\u1EBFn th\u1EC3 stepper kh\u00E1c \u0111\u00E3\n d\u00F9ng `<button>` s\u1EB5n; ch\u1EC9 ri\u00EAng `cards` l\u1EC7ch.\n\n `w-full` v\u00EC button kh\u00F4ng t\u1EF1 gi\u00E3n nh\u01B0 div.\n -->\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"step.effectiveState() === 'active'\"\n [disabled]=\"step.effectiveState() === 'disabled'\"\n (click)=\"goToStep(i)\"\n [class]=\"\n 'w-full flex items-center justify-between p-4 rounded-md border transition-all duration-200 text-left ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-secondary-soft border-default text-fg-disabled'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer shadow-xs ring-2 ring-brand-subtle'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer hover:shadow-xs border-danger-subtle bg-danger-soft'\n : 'cursor-pointer hover:shadow-xs border-default-medium bg-neutral-secondary-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n <!-- Left Icon / Step Badge -->\n <div\n [class]=\"\n 'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 font-bold text-xs transition-colors ' +\n (step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'active'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body')\n \"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </div>\n\n <!-- Title & Subtitle -->\n <div class=\"min-w-0\">\n <div\n [class]=\"\n 'text-sm truncate ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand-strong '\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled truncate\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n\n <!-- Right Status Icon -->\n <div class=\"shrink-0 ml-3\">\n @if (step.effectiveState() === 'completed') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <ol-icon name=\"check-circle\" size=\"xs\" color=\"brand\"></ol-icon>\n <span class=\"hidden sm:inline\">Ho\u00E0n t\u1EA5t</span>\n </span>\n } @else if (step.effectiveState() === 'active') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <span class=\"w-2 h-2 rounded-full bg-brand animate-pulse\"></span>\n <span class=\"hidden sm:inline\">\u0110ang th\u1EF1c hi\u1EC7n</span>\n </span>\n } @else if (step.effectiveState() === 'error') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n <span class=\"hidden sm:inline\">L\u1ED7i</span>\n </span>\n } @else {\n <ol-icon name=\"chevron-right\" size=\"xs\" class=\"text-fg-disabled\"></ol-icon>\n }\n </div>\n </button>\n }\n </div>\n } @else if (isVertical()) {\n <!-- Vertical Variant -->\n <div class=\"flex flex-col relative\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-start gap-4 relative pb-8 last:pb-0\">\n <!-- Connecting Vertical Line -->\n @if (i < steps().length - 1) {\n <div\n class=\"absolute left-4.25 top-9 bottom-0 w-0.5 transition-colors duration-200\"\n [class.bg-brand]=\"i < currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i >= currentStep()\"\n ></div>\n }\n\n <!-- Step Circle / Icon Shape -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 z-10 ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-tertiary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'cursor-pointer bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n\n <!-- Step Title & Subtitle -->\n <div class=\"text-left pt-1 cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled mt-0.5\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- Horizontal Variants: Default | Only Icons | Icons & Text | With Dots -->\n <div\n class=\"flex items-center justify-between w-full relative\"\n role=\"tablist\"\n aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-center flex-1 last:flex-none relative\">\n <div class=\"flex items-center gap-3\">\n @if (variant() === 'with-dots') {\n <!-- With Dots Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'flex items-center justify-center transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'w-3.5 h-3.5 rounded-full opacity-50 bg-neutral-primary text-fg-disabled border-2 border-default-medium'\n : step.effectiveState() === 'completed'\n ? 'w-4 h-4 rounded-full bg-brand text-white'\n : step.effectiveState() === 'active'\n ? 'w-4 h-4 rounded-full bg-neutral-primary border-2 border-brand-light ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'w-4 h-4 rounded-full bg-danger-surface ring-4 ring-danger-subtle'\n : 'w-3.5 h-3.5 rounded-full bg-neutral-primary border-2 border-default-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" class=\"scale-75\"></ol-icon>\n }\n </button>\n } @else if (variant() === 'only-icons' || variant() === 'icons-text') {\n <!-- Icon Shape Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-10 h-10 rounded-full flex items-center justify-center font-semibold text-sm transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-tertiary text-fg-disabled'\n : step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body border border-default')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"sm\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"sm\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"sm\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n } @else {\n <!-- Default Variant (Numbered Circle) -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-primary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n }\n\n <!-- Text Title & Subtitle (unless only-icons) -->\n @if (variant() !== 'only-icons') {\n <div class=\"text-left cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled hidden md:block\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Connecting Line (Horizontal) -->\n @if (i < steps().length - 1) {\n <div class=\"flex-1 mx-4 h-0.5 bg-neutral-tertiary-medium relative overflow-hidden\">\n <div\n class=\"h-full bg-brand transition-all duration-300\"\n [class.w-full]=\"i < currentStep()\"\n [class.w-0]=\"i >= currentStep()\"\n ></div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Stepper Body Content Area -->\n <div [class]=\"isVertical() ? 'flex-1 min-w-0' : 'w-full'\">\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15343
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlStepper, isStandalone: true, selector: "ol-stepper", inputs: { currentStep: { classPropertyName: "currentStep", publicName: "currentStep", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, breakpoint: { classPropertyName: "breakpoint", publicName: "breakpoint", isSignal: true, isRequired: false, transformFunction: null }, linear: { classPropertyName: "linear", publicName: "linear", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentStep: "currentStepChange", stepChange: "stepChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "steps", predicate: OlStepItem, isSignal: true }], ngImport: i0, template: "<div [class]=\"isVertical() ? 'flex flex-col md:flex-row gap-8 w-full' : 'w-full'\">\n <!-- Stepper Header / Navigation Area -->\n <div [class]=\"isVertical() ? 'w-full md:w-80 shrink-0' : 'w-full mb-6'\">\n @if (isMobile()) {\n <!-- Mobile Layout -->\n <div class=\"p-4 rounded-md bg-neutral-tertiary/80 border border-default\">\n <div class=\"flex items-center justify-between mb-2\">\n <span class=\"text-xs font-semibold text-fg-brand uppercase tracking-wider\">\n B\u01B0\u1EDBc {{ currentStep() + 1 }} / {{ steps().length }}\n </span>\n @if (steps()[currentStep()]; as activeStep) {\n <span class=\"text-xs text-body-subtle font-medium\">\n {{ activeStep.title() }}\n </span>\n }\n </div>\n <!-- Progress Bar -->\n <div\n class=\"w-full bg-neutral-tertiary-medium h-2 rounded-full overflow-hidden flex gap-1 p-0.5\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div\n class=\"h-full flex-1 rounded-full transition-all duration-300\"\n [class.bg-brand]=\"i <= currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i > currentStep()\"\n ></div>\n }\n </div>\n </div>\n } @else if (variant() === 'cards') {\n <!-- Cards Variant (Vertical Stack) -->\n <div class=\"flex flex-col gap-3\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <!--\n `<button role=\"tab\">` ch\u1EE9 KH\u00D4NG ph\u1EA3i `<div (click)>`. Khung ngo\u00E0i\n khai `role=\"tablist\"` nh\u01B0ng \u0111o 2026-09-17 b\u00EAn trong c\u00F3 **0\n `role=\"tab\"` v\u00E0 0 ph\u1EA7n t\u1EED tab t\u1EDBi \u0111\u01B0\u1EE3c** \u2014 `tablist` r\u1ED7ng vai tr\u00F2,\n v\u00E0 b\u00E0n ph\u00EDm kh\u00F4ng ch\u1ECDn n\u1ED5i b\u01B0\u1EDBc n\u00E0o. Ba bi\u1EBFn th\u1EC3 stepper kh\u00E1c \u0111\u00E3\n d\u00F9ng `<button>` s\u1EB5n; ch\u1EC9 ri\u00EAng `cards` l\u1EC7ch.\n\n `w-full` v\u00EC button kh\u00F4ng t\u1EF1 gi\u00E3n nh\u01B0 div.\n -->\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"step.effectiveState() === 'active'\"\n [disabled]=\"step.effectiveState() === 'disabled'\"\n (click)=\"goToStep(i)\"\n [class]=\"\n 'w-full flex items-center justify-between p-4 rounded-md border transition-all duration-200 text-left ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-secondary-soft border-default text-fg-disabled'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer shadow-xs ring-2 ring-brand-subtle'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer hover:shadow-xs border-danger-subtle bg-danger-soft'\n : 'cursor-pointer hover:shadow-xs border-default-medium bg-neutral-secondary-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n <!-- Left Icon / Step Badge -->\n <div\n [class]=\"\n 'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 font-bold text-xs transition-colors ' +\n (step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'active'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body')\n \"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </div>\n\n <!-- Title & Subtitle -->\n <div class=\"min-w-0\">\n <div\n [class]=\"\n 'text-sm truncate ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand-strong '\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled truncate\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n\n <!-- Right Status Icon -->\n <div class=\"shrink-0 ml-3\">\n @if (step.effectiveState() === 'completed') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <ol-icon name=\"check-circle\" size=\"xs\" color=\"brand\"></ol-icon>\n <span class=\"hidden sm:inline\">Ho\u00E0n t\u1EA5t</span>\n </span>\n } @else if (step.effectiveState() === 'active') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <span class=\"w-2 h-2 rounded-full bg-brand animate-pulse\"></span>\n <span class=\"hidden sm:inline\">\u0110ang th\u1EF1c hi\u1EC7n</span>\n </span>\n } @else if (step.effectiveState() === 'error') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n <span class=\"hidden sm:inline\">L\u1ED7i</span>\n </span>\n } @else {\n <ol-icon name=\"chevron-right\" size=\"xs\" class=\"text-fg-disabled\"></ol-icon>\n }\n </div>\n </button>\n }\n </div>\n } @else if (isVertical()) {\n <!-- Vertical Variant -->\n <div class=\"flex flex-col relative\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-start gap-4 relative pb-8 last:pb-0\">\n <!-- Connecting Vertical Line -->\n @if (i < steps().length - 1) {\n <div\n class=\"absolute left-4.25 top-9 bottom-0 w-0.5 transition-colors duration-200\"\n [class.bg-brand]=\"i < currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i >= currentStep()\"\n ></div>\n }\n\n <!-- Step Circle / Icon Shape -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 z-10 ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-tertiary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'cursor-pointer bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n\n <!-- Step Title & Subtitle -->\n <div class=\"text-left pt-1 cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled mt-0.5\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- Horizontal Variants: Default | Only Icons | Icons & Text | With Dots -->\n <div\n class=\"flex items-center justify-between w-full relative\"\n role=\"tablist\"\n aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-center flex-1 last:flex-none relative\">\n <div class=\"flex items-center gap-3\">\n @if (variant() === 'with-dots') {\n <!-- With Dots Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'flex items-center justify-center transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'w-3.5 h-3.5 rounded-full opacity-50 bg-neutral-primary text-fg-disabled border-2 border-default-medium'\n : step.effectiveState() === 'completed'\n ? 'w-4 h-4 rounded-full bg-brand text-white'\n : step.effectiveState() === 'active'\n ? 'w-4 h-4 rounded-full bg-neutral-primary border-2 border-brand-light ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'w-4 h-4 rounded-full bg-danger-surface ring-4 ring-danger-subtle'\n : 'w-3.5 h-3.5 rounded-full bg-neutral-primary border-2 border-default-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" class=\"scale-75\"></ol-icon>\n }\n </button>\n } @else if (variant() === 'only-icons' || variant() === 'icons-text') {\n <!-- Icon Shape Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-10 h-10 rounded-full flex items-center justify-center font-semibold text-sm transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-tertiary text-fg-disabled'\n : step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body border border-default')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"sm\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"sm\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"sm\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n } @else {\n <!-- Default Variant (Numbered Circle) -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-primary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n }\n\n <!-- Text Title & Subtitle (unless only-icons) -->\n @if (variant() !== 'only-icons') {\n <div class=\"text-left cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled hidden md:block\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Connecting Line (Horizontal) -->\n @if (i < steps().length - 1) {\n <div class=\"flex-1 mx-4 h-0.5 bg-neutral-tertiary-medium relative overflow-hidden\">\n <div\n class=\"h-full bg-brand transition-all duration-300\"\n [class.w-full]=\"i < currentStep()\"\n [class.w-0]=\"i >= currentStep()\"\n ></div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Stepper Body Content Area -->\n <div [class]=\"isVertical() ? 'flex-1 min-w-0' : 'w-full'\">\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14963
15344
|
}
|
|
14964
15345
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlStepper, decorators: [{
|
|
14965
15346
|
type: Component,
|
|
@@ -15041,15 +15422,50 @@ class OlTreeNodeItem {
|
|
|
15041
15422
|
expanded: !!this.node().expanded,
|
|
15042
15423
|
}), /* @ts-ignore */
|
|
15043
15424
|
...(ngDevMode ? [{ debugName: "tplContext" }] : /* istanbul ignore next */ []));
|
|
15425
|
+
/**
|
|
15426
|
+
* Chỉ `id` của nút, tách khỏi cả object.
|
|
15427
|
+
*
|
|
15428
|
+
* `effect` bên dưới đọc tín hiệu này thay vì `this.node()`: đọc cả object
|
|
15429
|
+
* thì effect chạy lại mỗi lần nơi dùng thay MẢNG gốc — kiểu tải lại danh
|
|
15430
|
+
* sách từ máy chủ — dù chính nút này không đổi gì. `computed` chỉ phát tín
|
|
15431
|
+
* hiệu khi GIÁ TRỊ đổi, mà `id` là chuỗi nên so sánh bằng `===`: tải lại
|
|
15432
|
+
* 100 nút với cùng id thì effect không chạy lần nào.
|
|
15433
|
+
*/
|
|
15434
|
+
nodeId = computed(() => this.node().id, /* @ts-ignore */
|
|
15435
|
+
...(ngDevMode ? [{ debugName: "nodeId" }] : /* istanbul ignore next */ []));
|
|
15044
15436
|
constructor() {
|
|
15045
|
-
|
|
15046
|
-
|
|
15437
|
+
/*
|
|
15438
|
+
* Điều hướng bằng phím chỉ đổi `focusedId`; phải gọi `.focus()` thật thì
|
|
15439
|
+
* trình duyệt mới chuyển con trỏ.
|
|
15440
|
+
*
|
|
15441
|
+
* HAI ĐIỀU KIỆN CHẶN, cả hai đều có lý do đo được:
|
|
15442
|
+
*
|
|
15443
|
+
* 1. Đọc `nodeId()` chứ không `node()`. Đo bằng ca thử "tải lại toàn bộ
|
|
15444
|
+
* cây, id giữ nguyên": bản cũ effect chạy lại và CƯỚP focus khỏi phần
|
|
15445
|
+
* tử người dùng đang đứng (đo được focus nhảy từ nút ngoài cây vào một
|
|
15446
|
+
* `div` trong cây).
|
|
15447
|
+
*
|
|
15448
|
+
* 2. Chỉ lấy focus khi focus ĐANG Ở TRONG CÂY. Người dùng bấm ra ô tìm
|
|
15449
|
+
* kiếm rồi dữ liệu tự cập nhật thì con trỏ đang ở ô tìm — kéo nó về cây
|
|
15450
|
+
* là cướp thao tác đang làm dở. `focusedId` chỉ được đặt khi điều hướng
|
|
15451
|
+
* bằng phím, nhưng nó KHÔNG TỰ XOÁ khi người dùng rời cây.
|
|
15452
|
+
*
|
|
15453
|
+
* Hệ quả phụ, cũng là thứ người dùng báo: `.focus()` gọi BẰNG MÃ làm
|
|
15454
|
+
* `:focus-visible` khớp, nên viền brand 2px hiện ra sau mỗi lần cập nhật
|
|
15455
|
+
* dữ liệu — trông như "viền đen to" trên nền sẫm. Chặn effect chạy oan là
|
|
15456
|
+
* hết luôn viền oan.
|
|
15457
|
+
*/
|
|
15047
15458
|
effect(() => {
|
|
15048
|
-
if (this.focusedId() !== this.
|
|
15459
|
+
if (this.focusedId() !== this.nodeId())
|
|
15049
15460
|
return;
|
|
15050
15461
|
const el = this.hang()?.nativeElement;
|
|
15051
|
-
if (el
|
|
15052
|
-
|
|
15462
|
+
if (!el || document.activeElement === el)
|
|
15463
|
+
return;
|
|
15464
|
+
// Chỉ cướp focus khi nó vốn đã ở trong cây này.
|
|
15465
|
+
const tree = el.closest('[role="tree"]');
|
|
15466
|
+
if (tree && !tree.contains(document.activeElement))
|
|
15467
|
+
return;
|
|
15468
|
+
el.focus();
|
|
15053
15469
|
});
|
|
15054
15470
|
}
|
|
15055
15471
|
/**
|
|
@@ -15094,7 +15510,7 @@ class OlTreeNodeItem {
|
|
|
15094
15510
|
this.nodeCheck.emit({ node: this.node(), checked });
|
|
15095
15511
|
}
|
|
15096
15512
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTreeNodeItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15097
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTreeNodeItem, isStandalone: true, selector: "ol-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null }, actionsTemplate: { classPropertyName: "actionsTemplate", publicName: "actionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, checkable: { classPropertyName: "checkable", publicName: "checkable", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, toggleIcon: { classPropertyName: "toggleIcon", publicName: "toggleIcon", isSignal: true, isRequired: false, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, tabbableId: { classPropertyName: "tabbableId", publicName: "tabbableId", isSignal: true, isRequired: false, transformFunction: null }, focusedId: { classPropertyName: "focusedId", publicName: "focusedId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeClick: "nodeClick", nodeCheck: "nodeCheck", toggleExpand: "toggleExpand", nodeFocus: "nodeFocus" }, viewQueries: [{ propertyName: "hang", first: true, predicate: ["hang"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"select-none py-0.5\">\n <!-- Focus v\u1EBD VI\u1EC0N 2px b\u00EAn trong, kh\u00F4ng ph\u1EA3i ring: Figma `ol-tree`\n State=Focus \u0111o \u0111\u01B0\u1EE3c `strokeWeight: 2`, `strokeAlign: INSIDE`, m\u00E0u\n `border/border-brand` #2b4182. Ring v\u1EBD NGO\u00C0I h\u1ED9p n\u00EAn kh\u00E1c h\u00ECnh, v\u00E0 \u1EDF\n c\u00E2y l\u1ED3ng nhau n\u00F3 ch\u1ED3ng l\u00EAn node b\u00EAn c\u1EA1nh. -->\n <div\n #hang\n role=\"treeitem\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-level]=\"ariaLevel()\"\n [attr.aria-expanded]=\"ariaExpanded()\"\n [attr.aria-selected]=\"!!node().selected\"\n [attr.aria-disabled]=\"node().disabled ? true : null\"\n (focus)=\"nodeFocus.emit(node().id)\"\n class=\"group flex cursor-pointer items-center gap-2 rounded-lg border-2 border-transparent p-1 text-sm outline-hidden transition-colors focus:outline-hidden focus-visible:border-brand\"\n [class.bg-brand-softer]=\"node().selected\"\n [class.text-fg-brand]=\"node().selected\"\n [class.font-medium]=\"node().selected\"\n [class.text-heading]=\"!node().selected && !node().disabled\"\n [class.hover:bg-brand-softer]=\"!node().selected && !node().disabled\"\n [class.opacity-50]=\"node().disabled\"\n [class.cursor-not-allowed]=\"node().disabled\"\n (click)=\"handleClick()\"\n >\n <!-- Expand Toggle Button -->\n @if (node().children && node().children!.length > 0) {\n <!--\n N\u00FAt ch\u1EC9 c\u00F3 icon n\u00EAn PH\u1EA2I c\u00F3 t\u00EAn: \u0111o `/tree` ra 18 n\u00FAt g\u1EADp kh\u00F4ng t\u00EAn\n trong c\u00E2y tr\u1EE3 n\u0103ng. `aria-expanded` cho bi\u1EBFt tr\u1EA1ng th\u00E1i, `aria-label`\n cho bi\u1EBFt n\u00FAt n\u00E0y g\u1EADp/m\u1EDF NH\u00C1NH N\u00C0O \u2014 nhi\u1EC1u n\u00FAt gi\u1ED1ng h\u1EC7t nhau tr\u00EAn m\u1ED9t\n trang, ch\u1EC9 \"M\u1EDF r\u1ED9ng\" th\u00F4i th\u00EC v\u1EABn kh\u00F4ng ph\u00E2n bi\u1EC7t \u0111\u01B0\u1EE3c.\n -->\n <button\n type=\"button\"\n (click)=\"handleToggle($event)\"\n [attr.aria-expanded]=\"!!node().expanded\"\n [attr.aria-label]=\"(node().expanded ? 'Thu g\u1ECDn ' : 'M\u1EDF r\u1ED9ng ') + (node().label || 'nh\u00E1nh')\"\n class=\"w-5 h-5 flex items-center justify-center rounded text-body-subtle hover:text-heading transition-colors shrink-0\"\n >\n @if (toggleIcon() === 'plus-minus') {\n <span\n class=\"w-4 h-4 rounded border border-default-strong flex items-center justify-center text-xs font-mono font-bold leading-none text-body\"\n >\n {{ node().expanded ? '\u2212' : '+' }}\n </span>\n } @else {\n <ol-icon\n [name]=\"node().expanded ? 'alt-arrow-down' : 'alt-arrow-right'\"\n size=\"xs\"\n ></ol-icon>\n }\n </button>\n } @else {\n @if (!checkable()) {\n <span class=\"w-5 h-5 flex items-center justify-center shrink-0\">\n <span\n class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand transition-colors\"\n ></span>\n </span>\n }\n }\n <!-- Checkbox option (when checkable) -->\n @if (checkable()) {\n <div class=\"flex items-center justify-center shrink-0\" (click)=\"$event.stopPropagation()\">\n <ol-checkbox\n [standalone]=\"true\"\n [ariaLabel]=\"'Ch\u1ECDn ' + (node().label || 'nh\u00E1nh')\"\n [checked]=\"node().selected ?? false\"\n [indeterminate]=\"indeterminate()\"\n [disabled]=\"node().disabled ?? false\"\n (checkedChange)=\"handleCheck($event)\"\n size=\"base\"\n ></ol-checkbox>\n </div>\n }\n <!--\n Template tu\u1EF3 bi\u1EBFn CH\u1EC8 thay ph\u1EA7n n\u1ED9i dung (icon \u00B7 nh\u00E3n \u00B7 count \u00B7 badge).\n N\u00FAt m\u1EDF r\u1ED9ng, checkbox v\u00E0 th\u1EBB `role=\"treeitem\"` b\u1ECDc ngo\u00E0i v\u1EABn do lib gi\u1EEF \u2014\n cho ng\u01B0\u1EDDi d\u00F9ng v\u1EBD c\u1EA3 h\u00E0ng l\u00E0 m\u1EA5t ARIA \u0111\u00E3 l\u00E0m \u0111\u00FAng chu\u1EA9n \u1EDF BUG-017.\n -->\n @if (nodeTemplate()) {\n <ng-container *ngTemplateOutlet=\"nodeTemplate()!; context: tplContext()\" />\n } @else {\n <!-- Node Icon (if provided) -->\n @if (node().icon) {\n <ol-icon\n [name]=\"node().icon!\"\n size=\"xs\"\n [class.text-fg-brand]=\"node().selected\"\n [class.text-body-subtle]=\"!node().selected\"\n ></ol-icon>\n }\n <!-- Node Label -->\n <span class=\"truncate flex-1\">{{ node().label }}</span>\n <!-- Count or Badge -->\n @if (node().count !== undefined) {\n <span class=\"text-2xs text-body-subtle font-mono\"> ({{ node().count }}) </span>\n }\n @if (node().badge) {\n <ol-badge [color]=\"node().badgeColor || 'brand'\" size=\"xs\" shape=\"pill\">\n {{ node().badge }}\n </ol-badge>\n }\n }\n <!--\n H\u00E0nh \u0111\u1ED9ng cu\u1ED1i h\u00E0ng. \u0110\u1EB7t NGO\u00C0I c\u1EB7p @if/@else \u1EDF tr\u00EAn l\u00E0 c\u00F3 ch\u1EE7 \u00FD: n\u00FAt ph\u1EA3i\n hi\u1EC7n c\u1EA3 khi ng\u01B0\u1EDDi d\u00F9ng t\u1EF1 v\u1EBD n\u1ED9i dung b\u1EB1ng `nodeTemplate`. \u0110\u01B0a v\u00E0o trong\n nh\u00E1nh @else th\u00EC b\u1EADt `nodeTemplate` l\u00E0 n\u00FAt bi\u1EBFn m\u1EA5t \u2014 build v\u1EABn xanh.\n -->\n @if (actionsTemplate()) {\n <span class=\"ms-auto flex shrink-0 items-center gap-1\">\n <ng-container *ngTemplateOutlet=\"actionsTemplate()!; context: tplContext()\" />\n </span>\n }\n </div>\n <!-- Children Nodes (Indented 20px guide line + pl-5 child offset) -->\n @if (node().expanded && node().children && node().children!.length > 0) {\n <div role=\"group\" class=\"ml-5 mt-1 space-y-1 border-l border-default pl-5\">\n @for (child of node().children; track child.id) {\n <ol-tree-node\n [node]=\"child\"\n [nodeTemplate]=\"nodeTemplate()\"\n [actionsTemplate]=\"actionsTemplate()\"\n [checkable]=\"checkable()\"\n [selectionMode]=\"selectionMode()\"\n [toggleIcon]=\"toggleIcon()\"\n [level]=\"level() + 1\"\n [tabbableId]=\"tabbableId()\"\n [focusedId]=\"focusedId()\"\n (nodeFocus)=\"nodeFocus.emit($event)\"\n (nodeClick)=\"nodeClick.emit($event)\"\n (nodeCheck)=\"nodeCheck.emit($event)\"\n (toggleExpand)=\"toggleExpand.emit($event)\"\n ></ol-tree-node>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: OlTreeNodeItem, selector: "ol-tree-node", inputs: ["node", "nodeTemplate", "actionsTemplate", "checkable", "selectionMode", "toggleIcon", "level", "tabbableId", "focusedId"], outputs: ["nodeClick", "nodeCheck", "toggleExpand", "nodeFocus"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "component", type: OlCheckbox, selector: "ol-checkbox", inputs: ["checked", "label", "ariaLabel", "description", "disabled", "size", "indeterminate", "card", "id", "standalone"], outputs: ["checkedChange", "disabledChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15513
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTreeNodeItem, isStandalone: true, selector: "ol-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null }, actionsTemplate: { classPropertyName: "actionsTemplate", publicName: "actionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, checkable: { classPropertyName: "checkable", publicName: "checkable", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, toggleIcon: { classPropertyName: "toggleIcon", publicName: "toggleIcon", isSignal: true, isRequired: false, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, tabbableId: { classPropertyName: "tabbableId", publicName: "tabbableId", isSignal: true, isRequired: false, transformFunction: null }, focusedId: { classPropertyName: "focusedId", publicName: "focusedId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeClick: "nodeClick", nodeCheck: "nodeCheck", toggleExpand: "toggleExpand", nodeFocus: "nodeFocus" }, viewQueries: [{ propertyName: "hang", first: true, predicate: ["hang"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"select-none py-0.5\">\n <!-- Focus v\u1EBD VI\u1EC0N 2px b\u00EAn trong, kh\u00F4ng ph\u1EA3i ring: Figma `ol-tree`\n State=Focus \u0111o \u0111\u01B0\u1EE3c `strokeWeight: 2`, `strokeAlign: INSIDE`, m\u00E0u\n `border/border-brand` #2b4182. Ring v\u1EBD NGO\u00C0I h\u1ED9p n\u00EAn kh\u00E1c h\u00ECnh, v\u00E0 \u1EDF\n c\u00E2y l\u1ED3ng nhau n\u00F3 ch\u1ED3ng l\u00EAn node b\u00EAn c\u1EA1nh. -->\n <div\n #hang\n role=\"treeitem\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-level]=\"ariaLevel()\"\n [attr.aria-expanded]=\"ariaExpanded()\"\n [attr.aria-selected]=\"!!node().selected\"\n [attr.aria-disabled]=\"node().disabled ? true : null\"\n (focus)=\"nodeFocus.emit(node().id)\"\n class=\"group flex cursor-pointer items-center gap-2 rounded-lg border-2 border-transparent p-1 text-sm outline-hidden transition-colors focus:outline-hidden focus-visible:border-brand\"\n [class.bg-brand-softer]=\"node().selected\"\n [class.text-fg-brand]=\"node().selected\"\n [class.font-medium]=\"node().selected\"\n [class.text-heading]=\"!node().selected && !node().disabled\"\n [class.hover:bg-brand-softer]=\"!node().selected && !node().disabled\"\n [class.opacity-50]=\"node().disabled\"\n [class.cursor-not-allowed]=\"node().disabled\"\n (click)=\"handleClick()\"\n >\n <!-- Expand Toggle Button -->\n @if (node().children && node().children!.length > 0) {\n <!--\n N\u00FAt ch\u1EC9 c\u00F3 icon n\u00EAn PH\u1EA2I c\u00F3 t\u00EAn: \u0111o `/tree` ra 18 n\u00FAt g\u1EADp kh\u00F4ng t\u00EAn\n trong c\u00E2y tr\u1EE3 n\u0103ng. `aria-expanded` cho bi\u1EBFt tr\u1EA1ng th\u00E1i, `aria-label`\n cho bi\u1EBFt n\u00FAt n\u00E0y g\u1EADp/m\u1EDF NH\u00C1NH N\u00C0O \u2014 nhi\u1EC1u n\u00FAt gi\u1ED1ng h\u1EC7t nhau tr\u00EAn m\u1ED9t\n trang, ch\u1EC9 \"M\u1EDF r\u1ED9ng\" th\u00F4i th\u00EC v\u1EABn kh\u00F4ng ph\u00E2n bi\u1EC7t \u0111\u01B0\u1EE3c.\n -->\n <button\n type=\"button\"\n (click)=\"handleToggle($event)\"\n [attr.aria-expanded]=\"!!node().expanded\"\n [attr.aria-label]=\"(node().expanded ? 'Thu g\u1ECDn ' : 'M\u1EDF r\u1ED9ng ') + (node().label || 'nh\u00E1nh')\"\n class=\"w-5 h-5 flex items-center justify-center rounded text-body-subtle hover:text-heading transition-colors shrink-0\"\n >\n @if (toggleIcon() === 'plus-minus') {\n <span\n class=\"w-4 h-4 rounded border border-default-strong flex items-center justify-center text-xs font-mono font-bold leading-none text-body\"\n >\n {{ node().expanded ? '\u2212' : '+' }}\n </span>\n } @else {\n <ol-icon\n [name]=\"node().expanded ? 'alt-arrow-down' : 'alt-arrow-right'\"\n size=\"xs\"\n ></ol-icon>\n }\n </button>\n } @else {\n @if (!checkable()) {\n <span class=\"w-5 h-5 flex items-center justify-center shrink-0\">\n <span\n class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand transition-colors\"\n ></span>\n </span>\n }\n }\n <!-- Checkbox option (when checkable) -->\n @if (checkable()) {\n <div class=\"flex items-center justify-center shrink-0\" (click)=\"$event.stopPropagation()\">\n <ol-checkbox\n [standalone]=\"true\"\n [ariaLabel]=\"'Ch\u1ECDn ' + (node().label || 'nh\u00E1nh')\"\n [checked]=\"node().selected ?? false\"\n [indeterminate]=\"indeterminate()\"\n [disabled]=\"node().disabled ?? false\"\n (checkedChange)=\"handleCheck($event)\"\n size=\"base\"\n ></ol-checkbox>\n </div>\n }\n <!--\n Template tu\u1EF3 bi\u1EBFn CH\u1EC8 thay ph\u1EA7n n\u1ED9i dung (icon \u00B7 nh\u00E3n \u00B7 count \u00B7 badge).\n N\u00FAt m\u1EDF r\u1ED9ng, checkbox v\u00E0 th\u1EBB `role=\"treeitem\"` b\u1ECDc ngo\u00E0i v\u1EABn do lib gi\u1EEF \u2014\n cho ng\u01B0\u1EDDi d\u00F9ng v\u1EBD c\u1EA3 h\u00E0ng l\u00E0 m\u1EA5t ARIA \u0111\u00E3 l\u00E0m \u0111\u00FAng chu\u1EA9n \u1EDF BUG-017.\n -->\n @if (nodeTemplate()) {\n <ng-container *ngTemplateOutlet=\"nodeTemplate()!; context: tplContext()\" />\n } @else {\n <!-- Node Icon (if provided) -->\n @if (node().icon) {\n <ol-icon\n [name]=\"node().icon!\"\n size=\"xs\"\n [class.text-fg-brand]=\"node().selected\"\n [class.text-body-subtle]=\"!node().selected\"\n ></ol-icon>\n }\n <!-- Node Label -->\n <span class=\"truncate flex-1\">{{ node().label }}</span>\n <!-- Count or Badge -->\n @if (node().count !== undefined) {\n <span class=\"text-2xs text-body-subtle font-mono\"> ({{ node().count }}) </span>\n }\n @if (node().badge) {\n <ol-badge [color]=\"node().badgeColor || 'brand'\" size=\"xs\" shape=\"pill\">\n {{ node().badge }}\n </ol-badge>\n }\n }\n <!--\n H\u00E0nh \u0111\u1ED9ng cu\u1ED1i h\u00E0ng. \u0110\u1EB7t NGO\u00C0I c\u1EB7p @if/@else \u1EDF tr\u00EAn l\u00E0 c\u00F3 ch\u1EE7 \u00FD: n\u00FAt ph\u1EA3i\n hi\u1EC7n c\u1EA3 khi ng\u01B0\u1EDDi d\u00F9ng t\u1EF1 v\u1EBD n\u1ED9i dung b\u1EB1ng `nodeTemplate`. \u0110\u01B0a v\u00E0o trong\n nh\u00E1nh @else th\u00EC b\u1EADt `nodeTemplate` l\u00E0 n\u00FAt bi\u1EBFn m\u1EA5t \u2014 build v\u1EABn xanh.\n -->\n @if (actionsTemplate()) {\n <span class=\"ms-auto flex shrink-0 items-center gap-1\">\n <ng-container *ngTemplateOutlet=\"actionsTemplate()!; context: tplContext()\" />\n </span>\n }\n </div>\n <!-- Children Nodes (Indented 20px guide line + pl-5 child offset) -->\n @if (node().expanded && node().children && node().children!.length > 0) {\n <div role=\"group\" class=\"ml-5 mt-1 space-y-1 border-l border-default pl-5\">\n @for (child of node().children; track child.id) {\n <ol-tree-node\n [node]=\"child\"\n [nodeTemplate]=\"nodeTemplate()\"\n [actionsTemplate]=\"actionsTemplate()\"\n [checkable]=\"checkable()\"\n [selectionMode]=\"selectionMode()\"\n [toggleIcon]=\"toggleIcon()\"\n [level]=\"level() + 1\"\n [tabbableId]=\"tabbableId()\"\n [focusedId]=\"focusedId()\"\n (nodeFocus)=\"nodeFocus.emit($event)\"\n (nodeClick)=\"nodeClick.emit($event)\"\n (nodeCheck)=\"nodeCheck.emit($event)\"\n (toggleExpand)=\"toggleExpand.emit($event)\"\n ></ol-tree-node>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: OlTreeNodeItem, selector: "ol-tree-node", inputs: ["node", "nodeTemplate", "actionsTemplate", "checkable", "selectionMode", "toggleIcon", "level", "tabbableId", "focusedId"], outputs: ["nodeClick", "nodeCheck", "toggleExpand", "nodeFocus"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "component", type: OlCheckbox, selector: "ol-checkbox", inputs: ["checked", "label", "ariaLabel", "description", "disabled", "size", "indeterminate", "card", "id", "standalone"], outputs: ["checkedChange", "disabledChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15098
15514
|
}
|
|
15099
15515
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTreeNodeItem, decorators: [{
|
|
15100
15516
|
type: Component,
|
|
@@ -16076,11 +16492,11 @@ class OlChart {
|
|
|
16076
16492
|
}, /* @ts-ignore */
|
|
16077
16493
|
...(ngDevMode ? [{ debugName: "horizontalBarsComputed" }] : /* istanbul ignore next */ []));
|
|
16078
16494
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
16079
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlChart, isStandalone: true, selector: "ol-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, categories: { classPropertyName: "categories", publicName: "categories", isSignal: true, isRequired: false, transformFunction: null }, series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: false, transformFunction: null }, dataPoints: { classPropertyName: "dataPoints", publicName: "dataPoints", isSignal: true, isRequired: false, transformFunction: null }, indicators: { classPropertyName: "indicators", publicName: "indicators", isSignal: true, isRequired: false, transformFunction: null }, scatterSeries: { classPropertyName: "scatterSeries", publicName: "scatterSeries", isSignal: true, isRequired: false, transformFunction: null }, heatmapData: { classPropertyName: "heatmapData", publicName: "heatmapData", isSignal: true, isRequired: false, transformFunction: null }, boxplotData: { classPropertyName: "boxplotData", publicName: "boxplotData", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, xAxisTitle: { classPropertyName: "xAxisTitle", publicName: "xAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, yAxisTitle: { classPropertyName: "yAxisTitle", publicName: "yAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 13/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n\n 38 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n text-gray-700 / dark:text-gray-300 s\u00E1ng \u0111\u1EADm, t\u1ED1i nh\u1EA1t\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
16495
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlChart, isStandalone: true, selector: "ol-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, categories: { classPropertyName: "categories", publicName: "categories", isSignal: true, isRequired: false, transformFunction: null }, series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: false, transformFunction: null }, dataPoints: { classPropertyName: "dataPoints", publicName: "dataPoints", isSignal: true, isRequired: false, transformFunction: null }, indicators: { classPropertyName: "indicators", publicName: "indicators", isSignal: true, isRequired: false, transformFunction: null }, scatterSeries: { classPropertyName: "scatterSeries", publicName: "scatterSeries", isSignal: true, isRequired: false, transformFunction: null }, heatmapData: { classPropertyName: "heatmapData", publicName: "heatmapData", isSignal: true, isRequired: false, transformFunction: null }, boxplotData: { classPropertyName: "boxplotData", publicName: "boxplotData", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, xAxisTitle: { classPropertyName: "xAxisTitle", publicName: "xAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, yAxisTitle: { classPropertyName: "yAxisTitle", publicName: "yAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 16/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n text-gray-700 dark:text-gray-300 -> text-body (3 l\u01B0\u1EE3t, 2026-09-24)\n\n Ba l\u01B0\u1EE3t cu\u1ED1i: ch\u00FA th\u00EDch c\u0169 x\u1EBFp ch\u00FAng v\u00E0o nh\u00F3m \"kh\u00F4ng token n\u00E0o kh\u1EDBp\", \u0111o l\u1EA1i\n th\u00EC `--text-body` \u0111\u1ED5i \u0110\u00DANG chi\u1EC1u #4a5565 -> #d1d5db, v\u00E0 gi\u00E1 tr\u1ECB t\u1ED1i TR\u00D9NG\n KH\u00CDT gray-300 c\u0169. S\u00E1ng nh\u1EA1t h\u01A1n m\u1ED9t b\u1EADc (#364153 -> #4a5565), t\u01B0\u01A1ng ph\u1EA3n\n tr\u00EAn n\u1EC1n tr\u1EAFng v\u1EABn 7,56 \u2014 d\u01B0 chu\u1EA9n AA 4,5.\n\n 35 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
16080
16496
|
}
|
|
16081
16497
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlChart, decorators: [{
|
|
16082
16498
|
type: Component,
|
|
16083
|
-
args: [{ selector: 'ol-chart', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 13/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n\n 38 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n text-gray-700 / dark:text-gray-300 s\u00E1ng \u0111\u1EADm, t\u1ED1i nh\u1EA1t\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"] }]
|
|
16499
|
+
args: [{ selector: 'ol-chart', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 16/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n text-gray-700 dark:text-gray-300 -> text-body (3 l\u01B0\u1EE3t, 2026-09-24)\n\n Ba l\u01B0\u1EE3t cu\u1ED1i: ch\u00FA th\u00EDch c\u0169 x\u1EBFp ch\u00FAng v\u00E0o nh\u00F3m \"kh\u00F4ng token n\u00E0o kh\u1EDBp\", \u0111o l\u1EA1i\n th\u00EC `--text-body` \u0111\u1ED5i \u0110\u00DANG chi\u1EC1u #4a5565 -> #d1d5db, v\u00E0 gi\u00E1 tr\u1ECB t\u1ED1i TR\u00D9NG\n KH\u00CDT gray-300 c\u0169. S\u00E1ng nh\u1EA1t h\u01A1n m\u1ED9t b\u1EADc (#364153 -> #4a5565), t\u01B0\u01A1ng ph\u1EA3n\n tr\u00EAn n\u1EC1n tr\u1EAFng v\u1EABn 7,56 \u2014 d\u01B0 chu\u1EA9n AA 4,5.\n\n 35 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"] }]
|
|
16084
16500
|
}], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], categories: [{ type: i0.Input, args: [{ isSignal: true, alias: "categories", required: false }] }], series: [{ type: i0.Input, args: [{ isSignal: true, alias: "series", required: false }] }], dataPoints: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataPoints", required: false }] }], indicators: [{ type: i0.Input, args: [{ isSignal: true, alias: "indicators", required: false }] }], scatterSeries: [{ type: i0.Input, args: [{ isSignal: true, alias: "scatterSeries", required: false }] }], heatmapData: [{ type: i0.Input, args: [{ isSignal: true, alias: "heatmapData", required: false }] }], boxplotData: [{ type: i0.Input, args: [{ isSignal: true, alias: "boxplotData", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], showGrid: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGrid", required: false }] }], xAxisTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "xAxisTitle", required: false }] }], yAxisTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "yAxisTitle", required: false }] }], unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: false }] }] } });
|
|
16085
16501
|
|
|
16086
16502
|
/** Nhãn hiển thị + icon cho từng trạng thái. */
|
|
@@ -16235,11 +16651,11 @@ class OlJumbotron {
|
|
|
16235
16651
|
}, /* @ts-ignore */
|
|
16236
16652
|
...(ngDevMode ? [{ debugName: "badgeClasses" }] : /* istanbul ignore next */ []));
|
|
16237
16653
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlJumbotron, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
16238
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlJumbotron, isStandalone: true, selector: "ol-jumbotron", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, badgeText: { classPropertyName: "badgeText", publicName: "badgeText", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\
|
|
16654
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlJumbotron, isStandalone: true, selector: "ol-jumbotron", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, badgeText: { classPropertyName: "badgeText", publicName: "badgeText", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\u0169, \u0110\u00C3 G\u1EE0 H\u1EBET (2026-09-24). Ch\u00FA th\u00EDch tr\u01B0\u1EDBc \u0111\u00E2y ghi\n \"kh\u00F4ng token n\u00E0o kh\u1EDBp\" l\u00E0 SAI \u2014 ph\u00E9p \u0111o c\u0169 \u0111\u1ECDc hex t\u1EEB file CSS v\u00E0 l\u1EA5y nh\u1EA7m\n l\u1EA7n khai trong kh\u1ED1i `.dark`, n\u00EAn token n\u00E0o c\u0169ng ra gi\u00E1 tr\u1ECB ch\u1EBF \u0111\u1ED9 t\u1ED1i.\n\n \u0110o l\u1EA1i tr\u00EAn Chrome, \u0111\u1ECDc `getComputedStyle` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 (\u0111\u1ED1i ch\u1EE9ng d\u01B0\u01A1ng:\n `--text-heading` #101828 -> #ffffff), ba token n\u00E0y T\u1EF0 \u0110\u1ED4I theo `.dark`:\n\n --bg-secondary #f9fafb -> #030712\n --text-body #4a5565 -> #d1d5db (tr\u00F9ng kh\u00EDt gray-300 c\u0169)\n --bg-primary-soft #ffffff -> #101828 (tr\u00F9ng kh\u00EDt gray-900 c\u0169)\n\n N\u00EAn `dark:bg-gray-900` v\u00E0 `dark:text-gray-300` ch\u1EC9 \u0110\u00C8 L\u00CAN token \u0111\u00E3 \u0111\u00FAng:\n nh\u00ECn kh\u00F4ng kh\u00E1c g\u00EC, nh\u01B0ng token \u0111\u1ED5i l\u00E0 h\u1ECFng ngay m\u00E0 kh\u00F4ng ai th\u1EA5y.\n-->\n<!-- Figma trang Jumbotron ch\u1EC9 \u0111\u1ECBnh ngh\u0129a bi\u1EBFn th\u1EC3 n\u1EC1n brand; c\u00E1c bi\u1EBFn th\u1EC3\n `gradient`, `dark`, `light` b\u00EAn d\u01B0\u1EDBi l\u00E0 m\u1EDF r\u1ED9ng NGO\u00C0I Figma n\u00EAn gi\u1EEF\n b\u1EADc s\u1ED1 cho t\u1EDBi khi thi\u1EBFt k\u1EBF \u0111\u1ECBnh ngh\u0129a. -->\n<div\n class=\"relative overflow-hidden rounded-3xl p-6 sm:p-8 md:p-12 border shadow-xl\"\n [class.bg-linear-to-tr]=\"variant() === 'gradient'\"\n [class.from-brand-950]=\"variant() === 'gradient'\"\n [class.via-brand-800]=\"variant() === 'gradient'\"\n [class.to-indigo-900]=\"variant() === 'gradient'\"\n [class.text-white]=\"variant() === 'gradient' || variant() === 'brand' || variant() === 'dark'\"\n [class.border-brand/50]=\"variant() === 'gradient'\"\n [class.bg-brand]=\"variant() === 'brand'\"\n [class.border-brand-subtle]=\"variant() === 'brand'\"\n [class.bg-gray-900]=\"variant() === 'dark'\"\n [class.border-gray-800]=\"variant() === 'dark'\"\n [class.bg-neutral-secondary]=\"variant() === 'light'\"\n [class.border-default]=\"variant() === 'light'\"\n [class.dark:border-default]=\"variant() === 'light'\"\n [class.text-heading]=\"variant() === 'light'\"\n [class.dark:text-white]=\"variant() === 'light'\"\n>\n <div class=\"relative z-10 max-w-3xl space-y-4\">\n <!-- Optional Badge Slot or text -->\n @if (badgeText()) {\n <div [class]=\"badgeClasses()\">\n <ng-content select=\"[slot=badge-icon]\"></ng-content>\n <span>{{ badgeText() }}</span>\n </div>\n }\n\n <!-- Title -->\n @if (title()) {\n @switch (headingLevel()) {\n @case (2) {\n <h2 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h2>\n }\n @case (3) {\n <h3 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h3>\n }\n @case (4) {\n <h4 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h4>\n }\n @case (5) {\n <h5 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h5>\n }\n @case (6) {\n <h6 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h6>\n }\n @default {\n <h1 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h1>\n }\n }\n }\n\n <!--\n Subtitle / Description\n\n Bi\u1EBFn th\u1EC3 n\u1EC1n BRAND d\u00F9ng `text-fg-white` + `opacity-90`, KH\u00D4NG\n `text-fg-brand-soft`. T\u00E1ch hai class ch\u1EE9 kh\u00F4ng vi\u1EBFt `text-fg-white/90`:\n c\u00FA ph\u00E1p opacity k\u00E9p kh\u00F4ng sinh CSS \u1EDF d\u1EF1 \u00E1n n\u00E0y, class v\u1EABn n\u1EB1m trong\n `classList` m\u00E0 ph\u1EA7n t\u1EED m\u1EA5t m\u00E0u.\n\n `--text-brand-soft` l\u1EADt theo n\u1EC1n TRANG (brand-100 \u1EDF s\u00E1ng, brand-900 \u1EDF\n t\u1ED1i), nh\u01B0ng jumbotron t\u1EF1 t\u00F4 gradient brand \u0111\u1EADm \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 l\u1EADt\n xong th\u00E0nh xanh \u0111\u1EADm tr\u00EAn xanh \u0111\u1EADm. \u0110o /jumbotron: s\u00E1ng 1,20 \u00B7 t\u1ED1i 1,29,\n \u1EA3nh ch\u1EE5p cho th\u1EA5y d\u00F2ng m\u00F4 t\u1EA3 ch\u00ECm h\u1EB3n (BUG-035).\n\n N\u1EC1n gradient n\u00EAn m\u1ECDi ph\u00E9p \u0111o t\u1EF1 \u0111\u1ED9ng \u0111\u1EC1u sai \u2014 ph\u1EA3i x\u00E1c nh\u1EADn b\u1EB1ng \u1EA3nh.\n\n Bi\u1EBFn th\u1EC3 `light` gi\u1EEF nguy\u00EAn: n\u00F3 \u0111i theo n\u1EC1n trang n\u00EAn `dark:` l\u00E0 \u0111\u00FAng.\n -->\n @if (subtitle()) {\n <p\n class=\"text-sm md:text-base max-w-2xl\"\n [class.text-fg-white]=\"variant() !== 'light'\"\n [class.opacity-90]=\"variant() !== 'light'\"\n [class.text-body]=\"variant() === 'light'\"\n >\n {{ subtitle() }}\n </p>\n }\n\n <!-- Content / CTA Slot -->\n <div class=\"pt-2 flex flex-wrap items-center gap-3\">\n <ng-content select=\"[slot=actions]\"></ng-content>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
16239
16655
|
}
|
|
16240
16656
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlJumbotron, decorators: [{
|
|
16241
16657
|
type: Component,
|
|
16242
|
-
args: [{ selector: 'ol-jumbotron', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\
|
|
16658
|
+
args: [{ selector: 'ol-jumbotron', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\u0169, \u0110\u00C3 G\u1EE0 H\u1EBET (2026-09-24). Ch\u00FA th\u00EDch tr\u01B0\u1EDBc \u0111\u00E2y ghi\n \"kh\u00F4ng token n\u00E0o kh\u1EDBp\" l\u00E0 SAI \u2014 ph\u00E9p \u0111o c\u0169 \u0111\u1ECDc hex t\u1EEB file CSS v\u00E0 l\u1EA5y nh\u1EA7m\n l\u1EA7n khai trong kh\u1ED1i `.dark`, n\u00EAn token n\u00E0o c\u0169ng ra gi\u00E1 tr\u1ECB ch\u1EBF \u0111\u1ED9 t\u1ED1i.\n\n \u0110o l\u1EA1i tr\u00EAn Chrome, \u0111\u1ECDc `getComputedStyle` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 (\u0111\u1ED1i ch\u1EE9ng d\u01B0\u01A1ng:\n `--text-heading` #101828 -> #ffffff), ba token n\u00E0y T\u1EF0 \u0110\u1ED4I theo `.dark`:\n\n --bg-secondary #f9fafb -> #030712\n --text-body #4a5565 -> #d1d5db (tr\u00F9ng kh\u00EDt gray-300 c\u0169)\n --bg-primary-soft #ffffff -> #101828 (tr\u00F9ng kh\u00EDt gray-900 c\u0169)\n\n N\u00EAn `dark:bg-gray-900` v\u00E0 `dark:text-gray-300` ch\u1EC9 \u0110\u00C8 L\u00CAN token \u0111\u00E3 \u0111\u00FAng:\n nh\u00ECn kh\u00F4ng kh\u00E1c g\u00EC, nh\u01B0ng token \u0111\u1ED5i l\u00E0 h\u1ECFng ngay m\u00E0 kh\u00F4ng ai th\u1EA5y.\n-->\n<!-- Figma trang Jumbotron ch\u1EC9 \u0111\u1ECBnh ngh\u0129a bi\u1EBFn th\u1EC3 n\u1EC1n brand; c\u00E1c bi\u1EBFn th\u1EC3\n `gradient`, `dark`, `light` b\u00EAn d\u01B0\u1EDBi l\u00E0 m\u1EDF r\u1ED9ng NGO\u00C0I Figma n\u00EAn gi\u1EEF\n b\u1EADc s\u1ED1 cho t\u1EDBi khi thi\u1EBFt k\u1EBF \u0111\u1ECBnh ngh\u0129a. -->\n<div\n class=\"relative overflow-hidden rounded-3xl p-6 sm:p-8 md:p-12 border shadow-xl\"\n [class.bg-linear-to-tr]=\"variant() === 'gradient'\"\n [class.from-brand-950]=\"variant() === 'gradient'\"\n [class.via-brand-800]=\"variant() === 'gradient'\"\n [class.to-indigo-900]=\"variant() === 'gradient'\"\n [class.text-white]=\"variant() === 'gradient' || variant() === 'brand' || variant() === 'dark'\"\n [class.border-brand/50]=\"variant() === 'gradient'\"\n [class.bg-brand]=\"variant() === 'brand'\"\n [class.border-brand-subtle]=\"variant() === 'brand'\"\n [class.bg-gray-900]=\"variant() === 'dark'\"\n [class.border-gray-800]=\"variant() === 'dark'\"\n [class.bg-neutral-secondary]=\"variant() === 'light'\"\n [class.border-default]=\"variant() === 'light'\"\n [class.dark:border-default]=\"variant() === 'light'\"\n [class.text-heading]=\"variant() === 'light'\"\n [class.dark:text-white]=\"variant() === 'light'\"\n>\n <div class=\"relative z-10 max-w-3xl space-y-4\">\n <!-- Optional Badge Slot or text -->\n @if (badgeText()) {\n <div [class]=\"badgeClasses()\">\n <ng-content select=\"[slot=badge-icon]\"></ng-content>\n <span>{{ badgeText() }}</span>\n </div>\n }\n\n <!-- Title -->\n @if (title()) {\n @switch (headingLevel()) {\n @case (2) {\n <h2 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h2>\n }\n @case (3) {\n <h3 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h3>\n }\n @case (4) {\n <h4 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h4>\n }\n @case (5) {\n <h5 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h5>\n }\n @case (6) {\n <h6 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h6>\n }\n @default {\n <h1 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h1>\n }\n }\n }\n\n <!--\n Subtitle / Description\n\n Bi\u1EBFn th\u1EC3 n\u1EC1n BRAND d\u00F9ng `text-fg-white` + `opacity-90`, KH\u00D4NG\n `text-fg-brand-soft`. T\u00E1ch hai class ch\u1EE9 kh\u00F4ng vi\u1EBFt `text-fg-white/90`:\n c\u00FA ph\u00E1p opacity k\u00E9p kh\u00F4ng sinh CSS \u1EDF d\u1EF1 \u00E1n n\u00E0y, class v\u1EABn n\u1EB1m trong\n `classList` m\u00E0 ph\u1EA7n t\u1EED m\u1EA5t m\u00E0u.\n\n `--text-brand-soft` l\u1EADt theo n\u1EC1n TRANG (brand-100 \u1EDF s\u00E1ng, brand-900 \u1EDF\n t\u1ED1i), nh\u01B0ng jumbotron t\u1EF1 t\u00F4 gradient brand \u0111\u1EADm \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 l\u1EADt\n xong th\u00E0nh xanh \u0111\u1EADm tr\u00EAn xanh \u0111\u1EADm. \u0110o /jumbotron: s\u00E1ng 1,20 \u00B7 t\u1ED1i 1,29,\n \u1EA3nh ch\u1EE5p cho th\u1EA5y d\u00F2ng m\u00F4 t\u1EA3 ch\u00ECm h\u1EB3n (BUG-035).\n\n N\u1EC1n gradient n\u00EAn m\u1ECDi ph\u00E9p \u0111o t\u1EF1 \u0111\u1ED9ng \u0111\u1EC1u sai \u2014 ph\u1EA3i x\u00E1c nh\u1EADn b\u1EB1ng \u1EA3nh.\n\n Bi\u1EBFn th\u1EC3 `light` gi\u1EEF nguy\u00EAn: n\u00F3 \u0111i theo n\u1EC1n trang n\u00EAn `dark:` l\u00E0 \u0111\u00FAng.\n -->\n @if (subtitle()) {\n <p\n class=\"text-sm md:text-base max-w-2xl\"\n [class.text-fg-white]=\"variant() !== 'light'\"\n [class.opacity-90]=\"variant() !== 'light'\"\n [class.text-body]=\"variant() === 'light'\"\n >\n {{ subtitle() }}\n </p>\n }\n\n <!-- Content / CTA Slot -->\n <div class=\"pt-2 flex flex-wrap items-center gap-3\">\n <ng-content select=\"[slot=actions]\"></ng-content>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n" }]
|
|
16243
16659
|
}], propDecorators: { headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], badgeText: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeText", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
|
|
16244
16660
|
|
|
16245
16661
|
/**
|
|
@@ -16331,5 +16747,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
16331
16747
|
* Generated bundle index. Do not edit.
|
|
16332
16748
|
*/
|
|
16333
16749
|
|
|
16334
|
-
export { CHAT_BUBBLE_STATUS, MONTHS_FULL_EN, MONTHS_FULL_VI, MONTHS_SHORT_VI, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppNav, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, OlEmpty, OlFileInput, OlFloatingLabel, OlFocusTrapService, OlFooter, OlGallery, OlGalleryItem, OlHelper, OlIcon, OlIconService, OlIconShape, OlInput, OlInputCounter, OlJumbotron, OlKbd, OlLabel, OlListGroup, OlListGroupItem, OlLoadingOverlay, OlModal, OlModalFooter, OlModalHeader, OlMonthView, OlNavDropdown, OlNavbar, OlNavbarItem, OlOtpInput, OlOverlayContainer, OlPagination, OlPopover, OlPopoverContent, OlPositioningService, OlProgress, OlProgressBarService, OlProgressBarTop, OlRadio, OlRange, OlRating, OlRichTextEditor, OlScrollArea, OlSelect, OlSeparator, OlSidebar, OlSidebarGroup, OlSidebarItem, OlSidebarSubmenu, OlSkeleton, OlSpeedDial, OlSpeedDialItem, OlSpinner, OlStepItem, OlStepper, OlTabItem, OlTable, OlTableLoading, OlTableRow, OlTabs, OlTagInput, OlTextarea, OlTimeline, OlTimelineItem, OlTimepicker, OlToast, OlToastContainer, OlToastService, OlToggle, OlToggleGroup, OlToggleInput, OlTooltip, OlTooltipDirective, OlTree, OlTreeNodeItem, OlYearView, WEEKDAYS_SHORT_EN, WEEKDAYS_SHORT_VI, boDau, cloneDate, endOfDay, formatDate, generateMonthMatrix, generateMonthsMatrix, generateYearsMatrix, isBetween, isDateDisabled, isSameDay, isSameMonth, isSameYear, isToday, parseDate, startOfDay, stripDiacritics };
|
|
16750
|
+
export { CHAT_BUBBLE_STATUS, FIELD_ASTERISK, FIELD_DISABLED, FIELD_HEIGHT, FIELD_HELPER, FIELD_HELPER_GAP, FIELD_LABEL, FIELD_LABEL_GAP, FIELD_MIN_HEIGHT, FIELD_PADDING_X, FIELD_RADIUS, FIELD_STATE, FIELD_TEXT, MONTHS_FULL_EN, MONTHS_FULL_VI, MONTHS_SHORT_VI, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppNav, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, OlEmpty, OlFileInput, OlFloatingLabel, OlFocusTrapService, OlFooter, OlGallery, OlGalleryItem, OlHelper, OlIcon, OlIconService, OlIconShape, OlInput, OlInputCounter, OlJumbotron, OlKbd, OlLabel, OlListGroup, OlListGroupItem, OlLoadingOverlay, OlModal, OlModalFooter, OlModalHeader, OlMonthView, OlNavDropdown, OlNavbar, OlNavbarItem, OlOtpInput, OlOverlayContainer, OlPagination, OlPopover, OlPopoverContent, OlPositioningService, OlProgress, OlProgressBarService, OlProgressBarTop, OlRadio, OlRange, OlRating, OlRichTextEditor, OlScrollArea, OlSelect, OlSeparator, OlSidebar, OlSidebarGroup, OlSidebarItem, OlSidebarSubmenu, OlSkeleton, OlSpeedDial, OlSpeedDialItem, OlSpinner, OlStepItem, OlStepper, OlTabItem, OlTable, OlTableLoading, OlTableRow, OlTabs, OlTagInput, OlTextarea, OlTimeline, OlTimelineItem, OlTimepicker, OlToast, OlToastContainer, OlToastService, OlToggle, OlToggleGroup, OlToggleInput, OlTooltip, OlTooltipDirective, OlTree, OlTreeNodeItem, OlYearView, WEEKDAYS_SHORT_EN, WEEKDAYS_SHORT_VI, boDau, cloneDate, endOfDay, fieldSizeClasses, formatDate, generateMonthMatrix, generateMonthsMatrix, generateYearsMatrix, isBetween, isDateDisabled, isSameDay, isSameMonth, isSameYear, isToday, parseDate, startOfDay, stripDiacritics };
|
|
16335
16751
|
//# sourceMappingURL=edmicro-olkit.mjs.map
|