@edmicro/olkit 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -28
- package/fesm2022/edmicro-olkit.mjs +736 -340
- package/fesm2022/edmicro-olkit.mjs.map +1 -1
- package/olkit.css +1 -1
- package/package.json +1 -1
- package/types/edmicro-olkit.d.ts +382 -48
- 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)
|
|
1574
1578
|
return;
|
|
1575
|
-
this.
|
|
1576
|
-
|
|
1579
|
+
this.clickListener = (event) => {
|
|
1580
|
+
// Duyệt từ trên xuống, dừng ở lớp đầu tiên có khai `contains`.
|
|
1581
|
+
for (let i = this.openLayers.length - 1; i >= 0; i--) {
|
|
1582
|
+
const layer = this.openLayers[i];
|
|
1583
|
+
if (!layer.contains)
|
|
1584
|
+
continue;
|
|
1585
|
+
// Bấm TRONG lớp trên cùng: không đóng gì cả.
|
|
1586
|
+
if (layer.contains(event.target))
|
|
1587
|
+
return;
|
|
1588
|
+
/**
|
|
1589
|
+
* Bấm ngoài: đóng đúng lớp này, và chặn lớp dưới nhận cùng cú bấm.
|
|
1590
|
+
*
|
|
1591
|
+
* Điều kiện chặn là "CÒN LỚP NÀO bên dưới", không phải "còn lớp nào
|
|
1592
|
+
* khai `contains`". Modal đóng qua `onBackdropClick` — sự kiện click
|
|
1593
|
+
* trên chính lớp nền, KHÔNG đi qua ngăn xếp này — nên nó không khai
|
|
1594
|
+
* `contains`. Lọc theo `contains` thì `hasLayerBelow` ra `false` và
|
|
1595
|
+
* modal vẫn đóng cùng lượt: đo /modal thấy bấm một cú là mất cả hai.
|
|
1596
|
+
*
|
|
1597
|
+
* Chặn vô điều kiện cũng sai: khi CHỈ có một lớp mở, nút mà người dùng
|
|
1598
|
+
* vừa bấm không nhận được sự kiện. Đo bằng bộ test thư viện: 1-2 ca đỏ
|
|
1599
|
+
* mỗi lượt, 4/4 lượt, trong khi mã gốc 3/3 lượt xanh.
|
|
1600
|
+
*
|
|
1601
|
+
* Nên: có lớp dưới thì chặn, không có thì để sự kiện đi tiếp.
|
|
1602
|
+
*/
|
|
1603
|
+
if (i > 0)
|
|
1604
|
+
event.stopPropagation();
|
|
1605
|
+
layer.close();
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
// `capture: true` để chạy TRƯỚC `@HostListener` của component, nhờ đó
|
|
1610
|
+
// `stopPropagation` mới chặn được chúng.
|
|
1611
|
+
this.doc.addEventListener('click', this.clickListener, true);
|
|
1612
|
+
}
|
|
1613
|
+
/** Gỡ hai trình nghe khi lớp cuối cùng đóng — không để chúng nghe vô ích. */
|
|
1614
|
+
teardownListenersIfIdle() {
|
|
1615
|
+
if (this.openLayers.length > 0)
|
|
1616
|
+
return;
|
|
1617
|
+
if (this.escapeListener) {
|
|
1618
|
+
this.doc.removeEventListener('keydown', this.escapeListener, true);
|
|
1619
|
+
this.escapeListener = null;
|
|
1620
|
+
}
|
|
1621
|
+
if (this.clickListener) {
|
|
1622
|
+
this.doc.removeEventListener('click', this.clickListener, true);
|
|
1623
|
+
this.clickListener = null;
|
|
1624
|
+
}
|
|
1577
1625
|
}
|
|
1578
1626
|
/**
|
|
1579
1627
|
* @deprecated Dùng `attach()`. Giữ lại vì đã publish trong
|
|
@@ -2753,6 +2801,200 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
2753
2801
|
}, template: "<!--\n `olTooltip` l\u00E0 `input.required<string>()` nh\u01B0ng directive t\u1EF1 b\u1ECF qua chu\u1ED7i\n R\u1ED6NG (xem `tooltipDisabled` v\u00E0 nh\u00E1nh `if (!content)` trong directive), n\u00EAn\n truy\u1EC1n `''` cho n\u00FAt c\u00F3 ch\u1EEF l\u00E0 an to\u00E0n \u2014 kh\u00F4ng c\u1EA7n `@if` b\u1ECDc ngo\u00E0i, v\u1ED1n s\u1EBD\n nh\u00E2n \u0111\u00F4i c\u1EA3 kh\u1ED1i template.\n-->\n<button\n [olTooltip]=\"tooltipText()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-expanded]=\"ariaExpanded() === null ? null : ariaExpanded()\"\n [attr.aria-controls]=\"ariaControls() || null\"\n [attr.aria-pressed]=\"ariaPressed() === null ? null : ariaPressed()\"\n [type]=\"type()\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"handleClick($event)\"\n [class]=\"buttonClasses()\"\n>\n @if (loading()) {\n <svg\n class=\"animate-spin shrink-0\"\n [style.width.px]=\"iconPx()\"\n [style.height.px]=\"iconPx()\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <ng-content select=\"[leftIcon]\" />\n <ng-content select=\"[logo]\" />\n <!--\n `iconOnly` \u1EA9n ch\u1EEF b\u1EB1ng `sr-only`, KH\u00D4NG b\u1EB1ng `@if`: Angular chi\u1EBFu n\u1ED9i dung\n \u0111\u00FAng m\u1ED9t l\u1EA7n, \u0111\u1EB7t `<ng-content />` trong c\u00E1c nh\u00E1nh lo\u1EA1i tr\u1EEB nhau l\u00E0 m\u1EA5t ru\u1ED9t\n \u1EDF m\u1ECDi nh\u00E1nh. Gi\u1EEF ch\u1EEF trong DOM c\u00F2n \u0111\u00FAng v\u1EC1 tr\u1EE3 n\u0103ng \u2014 n\u00FAt ch\u1EC9-icon ph\u1EA3i c\u00F3\n t\u00EAn \u0111\u1ECDc \u0111\u01B0\u1EE3c.\n -->\n <span data-noi-dung [class.sr-only]=\"iconOnly()\">\n <ng-content />\n </span>\n <ng-content select=\"[rightIcon]\" />\n</button>\n", styles: [".ol-btn-ico-14 ::ng-deep ol-icon,.ol-btn-ico-14 ::ng-deep ol-icon>span{width:14px!important;height:14px!important}.ol-btn-ico-16 ::ng-deep ol-icon,.ol-btn-ico-16 ::ng-deep ol-icon>span{width:16px!important;height:16px!important}.ol-btn-ico-20 ::ng-deep ol-icon,.ol-btn-ico-20 ::ng-deep ol-icon>span{width:20px!important;height:20px!important}[data-noi-dung].sr-only:has(ol-icon,svg,img){position:static;width:auto;height:auto;margin:0;padding:0;overflow:visible;clip:auto;clip-path:none;white-space:normal;font-size:0;line-height:0}.ol-btn-loading ::ng-deep ol-icon,.ol-btn-loading ::ng-deep>svg:not(.animate-spin),.ol-btn-loading ::ng-deep>img{display:none!important}.ol-btn-ico-14 ::ng-deep>svg:not(.animate-spin),.ol-btn-ico-14 ::ng-deep>img{width:14px;height:14px}.ol-btn-ico-16 ::ng-deep>svg:not(.animate-spin),.ol-btn-ico-16 ::ng-deep>img{width:16px;height:16px}.ol-btn-ico-20 ::ng-deep>svg:not(.animate-spin),.ol-btn-ico-20 ::ng-deep>img{width:20px;height:20px}\n"] }]
|
|
2754
2802
|
}], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], pill: [{ type: i0.Input, args: [{ isSignal: true, alias: "pill", required: false }] }], outline: [{ type: i0.Input, args: [{ isSignal: true, alias: "outline", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaExpanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaExpanded", required: false }] }], ariaControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaControls", required: false }] }], ariaPressed: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaPressed", required: false }] }], btnClick: [{ type: i0.Output, args: ["btnClick"] }] } });
|
|
2755
2803
|
|
|
2804
|
+
/**
|
|
2805
|
+
* Tiêu đề Modal — khớp Figma `ol-modal-header` (node 1149:71403).
|
|
2806
|
+
* Figma có 7 biến thể: Default, Heading & supporting text, Icon + Heading &
|
|
2807
|
+
* helper text, With Logo, Logo & icons, Avatar group, Breadcrumb.
|
|
2808
|
+
* Component lo phần khung dùng chung; nội dung giàu hơn lắp qua <ng-content>.
|
|
2809
|
+
*/
|
|
2810
|
+
/*
|
|
2811
|
+
* GHÉP VỚI `ol-modal`: lắp thẳng vào, KHÔNG phải tắt gì cả.
|
|
2812
|
+
*
|
|
2813
|
+
* `ol-modal` dùng `contentChild(OlModalHeader)` để tự nhận ra có header lồng
|
|
2814
|
+
* vào rồi thôi không dựng phần đầu mặc định (xem `showDefaultHeader`).
|
|
2815
|
+
*
|
|
2816
|
+
* ```html
|
|
2817
|
+
* <ol-modal [(isOpen)]="open">
|
|
2818
|
+
* <ol-modal-header heading="Tiêu đề" (closeClick)="open.set(false)" />
|
|
2819
|
+
* …nội dung…
|
|
2820
|
+
* </ol-modal>
|
|
2821
|
+
* ```
|
|
2822
|
+
*
|
|
2823
|
+
* Trước đây phải tự tắt bằng `title=""` kèm `[dismissible]="false"`, quên một
|
|
2824
|
+
* trong hai là lồng HAI khung header chồng nhau và nút đóng hiện hai lần —
|
|
2825
|
+
* build vẫn xanh nên chỉ nhìn mới thấy.
|
|
2826
|
+
*
|
|
2827
|
+
* Nút đóng: header này có `closeClick` riêng, người dùng tự nối. `ol-modal`
|
|
2828
|
+
* không chen nút vào header lồng ngoài.
|
|
2829
|
+
*/
|
|
2830
|
+
class OlModalHeader {
|
|
2831
|
+
/**
|
|
2832
|
+
* Bậc thẻ tiêu đề (h1–h6). Mặc định 3 — giữ nguyên bậc đang chạy.
|
|
2833
|
+
*
|
|
2834
|
+
* Khoá cứng một bậc thì nơi gọi không sửa được: đặt component này sau
|
|
2835
|
+
* `<h2>` của trang là nhảy bậc.
|
|
2836
|
+
*/
|
|
2837
|
+
headingLevel = input(3, /* @ts-ignore */
|
|
2838
|
+
...(ngDevMode ? [{ debugName: "headingLevel" }] : /* istanbul ignore next */ []));
|
|
2839
|
+
heading = input('', /* @ts-ignore */
|
|
2840
|
+
...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
2841
|
+
/**
|
|
2842
|
+
* Icon đầu tiêu đề. Rỗng thì KHÔNG dựng khung — không để lại thẻ trống.
|
|
2843
|
+
*
|
|
2844
|
+
* Figma `Type=Icon + Heading & helper text` (#1149:71412) vẽ khung 48px bo
|
|
2845
|
+
* 12px, icon 28px bên trong. Đó đúng là `ol-icon-shape` size `lg`, nên dùng
|
|
2846
|
+
* lại thay vì tự vẽ khung.
|
|
2847
|
+
*
|
|
2848
|
+
* `color="white"` chứ KHÔNG `gray`: Figma bind nền
|
|
2849
|
+
* `bg-neutral-primary-medium` (trắng) kèm viền `border-default-medium`, mà
|
|
2850
|
+
* trong `ol-icon-shape` chỉ nhánh `white` có cặp đó — `gray` ra nền xám đặc
|
|
2851
|
+
* và KHÔNG viền. Đo /modal lượt đầu: nền rgb(243,244,246), viền 0px.
|
|
2852
|
+
*/
|
|
2853
|
+
icon = input('', /* @ts-ignore */
|
|
2854
|
+
...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
2855
|
+
/**
|
|
2856
|
+
* Mô tả phụ dưới tiêu đề.
|
|
2857
|
+
*
|
|
2858
|
+
* Tên `description` theo `ol-alert-dialog` — thứ dựng trên chính `ol-modal`
|
|
2859
|
+
* — và `ol-empty`. `helperText` mang nghĩa "chú thích dưới ô nhập" ở họ form
|
|
2860
|
+
* (16 component dùng), đặt cho tiêu đề hộp thoại là lệch nghĩa.
|
|
2861
|
+
*/
|
|
2862
|
+
description = input('', /* @ts-ignore */
|
|
2863
|
+
...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
|
|
2864
|
+
/** @deprecated Dùng `description`. Giữ để không phá mã đang chạy. */
|
|
2865
|
+
supportingText = input('', /* @ts-ignore */
|
|
2866
|
+
...(ngDevMode ? [{ debugName: "supportingText" }] : /* istanbul ignore next */ []));
|
|
2867
|
+
/** @deprecated Dùng `description`. Tên cũ của `ol-drawer-header`. */
|
|
2868
|
+
helperText = input('', /* @ts-ignore */
|
|
2869
|
+
...(ngDevMode ? [{ debugName: "helperText" }] : /* istanbul ignore next */ []));
|
|
2870
|
+
/**
|
|
2871
|
+
* Cái nào được TRUYỀN thì thắng — không phụ thuộc giá trị.
|
|
2872
|
+
*
|
|
2873
|
+
* `ol-badge` từng hoà giải hai input cùng nghĩa bằng cách so với giá trị mặc
|
|
2874
|
+
* định (`theme` thắng trừ khi nó bằng `'gray'`), nên truyền cả hai thì một
|
|
2875
|
+
* cái bị nuốt im lặng. Ở đây chuỗi rỗng là "không truyền", nên chỉ cần lấy
|
|
2876
|
+
* cái đầu tiên khác rỗng.
|
|
2877
|
+
*/
|
|
2878
|
+
descriptionText = computed(() => this.description() || this.supportingText() || this.helperText(), /* @ts-ignore */
|
|
2879
|
+
...(ngDevMode ? [{ debugName: "descriptionText" }] : /* istanbul ignore next */ []));
|
|
2880
|
+
showClose = input(true, /* @ts-ignore */
|
|
2881
|
+
...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
|
|
2882
|
+
closeClick = output();
|
|
2883
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2884
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModalHeader, isStandalone: true, selector: "ol-modal-header", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, supportingText: { classPropertyName: "supportingText", publicName: "supportingText", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, showClose: { classPropertyName: "showClose", publicName: "showClose", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeClick: "closeClick" }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
2885
|
+
<!--
|
|
2886
|
+
PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
|
|
2887
|
+
kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
|
|
2888
|
+
và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
|
|
2889
|
+
CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
|
|
2890
|
+
-->
|
|
2891
|
+
<div class="flex items-start justify-between gap-3 p-3">
|
|
2892
|
+
@if (icon()) {
|
|
2893
|
+
<ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
|
|
2894
|
+
}
|
|
2895
|
+
<div class="min-w-0 flex-1">
|
|
2896
|
+
@if (heading()) {
|
|
2897
|
+
@switch (headingLevel()) {
|
|
2898
|
+
@case (1) {
|
|
2899
|
+
<h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
|
|
2900
|
+
}
|
|
2901
|
+
@case (2) {
|
|
2902
|
+
<h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
|
|
2903
|
+
}
|
|
2904
|
+
@case (4) {
|
|
2905
|
+
<h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
|
|
2906
|
+
}
|
|
2907
|
+
@case (5) {
|
|
2908
|
+
<h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
|
|
2909
|
+
}
|
|
2910
|
+
@case (6) {
|
|
2911
|
+
<h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
|
|
2912
|
+
}
|
|
2913
|
+
@default {
|
|
2914
|
+
<h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
@if (descriptionText()) {
|
|
2919
|
+
<p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
|
|
2920
|
+
}
|
|
2921
|
+
<ng-content />
|
|
2922
|
+
</div>
|
|
2923
|
+
@if (showClose()) {
|
|
2924
|
+
<button
|
|
2925
|
+
type="button"
|
|
2926
|
+
(click)="closeClick.emit()"
|
|
2927
|
+
aria-label="Đóng hộp thoại"
|
|
2928
|
+
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
|
|
2929
|
+
>
|
|
2930
|
+
<ol-icon name="close" size="sm" />
|
|
2931
|
+
</button>
|
|
2932
|
+
}
|
|
2933
|
+
</div>
|
|
2934
|
+
`, isInline: true, dependencies: [{ kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2935
|
+
}
|
|
2936
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, decorators: [{
|
|
2937
|
+
type: Component,
|
|
2938
|
+
args: [{
|
|
2939
|
+
selector: 'ol-modal-header',
|
|
2940
|
+
standalone: true,
|
|
2941
|
+
imports: [OlIcon, OlIconShape],
|
|
2942
|
+
template: `
|
|
2943
|
+
<!--
|
|
2944
|
+
PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
|
|
2945
|
+
kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
|
|
2946
|
+
và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
|
|
2947
|
+
CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
|
|
2948
|
+
-->
|
|
2949
|
+
<div class="flex items-start justify-between gap-3 p-3">
|
|
2950
|
+
@if (icon()) {
|
|
2951
|
+
<ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
|
|
2952
|
+
}
|
|
2953
|
+
<div class="min-w-0 flex-1">
|
|
2954
|
+
@if (heading()) {
|
|
2955
|
+
@switch (headingLevel()) {
|
|
2956
|
+
@case (1) {
|
|
2957
|
+
<h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
|
|
2958
|
+
}
|
|
2959
|
+
@case (2) {
|
|
2960
|
+
<h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
|
|
2961
|
+
}
|
|
2962
|
+
@case (4) {
|
|
2963
|
+
<h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
|
|
2964
|
+
}
|
|
2965
|
+
@case (5) {
|
|
2966
|
+
<h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
|
|
2967
|
+
}
|
|
2968
|
+
@case (6) {
|
|
2969
|
+
<h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
|
|
2970
|
+
}
|
|
2971
|
+
@default {
|
|
2972
|
+
<h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
@if (descriptionText()) {
|
|
2977
|
+
<p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
|
|
2978
|
+
}
|
|
2979
|
+
<ng-content />
|
|
2980
|
+
</div>
|
|
2981
|
+
@if (showClose()) {
|
|
2982
|
+
<button
|
|
2983
|
+
type="button"
|
|
2984
|
+
(click)="closeClick.emit()"
|
|
2985
|
+
aria-label="Đóng hộp thoại"
|
|
2986
|
+
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
|
|
2987
|
+
>
|
|
2988
|
+
<ol-icon name="close" size="sm" />
|
|
2989
|
+
</button>
|
|
2990
|
+
}
|
|
2991
|
+
</div>
|
|
2992
|
+
`,
|
|
2993
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2994
|
+
host: { class: 'block' },
|
|
2995
|
+
}]
|
|
2996
|
+
}], propDecorators: { headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], supportingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "supportingText", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], showClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClose", required: false }] }], closeClick: [{ type: i0.Output, args: ["closeClick"] }] } });
|
|
2997
|
+
|
|
2756
2998
|
class OlModal {
|
|
2757
2999
|
focusTrap = inject(OlFocusTrapService);
|
|
2758
3000
|
destroyRef = inject(DestroyRef);
|
|
@@ -2781,10 +3023,81 @@ class OlModal {
|
|
|
2781
3023
|
...(ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []));
|
|
2782
3024
|
title = input('', /* @ts-ignore */
|
|
2783
3025
|
...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
3026
|
+
/**
|
|
3027
|
+
* Icon đứng trước tiêu đề ở phần đầu mặc định.
|
|
3028
|
+
*
|
|
3029
|
+
* Thêm 2026-09-24 cùng `description`. Trước đó `ol-modal` chỉ có `title`,
|
|
3030
|
+
* nên muốn có icon hay dòng mô tả là phải lắp `ol-modal-header` thủ công —
|
|
3031
|
+
* và lắp rồi thì `title` bị bỏ qua, phải khai lại qua `heading`. Đo trên
|
|
3032
|
+
* /modal: 13 chỗ dùng modal, chỗ nào cần icon đều lặp lại đúng khuôn đó.
|
|
3033
|
+
*
|
|
3034
|
+
* Vẫn dùng `ol-modal-header` được khi cần thứ phần đầu mặc định không có
|
|
3035
|
+
* (`supportingText`, `helperText`, nút tuỳ biến); lắp nó vào thì phần đầu
|
|
3036
|
+
* mặc định tự tắt, xem `showDefaultHeader`.
|
|
3037
|
+
*/
|
|
3038
|
+
icon = input('', /* @ts-ignore */
|
|
3039
|
+
...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
3040
|
+
/** Dòng mô tả dưới tiêu đề ở phần đầu mặc định. Xem chú thích ở `icon`. */
|
|
3041
|
+
description = input('', /* @ts-ignore */
|
|
3042
|
+
...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
|
|
2784
3043
|
size = input('base', /* @ts-ignore */
|
|
2785
3044
|
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
3045
|
+
/**
|
|
3046
|
+
* Cho phép đóng hộp thoại bằng Escape hoặc bấm ra nền mờ.
|
|
3047
|
+
*
|
|
3048
|
+
* Tắt khi người dùng BUỘC phải chọn một phương án — xác nhận xoá, đồng ý
|
|
3049
|
+
* điều khoản. Không còn ẩn nút X nữa: xem `showClose`.
|
|
3050
|
+
*/
|
|
2786
3051
|
dismissible = input(true, /* @ts-ignore */
|
|
2787
3052
|
...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
|
|
3053
|
+
/**
|
|
3054
|
+
* Có vẽ nút X ở góc phải phần đầu không. Mặc định **theo `dismissible`**.
|
|
3055
|
+
*
|
|
3056
|
+
* TÁCH KHỎI `dismissible` (2026-09-24). Trước đây một input gánh BỐN việc —
|
|
3057
|
+
* hiện nút X, đóng bằng Escape, đóng khi bấm nền, và quyết định có dựng
|
|
3058
|
+
* phần đầu — nên `[dismissible]="false"` (chỉ định chặn bấm-ra-ngoài) kéo
|
|
3059
|
+
* theo MẤT nút X. Người dùng phải tự lắp `ol-modal-header` để lấy lại nút,
|
|
3060
|
+
* tức phải khai lại cả tiêu đề vốn đã truyền qua `title`.
|
|
3061
|
+
*
|
|
3062
|
+
* Nay ba trường hợp tách bạch:
|
|
3063
|
+
*
|
|
3064
|
+
* `[dismissible]="false"` chặn Escape + nền mờ, VẪN có nút X
|
|
3065
|
+
* `[dismissible]="false" [showClose]="false"` không lối thoát nào ngoài nút trong nội dung
|
|
3066
|
+
* `[showClose]="false"` giấu nút X, vẫn đóng được bằng Escape/nền
|
|
3067
|
+
*
|
|
3068
|
+
* Mặc định `true` — nút X LUÔN hiện trừ khi tắt tường minh.
|
|
3069
|
+
*
|
|
3070
|
+
* ĐÂY LÀ THAY ĐỔI PHÁ VỠ so với bản trước: `[dismissible]="false"` vốn ẩn
|
|
3071
|
+
* nút X, nay vẫn giữ. Nơi nào thật sự muốn không có lối thoát nào phải thêm
|
|
3072
|
+
* `[showClose]="false"`. Đổi vì hành vi cũ là LỖI chứ không phải lựa chọn:
|
|
3073
|
+
* người dùng chặn bấm-ra-ngoài rồi mất nút đóng, phải tự lắp
|
|
3074
|
+
* `ol-modal-header` để lấy lại — tức khai lại tiêu đề đã truyền qua `title`.
|
|
3075
|
+
*/
|
|
3076
|
+
showClose = input(true, /* @ts-ignore */
|
|
3077
|
+
...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
|
|
3078
|
+
/** Nút X có hiện không. Tách hẳn khỏi `dismissible` từ 2026-09-24. */
|
|
3079
|
+
closeVisible = computed(() => this.showClose(), /* @ts-ignore */
|
|
3080
|
+
...(ngDevMode ? [{ debugName: "closeVisible" }] : /* istanbul ignore next */ []));
|
|
3081
|
+
/**
|
|
3082
|
+
* Có `<ol-modal-header>` lồng bên trong không.
|
|
3083
|
+
*
|
|
3084
|
+
* Dùng để KHÔNG dựng phần đầu mặc định khi người dùng đã tự lắp header
|
|
3085
|
+
* riêng. Trước đây phải tắt tay bằng `title=""` kèm `[dismissible]="false"`
|
|
3086
|
+
* — quên một trong hai là lồng HAI khung header chồng nhau, mà build vẫn
|
|
3087
|
+
* xanh nên chỉ nhìn mới thấy.
|
|
3088
|
+
*
|
|
3089
|
+
* `descendants: false` để chỉ bắt header là CON TRỰC TIẾP. Một `ol-modal`
|
|
3090
|
+
* lồng trong nội dung của modal này thì header của nó không tính.
|
|
3091
|
+
*/
|
|
3092
|
+
projectedHeader = contentChild(OlModalHeader, { ...(ngDevMode ? { debugName: "projectedHeader" } : /* istanbul ignore next */ {}), descendants: false });
|
|
3093
|
+
/**
|
|
3094
|
+
* Có dựng phần đầu mặc định không.
|
|
3095
|
+
*
|
|
3096
|
+
* Người dùng lắp `ol-modal-header` riêng thì thôi — nếu không sẽ có hai
|
|
3097
|
+
* khung `p-3` chồng nhau, nút đóng hiện hai lần.
|
|
3098
|
+
*/
|
|
3099
|
+
showDefaultHeader = computed(() => !this.projectedHeader() && (this.title() !== '' || this.closeVisible()), /* @ts-ignore */
|
|
3100
|
+
...(ngDevMode ? [{ debugName: "showDefaultHeader" }] : /* istanbul ignore next */ []));
|
|
2788
3101
|
backdrop = input('dynamic', /* @ts-ignore */
|
|
2789
3102
|
...(ngDevMode ? [{ debugName: "backdrop" }] : /* istanbul ignore next */ []));
|
|
2790
3103
|
placement = input('center', /* @ts-ignore */
|
|
@@ -2891,12 +3204,12 @@ class OlModal {
|
|
|
2891
3204
|
return this.isOpen();
|
|
2892
3205
|
}
|
|
2893
3206
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModal, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2894
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModal, isStandalone: true, selector: "ol-modal", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, backdrop: { classPropertyName: "backdrop", publicName: "backdrop", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", opened: "opened", closed: "closed" }, viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isOpen()) {\n <div\n (click)=\"onBackdropClick($event)\"\n class=\"fixed inset-0 z-modal overflow-y-auto overflow-x-hidden flex w-full h-full bg-overlay-backdrop backdrop-blur-xs transition-opacity duration-200\"\n [class]=\"placementClasses()\"\n >\n <div\n #panel\n [attr.role]=\"role()\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title() || 'H\u1ED9p tho\u1EA1i'\"\n tabindex=\"-1\"\n class=\"relative w-full max-h-full transition-all duration-200 animate-in fade-in zoom-in-95\"\n [class]=\"sizeClasses()\"\n >\n <div\n class=\"relative bg-neutral-primary rounded-md shadow-2xl border border-default overflow-hidden\"\n >\n
|
|
3207
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModal, isStandalone: true, selector: "ol-modal", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, showClose: { classPropertyName: "showClose", publicName: "showClose", isSignal: true, isRequired: false, transformFunction: null }, backdrop: { classPropertyName: "backdrop", publicName: "backdrop", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, role: { classPropertyName: "role", publicName: "role", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", opened: "opened", closed: "closed" }, queries: [{ propertyName: "projectedHeader", first: true, predicate: OlModalHeader, isSignal: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isOpen()) {\n <div\n (click)=\"onBackdropClick($event)\"\n class=\"fixed inset-0 z-modal overflow-y-auto overflow-x-hidden flex w-full h-full bg-overlay-backdrop backdrop-blur-xs transition-opacity duration-200\"\n [class]=\"placementClasses()\"\n >\n <div\n #panel\n [attr.role]=\"role()\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title() || 'H\u1ED9p tho\u1EA1i'\"\n tabindex=\"-1\"\n class=\"relative w-full max-h-full transition-all duration-200 animate-in fade-in zoom-in-95\"\n [class]=\"sizeClasses()\"\n >\n <div\n class=\"relative bg-neutral-primary rounded-md shadow-2xl border border-default overflow-hidden\"\n >\n <!--\n Ph\u1EA7n \u0111\u1EA7u M\u1EB6C \u0110\u1ECANH \u2014 ch\u1EC9 d\u1EF1ng khi ng\u01B0\u1EDDi d\u00F9ng KH\u00D4NG t\u1EF1 l\u1EAFp\n `<ol-modal-header>`. Xem `showDefaultHeader` trong `.ts`: l\u1EAFp c\u1EA3 hai\n l\u00E0 hai khung `p-3` ch\u1ED3ng nhau v\u00E0 n\u00FAt \u0111\u00F3ng hi\u1EC7n hai l\u1EA7n.\n -->\n <!--\n Ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh D\u00D9NG L\u1EA0I `ol-modal-header` thay v\u00EC d\u1EF1ng l\u1EA1i s\u00E1u\n nh\u00E1nh `@switch` cho `headingLevel` c\u1ED9ng m\u1ED9t n\u00FAt \u0111\u00F3ng.\n\n B\u1EA3n c\u0169 ch\u00E9p khu\u00F4n: modal c\u00F3 `title` th\u00EC t\u1EF1 v\u1EBD `<h1..h6>` + n\u00FAt X, c\u00F2n\n ai c\u1EA7n `icon` hay `description` ph\u1EA3i l\u1EAFp `ol-modal-header` th\u1EE7 c\u00F4ng \u2014\n m\u00E0 l\u1EAFp r\u1ED3i th\u00EC `title` b\u1ECB b\u1ECF qua, ph\u1EA3i khai l\u1EA1i qua `heading`. Hai\n khu\u00F4n song song n\u00EAn s\u1EEDa m\u1ED9t b\u00EAn l\u00E0 l\u1EC7ch b\u00EAn kia.\n\n Nay `ol-modal` nh\u1EADn th\u1EB3ng `icon`/`description` v\u00E0 chuy\u1EC3n xu\u1ED1ng. L\u1EAFp\n `ol-modal-header` ri\u00EAng v\u1EABn \u0111\u01B0\u1EE3c khi c\u1EA7n th\u1EE9 ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh kh\u00F4ng\n c\u00F3 (`supportingText`, `helperText`) \u2014 l\u00FAc \u0111\u00F3 `showDefaultHeader` t\u1EF1\n t\u1EAFt, kh\u00F4ng c\u00F3 hai khung ch\u1ED3ng nhau.\n -->\n @if (showDefaultHeader()) {\n <ol-modal-header\n [heading]=\"title()\"\n [headingLevel]=\"headingLevel()\"\n [icon]=\"icon()\"\n [description]=\"description()\"\n [showClose]=\"closeVisible()\"\n (closeClick)=\"hide()\"\n />\n }\n\n <!-- Modal Body Content -->\n <!--\n \u0110\u1EC7m 12px B\u1EB0NG header v\u00E0 footer \u2014 ch\u1EEF th\u00E2n b\u00E0i th\u1EB3ng h\u00E0ng v\u1EDBi ti\u00EAu \u0111\u1EC1.\n Tr\u01B0\u1EDBc \u0111\u00E2y `p-6` (24px) trong khi hai ph\u1EA7n kia `p-3`, n\u00EAn n\u1ED9i dung\n th\u1EE5t v\u00E0o s\u00E2u h\u01A1n ti\u00EAu \u0111\u1EC1 \u0111\u00FAng 12px, nh\u00ECn nh\u01B0 l\u1ED7i c\u0103n l\u1EC1.\n -->\n <div class=\"p-3 space-y-4 text-body leading-relaxed text-sm md:text-base\">\n <ng-content />\n </div>\n\n <!--\n Modal Footer Actions Slot.\n\n `[&>[footer]]:*` \u00E1p th\u1EB3ng l\u00EAn PH\u1EA6N T\u1EEC \u0110\u01AF\u1EE2C CHI\u1EBEU V\u00C0O, kh\u00F4ng ch\u1EC9 l\u00EAn\n khung n\u00E0y. N\u01A1i d\u00F9ng g\u1EA7n nh\u01B0 lu\u00F4n b\u1ECDc n\u00FAt trong m\u1ED9t `<div footer>`, m\u00E0\n `gap-3` c\u1EE7a khung th\u00EC \u00E1p gi\u1EEFa khung v\u00E0 c\u00E1c con TR\u1EF0C TI\u1EBEP c\u1EE7a n\u00F3 \u2014\n t\u1EE9c gi\u1EEFa c\u00E1i `div` \u0111\u00F3 v\u00E0 anh em c\u1EE7a n\u00F3, KH\u00D4NG ph\u1EA3i gi\u1EEFa hai n\u00FAt b\u00EAn\n trong. \u0110o /modal ng\u00E0y 2026-09-24: modal \"Giao b\u00E0i t\u1EADp m\u1EDBi\" c\u00F3 hai n\u00FAt\n c\u00E1ch nhau \u0111\u00FAng **0px**, d\u00EDnh li\u1EC1n.\n\n Qu\u00E9t to\u00E0n repo l\u00FAc \u0111\u00F3: 13 ch\u1ED7 d\u00F9ng `<div footer>`, ch\u1EC9 1 ch\u1ED7 t\u1EF1 nh\u1EDB\n th\u00EAm `class=\"flex gap-2\"`; 0 ch\u1ED7 d\u00F9ng `ol-modal-footer`. B\u1EAFt n\u01A1i d\u00F9ng\n nh\u1EDB m\u1ED9t class m\u00E0 12/13 ch\u1ED7 qu\u00EAn th\u00EC \u0111\u00F3 l\u00E0 l\u1ED7i c\u1EE7a API, kh\u00F4ng ph\u1EA3i l\u1ED7i\n ng\u01B0\u1EDDi d\u00F9ng \u2014 n\u00EAn component t\u1EF1 lo.\n\n `justify-end` v\u00E0 `gap-3` gi\u1EEF nguy\u00EAn tr\u00EAn khung \u0111\u1EC3 nh\u00E1nh KH\u00D4NG b\u1ECDc\n `div` (chi\u1EBFu th\u1EB3ng nhi\u1EC1u `ol-button`) v\u1EABn \u0111\u00FAng.\n -->\n <div\n footer-frame\n class=\"p-3 flex items-center justify-end gap-3 [&>[footer]]:flex [&>[footer]]:flex-wrap [&>[footer]]:items-center [&>[footer]]:gap-3\"\n >\n <ng-content select=\"[footer]\">\n <button\n type=\"button\"\n (click)=\"hide()\"\n class=\"px-4 py-2 text-sm font-medium rounded-md text-body bg-neutral-primary border border-default hover:bg-neutral-tertiary cursor-pointer transition-colors focus:outline-hidden focus:ring-2 focus:ring-brand-subtle\"\n >\n \u0110\u00F3ng\n </button>\n </ng-content>\n </div>\n </div>\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlModalHeader, selector: "ol-modal-header", inputs: ["headingLevel", "heading", "icon", "description", "supportingText", "helperText", "showClose"], outputs: ["closeClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2895
3208
|
}
|
|
2896
3209
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModal, decorators: [{
|
|
2897
3210
|
type: Component,
|
|
2898
|
-
args: [{ selector: 'ol-modal', standalone: true, imports: [CommonModule,
|
|
2899
|
-
}], ctorParameters: () => [], propDecorators: { panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }, { type: i0.Output, args: ["isOpenChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], backdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdrop", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
3211
|
+
args: [{ selector: 'ol-modal', standalone: true, imports: [CommonModule, OlModalHeader], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (isOpen()) {\n <div\n (click)=\"onBackdropClick($event)\"\n class=\"fixed inset-0 z-modal overflow-y-auto overflow-x-hidden flex w-full h-full bg-overlay-backdrop backdrop-blur-xs transition-opacity duration-200\"\n [class]=\"placementClasses()\"\n >\n <div\n #panel\n [attr.role]=\"role()\"\n aria-modal=\"true\"\n [attr.aria-label]=\"title() || 'H\u1ED9p tho\u1EA1i'\"\n tabindex=\"-1\"\n class=\"relative w-full max-h-full transition-all duration-200 animate-in fade-in zoom-in-95\"\n [class]=\"sizeClasses()\"\n >\n <div\n class=\"relative bg-neutral-primary rounded-md shadow-2xl border border-default overflow-hidden\"\n >\n <!--\n Ph\u1EA7n \u0111\u1EA7u M\u1EB6C \u0110\u1ECANH \u2014 ch\u1EC9 d\u1EF1ng khi ng\u01B0\u1EDDi d\u00F9ng KH\u00D4NG t\u1EF1 l\u1EAFp\n `<ol-modal-header>`. Xem `showDefaultHeader` trong `.ts`: l\u1EAFp c\u1EA3 hai\n l\u00E0 hai khung `p-3` ch\u1ED3ng nhau v\u00E0 n\u00FAt \u0111\u00F3ng hi\u1EC7n hai l\u1EA7n.\n -->\n <!--\n Ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh D\u00D9NG L\u1EA0I `ol-modal-header` thay v\u00EC d\u1EF1ng l\u1EA1i s\u00E1u\n nh\u00E1nh `@switch` cho `headingLevel` c\u1ED9ng m\u1ED9t n\u00FAt \u0111\u00F3ng.\n\n B\u1EA3n c\u0169 ch\u00E9p khu\u00F4n: modal c\u00F3 `title` th\u00EC t\u1EF1 v\u1EBD `<h1..h6>` + n\u00FAt X, c\u00F2n\n ai c\u1EA7n `icon` hay `description` ph\u1EA3i l\u1EAFp `ol-modal-header` th\u1EE7 c\u00F4ng \u2014\n m\u00E0 l\u1EAFp r\u1ED3i th\u00EC `title` b\u1ECB b\u1ECF qua, ph\u1EA3i khai l\u1EA1i qua `heading`. Hai\n khu\u00F4n song song n\u00EAn s\u1EEDa m\u1ED9t b\u00EAn l\u00E0 l\u1EC7ch b\u00EAn kia.\n\n Nay `ol-modal` nh\u1EADn th\u1EB3ng `icon`/`description` v\u00E0 chuy\u1EC3n xu\u1ED1ng. L\u1EAFp\n `ol-modal-header` ri\u00EAng v\u1EABn \u0111\u01B0\u1EE3c khi c\u1EA7n th\u1EE9 ph\u1EA7n \u0111\u1EA7u m\u1EB7c \u0111\u1ECBnh kh\u00F4ng\n c\u00F3 (`supportingText`, `helperText`) \u2014 l\u00FAc \u0111\u00F3 `showDefaultHeader` t\u1EF1\n t\u1EAFt, kh\u00F4ng c\u00F3 hai khung ch\u1ED3ng nhau.\n -->\n @if (showDefaultHeader()) {\n <ol-modal-header\n [heading]=\"title()\"\n [headingLevel]=\"headingLevel()\"\n [icon]=\"icon()\"\n [description]=\"description()\"\n [showClose]=\"closeVisible()\"\n (closeClick)=\"hide()\"\n />\n }\n\n <!-- Modal Body Content -->\n <!--\n \u0110\u1EC7m 12px B\u1EB0NG header v\u00E0 footer \u2014 ch\u1EEF th\u00E2n b\u00E0i th\u1EB3ng h\u00E0ng v\u1EDBi ti\u00EAu \u0111\u1EC1.\n Tr\u01B0\u1EDBc \u0111\u00E2y `p-6` (24px) trong khi hai ph\u1EA7n kia `p-3`, n\u00EAn n\u1ED9i dung\n th\u1EE5t v\u00E0o s\u00E2u h\u01A1n ti\u00EAu \u0111\u1EC1 \u0111\u00FAng 12px, nh\u00ECn nh\u01B0 l\u1ED7i c\u0103n l\u1EC1.\n -->\n <div class=\"p-3 space-y-4 text-body leading-relaxed text-sm md:text-base\">\n <ng-content />\n </div>\n\n <!--\n Modal Footer Actions Slot.\n\n `[&>[footer]]:*` \u00E1p th\u1EB3ng l\u00EAn PH\u1EA6N T\u1EEC \u0110\u01AF\u1EE2C CHI\u1EBEU V\u00C0O, kh\u00F4ng ch\u1EC9 l\u00EAn\n khung n\u00E0y. N\u01A1i d\u00F9ng g\u1EA7n nh\u01B0 lu\u00F4n b\u1ECDc n\u00FAt trong m\u1ED9t `<div footer>`, m\u00E0\n `gap-3` c\u1EE7a khung th\u00EC \u00E1p gi\u1EEFa khung v\u00E0 c\u00E1c con TR\u1EF0C TI\u1EBEP c\u1EE7a n\u00F3 \u2014\n t\u1EE9c gi\u1EEFa c\u00E1i `div` \u0111\u00F3 v\u00E0 anh em c\u1EE7a n\u00F3, KH\u00D4NG ph\u1EA3i gi\u1EEFa hai n\u00FAt b\u00EAn\n trong. \u0110o /modal ng\u00E0y 2026-09-24: modal \"Giao b\u00E0i t\u1EADp m\u1EDBi\" c\u00F3 hai n\u00FAt\n c\u00E1ch nhau \u0111\u00FAng **0px**, d\u00EDnh li\u1EC1n.\n\n Qu\u00E9t to\u00E0n repo l\u00FAc \u0111\u00F3: 13 ch\u1ED7 d\u00F9ng `<div footer>`, ch\u1EC9 1 ch\u1ED7 t\u1EF1 nh\u1EDB\n th\u00EAm `class=\"flex gap-2\"`; 0 ch\u1ED7 d\u00F9ng `ol-modal-footer`. B\u1EAFt n\u01A1i d\u00F9ng\n nh\u1EDB m\u1ED9t class m\u00E0 12/13 ch\u1ED7 qu\u00EAn th\u00EC \u0111\u00F3 l\u00E0 l\u1ED7i c\u1EE7a API, kh\u00F4ng ph\u1EA3i l\u1ED7i\n ng\u01B0\u1EDDi d\u00F9ng \u2014 n\u00EAn component t\u1EF1 lo.\n\n `justify-end` v\u00E0 `gap-3` gi\u1EEF nguy\u00EAn tr\u00EAn khung \u0111\u1EC3 nh\u00E1nh KH\u00D4NG b\u1ECDc\n `div` (chi\u1EBFu th\u1EB3ng nhi\u1EC1u `ol-button`) v\u1EABn \u0111\u00FAng.\n -->\n <div\n footer-frame\n class=\"p-3 flex items-center justify-end gap-3 [&>[footer]]:flex [&>[footer]]:flex-wrap [&>[footer]]:items-center [&>[footer]]:gap-3\"\n >\n <ng-content select=\"[footer]\">\n <button\n type=\"button\"\n (click)=\"hide()\"\n class=\"px-4 py-2 text-sm font-medium rounded-md text-body bg-neutral-primary border border-default hover:bg-neutral-tertiary cursor-pointer transition-colors focus:outline-hidden focus:ring-2 focus:ring-brand-subtle\"\n >\n \u0110\u00F3ng\n </button>\n </ng-content>\n </div>\n </div>\n </div>\n </div>\n}\n" }]
|
|
3212
|
+
}], ctorParameters: () => [], propDecorators: { panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }, { type: i0.Output, args: ["isOpenChange"] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], showClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClose", required: false }] }], projectedHeader: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlModalHeader), { ...{ descendants: false }, isSignal: true }] }], backdrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "backdrop", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], role: [{ type: i0.Input, args: [{ isSignal: true, alias: "role", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
2900
3213
|
|
|
2901
3214
|
/**
|
|
2902
3215
|
* Hộp thoại bắt buộc trả lời: xoá, huỷ, rời trang khi chưa lưu.
|
|
@@ -3008,12 +3321,22 @@ class OlAlertDialog {
|
|
|
3008
3321
|
}
|
|
3009
3322
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAlertDialog, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3010
3323
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAlertDialog, isStandalone: true, selector: "ol-alert-dialog", inputs: { isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, confirmText: { classPropertyName: "confirmText", publicName: "confirmText", isSignal: true, isRequired: false, transformFunction: null }, cancelText: { classPropertyName: "cancelText", publicName: "cancelText", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", confirm: "confirm", cancel: "cancel", closed: "closed" }, ngImport: i0, template: `
|
|
3324
|
+
<!--
|
|
3325
|
+
showClose=false là BẮT BUỘC từ 2026-09-24: showClose đã TÁCH khỏi
|
|
3326
|
+
dismissible, nên chỉ tắt dismissible thì nút X VẪN hiện. Hộp thoại này
|
|
3327
|
+
buộc người dùng chọn một trong hai nút, không có lối thoát thứ ba.
|
|
3328
|
+
|
|
3329
|
+
Hai bẫy đã mắc khi viết chú thích này, ghi lại: chú thích đặt XEN GIỮA
|
|
3330
|
+
danh sách thuộc tính làm hỏng build với NG1002; và dấu nháy ngược trong
|
|
3331
|
+
chú thích đóng template literal sớm, lỗi hiện ở dòng chẳng liên quan.
|
|
3332
|
+
-->
|
|
3011
3333
|
<ol-modal
|
|
3012
3334
|
[isOpen]="isOpen()"
|
|
3013
3335
|
(isOpenChange)="isOpen.set($event)"
|
|
3014
3336
|
[role]="'alertdialog'"
|
|
3015
3337
|
size="sm"
|
|
3016
3338
|
[dismissible]="false"
|
|
3339
|
+
[showClose]="false"
|
|
3017
3340
|
backdrop="static"
|
|
3018
3341
|
(closed)="closed.emit()"
|
|
3019
3342
|
>
|
|
@@ -3038,7 +3361,7 @@ class OlAlertDialog {
|
|
|
3038
3361
|
</ol-button>
|
|
3039
3362
|
</div>
|
|
3040
3363
|
</ol-modal>
|
|
3041
|
-
`, isInline: true, dependencies: [{ kind: "component", type: OlModal, selector: "ol-modal", inputs: ["headingLevel", "isOpen", "title", "size", "dismissible", "backdrop", "placement", "role"], outputs: ["isOpenChange", "opened", "closed"] }, { kind: "component", type: OlButton, selector: "ol-button", inputs: ["color", "size", "pill", "outline", "iconOnly", "loading", "disabled", "type", "ariaLabel", "ariaExpanded", "ariaControls", "ariaPressed"], outputs: ["btnClick"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3364
|
+
`, isInline: true, dependencies: [{ kind: "component", type: OlModal, selector: "ol-modal", inputs: ["headingLevel", "isOpen", "title", "icon", "description", "size", "dismissible", "showClose", "backdrop", "placement", "role"], outputs: ["isOpenChange", "opened", "closed"] }, { kind: "component", type: OlButton, selector: "ol-button", inputs: ["color", "size", "pill", "outline", "iconOnly", "loading", "disabled", "type", "ariaLabel", "ariaExpanded", "ariaControls", "ariaPressed"], outputs: ["btnClick"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3042
3365
|
}
|
|
3043
3366
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAlertDialog, decorators: [{
|
|
3044
3367
|
type: Component,
|
|
@@ -3047,12 +3370,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
3047
3370
|
standalone: true,
|
|
3048
3371
|
imports: [OlModal, OlButton, OlIconShape],
|
|
3049
3372
|
template: `
|
|
3373
|
+
<!--
|
|
3374
|
+
showClose=false là BẮT BUỘC từ 2026-09-24: showClose đã TÁCH khỏi
|
|
3375
|
+
dismissible, nên chỉ tắt dismissible thì nút X VẪN hiện. Hộp thoại này
|
|
3376
|
+
buộc người dùng chọn một trong hai nút, không có lối thoát thứ ba.
|
|
3377
|
+
|
|
3378
|
+
Hai bẫy đã mắc khi viết chú thích này, ghi lại: chú thích đặt XEN GIỮA
|
|
3379
|
+
danh sách thuộc tính làm hỏng build với NG1002; và dấu nháy ngược trong
|
|
3380
|
+
chú thích đóng template literal sớm, lỗi hiện ở dòng chẳng liên quan.
|
|
3381
|
+
-->
|
|
3050
3382
|
<ol-modal
|
|
3051
3383
|
[isOpen]="isOpen()"
|
|
3052
3384
|
(isOpenChange)="isOpen.set($event)"
|
|
3053
3385
|
[role]="'alertdialog'"
|
|
3054
3386
|
size="sm"
|
|
3055
3387
|
[dismissible]="false"
|
|
3388
|
+
[showClose]="false"
|
|
3056
3389
|
backdrop="static"
|
|
3057
3390
|
(closed)="closed.emit()"
|
|
3058
3391
|
>
|
|
@@ -3300,6 +3633,22 @@ class OlBadge {
|
|
|
3300
3633
|
*/
|
|
3301
3634
|
color = input('gray', /* @ts-ignore */
|
|
3302
3635
|
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
3636
|
+
/**
|
|
3637
|
+
* Có vẽ viền không. Tắt thì giữ nguyên nền và màu chữ của `color`.
|
|
3638
|
+
*
|
|
3639
|
+
* Viền là thuộc tính ĐỘC LẬP với màu, nên tách thành input riêng thay vì
|
|
3640
|
+
* thêm giá trị vào `BadgeColor`: làm thành màu thì muốn "brand không viền"
|
|
3641
|
+
* và "danger không viền" phải nhân đôi cả bảng 12 màu thành 24.
|
|
3642
|
+
*
|
|
3643
|
+
* Khác `color="ghost"`: ghost bỏ CẢ nền lẫn viền và ép chữ về `text-heading`
|
|
3644
|
+
* — nó là một màu. `[bordered]="false"` chỉ bỏ viền.
|
|
3645
|
+
*
|
|
3646
|
+
* Class `border` VẪN giữ (đổi thành `border-transparent`) chứ không bỏ hẳn:
|
|
3647
|
+
* badge tính chiều cao theo hộp viền, bỏ đi là badge không viền thấp hơn
|
|
3648
|
+
* badge thường đúng 2px khi đặt cạnh nhau. Cùng lý do với `ghost`.
|
|
3649
|
+
*/
|
|
3650
|
+
bordered = input(true, /* @ts-ignore */
|
|
3651
|
+
...(ngDevMode ? [{ debugName: "bordered" }] : /* istanbul ignore next */ []));
|
|
3303
3652
|
/**
|
|
3304
3653
|
* @deprecated Dùng `color`.
|
|
3305
3654
|
*
|
|
@@ -3455,6 +3804,22 @@ class OlBadge {
|
|
|
3455
3804
|
classes.push('bg-neutral-secondary', 'border-default-medium', 'text-heading');
|
|
3456
3805
|
break;
|
|
3457
3806
|
}
|
|
3807
|
+
/*
|
|
3808
|
+
* `[bordered]="false"` — bỏ viền, GIỮ nền và màu chữ.
|
|
3809
|
+
*
|
|
3810
|
+
* Phải LỌC class viền của màu ra rồi mới thêm `border-transparent`: hai
|
|
3811
|
+
* class viền cùng lúc thì trình duyệt chọn theo thứ tự luật trong file CSS
|
|
3812
|
+
* chứ không theo thứ tự đẩy vào mảng, nên kết quả đổi theo bản build.
|
|
3813
|
+
*
|
|
3814
|
+
* `border` (độ dày) vẫn giữ — xem chú thích ở input `bordered`.
|
|
3815
|
+
*/
|
|
3816
|
+
if (!this.bordered()) {
|
|
3817
|
+
for (let i = classes.length - 1; i >= 0; i--) {
|
|
3818
|
+
if (classes[i].startsWith('border-'))
|
|
3819
|
+
classes.splice(i, 1);
|
|
3820
|
+
}
|
|
3821
|
+
classes.push('border-transparent');
|
|
3822
|
+
}
|
|
3458
3823
|
return classes.join(' ');
|
|
3459
3824
|
}, /* @ts-ignore */
|
|
3460
3825
|
...(ngDevMode ? [{ debugName: "badgeClasses" }] : /* istanbul ignore next */ []));
|
|
@@ -3488,7 +3853,7 @@ class OlBadge {
|
|
|
3488
3853
|
this.dismiss.emit(event);
|
|
3489
3854
|
}
|
|
3490
3855
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBadge, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3491
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBadge, isStandalone: true, selector: "ol-badge", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null }, leftIcon: { classPropertyName: "leftIcon", publicName: "leftIcon", isSignal: true, isRequired: false, transformFunction: null }, rightIcon: { classPropertyName: "rightIcon", publicName: "rightIcon", isSignal: true, isRequired: false, transformFunction: null }, avatar: { classPropertyName: "avatar", publicName: "avatar", isSignal: true, isRequired: false, transformFunction: null }, avatarText: { classPropertyName: "avatarText", publicName: "avatarText", isSignal: true, isRequired: false, transformFunction: null }, secondaryText: { classPropertyName: "secondaryText", publicName: "secondaryText", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, host: { classAttribute: "inline-flex align-middle max-w-full" }, ngImport: i0, template: "<!--\n `href` bi\u1EBFn nh\u00E3n th\u00E0nh li\u00EAn k\u1EBFt. Ru\u1ED9t ch\u1EC9 c\u00F3 M\u1ED8T `<ng-content />` n\u00EAn kh\u00F4ng\n \u0111\u01B0\u1EE3c nh\u00E2n \u0111\u00F4i cho hai nh\u00E1nh \u2014 Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung v\u00E0o m\u1ED9t ch\u1ED7, nh\u00E2n\n \u0111\u00F4i l\u00E0 m\u1EA5t ru\u1ED9t \u1EDF M\u1ECCI nh\u00E1nh m\u00E0 build v\u1EABn xanh.\n-->\n@if (href()) {\n <a [href]=\"href()\" [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <span [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n}\n\n<ng-template #ruot>\n <!-- 1. Dot indicator -->\n @if (dot() || type() === 'dot') {\n <span class=\"w-1.5 h-1.5 rounded-full shrink-0\" [class]=\"dotClasses()\"></span>\n }\n\n <!-- 2. Avatar -->\n @if (avatar() || avatarText() || type() === 'avatar') {\n <ol-avatar\n [src]=\"avatar() || ''\"\n [initials]=\"avatarText() || ''\"\n [size]=\"size() === 'lg' ? 'xs' : '2xs'\"\n class=\"-ms-1 rounded-full shrink-0 flex items-center justify-center overflow-hidden\"\n />\n }\n\n <!-- 3. Loading Spinner -->\n @if (loading() || type() === 'loader') {\n <svg\n class=\"animate-spin w-3 h-3 shrink-0 text-current\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <!-- 4. Left icon \u2014 nh\u01B0\u1EDDng ch\u1ED7 cho spinner khi \u0111ang loading -->\n @if (leftIcon() && !loading() && type() !== 'loader') {\n <ol-icon [name]=\"leftIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 5. Main content -->\n <span class=\"truncate min-w-0 max-w-full block\"><ng-content /></span>\n\n <!-- 6. Secondary text separator & content -->\n @if (secondaryText() || type() === 'secondary') {\n <span class=\"w-px h-3 bg-current opacity-30 shrink-0 mx-0.5\"></span>\n <span class=\"opacity-80 font-normal truncate max-w-full\">{{ secondaryText() }}</span>\n }\n\n <!-- 7. Right icon -->\n @if (rightIcon()) {\n <ol-icon [name]=\"rightIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 8. Dismiss / Remove button (Figma Close X) -->\n @if (removable()) {\n <button\n type=\"button\"\n (click)=\"onDismiss($event)\"\n class=\"inline-flex items-center justify-center -me-0.5 p-0.5 rounded-xs hover:bg-black/10 dark:hover:bg-white/10 cursor-pointer transition-colors\"\n aria-label=\"G\u1EE1 nh\u00E3n\"\n >\n <svg\n class=\"w-3 h-3 text-current\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3856
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBadge, isStandalone: true, selector: "ol-badge", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null }, leftIcon: { classPropertyName: "leftIcon", publicName: "leftIcon", isSignal: true, isRequired: false, transformFunction: null }, rightIcon: { classPropertyName: "rightIcon", publicName: "rightIcon", isSignal: true, isRequired: false, transformFunction: null }, avatar: { classPropertyName: "avatar", publicName: "avatar", isSignal: true, isRequired: false, transformFunction: null }, avatarText: { classPropertyName: "avatarText", publicName: "avatarText", isSignal: true, isRequired: false, transformFunction: null }, secondaryText: { classPropertyName: "secondaryText", publicName: "secondaryText", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, removable: { classPropertyName: "removable", publicName: "removable", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismiss: "dismiss" }, host: { classAttribute: "inline-flex align-middle max-w-full" }, ngImport: i0, template: "<!--\n `href` bi\u1EBFn nh\u00E3n th\u00E0nh li\u00EAn k\u1EBFt. Ru\u1ED9t ch\u1EC9 c\u00F3 M\u1ED8T `<ng-content />` n\u00EAn kh\u00F4ng\n \u0111\u01B0\u1EE3c nh\u00E2n \u0111\u00F4i cho hai nh\u00E1nh \u2014 Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung v\u00E0o m\u1ED9t ch\u1ED7, nh\u00E2n\n \u0111\u00F4i l\u00E0 m\u1EA5t ru\u1ED9t \u1EDF M\u1ECCI nh\u00E1nh m\u00E0 build v\u1EABn xanh.\n-->\n@if (href()) {\n <a [href]=\"href()\" [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <span [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n}\n\n<ng-template #ruot>\n <!-- 1. Dot indicator -->\n @if (dot() || type() === 'dot') {\n <span class=\"w-1.5 h-1.5 rounded-full shrink-0\" [class]=\"dotClasses()\"></span>\n }\n\n <!-- 2. Avatar -->\n @if (avatar() || avatarText() || type() === 'avatar') {\n <ol-avatar\n [src]=\"avatar() || ''\"\n [initials]=\"avatarText() || ''\"\n [size]=\"size() === 'lg' ? 'xs' : '2xs'\"\n class=\"-ms-1 rounded-full shrink-0 flex items-center justify-center overflow-hidden\"\n />\n }\n\n <!-- 3. Loading Spinner -->\n @if (loading() || type() === 'loader') {\n <svg\n class=\"animate-spin w-3 h-3 shrink-0 text-current\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <!-- 4. Left icon \u2014 nh\u01B0\u1EDDng ch\u1ED7 cho spinner khi \u0111ang loading -->\n @if (leftIcon() && !loading() && type() !== 'loader') {\n <ol-icon [name]=\"leftIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 5. Main content -->\n <span class=\"truncate min-w-0 max-w-full block\"><ng-content /></span>\n\n <!-- 6. Secondary text separator & content -->\n @if (secondaryText() || type() === 'secondary') {\n <span class=\"w-px h-3 bg-current opacity-30 shrink-0 mx-0.5\"></span>\n <span class=\"opacity-80 font-normal truncate max-w-full\">{{ secondaryText() }}</span>\n }\n\n <!-- 7. Right icon -->\n @if (rightIcon()) {\n <ol-icon [name]=\"rightIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 8. Dismiss / Remove button (Figma Close X) -->\n @if (removable()) {\n <button\n type=\"button\"\n (click)=\"onDismiss($event)\"\n class=\"inline-flex items-center justify-center -me-0.5 p-0.5 rounded-xs hover:bg-black/10 dark:hover:bg-white/10 cursor-pointer transition-colors\"\n aria-label=\"G\u1EE1 nh\u00E3n\"\n >\n <svg\n class=\"w-3 h-3 text-current\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3492
3857
|
}
|
|
3493
3858
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBadge, decorators: [{
|
|
3494
3859
|
type: Component,
|
|
@@ -3503,7 +3868,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
3503
3868
|
// hay không có `min-w-0` badge đều dừng đúng 60px.
|
|
3504
3869
|
class: 'inline-flex align-middle max-w-full',
|
|
3505
3870
|
}, template: "<!--\n `href` bi\u1EBFn nh\u00E3n th\u00E0nh li\u00EAn k\u1EBFt. Ru\u1ED9t ch\u1EC9 c\u00F3 M\u1ED8T `<ng-content />` n\u00EAn kh\u00F4ng\n \u0111\u01B0\u1EE3c nh\u00E2n \u0111\u00F4i cho hai nh\u00E1nh \u2014 Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung v\u00E0o m\u1ED9t ch\u1ED7, nh\u00E2n\n \u0111\u00F4i l\u00E0 m\u1EA5t ru\u1ED9t \u1EDF M\u1ECCI nh\u00E1nh m\u00E0 build v\u1EABn xanh.\n-->\n@if (href()) {\n <a [href]=\"href()\" [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <span [class]=\"badgeClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n}\n\n<ng-template #ruot>\n <!-- 1. Dot indicator -->\n @if (dot() || type() === 'dot') {\n <span class=\"w-1.5 h-1.5 rounded-full shrink-0\" [class]=\"dotClasses()\"></span>\n }\n\n <!-- 2. Avatar -->\n @if (avatar() || avatarText() || type() === 'avatar') {\n <ol-avatar\n [src]=\"avatar() || ''\"\n [initials]=\"avatarText() || ''\"\n [size]=\"size() === 'lg' ? 'xs' : '2xs'\"\n class=\"-ms-1 rounded-full shrink-0 flex items-center justify-center overflow-hidden\"\n />\n }\n\n <!-- 3. Loading Spinner -->\n @if (loading() || type() === 'loader') {\n <svg\n class=\"animate-spin w-3 h-3 shrink-0 text-current\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n class=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n stroke-width=\"4\"\n ></circle>\n <path\n class=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n }\n\n <!-- 4. Left icon \u2014 nh\u01B0\u1EDDng ch\u1ED7 cho spinner khi \u0111ang loading -->\n @if (leftIcon() && !loading() && type() !== 'loader') {\n <ol-icon [name]=\"leftIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 5. Main content -->\n <span class=\"truncate min-w-0 max-w-full block\"><ng-content /></span>\n\n <!-- 6. Secondary text separator & content -->\n @if (secondaryText() || type() === 'secondary') {\n <span class=\"w-px h-3 bg-current opacity-30 shrink-0 mx-0.5\"></span>\n <span class=\"opacity-80 font-normal truncate max-w-full\">{{ secondaryText() }}</span>\n }\n\n <!-- 7. Right icon -->\n @if (rightIcon()) {\n <ol-icon [name]=\"rightIcon()!\" size=\"xs\" class=\"shrink-0\" />\n }\n\n <!-- 8. Dismiss / Remove button (Figma Close X) -->\n @if (removable()) {\n <button\n type=\"button\"\n (click)=\"onDismiss($event)\"\n class=\"inline-flex items-center justify-center -me-0.5 p-0.5 rounded-xs hover:bg-black/10 dark:hover:bg-white/10 cursor-pointer transition-colors\"\n aria-label=\"G\u1EE1 nh\u00E3n\"\n >\n <svg\n class=\"w-3 h-3 text-current\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2.5\"\n stroke-linecap=\"round\"\n >\n <path d=\"M18 6L6 18M6 6l12 12\" />\n </svg>\n </button>\n }\n</ng-template>\n" }]
|
|
3506
|
-
}], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], rightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightIcon", required: false }] }], avatar: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatar", required: false }] }], avatarText: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarText", required: false }] }], secondaryText: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondaryText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
|
|
3871
|
+
}], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], theme: [{ type: i0.Input, args: [{ isSignal: true, alias: "theme", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], rightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightIcon", required: false }] }], avatar: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatar", required: false }] }], avatarText: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarText", required: false }] }], secondaryText: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondaryText", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], removable: [{ type: i0.Input, args: [{ isSignal: true, alias: "removable", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
|
|
3507
3872
|
|
|
3508
3873
|
/**
|
|
3509
3874
|
* Figma `ol-banner` (#1146:21254) KHÔNG có trục màu — Type chỉ là bố cục
|
|
@@ -3734,6 +4099,31 @@ class OlBreadcrumbItem {
|
|
|
3734
4099
|
*/
|
|
3735
4100
|
disabled = input(false, /* @ts-ignore */
|
|
3736
4101
|
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
4102
|
+
/**
|
|
4103
|
+
* Cho phép bấm để chuyển trang hay không. Mặc định **bật** — giữ hành vi cũ.
|
|
4104
|
+
*
|
|
4105
|
+
* Tắt khi muốn chặng VẪN hiện như liên kết (chữ xanh khi rê chuột, Tab tới
|
|
4106
|
+
* được, trình đọc màn hình vẫn đọc là liên kết) nhưng KHÔNG tự điều hướng —
|
|
4107
|
+
* ví dụ cần hỏi xác nhận trước khi rời trang đang sửa dở, hoặc nơi dùng tự
|
|
4108
|
+
* xử lý điều hướng qua `(itemClick)`.
|
|
4109
|
+
*
|
|
4110
|
+
* Khác `disabled`: `disabled` bỏ HẲN thẻ `<a>`, chặng thành chữ xám không
|
|
4111
|
+
* bấm được và không Tab tới được. `[clickable]="false"` giữ nguyên vẻ ngoài
|
|
4112
|
+
* và trợ năng của liên kết, chỉ chặn hành vi điều hướng.
|
|
4113
|
+
*
|
|
4114
|
+
* Chặn bằng `preventDefault()` trong `(click)` chứ không bỏ `[routerLink]`:
|
|
4115
|
+
* bỏ binding thì `<a>` mất `href`, trình đọc màn hình không còn coi nó là
|
|
4116
|
+
* liên kết, và người dùng mất luôn khả năng xem đích đến ở thanh trạng thái.
|
|
4117
|
+
*/
|
|
4118
|
+
clickable = input(true, /* @ts-ignore */
|
|
4119
|
+
...(ngDevMode ? [{ debugName: "clickable" }] : /* istanbul ignore next */ []));
|
|
4120
|
+
/**
|
|
4121
|
+
* Phát khi người dùng bấm vào chặng — kể cả khi `[clickable]="false"`.
|
|
4122
|
+
*
|
|
4123
|
+
* Nhờ vậy nơi dùng chặn điều hướng mặc định rồi tự quyết: hỏi xác nhận, lưu
|
|
4124
|
+
* nháp, hay điều hướng bằng đường khác.
|
|
4125
|
+
*/
|
|
4126
|
+
itemClick = output();
|
|
3737
4127
|
parentBreadcrumb = inject(forwardRef(() => OlBreadcrumb), {
|
|
3738
4128
|
optional: true,
|
|
3739
4129
|
});
|
|
@@ -3750,13 +4140,56 @@ class OlBreadcrumbItem {
|
|
|
3750
4140
|
*/
|
|
3751
4141
|
isLink = computed(() => !this.isCurrent() && !this.disabled() && (this.routerLink() !== null || this.href() !== null), /* @ts-ignore */
|
|
3752
4142
|
...(ngDevMode ? [{ debugName: "isLink" }] : /* istanbul ignore next */ []));
|
|
4143
|
+
/**
|
|
4144
|
+
* `href` tĩnh dựng từ `routerLink()`, dùng cho nhánh `clickable=false`.
|
|
4145
|
+
*
|
|
4146
|
+
* Giữ được thuộc tính `href` thật trên thẻ `<a>` nên trợ năng không đổi;
|
|
4147
|
+
* `preventDefault()` trong `onClick` lo phần chặn đi.
|
|
4148
|
+
*/
|
|
4149
|
+
staticHref = computed(() => {
|
|
4150
|
+
const link = this.routerLink();
|
|
4151
|
+
if (link === null)
|
|
4152
|
+
return this.href() ?? '';
|
|
4153
|
+
return Array.isArray(link) ? link.join('/').replace(/\/{2,}/g, '/') : String(link);
|
|
4154
|
+
}, /* @ts-ignore */
|
|
4155
|
+
...(ngDevMode ? [{ debugName: "staticHref" }] : /* istanbul ignore next */ []));
|
|
4156
|
+
/**
|
|
4157
|
+
* Chặn điều hướng khi `[clickable]="false"`, nhưng VẪN phát `itemClick` để
|
|
4158
|
+
* nơi dùng tự xử lý.
|
|
4159
|
+
*
|
|
4160
|
+
* `preventDefault()` chặn cả `routerLink` lẫn `href`: RouterLink bỏ qua sự
|
|
4161
|
+
* kiện đã bị chặn, còn trình duyệt không đi theo `href` nữa.
|
|
4162
|
+
*/
|
|
4163
|
+
onClick(event) {
|
|
4164
|
+
if (!this.clickable())
|
|
4165
|
+
event.preventDefault();
|
|
4166
|
+
this.itemClick.emit(event);
|
|
4167
|
+
}
|
|
3753
4168
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBreadcrumbItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3754
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBreadcrumbItem, isStandalone: true, selector: "ol-breadcrumb-item", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, showSeparator: { classPropertyName: "showSeparator", publicName: "showSeparator", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ng-template #contentTpl>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span>{{ label() }}</span>\n } @else {\n <span><ng-content /></span>\n }\n</ng-template>\n\n<li class=\"inline-flex items-center gap-1.5\">\n @if (isLink()) {\n @if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else {\n <a\n [href]=\"href()\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n }\n } @else {\n <!--\n Th\u1EE9 t\u1EF1 ba nh\u00E1nh l\u00E0 C\u1ED0 \u00DD: `disabled` \u0111\u1EE9ng TR\u01AF\u1EDAC `isCurrent`. M\u1EE5c cu\u1ED1i m\u00E0\n c\u0169ng b\u1ECB kho\u00E1 th\u00EC ph\u1EA3i hi\u1EC7n l\u00E0 kho\u00E1 \u2014 ng\u01B0\u1EE3c l\u1EA1i ng\u01B0\u1EDDi d\u00F9ng th\u1EA5y n\u00F3 y h\u1EC7t\n trang hi\u1EC7n t\u1EA1i v\u00E0 kh\u00F4ng hi\u1EC3u v\u00EC sao b\u1EA5m kh\u00F4ng \u0111\u01B0\u1EE3c.\n\n M\u00E0u theo chu\u1EA9n disabled c\u1EE7a th\u01B0 vi\u1EC7n (`bg-disabled` + `text-fg-disabled`,\n \u0111\u00FAng Figma State=Disabled, xem button.component.ts). KH\u00D4NG d\u00F9ng\n `opacity-50`: m\u1EABu \u0111\u00F3 ch\u1EC9 c\u00F2n 2 component d\u00F9ng, v\u00E0 th\u01B0 vi\u1EC7n \u0111ang c\u00F3 b\u1ED1n\n b\u1EADc opacity song song ch\u01B0a h\u1ED9i t\u1EE5.\n\n
|
|
4169
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBreadcrumbItem, isStandalone: true, selector: "ol-breadcrumb-item", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, showSeparator: { classPropertyName: "showSeparator", publicName: "showSeparator", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, host: { classAttribute: "inline-flex" }, ngImport: i0, template: "<ng-template #contentTpl>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span>{{ label() }}</span>\n } @else {\n <span><ng-content /></span>\n }\n</ng-template>\n\n<li class=\"inline-flex items-center gap-1.5\">\n @if (isLink()) {\n <!--\n `[clickable]=\"false\"` ph\u1EA3i B\u1ECE H\u1EB2N `[routerLink]`, kh\u00F4ng ch\u1EC9\n `preventDefault()` trong `(click)`.\n\n \u0110\u00E3 th\u1EED c\u00E1ch kia tr\u01B0\u1EDBc: RouterLink t\u1EF1 \u0111\u0103ng k\u00FD handler click c\u1EE7a n\u00F3 v\u00E0\n KH\u00D4NG ki\u1EC3m `event.defaultPrevented`, n\u00EAn n\u00F3 \u0111i\u1EC1u h\u01B0\u1EDBng b\u1EA5t k\u1EC3 m\u00ECnh \u0111\u00E3\n ch\u1EB7n hay ch\u01B0a \u2014 spec `breadcrumb-item-clickable.spec.ts` b\u1EAFt \u0111\u00FAng ch\u1ED7 n\u00E0y.\n\n Thay b\u1EB1ng `href` t\u0129nh d\u1EF1ng t\u1EEB ch\u00EDnh `routerLink()`: th\u1EBB v\u1EABn l\u00E0 `<a>` c\u00F3\n `href` th\u1EADt n\u00EAn tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc l\u00E0 li\u00EAn k\u1EBFt, v\u1EABn Tab t\u1EDBi \u0111\u01B0\u1EE3c,\n v\u1EABn xem \u0111\u01B0\u1EE3c \u0111\u00EDch \u0111\u1EBFn \u1EDF thanh tr\u1EA1ng th\u00E1i \u2014 ch\u1EC9 `preventDefault()` ch\u1EB7n\n tr\u00ECnh duy\u1EC7t \u0111i theo. \u0110\u00F3 l\u00E0 kh\u00E1c bi\u1EC7t v\u1EDBi `disabled`, v\u1ED1n b\u1ECF lu\u00F4n th\u1EBB `<a>`.\n -->\n @if (routerLink() && clickable()) {\n <a\n [routerLink]=\"routerLink()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else if (routerLink()) {\n <a\n [href]=\"staticHref()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else {\n <a\n [href]=\"href()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n }\n } @else {\n <!--\n Th\u1EE9 t\u1EF1 ba nh\u00E1nh l\u00E0 C\u1ED0 \u00DD: `disabled` \u0111\u1EE9ng TR\u01AF\u1EDAC `isCurrent`. M\u1EE5c cu\u1ED1i m\u00E0\n c\u0169ng b\u1ECB kho\u00E1 th\u00EC ph\u1EA3i hi\u1EC7n l\u00E0 kho\u00E1 \u2014 ng\u01B0\u1EE3c l\u1EA1i ng\u01B0\u1EDDi d\u00F9ng th\u1EA5y n\u00F3 y h\u1EC7t\n trang hi\u1EC7n t\u1EA1i v\u00E0 kh\u00F4ng hi\u1EC3u v\u00EC sao b\u1EA5m kh\u00F4ng \u0111\u01B0\u1EE3c.\n\n M\u00E0u theo chu\u1EA9n disabled c\u1EE7a th\u01B0 vi\u1EC7n (`bg-disabled` + `text-fg-disabled`,\n \u0111\u00FAng Figma State=Disabled, xem button.component.ts). KH\u00D4NG d\u00F9ng\n `opacity-50`: m\u1EABu \u0111\u00F3 ch\u1EC9 c\u00F2n 2 component d\u00F9ng, v\u00E0 th\u01B0 vi\u1EC7n \u0111ang c\u00F3 b\u1ED1n\n b\u1EADc opacity song song ch\u01B0a h\u1ED9i t\u1EE5.\n\n M\u1EE5c \u0110ANG \u0110\u1EE8NG d\u00F9ng `text-heading` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 L\u1EC6CH FIGMA C\u00D3 CH\u1EE6 \u00DD\n (2026-09-24, theo y\u00EAu c\u1EA7u ng\u01B0\u1EDDi d\u00F9ng \"cho m\u00E0u \u0111\u1EADm h\u01A1n c\u00E1c m\u00E0u kh\u00E1c\").\n\n Figma `ol-breadcrumb-item` #1148:31348 `Status=Active` bind\n `colors/text/text-body-subtle` = #6B7280. \u0110o /breadcrumb \u1EDF ch\u1EBF \u0111\u1ED9 S\u00C1NG\n th\u00EC n\u00F3 cho t\u01B0\u01A1ng ph\u1EA3n **4,83** trong khi c\u00E1c c\u1EA5p cha (`text-body`\n #4A5565) \u0111\u1EA1t **7,56** \u2014 t\u1EE9c trang \u0111ang \u0111\u1EE9ng NH\u1EA0T H\u01A0N c\u1EA5p cha n\u00F3, ng\u01B0\u1EE3c\n ng\u1EEF ngh\u0129a. Nay `text-heading`: s\u00E1ng #101828 (17,75) \u00B7 t\u1ED1i #FFFFFF\n (20,13), \u0111\u1EADm h\u01A1n h\u1EB3n v\u00E0 t\u1EF1 l\u1EADt theo `.dark`.\n\n B\u1EA3n tr\u01B0\u1EDBc ph\u1EA3i vi\u1EBFt `text-body-subtle dark:text-heading` v\u00EC \u1EDF nh\u00E1nh t\u1ED1i\n `--text-body-subtle` b\u1ECB \u0111\u1EB7t B\u1EB0NG H\u1EC6T `--text-body` (c\u00F9ng gray-300) n\u00EAn\n m\u1EE5c cu\u1ED1i v\u00E0 m\u1EE5c th\u01B0\u1EDDng tr\u00F9ng m\u00E0u. D\u00F9ng `text-heading` cho c\u1EA3 hai ch\u1EBF \u0111\u1ED9\n th\u00EC h\u1EBFt c\u1EA7n ngo\u1EA1i l\u1EC7 \u0111\u00F3.\n\n N\u1EE3: c\u1EADp nh\u1EADt Figma `Status=Active` sang `colors/text/text-heading`.\n -->\n <span\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\n [class]=\"\n disabled()\n ? 'font-medium text-fg-disabled cursor-not-allowed'\n : isCurrent()\n ? 'font-medium text-heading'\n : status() === 'hover'\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\n : 'font-medium text-body'\n \"\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </span>\n }\n\n @if (!isCurrent() && showSeparator()) {\n <!--\n D\u1EA5u ng\u0103n d\u00F9ng `text-body-subtle`, KH\u00D4NG `text-fg-disabled`.\n\n `text-fg-disabled` (#99A1AF) tr\u00EAn n\u1EC1n tr\u1EAFng ch\u1EC9 \u0111\u1EA1t **2,60** \u2014 c\u00F3\n `aria-hidden` n\u00EAn kh\u00F4ng vi ph\u1EA1m WCAG, nh\u01B0ng ng\u01B0\u1EDDi nh\u00ECn k\u00E9m kh\u00F3 th\u1EA5y ranh\n gi\u1EDBi gi\u1EEFa c\u00E1c c\u1EA5p. `text-body-subtle` (#6B7280) \u0111\u1EA1t 4,83 m\u00E0 v\u1EABn nh\u1EA1t h\u01A1n\n ch\u1EEF c\u1EE7a ch\u1EB7ng, n\u00EAn v\u1EABn ra \"d\u1EA5u ng\u0103n\" ch\u1EE9 kh\u00F4ng c\u1EA1nh tranh v\u1EDBi n\u1ED9i dung.\n\n KH\u00C1C d\u00F2ng tr\u00EAn: ch\u1EB7ng `disabled` gi\u1EEF `text-fg-disabled` \u2014 \u0111\u00F3 l\u00E0 chu\u1EA9n\n tr\u1EA1ng th\u00E1i kho\u00E1 c\u1EE7a th\u01B0 vi\u1EC7n, v\u00E0 WCAG mi\u1EC5n tr\u1EEB ph\u1EA7n t\u1EED kh\u00F4ng b\u1EA5m \u0111\u01B0\u1EE3c.\n -->\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\n @switch (effectiveSeparator()) {\n @case ('slash') {\n <span class=\"text-xs font-normal\">/</span>\n }\n @case ('arrow') {\n <span class=\"text-xs\">\u2192</span>\n }\n @case ('dot') {\n <span class=\"text-xs\">\u2022</span>\n }\n @case ('chevron') {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n @default {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n }\n </span>\n }\n</li>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3755
4170
|
}
|
|
3756
4171
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBreadcrumbItem, decorators: [{
|
|
3757
4172
|
type: Component,
|
|
3758
|
-
args: [{ selector: 'ol-breadcrumb-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon],
|
|
3759
|
-
|
|
4173
|
+
args: [{ selector: 'ol-breadcrumb-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], host: {
|
|
4174
|
+
/*
|
|
4175
|
+
* Thẻ tuỳ biến KHÔNG khai `display` thì mặc định là `inline`, và bản cũ
|
|
4176
|
+
* không khai `host` nào cả. Hậu quả: host chứa `<li>` inline-flex nên nó
|
|
4177
|
+
* tính chiều cao theo dòng chữ — cộng thêm khoảng trống dưới baseline cho
|
|
4178
|
+
* phần đuôi chữ. Mục CÓ ICON có `<svg>` 16px đẩy dòng cao hơn, nên host
|
|
4179
|
+
* của nó cao 23px trong khi mục không icon chỉ 20px.
|
|
4180
|
+
*
|
|
4181
|
+
* `<ol>` bên ngoài dùng `items-center`, mà căn giữa ba hộp CAO KHÁC NHAU
|
|
4182
|
+
* thì chữ không thể thẳng hàng: đo /breadcrumb 2026-09-24, chữ của mục có
|
|
4183
|
+
* icon nằm cao hơn hai mục sau đúng **1,5px** — thấy rõ bằng mắt trên
|
|
4184
|
+
* thanh "Trang chủ › Bài tập › Học sinh".
|
|
4185
|
+
*
|
|
4186
|
+
* `inline-flex` làm host thành hộp flex, chiều cao bám đúng nội dung
|
|
4187
|
+
* (20px) và không còn khoảng baseline. Ba host bằng nhau thì `items-center`
|
|
4188
|
+
* căn ra cùng một đường.
|
|
4189
|
+
*/
|
|
4190
|
+
class: 'inline-flex',
|
|
4191
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-template #contentTpl>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span>{{ label() }}</span>\n } @else {\n <span><ng-content /></span>\n }\n</ng-template>\n\n<li class=\"inline-flex items-center gap-1.5\">\n @if (isLink()) {\n <!--\n `[clickable]=\"false\"` ph\u1EA3i B\u1ECE H\u1EB2N `[routerLink]`, kh\u00F4ng ch\u1EC9\n `preventDefault()` trong `(click)`.\n\n \u0110\u00E3 th\u1EED c\u00E1ch kia tr\u01B0\u1EDBc: RouterLink t\u1EF1 \u0111\u0103ng k\u00FD handler click c\u1EE7a n\u00F3 v\u00E0\n KH\u00D4NG ki\u1EC3m `event.defaultPrevented`, n\u00EAn n\u00F3 \u0111i\u1EC1u h\u01B0\u1EDBng b\u1EA5t k\u1EC3 m\u00ECnh \u0111\u00E3\n ch\u1EB7n hay ch\u01B0a \u2014 spec `breadcrumb-item-clickable.spec.ts` b\u1EAFt \u0111\u00FAng ch\u1ED7 n\u00E0y.\n\n Thay b\u1EB1ng `href` t\u0129nh d\u1EF1ng t\u1EEB ch\u00EDnh `routerLink()`: th\u1EBB v\u1EABn l\u00E0 `<a>` c\u00F3\n `href` th\u1EADt n\u00EAn tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc l\u00E0 li\u00EAn k\u1EBFt, v\u1EABn Tab t\u1EDBi \u0111\u01B0\u1EE3c,\n v\u1EABn xem \u0111\u01B0\u1EE3c \u0111\u00EDch \u0111\u1EBFn \u1EDF thanh tr\u1EA1ng th\u00E1i \u2014 ch\u1EC9 `preventDefault()` ch\u1EB7n\n tr\u00ECnh duy\u1EC7t \u0111i theo. \u0110\u00F3 l\u00E0 kh\u00E1c bi\u1EC7t v\u1EDBi `disabled`, v\u1ED1n b\u1ECF lu\u00F4n th\u1EBB `<a>`.\n -->\n @if (routerLink() && clickable()) {\n <a\n [routerLink]=\"routerLink()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else if (routerLink()) {\n <a\n [href]=\"staticHref()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n } @else {\n <a\n [href]=\"href()\"\n (click)=\"onClick($event)\"\n class=\"inline-flex items-center gap-1.5 text-sm font-medium text-body hover:text-fg-brand dark:hover:text-fg-brand transition-colors select-none\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </a>\n }\n } @else {\n <!--\n Th\u1EE9 t\u1EF1 ba nh\u00E1nh l\u00E0 C\u1ED0 \u00DD: `disabled` \u0111\u1EE9ng TR\u01AF\u1EDAC `isCurrent`. M\u1EE5c cu\u1ED1i m\u00E0\n c\u0169ng b\u1ECB kho\u00E1 th\u00EC ph\u1EA3i hi\u1EC7n l\u00E0 kho\u00E1 \u2014 ng\u01B0\u1EE3c l\u1EA1i ng\u01B0\u1EDDi d\u00F9ng th\u1EA5y n\u00F3 y h\u1EC7t\n trang hi\u1EC7n t\u1EA1i v\u00E0 kh\u00F4ng hi\u1EC3u v\u00EC sao b\u1EA5m kh\u00F4ng \u0111\u01B0\u1EE3c.\n\n M\u00E0u theo chu\u1EA9n disabled c\u1EE7a th\u01B0 vi\u1EC7n (`bg-disabled` + `text-fg-disabled`,\n \u0111\u00FAng Figma State=Disabled, xem button.component.ts). KH\u00D4NG d\u00F9ng\n `opacity-50`: m\u1EABu \u0111\u00F3 ch\u1EC9 c\u00F2n 2 component d\u00F9ng, v\u00E0 th\u01B0 vi\u1EC7n \u0111ang c\u00F3 b\u1ED1n\n b\u1EADc opacity song song ch\u01B0a h\u1ED9i t\u1EE5.\n\n M\u1EE5c \u0110ANG \u0110\u1EE8NG d\u00F9ng `text-heading` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 L\u1EC6CH FIGMA C\u00D3 CH\u1EE6 \u00DD\n (2026-09-24, theo y\u00EAu c\u1EA7u ng\u01B0\u1EDDi d\u00F9ng \"cho m\u00E0u \u0111\u1EADm h\u01A1n c\u00E1c m\u00E0u kh\u00E1c\").\n\n Figma `ol-breadcrumb-item` #1148:31348 `Status=Active` bind\n `colors/text/text-body-subtle` = #6B7280. \u0110o /breadcrumb \u1EDF ch\u1EBF \u0111\u1ED9 S\u00C1NG\n th\u00EC n\u00F3 cho t\u01B0\u01A1ng ph\u1EA3n **4,83** trong khi c\u00E1c c\u1EA5p cha (`text-body`\n #4A5565) \u0111\u1EA1t **7,56** \u2014 t\u1EE9c trang \u0111ang \u0111\u1EE9ng NH\u1EA0T H\u01A0N c\u1EA5p cha n\u00F3, ng\u01B0\u1EE3c\n ng\u1EEF ngh\u0129a. Nay `text-heading`: s\u00E1ng #101828 (17,75) \u00B7 t\u1ED1i #FFFFFF\n (20,13), \u0111\u1EADm h\u01A1n h\u1EB3n v\u00E0 t\u1EF1 l\u1EADt theo `.dark`.\n\n B\u1EA3n tr\u01B0\u1EDBc ph\u1EA3i vi\u1EBFt `text-body-subtle dark:text-heading` v\u00EC \u1EDF nh\u00E1nh t\u1ED1i\n `--text-body-subtle` b\u1ECB \u0111\u1EB7t B\u1EB0NG H\u1EC6T `--text-body` (c\u00F9ng gray-300) n\u00EAn\n m\u1EE5c cu\u1ED1i v\u00E0 m\u1EE5c th\u01B0\u1EDDng tr\u00F9ng m\u00E0u. D\u00F9ng `text-heading` cho c\u1EA3 hai ch\u1EBF \u0111\u1ED9\n th\u00EC h\u1EBFt c\u1EA7n ngo\u1EA1i l\u1EC7 \u0111\u00F3.\n\n N\u1EE3: c\u1EADp nh\u1EADt Figma `Status=Active` sang `colors/text/text-heading`.\n -->\n <span\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\n [class]=\"\n disabled()\n ? 'font-medium text-fg-disabled cursor-not-allowed'\n : isCurrent()\n ? 'font-medium text-heading'\n : status() === 'hover'\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\n : 'font-medium text-body'\n \"\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n >\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\n </span>\n }\n\n @if (!isCurrent() && showSeparator()) {\n <!--\n D\u1EA5u ng\u0103n d\u00F9ng `text-body-subtle`, KH\u00D4NG `text-fg-disabled`.\n\n `text-fg-disabled` (#99A1AF) tr\u00EAn n\u1EC1n tr\u1EAFng ch\u1EC9 \u0111\u1EA1t **2,60** \u2014 c\u00F3\n `aria-hidden` n\u00EAn kh\u00F4ng vi ph\u1EA1m WCAG, nh\u01B0ng ng\u01B0\u1EDDi nh\u00ECn k\u00E9m kh\u00F3 th\u1EA5y ranh\n gi\u1EDBi gi\u1EEFa c\u00E1c c\u1EA5p. `text-body-subtle` (#6B7280) \u0111\u1EA1t 4,83 m\u00E0 v\u1EABn nh\u1EA1t h\u01A1n\n ch\u1EEF c\u1EE7a ch\u1EB7ng, n\u00EAn v\u1EABn ra \"d\u1EA5u ng\u0103n\" ch\u1EE9 kh\u00F4ng c\u1EA1nh tranh v\u1EDBi n\u1ED9i dung.\n\n KH\u00C1C d\u00F2ng tr\u00EAn: ch\u1EB7ng `disabled` gi\u1EEF `text-fg-disabled` \u2014 \u0111\u00F3 l\u00E0 chu\u1EA9n\n tr\u1EA1ng th\u00E1i kho\u00E1 c\u1EE7a th\u01B0 vi\u1EC7n, v\u00E0 WCAG mi\u1EC5n tr\u1EEB ph\u1EA7n t\u1EED kh\u00F4ng b\u1EA5m \u0111\u01B0\u1EE3c.\n -->\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\n @switch (effectiveSeparator()) {\n @case ('slash') {\n <span class=\"text-xs font-normal\">/</span>\n }\n @case ('arrow') {\n <span class=\"text-xs\">\u2192</span>\n }\n @case ('dot') {\n <span class=\"text-xs\">\u2022</span>\n }\n @case ('chevron') {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n @default {\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\n }\n }\n </span>\n }\n</li>\n" }]
|
|
4192
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], current: [{ type: i0.Input, args: [{ isSignal: true, alias: "current", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], showSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSeparator", required: false }] }], separator: [{ type: i0.Input, args: [{ isSignal: true, alias: "separator", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
|
|
3760
4193
|
|
|
3761
4194
|
class OlDownloadAppButton {
|
|
3762
4195
|
store = input('appstore', /* @ts-ignore */
|
|
@@ -5503,12 +5936,12 @@ class OlDatepicker {
|
|
|
5503
5936
|
const el = this.panel()?.nativeElement;
|
|
5504
5937
|
const raBody = this.appendTo() === 'body';
|
|
5505
5938
|
if (!open || !el) {
|
|
5506
|
-
this.
|
|
5939
|
+
this.detachPanelNow();
|
|
5507
5940
|
return;
|
|
5508
5941
|
}
|
|
5509
5942
|
// Vào ngăn xếp Escape khi mở. Đăng ký ở đây chứ không trong `openPopup`
|
|
5510
5943
|
// để mọi đường mở popup đều đi qua — kể cả mở bằng bàn phím.
|
|
5511
|
-
this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup());
|
|
5944
|
+
this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup(), (target) => this.ownsTarget(target));
|
|
5512
5945
|
// `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
|
|
5513
5946
|
if (raBody && !this.detachPanel) {
|
|
5514
5947
|
this.detachPanel = this.overlayContainer.attach(el);
|
|
@@ -5692,7 +6125,7 @@ class OlDatepicker {
|
|
|
5692
6125
|
this.state.clear();
|
|
5693
6126
|
}
|
|
5694
6127
|
/** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
|
|
5695
|
-
|
|
6128
|
+
detachPanelNow() {
|
|
5696
6129
|
if (this.stopTrackingHeight) {
|
|
5697
6130
|
this.stopTrackingHeight();
|
|
5698
6131
|
this.stopTrackingHeight = null;
|
|
@@ -5705,37 +6138,67 @@ class OlDatepicker {
|
|
|
5705
6138
|
this.releaseEscape?.();
|
|
5706
6139
|
this.releaseEscape = null;
|
|
5707
6140
|
}
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
6141
|
+
/**
|
|
6142
|
+
* Phần tử được bấm có thuộc datepicker này không.
|
|
6143
|
+
*
|
|
6144
|
+
* Ngăn xếp `OlOverlayContainer` gọi hàm này để biết một cú click là "trong"
|
|
6145
|
+
* hay "ngoài". Trước đây mỗi component tự nghe `document:click`, nên một cú
|
|
6146
|
+
* bấm ra ngoài đóng luôn MỌI lớp — mở datepicker trong modal rồi bấm nền là
|
|
6147
|
+
* mất cả hộp thoại đang điền dở. Đo /modal ngày 2026-09-24 xác nhận.
|
|
6148
|
+
*
|
|
6149
|
+
* ## Phải kiểm CẢ PANEL, không chỉ host
|
|
6150
|
+
*
|
|
6151
|
+
* Với `appendTo="body"` panel đã bị `overlayContainer.attach()` chuyển ra
|
|
6152
|
+
* `.ol-overlay-container` — không còn là con của host, nên click vào chính
|
|
6153
|
+
* popup bị coi là click ra ngoài. Đo /modal: bật `appendTo` rồi bấm một
|
|
6154
|
+
* ngày → popup ĐÓNG NGAY, range picker cần hai ngày nên người dùng không
|
|
6155
|
+
* chọn nổi ngày kết thúc.
|
|
6156
|
+
*
|
|
6157
|
+
* Tắt `appendTo` thì popup ở lại đúng — lỗi chỉ xuất hiện khi treo ra ngoài,
|
|
6158
|
+
* tức đúng ca sinh ra để dùng trong modal.
|
|
6159
|
+
*/
|
|
6160
|
+
ownsTarget(target) {
|
|
6161
|
+
if (!(target instanceof Node))
|
|
6162
|
+
return false;
|
|
6163
|
+
const panelEl = this.panel()?.nativeElement;
|
|
6164
|
+
return this.hostEl.nativeElement.contains(target) || (!!panelEl && panelEl.contains(target));
|
|
6165
|
+
}
|
|
6166
|
+
/**
|
|
6167
|
+
* Đóng popup khi component bị huỷ.
|
|
6168
|
+
*
|
|
6169
|
+
* Bắt buộc với `appendTo="body"`: panel treo vào `<body>` chứ không phải cây
|
|
6170
|
+
* DOM của component, nên Angular huỷ component mà panel VẪN Ở LẠI. Chuyển
|
|
6171
|
+
* trang khi đang mở lịch là để lại một popup mồ côi không ai đóng được.
|
|
6172
|
+
*/
|
|
6173
|
+
ngOnDestroy() {
|
|
6174
|
+
this.closePopup();
|
|
5712
6175
|
/**
|
|
5713
|
-
*
|
|
6176
|
+
* Gọi THẲNG `detachPanelNow()`, không trông vào `effect`.
|
|
5714
6177
|
*
|
|
5715
|
-
*
|
|
5716
|
-
*
|
|
5717
|
-
*
|
|
5718
|
-
*
|
|
5719
|
-
*
|
|
6178
|
+
* Việc trả panel về chỗ cũ nằm trong `effect()` ở constructor. Angular
|
|
6179
|
+
* DỪNG mọi effect của component ngay khi huỷ, nên `closePopup()` ở trên
|
|
6180
|
+
* đặt `isOpen = false` mà effect không bao giờ chạy để dọn.
|
|
6181
|
+
*
|
|
6182
|
+
* Với `appendTo="body"` panel đã treo vào `.ol-overlay-container` — không
|
|
6183
|
+
* còn là con của component — nên Angular xoá cây DOM của component mà panel
|
|
6184
|
+
* VẪN Ở LẠI. Nó mất luôn liên kết với ô nhập nên Floating UI trả về toạ độ
|
|
6185
|
+
* `(0, 0)`: popup nhảy lên góc trái trên màn hình và không cách nào đóng
|
|
6186
|
+
* được vì component đã biến mất. Đúng hiện tượng anh Vinh mô tả.
|
|
5720
6187
|
*
|
|
5721
|
-
*
|
|
5722
|
-
*
|
|
6188
|
+
* Chỉ "thỉnh thoảng" vì nó cần popup ĐANG MỞ lúc chuyển trang; đóng trước
|
|
6189
|
+
* rồi mới chuyển thì effect đã kịp dọn.
|
|
5723
6190
|
*/
|
|
5724
|
-
|
|
5725
|
-
const clickedInside = path.includes(this.hostEl.nativeElement) || (!!panelEl && path.includes(panelEl));
|
|
5726
|
-
if (!clickedInside) {
|
|
5727
|
-
this.closePopup();
|
|
5728
|
-
}
|
|
6191
|
+
this.detachPanelNow();
|
|
5729
6192
|
}
|
|
5730
6193
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDatepicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5731
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDatepicker, isStandalone: true, selector: "ol-datepicker", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", dateChange: "dateChange", timestampChange: "timestampChange", isoChange: "isoChange", formattedChange: "formattedChange" }, host: {
|
|
6194
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDatepicker, isStandalone: true, selector: "ol-datepicker", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", dateChange: "dateChange", timestampChange: "timestampChange", isoChange: "isoChange", formattedChange: "formattedChange" }, host: { properties: { "class.block": "!inline()", "class.w-full": "!inline()", "class.inline-block": "inline()", "class.z-dropdown": "isOpen()" }, classAttribute: "relative" }, providers: [
|
|
5732
6195
|
OlDatepickerStateService,
|
|
5733
6196
|
{
|
|
5734
6197
|
provide: NG_VALUE_ACCESSOR,
|
|
5735
6198
|
useExisting: forwardRef(() => OlDatepicker),
|
|
5736
6199
|
multi: true,
|
|
5737
6200
|
},
|
|
5738
|
-
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-
|
|
6201
|
+
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <!-- Input Trigger Field -->\n <div class=\"relative w-full\">\n <!-- Calendar Icon Prefix -->\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n\n <input\n [id]=\"inputId()\"\n type=\"text\"\n [value]=\"inputValue()\"\n (input)=\"onInputChange($any($event.target).value)\"\n (click)=\"togglePopup($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [class]=\"inputSizeClasses()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:ring-brand-950 shadow-xs transition-colors cursor-pointer\"\n />\n\n <!-- Clear Button or Arrow Dropdown -->\n <div class=\"absolute inset-y-0 inset-e-0 flex items-center pe-3 gap-1\">\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearInput($event)\"\n class=\"text-body-subtle hover:text-heading p-0.5 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <!--\n `rounded-base` PH\u1EA2I kh\u1EDBp bo g\u00F3c c\u1EE7a `ol-datepicker-popup` b\u00EAn trong.\n\n Khung n\u00E0y mang `shadow-2xl` nh\u01B0ng tr\u01B0\u1EDBc \u0111\u00E2y kh\u00F4ng c\u00F3 `rounded` n\u00E0o,\n n\u00EAn b\u00F3ng \u0111\u1ED5 v\u1EBD theo h\u00ECnh CH\u1EEE NH\u1EACT VU\u00D4NG trong khi popup b\u00EAn trong bo\n 12px \u2014 hai g\u00F3c d\u01B0\u1EDBi l\u1ED9 ra hai v\u1EC7t vu\u00F4ng x\u00E1m. \u0110o ng\u00E0y 2026-09-24: hai\n khung c\u00F9ng 294\u00D7374, khung ngo\u00E0i `radius: 0px`, khung trong\n `radius: 12px`.\n -->\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n </div>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlDatepickerPopup, selector: "ol-datepicker-popup", inputs: ["showFooter", "dualMonth", "presets", "inline"], outputs: ["valueChange", "clearClick", "applyClick", "cancelClick"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5739
6202
|
}
|
|
5740
6203
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDatepicker, decorators: [{
|
|
5741
6204
|
type: Component,
|
|
@@ -5775,11 +6238,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
5775
6238
|
'[class.w-full]': '!inline()',
|
|
5776
6239
|
'[class.inline-block]': 'inline()',
|
|
5777
6240
|
'[class.z-dropdown]': 'isOpen()',
|
|
5778
|
-
}, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-
|
|
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
|
-
}] } });
|
|
6241
|
+
}, template: "<!--\n KH\u00D4NG ghim `max-w-sm`: `ol-input`, `ol-select`, `ol-textarea` \u0111\u1EC1u \u0111\u1EC3 n\u01A1i g\u1ECDi\n quy\u1EBFt \u0111\u1ECBnh b\u1EC1 r\u1ED9ng, ch\u1EC9 `ol-datepicker` t\u1EF1 gi\u1EDBi h\u1EA1n 384px. \u0110\u1EB7t b\u1ED1n \u00F4 c\u1EA1nh\n nhau trong m\u1ED9t modal th\u00EC \u00F4 ng\u00E0y h\u1EB9p h\u01A1n h\u1EB3n \u2014 \u0111o /modal: ba \u00F4 kia 646px, \u00F4\n ng\u00E0y 345px. Mu\u1ED1n h\u1EB9p th\u00EC b\u1ECDc ngo\u00E0i, nh\u01B0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD.\n-->\n<div class=\"relative inline-block\" [class.w-full]=\"!inline()\" [class.z-dropdown]=\"isOpen()\">\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n <!-- Label -->\n @if (label()) {\n <label [for]=\"inputId()\" class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <!-- Input Trigger Field -->\n <div class=\"relative w-full\">\n <!-- Calendar Icon Prefix -->\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n\n <input\n [id]=\"inputId()\"\n type=\"text\"\n [value]=\"inputValue()\"\n (input)=\"onInputChange($any($event.target).value)\"\n (click)=\"togglePopup($event)\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled()\"\n [class]=\"inputSizeClasses()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:ring-brand-950 shadow-xs transition-colors cursor-pointer\"\n />\n\n <!-- Clear Button or Arrow Dropdown -->\n <div class=\"absolute inset-y-0 inset-e-0 flex items-center pe-3 gap-1\">\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearInput($event)\"\n class=\"text-body-subtle hover:text-heading p-0.5 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <!--\n `rounded-base` PH\u1EA2I kh\u1EDBp bo g\u00F3c c\u1EE7a `ol-datepicker-popup` b\u00EAn trong.\n\n Khung n\u00E0y mang `shadow-2xl` nh\u01B0ng tr\u01B0\u1EDBc \u0111\u00E2y kh\u00F4ng c\u00F3 `rounded` n\u00E0o,\n n\u00EAn b\u00F3ng \u0111\u1ED5 v\u1EBD theo h\u00ECnh CH\u1EEE NH\u1EACT VU\u00D4NG trong khi popup b\u00EAn trong bo\n 12px \u2014 hai g\u00F3c d\u01B0\u1EDBi l\u1ED9 ra hai v\u1EC7t vu\u00F4ng x\u00E1m. \u0110o ng\u00E0y 2026-09-24: hai\n khung c\u00F9ng 294\u00D7374, khung ngo\u00E0i `radius: 0px`, khung trong\n `radius: 12px`.\n -->\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n />\n </div>\n }\n </div>\n }\n</div>\n" }]
|
|
6242
|
+
}], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], daysDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "daysDisabled", required: false }] }], datesDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesDisabled", required: false }] }], datesEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesEnabled", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooter", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], dateChange: [{ type: i0.Output, args: ["dateChange"] }], timestampChange: [{ type: i0.Output, args: ["timestampChange"] }], isoChange: [{ type: i0.Output, args: ["isoChange"] }], formattedChange: [{ type: i0.Output, args: ["formattedChange"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
|
|
5783
6243
|
|
|
5784
6244
|
class OlDateRangePicker {
|
|
5785
6245
|
state = inject(OlDatepickerStateService);
|
|
@@ -5896,11 +6356,11 @@ class OlDateRangePicker {
|
|
|
5896
6356
|
const el = this.panel()?.nativeElement;
|
|
5897
6357
|
const raBody = this.appendTo() === 'body';
|
|
5898
6358
|
if (!open || !el) {
|
|
5899
|
-
this.
|
|
6359
|
+
this.detachPanelNow();
|
|
5900
6360
|
return;
|
|
5901
6361
|
}
|
|
5902
6362
|
// Vào ngăn xếp Escape — chỉ lớp trên cùng đóng.
|
|
5903
|
-
this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup());
|
|
6363
|
+
this.releaseEscape ??= this.overlayContainer.register(() => this.closePopup(), (target) => this.ownsTarget(target));
|
|
5904
6364
|
// `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
|
|
5905
6365
|
if (raBody && !this.detachPanel) {
|
|
5906
6366
|
this.detachPanel = this.overlayContainer.attach(el);
|
|
@@ -6146,7 +6606,7 @@ class OlDateRangePicker {
|
|
|
6146
6606
|
this.state.clear();
|
|
6147
6607
|
}
|
|
6148
6608
|
/** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
|
|
6149
|
-
|
|
6609
|
+
detachPanelNow() {
|
|
6150
6610
|
if (this.stopTrackingHeight) {
|
|
6151
6611
|
this.stopTrackingHeight();
|
|
6152
6612
|
this.stopTrackingHeight = null;
|
|
@@ -6159,37 +6619,58 @@ class OlDateRangePicker {
|
|
|
6159
6619
|
this.releaseEscape?.();
|
|
6160
6620
|
this.releaseEscape = null;
|
|
6161
6621
|
}
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6622
|
+
/**
|
|
6623
|
+
* Phần tử được bấm có thuộc bộ chọn khoảng này không.
|
|
6624
|
+
*
|
|
6625
|
+
* Xem chú thích cùng tên ở `ol-datepicker` — hai component dùng chung cơ
|
|
6626
|
+
* chế. Range picker còn nhạy hơn: nó cần HAI cú bấm (ngày đầu, ngày cuối),
|
|
6627
|
+
* nên popup đóng sớm là người dùng không chọn nổi khoảng.
|
|
6628
|
+
*
|
|
6629
|
+
* Phải kiểm cả panel vì với `appendTo="body"` nó đã bị chuyển ra
|
|
6630
|
+
* `.ol-overlay-container`, không còn là con của host.
|
|
6631
|
+
*/
|
|
6632
|
+
ownsTarget(target) {
|
|
6633
|
+
if (!(target instanceof Node))
|
|
6634
|
+
return false;
|
|
6635
|
+
const panelEl = this.panel()?.nativeElement;
|
|
6636
|
+
return this.hostEl.nativeElement.contains(target) || (!!panelEl && panelEl.contains(target));
|
|
6637
|
+
}
|
|
6638
|
+
/**
|
|
6639
|
+
* Đóng popup khi component bị huỷ.
|
|
6640
|
+
*
|
|
6641
|
+
* Bắt buộc với `appendTo="body"`: panel treo vào `<body>` nên Angular huỷ
|
|
6642
|
+
* component mà panel VẪN Ở LẠI. Chuyển trang khi đang mở lịch là để lại một
|
|
6643
|
+
* popup mồ côi không ai đóng được.
|
|
6644
|
+
*/
|
|
6645
|
+
ngOnDestroy() {
|
|
6646
|
+
this.closePopup();
|
|
6166
6647
|
/**
|
|
6167
|
-
*
|
|
6648
|
+
* Gọi THẲNG `detachPanelNow()`, không trông vào `effect`.
|
|
6168
6649
|
*
|
|
6169
|
-
*
|
|
6170
|
-
*
|
|
6171
|
-
*
|
|
6172
|
-
*
|
|
6173
|
-
*
|
|
6650
|
+
* Việc trả panel về chỗ cũ nằm trong `effect()` ở constructor. Angular
|
|
6651
|
+
* DỪNG mọi effect của component ngay khi huỷ, nên `closePopup()` ở trên
|
|
6652
|
+
* đặt `isOpen = false` mà effect không bao giờ chạy để dọn.
|
|
6653
|
+
*
|
|
6654
|
+
* Với `appendTo="body"` panel đã treo vào `.ol-overlay-container` — không
|
|
6655
|
+
* còn là con của component — nên Angular xoá cây DOM của component mà panel
|
|
6656
|
+
* VẪN Ở LẠI. Nó mất luôn liên kết với ô nhập nên Floating UI trả về toạ độ
|
|
6657
|
+
* `(0, 0)`: popup nhảy lên góc trái trên màn hình và không cách nào đóng
|
|
6658
|
+
* được vì component đã biến mất. Đúng hiện tượng anh Vinh mô tả.
|
|
6174
6659
|
*
|
|
6175
|
-
*
|
|
6176
|
-
*
|
|
6660
|
+
* Chỉ "thỉnh thoảng" vì nó cần popup ĐANG MỞ lúc chuyển trang; đóng trước
|
|
6661
|
+
* rồi mới chuyển thì effect đã kịp dọn.
|
|
6177
6662
|
*/
|
|
6178
|
-
|
|
6179
|
-
const clickedInside = path.includes(this.hostEl.nativeElement) || (!!panelEl && path.includes(panelEl));
|
|
6180
|
-
if (!clickedInside) {
|
|
6181
|
-
this.closePopup();
|
|
6182
|
-
}
|
|
6663
|
+
this.detachPanelNow();
|
|
6183
6664
|
}
|
|
6184
6665
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDateRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6185
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDateRangePicker, isStandalone: true, selector: "ol-date-range-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, fromLabel: { classPropertyName: "fromLabel", publicName: "fromLabel", isSignal: true, isRequired: false, transformFunction: null }, toLabel: { classPropertyName: "toLabel", publicName: "toLabel", isSignal: true, isRequired: false, transformFunction: null }, showInfoIcon: { classPropertyName: "showInfoIcon", publicName: "showInfoIcon", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, dualMonth: { classPropertyName: "dualMonth", publicName: "dualMonth", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", rangeChange: "rangeChange" }, host: {
|
|
6666
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDateRangePicker, isStandalone: true, selector: "ol-date-range-picker", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, fromLabel: { classPropertyName: "fromLabel", publicName: "fromLabel", isSignal: true, isRequired: false, transformFunction: null }, toLabel: { classPropertyName: "toLabel", publicName: "toLabel", isSignal: true, isRequired: false, transformFunction: null }, showInfoIcon: { classPropertyName: "showInfoIcon", publicName: "showInfoIcon", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, dualMonth: { classPropertyName: "dualMonth", publicName: "dualMonth", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, daysDisabled: { classPropertyName: "daysDisabled", publicName: "daysDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesDisabled: { classPropertyName: "datesDisabled", publicName: "datesDisabled", isSignal: true, isRequired: false, transformFunction: null }, datesEnabled: { classPropertyName: "datesEnabled", publicName: "datesEnabled", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, showFooter: { classPropertyName: "showFooter", publicName: "showFooter", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, inline: { classPropertyName: "inline", publicName: "inline", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", rangeChange: "rangeChange" }, host: { properties: { "class.z-dropdown": "isOpen()" }, classAttribute: "relative inline-block" }, providers: [
|
|
6186
6667
|
OlDatepickerStateService,
|
|
6187
6668
|
{
|
|
6188
6669
|
provide: NG_VALUE_ACCESSOR,
|
|
6189
6670
|
useExisting: forwardRef(() => OlDateRangePicker),
|
|
6190
6671
|
multi: true,
|
|
6191
6672
|
},
|
|
6192
|
-
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-
|
|
6673
|
+
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ fromLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-3\"\n [class]=\"inputSizeClasses()\"\n />\n </div>\n </div>\n\n <!-- To Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ toLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-8\"\n [class]=\"inputSizeClasses()\"\n />\n\n <!-- Clear button on the right input -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"absolute inset-y-0 inset-e-0 flex items-center pe-2.5 text-body-subtle hover:text-heading transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n </div>\n } @else {\n <!-- Joined Single Container Variant -->\n @if (label()) {\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <div\n class=\"flex items-center gap-2 bg-neutral-secondary-medium border border-default-medium rounded-md p-1 shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle focus-within:border-brand dark:focus-within:border-brand-subtle transition-all cursor-pointer\"\n [class]=\"joinedWrapperClasses()\"\n (click)=\"togglePopup($event)\"\n >\n <!-- Start Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Separator Arrow -->\n <span class=\"text-fg-disabled text-xs font-medium px-1 select-none\"> \u0111\u1EBFn </span>\n\n <!-- End Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Clear Button -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"text-body-subtle hover:text-heading p-1 mr-1 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n }\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl w-max max-w-[calc(100vw-2rem)]\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n </div>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlDatepickerPopup, selector: "ol-datepicker-popup", inputs: ["showFooter", "dualMonth", "presets", "inline"], outputs: ["valueChange", "clearClick", "applyClick", "cancelClick"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6193
6674
|
}
|
|
6194
6675
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDateRangePicker, decorators: [{
|
|
6195
6676
|
type: Component,
|
|
@@ -6213,11 +6694,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
6213
6694
|
*/
|
|
6214
6695
|
class: 'relative inline-block',
|
|
6215
6696
|
'[class.z-dropdown]': 'isOpen()',
|
|
6216
|
-
}, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-
|
|
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
|
-
}] } });
|
|
6697
|
+
}, template: "<div\n class=\"relative inline-block\"\n [class.w-full]=\"!inline()\"\n [class.max-w-xl]=\"!inline()\"\n [class.z-dropdown]=\"isOpen()\"\n>\n @if (inline()) {\n <!-- Inline Calendar Mode -->\n <ol-datepicker-popup\n [inline]=\"true\"\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n } @else {\n <!-- Input Trigger & Dropdown Mode -->\n <div class=\"flex flex-col gap-1.5 w-full\">\n @if (variant() === 'dual-inputs') {\n <!-- Figma Variant: 2 Separate Inputs (From & To) -->\n <div\n class=\"grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 w-full\"\n (click)=\"togglePopup($event)\"\n >\n <!-- From Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ fromLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-3\"\n [class]=\"inputSizeClasses()\"\n />\n </div>\n </div>\n\n <!-- To Input -->\n <div class=\"flex flex-col gap-1.5\">\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading select-none\">\n <span>{{ toLabel() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (showInfoIcon()) {\n <ol-icon name=\"question-circle\" size=\"xs\" />\n }\n </label>\n\n <div class=\"relative w-full\">\n <div\n class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3.5\"\n >\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-neutral-secondary-medium border border-default-medium text-heading placeholder:text-body-subtle focus:outline-hidden focus:ring-2 focus:ring-brand-subtle focus:border-brand dark:focus:border-brand-subtle shadow-xs cursor-pointer transition-colors ps-10 pe-8\"\n [class]=\"inputSizeClasses()\"\n />\n\n <!-- Clear button on the right input -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"absolute inset-y-0 inset-e-0 flex items-center pe-2.5 text-body-subtle hover:text-heading transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n </div>\n </div>\n } @else {\n <!-- Joined Single Container Variant -->\n @if (label()) {\n <label class=\"flex items-center gap-1 text-sm font-medium text-heading\">\n <span>{{ label() }}</span>\n @if (required()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n }\n\n <div\n class=\"flex items-center gap-2 bg-neutral-secondary-medium border border-default-medium rounded-md p-1 shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle focus-within:border-brand dark:focus-within:border-brand-subtle transition-all cursor-pointer\"\n [class]=\"joinedWrapperClasses()\"\n (click)=\"togglePopup($event)\"\n >\n <!-- Start Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"startInputValue()\"\n (input)=\"onStartInputChange($any($event.target).value)\"\n [placeholder]=\"startPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Separator Arrow -->\n <span class=\"text-fg-disabled text-xs font-medium px-1 select-none\"> \u0111\u1EBFn </span>\n\n <!-- End Date Input -->\n <div class=\"relative flex-1 flex items-center\">\n <div class=\"absolute inset-y-0 inset-s-0 flex items-center pointer-events-none ps-3\">\n <ol-icon name=\"calendar\" [size]=\"iconSize()\" />\n </div>\n <input\n type=\"text\"\n [value]=\"endInputValue()\"\n (input)=\"onEndInputChange($any($event.target).value)\"\n [placeholder]=\"endPlaceholder()\"\n [disabled]=\"disabled()\"\n class=\"block w-full bg-transparent text-heading placeholder:text-body-subtle ps-9 pe-2 focus:outline-hidden cursor-pointer\"\n [class]=\"joinedInputClasses()\"\n />\n </div>\n\n <!-- Clear Button -->\n @if (clearable() && hasContent() && !disabled()) {\n <button\n type=\"button\"\n (click)=\"clearRange($event)\"\n class=\"text-body-subtle hover:text-heading p-1 mr-1 rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer\"\n aria-label=\"Xo\u00E1 kho\u1EA3ng ng\u00E0y \u0111\u00E3 ch\u1ECDn\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </div>\n }\n\n <!-- Helper Text -->\n @if (helperText()) {\n <p class=\"text-xs text-body-subtle\">\n {{ helperText() }}\n </p>\n }\n\n <!-- Dropdown Popup -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown animate-in fade-in zoom-in-95 duration-100 rounded-base shadow-2xl w-max max-w-[calc(100vw-2rem)]\"\n >\n <ol-datepicker-popup\n [showFooter]=\"showFooter()\"\n [dualMonth]=\"dualMonth()\"\n [presets]=\"presets()\"\n (valueChange)=\"onValueSelected($event)\"\n (clearClick)=\"onPopupClear()\"\n (applyClick)=\"onPopupApply()\"\n (cancelClick)=\"onPopupCancel()\"\n />\n </div>\n }\n </div>\n }\n</div>\n" }]
|
|
6698
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], startPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPlaceholder", required: false }] }], endPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "endPlaceholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], fromLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fromLabel", required: false }] }], toLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toLabel", required: false }] }], showInfoIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showInfoIcon", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], dualMonth: [{ type: i0.Input, args: [{ isSignal: true, alias: "dualMonth", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], daysDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "daysDisabled", required: false }] }], datesDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesDisabled", required: false }] }], datesEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "datesEnabled", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showFooter: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFooter", required: false }] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }] } });
|
|
6221
6699
|
|
|
6222
6700
|
class OlDatepickerDirective {
|
|
6223
6701
|
el = inject((ElementRef));
|
|
@@ -7563,10 +8041,10 @@ class OlInput {
|
|
|
7563
8041
|
* lệch 2px so với thiết kế.
|
|
7564
8042
|
*/
|
|
7565
8043
|
paddingClasses = computed(() => {
|
|
7566
|
-
const
|
|
7567
|
-
const
|
|
8044
|
+
const hasLeadingIcon = this.leftIcon() && !this.addOnIcon();
|
|
8045
|
+
const hasTrailingIcon = this.rightIcon() || this.state() === 'error' || this.state() === 'success';
|
|
7568
8046
|
const classes = [];
|
|
7569
|
-
if (!
|
|
8047
|
+
if (!hasLeadingIcon) {
|
|
7570
8048
|
switch (this.size()) {
|
|
7571
8049
|
case 'sm':
|
|
7572
8050
|
classes.push('px-2.5');
|
|
@@ -7584,7 +8062,7 @@ class OlInput {
|
|
|
7584
8062
|
else {
|
|
7585
8063
|
classes.push('ps-2.5');
|
|
7586
8064
|
}
|
|
7587
|
-
if (
|
|
8065
|
+
if (hasTrailingIcon)
|
|
7588
8066
|
classes.push('pe-2.5');
|
|
7589
8067
|
return classes.join(' ');
|
|
7590
8068
|
}, /* @ts-ignore */
|
|
@@ -7610,7 +8088,7 @@ class OlInput {
|
|
|
7610
8088
|
this.disabled.set(isDisabled);
|
|
7611
8089
|
}
|
|
7612
8090
|
/** Gọi từ template mỗi khi giá trị đổi, để form biết. */
|
|
7613
|
-
|
|
8091
|
+
emitChange(val) {
|
|
7614
8092
|
this.value.set(val);
|
|
7615
8093
|
this.onChange(val);
|
|
7616
8094
|
}
|
|
@@ -7638,7 +8116,7 @@ class OlInput {
|
|
|
7638
8116
|
useExisting: forwardRef(() => OlInput),
|
|
7639
8117
|
multi: true,
|
|
7640
8118
|
},
|
|
7641
|
-
], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"
|
|
8119
|
+
], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\n [class]=\"paddingClasses()\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n />\n\n <!--\n N\u00DAT XO\u00C1 \u2014 \u0111\u1EB7t TR\u01AF\u1EDAC icon tr\u1EA1ng th\u00E1i.\n\n Icon l\u1ED7i/th\u00E0nh c\u00F4ng ch\u1EC9 \u0111\u1EC3 \u0111\u1ECDc, n\u00FAt xo\u00E1 th\u00EC b\u1EA5m \u0111\u01B0\u1EE3c; \u0111\u1EB7t n\u00FAt sau th\u00EC \u1EDF\n tr\u1EA1ng th\u00E1i l\u1ED7i n\u00F3 bi\u1EBFn m\u1EA5t, \u0111\u00FAng l\u00FAc ng\u01B0\u1EDDi d\u00F9ng mu\u1ED1n xo\u00E1 \u0111i g\u00F5 l\u1EA1i.\n\n \u1EA8n khi `readonly` ho\u1EB7c `disabled`: \u00F4 kh\u00F4ng s\u1EEDa \u0111\u01B0\u1EE3c th\u00EC n\u00FAt xo\u00E1 l\u00E0 m\u1EDDi\n b\u1EA5m v\u00E0o ch\u1ED7 kh\u00F4ng l\u00E0m g\u00EC.\n -->\n @if (clearable() && value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n class=\"flex shrink-0 cursor-pointer items-center rounded-full p-0.5 pe-3 text-body-subtle transition-colors hover:text-heading\"\n [attr.aria-label]=\"clearAriaLabel()\"\n (click)=\"clearValue($event)\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n\n <!-- Right Icon / Status Icon -->\n @if (state() === 'error') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"sm\" />\n </div>\n } @else if (state() === 'success') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-success\">\n <ol-icon name=\"check-circle\" size=\"sm\" />\n </div>\n } @else if (rightIcon()) {\n <div class=\"flex items-center pe-3 text-body-subtle shrink-0\">\n <ng-content select=\"[rightIcon]\" />\n </div>\n }\n\n <!-- Suffix Add-on Text -->\n @if (suffixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-s border-default-medium select-none shrink-0\"\n >\n {{ suffixText() }}\n </span>\n }\n </div>\n\n <!-- Helper Text & Right Helper Action -->\n @if (helperText() || rightHelperText()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [id]=\"helperId()\"\n [attr.role]=\"state() === 'error' ? 'alert' : null\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger]=\"state() === 'error'\"\n [class.text-fg-success]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (rightHelperText()) {\n <span class=\"text-xs text-fg-brand hover:underline cursor-pointer ms-auto font-medium\">\n {{ rightHelperText() }}\n </span>\n }\n <ng-content select=\"[rightHelperAction]\" />\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7642
8120
|
}
|
|
7643
8121
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlInput, decorators: [{
|
|
7644
8122
|
type: Component,
|
|
@@ -7654,7 +8132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
7654
8132
|
useExisting: forwardRef(() => OlInput),
|
|
7655
8133
|
multi: true,
|
|
7656
8134
|
},
|
|
7657
|
-
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"
|
|
8135
|
+
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n @if (labelIcon()) {\n <span\n class=\"text-body-subtle hover:text-heading cursor-help inline-flex items-center\"\n [title]=\"labelTooltip() || 'Tr\u1EE3 gi\u00FAp'\"\n >\n <ol-icon name=\"question-circle\" size=\"xs\" />\n </span>\n }\n </label>\n </div>\n }\n\n <!-- Main Unified Input Container (S\u1EED d\u1EE5ng Semantic Token Classes 100%) -->\n <div\n class=\"relative flex items-center w-full gap-2 border shadow-xs overflow-hidden rounded-md\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.focus-within:ring-2]=\"\n (!disabled() && !readonly()) ||\n (state() === 'success' && !disabled()) ||\n (state() === 'error' && !disabled())\n \"\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-disabled]=\"readonly() || disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.opacity-60]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Add-on Icon Box -->\n @if (addOnIcon()) {\n <span\n class=\"inline-flex items-center justify-center px-3.5 h-full text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n <ng-content select=\"[addOnIcon], [leftIcon]\" />\n </span>\n }\n\n <!-- Prefix Add-on Text -->\n @if (prefixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-e border-default-medium select-none shrink-0\"\n >\n {{ prefixText() }}\n </span>\n }\n\n <!-- Left Icon (Inside Input) -->\n @if (leftIcon() && !addOnIcon()) {\n <div class=\"flex items-center ps-3.5 text-body-subtle pointer-events-none shrink-0\">\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!-- Real HTML Input -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [type]=\"type()\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"placeholder()\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\n [class]=\"paddingClasses()\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.cursor-default]=\"readonly()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n />\n\n <!--\n N\u00DAT XO\u00C1 \u2014 \u0111\u1EB7t TR\u01AF\u1EDAC icon tr\u1EA1ng th\u00E1i.\n\n Icon l\u1ED7i/th\u00E0nh c\u00F4ng ch\u1EC9 \u0111\u1EC3 \u0111\u1ECDc, n\u00FAt xo\u00E1 th\u00EC b\u1EA5m \u0111\u01B0\u1EE3c; \u0111\u1EB7t n\u00FAt sau th\u00EC \u1EDF\n tr\u1EA1ng th\u00E1i l\u1ED7i n\u00F3 bi\u1EBFn m\u1EA5t, \u0111\u00FAng l\u00FAc ng\u01B0\u1EDDi d\u00F9ng mu\u1ED1n xo\u00E1 \u0111i g\u00F5 l\u1EA1i.\n\n \u1EA8n khi `readonly` ho\u1EB7c `disabled`: \u00F4 kh\u00F4ng s\u1EEDa \u0111\u01B0\u1EE3c th\u00EC n\u00FAt xo\u00E1 l\u00E0 m\u1EDDi\n b\u1EA5m v\u00E0o ch\u1ED7 kh\u00F4ng l\u00E0m g\u00EC.\n -->\n @if (clearable() && value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n class=\"flex shrink-0 cursor-pointer items-center rounded-full p-0.5 pe-3 text-body-subtle transition-colors hover:text-heading\"\n [attr.aria-label]=\"clearAriaLabel()\"\n (click)=\"clearValue($event)\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n\n <!-- Right Icon / Status Icon -->\n @if (state() === 'error') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"sm\" />\n </div>\n } @else if (state() === 'success') {\n <div class=\"flex items-center pe-3 shrink-0 text-fg-success\">\n <ol-icon name=\"check-circle\" size=\"sm\" />\n </div>\n } @else if (rightIcon()) {\n <div class=\"flex items-center pe-3 text-body-subtle shrink-0\">\n <ng-content select=\"[rightIcon]\" />\n </div>\n }\n\n <!-- Suffix Add-on Text -->\n @if (suffixText()) {\n <span\n class=\"inline-flex items-center px-3.5 h-full text-sm font-normal text-body bg-neutral-tertiary border-s border-default-medium select-none shrink-0\"\n >\n {{ suffixText() }}\n </span>\n }\n </div>\n\n <!-- Helper Text & Right Helper Action -->\n @if (helperText() || rightHelperText()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [id]=\"helperId()\"\n [attr.role]=\"state() === 'error' ? 'alert' : null\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger]=\"state() === 'error'\"\n [class.text-fg-success]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (rightHelperText()) {\n <span class=\"text-xs text-fg-brand hover:underline cursor-pointer ms-auto font-medium\">\n {{ rightHelperText() }}\n </span>\n }\n <ng-content select=\"[rightHelperAction]\" />\n </div>\n }\n</div>\n" }]
|
|
7658
8136
|
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], labelIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelIcon", required: false }] }], labelTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelTooltip", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], prefixText: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixText", required: false }] }], suffixText: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffixText", required: false }] }], addOnIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addOnIcon", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], rightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightIcon", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearAriaLabel", required: false }] }], cleared: [{ type: i0.Output, args: ["cleared"] }], rightHelperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "rightHelperText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }] } });
|
|
7659
8137
|
|
|
7660
8138
|
let nextTextareaId = 0;
|
|
@@ -7738,7 +8216,7 @@ class OlTextarea {
|
|
|
7738
8216
|
this.disabled.set(isDisabled);
|
|
7739
8217
|
}
|
|
7740
8218
|
/** Gọi từ template mỗi khi giá trị đổi, để form biết. */
|
|
7741
|
-
|
|
8219
|
+
emitChange(val) {
|
|
7742
8220
|
this.value.set(val);
|
|
7743
8221
|
this.onChange(val);
|
|
7744
8222
|
}
|
|
@@ -7753,7 +8231,7 @@ class OlTextarea {
|
|
|
7753
8231
|
useExisting: forwardRef(() => OlTextarea),
|
|
7754
8232
|
multi: true,
|
|
7755
8233
|
},
|
|
7756
|
-
], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"
|
|
8234
|
+
], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm rounded-base outline-none border font-normal shadow-xs\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n ></textarea>\n </div>\n } @else {\n <!--\n Figma Ready-to-use Comment / Reply Textarea Card (`Type=Chatroom`).\n\n `rounded-md` (12px) ch\u1EE9 kh\u00F4ng `rounded-lg` (16px): \u0111\u1ECDc Figma\n `ol-textarea` bi\u1EBFn th\u1EC3 `Type=Chatroom, State=Initial` ra **radius 12** \u2014\n c\u00F9ng b\u1EADc v\u1EDBi `ol-input` v\u00E0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD. \u0110o trang /forms\n 2026-09-17: `ol-textarea` l\u00E0 component DUY NH\u1EA4T trong h\u1ECD c\u00F3 hai gi\u00E1 tr\u1ECB\n bo g\u00F3c (12px v\u00E0 16px), l\u1EC7ch v\u1EDBi 9 component c\u00F2n l\u1EA1i.\n -->\n <div\n class=\"border border-default-medium rounded-md bg-neutral-primary overflow-hidden shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle dark:focus-within:ring-brand-950 focus-within:border-brand\"\n >\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() || 'Vi\u1EBFt b\u00ECnh lu\u1EADn ho\u1EB7c ph\u1EA3n h\u1ED3i...'\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm text-body bg-transparent border-0 outline-none resize-none placeholder:text-body-subtle\"\n ></textarea>\n <!-- Bottom Action Bar -->\n <div\n class=\"flex items-center justify-between px-3 py-2 border-t border-default-medium bg-neutral-secondary-medium\"\n >\n <div class=\"flex items-center gap-1 text-body-subtle\">\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"\u0110\u00EDnh k\u00E8m t\u1EC7p\"\n >\n <ol-icon name=\"link\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Ch\u00E8n v\u1ECB tr\u00ED/h\u00ECnh \u1EA3nh\"\n >\n <ol-icon name=\"photo\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Bi\u1EC3u t\u01B0\u1EE3ng c\u1EA3m x\u00FAc\"\n >\n <ol-icon name=\"mynaui:smile\" size=\"sm\" />\n </button>\n </div>\n <button\n type=\"button\"\n (click)=\"onSubmit()\"\n [disabled]=\"disabled() || !value().trim()\"\n class=\"inline-flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong disabled:opacity-50 disabled:cursor-not-allowed rounded-lg shadow-xs\"\n >\n <span>G\u1EEDi</span>\n <ol-icon name=\"arrow-right\" size=\"xs\" />\n </button>\n </div>\n </div>\n }\n @if (helperText() || showCharCount()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (showCharCount()) {\n <span class=\"text-body-subtle ms-auto\">\n {{ value().length }}{{ maxLength() ? '/' + maxLength() : '' }} k\u00FD t\u1EF1\n </span>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7757
8235
|
}
|
|
7758
8236
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTextarea, decorators: [{
|
|
7759
8237
|
type: Component,
|
|
@@ -7769,7 +8247,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
7769
8247
|
useExisting: forwardRef(() => OlTextarea),
|
|
7770
8248
|
multi: true,
|
|
7771
8249
|
},
|
|
7772
|
-
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"
|
|
8250
|
+
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n @if (!commentBox()) {\n <!-- Standard Textarea Input -->\n <div class=\"relative w-full\">\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder()\"\n [maxLength]=\"maxLength() ?? 5000\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm rounded-base outline-none border font-normal shadow-xs\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n ></textarea>\n </div>\n } @else {\n <!--\n Figma Ready-to-use Comment / Reply Textarea Card (`Type=Chatroom`).\n\n `rounded-md` (12px) ch\u1EE9 kh\u00F4ng `rounded-lg` (16px): \u0111\u1ECDc Figma\n `ol-textarea` bi\u1EBFn th\u1EC3 `Type=Chatroom, State=Initial` ra **radius 12** \u2014\n c\u00F9ng b\u1EADc v\u1EDBi `ol-input` v\u00E0 m\u1ECDi \u00F4 nh\u1EADp kh\u00E1c trong h\u1ECD. \u0110o trang /forms\n 2026-09-17: `ol-textarea` l\u00E0 component DUY NH\u1EA4T trong h\u1ECD c\u00F3 hai gi\u00E1 tr\u1ECB\n bo g\u00F3c (12px v\u00E0 16px), l\u1EC7ch v\u1EDBi 9 component c\u00F2n l\u1EA1i.\n -->\n <div\n class=\"border border-default-medium rounded-md bg-neutral-primary overflow-hidden shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle dark:focus-within:ring-brand-950 focus-within:border-brand\"\n >\n <textarea\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-describedby]=\"describedBy()\"\n [rows]=\"rows()\"\n [disabled]=\"disabled()\"\n [placeholder]=\"placeholder() || 'Vi\u1EBFt b\u00ECnh lu\u1EADn ho\u1EB7c ph\u1EA3n h\u1ED3i...'\"\n [ngModel]=\"value()\"\n (ngModelChange)=\"emitChange($event)\"\n (blur)=\"markTouched()\"\n class=\"block w-full p-3.5 text-sm text-body bg-transparent border-0 outline-none resize-none placeholder:text-body-subtle\"\n ></textarea>\n <!-- Bottom Action Bar -->\n <div\n class=\"flex items-center justify-between px-3 py-2 border-t border-default-medium bg-neutral-secondary-medium\"\n >\n <div class=\"flex items-center gap-1 text-body-subtle\">\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"\u0110\u00EDnh k\u00E8m t\u1EC7p\"\n >\n <ol-icon name=\"link\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Ch\u00E8n v\u1ECB tr\u00ED/h\u00ECnh \u1EA3nh\"\n >\n <ol-icon name=\"photo\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\n title=\"Bi\u1EC3u t\u01B0\u1EE3ng c\u1EA3m x\u00FAc\"\n >\n <ol-icon name=\"mynaui:smile\" size=\"sm\" />\n </button>\n </div>\n <button\n type=\"button\"\n (click)=\"onSubmit()\"\n [disabled]=\"disabled() || !value().trim()\"\n class=\"inline-flex items-center gap-1.5 px-3.5 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong disabled:opacity-50 disabled:cursor-not-allowed rounded-lg shadow-xs\"\n >\n <span>G\u1EEDi</span>\n <ol-icon name=\"arrow-right\" size=\"xs\" />\n </button>\n </div>\n </div>\n }\n @if (helperText() || showCharCount()) {\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\n @if (helperText()) {\n <p\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n @if (showCharCount()) {\n <span class=\"text-body-subtle ms-auto\">\n {{ value().length }}{{ maxLength() ? '/' + maxLength() : '' }} k\u00FD t\u1EF1\n </span>\n }\n </div>\n }\n</div>\n" }]
|
|
7773
8251
|
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], showCharCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCharCount", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], commentBox: [{ type: i0.Input, args: [{ isSignal: true, alias: "commentBox", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], submitComment: [{ type: i0.Output, args: ["submitComment"] }] } });
|
|
7774
8252
|
|
|
7775
8253
|
class OlSpinner {
|
|
@@ -8417,7 +8895,7 @@ class OlSelect {
|
|
|
8417
8895
|
const el = this.panel()?.nativeElement;
|
|
8418
8896
|
const raBody = this.appendTo() === 'body';
|
|
8419
8897
|
if (!open || !el) {
|
|
8420
|
-
this.
|
|
8898
|
+
this.detachPanelNow();
|
|
8421
8899
|
return;
|
|
8422
8900
|
}
|
|
8423
8901
|
/**
|
|
@@ -8451,11 +8929,11 @@ class OlSelect {
|
|
|
8451
8929
|
this.destroyRef.onDestroy(() => {
|
|
8452
8930
|
if (this.hengio)
|
|
8453
8931
|
clearTimeout(this.hengio);
|
|
8454
|
-
this.
|
|
8932
|
+
this.detachPanelNow();
|
|
8455
8933
|
});
|
|
8456
8934
|
}
|
|
8457
8935
|
/** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
|
|
8458
|
-
|
|
8936
|
+
detachPanelNow() {
|
|
8459
8937
|
if (this.stopTracking) {
|
|
8460
8938
|
this.stopTracking();
|
|
8461
8939
|
this.stopTracking = null;
|
|
@@ -8864,13 +9342,13 @@ class OlSelect {
|
|
|
8864
9342
|
this.searchQuery.set(val);
|
|
8865
9343
|
this.activeIndex.set(0);
|
|
8866
9344
|
this.scrollTop.set(0);
|
|
8867
|
-
this.
|
|
9345
|
+
this.emitSearch(val);
|
|
8868
9346
|
}
|
|
8869
9347
|
/**
|
|
8870
9348
|
* Phát `search` có GỠ NẢY. Gõ "nguyễn văn a" là 12 phím — không gỡ nảy thì
|
|
8871
9349
|
* 12 lượt gọi API, mà chỉ lượt cuối có ích.
|
|
8872
9350
|
*/
|
|
8873
|
-
|
|
9351
|
+
emitSearch(val) {
|
|
8874
9352
|
if (this.hengio) {
|
|
8875
9353
|
clearTimeout(this.hengio);
|
|
8876
9354
|
this.hengio = null;
|
|
@@ -9104,7 +9582,7 @@ class OlSelect {
|
|
|
9104
9582
|
useExisting: forwardRef(() => OlSelect),
|
|
9105
9583
|
multi: true,
|
|
9106
9584
|
},
|
|
9107
|
-
], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }, { propertyName: "labelTemplate", first: true, predicate: ["labelTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "listboxRef", first: true, predicate: ["listboxRef"], descendants: true, isSignal: true }, { propertyName: "oTimPanel", first: true, predicate: ["oTimPanel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9585
|
+
], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }, { propertyName: "labelTemplate", first: true, predicate: ["labelTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "listboxRef", first: true, predicate: ["listboxRef"], descendants: true, isSignal: true }, { propertyName: "oTimPanel", first: true, predicate: ["oTimPanel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-heading]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9108
9586
|
}
|
|
9109
9587
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSelect, decorators: [{
|
|
9110
9588
|
type: Component,
|
|
@@ -9118,7 +9596,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
9118
9596
|
useExisting: forwardRef(() => OlSelect),
|
|
9119
9597
|
multi: true,
|
|
9120
9598
|
},
|
|
9121
|
-
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-body]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n" }]
|
|
9599
|
+
], template: "<div class=\"w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <div class=\"relative w-full\">\n @if (leftIcon()) {\n <div\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center ps-3.5 pointer-events-none text-body-subtle\"\n >\n <ng-content select=\"[leftIcon]\" />\n </div>\n }\n\n <!--\n \u00D4 \u0110I\u1EC0U KHI\u1EC2N \u2014 gi\u1EEF NGUY\u00CAN b\u1EA3ng class c\u1EE7a b\u1EA3n `<select>` c\u0169 \u0111\u1EC3 giao di\u1EC7n\n Figma kh\u00F4ng \u0111\u1ED5i m\u1ED9t pixel: c\u00F9ng vi\u1EC1n, n\u1EC1n, bo g\u00F3c, thang c\u1EE1 (h-9/h-10/h-12)\n v\u00E0 c\u00F9ng b\u1EA3ng m\u00E0u theo `state`.\n\n `dark:focus:ring-brand-950` GI\u1EEE b\u1EADc s\u1ED1: `brand` l\u00E0 b\u1EA3ng Figma C\u00D3 n\u00EAn b\u1EADc\n s\u1ED1 h\u1EE3p l\u1EC7, c\u00F2n `ring-brand-softer` (token duy nh\u1EA5t tr\u1ECF brand-950 \u1EDF ch\u1EBF \u0111\u1ED9\n t\u1ED1i) ch\u01B0a n\u01A1i n\u00E0o d\u00F9ng n\u00EAn Tailwind kh\u00F4ng sinh CSS \u2014 thay v\u00E0o l\u00E0 class\n CH\u1EBET m\u00E0 build v\u1EABn xanh (\u0111o 2026-09-15).\n\n Kh\u00E1c b\u1EA3n c\u0169 \u1EDF ch\u1ED7 \u0111\u00E2y l\u00E0 `<div role=\"combobox\">` ch\u1EE9 kh\u00F4ng ph\u1EA3i `<select>`\n g\u1ED1c: th\u1EBB g\u1ED1c kh\u00F4ng cho t\u00ECm ki\u1EBFm, \u0111a ch\u1ECDn hay t\u1EF1 v\u1EBD t\u1EEBng d\u00F2ng.\n -->\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n tabindex=\"0\"\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n aria-haspopup=\"listbox\"\n (click)=\"toggle()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"relative flex w-full items-center gap-1.5 border shadow-xs outline-none pe-10 font-normal\"\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\n [class.rounded-base]=\"true\"\n [class]=\"paddingStartClass()\"\n [class.flex-wrap]=\"multiple()\"\n [class.content-center]=\"multiple()\"\n [class.py-1]=\"multiple()\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\n [class.text-heading]=\"state() === 'default' && !disabled()\"\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\n [class.focus:ring-2]=\"!disabled()\"\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled()\"\n [class.text-fg-disabled]=\"disabled()\"\n [class.cursor-not-allowed]=\"disabled()\"\n >\n <!-- Th\u1EBB c\u1EE7a c\u00E1c gi\u00E1 tr\u1ECB \u0111\u00E3 ch\u1ECDn (\u0111a ch\u1ECDn) -->\n @if (multiple()) {\n @for (val of selectedValues(); track val) {\n <!-- `h-6` c\u1ED1 \u0111\u1ECBnh: \u0111\u1EC3 th\u1EBB co theo ch\u1EEF th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t ki\u1EC3u v\u00E0 \u00F4\n b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. C\u00F9ng c\u00E1ch \u0111\u00E3 ch\u1ED1t \u1EDF `ol-autocomplete`. -->\n <!-- Chip kho\u00E1 d\u00F9ng b\u1EA3ng m\u00E0u TRUNG T\u00CDNH: m\u1EA5t n\u00FAt \u00D7 l\u00E0 d\u1EA5u hi\u1EC7u qu\u00E1\n nh\u1EB9, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng n\u00FAt ch\u01B0a hi\u1EC7n. \u0110\u1ED5i m\u00E0u cho th\u1EA5y ngay \u0111\u00E2y\n l\u00E0 lo\u1EA1i chip kh\u00E1c. -->\n <span\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\n [class.border-brand-subtle]=\"!isLocked(val)\"\n [class.bg-brand-softer]=\"!isLocked(val)\"\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\n [class.border-default]=\"isLocked(val)\"\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\n [class.text-body]=\"isLocked(val)\"\n >\n @if (isLocked(val)) {\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\n } @else if (optionOf(val)?.icon) {\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\n } @else if (optionOf(val)?.prefix) {\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\n optionOf(val)?.prefix\n }}</span>\n }\n <span>{{ getDisplayLabel(val) }}</span>\n @if (!disabled() && !readonly() && !isLocked(val)) {\n <button\n type=\"button\"\n (click)=\"removeValue(val, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabel(val)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!--\n PH\u1EA6N C\u1ED0 \u0110\u1ECANH c\u1EE7a \u00F4 \u0111\u00F3ng \u2014 icon v\u00E0 nh\u00E3n LU\u00D4N hi\u1EC7n, d\u00F9 \u0111\u00E3 ch\u1ECDn hay ch\u01B0a,\n d\u00F9 danh s\u00E1ch \u0111ang m\u1EDF hay \u0111\u00F3ng. Ch\u1EC9 ph\u1EA7n GI\u00C1 TR\u1ECA b\u00EAn ph\u1EA3i m\u1EDBi \u0111\u1ED5i.\n\n \u0110\u1EB7t NGO\u00C0I c\u1EA3 ba nh\u00E1nh d\u01B0\u1EDBi (\u00F4 t\u00ECm / nh\u00E3n \u0111\u00E3 ch\u1ECDn / placeholder) ch\u00EDnh\n l\u00E0 \u0111i\u1EC1u l\u00E0m n\u00F3 c\u1ED1 \u0111\u1ECBnh \u2014 n\u1EB1m trong nh\u00E1nh n\u00E0o th\u00EC n\u00F3 bi\u1EBFn m\u1EA5t \u1EDF hai\n nh\u00E1nh kia.\n\n Kh\u00E1c `icon` c\u1EE7a M\u1EE4C: c\u00E1i \u0111\u00F3 \u0111\u1ED5i theo m\u1EE5c \u0111ang ch\u1ECDn. Khai c\u1EA3 hai th\u00EC\n `triggerIcon` th\u1EAFng, v\u00EC nh\u00E3n c\u1ED1 \u0111\u1ECBnh m\u00E0 nh\u1EA3y theo m\u1EE5c th\u00EC h\u1EBFt c\u1ED1 \u0111\u1ECBnh.\n -->\n @if (triggerIcon()) {\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n }\n @if (triggerLabel()) {\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\n triggerLabel()\n }}</span>\n }\n\n <!--\n \u00D4 G\u00D5 T\u00CCM ch\u1EC9 hi\u1EC7n khi danh s\u00E1ch \u0110ANG M\u1EDE. \u0110\u00F3ng l\u1EA1i th\u00EC hi\u1EC7n nh\u00E3n t\u0129nh \u2014\n n\u1EBFu \u0111\u1EC3 \u00F4 nh\u1EADp lu\u00F4n \u0111\u00F3, ng\u01B0\u1EDDi d\u00F9ng t\u01B0\u1EDFng g\u00F5 th\u1EB3ng v\u00E0o \u0111\u01B0\u1EE3c, m\u00E0 gi\u00E1 tr\u1ECB\n l\u1EA1i kh\u00F4ng \u0111\u1ED5i theo ch\u1EEF h\u1ECD g\u00F5.\n -->\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\n <input\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n autocomplete=\"off\"\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\n />\n } @else if (!multiple() && hasValue()) {\n @if (labelTemplate) {\n <ng-container\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\n />\n } @else {\n <!-- \u00D4 \u0111\u00F3ng hi\u1EC7n C\u1EA2 ph\u1EA7n \u0111\u1EA7u c\u1EE7a m\u1EE5c \u0111ang ch\u1ECDn, kh\u00F4ng ch\u1EC9 ch\u1EEF \u2014 \u0111\u00E2y\n l\u00E0 \u0111i\u1EC3m kh\u00E1c c\u1EE7a combobox reui so v\u1EDBi \u00F4 ch\u1ECDn th\u01B0\u1EDDng. -->\n @if (selectedItem()?.icon && !triggerIcon()) {\n <ol-icon\n [name]=\"$any(selectedItem()).icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"selectedItem()?.prefixClass || ''\"\n />\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\n >{{ selectedItem()?.prefix }}</span\n >\n }\n <span class=\"truncate\">{{ singleLabel() }}</span>\n }\n } @else if (!hasValue()) {\n <!--\n \u1EDE tr\u1EA1ng th\u00E1i L\u1ED6I, n\u1EC1n \u0111\u1ED5i sang `bg-danger-soft` (#fef0f2) v\u00E0\n `text-body-subtle` ch\u1EC9 c\u00F2n **4,36** \u2014 d\u01B0\u1EDBi chu\u1EA9n 4,5. N\u1EC1n `success`\n (#ecfdf5) cho 4,59, v\u1EEBa \u0111\u1EE7, n\u00EAn ch\u1EC9 nh\u00E1nh l\u1ED7i c\u1EA7n \u0111\u1EADm h\u01A1n.\n `text-body` tr\u00EAn n\u1EC1n \u0111\u00F3 ra 6,82 (s\u00E1ng) v\u00E0 10,66 (t\u1ED1i).\n -->\n <span\n class=\"truncate\"\n [class.text-body-subtle]=\"state() !== 'error'\"\n [class.text-body]=\"state() === 'error'\"\n >{{ placeholder() }}</span\n >\n }\n\n <!--\n M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0110\u1ED4I VAI theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n B\u1EA3n \u0111\u1EA7u c\u1EE7a t\u00F4i \u0111\u1EC3 n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o 2026-09-17\n tr\u00EAn `/forms` \u0111\u1EBFm **2 icon b\u00EAn ph\u1EA3i \u1EDF M\u1ECCI tr\u1EA1ng th\u00E1i**, k\u1EC3 c\u1EA3 khi danh\n s\u00E1ch \u0111ang m\u1EDF. C\u1EE5m b\u00EAn ph\u1EA3i ch\u1EADt, hai icon 16px chen nhau tr\u00F4ng r\u1ED1i.\n\n `ol-autocomplete` \u0111\u00E3 ch\u1ED1t l\u1ED1i n\u00E0y t\u1EEB tr\u01B0\u1EDBc (\u00F4 `w-7` \u0111\u1ED5i vai\n spinner / xo\u00E1 / chevron); `ol-select` ph\u1EA3i theo cho nh\u1EA5t qu\u00E1n.\n\n \u00D4 r\u1ED9ng C\u1ED0 \u0110\u1ECANH \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y m\u1ED7i l\u1EA7n icon \u0111\u1ED5i vai.\n\n \u0110\u1EC6M PH\u1EA2I l\u00E0 B\u1EAET BU\u1ED8C, kh\u00F4ng ph\u1EA3i trang tr\u00ED: c\u1EE5m n\u00E0y `absolute` n\u00EAn BUNG\n KH\u1ECEI LU\u1ED2NG, kh\u00F4ng h\u01B0\u1EDFng `px-3.5` c\u1EE7a khung nh\u01B0 `ol-autocomplete` (c\u00E1i\n \u0111\u00F3 d\u00F9ng `ms-auto`, v\u1EABn n\u1EB1m trong lu\u1ED3ng). B\u1EA3n \u0111\u1EA7u ch\u1EC9 c\u00F3 `w-7` \u2014 \u0111o\n 2026-09-17 ra icon c\u00E1ch m\u00E9p ph\u1EA3i **3px**, d\u00EDnh s\u00E1t l\u1EC1.\n\n Figma `ol-select` Type=Default: icon `angle-down` c\u00E1ch m\u00E9p ph\u1EA3i **12px**.\n Con s\u1ED1 `pe-2` (8px) tr\u00F4ng c\u00F3 v\u1EBB h\u1EE5t, nh\u01B0ng n\u00FAt xo\u00E1 l\u00E0 `size-6` (24px)\n b\u1ECDc icon 16px n\u00EAn d\u01B0 4px m\u1ED7i b\u00EAn \u2014 8 + 4 = 12px t\u1EDBi SVG th\u1EADt. \u0110o b\u1EB1ng\n m\u00E9p SVG ch\u1EE9 kh\u00F4ng m\u00E9p n\u00FAt, v\u00EC m\u1EAFt ng\u01B0\u1EDDi nh\u00ECn th\u1EA5y SVG.\n\n `justify-end` ch\u1EE9 kh\u00F4ng `justify-center`: c\u0103n gi\u1EEFa th\u00EC icon tr\u00F4i theo\n b\u1EC1 r\u1ED9ng \u00F4, \u0111\u1ED5i `w-*` l\u00E0 l\u1EC7ch l\u1EA1i.\n -->\n <div class=\"absolute inset-y-0 inset-e-0 flex w-10 items-center justify-end pe-3.5\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n DANH S\u00C1CH. Token \u0111\u1ECDc th\u1EB3ng t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147),\n bi\u1EBFn th\u1EC3 `Type=Default` \u2014 \u0111\u00F3 l\u00E0 dropdown chu\u1EA9n c\u1EE7a design system:\n\n bo g\u00F3c 12px -> rounded-md\n \u0111\u1EC7m 8px -> p-2 (tr\u01B0\u1EDBc l\u00E0 p-1.5 = 6px)\n n\u1EC1n bg-neutral-primary-medium (tr\u01B0\u1EDBc l\u00E0 bg-neutral-primary)\n vi\u1EC1n border-default-medium (tr\u01B0\u1EDBc l\u00E0 border-default)\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng (#ffffff / #e5e7eb) n\u00EAn \u0111\u1ED5i\n hay kh\u00F4ng nh\u00ECn c\u0169ng th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I th\u00EC kh\u00E1c h\u1EB3n:\n\n bg-primary gray-950 \u2190 \u0111ang d\u00F9ng, g\u1EA7n \u0111en, panel CH\u00CCM v\u00E0o n\u1EC1n\n bg-primary-medium gray-800 \u2190 Figma, panel n\u1ED5i l\u00EAn\n border-default gray-800\n border-default-medium gray-700\n\n \u0110\u00E2y \u0111\u00FAng lo\u1EA1i l\u1ED7i ch\u1EC9 l\u1ED9 \u1EDF ch\u1EBF \u0111\u1ED9 t\u1ED1i m\u00E0 ch\u1EBF \u0111\u1ED9 s\u00E1ng kh\u00F4ng bao gi\u1EDD th\u1EA5y.\n -->\n @if (isOpen()) {\n <!--\n \u00D4 T\u00CCM \u1EDE \u0110\u1EA6U PANEL (`searchPosition=\"panel\"`).\n \u0110\u1EB7t NGO\u00C0I th\u1EBB `role=\"listbox\"`: m\u1ED9t `<input>` n\u1EB1m trong listbox l\u00E0 sai\n ARIA \u2014 m\u1ECDi con c\u1EE7a listbox ph\u1EA3i l\u00E0 `option` ho\u1EB7c `group`. \u0110\u00E2y l\u00E0 l\u1ED1i\n `ol-command` v\u00E0 combobox c\u1EE7a reui d\u00F9ng.\n -->\n <!--\n H\u1ED8P B\u1ECCC mang `#panel` v\u00E0 `position: absolute`.\n\n `#panel` ph\u1EA3i \u1EDF \u0110\u00C2Y ch\u1EE9 kh\u00F4ng \u1EDF th\u1EBB `role=\"listbox\"`: `appendTo=\"body\"`\n \u0111\u1EA9y ch\u00EDnh ph\u1EA7n t\u1EED `#panel` ra `<body>`. \u0110\u1EC3 n\u00F3 \u1EDF listbox th\u00EC \u00F4 t\u00ECm \u1EDF l\u1EA1i\n trong c\u00E2y DOM c\u0169 c\u00F2n danh s\u00E1ch bay \u0111i \u2014 h\u1ED9p tho\u1EA1i v\u1EE1 l\u00E0m \u0111\u00F4i.\n -->\n <div\n #panel\n class=\"absolute z-dropdown overflow-hidden rounded-md border border-default-medium bg-neutral-primary-medium shadow-lg flex flex-col max-h-[var(--ol-overlay-max-height,24rem)]\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (searchable() && searchPosition() === 'panel') {\n <div class=\"border-b border-default-subtle p-2\">\n <!-- Icon/ch\u1EEF \u0111\u1EE9ng \u0111\u1EA7u \u00F4 T\u00CCM \u2014 c\u1EE7a vi\u1EC7c T\u00CCM, kh\u00F4ng ph\u1EA3i c\u1EE7a m\u1EE5c\n \u0111ang ch\u1ECDn. B\u1ECDc trong khung c\u00F3 vi\u1EC1n \u0111\u1EC3 icon n\u1EB1m TRONG \u00F4, kh\u00F4ng\n \u0111\u1EE9ng r\u1EDDi b\u00EAn ngo\u00E0i nh\u01B0 m\u1ED9t nh\u00E3n. -->\n <div\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\n >\n @if (searchIcon()) {\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\n } @else if (searchPrefix()) {\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\n searchPrefix()\n }}</span>\n }\n <input\n #oTimPanel\n type=\"text\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchInput($event)\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"onKeyDown($event)\"\n [placeholder]=\"searchPlaceholder()\"\n [attr.aria-label]=\"searchPlaceholder()\"\n [attr.aria-controls]=\"listboxId()\"\n autocomplete=\"off\"\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\n />\n </div>\n </div>\n }\n <!--\n `overflow-x-hidden` KHAI R\u00D5: khai `overflow-y-auto` m\u1ED9t m\u00ECnh th\u00EC tr\u00ECnh\n duy\u1EC7t t\u1EF1 t\u00EDnh `overflow-x` th\u00E0nh `auto` (spec CSS: m\u1ED9t tr\u1EE5c `visible`\n c\u00F2n tr\u1EE5c kia kh\u00E1c `visible` l\u00E0 kh\u00F4ng h\u1EE3p l\u1EC7). \u0110o \u0111\u01B0\u1EE3c `scrollWidth\n 765 > 620` khi option c\u00F3 `white-space: nowrap`.\n\n Ch\u1EC9 ch\u1EB7n \u1EDF PANEL CH\u1ECCN c\u00F3 b\u1EC1 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh \u2014 n\u1ED9i dung ph\u1EA3i v\u1EEBa b\u1EC1 ngang.\n KH\u00D4NG ch\u1EB7n \u1EDF khung ch\u1EE9a n\u1ED9i dung t\u1EF1 do (`device-mockup`,\n `chat-container`, `drawer`): \u1EDF \u0111\u00F3 ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu n\u1ED9i dung b\u1EA5t k\u1EF3\n v\u00E0o, ch\u1EB7n l\u00E0 c\u1EAFt m\u1EA5t ph\u1EA7n r\u1ED9ng c\u1EE7a h\u1ECD.\n -->\n <div\n #listboxRef\n role=\"listbox\"\n [id]=\"listboxId()\"\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\n (scroll)=\"onPanelScroll($event)\"\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\n >\n @if (waitingForTerm()) {\n <!-- Ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`: \"kh\u00F4ng t\u00ECm th\u1EA5y\" l\u00E0 sai s\u1EF1 th\u1EADt, v\u00EC\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i `typeToSearchText` c\u1EE7a ng-select. -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\n } @else if (rows().length === 0) {\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (canCreate()) {\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"mt-2 inline-flex items-center gap-1.5 rounded-lg bg-brand-softer px-3 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-subtle\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>{{ createLabel() }}</span>\n </button>\n }\n </div>\n } @else {\n <!-- \u0110\u1EC7m tr\u00EAn: gi\u1EEF thanh cu\u1ED9n d\u00E0i \u0111\u00FAng nh\u01B0 khi v\u1EBD h\u1EBFt danh s\u00E1ch. -->\n @if (viewport().padTop > 0) {\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\n }\n\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\n @if (row.kind === 'group') {\n <!-- `dauTien` t\u00EDnh khi d\u1EF1ng danh s\u00E1ch, KH\u00D4NG d\u00F9ng `$first` c\u1EE7a\n `@for`: khi `virtualScroll` b\u1EADt, `$first` l\u00E0 d\u00F2ng \u0111\u1EA7u c\u1EE7a\n khung nh\u00ECn n\u00EAn cu\u1ED9n xu\u1ED1ng l\u00E0 m\u1EA5t \u0111\u01B0\u1EDDng ng\u0103n. -->\n @if (groupSeparator() && !row.isFirstGroup) {\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\n }\n <div\n role=\"presentation\"\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ row.label }}\n </div>\n } @else {\n <!--\n `rounded-sm` (6px) ch\u1EE9 kh\u00F4ng `rounded-md` (12px): Figma v\u1EBD\n `ol-autocomplete-item` bo **8px**, m\u00E0 thang token c\u1EE7a lib kh\u00F4ng\n c\u00F3 b\u1EADc 8 (xs 4 \u00B7 sm 6 \u00B7 md/base 12 \u00B7 lg 16). L\u1EA5y b\u1EADc G\u1EA6N NH\u1EA4T\n thay v\u00EC vi\u1EBFt `rounded-[8px]` \u2014 gi\u00E1 tr\u1ECB th\u00F4 n\u1EB1m ngo\u00E0i h\u1EC7 token\n th\u00EC \u0111\u1ED5i theme kh\u00F4ng theo \u0111\u01B0\u1EE3c.\n\n `ol-dropdown-item` c\u0169ng \u0111\u00E3 ch\u1ED1t `rounded-sm` cho m\u1EE5c menu, n\u00EAn\n \u0111\u00E2y c\u00F2n l\u00E0 gi\u1EEF nh\u1EA5t qu\u00E1n trong lib.\n -->\n <!--\n `min-w-0` cho ph\u00E9p h\u00E0ng CO l\u1EA1i, \u0111i\u1EC1u ki\u1EC7n c\u1EA7n \u0111\u1EC3 `truncate`\n c\u1EE7a ng\u01B0\u1EDDi d\u00F9ng ch\u1EA1y. Nh\u01B0ng n\u00F3 KH\u00D4NG t\u1EF1 ch\u1EB7n tr\u00E0n: \u0111o b\u1EB1ng c\u00E1ch\n ti\u00EAm `white-space: nowrap` v\u00E0o h\u00E0ng \u2014 panel v\u1EABn `scrollWidth\n 1763 > 831`. Ch\u1EC9 `overflow: hidden` \u0111\u1EB7t tr\u00EAn CH\u00CDNH ph\u1EA7n t\u1EED n\u1ED9i\n dung m\u1EDBi ch\u1EB7n \u0111\u01B0\u1EE3c, m\u00E0 ph\u1EA7n t\u1EED \u0111\u00F3 do ng\u01B0\u1EDDi d\u00F9ng chi\u1EBFu v\u00E0o qua\n `optionTemplate` n\u00EAn lib kh\u00F4ng v\u1EDBi t\u1EDBi. `overflow-x-hidden` \u1EDF\n panel l\u00E0 l\u1EDBp ch\u1EB7n cu\u1ED1i.\n -->\n <div\n role=\"option\"\n [id]=\"optionId(row.index)\"\n [attr.data-option-index]=\"row.index\"\n [attr.aria-selected]=\"isSelected(row.item)\"\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\n [class.cursor-pointer]=\"!row.item?.disabled\"\n [class.cursor-not-allowed]=\"row.item?.disabled\"\n [class.bg-neutral-tertiary-medium]=\"\n activeIndex() === row.index && !row.item?.disabled\n \"\n [class.hover:bg-neutral-tertiary-medium]=\"\n !row.item?.disabled && activeIndex() !== row.index\n \"\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\n [class.hover:text-heading]=\"!row.item?.disabled\"\n [class.font-medium]=\"isSelected(row.item)\"\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\n [class.text-fg-disabled]=\"row.item?.disabled\"\n >\n @if (optionTemplate) {\n <ng-container\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\n />\n } @else {\n <!-- Ph\u1EA7n \u0111\u1EA7u: `icon` \u01B0u ti\u00EAn, kh\u00F4ng c\u00F3 th\u00EC `prefix` (ch\u1EEF).\n `prefixClass` nh\u1EADn CLASS ch\u1EE9 kh\u00F4ng nh\u1EADn m\u00E3 m\u00E0u \u2014 m\u00E3 m\u00E0u\n c\u1EE9ng kh\u00F4ng \u0111\u1ED5i theo theme v\u00E0 theo ch\u1EBF \u0111\u1ED9 t\u1ED1i. -->\n @if (row.item?.icon) {\n <ol-icon\n [name]=\"row.item.icon\"\n size=\"sm\"\n class=\"shrink-0\"\n [class]=\"row.item.prefixClass || ''\"\n />\n } @else if (row.item?.prefix) {\n <span\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\n >{{ row.item.prefix }}</span\n >\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\n }\n @if (isSelected(row.item)) {\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\n }\n </div>\n }\n }\n\n <!-- \u0110\u1EC7m d\u01B0\u1EDBi \u2014 xem l\u00FD do \u1EDF \u0111\u1EC7m tr\u00EAn. -->\n @if (viewport().padBottom > 0) {\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\n }\n\n <!-- Danh s\u00E1ch C\u00D2N m\u1EE5c nh\u01B0ng ch\u1EEF \u0111ang g\u00F5 ch\u01B0a tr\u00F9ng nh\u00E3n n\u00E0o: v\u1EABn ph\u1EA3i\n cho t\u1EA1o. V\u00ED d\u1EE5 \u0111\u00E3 c\u00F3 \"To\u00E1n 10\" m\u00E0 ng\u01B0\u1EDDi d\u00F9ng g\u00F5 \"To\u00E1n 11\" \u2014\n nh\u00E1nh r\u1ED7ng \u1EDF tr\u00EAn kh\u00F4ng ch\u1EA1y v\u00EC b\u1ED9 l\u1ECDc v\u1EABn ra m\u1ED9t m\u1EE5c. -->\n @if (canCreate()) {\n <div class=\"mt-1 border-t border-default-subtle p-2\">\n <button\n type=\"button\"\n (click)=\"createOption()\"\n class=\"inline-flex w-full items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-fg-brand transition-colors hover:bg-brand-softer\"\n >\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\n <span class=\"truncate\">{{ createLabel() }}</span>\n </button>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n\n @if (helperText()) {\n <p\n class=\"mt-1.5 text-xs\"\n [class.text-body]=\"state() === 'default'\"\n [class.text-fg-danger-strong]=\"state() === 'error'\"\n [class.text-fg-success-strong]=\"state() === 'success'\"\n >\n {{ helperText() }}\n </p>\n }\n</div>\n" }]
|
|
9122
9600
|
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], leftIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leftIcon", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], searchable: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchable", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], labelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKey", required: false }] }], valueKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueKey", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], menuClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuClass", required: false }] }], matchTriggerWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "matchTriggerWidth", required: false }] }], maxSelectedItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelectedItems", required: false }] }], closeOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeOnSelect", required: false }] }], hideSelected: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideSelected", required: false }] }], allowCreate: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCreate", required: false }] }], createText: [{ type: i0.Input, args: [{ isSignal: true, alias: "createText", required: false }] }], groupSeparator: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupSeparator", required: false }] }], searchPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPosition", required: false }] }], searchIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchIcon", required: false }] }], searchPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPrefix", required: false }] }], triggerIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerIcon", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], remoteSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteSearch", required: false }] }], minTermLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minTermLength", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], typeToSearchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeToSearchText", required: false }] }], virtualScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "virtualScroll", required: false }] }], itemHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemHeight", required: false }] }], bufferAmount: [{ type: i0.Input, args: [{ isSignal: true, alias: "bufferAmount", required: false }] }], search: [{ type: i0.Output, args: ["search"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], added: [{ type: i0.Output, args: ["added"] }], removed: [{ type: i0.Output, args: ["removed"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], optionTemplate: [{
|
|
9123
9601
|
type: ContentChild,
|
|
9124
9602
|
args: ['optionTemplate']
|
|
@@ -10618,6 +11096,24 @@ class OlTimepicker {
|
|
|
10618
11096
|
...(ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []));
|
|
10619
11097
|
size = input('base', /* @ts-ignore */
|
|
10620
11098
|
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
11099
|
+
/**
|
|
11100
|
+
* Đệm ngang và cỡ chữ theo BẢNG CHUẨN họ form.
|
|
11101
|
+
*
|
|
11102
|
+
* Trước đây template ghim cứng `px-3.5` (14px cho mọi cỡ) và không đặt cỡ
|
|
11103
|
+
* chữ nào, nên nó thừa kế 16px mặc định. Đo trên /datepicker mục 8 ngày
|
|
11104
|
+
* 2026-09-24, đặt cạnh ba ô kia:
|
|
11105
|
+
*
|
|
11106
|
+
* sm/base timepicker chữ 16px · ba ô kia 14px
|
|
11107
|
+
* mọi cỡ timepicker đệm 14px · input 10/12/14/16px theo cỡ
|
|
11108
|
+
*
|
|
11109
|
+
* Gom vào MỘT chuỗi thay vì nhiều `[class.px-*]`: Angular cắt tên class ở
|
|
11110
|
+
* dấu chấm nên `[class.px-2.5]` render ra `px-2`, lệch 2px so với thiết kế.
|
|
11111
|
+
*/
|
|
11112
|
+
fieldTextClasses = computed(() => {
|
|
11113
|
+
const size = this.size();
|
|
11114
|
+
return `${FIELD_PADDING_X[size]} ${FIELD_TEXT[size]}`;
|
|
11115
|
+
}, /* @ts-ignore */
|
|
11116
|
+
...(ngDevMode ? [{ debugName: "fieldTextClasses" }] : /* istanbul ignore next */ []));
|
|
10621
11117
|
state = input('default', /* @ts-ignore */
|
|
10622
11118
|
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
10623
11119
|
/**
|
|
@@ -10778,7 +11274,7 @@ class OlTimepicker {
|
|
|
10778
11274
|
const el = this.panel()?.nativeElement;
|
|
10779
11275
|
const raBody = this.appendTo() === 'body';
|
|
10780
11276
|
if (!open || !el) {
|
|
10781
|
-
this.
|
|
11277
|
+
this.detachPanelNow();
|
|
10782
11278
|
return;
|
|
10783
11279
|
}
|
|
10784
11280
|
// `appendTo="body"` chỉ đổi CHỖ TREO, không đổi cách định vị.
|
|
@@ -10801,7 +11297,7 @@ class OlTimepicker {
|
|
|
10801
11297
|
});
|
|
10802
11298
|
}
|
|
10803
11299
|
/** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
|
|
10804
|
-
|
|
11300
|
+
detachPanelNow() {
|
|
10805
11301
|
if (this.stopTracking) {
|
|
10806
11302
|
this.stopTracking();
|
|
10807
11303
|
this.stopTracking = null;
|
|
@@ -10966,7 +11462,7 @@ class OlTimepicker {
|
|
|
10966
11462
|
useExisting: forwardRef(() => OlTimepicker),
|
|
10967
11463
|
multi: true,
|
|
10968
11464
|
},
|
|
10969
|
-
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "hourAreaRef", first: true, predicate: ["hourArea"], descendants: true, isSignal: true }, { propertyName: "minuteAreaRef", first: true, predicate: ["minuteArea"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none px-3.5 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlScrollArea, selector: "ol-scroll-area", inputs: ["scrollbar", "axis", "smooth", "innerClass"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11465
|
+
], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }, { propertyName: "hourAreaRef", first: true, predicate: ["hourArea"], descendants: true, isSignal: true }, { propertyName: "minuteAreaRef", first: true, predicate: ["minuteArea"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [ngClass]=\"fieldTextClasses()\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: OlScrollArea, selector: "ol-scroll-area", inputs: ["scrollbar", "axis", "smooth", "innerClass"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10970
11466
|
}
|
|
10971
11467
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTimepicker, decorators: [{
|
|
10972
11468
|
type: Component,
|
|
@@ -10982,7 +11478,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
10982
11478
|
useExisting: forwardRef(() => OlTimepicker),
|
|
10983
11479
|
multi: true,
|
|
10984
11480
|
},
|
|
10985
|
-
], template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none px-3.5 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
|
|
11481
|
+
], template: "<!-- Bo g\u00F3c: Figma `ol-timepicker` (#1149:52243) d\u00F9ng 12px cho C\u1EA2 B\u1ED0N size\n (rounded-md). Code tr\u01B0\u1EDBc \u0111\u00E2y d\u00F9ng rounded-lg 16px cho sm/base v\u00E0\n rounded-md 24px cho lg \u2014 c\u00F9ng l\u1ED7i \u0111\u00E3 s\u1EEDa \u1EDF `ol-input`. -->\n<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [id]=\"labelId()\"\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!-- Trigger Box Container with Add-on Support -->\n <div class=\"flex items-stretch w-full shadow-xs\">\n @if (addonType() === 'left') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-e-0 text-body shrink-0 rounded-s-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n\n <div\n [id]=\"inputId()\"\n role=\"combobox\"\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\n [attr.aria-expanded]=\"isOpen()\"\n aria-haspopup=\"listbox\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.tabindex]=\"disabled() ? -1 : 0\"\n (click)=\"toggleDropdown()\"\n (keydown.enter)=\"toggleDropdown()\"\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\n class=\"relative flex items-center justify-between flex-1 w-full border cursor-pointer select-none focus:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n [ngClass]=\"fieldTextClasses()\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"addonType() === 'none'\"\n [class.rounded-e-md]=\"addonType() === 'left'\"\n [class.rounded-s-md]=\"addonType() === 'right'\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n >\n <div class=\"flex items-center gap-2.5 overflow-hidden min-w-0\">\n @if (addonType() === 'none') {\n <!-- Icon trong \u00F4 khi kh\u00F4ng c\u00F3 addon; `addonIcon` \u0111\u1ED5i \u0111\u01B0\u1EE3c. -->\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n <span\n class=\"truncate font-normal\"\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\n [class.text-body]=\"!value() && state() === 'error'\"\n [class.text-heading]=\"!!value()\"\n >\n {{ value() || placeholder() }}\n </span>\n </div>\n\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\n @if (value() && !disabled() && !readonly()) {\n <button\n type=\"button\"\n (click)=\"clear($event)\"\n aria-label=\"Xo\u00E1 gi\u1EDD \u0111\u00E3 ch\u1ECDn\"\n class=\"p-0.5 text-body-subtle hover:text-heading rounded-full hover:bg-neutral-tertiary-medium transition-colors cursor-pointer flex items-center justify-center\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n <ol-icon name=\"chevron-down\" size=\"sm\" />\n </div>\n </div>\n\n @if (addonType() === 'right') {\n <div\n class=\"flex items-center justify-center px-3 bg-neutral-tertiary border border-default border-s-0 text-body shrink-0 rounded-e-md\"\n [class.h-9]=\"size() === 'sm'\"\n [class.h-10]=\"size() === 'base'\"\n [class.h-12]=\"size() === 'lg'\"\n [class.h-13]=\"size() === 'xl'\"\n >\n @if (addonText()) {\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\n } @else {\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\n }\n </div>\n }\n </div>\n\n <!-- Popover Panel -->\n @if (isOpen()) {\n <div\n #panel\n class=\"absolute z-dropdown bg-neutral-primary border border-default rounded-md shadow-lg p-3 min-w-70 select-none overflow-y-auto overflow-x-hidden max-h-[var(--ol-overlay-max-height,28rem)] animate-in fade-in zoom-in-95 duration-150\"\n >\n @if (showPresets()) {\n <!-- Preset quick times -->\n <div class=\"mb-3 pb-2 border-b border-default\">\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">M\u1ED1c gi\u1EDD nhanh</span>\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\n @for (preset of presetTimes; track preset) {\n <button\n type=\"button\"\n (click)=\"selectPreset(preset)\"\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\n [class.bg-brand-softer]=\"value() === preset\"\n [class.text-fg-brand]=\"value() === preset\"\n [class.text-body]=\"value() !== preset\"\n >\n {{ preset }}\n </button>\n }\n </div>\n </div>\n }\n\n <!-- Columns Time Picker -->\n <div class=\"flex items-center justify-center gap-2 py-2\">\n <!-- Hour Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Gi\u1EDD</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #hourArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @if (format() === '12h') {\n @for (h of hours12; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n } @else {\n @for (h of hours24; track h) {\n <button\n type=\"button\"\n (click)=\"selectHour(h)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedHour() === h\"\n [class.text-white]=\"selectedHour() === h\"\n [class.font-semibold]=\"selectedHour() === h\"\n [class.scale-105]=\"selectedHour() === h\"\n [class.shadow-xs]=\"selectedHour() === h\"\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\n [class.text-heading]=\"selectedHour() !== h\"\n >\n {{ pad(h) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\n\n <!-- Minute Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Ph\u00FAt</span>\n <div\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\n >\n <!-- Top Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\n ></div>\n\n <ol-scroll-area\n #minuteArea\n scrollbar=\"hidden\"\n [smooth]=\"true\"\n innerClass=\"p-1 py-4 space-y-1\"\n >\n @for (m of minutes; track m) {\n @if (m % minuteStep() === 0) {\n <button\n type=\"button\"\n (click)=\"selectMinute(m)\"\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\n [class.bg-brand]=\"selectedMinute() === m\"\n [class.text-white]=\"selectedMinute() === m\"\n [class.font-semibold]=\"selectedMinute() === m\"\n [class.scale-105]=\"selectedMinute() === m\"\n [class.shadow-xs]=\"selectedMinute() === m\"\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\n [class.text-heading]=\"selectedMinute() !== m\"\n >\n {{ pad(m) }}\n </button>\n }\n }\n </ol-scroll-area>\n\n <!-- Bottom Fade Gradient -->\n <div\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\n ></div>\n </div>\n </div>\n\n @if (format() === '12h') {\n <!-- AM/PM Column -->\n <div class=\"flex flex-col items-center\">\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Bu\u1ED5i</span>\n <div\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\n >\n <button\n type=\"button\"\n (click)=\"selectPeriod('AM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\n [class.text-white]=\"selectedPeriod() === 'AM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\n >\n AM\n </button>\n <button\n type=\"button\"\n (click)=\"selectPeriod('PM')\"\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\n [class.text-white]=\"selectedPeriod() === 'PM'\"\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\n >\n PM\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Action Footer -->\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\n <button\n type=\"button\"\n (click)=\"cancel()\"\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\n >\n H\u1EE7y\n </button>\n <button\n type=\"button\"\n (click)=\"apply()\"\n class=\"px-3 py-1.5 text-xs font-medium text-white bg-brand hover:bg-brand-strong rounded-lg shadow-xs transition-colors\"\n >\n \u00C1p d\u1EE5ng\n </button>\n </div>\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
|
|
10986
11482
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], errorText: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], minuteStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "minuteStep", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], showPresets: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPresets", required: false }] }], addonType: [{ type: i0.Input, args: [{ isSignal: true, alias: "addonType", required: false }] }], addonText: [{ type: i0.Input, args: [{ isSignal: true, alias: "addonText", required: false }] }], addonIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "addonIcon", required: false }] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], hourAreaRef: [{ type: i0.ViewChild, args: ['hourArea', { isSignal: true }] }], minuteAreaRef: [{ type: i0.ViewChild, args: ['minuteArea', { isSignal: true }] }], timeChange: [{ type: i0.Output, args: ["timeChange"] }], timestampChange: [{ type: i0.Output, args: ["timestampChange"] }], isoChange: [{ type: i0.Output, args: ["isoChange"] }], timeObjectChange: [{ type: i0.Output, args: ["timeObjectChange"] }], onDocumentClick: [{
|
|
10987
11483
|
type: HostListener,
|
|
10988
11484
|
args: ['document:click', ['$event']]
|
|
@@ -11019,8 +11515,32 @@ class OlAutocomplete {
|
|
|
11019
11515
|
size = input('base', /* @ts-ignore */
|
|
11020
11516
|
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
11021
11517
|
/** Cỡ đã quy bí danh: `'md'` -> `'base'`. Template so với cái này. */
|
|
11022
|
-
resolvedSize = computed(() =>
|
|
11518
|
+
resolvedSize = computed(() => {
|
|
11519
|
+
const size = this.size();
|
|
11520
|
+
// `md` là BÍ DANH của `base` — gập ở đây để kiểu trả về đúng `FieldSize`,
|
|
11521
|
+
// nhờ vậy tra bảng chuẩn không cần ép kiểu.
|
|
11522
|
+
return size === 'md' ? 'base' : size;
|
|
11523
|
+
}, /* @ts-ignore */
|
|
11023
11524
|
...(ngDevMode ? [{ debugName: "resolvedSize" }] : /* istanbul ignore next */ []));
|
|
11525
|
+
/**
|
|
11526
|
+
* Đệm ngang theo BẢNG CHUẨN họ form. Chỉ dùng khi KHÔNG `multiple` —
|
|
11527
|
+
* chế độ nhiều thẻ có đệm riêng (`px-1.5 py-1`) vì các thẻ tự mang đệm.
|
|
11528
|
+
*
|
|
11529
|
+
* Ba lỗi cũ gộp làm một chỗ này (đo /autocomplete 2026-09-24):
|
|
11530
|
+
* sm `[class.px-2.5]` -> Angular CẮT ở dấu chấm, render `px-2` = 8px
|
|
11531
|
+
* thay vì 10px. Đây là lý do phải gom vào chuỗi, không bind lẻ.
|
|
11532
|
+
* base dùng chung `px-3.5` với `lg` = 14px, bảng chuẩn quy định 12px.
|
|
11533
|
+
* xl có `px-4` nhưng nằm ở `[class.px-4]` rời, dễ lệch khỏi ba cỡ kia.
|
|
11534
|
+
*/
|
|
11535
|
+
fieldPaddingClass = computed(() => FIELD_PADDING_X[this.resolvedSize()], /* @ts-ignore */
|
|
11536
|
+
...(ngDevMode ? [{ debugName: "fieldPaddingClass" }] : /* istanbul ignore next */ []));
|
|
11537
|
+
/**
|
|
11538
|
+
* Cỡ chữ theo BẢNG CHUẨN. Trước đây `[class.text-base]` chỉ khai cho `lg`,
|
|
11539
|
+
* `xl` không có nhánh nào nên rơi về 16px MẶC ĐỊNH của trình duyệt — trùng
|
|
11540
|
+
* giá trị đúng một cách tình cờ, hỏng ngay khi cỡ gốc đổi.
|
|
11541
|
+
*/
|
|
11542
|
+
fieldTextClass = computed(() => FIELD_TEXT[this.resolvedSize()], /* @ts-ignore */
|
|
11543
|
+
...(ngDevMode ? [{ debugName: "fieldTextClass" }] : /* istanbul ignore next */ []));
|
|
11024
11544
|
state = input('default', /* @ts-ignore */
|
|
11025
11545
|
...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
|
|
11026
11546
|
required = input(false, /* @ts-ignore */
|
|
@@ -11188,7 +11708,7 @@ class OlAutocomplete {
|
|
|
11188
11708
|
const el = this.panel()?.nativeElement;
|
|
11189
11709
|
const raBody = this.appendTo() === 'body';
|
|
11190
11710
|
if (!open || !el) {
|
|
11191
|
-
this.
|
|
11711
|
+
this.detachPanelNow();
|
|
11192
11712
|
return;
|
|
11193
11713
|
}
|
|
11194
11714
|
/**
|
|
@@ -11222,11 +11742,11 @@ class OlAutocomplete {
|
|
|
11222
11742
|
this.destroyRef.onDestroy(() => {
|
|
11223
11743
|
if (this.hengio)
|
|
11224
11744
|
clearTimeout(this.hengio);
|
|
11225
|
-
this.
|
|
11745
|
+
this.detachPanelNow();
|
|
11226
11746
|
});
|
|
11227
11747
|
}
|
|
11228
11748
|
/** Ngắt theo dõi vị trí và trả panel về chỗ cũ. Gọi được nhiều lần. */
|
|
11229
|
-
|
|
11749
|
+
detachPanelNow() {
|
|
11230
11750
|
if (this.stopTracking) {
|
|
11231
11751
|
this.stopTracking();
|
|
11232
11752
|
this.stopTracking = null;
|
|
@@ -11394,7 +11914,7 @@ class OlAutocomplete {
|
|
|
11394
11914
|
this.searchQuery.set(val);
|
|
11395
11915
|
this.isOpen.set(true);
|
|
11396
11916
|
this.activeIndex.set(0);
|
|
11397
|
-
this.
|
|
11917
|
+
this.emitSearch(val);
|
|
11398
11918
|
}
|
|
11399
11919
|
/**
|
|
11400
11920
|
* Phát `search` có GỠ NẢY. Gõ "nguyễn văn a" là 12 phím — không gỡ nảy thì
|
|
@@ -11406,7 +11926,7 @@ class OlAutocomplete {
|
|
|
11406
11926
|
*
|
|
11407
11927
|
* Dưới `minTermLength` thì KHÔNG phát: nơi gọi không phải tự kiểm lại.
|
|
11408
11928
|
*/
|
|
11409
|
-
|
|
11929
|
+
emitSearch(val) {
|
|
11410
11930
|
if (this.hengio) {
|
|
11411
11931
|
clearTimeout(this.hengio);
|
|
11412
11932
|
this.hengio = null;
|
|
@@ -11542,7 +12062,7 @@ class OlAutocomplete {
|
|
|
11542
12062
|
useExisting: forwardRef(() => OlAutocomplete),
|
|
11543
12063
|
multi: true,
|
|
11544
12064
|
},
|
|
11545
|
-
], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.px-2.5]=\"!multiple() && resolvedSize() === 'sm'\"\n [class.px-4]=\"!multiple() && resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"{\n 'px-1.5 py-1': multiple(),\n 'px-3.5': !multiple() && (resolvedSize() === 'base' || resolvedSize() === 'lg'),\n }\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg'\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12065
|
+
], queries: [{ propertyName: "optionTemplate", first: true, predicate: ["optionTemplate"], descendants: true }], viewQueries: [{ propertyName: "panel", first: true, predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"multiple() ? 'px-1.5 py-1' : fieldPaddingClass()\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [ngClass]=\"fieldTextClass()\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlSpinner, selector: "ol-spinner", inputs: ["variant", "size", "color", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11546
12066
|
}
|
|
11547
12067
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAutocomplete, decorators: [{
|
|
11548
12068
|
type: Component,
|
|
@@ -11558,7 +12078,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
11558
12078
|
useExisting: forwardRef(() => OlAutocomplete),
|
|
11559
12079
|
multi: true,
|
|
11560
12080
|
},
|
|
11561
|
-
], template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.px-2.5]=\"!multiple() && resolvedSize() === 'sm'\"\n [class.px-4]=\"!multiple() && resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"{\n 'px-1.5 py-1': multiple(),\n 'px-3.5': !multiple() && (resolvedSize() === 'base' || resolvedSize() === 'lg'),\n }\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\n [class.text-base]=\"resolvedSize() === 'lg'\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
|
|
12081
|
+
], template: "<div class=\"relative w-full\">\n @if (label()) {\n <div class=\"flex items-center justify-between mb-1.5\">\n <label\n [for]=\"inputId()\"\n class=\"inline-flex items-center gap-1 text-sm font-medium text-heading\"\n >\n <span>{{ label() }}</span>\n @if (required() || showAsterisk()) {\n <span class=\"text-fg-danger font-bold\">*</span>\n }\n </label>\n </div>\n }\n\n <!--\n Main Input Box Container.\n\n `py-1` ch\u1EE9 KH\u00D4NG ph\u1EA3i `p-1.5` cho ch\u1EBF \u0111\u1ED9 multiple: `p-1.5` \u0111\u1EC7m M\u1ECCI PH\u00CDA 6px,\n c\u1ED9ng v\u00E0o n\u1ED9i dung 34px th\u00E0nh 46px \u2014 v\u01B0\u1EE3t `min-h-10` (40px). \u0110o 2026-09-17:\n \u00F4 \u0111a ch\u1ECDn cao **46px** trong khi \u00F4 \u0111\u01A1n ch\u1ECDn ngay c\u1EA1nh cao **40px**, hai \u00F4\n c\u00F9ng m\u1ED9t h\u00E0ng l\u01B0\u1EDBi n\u00EAn l\u1EC7ch th\u1EA5y r\u00F5. \u0110\u1EC7m d\u1ECDc 4px v\u1EEBa \u0111\u1EE7 cho th\u1EBB cao 24px\n (4 + 24 + 4 = 32 < 40) m\u00E0 v\u1EABn c\u00F2n ch\u1ED7 khi th\u1EBB xu\u1ED1ng d\u00F2ng th\u1EE9 hai.\n\n `content-center` \u0111i k\u00E8m `items-center`: v\u1EDBi `flex-wrap`, `items-center` ch\u1EC9\n c\u0103n gi\u1EEFa TRONG T\u1EEANG D\u00D2NG, c\u1EA3 kh\u1ED1i nhi\u1EC1u d\u00F2ng v\u1EABn d\u00EDnh m\u00E9p tr\u00EAn.\n -->\n <div\n class=\"relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\n [class.rounded-md]=\"true\"\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\n [class.min-h-10]=\"resolvedSize() === 'base'\"\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\n [class.border-brand]=\"isOpen() && state() === 'default'\"\n [class.ring-2]=\"isOpen()\"\n [class.ring-brand-subtle]=\"isOpen()\"\n [class.border-danger]=\"state() === 'error' && !disabled()\"\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\n [class.border-success]=\"state() === 'success' && !disabled()\"\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\n [class.bg-disabled]=\"disabled() || readonly()\"\n [class.cursor-not-allowed]=\"disabled()\"\n [class.opacity-60]=\"disabled()\"\n [ngClass]=\"multiple() ? 'px-1.5 py-1' : fieldPaddingClass()\"\n >\n <!--\n Icon k\u00EDnh l\u00FAp LU\u00D4N hi\u1EC7n. B\u1EA3n c\u0169 \u1EA9n n\u00F3 khi `multiple` \u0111\u00E3 c\u00F3 gi\u00E1 tr\u1ECB\n (`@if (!multiple() || !value() || value()?.length === 0)`) \u2014 ch\u1ECDn m\u1ED9t th\u1EBB\n l\u00E0 icon bi\u1EBFn m\u1EA5t, xo\u00E1 h\u1EBFt th\u1EBB th\u00EC n\u00F3 quay l\u1EA1i. \u0110o 2026-09-17 tr\u00EAn\n `/autocomplete`: \u00F4 \u0111\u01A1n ch\u1ECDn 1 icon `magnifer`, \u00F4 \u0111a ch\u1ECDn **0 icon** khi\n c\u00F3 2 th\u1EBB.\n\n Figma kh\u00F4ng c\u00F3 c\u01A1 s\u1EDF cho vi\u1EC7c \u1EA9n: \u0111\u1ECDc `ol-autocomplete-examples`\n (#1149:53126) th\u00EC **6/6 Type \u0111\u1EC1u c\u00F3 `search` trong `ol-input`**, kh\u00F4ng\n Type n\u00E0o b\u1ECF. Figma c\u0169ng kh\u00F4ng v\u1EBD bi\u1EBFn th\u1EC3 \u0111a ch\u1ECDn c\u00F3 th\u1EBB, n\u00EAn \u0111\u00E2y l\u00E0 l\u1EF1a\n ch\u1ECDn c\u1EE7a code ch\u1EE9 kh\u00F4ng ph\u1EA3i thi\u1EBFt k\u1EBF.\n\n `shrink-0` \u0111\u1EC3 icon kh\u00F4ng b\u1ECB c\u00E1c th\u1EBB \u00E9p b\u1EB9p khi \u00F4 ch\u1EADt.\n -->\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0\" />\n\n <!-- Multiple Selected Tags / Chips -->\n @if (multiple() && value() && value()?.length > 0) {\n @for (itemVal of value(); track itemVal) {\n <!--\n `h-6` c\u1ED1 \u0111\u1ECBnh (24px): \u0111\u1EC3 th\u1EBB t\u1EF1 co theo n\u1ED9i dung th\u00EC m\u1ED7i th\u1EBB cao m\u1ED9t\n ki\u1EC3u v\u00E0 \u00F4 nh\u1EADp b\u1ECB \u0111\u1EA9y theo th\u1EBB cao nh\u1EA5t. 24px + \u0111\u1EC7m d\u1ECDc 4\u00D72 = 32px,\n v\u1EABn n\u1EB1m trong `min-h-10` (40px) n\u00EAn \u00F4 \u0111a ch\u1ECDn cao \u0111\u00FAng b\u1EB1ng \u00F4 \u0111\u01A1n ch\u1ECDn.\n -->\n <span\n class=\"inline-flex h-6 items-center gap-1 px-2 text-xs font-medium bg-brand-softer text-fg-brand-strong rounded-md border border-brand-subtle shrink-0\"\n >\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\n @if (!disabled() && !readonly()) {\n <!-- Vu\u00F4ng nh\u01B0 n\u00FAt xo\u00E1 chung \u2014 xem l\u00FD do \u1EDF \u0111\u00F3. -->\n <button\n type=\"button\"\n (click)=\"removeTag(itemVal, $event)\"\n [attr.aria-label]=\"'G\u1EE1 ' + getDisplayLabelForValue(itemVal)\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-fg-brand hover:text-fg-brand-strong focus:outline-hidden\"\n >\n <ol-icon name=\"close\" size=\"xs\" />\n </button>\n }\n </span>\n }\n }\n\n <!-- Input text field -->\n <input\n [id]=\"inputId()\"\n [attr.aria-invalid]=\"ariaInvalid()\"\n [attr.aria-describedby]=\"describedBy()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"listboxId()\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n type=\"text\"\n [disabled]=\"disabled()\"\n [readonly]=\"readonly()\"\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\n [ngModel]=\"searchQuery()\"\n (ngModelChange)=\"onSearchChange($event)\"\n (focus)=\"onInputFocus()\"\n (keydown)=\"onKeyDown($event)\"\n class=\"flex-1 min-w-20 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\n [ngClass]=\"fieldTextClass()\"\n />\n\n <!-- M\u1ED8T \u00F4 \u0111i\u1EC1u khi\u1EC3n, \u0111\u1ED5i vai theo tr\u1EA1ng th\u00E1i \u2014 kh\u00F4ng x\u1EBFp c\u1EA1nh nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y n\u00FAt xo\u00E1 v\u00E0 `chevron-down` C\u00D9NG hi\u1EC7n: \u0111o /autocomplete ng\u00E0y\n 2026-09-16 \u0111\u1EBFm 3 ph\u1EA7n t\u1EED chen trong c\u1EE5m 44px, c\u1ED9ng spinner khi `loading`\n l\u00E0 b\u1ED1n. \u00D4 r\u1ED9ng c\u1ED1 \u0111\u1ECBnh `w-7` \u0111\u1EC3 khung kh\u00F4ng nh\u1EA3y khi icon \u0111\u1ED5i vai.\n\n `justify-end` ch\u1EE9 KH\u00D4NG `justify-center`: \u00F4 r\u1ED9ng 28px m\u00E0 icon ch\u1EC9 16px\n n\u00EAn c\u0103n gi\u1EEFa \u0111\u1EC3 d\u01B0 6px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong. \u0110o 2026-09-17 tr\u00EAn\n 13 \u00F4 c\u1EE7a /autocomplete: l\u1EC1 tr\u00E1i 15px, l\u1EC1 ph\u1EA3i **21px** \u2014 l\u1EC7ch \u0111\u00FAng 6px\n \u1EDF M\u1ECCI \u00F4, c\u1EA3 \u0111\u01A1n ch\u1ECDn l\u1EABn \u0111a ch\u1ECDn. Con s\u1ED1 kh\u00F4ng \u0111\u1ED5i theo n\u1ED9i dung n\u00EAn\n \u0111\u00E2y l\u00E0 l\u1ED7i c\u1EA5u tr\u00FAc, kh\u00F4ng ph\u1EA3i do ch\u1EEF d\u00E0i ng\u1EAFn.\n\n `ol-select` \u0111\u00E3 ch\u1ED1t `justify-end` t\u1EEB l\u01B0\u1EE3t tr\u01B0\u1EDBc; \u0111\u00E2y l\u00E0 l\u1EA7n s\u1EEDa cho\n hai component kh\u1EDBp nhau. -->\n <div class=\"ms-auto flex w-7 shrink-0 items-center justify-end\">\n @if (loading()) {\n <ol-spinner size=\"xs\" color=\"brand\" />\n } @else if (\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\n !disabled() &&\n !readonly()\n ) {\n <!--\n N\u00FAt b\u1ECDc `size-4` \u0110\u00DANG B\u1EB0NG icon, kh\u00F4ng `size-6`.\n\n `size-6` (24px) b\u1ECDc SVG 16px th\u00EC d\u01B0 4px m\u1ED7i b\u00EAn, \u0111\u1EA9y icon v\u00E0o trong:\n \u0111o 2026-09-17 th\u1EA5y \u00F4 \u0111ang hi\u1EC7n n\u00FAt xo\u00E1 c\u00F3 l\u1EC1 ph\u1EA3i **19px** trong khi\n \u00F4 hi\u1EC7n chevron tr\u1EA7n ch\u1EC9 15px \u2014 c\u00F9ng m\u1ED9t component m\u00E0 hai tr\u1EA1ng th\u00E1i\n l\u1EC7ch nhau 4px, \u0111\u00FAng ch\u1ED7 anh ch\u1EC9 trong \u1EA3nh.\n\n V\u00F9ng b\u1EA5m v\u1EABn \u0111\u1EE7 l\u1EDBn nh\u1EDD `p-1` \u1EDF NGO\u00C0I khung \u0111o: `size-4` + \u0111\u1EC7m\n kh\u00F4ng c\u1ED9ng v\u00E0o b\u1EC1 r\u1ED9ng chi\u1EBFm ch\u1ED7 v\u00EC n\u00FAt n\u1EB1m trong \u00F4 `w-7` c\u1ED1 \u0111\u1ECBnh.\n -->\n <button\n type=\"button\"\n (click)=\"clearAll($event)\"\n aria-label=\"Xo\u00E1 to\u00E0n b\u1ED9 l\u1EF1a ch\u1ECDn\"\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\n >\n <ol-icon name=\"close\" size=\"sm\" />\n </button>\n } @else {\n <ol-icon\n name=\"chevron-down\"\n size=\"sm\"\n class=\"transition-transform\"\n [class.rotate-180]=\"isOpen()\"\n />\n }\n </div>\n </div>\n\n <!--\n Dropdown Suggestions Menu.\n\n Token \u0111\u1ECDc t\u1EEB Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\n bo g\u00F3c 12px \u00B7 \u0111\u1EC7m 8px \u00B7 n\u1EC1n `bg-neutral-primary-medium` \u00B7 vi\u1EC1n\n `border-default-medium`.\n\n Hai token n\u1EC1n/vi\u1EC1n TR\u00D9NG NHAU \u1EDF ch\u1EBF \u0111\u1ED9 s\u00E1ng n\u00EAn \u0111\u1ED5i hay kh\u00F4ng nh\u00ECn c\u0169ng\n th\u1EBF \u2014 nh\u01B0ng CH\u1EBE \u0110\u1ED8 T\u1ED0I kh\u00E1c h\u1EB3n: `bg-primary` l\u00E0 gray-950 (g\u1EA7n \u0111en, panel\n ch\u00ECm v\u00E0o n\u1EC1n) c\u00F2n `bg-primary-medium` l\u00E0 gray-800 (panel n\u1ED5i l\u00EAn).\n `ol-select` c\u00F9ng l\u1EC7ch n\u00E0y, s\u1EEDa chung m\u1ED9t l\u01B0\u1EE3t.\n -->\n @if (isOpen()) {\n <div\n #panel\n [id]=\"listboxId()\"\n role=\"listbox\"\n [attr.aria-label]=\"label() || 'G\u1EE3i \u00FD'\"\n class=\"absolute z-dropdown bg-neutral-primary-medium border border-default-medium rounded-md shadow-lg max-h-[var(--ol-overlay-max-height,15rem)] overflow-y-auto overflow-x-hidden p-2 animate-in fade-in zoom-in-95 duration-150\"\n [class.w-full]=\"matchTriggerWidth()\"\n [class]=\"menuClass()\"\n >\n @if (filteredOptions().length > 0) {\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\n <!-- Ti\u00EAu \u0111\u1EC1 nh\u00F3m ch\u00E8n gi\u1EEFa v\u00F2ng l\u1EB7p ph\u1EB3ng, xem `groupNameAt`. -->\n @if (groupNameAt(idx); as groupName) {\n <div\n role=\"presentation\"\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\n >\n {{ groupName }}\n </div>\n }\n <div\n [id]=\"optionId(idx)\"\n role=\"option\"\n [attr.aria-selected]=\"isSelected(item)\"\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\n (click)=\"selectOption(item)\"\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\n [class.opacity-50]=\"item?.disabled\"\n [class.cursor-not-allowed]=\"item?.disabled\"\n >\n @if (optionTemplate) {\n <!-- Custom Option Template -->\n <ng-container\n *ngTemplateOutlet=\"\n optionTemplate;\n context: { $implicit: item, selected: isSelected(item) }\n \"\n />\n } @else {\n <!-- Default Option Display -->\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\n @if (item?.avatar) {\n <img\n [src]=\"item.avatar\"\n alt=\"\u1EA2nh \u0111\u1EA1i di\u1EC7n\"\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\n />\n }\n <div class=\"truncate\">\n <span class=\"font-medium text-heading block truncate\">{{\n getItemLabel(item)\n }}</span>\n @if (item?.subLabel) {\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\n }\n </div>\n </div>\n\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\n @if (item?.badge) {\n <span\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\n >\n {{ item.badge }}\n </span>\n }\n @if (isSelected(item)) {\n <ol-icon name=\"check\" size=\"sm\" />\n }\n </div>\n }\n </div>\n }\n } @else if (waitingForTerm()) {\n <!--\n Ch\u1EBF \u0111\u1ED9 L\u1ECCC T\u1EEA XA, ch\u01B0a g\u00F5 \u0111\u1EE7 `minTermLength`. Ph\u1EA3i l\u00E0 th\u00F4ng b\u00E1o RI\u00CANG\n ch\u1EE9 kh\u00F4ng d\u00F9ng `emptyText`: \"Kh\u00F4ng t\u00ECm th\u1EA5y k\u1EBFt qu\u1EA3\" l\u00E0 sai s\u1EF1 th\u1EADt \u2014\n ch\u01B0a t\u00ECm g\u00EC c\u1EA3. C\u00F9ng l\u1ED1i v\u1EDBi `typeToSearchText` c\u1EE7a ng-select.\n -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ typeToSearchText() }}</p>\n </div>\n } @else {\n <!-- Empty State & Allow Create Option -->\n <div class=\"p-3 text-center text-sm text-body-subtle\">\n <p>{{ emptyText() }}</p>\n @if (allowCreate() && searchQuery().trim()) {\n <button\n type=\"button\"\n (click)=\"onCreateNew()\"\n class=\"mt-2 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-fg-brand bg-brand-softer hover:bg-brand-subtle rounded-lg transition-colors\"\n >\n <ol-icon name=\"plus\" size=\"xs\" />\n <span>Th\u00EAm m\u1EDBi: \"{{ searchQuery().trim() }}\"</span>\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!-- Helper Text & Error Text -->\n @if (state() === 'error' && errorText()) {\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\n } @else if (helperText()) {\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\n }\n</div>\n" }]
|
|
11562
12082
|
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], labelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKey", required: false }] }], valueKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueKey", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showAsterisk: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAsterisk", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], errorText: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorText", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], allowCreate: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCreate", required: false }] }], emptyText: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyText", required: false }] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], menuClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuClass", required: false }] }], matchTriggerWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "matchTriggerWidth", required: false }] }], groupBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupBy", required: false }] }], remoteSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "remoteSearch", required: false }] }], minTermLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minTermLength", required: false }] }], debounceMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "debounceMs", required: false }] }], typeToSearchText: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeToSearchText", required: false }] }], createOption: [{ type: i0.Output, args: ["createOption"] }], search: [{ type: i0.Output, args: ["search"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], cleared: [{ type: i0.Output, args: ["cleared"] }], added: [{ type: i0.Output, args: ["added"] }], removed: [{ type: i0.Output, args: ["removed"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], optionTemplate: [{
|
|
11563
12083
|
type: ContentChild,
|
|
11564
12084
|
args: ['optionTemplate']
|
|
@@ -11645,186 +12165,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
11645
12165
|
args: [{ selector: 'ol-list-group-item', standalone: true, imports: [NgTemplateOutlet], host: { class: 'block w-full' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<li\n (click)=\"handleClick($event)\"\n class=\"w-full transition-colors\"\n [class]=\"textClasses()\"\n [class.border-b]=\"hasDivider()\"\n [class.border-default-strong]=\"hasDivider()\"\n>\n <!--\n `href` bi\u1EBFn m\u1EE5c th\u00E0nh li\u00EAn k\u1EBFt. Th\u1EBB `<a>` n\u1EB1m B\u00CAN TRONG `<li>`, kh\u00F4ng thay\n th\u1EBF n\u00F3 \u2014 `<a>` tr\u1EF1c ti\u1EBFp trong `<ul>` l\u00E0 HTML h\u1ECFng. \u0110\u1EC7m chuy\u1EC3n xu\u1ED1ng th\u1EBB\n trong c\u00F9ng \u0111\u1EC3 v\u00F9ng b\u1EA5m ph\u1EE7 tr\u1ECDn m\u1EE5c.\n -->\n @if (href() && !disabled()) {\n <a [href]=\"href()\" class=\"block w-full px-4 py-2\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n } @else {\n <span class=\"block w-full px-4 py-2\" [attr.aria-disabled]=\"disabled() ? 'true' : null\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n }\n</li>\n\n<ng-template #ruot><ng-content /></ng-template>\n" }]
|
|
11646
12166
|
}], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
|
|
11647
12167
|
|
|
11648
|
-
/**
|
|
11649
|
-
* Tiêu đề Modal — khớp Figma `ol-modal-header` (node 1149:71403).
|
|
11650
|
-
* Figma có 7 biến thể: Default, Heading & supporting text, Icon + Heading &
|
|
11651
|
-
* helper text, With Logo, Logo & icons, Avatar group, Breadcrumb.
|
|
11652
|
-
* Component lo phần khung dùng chung; nội dung giàu hơn lắp qua <ng-content>.
|
|
11653
|
-
*/
|
|
11654
|
-
/*
|
|
11655
|
-
* GHÉP VỚI `ol-modal`: phần đầu của `ol-modal` nằm trong `@if (title() ||
|
|
11656
|
-
* dismissible())`, nên muốn dùng component này thì phải TẮT nó đi —
|
|
11657
|
-
* `title=""` kèm `[dismissible]="false"` — rồi lắp vào `<ng-content />` trần
|
|
11658
|
-
* của phần thân. Để nguyên `title` là lồng hai khung `p-5 border-b`.
|
|
11659
|
-
*/
|
|
11660
|
-
class OlModalHeader {
|
|
11661
|
-
/**
|
|
11662
|
-
* Bậc thẻ tiêu đề (h1–h6). Mặc định 3 — giữ nguyên bậc đang chạy.
|
|
11663
|
-
*
|
|
11664
|
-
* Khoá cứng một bậc thì nơi gọi không sửa được: đặt component này sau
|
|
11665
|
-
* `<h2>` của trang là nhảy bậc.
|
|
11666
|
-
*/
|
|
11667
|
-
headingLevel = input(3, /* @ts-ignore */
|
|
11668
|
-
...(ngDevMode ? [{ debugName: "headingLevel" }] : /* istanbul ignore next */ []));
|
|
11669
|
-
heading = input('', /* @ts-ignore */
|
|
11670
|
-
...(ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []));
|
|
11671
|
-
/**
|
|
11672
|
-
* Icon đầu tiêu đề. Rỗng thì KHÔNG dựng khung — không để lại thẻ trống.
|
|
11673
|
-
*
|
|
11674
|
-
* Figma `Type=Icon + Heading & helper text` (#1149:71412) vẽ khung 48px bo
|
|
11675
|
-
* 12px, icon 28px bên trong. Đó đúng là `ol-icon-shape` size `lg`, nên dùng
|
|
11676
|
-
* lại thay vì tự vẽ khung.
|
|
11677
|
-
*
|
|
11678
|
-
* `color="white"` chứ KHÔNG `gray`: Figma bind nền
|
|
11679
|
-
* `bg-neutral-primary-medium` (trắng) kèm viền `border-default-medium`, mà
|
|
11680
|
-
* trong `ol-icon-shape` chỉ nhánh `white` có cặp đó — `gray` ra nền xám đặc
|
|
11681
|
-
* và KHÔNG viền. Đo /modal lượt đầu: nền rgb(243,244,246), viền 0px.
|
|
11682
|
-
*/
|
|
11683
|
-
icon = input('', /* @ts-ignore */
|
|
11684
|
-
...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
11685
|
-
/**
|
|
11686
|
-
* Mô tả phụ dưới tiêu đề.
|
|
11687
|
-
*
|
|
11688
|
-
* Tên `description` theo `ol-alert-dialog` — thứ dựng trên chính `ol-modal`
|
|
11689
|
-
* — và `ol-empty`. `helperText` mang nghĩa "chú thích dưới ô nhập" ở họ form
|
|
11690
|
-
* (16 component dùng), đặt cho tiêu đề hộp thoại là lệch nghĩa.
|
|
11691
|
-
*/
|
|
11692
|
-
description = input('', /* @ts-ignore */
|
|
11693
|
-
...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
|
|
11694
|
-
/** @deprecated Dùng `description`. Giữ để không phá mã đang chạy. */
|
|
11695
|
-
supportingText = input('', /* @ts-ignore */
|
|
11696
|
-
...(ngDevMode ? [{ debugName: "supportingText" }] : /* istanbul ignore next */ []));
|
|
11697
|
-
/** @deprecated Dùng `description`. Tên cũ của `ol-drawer-header`. */
|
|
11698
|
-
helperText = input('', /* @ts-ignore */
|
|
11699
|
-
...(ngDevMode ? [{ debugName: "helperText" }] : /* istanbul ignore next */ []));
|
|
11700
|
-
/**
|
|
11701
|
-
* Cái nào được TRUYỀN thì thắng — không phụ thuộc giá trị.
|
|
11702
|
-
*
|
|
11703
|
-
* `ol-badge` từng hoà giải hai input cùng nghĩa bằng cách so với giá trị mặc
|
|
11704
|
-
* định (`theme` thắng trừ khi nó bằng `'gray'`), nên truyền cả hai thì một
|
|
11705
|
-
* cái bị nuốt im lặng. Ở đây chuỗi rỗng là "không truyền", nên chỉ cần lấy
|
|
11706
|
-
* cái đầu tiên khác rỗng.
|
|
11707
|
-
*/
|
|
11708
|
-
descriptionText = computed(() => this.description() || this.supportingText() || this.helperText(), /* @ts-ignore */
|
|
11709
|
-
...(ngDevMode ? [{ debugName: "descriptionText" }] : /* istanbul ignore next */ []));
|
|
11710
|
-
showClose = input(true, /* @ts-ignore */
|
|
11711
|
-
...(ngDevMode ? [{ debugName: "showClose" }] : /* istanbul ignore next */ []));
|
|
11712
|
-
closeClick = output();
|
|
11713
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11714
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlModalHeader, isStandalone: true, selector: "ol-modal-header", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, supportingText: { classPropertyName: "supportingText", publicName: "supportingText", isSignal: true, isRequired: false, transformFunction: null }, helperText: { classPropertyName: "helperText", publicName: "helperText", isSignal: true, isRequired: false, transformFunction: null }, showClose: { classPropertyName: "showClose", publicName: "showClose", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeClick: "closeClick" }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
11715
|
-
<!--
|
|
11716
|
-
PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
|
|
11717
|
-
kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
|
|
11718
|
-
và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
|
|
11719
|
-
CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
|
|
11720
|
-
-->
|
|
11721
|
-
<div class="flex items-start justify-between gap-3 p-3">
|
|
11722
|
-
@if (icon()) {
|
|
11723
|
-
<ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
|
|
11724
|
-
}
|
|
11725
|
-
<div class="min-w-0 flex-1">
|
|
11726
|
-
@if (heading()) {
|
|
11727
|
-
@switch (headingLevel()) {
|
|
11728
|
-
@case (1) {
|
|
11729
|
-
<h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
|
|
11730
|
-
}
|
|
11731
|
-
@case (2) {
|
|
11732
|
-
<h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
|
|
11733
|
-
}
|
|
11734
|
-
@case (4) {
|
|
11735
|
-
<h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
|
|
11736
|
-
}
|
|
11737
|
-
@case (5) {
|
|
11738
|
-
<h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
|
|
11739
|
-
}
|
|
11740
|
-
@case (6) {
|
|
11741
|
-
<h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
|
|
11742
|
-
}
|
|
11743
|
-
@default {
|
|
11744
|
-
<h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
|
|
11745
|
-
}
|
|
11746
|
-
}
|
|
11747
|
-
}
|
|
11748
|
-
@if (descriptionText()) {
|
|
11749
|
-
<p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
|
|
11750
|
-
}
|
|
11751
|
-
<ng-content />
|
|
11752
|
-
</div>
|
|
11753
|
-
@if (showClose()) {
|
|
11754
|
-
<button
|
|
11755
|
-
type="button"
|
|
11756
|
-
(click)="closeClick.emit()"
|
|
11757
|
-
aria-label="Đóng hộp thoại"
|
|
11758
|
-
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
|
|
11759
|
-
>
|
|
11760
|
-
<ol-icon name="close" size="sm" />
|
|
11761
|
-
</button>
|
|
11762
|
-
}
|
|
11763
|
-
</div>
|
|
11764
|
-
`, isInline: true, dependencies: [{ kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlIconShape, selector: "ol-icon-shape", inputs: ["icon", "size", "color", "type"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11765
|
-
}
|
|
11766
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlModalHeader, decorators: [{
|
|
11767
|
-
type: Component,
|
|
11768
|
-
args: [{
|
|
11769
|
-
selector: 'ol-modal-header',
|
|
11770
|
-
standalone: true,
|
|
11771
|
-
imports: [OlIcon, OlIconShape],
|
|
11772
|
-
template: `
|
|
11773
|
-
<!--
|
|
11774
|
-
PHẲNG, KHÔNG đường kẻ — CỐ Ý LỆCH FIGMA (2026-09-23) theo yêu cầu thiết
|
|
11775
|
-
kế. Figma \`ol-modal-header\` #1149:71403 vẫn còn \`border-b border-default\`
|
|
11776
|
-
và mô tả component còn ghi "đường kẻ dưới" là quy ước chung ba overlay.
|
|
11777
|
-
CÒN NỢ sửa Figma cho khớp — đừng thêm lại khi đối chiếu thấy lệch.
|
|
11778
|
-
-->
|
|
11779
|
-
<div class="flex items-start justify-between gap-3 p-3">
|
|
11780
|
-
@if (icon()) {
|
|
11781
|
-
<ol-icon-shape [icon]="icon()" size="lg" color="white" type="square" class="shrink-0" />
|
|
11782
|
-
}
|
|
11783
|
-
<div class="min-w-0 flex-1">
|
|
11784
|
-
@if (heading()) {
|
|
11785
|
-
@switch (headingLevel()) {
|
|
11786
|
-
@case (1) {
|
|
11787
|
-
<h1 class="truncate text-xl font-bold text-heading">{{ heading() }}</h1>
|
|
11788
|
-
}
|
|
11789
|
-
@case (2) {
|
|
11790
|
-
<h2 class="truncate text-xl font-bold text-heading">{{ heading() }}</h2>
|
|
11791
|
-
}
|
|
11792
|
-
@case (4) {
|
|
11793
|
-
<h4 class="truncate text-xl font-bold text-heading">{{ heading() }}</h4>
|
|
11794
|
-
}
|
|
11795
|
-
@case (5) {
|
|
11796
|
-
<h5 class="truncate text-xl font-bold text-heading">{{ heading() }}</h5>
|
|
11797
|
-
}
|
|
11798
|
-
@case (6) {
|
|
11799
|
-
<h6 class="truncate text-xl font-bold text-heading">{{ heading() }}</h6>
|
|
11800
|
-
}
|
|
11801
|
-
@default {
|
|
11802
|
-
<h3 class="truncate text-xl font-bold text-heading">{{ heading() }}</h3>
|
|
11803
|
-
}
|
|
11804
|
-
}
|
|
11805
|
-
}
|
|
11806
|
-
@if (descriptionText()) {
|
|
11807
|
-
<p class="mt-1 text-sm text-body">{{ descriptionText() }}</p>
|
|
11808
|
-
}
|
|
11809
|
-
<ng-content />
|
|
11810
|
-
</div>
|
|
11811
|
-
@if (showClose()) {
|
|
11812
|
-
<button
|
|
11813
|
-
type="button"
|
|
11814
|
-
(click)="closeClick.emit()"
|
|
11815
|
-
aria-label="Đóng hộp thoại"
|
|
11816
|
-
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer"
|
|
11817
|
-
>
|
|
11818
|
-
<ol-icon name="close" size="sm" />
|
|
11819
|
-
</button>
|
|
11820
|
-
}
|
|
11821
|
-
</div>
|
|
11822
|
-
`,
|
|
11823
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
11824
|
-
host: { class: 'block' },
|
|
11825
|
-
}]
|
|
11826
|
-
}], propDecorators: { headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], supportingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "supportingText", required: false }] }], helperText: [{ type: i0.Input, args: [{ isSignal: true, alias: "helperText", required: false }] }], showClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClose", required: false }] }], closeClick: [{ type: i0.Output, args: ["closeClick"] }] } });
|
|
11827
|
-
|
|
11828
12168
|
/**
|
|
11829
12169
|
* Dải nút hành động cuối Modal — khớp Figma `ol-modal-footer`.
|
|
11830
12170
|
* Figma có 4 biến thể: Buttons, 3 buttons, Buttons & links, Pagination.
|
|
@@ -11988,15 +12328,36 @@ class OlNavbarItem {
|
|
|
11988
12328
|
'inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg transition-all duration-150 whitespace-nowrap shrink-0',
|
|
11989
12329
|
/**
|
|
11990
12330
|
* Hover CHỈ cho mục KHÔNG active — cùng lý do với `ol-sidebar-item`:
|
|
11991
|
-
* `:hover` tính như một class nên hover (0,2,0) thắng
|
|
11992
|
-
* di chuột vào mục đang mở là mất
|
|
12331
|
+
* `:hover` tính như một class nên hover (0,2,0) thắng nền active
|
|
12332
|
+
* (0,1,0), di chuột vào mục đang mở là mất trạng thái. Xem chú thích
|
|
12333
|
+
* đầy đủ ở đó.
|
|
12334
|
+
*/
|
|
12335
|
+
'text-body not-[.active]:hover:text-fg-brand not-[.active]:hover:bg-brand-softer',
|
|
12336
|
+
/**
|
|
12337
|
+
* Trạng thái active — BÁM THEO `ol-sidebar-item`, xem ba đời màu ở đó.
|
|
12338
|
+
*
|
|
12339
|
+
* Đồng bộ 2026-09-24. Trước đó navbar kẹt lại ở ĐỜI 2 (`bg-brand` đặc +
|
|
12340
|
+
* chữ trắng + gạch mép trái 4px): commit 89b2e49 ngày 21/09 đổi cả hai
|
|
12341
|
+
* component sang đời 2 cùng lúc, nhưng khi sidebar chuyển sang đời 3
|
|
12342
|
+
* (c978f10 + 224d048 ngày 23/09 — nền nhạt `bg-brand-soft`, gỡ gạch
|
|
12343
|
+
* mép) thì navbar không được đổi theo. Chú thích ở đây vẫn ghi "cùng
|
|
12344
|
+
* luật với ol-sidebar-item" trong khi hai bên đã khác hẳn.
|
|
12345
|
+
*
|
|
12346
|
+
* Gỡ luôn gạch mép trái, cùng lý do sidebar đã gỡ: nền đã đánh dấu đủ
|
|
12347
|
+
* rõ, gạch là dấu hiệu thứ hai cho cùng một trạng thái, và nó ăn 4px
|
|
12348
|
+
* đệm trái làm chữ mục active lệch so với các mục còn lại — ở navbar
|
|
12349
|
+
* xếp NGANG thì lệch này thấy rõ hơn sidebar xếp dọc.
|
|
12350
|
+
*
|
|
12351
|
+
* VẪN LỆCH FIGMA CÓ CHỦ Ý. Figma `ol-navbar-item` #6375:261 cho
|
|
12352
|
+
* `State=Active` chỉ đổi màu chữ (`text-body` -> `text-fg-brand`),
|
|
12353
|
+
* KHÔNG nền, KHÔNG viền — đọc biến bind ngày 2026-09-24. Đó chính là
|
|
12354
|
+
* phương án đã bị bác hai lần vì nổi bật chỉ 1.06 lần so với nền trang.
|
|
12355
|
+
* Nợ: cập nhật Figma cho khớp đời 3, cho cả sidebar lẫn navbar.
|
|
11993
12356
|
*/
|
|
11994
|
-
'
|
|
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',
|
|
12357
|
+
'[&.active]:text-fg-brand [&.active]:bg-brand-soft [&.active]:font-semibold',
|
|
12358
|
+
this.active() ? 'active text-fg-brand bg-brand-soft font-semibold' : '',
|
|
12359
|
+
/** Hover của mục ĐANG active: ĐẬM thêm một bậc, không nhạt đi. */
|
|
12360
|
+
'[&.active]:hover:bg-brand-medium',
|
|
12000
12361
|
this.disabled() ? 'opacity-50 pointer-events-none' : 'cursor-pointer',
|
|
12001
12362
|
]
|
|
12002
12363
|
.filter(Boolean)
|
|
@@ -12004,11 +12365,11 @@ class OlNavbarItem {
|
|
|
12004
12365
|
}, /* @ts-ignore */
|
|
12005
12366
|
...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
|
|
12006
12367
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlNavbarItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
12007
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlNavbarItem, isStandalone: true, selector: "ol-navbar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "
|
|
12368
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlNavbarItem, isStandalone: true, selector: "ol-navbar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n BA NH\u00C1NH, `routerLinkActive` CH\u1EC8 \u1EDF nh\u00E1nh c\u00F3 `routerLink`.\n\n B\u1EA3n c\u0169 d\u00F9ng M\u1ED8T `<a>` mang c\u1EA3 `[routerLink]`, `[routerLinkActive]` v\u00E0\n `[class]=\"itemClasses()\"`. Hai th\u1EE9 sau c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh `class`, v\u00E0\n `RouterLinkActive` ghi SAU n\u00EAn n\u00F3 th\u1EAFng: chu\u1ED7i `'active \u2026'` m\u00E0 `itemClasses()`\n th\u00EAm khi `[active]=\"true\"` b\u1ECB nu\u1ED1t s\u1EA1ch.\n\n T\u00E1ch nh\u00E1nh th\u00F4i KH\u00D4NG \u0111\u1EE7 \u2014 nh\u00E1nh `routerLink` v\u1EABn c\u00F3 c\u1EA3 hai directive tr\u00EAn\n c\u00F9ng th\u1EBB. N\u00EAn `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET: \u0111\u00E3 \u00E9p active\n b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh, hai b\u00EAn h\u1EBFt tranh nhau.\n\n H\u1ECFng im l\u1EB7ng \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (ch\u00FAng n\u1EB1m trong c\u00F9ng chu\u1ED7i), ch\u1EC9 ri\u00EAng\n ch\u1EEF `active` m\u1EA5t. H\u1EADu qu\u1EA3 ch\u1EC9 l\u1ED9 ra khi r\u00EA chu\u1ED9t: \u0111o /navbar 2026-09-24, m\u1EE5c\n \u0111ang m\u1EDF NH\u1EA0T \u0110I (#e3eaff -> #f5f8ff) v\u00EC `not-[.active]:hover:bg-brand-softer`\n v\u1EABn \u00E1p d\u1EE5ng, c\u00F2n `[&.active]:hover:bg-brand-medium` th\u00EC kh\u00F4ng. Sidebar c\u00F9ng\n b\u1ED9 class nh\u01B0ng \u0110\u1EACM L\u00CAN \u0111\u00FAng (#d2dcfa) v\u00EC n\u00F3 \u0111\u00E3 t\u00E1ch nh\u00E1nh t\u1EEB tr\u01B0\u1EDBc.\n\n Unit test KH\u00D4NG b\u1EAFt \u0111\u01B0\u1EE3c: `provideRouter([])` kh\u00F4ng c\u00F3 route n\u00E0o kh\u1EDBp n\u00EAn\n directive kh\u00F4ng ch\u1EA1m v\u00E0o `class`, test xanh trong khi tr\u00ECnh duy\u1EC7t h\u1ECFng.\n\n N\u1ED9i dung g\u00F3i trong `ng-template` v\u00E0 chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`: khai\n nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh lo\u1EA1i tr\u1EEB nhau \u2014\n th\u00EC Angular ch\u1EC9 chi\u1EBFu v\u00E0o M\u1ED8T ch\u1ED7 v\u00E0 th\u1EF1c t\u1EBF m\u1EA5t \u1EDF m\u1ECDi nh\u00E1nh.\n-->\n<ng-template #itemContent>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span class=\"whitespace-nowrap\">{{ label() }}</span>\n }\n <ng-content />\n</ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else {\n <a [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12008
12369
|
}
|
|
12009
12370
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlNavbarItem, decorators: [{
|
|
12010
12371
|
type: Component,
|
|
12011
|
-
args: [{ selector: 'ol-navbar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "
|
|
12372
|
+
args: [{ selector: 'ol-navbar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n BA NH\u00C1NH, `routerLinkActive` CH\u1EC8 \u1EDF nh\u00E1nh c\u00F3 `routerLink`.\n\n B\u1EA3n c\u0169 d\u00F9ng M\u1ED8T `<a>` mang c\u1EA3 `[routerLink]`, `[routerLinkActive]` v\u00E0\n `[class]=\"itemClasses()\"`. Hai th\u1EE9 sau c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh `class`, v\u00E0\n `RouterLinkActive` ghi SAU n\u00EAn n\u00F3 th\u1EAFng: chu\u1ED7i `'active \u2026'` m\u00E0 `itemClasses()`\n th\u00EAm khi `[active]=\"true\"` b\u1ECB nu\u1ED1t s\u1EA1ch.\n\n T\u00E1ch nh\u00E1nh th\u00F4i KH\u00D4NG \u0111\u1EE7 \u2014 nh\u00E1nh `routerLink` v\u1EABn c\u00F3 c\u1EA3 hai directive tr\u00EAn\n c\u00F9ng th\u1EBB. N\u00EAn `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET: \u0111\u00E3 \u00E9p active\n b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh, hai b\u00EAn h\u1EBFt tranh nhau.\n\n H\u1ECFng im l\u1EB7ng \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (ch\u00FAng n\u1EB1m trong c\u00F9ng chu\u1ED7i), ch\u1EC9 ri\u00EAng\n ch\u1EEF `active` m\u1EA5t. H\u1EADu qu\u1EA3 ch\u1EC9 l\u1ED9 ra khi r\u00EA chu\u1ED9t: \u0111o /navbar 2026-09-24, m\u1EE5c\n \u0111ang m\u1EDF NH\u1EA0T \u0110I (#e3eaff -> #f5f8ff) v\u00EC `not-[.active]:hover:bg-brand-softer`\n v\u1EABn \u00E1p d\u1EE5ng, c\u00F2n `[&.active]:hover:bg-brand-medium` th\u00EC kh\u00F4ng. Sidebar c\u00F9ng\n b\u1ED9 class nh\u01B0ng \u0110\u1EACM L\u00CAN \u0111\u00FAng (#d2dcfa) v\u00EC n\u00F3 \u0111\u00E3 t\u00E1ch nh\u00E1nh t\u1EEB tr\u01B0\u1EDBc.\n\n Unit test KH\u00D4NG b\u1EAFt \u0111\u01B0\u1EE3c: `provideRouter([])` kh\u00F4ng c\u00F3 route n\u00E0o kh\u1EDBp n\u00EAn\n directive kh\u00F4ng ch\u1EA1m v\u00E0o `class`, test xanh trong khi tr\u00ECnh duy\u1EC7t h\u1ECFng.\n\n N\u1ED9i dung g\u00F3i trong `ng-template` v\u00E0 chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`: khai\n nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh lo\u1EA1i tr\u1EEB nhau \u2014\n th\u00EC Angular ch\u1EC9 chi\u1EBFu v\u00E0o M\u1ED8T ch\u1ED7 v\u00E0 th\u1EF1c t\u1EBF m\u1EA5t \u1EDF m\u1ECDi nh\u00E1nh.\n-->\n<ng-template #itemContent>\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n @if (label()) {\n <span class=\"whitespace-nowrap\">{{ label() }}</span>\n }\n <ng-content />\n</ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n} @else {\n <a [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <ng-container [ngTemplateOutlet]=\"itemContent\" />\n </a>\n}\n" }]
|
|
12012
12373
|
}], propDecorators: { routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], routerLinkActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActive", required: false }] }], routerLinkActiveOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActiveOptions", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
|
|
12013
12374
|
|
|
12014
12375
|
class OlNavDropdown {
|
|
@@ -13180,7 +13541,7 @@ class OlAppNav {
|
|
|
13180
13541
|
this.collapsed.update((v) => !v);
|
|
13181
13542
|
}
|
|
13182
13543
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13183
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAppNav, isStandalone: true, selector: "ol-app-nav", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, logoSrc: { classPropertyName: "logoSrc", publicName: "logoSrc", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, showRail: { classPropertyName: "showRail", publicName: "showRail", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", itemClick: "itemClick", userClick: "userClick" }, host: { classAttribute: "block" }, ngImport: i0, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n class=\"relative flex h-full flex-col justify-between rounded-2xl pt-6 transition-[width] duration-200\"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 13px khi thu g\u1ECDn, KH\u00D4NG ph\u1EA3i 12px: `<ul>` l\u00E0 flex-col n\u00EAn `<li>`\n k\u00E9o gi\u00E3n m\u1EE5c con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-11` kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 13\u00D72 = 44,\n \u0111\u00FAng \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c. \u0110\u1EC7m 12px cho ra 46\u00D744, l\u1EC7ch m\u1ED9t chi\u1EC1u.\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex flex-col gap-7 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoSrc()) {\n <img [src]=\"logoSrc()\" alt=\"\u00D4n Luy\u1EC7n\" class=\"h-10 w-auto max-w-full object-contain\" />\n } @else if (collapsed()) {\n <!--\n Thu g\u1ECDn: ch\u1EC9 ch\u1EEF t\u1EAFt. Logo \u0111\u1EA7y \u0111\u1EE7 r\u1ED9ng 155px (Figma) n\u00EAn \u1EDF khung 70px\n n\u00F3 tr\u00E0n h\u1EB3n ra ngo\u00E0i \u2014 \u0111o \u0111\u01B0\u1EE3c tr\u00EAn tr\u00ECnh duy\u1EC7t, ch\u1EEF \"\u00D4nluy\u1EC7n\" b\u1ECB c\u1EAFt\n gi\u1EEFa ch\u1EEBng \u1EDF m\u00E9p tr\u00E1i.\n -->\n <span class=\"text-2xl font-bold text-warning\" aria-label=\"\u00D4n Luy\u1EC7n\">\u00D4L</span>\n } @else {\n <!-- D\u1EF1 ph\u00F2ng khi ch\u01B0a truy\u1EC1n \u1EA3nh: ch\u1EEF gi\u1EEF \u0111\u00FAng hai m\u00E0u c\u1EE7a logo -->\n <span class=\"whitespace-nowrap text-2xl font-bold text-white\">\n \u00D4n<span class=\"text-warning\">luy\u1EC7n</span>\n </span>\n }\n </div>\n\n <ul class=\"flex flex-col gap-2\">\n @for (item of mainItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex flex-col gap-[7px] overflow-hidden rounded-b-2xl pb-5\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ul class=\"flex flex-col gap-[7px]\">\n @for (item of footerItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n\n @if (config().user; as user) {\n <button\n type=\"button\"\n class=\"mt-[30px] flex cursor-pointer items-center rounded-md transition-colors hover:bg-[var(--bg-app-nav-hover,#5168ac)]\"\n [class]=\"collapsed() ? 'mx-auto justify-center p-1' : 'w-full gap-2 p-1 text-start'\"\n (click)=\"userClick.emit()\"\n >\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n\n <!--\n Kh\u1ED1i t\u00EAn v\u00E0 email ch\u1EC9 hi\u1EC7n khi m\u1EDF. D\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class:\n \u1EA9n b\u1EB1ng `hidden` th\u00EC tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc, v\u00E0 ch\u1EEF v\u1EABn chi\u1EBFm ch\u1ED7\n trong t\u00EDnh to\u00E1n b\u1ED1 c\u1EE5c \u1EDF b\u1EC1 r\u1ED9ng 70px.\n -->\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs\">{{ user.subtitle }}</span>\n }\n </span>\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" class=\"shrink-0 text-white\" />\n }\n </button>\n }\n </div>\n</nav>\n\n<!--\n M\u1ED9t m\u1EE5c menu \u2014 d\u00F9ng chung cho c\u1EA3 ph\u1EA7n tr\u00EAn l\u1EABn ph\u1EA7n d\u01B0\u1EDBi.\n\n Ba nh\u00E1nh v\u00EC ba lo\u1EA1i h\u00E0nh vi kh\u00E1c nhau, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 `route` \u2192 `<a routerLink>` \u0111\u1EC3 router lo \u0111i\u1EC1u h\u01B0\u1EDBng v\u00E0 tr\u1EA1ng th\u00E1i active\n kh\u00F4ng route \u2192 `<button>` ch\u1EC9 ph\u00E1t s\u1EF1 ki\u1EC7n\n disabled \u2192 v\u1EABn `<button>` nh\u01B0ng `pointer-events-none` (lo trong class)\n-->\n<ng-template #navItem let-item>\n @if (item.route) {\n <a\n [routerLink]=\"item.route\"\n routerLinkActive=\"ol-app-nav-item-active font-medium\"\n [class]=\"itemClasses(item)\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </button>\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c: icon 24px + nh\u00E3n + huy hi\u1EC7u. Nh\u00E3n \u1EA9n khi thu g\u1ECDn. -->\n<ng-template #itemBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n\n @if (!collapsed()) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "directive", type: OlTooltipDirective, selector: "[olTooltip]", inputs: ["olTooltip", "tooltipPosition", "tooltipTheme", "tooltipDelay", "tooltipDisabled", "tooltipArrow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13544
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAppNav, isStandalone: true, selector: "ol-app-nav", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, logoSrc: { classPropertyName: "logoSrc", publicName: "logoSrc", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, showRail: { classPropertyName: "showRail", publicName: "showRail", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", itemClick: "itemClick", userClick: "userClick" }, host: { classAttribute: "block" }, ngImport: i0, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n class=\"relative flex h-full flex-col justify-between rounded-2xl pt-6 transition-[width] duration-200\"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 13px khi thu g\u1ECDn, KH\u00D4NG ph\u1EA3i 12px: `<ul>` l\u00E0 flex-col n\u00EAn `<li>`\n k\u00E9o gi\u00E3n m\u1EE5c con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-11` kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 13\u00D72 = 44,\n \u0111\u00FAng \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c. \u0110\u1EC7m 12px cho ra 46\u00D744, l\u1EC7ch m\u1ED9t chi\u1EC1u.\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex flex-col gap-7 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoSrc()) {\n <img [src]=\"logoSrc()\" alt=\"\u00D4n Luy\u1EC7n\" class=\"h-10 w-auto max-w-full object-contain\" />\n } @else if (collapsed()) {\n <!--\n Thu g\u1ECDn: ch\u1EC9 ch\u1EEF t\u1EAFt. Logo \u0111\u1EA7y \u0111\u1EE7 r\u1ED9ng 155px (Figma) n\u00EAn \u1EDF khung 70px\n n\u00F3 tr\u00E0n h\u1EB3n ra ngo\u00E0i \u2014 \u0111o \u0111\u01B0\u1EE3c tr\u00EAn tr\u00ECnh duy\u1EC7t, ch\u1EEF \"\u00D4nluy\u1EC7n\" b\u1ECB c\u1EAFt\n gi\u1EEFa ch\u1EEBng \u1EDF m\u00E9p tr\u00E1i.\n -->\n <span class=\"text-2xl font-bold text-warning\" aria-label=\"\u00D4n Luy\u1EC7n\">\u00D4L</span>\n } @else {\n <!-- D\u1EF1 ph\u00F2ng khi ch\u01B0a truy\u1EC1n \u1EA3nh: ch\u1EEF gi\u1EEF \u0111\u00FAng hai m\u00E0u c\u1EE7a logo -->\n <span class=\"whitespace-nowrap text-2xl font-bold text-white\">\n \u00D4n<span class=\"text-warning\">luy\u1EC7n</span>\n </span>\n }\n </div>\n\n <ul class=\"flex flex-col gap-2\">\n @for (item of mainItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex flex-col gap-[7px] overflow-hidden rounded-b-2xl pb-5\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ul class=\"flex flex-col gap-[7px]\">\n @for (item of footerItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n\n @if (config().user; as user) {\n <button\n type=\"button\"\n class=\"mt-[30px] flex cursor-pointer items-center rounded-md transition-colors hover:bg-[var(--bg-app-nav-hover,#5168ac)]\"\n [class]=\"collapsed() ? 'mx-auto justify-center p-1' : 'w-full gap-2 p-1 text-start'\"\n (click)=\"userClick.emit()\"\n >\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n\n <!--\n Kh\u1ED1i t\u00EAn v\u00E0 email ch\u1EC9 hi\u1EC7n khi m\u1EDF. D\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class:\n \u1EA9n b\u1EB1ng `hidden` th\u00EC tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc, v\u00E0 ch\u1EEF v\u1EABn chi\u1EBFm ch\u1ED7\n trong t\u00EDnh to\u00E1n b\u1ED1 c\u1EE5c \u1EDF b\u1EC1 r\u1ED9ng 70px.\n -->\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs\">{{ user.subtitle }}</span>\n }\n </span>\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" class=\"shrink-0 text-white\" />\n }\n </button>\n }\n </div>\n</nav>\n\n<!--\n M\u1ED9t m\u1EE5c menu \u2014 d\u00F9ng chung cho c\u1EA3 ph\u1EA7n tr\u00EAn l\u1EABn ph\u1EA7n d\u01B0\u1EDBi.\n\n Ba nh\u00E1nh v\u00EC ba lo\u1EA1i h\u00E0nh vi kh\u00E1c nhau, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 `route` \u2192 `<a routerLink>` \u0111\u1EC3 router lo \u0111i\u1EC1u h\u01B0\u1EDBng v\u00E0 tr\u1EA1ng th\u00E1i active\n kh\u00F4ng route \u2192 `<button>` ch\u1EC9 ph\u00E1t s\u1EF1 ki\u1EC7n\n disabled \u2192 v\u1EABn `<button>` nh\u01B0ng `pointer-events-none` (lo trong class)\n-->\n<ng-template #navItem let-item>\n @if (item.route) {\n <a\n [routerLink]=\"item.route\"\n routerLinkActive=\"ol-app-nav-item-active font-medium\"\n [class]=\"itemClasses(item)\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </button>\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c: icon 24px + nh\u00E3n + huy hi\u1EC7u. Nh\u00E3n \u1EA9n khi thu g\u1ECDn. -->\n<ng-template #itemBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n\n @if (!collapsed()) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "directive", type: OlTooltipDirective, selector: "[olTooltip]", inputs: ["olTooltip", "tooltipPosition", "tooltipTheme", "tooltipDelay", "tooltipDisabled", "tooltipArrow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13184
13545
|
}
|
|
13185
13546
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, decorators: [{
|
|
13186
13547
|
type: Component,
|
|
@@ -13371,11 +13732,11 @@ class OlSidebarItem {
|
|
|
13371
13732
|
}, /* @ts-ignore */
|
|
13372
13733
|
...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
|
|
13373
13734
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13374
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarItem, isStandalone: true, selector: "ol-sidebar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13735
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarItem, isStandalone: true, selector: "ol-sidebar-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, dot: { classPropertyName: "dot", publicName: "dot", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!--\n `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET.\n\n `RouterLinkActive` v\u00E0 `[class]=\"itemClasses()\"` c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh\n `class`, directive ghi SAU n\u00EAn n\u00F3 th\u1EAFng: k\u1EBFt h\u1EE3p `routerLink` + `[active]`\n th\u00EC chu\u1ED7i `'active \u2026'` b\u1ECB nu\u1ED1t. H\u1ECFng IM L\u1EB6NG \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (c\u00F9ng\n chu\u1ED7i), ch\u1EC9 ri\u00EAng ch\u1EEF `active` m\u1EA5t, n\u00EAn `not-[.active]:hover:*` v\u1EABn \u00E1p d\u1EE5ng\n v\u00E0 m\u1EE5c \u0111ang m\u1EDF NH\u1EA0T \u0110I khi r\u00EA chu\u1ED9t thay v\u00EC \u0111\u1EADm l\u00EAn.\n\n \u0110\u00E3 \u00E9p active b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh \u2014 t\u1EAFt n\u00F3 \u0111i l\u00E0\n h\u1EBFt tranh nhau. B\u1EAFt \u0111\u01B0\u1EE3c khi \u0111\u1ED1i chi\u1EBFu v\u1EDBi `ol-navbar-item` ng\u00E0y 2026-09-24;\n trang docs kh\u00F4ng l\u1ED9 v\u00EC c\u00E1c m\u1EE5c active \u1EDF \u0111\u00E2y d\u00F9ng nh\u00E1nh `div`/`href`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13375
13736
|
}
|
|
13376
13737
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarItem, decorators: [{
|
|
13377
13738
|
type: Component,
|
|
13378
|
-
args: [{ selector: 'ol-sidebar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon, OlBadge, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n" }]
|
|
13739
|
+
args: [{ selector: 'ol-sidebar-item', standalone: true, imports: [CommonModule, RouterModule, OlIcon, OlBadge, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!--\n `routerLinkActive` ch\u1EC9 g\u1EAFn khi `active()` T\u1EAET.\n\n `RouterLinkActive` v\u00E0 `[class]=\"itemClasses()\"` c\u00F9ng ghi v\u00E0o thu\u1ED9c t\u00EDnh\n `class`, directive ghi SAU n\u00EAn n\u00F3 th\u1EAFng: k\u1EBFt h\u1EE3p `routerLink` + `[active]`\n th\u00EC chu\u1ED7i `'active \u2026'` b\u1ECB nu\u1ED1t. H\u1ECFng IM L\u1EB6NG \u2014 n\u1EC1n v\u00E0 ch\u1EEF v\u1EABn \u0111\u00FAng (c\u00F9ng\n chu\u1ED7i), ch\u1EC9 ri\u00EAng ch\u1EEF `active` m\u1EA5t, n\u00EAn `not-[.active]:hover:*` v\u1EABn \u00E1p d\u1EE5ng\n v\u00E0 m\u1EE5c \u0111ang m\u1EDF NH\u1EA0T \u0110I khi r\u00EA chu\u1ED9t thay v\u00EC \u0111\u1EADm l\u00EAn.\n\n \u0110\u00E3 \u00E9p active b\u1EB1ng input th\u00EC kh\u00F4ng c\u00F2n g\u00EC \u0111\u1EC3 directive t\u00EDnh \u2014 t\u1EAFt n\u00F3 \u0111i l\u00E0\n h\u1EBFt tranh nhau. B\u1EAFt \u0111\u01B0\u1EE3c khi \u0111\u1ED1i chi\u1EBFu v\u1EDBi `ol-navbar-item` ng\u00E0y 2026-09-24;\n trang docs kh\u00F4ng l\u1ED9 v\u00EC c\u00E1c m\u1EE5c active \u1EDF \u0111\u00E2y d\u00F9ng nh\u00E1nh `div`/`href`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"active() ? '' : routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n [attr.aria-disabled]=\"disabled()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </a>\n} @else {\n <div [class]=\"itemClasses()\" [attr.aria-disabled]=\"disabled()\">\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (dot()) {\n <span class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand shrink-0\"></span>\n } @else if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\"><ng-container [ngTemplateOutlet]=\"noiDungLong\" /></span>\n </div>\n\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n </div>\n}\n" }]
|
|
13379
13740
|
}], propDecorators: { routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], routerLinkActive: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActive", required: false }] }], routerLinkActiveOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLinkActiveOptions", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], badgeColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeColor", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }] } });
|
|
13380
13741
|
|
|
13381
13742
|
class OlSidebarGroup {
|
|
@@ -13444,7 +13805,7 @@ class OlSidebarSubmenu {
|
|
|
13444
13805
|
this.isOpen.update((v) => !v);
|
|
13445
13806
|
}
|
|
13446
13807
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarSubmenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
13447
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarSubmenu, isStandalone: true, selector: "ol-sidebar-submenu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange" }, ngImport: i0, template: "<div>\n <button\n type=\"button\"\n (click)=\"toggle()\"\n [class]=\"triggerClasses()\"\n [attr.aria-expanded]=\"isOpen()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\">{{ label() }}</span>\n </div>\n\n <div class=\"flex items-center gap-2\">\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n <ol-icon\n name=\"chevron-right\"\n size=\"xs\"\n class=\"text-body-subtle transition-transform duration-200 shrink-0\"\n [class.rotate-90]=\"isOpen()\"\n ></ol-icon>\n </div>\n </button>\n\n <div\n class=\"pl-4 pr-1 py-1 space-y-1 border-l-2 border-default-subtle ml-5 my-1 transition-all duration-150\"\n [class.hidden]=\"!isOpen()\"\n >\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13808
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSidebarSubmenu, isStandalone: true, selector: "ol-sidebar-submenu", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange" }, ngImport: i0, template: "<div>\n <button\n type=\"button\"\n (click)=\"toggle()\"\n [class]=\"triggerClasses()\"\n [attr.aria-expanded]=\"isOpen()\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n @if (icon()) {\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\n }\n <span class=\"truncate\">{{ label() }}</span>\n </div>\n\n <div class=\"flex items-center gap-2\">\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n }\n <ol-icon\n name=\"chevron-right\"\n size=\"xs\"\n class=\"text-body-subtle transition-transform duration-200 shrink-0\"\n [class.rotate-90]=\"isOpen()\"\n ></ol-icon>\n </div>\n </button>\n\n <div\n class=\"pl-4 pr-1 py-1 space-y-1 border-l-2 border-default-subtle ml-5 my-1 transition-all duration-150\"\n [class.hidden]=\"!isOpen()\"\n >\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13448
13809
|
}
|
|
13449
13810
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSidebarSubmenu, decorators: [{
|
|
13450
13811
|
type: Component,
|
|
@@ -14409,7 +14770,7 @@ class OlTabs {
|
|
|
14409
14770
|
}
|
|
14410
14771
|
}
|
|
14411
14772
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14412
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTabs, isStandalone: true, selector: "ol-tabs", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, mobile: { classPropertyName: "mobile", publicName: "mobile", isSignal: true, isRequired: false, transformFunction: null }, mobileDropdown: { classPropertyName: "mobileDropdown", publicName: "mobileDropdown", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, activeTab: { classPropertyName: "activeTab", publicName: "activeTab", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", activeTab: "activeTabChange", tabChange: "tabChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "items", predicate: OlTabItem, isSignal: true }], ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!-- 1. Mobile Dropdown View (Type=Default / Pills / Border bottom, Mobile=True - Figma #1149:80537, #1149:80539, #1149:80541) -->\n@if (isMobileDropdown()) {\n <div class=\"w-full max-w-md\">\n <div class=\"relative\">\n <!--\n `[ngModel]` ch\u1EE9 KH\u00D4NG ph\u1EA3i `[value]`: `[value]` tr\u00EAn <select> \u0111\u1EB7t thu\u1ED9c\n t\u00EDnh DOM NGAY, l\u00FAc c\u00E1c <option> c\u1EE7a @for ch\u01B0a t\u1ED3n t\u1EA1i, n\u00EAn tr\u00ECnh duy\u1EC7t\n ho\u00E0n nguy\u00EAn v\u1EC1 m\u1EE5c \u0111\u1EA7u. \u0110o 2026-09-17 v\u1EDBi `[selectedIndex]=\"2\"`: tab\n \u0111ang ch\u1ECDn l\u00E0 \"Ba\" m\u00E0 \u00F4 ch\u1ECDn hi\u1EC7n \"M\u1ED9t\". Build xanh, kh\u00F4ng c\u1EA3nh b\u00E1o.\n\n V\u00E0 `selectTab($event)` ch\u1EE9 kh\u00F4ng ph\u1EA3i `.selectedIndex` \u2014 c\u00E1i sau l\u00E0 v\u1ECB\n tr\u00ED trong danh s\u00E1ch DOM, tr\u00F9ng v\u1EDBi ch\u1EC9 s\u1ED1 tab ch\u1EC9 khi m\u1ECDi tab \u0111\u1EC1u \u0111\u01B0\u1EE3c\n v\u1EBD; l\u1ECDc b\u1EDBt m\u1ED9t tab l\u00E0 l\u1EC7ch ngay. `[value]` c\u1EE7a option m\u1EDBi l\u00E0 ngu\u1ED3n.\n -->\n <select\n [ngModel]=\"currentIndex()\"\n (ngModelChange)=\"selectTab(+$event)\"\n aria-label=\"Ch\u1ECDn tab\"\n class=\"w-full appearance-none rounded-lg border border-default-medium bg-neutral-primary py-2.5 pl-3.5 pr-10 text-sm font-medium text-heading shadow-xs focus:border-brand-light focus:outline-hidden focus:ring-1 focus:ring-brand-subtle cursor-pointer\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <option [value]=\"i\" [disabled]=\"item.disabled()\">\n {{ item.displayLabel() }}\n </option>\n }\n </select>\n <div\n class=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-body-subtle\"\n >\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" />\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div class=\"pt-4 w-full\">\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n} @else {\n <!-- 2. Standard / Desktop / Vertical Stack View -->\n <div\n [class]=\"\n isVertical()\n ? 'flex flex-col md:flex-row gap-6 items-start w-full'\n : 'flex w-full flex-col min-h-0'\n \"\n >\n <!-- Tab Headers Navigation -->\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs w-full max-w-md'\n : mobile() && variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs divide-y divide-default overflow-hidden w-full max-w-md'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'w-full md:w-64 shrink-0'\n : 'w-full md:w-44 shrink-0'\n : variant() === 'button' || variant() === 'boxed'\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs inline-flex max-w-full overflow-x-auto'\n : variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs inline-flex divide-x divide-default overflow-hidden max-w-full overflow-x-auto'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'border-b border-default w-full'\n : 'w-full'\n \"\n >\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'flex flex-col gap-1 w-full'\n : mobile() && variant() === 'button-group'\n ? 'flex flex-col w-full'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'flex flex-col gap-1 p-2 bg-neutral-primary rounded-md border border-default shadow-xs'\n : 'flex flex-col gap-2.5'\n : variant() === 'button' || variant() === 'boxed'\n ? 'flex items-center gap-1 ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'button-group'\n ? 'flex items-center ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'flex items-center gap-6 overflow-x-auto -mb-px ' +\n (fullWidth() ? 'justify-between' : '')\n : 'flex items-center gap-6 overflow-x-auto ' +\n (fullWidth() ? 'justify-between' : '')\n \"\n role=\"tablist\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [id]=\"item.tabId()\"\n [disabled]=\"item.disabled()\"\n [attr.aria-selected]=\"i === currentIndex()\"\n (click)=\"selectTab(i)\"\n [class]=\"\n 'inline-flex items-center font-medium transition-all duration-150 select-none whitespace-nowrap ' +\n sizeClasses() +\n ' ' +\n (fullWidth() || mobile() ? 'w-full justify-start ' : '') +\n (item.disabled() ? 'opacity-40 cursor-not-allowed ' : 'cursor-pointer ') +\n (mobile() && (variant() === 'button' || variant() === 'boxed')\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : mobile() && variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'default'\n ? i === currentIndex()\n ? 'text-fg-brand font-semibold'\n : 'text-body hover:text-heading'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? i === currentIndex()\n ? 'border-b-2 border-brand text-fg-brand font-semibold pb-3 -mb-px'\n : 'border-b-2 border-transparent text-body hover:text-heading hover:border-default-strong pb-3 -mb-px'\n : variant() === 'pills'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-full shadow-xs'\n : 'text-body hover:bg-neutral-tertiary rounded-full'\n : variant() === 'button' || variant() === 'boxed'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'vertical-pills' || variant() === 'vertical'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-md shadow-xs justify-start w-full'\n : 'text-body hover:bg-neutral-tertiary rounded-md justify-start w-full'\n : variant() === 'vertical-list'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-fg-brand font-semibold rounded-md justify-between w-full'\n : 'text-body hover:bg-neutral-secondary-medium rounded-md justify-between w-full'\n : '')\n \"\n >\n <!-- User select format with Avatar (Figma #1149:80606) -->\n @if (item.avatar()) {\n <div class=\"inline-flex items-center gap-3 min-w-0 text-left\">\n <img\n [src]=\"item.avatar()!\"\n alt=\"\"\n class=\"w-8 h-8 rounded-full object-cover shrink-0 border border-default\"\n />\n <div class=\"flex flex-col min-w-0\">\n <span class=\"truncate font-medium text-xs leading-tight text-heading\">{{\n item.displayLabel()\n }}</span>\n <!--\n \u0110\u1ECDc `displaySubtitle()` ch\u1EE9 kh\u00F4ng ph\u1EA3i `item.helper()`:\n `subtitle` v\u00E0 `helper` l\u00E0 b\u00ED danh c\u1EE7a nhau (ch\u00FA th\u00EDch ngay\n ch\u1ED7 khai trong tab-item), nh\u01B0ng render th\u1EB3ng `helper` th\u00EC\n \u0111\u1EB7t `subtitle=\"...\"` kh\u00F4ng hi\u1EC7n g\u00EC.\n -->\n @if (item.displaySubtitle()) {\n <span class=\"truncate text-[11px] text-body-subtle font-normal leading-tight\">{{\n item.displaySubtitle()\n }}</span>\n }\n </div>\n </div>\n } @else {\n <div class=\"inline-flex items-center gap-2 min-w-0\">\n @if (item.icon()) {\n <ol-icon [name]=\"item.icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n <span class=\"truncate\">{{ item.displayLabel() }}</span>\n </div>\n }\n\n @if (item.badge() !== null && item.badge() !== undefined && item.badge() !== '') {\n <ol-badge\n [color]=\"\n i === currentIndex() && (variant() === 'pills' || variant() === 'vertical-pills')\n ? 'white'\n : item.badgeColor()\n \"\n size=\"xs\"\n shape=\"pill\"\n class=\"ml-auto\"\n >\n {{ item.badge() }}\n </ol-badge>\n }\n\n @if (variant() === 'vertical-list' && !item.avatar()) {\n @if (i === currentIndex()) {\n <ol-icon\n name=\"check-circle\"\n size=\"xs\"\n class=\"text-fg-brand shrink-0 ml-auto\"\n ></ol-icon>\n } @else {\n <ol-icon\n name=\"alt-arrow-down\"\n size=\"xs\"\n class=\"text-body-subtle shrink-0 -rotate-90 ml-auto\"\n ></ol-icon>\n }\n }\n </button>\n }\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div\n [class]=\"isVertical() ? 'flex-1 min-w-0 w-full' : 'flex min-h-0 flex-1 flex-col pt-4 w-full'\"\n >\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14773
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTabs, isStandalone: true, selector: "ol-tabs", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, mobile: { classPropertyName: "mobile", publicName: "mobile", isSignal: true, isRequired: false, transformFunction: null }, mobileDropdown: { classPropertyName: "mobileDropdown", publicName: "mobileDropdown", isSignal: true, isRequired: false, transformFunction: null }, selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, activeTab: { classPropertyName: "activeTab", publicName: "activeTab", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", activeTab: "activeTabChange", tabChange: "tabChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "items", predicate: OlTabItem, isSignal: true }], ngImport: i0, template: "<!--\n M\u1ED8T `<ng-content>` duy nh\u1EA5t cho m\u1ECDi nh\u00E1nh.\n\n Khai nhi\u1EC1u `<ng-content>` kh\u00F4ng `select` \u2014 k\u1EC3 c\u1EA3 \u1EDF c\u00E1c nh\u00E1nh `@if` lo\u1EA1i tr\u1EEB\n nhau \u2014 th\u00EC Angular ch\u1EC9 chi\u1EBFu n\u1ED9i dung l\u1ED3ng v\u00E0o M\u1ED8T ch\u1ED7, v\u00E0 th\u1EF1c t\u1EBF l\u00E0 m\u1EA5t \u1EDF\n m\u1ECDi nh\u00E1nh. H\u1ECFng im l\u1EB7ng: build xanh, khung v\u1EABn v\u1EBD, ch\u1EC9 ru\u1ED9t tr\u1ED1ng. G\u00F3i v\u00E0o m\u1ED9t\n `ng-template` r\u1ED3i m\u1ED7i nh\u00E1nh chi\u1EBFu l\u1EA1i b\u1EB1ng `ngTemplateOutlet`.\n-->\n<ng-template #noiDungLong><ng-content /></ng-template>\n\n<!-- 1. Mobile Dropdown View (Type=Default / Pills / Border bottom, Mobile=True - Figma #1149:80537, #1149:80539, #1149:80541) -->\n@if (isMobileDropdown()) {\n <div class=\"w-full max-w-md\">\n <div class=\"relative\">\n <!--\n `[ngModel]` ch\u1EE9 KH\u00D4NG ph\u1EA3i `[value]`: `[value]` tr\u00EAn <select> \u0111\u1EB7t thu\u1ED9c\n t\u00EDnh DOM NGAY, l\u00FAc c\u00E1c <option> c\u1EE7a @for ch\u01B0a t\u1ED3n t\u1EA1i, n\u00EAn tr\u00ECnh duy\u1EC7t\n ho\u00E0n nguy\u00EAn v\u1EC1 m\u1EE5c \u0111\u1EA7u. \u0110o 2026-09-17 v\u1EDBi `[selectedIndex]=\"2\"`: tab\n \u0111ang ch\u1ECDn l\u00E0 \"Ba\" m\u00E0 \u00F4 ch\u1ECDn hi\u1EC7n \"M\u1ED9t\". Build xanh, kh\u00F4ng c\u1EA3nh b\u00E1o.\n\n V\u00E0 `selectTab($event)` ch\u1EE9 kh\u00F4ng ph\u1EA3i `.selectedIndex` \u2014 c\u00E1i sau l\u00E0 v\u1ECB\n tr\u00ED trong danh s\u00E1ch DOM, tr\u00F9ng v\u1EDBi ch\u1EC9 s\u1ED1 tab ch\u1EC9 khi m\u1ECDi tab \u0111\u1EC1u \u0111\u01B0\u1EE3c\n v\u1EBD; l\u1ECDc b\u1EDBt m\u1ED9t tab l\u00E0 l\u1EC7ch ngay. `[value]` c\u1EE7a option m\u1EDBi l\u00E0 ngu\u1ED3n.\n -->\n <select\n [ngModel]=\"currentIndex()\"\n (ngModelChange)=\"selectTab(+$event)\"\n aria-label=\"Ch\u1ECDn tab\"\n class=\"w-full appearance-none rounded-lg border border-default-medium bg-neutral-primary py-2.5 pl-3.5 pr-10 text-sm font-medium text-heading shadow-xs focus:border-brand-light focus:outline-hidden focus:ring-1 focus:ring-brand-subtle cursor-pointer\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <option [value]=\"i\" [disabled]=\"item.disabled()\">\n {{ item.displayLabel() }}\n </option>\n }\n </select>\n <div\n class=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-body-subtle\"\n >\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" />\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div class=\"pt-4 w-full\">\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n} @else {\n <!-- 2. Standard / Desktop / Vertical Stack View -->\n <div\n [class]=\"\n isVertical()\n ? 'flex flex-col md:flex-row gap-6 items-start w-full'\n : 'flex w-full flex-col min-h-0'\n \"\n >\n <!-- Tab Headers Navigation -->\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs w-full max-w-md'\n : mobile() && variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs divide-y divide-default overflow-hidden w-full max-w-md'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'w-full md:w-64 shrink-0'\n : 'w-full md:w-44 shrink-0'\n : variant() === 'button' || variant() === 'boxed'\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs inline-flex max-w-full overflow-x-auto'\n : variant() === 'button-group'\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs inline-flex divide-x divide-default overflow-hidden max-w-full overflow-x-auto'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'border-b border-default w-full'\n : 'w-full'\n \"\n >\n <div\n [class]=\"\n mobile() && (variant() === 'button' || variant() === 'boxed')\n ? 'flex flex-col gap-1 w-full'\n : mobile() && variant() === 'button-group'\n ? 'flex flex-col w-full'\n : isVertical()\n ? variant() === 'vertical-list'\n ? 'flex flex-col gap-1 p-2 bg-neutral-primary rounded-md border border-default shadow-xs'\n : 'flex flex-col gap-2.5'\n : variant() === 'button' || variant() === 'boxed'\n ? 'flex items-center gap-1 ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'button-group'\n ? 'flex items-center ' + (fullWidth() ? 'w-full' : '')\n : variant() === 'border-bottom' || variant() === 'underline'\n ? 'flex items-center gap-6 overflow-x-auto -mb-px ' +\n (fullWidth() ? 'justify-between' : '')\n : 'flex items-center gap-6 overflow-x-auto ' +\n (fullWidth() ? 'justify-between' : '')\n \"\n role=\"tablist\"\n >\n @for (item of items(); track item.tabId(); let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [id]=\"item.tabId()\"\n [disabled]=\"item.disabled()\"\n [attr.aria-selected]=\"i === currentIndex()\"\n (click)=\"selectTab(i)\"\n [class]=\"\n 'inline-flex items-center font-medium transition-all duration-150 select-none whitespace-nowrap ' +\n sizeClasses() +\n ' ' +\n (fullWidth() || mobile() ? 'w-full justify-start ' : '') +\n (item.disabled() ? 'opacity-40 cursor-not-allowed ' : 'cursor-pointer ') +\n (mobile() && (variant() === 'button' || variant() === 'boxed')\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : mobile() && variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'default'\n ? i === currentIndex()\n ? 'text-fg-brand font-semibold'\n : 'text-body hover:text-heading'\n : variant() === 'border-bottom' || variant() === 'underline'\n ? i === currentIndex()\n ? 'border-b-2 border-brand text-fg-brand font-semibold pb-3 -mb-px'\n : 'border-b-2 border-transparent text-body hover:text-heading hover:border-default-strong pb-3 -mb-px'\n : variant() === 'pills'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-full shadow-xs'\n : 'text-body hover:bg-neutral-tertiary rounded-full'\n : variant() === 'button' || variant() === 'boxed'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\n : variant() === 'button-group'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-heading font-medium'\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\n : variant() === 'vertical-pills' || variant() === 'vertical'\n ? i === currentIndex()\n ? 'bg-brand text-white font-medium rounded-md shadow-xs justify-start w-full'\n : 'text-body hover:bg-neutral-tertiary rounded-md justify-start w-full'\n : variant() === 'vertical-list'\n ? i === currentIndex()\n ? 'bg-neutral-secondary-medium text-fg-brand font-semibold rounded-md justify-between w-full'\n : 'text-body hover:bg-neutral-secondary-medium rounded-md justify-between w-full'\n : '')\n \"\n >\n <!-- User select format with Avatar (Figma #1149:80606) -->\n @if (item.avatar()) {\n <div class=\"inline-flex items-center gap-3 min-w-0 text-left\">\n <img\n [src]=\"item.avatar()!\"\n alt=\"\"\n class=\"w-8 h-8 rounded-full object-cover shrink-0 border border-default\"\n />\n <div class=\"flex flex-col min-w-0\">\n <span class=\"truncate font-medium text-xs leading-tight text-heading\">{{\n item.displayLabel()\n }}</span>\n <!--\n \u0110\u1ECDc `displaySubtitle()` ch\u1EE9 kh\u00F4ng ph\u1EA3i `item.helper()`:\n `subtitle` v\u00E0 `helper` l\u00E0 b\u00ED danh c\u1EE7a nhau (ch\u00FA th\u00EDch ngay\n ch\u1ED7 khai trong tab-item), nh\u01B0ng render th\u1EB3ng `helper` th\u00EC\n \u0111\u1EB7t `subtitle=\"...\"` kh\u00F4ng hi\u1EC7n g\u00EC.\n -->\n @if (item.displaySubtitle()) {\n <span class=\"truncate text-[11px] text-body-subtle font-normal leading-tight\">{{\n item.displaySubtitle()\n }}</span>\n }\n </div>\n </div>\n } @else {\n <div class=\"inline-flex items-center gap-2 min-w-0\">\n @if (item.icon()) {\n <ol-icon [name]=\"item.icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\n }\n <span class=\"truncate\">{{ item.displayLabel() }}</span>\n </div>\n }\n\n @if (item.badge() !== null && item.badge() !== undefined && item.badge() !== '') {\n <ol-badge\n [color]=\"\n i === currentIndex() && (variant() === 'pills' || variant() === 'vertical-pills')\n ? 'white'\n : item.badgeColor()\n \"\n size=\"xs\"\n shape=\"pill\"\n class=\"ml-auto\"\n >\n {{ item.badge() }}\n </ol-badge>\n }\n\n @if (variant() === 'vertical-list' && !item.avatar()) {\n @if (i === currentIndex()) {\n <ol-icon\n name=\"check-circle\"\n size=\"xs\"\n class=\"text-fg-brand shrink-0 ml-auto\"\n ></ol-icon>\n } @else {\n <ol-icon\n name=\"alt-arrow-down\"\n size=\"xs\"\n class=\"text-body-subtle shrink-0 -rotate-90 ml-auto\"\n ></ol-icon>\n }\n }\n </button>\n }\n </div>\n </div>\n\n <!-- Tab Content Area -->\n <div\n [class]=\"isVertical() ? 'flex-1 min-w-0 w-full' : 'flex min-h-0 flex-1 flex-col pt-4 w-full'\"\n >\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\n </div>\n </div>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.SelectControlValueAccessor, selector: "select:not([multiple]):not([ngNoCva])[formControlName],select:not([multiple]):not([ngNoCva])[formControl],select:not([multiple]):not([ngNoCva])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14413
14774
|
}
|
|
14414
14775
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTabs, decorators: [{
|
|
14415
14776
|
type: Component,
|
|
@@ -14795,7 +15156,7 @@ class OlBottomNavItem {
|
|
|
14795
15156
|
}, /* @ts-ignore */
|
|
14796
15157
|
...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
|
|
14797
15158
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBottomNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14798
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBottomNavItem, isStandalone: true, selector: "ol-bottom-nav-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isCenterAction: { classPropertyName: "isCenterAction", publicName: "isCenterAction", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba nh\u00E1nh routerLink / href / button ch\u1EC9 kh\u00E1c nhau \u1EDF TH\u1EBA B\u1ECCC; ru\u1ED9t y h\u1EC7t nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y ru\u1ED9t \u0111\u01B0\u1EE3c ch\u00E9p ba l\u1EA7n (105 d\u00F2ng) \u2014 s\u1EEDa gradient hay badge ph\u1EA3i s\u1EEDa\n \u0111\u1EE7 ba ch\u1ED7, qu\u00EAn m\u1ED9t l\u00E0 l\u1EC7ch \u00E2m th\u1EA7m. Gom v\u00E0o `ng-template`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n >\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <button type=\"button\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </button>\n}\n\n<ng-template #ruot>\n @if (isCenterAction()) {\n <!-- Gradient n\u00FAt h\u00E0nh \u0111\u1ED9ng gi\u1EEFa: `from-brand-700 to-brand-500` GI\u1EEE b\u1EADc s\u1ED1.\n `brand` l\u00E0 b\u1EA3ng Figma C\u00D3, v\u00E0 h\u1ECD `from-`/`to-` kh\u00F4ng c\u00F3 token ng\u1EEF ngh\u0129a\n n\u00E0o \u0111\u01B0\u1EE3c @theme emit (\u0111o 2026-09-15). -->\n <div\n class=\"w-12 h-12 rounded-full bg-linear-to-tr from-brand-700 to-brand-500 text-white shadow-lg shadow-md/30 flex items-center justify-center transform group-hover:scale-105 group-active:scale-95 transition-all\"\n >\n @if (icon()) {\n <ol-icon [name]=\"icon()\" size=\"lg\" color=\"white\"></ol-icon>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs font-semibold text-fg-brand mt-1 truncate\">{{ label() }}</span>\n }\n } @else {\n <div class=\"relative flex items-center justify-center\">\n @if (icon()) {\n <ol-icon\n [name]=\"icon()\"\n size=\"base\"\n class=\"transition-transform group-hover:scale-110\"\n ></ol-icon>\n }\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <span class=\"absolute -top-1.5 -right-2\">\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n </span>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs mt-1 truncate font-medium\">{{ label() }}</span>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15159
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlBottomNavItem, isStandalone: true, selector: "ol-bottom-nav-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActive: { classPropertyName: "routerLinkActive", publicName: "routerLinkActive", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeColor: { classPropertyName: "badgeColor", publicName: "badgeColor", isSignal: true, isRequired: false, transformFunction: null }, isCenterAction: { classPropertyName: "isCenterAction", publicName: "isCenterAction", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba nh\u00E1nh routerLink / href / button ch\u1EC9 kh\u00E1c nhau \u1EDF TH\u1EBA B\u1ECCC; ru\u1ED9t y h\u1EC7t nhau.\n Tr\u01B0\u1EDBc \u0111\u00E2y ru\u1ED9t \u0111\u01B0\u1EE3c ch\u00E9p ba l\u1EA7n (105 d\u00F2ng) \u2014 s\u1EEDa gradient hay badge ph\u1EA3i s\u1EEDa\n \u0111\u1EE7 ba ch\u1ED7, qu\u00EAn m\u1ED9t l\u00E0 l\u1EC7ch \u00E2m th\u1EA7m. Gom v\u00E0o `ng-template`.\n-->\n@if (routerLink()) {\n <a\n [routerLink]=\"routerLink()\"\n [routerLinkActive]=\"routerLinkActive()\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\"\n [class]=\"itemClasses()\"\n >\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else if (href()) {\n <a [href]=\"href()\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n} @else {\n <button type=\"button\" [class]=\"itemClasses()\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </button>\n}\n\n<ng-template #ruot>\n @if (isCenterAction()) {\n <!-- Gradient n\u00FAt h\u00E0nh \u0111\u1ED9ng gi\u1EEFa: `from-brand-700 to-brand-500` GI\u1EEE b\u1EADc s\u1ED1.\n `brand` l\u00E0 b\u1EA3ng Figma C\u00D3, v\u00E0 h\u1ECD `from-`/`to-` kh\u00F4ng c\u00F3 token ng\u1EEF ngh\u0129a\n n\u00E0o \u0111\u01B0\u1EE3c @theme emit (\u0111o 2026-09-15). -->\n <div\n class=\"w-12 h-12 rounded-full bg-linear-to-tr from-brand-700 to-brand-500 text-white shadow-lg shadow-md/30 flex items-center justify-center transform group-hover:scale-105 group-active:scale-95 transition-all\"\n >\n @if (icon()) {\n <ol-icon [name]=\"icon()\" size=\"lg\" color=\"white\"></ol-icon>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs font-semibold text-fg-brand mt-1 truncate\">{{ label() }}</span>\n }\n } @else {\n <div class=\"relative flex items-center justify-center\">\n @if (icon()) {\n <ol-icon\n [name]=\"icon()\"\n size=\"base\"\n class=\"transition-transform group-hover:scale-110\"\n ></ol-icon>\n }\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\n <span class=\"absolute -top-1.5 -right-2\">\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\n </span>\n }\n </div>\n @if (label()) {\n <span class=\"text-2xs mt-1 truncate font-medium\">{{ label() }}</span>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: i2.RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14799
15160
|
}
|
|
14800
15161
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlBottomNavItem, decorators: [{
|
|
14801
15162
|
type: Component,
|
|
@@ -14959,7 +15320,7 @@ class OlStepper {
|
|
|
14959
15320
|
}
|
|
14960
15321
|
}
|
|
14961
15322
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlStepper, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14962
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlStepper, isStandalone: true, selector: "ol-stepper", inputs: { currentStep: { classPropertyName: "currentStep", publicName: "currentStep", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, breakpoint: { classPropertyName: "breakpoint", publicName: "breakpoint", isSignal: true, isRequired: false, transformFunction: null }, linear: { classPropertyName: "linear", publicName: "linear", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentStep: "currentStepChange", stepChange: "stepChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "steps", predicate: OlStepItem, isSignal: true }], ngImport: i0, template: "<div [class]=\"isVertical() ? 'flex flex-col md:flex-row gap-8 w-full' : 'w-full'\">\n <!-- Stepper Header / Navigation Area -->\n <div [class]=\"isVertical() ? 'w-full md:w-80 shrink-0' : 'w-full mb-6'\">\n @if (isMobile()) {\n <!-- Mobile Layout -->\n <div class=\"p-4 rounded-md bg-neutral-tertiary/80 border border-default\">\n <div class=\"flex items-center justify-between mb-2\">\n <span class=\"text-xs font-semibold text-fg-brand uppercase tracking-wider\">\n B\u01B0\u1EDBc {{ currentStep() + 1 }} / {{ steps().length }}\n </span>\n @if (steps()[currentStep()]; as activeStep) {\n <span class=\"text-xs text-body-subtle font-medium\">\n {{ activeStep.title() }}\n </span>\n }\n </div>\n <!-- Progress Bar -->\n <div\n class=\"w-full bg-neutral-tertiary-medium h-2 rounded-full overflow-hidden flex gap-1 p-0.5\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div\n class=\"h-full flex-1 rounded-full transition-all duration-300\"\n [class.bg-brand]=\"i <= currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i > currentStep()\"\n ></div>\n }\n </div>\n </div>\n } @else if (variant() === 'cards') {\n <!-- Cards Variant (Vertical Stack) -->\n <div class=\"flex flex-col gap-3\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <!--\n `<button role=\"tab\">` ch\u1EE9 KH\u00D4NG ph\u1EA3i `<div (click)>`. Khung ngo\u00E0i\n khai `role=\"tablist\"` nh\u01B0ng \u0111o 2026-09-17 b\u00EAn trong c\u00F3 **0\n `role=\"tab\"` v\u00E0 0 ph\u1EA7n t\u1EED tab t\u1EDBi \u0111\u01B0\u1EE3c** \u2014 `tablist` r\u1ED7ng vai tr\u00F2,\n v\u00E0 b\u00E0n ph\u00EDm kh\u00F4ng ch\u1ECDn n\u1ED5i b\u01B0\u1EDBc n\u00E0o. Ba bi\u1EBFn th\u1EC3 stepper kh\u00E1c \u0111\u00E3\n d\u00F9ng `<button>` s\u1EB5n; ch\u1EC9 ri\u00EAng `cards` l\u1EC7ch.\n\n `w-full` v\u00EC button kh\u00F4ng t\u1EF1 gi\u00E3n nh\u01B0 div.\n -->\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"step.effectiveState() === 'active'\"\n [disabled]=\"step.effectiveState() === 'disabled'\"\n (click)=\"goToStep(i)\"\n [class]=\"\n 'w-full flex items-center justify-between p-4 rounded-md border transition-all duration-200 text-left ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-secondary-soft border-default text-fg-disabled'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer shadow-xs ring-2 ring-brand-subtle'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer hover:shadow-xs border-danger-subtle bg-danger-soft'\n : 'cursor-pointer hover:shadow-xs border-default-medium bg-neutral-secondary-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n <!-- Left Icon / Step Badge -->\n <div\n [class]=\"\n 'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 font-bold text-xs transition-colors ' +\n (step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'active'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body')\n \"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </div>\n\n <!-- Title & Subtitle -->\n <div class=\"min-w-0\">\n <div\n [class]=\"\n 'text-sm truncate ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand-strong '\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled truncate\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n\n <!-- Right Status Icon -->\n <div class=\"shrink-0 ml-3\">\n @if (step.effectiveState() === 'completed') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <ol-icon name=\"check-circle\" size=\"xs\" color=\"brand\"></ol-icon>\n <span class=\"hidden sm:inline\">Ho\u00E0n t\u1EA5t</span>\n </span>\n } @else if (step.effectiveState() === 'active') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <span class=\"w-2 h-2 rounded-full bg-brand animate-pulse\"></span>\n <span class=\"hidden sm:inline\">\u0110ang th\u1EF1c hi\u1EC7n</span>\n </span>\n } @else if (step.effectiveState() === 'error') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n <span class=\"hidden sm:inline\">L\u1ED7i</span>\n </span>\n } @else {\n <ol-icon name=\"chevron-right\" size=\"xs\" class=\"text-fg-disabled\"></ol-icon>\n }\n </div>\n </button>\n }\n </div>\n } @else if (isVertical()) {\n <!-- Vertical Variant -->\n <div class=\"flex flex-col relative\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-start gap-4 relative pb-8 last:pb-0\">\n <!-- Connecting Vertical Line -->\n @if (i < steps().length - 1) {\n <div\n class=\"absolute left-4.25 top-9 bottom-0 w-0.5 transition-colors duration-200\"\n [class.bg-brand]=\"i < currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i >= currentStep()\"\n ></div>\n }\n\n <!-- Step Circle / Icon Shape -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 z-10 ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-tertiary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'cursor-pointer bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n\n <!-- Step Title & Subtitle -->\n <div class=\"text-left pt-1 cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled mt-0.5\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- Horizontal Variants: Default | Only Icons | Icons & Text | With Dots -->\n <div\n class=\"flex items-center justify-between w-full relative\"\n role=\"tablist\"\n aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-center flex-1 last:flex-none relative\">\n <div class=\"flex items-center gap-3\">\n @if (variant() === 'with-dots') {\n <!-- With Dots Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'flex items-center justify-center transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'w-3.5 h-3.5 rounded-full opacity-50 bg-neutral-primary text-fg-disabled border-2 border-default-medium'\n : step.effectiveState() === 'completed'\n ? 'w-4 h-4 rounded-full bg-brand text-white'\n : step.effectiveState() === 'active'\n ? 'w-4 h-4 rounded-full bg-neutral-primary border-2 border-brand-light ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'w-4 h-4 rounded-full bg-danger-surface ring-4 ring-danger-subtle'\n : 'w-3.5 h-3.5 rounded-full bg-neutral-primary border-2 border-default-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" class=\"scale-75\"></ol-icon>\n }\n </button>\n } @else if (variant() === 'only-icons' || variant() === 'icons-text') {\n <!-- Icon Shape Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-10 h-10 rounded-full flex items-center justify-center font-semibold text-sm transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-tertiary text-fg-disabled'\n : step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body border border-default')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"sm\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"sm\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"sm\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n } @else {\n <!-- Default Variant (Numbered Circle) -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-primary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n }\n\n <!-- Text Title & Subtitle (unless only-icons) -->\n @if (variant() !== 'only-icons') {\n <div class=\"text-left cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled hidden md:block\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Connecting Line (Horizontal) -->\n @if (i < steps().length - 1) {\n <div class=\"flex-1 mx-4 h-0.5 bg-neutral-tertiary-medium relative overflow-hidden\">\n <div\n class=\"h-full bg-brand transition-all duration-300\"\n [class.w-full]=\"i < currentStep()\"\n [class.w-0]=\"i >= currentStep()\"\n ></div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Stepper Body Content Area -->\n <div [class]=\"isVertical() ? 'flex-1 min-w-0' : 'w-full'\">\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15323
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlStepper, isStandalone: true, selector: "ol-stepper", inputs: { currentStep: { classPropertyName: "currentStep", publicName: "currentStep", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, breakpoint: { classPropertyName: "breakpoint", publicName: "breakpoint", isSignal: true, isRequired: false, transformFunction: null }, linear: { classPropertyName: "linear", publicName: "linear", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentStep: "currentStepChange", stepChange: "stepChange" }, host: { classAttribute: "block w-full" }, queries: [{ propertyName: "steps", predicate: OlStepItem, isSignal: true }], ngImport: i0, template: "<div [class]=\"isVertical() ? 'flex flex-col md:flex-row gap-8 w-full' : 'w-full'\">\n <!-- Stepper Header / Navigation Area -->\n <div [class]=\"isVertical() ? 'w-full md:w-80 shrink-0' : 'w-full mb-6'\">\n @if (isMobile()) {\n <!-- Mobile Layout -->\n <div class=\"p-4 rounded-md bg-neutral-tertiary/80 border border-default\">\n <div class=\"flex items-center justify-between mb-2\">\n <span class=\"text-xs font-semibold text-fg-brand uppercase tracking-wider\">\n B\u01B0\u1EDBc {{ currentStep() + 1 }} / {{ steps().length }}\n </span>\n @if (steps()[currentStep()]; as activeStep) {\n <span class=\"text-xs text-body-subtle font-medium\">\n {{ activeStep.title() }}\n </span>\n }\n </div>\n <!-- Progress Bar -->\n <div\n class=\"w-full bg-neutral-tertiary-medium h-2 rounded-full overflow-hidden flex gap-1 p-0.5\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div\n class=\"h-full flex-1 rounded-full transition-all duration-300\"\n [class.bg-brand]=\"i <= currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i > currentStep()\"\n ></div>\n }\n </div>\n </div>\n } @else if (variant() === 'cards') {\n <!-- Cards Variant (Vertical Stack) -->\n <div class=\"flex flex-col gap-3\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <!--\n `<button role=\"tab\">` ch\u1EE9 KH\u00D4NG ph\u1EA3i `<div (click)>`. Khung ngo\u00E0i\n khai `role=\"tablist\"` nh\u01B0ng \u0111o 2026-09-17 b\u00EAn trong c\u00F3 **0\n `role=\"tab\"` v\u00E0 0 ph\u1EA7n t\u1EED tab t\u1EDBi \u0111\u01B0\u1EE3c** \u2014 `tablist` r\u1ED7ng vai tr\u00F2,\n v\u00E0 b\u00E0n ph\u00EDm kh\u00F4ng ch\u1ECDn n\u1ED5i b\u01B0\u1EDBc n\u00E0o. Ba bi\u1EBFn th\u1EC3 stepper kh\u00E1c \u0111\u00E3\n d\u00F9ng `<button>` s\u1EB5n; ch\u1EC9 ri\u00EAng `cards` l\u1EC7ch.\n\n `w-full` v\u00EC button kh\u00F4ng t\u1EF1 gi\u00E3n nh\u01B0 div.\n -->\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"step.effectiveState() === 'active'\"\n [disabled]=\"step.effectiveState() === 'disabled'\"\n (click)=\"goToStep(i)\"\n [class]=\"\n 'w-full flex items-center justify-between p-4 rounded-md border transition-all duration-200 text-left ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-secondary-soft border-default text-fg-disabled'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer shadow-xs ring-2 ring-brand-subtle'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer hover:shadow-xs border-danger-subtle bg-danger-soft'\n : 'cursor-pointer hover:shadow-xs border-default-medium bg-neutral-secondary-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n <div class=\"flex items-center gap-3 min-w-0\">\n <!-- Left Icon / Step Badge -->\n <div\n [class]=\"\n 'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 font-bold text-xs transition-colors ' +\n (step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'active'\n ? 'bg-brand-softer text-fg-brand-strong'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body')\n \"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </div>\n\n <!-- Title & Subtitle -->\n <div class=\"min-w-0\">\n <div\n [class]=\"\n 'text-sm truncate ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand-strong '\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled truncate\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n\n <!-- Right Status Icon -->\n <div class=\"shrink-0 ml-3\">\n @if (step.effectiveState() === 'completed') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <ol-icon name=\"check-circle\" size=\"xs\" color=\"brand\"></ol-icon>\n <span class=\"hidden sm:inline\">Ho\u00E0n t\u1EA5t</span>\n </span>\n } @else if (step.effectiveState() === 'active') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\n <span class=\"w-2 h-2 rounded-full bg-brand animate-pulse\"></span>\n <span class=\"hidden sm:inline\">\u0110ang th\u1EF1c hi\u1EC7n</span>\n </span>\n } @else if (step.effectiveState() === 'error') {\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-danger\">\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n <span class=\"hidden sm:inline\">L\u1ED7i</span>\n </span>\n } @else {\n <ol-icon name=\"chevron-right\" size=\"xs\" class=\"text-fg-disabled\"></ol-icon>\n }\n </div>\n </button>\n }\n </div>\n } @else if (isVertical()) {\n <!-- Vertical Variant -->\n <div class=\"flex flex-col relative\" role=\"tablist\" aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\">\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-start gap-4 relative pb-8 last:pb-0\">\n <!-- Connecting Vertical Line -->\n @if (i < steps().length - 1) {\n <div\n class=\"absolute left-4.25 top-9 bottom-0 w-0.5 transition-colors duration-200\"\n [class.bg-brand]=\"i < currentStep()\"\n [class.bg-neutral-tertiary-medium]=\"i >= currentStep()\"\n ></div>\n }\n\n <!-- Step Circle / Icon Shape -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 z-10 ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 cursor-not-allowed bg-neutral-tertiary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'cursor-pointer bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'cursor-pointer border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'cursor-pointer bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'cursor-pointer bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n\n <!-- Step Title & Subtitle -->\n <div class=\"text-left pt-1 cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled mt-0.5\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n </div>\n }\n </div>\n } @else {\n <!-- Horizontal Variants: Default | Only Icons | Icons & Text | With Dots -->\n <div\n class=\"flex items-center justify-between w-full relative\"\n role=\"tablist\"\n aria-label=\"Ti\u1EBFn tr\u00ECnh c\u00E1c b\u01B0\u1EDBc\"\n >\n @for (step of steps(); track step.stepId(); let i = $index) {\n <div class=\"flex items-center flex-1 last:flex-none relative\">\n <div class=\"flex items-center gap-3\">\n @if (variant() === 'with-dots') {\n <!-- With Dots Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'flex items-center justify-center transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'w-3.5 h-3.5 rounded-full opacity-50 bg-neutral-primary text-fg-disabled border-2 border-default-medium'\n : step.effectiveState() === 'completed'\n ? 'w-4 h-4 rounded-full bg-brand text-white'\n : step.effectiveState() === 'active'\n ? 'w-4 h-4 rounded-full bg-neutral-primary border-2 border-brand-light ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'w-4 h-4 rounded-full bg-danger-surface ring-4 ring-danger-subtle'\n : 'w-3.5 h-3.5 rounded-full bg-neutral-primary border-2 border-default-medium')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" class=\"scale-75\"></ol-icon>\n }\n </button>\n } @else if (variant() === 'only-icons' || variant() === 'icons-text') {\n <!-- Icon Shape Variant -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-10 h-10 rounded-full flex items-center justify-center font-semibold text-sm transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-tertiary text-fg-disabled'\n : step.effectiveState() === 'completed'\n ? 'bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-tertiary text-body border border-default')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"sm\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"sm\" color=\"danger\"></ol-icon>\n } @else if (step.icon()) {\n <ol-icon [name]=\"step.icon()!\" size=\"sm\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n } @else {\n <!-- Default Variant (Numbered Circle) -->\n <button\n type=\"button\"\n (click)=\"goToStep(i)\"\n [disabled]=\"\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\n \"\n [class]=\"\n 'w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\n (step.effectiveState() === 'disabled'\n ? 'opacity-50 bg-neutral-primary text-fg-disabled border border-default'\n : step.effectiveState() === 'completed'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'active'\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\n : step.effectiveState() === 'error'\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\n : 'bg-neutral-primary text-body border border-default-strong')\n \"\n [attr.aria-label]=\"stepAriaLabel(i)\"\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\n >\n @if (step.effectiveState() === 'completed') {\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\n } @else if (step.effectiveState() === 'error') {\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\n } @else {\n <span>{{ i + 1 }}</span>\n }\n </button>\n }\n\n <!-- Text Title & Subtitle (unless only-icons) -->\n @if (variant() !== 'only-icons') {\n <div class=\"text-left cursor-pointer\" (click)=\"goToStep(i)\">\n <div\n [class]=\"\n 'text-sm transition-colors ' +\n (step.effectiveState() === 'active'\n ? 'font-bold text-fg-brand'\n : step.effectiveState() === 'completed'\n ? 'font-semibold text-heading'\n : step.effectiveState() === 'error'\n ? 'font-semibold text-fg-danger'\n : 'font-medium text-body-subtle')\n \"\n >\n {{ step.title() }}\n </div>\n @if (step.badge()) {\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\n }\n @if (step.effectiveSubtitle()) {\n <div class=\"text-xs text-fg-disabled hidden md:block\">\n {{ step.effectiveSubtitle() }}\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Connecting Line (Horizontal) -->\n @if (i < steps().length - 1) {\n <div class=\"flex-1 mx-4 h-0.5 bg-neutral-tertiary-medium relative overflow-hidden\">\n <div\n class=\"h-full bg-brand transition-all duration-300\"\n [class.w-full]=\"i < currentStep()\"\n [class.w-0]=\"i >= currentStep()\"\n ></div>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n\n <!-- Stepper Body Content Area -->\n <div [class]=\"isVertical() ? 'flex-1 min-w-0' : 'w-full'\">\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
14963
15324
|
}
|
|
14964
15325
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlStepper, decorators: [{
|
|
14965
15326
|
type: Component,
|
|
@@ -15041,15 +15402,50 @@ class OlTreeNodeItem {
|
|
|
15041
15402
|
expanded: !!this.node().expanded,
|
|
15042
15403
|
}), /* @ts-ignore */
|
|
15043
15404
|
...(ngDevMode ? [{ debugName: "tplContext" }] : /* istanbul ignore next */ []));
|
|
15405
|
+
/**
|
|
15406
|
+
* Chỉ `id` của nút, tách khỏi cả object.
|
|
15407
|
+
*
|
|
15408
|
+
* `effect` bên dưới đọc tín hiệu này thay vì `this.node()`: đọc cả object
|
|
15409
|
+
* thì effect chạy lại mỗi lần nơi dùng thay MẢNG gốc — kiểu tải lại danh
|
|
15410
|
+
* sách từ máy chủ — dù chính nút này không đổi gì. `computed` chỉ phát tín
|
|
15411
|
+
* hiệu khi GIÁ TRỊ đổi, mà `id` là chuỗi nên so sánh bằng `===`: tải lại
|
|
15412
|
+
* 100 nút với cùng id thì effect không chạy lần nào.
|
|
15413
|
+
*/
|
|
15414
|
+
nodeId = computed(() => this.node().id, /* @ts-ignore */
|
|
15415
|
+
...(ngDevMode ? [{ debugName: "nodeId" }] : /* istanbul ignore next */ []));
|
|
15044
15416
|
constructor() {
|
|
15045
|
-
|
|
15046
|
-
|
|
15417
|
+
/*
|
|
15418
|
+
* Điều hướng bằng phím chỉ đổi `focusedId`; phải gọi `.focus()` thật thì
|
|
15419
|
+
* trình duyệt mới chuyển con trỏ.
|
|
15420
|
+
*
|
|
15421
|
+
* HAI ĐIỀU KIỆN CHẶN, cả hai đều có lý do đo được:
|
|
15422
|
+
*
|
|
15423
|
+
* 1. Đọc `nodeId()` chứ không `node()`. Đo bằng ca thử "tải lại toàn bộ
|
|
15424
|
+
* cây, id giữ nguyên": bản cũ effect chạy lại và CƯỚP focus khỏi phần
|
|
15425
|
+
* tử người dùng đang đứng (đo được focus nhảy từ nút ngoài cây vào một
|
|
15426
|
+
* `div` trong cây).
|
|
15427
|
+
*
|
|
15428
|
+
* 2. Chỉ lấy focus khi focus ĐANG Ở TRONG CÂY. Người dùng bấm ra ô tìm
|
|
15429
|
+
* kiếm rồi dữ liệu tự cập nhật thì con trỏ đang ở ô tìm — kéo nó về cây
|
|
15430
|
+
* là cướp thao tác đang làm dở. `focusedId` chỉ được đặt khi điều hướng
|
|
15431
|
+
* bằng phím, nhưng nó KHÔNG TỰ XOÁ khi người dùng rời cây.
|
|
15432
|
+
*
|
|
15433
|
+
* Hệ quả phụ, cũng là thứ người dùng báo: `.focus()` gọi BẰNG MÃ làm
|
|
15434
|
+
* `:focus-visible` khớp, nên viền brand 2px hiện ra sau mỗi lần cập nhật
|
|
15435
|
+
* dữ liệu — trông như "viền đen to" trên nền sẫm. Chặn effect chạy oan là
|
|
15436
|
+
* hết luôn viền oan.
|
|
15437
|
+
*/
|
|
15047
15438
|
effect(() => {
|
|
15048
|
-
if (this.focusedId() !== this.
|
|
15439
|
+
if (this.focusedId() !== this.nodeId())
|
|
15049
15440
|
return;
|
|
15050
15441
|
const el = this.hang()?.nativeElement;
|
|
15051
|
-
if (el
|
|
15052
|
-
|
|
15442
|
+
if (!el || document.activeElement === el)
|
|
15443
|
+
return;
|
|
15444
|
+
// Chỉ cướp focus khi nó vốn đã ở trong cây này.
|
|
15445
|
+
const tree = el.closest('[role="tree"]');
|
|
15446
|
+
if (tree && !tree.contains(document.activeElement))
|
|
15447
|
+
return;
|
|
15448
|
+
el.focus();
|
|
15053
15449
|
});
|
|
15054
15450
|
}
|
|
15055
15451
|
/**
|
|
@@ -15094,7 +15490,7 @@ class OlTreeNodeItem {
|
|
|
15094
15490
|
this.nodeCheck.emit({ node: this.node(), checked });
|
|
15095
15491
|
}
|
|
15096
15492
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTreeNodeItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15097
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTreeNodeItem, isStandalone: true, selector: "ol-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null }, actionsTemplate: { classPropertyName: "actionsTemplate", publicName: "actionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, checkable: { classPropertyName: "checkable", publicName: "checkable", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, toggleIcon: { classPropertyName: "toggleIcon", publicName: "toggleIcon", isSignal: true, isRequired: false, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, tabbableId: { classPropertyName: "tabbableId", publicName: "tabbableId", isSignal: true, isRequired: false, transformFunction: null }, focusedId: { classPropertyName: "focusedId", publicName: "focusedId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeClick: "nodeClick", nodeCheck: "nodeCheck", toggleExpand: "toggleExpand", nodeFocus: "nodeFocus" }, viewQueries: [{ propertyName: "hang", first: true, predicate: ["hang"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"select-none py-0.5\">\n <!-- Focus v\u1EBD VI\u1EC0N 2px b\u00EAn trong, kh\u00F4ng ph\u1EA3i ring: Figma `ol-tree`\n State=Focus \u0111o \u0111\u01B0\u1EE3c `strokeWeight: 2`, `strokeAlign: INSIDE`, m\u00E0u\n `border/border-brand` #2b4182. Ring v\u1EBD NGO\u00C0I h\u1ED9p n\u00EAn kh\u00E1c h\u00ECnh, v\u00E0 \u1EDF\n c\u00E2y l\u1ED3ng nhau n\u00F3 ch\u1ED3ng l\u00EAn node b\u00EAn c\u1EA1nh. -->\n <div\n #hang\n role=\"treeitem\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-level]=\"ariaLevel()\"\n [attr.aria-expanded]=\"ariaExpanded()\"\n [attr.aria-selected]=\"!!node().selected\"\n [attr.aria-disabled]=\"node().disabled ? true : null\"\n (focus)=\"nodeFocus.emit(node().id)\"\n class=\"group flex cursor-pointer items-center gap-2 rounded-lg border-2 border-transparent p-1 text-sm outline-hidden transition-colors focus:outline-hidden focus-visible:border-brand\"\n [class.bg-brand-softer]=\"node().selected\"\n [class.text-fg-brand]=\"node().selected\"\n [class.font-medium]=\"node().selected\"\n [class.text-heading]=\"!node().selected && !node().disabled\"\n [class.hover:bg-brand-softer]=\"!node().selected && !node().disabled\"\n [class.opacity-50]=\"node().disabled\"\n [class.cursor-not-allowed]=\"node().disabled\"\n (click)=\"handleClick()\"\n >\n <!-- Expand Toggle Button -->\n @if (node().children && node().children!.length > 0) {\n <!--\n N\u00FAt ch\u1EC9 c\u00F3 icon n\u00EAn PH\u1EA2I c\u00F3 t\u00EAn: \u0111o `/tree` ra 18 n\u00FAt g\u1EADp kh\u00F4ng t\u00EAn\n trong c\u00E2y tr\u1EE3 n\u0103ng. `aria-expanded` cho bi\u1EBFt tr\u1EA1ng th\u00E1i, `aria-label`\n cho bi\u1EBFt n\u00FAt n\u00E0y g\u1EADp/m\u1EDF NH\u00C1NH N\u00C0O \u2014 nhi\u1EC1u n\u00FAt gi\u1ED1ng h\u1EC7t nhau tr\u00EAn m\u1ED9t\n trang, ch\u1EC9 \"M\u1EDF r\u1ED9ng\" th\u00F4i th\u00EC v\u1EABn kh\u00F4ng ph\u00E2n bi\u1EC7t \u0111\u01B0\u1EE3c.\n -->\n <button\n type=\"button\"\n (click)=\"handleToggle($event)\"\n [attr.aria-expanded]=\"!!node().expanded\"\n [attr.aria-label]=\"(node().expanded ? 'Thu g\u1ECDn ' : 'M\u1EDF r\u1ED9ng ') + (node().label || 'nh\u00E1nh')\"\n class=\"w-5 h-5 flex items-center justify-center rounded text-body-subtle hover:text-heading transition-colors shrink-0\"\n >\n @if (toggleIcon() === 'plus-minus') {\n <span\n class=\"w-4 h-4 rounded border border-default-strong flex items-center justify-center text-xs font-mono font-bold leading-none text-body\"\n >\n {{ node().expanded ? '\u2212' : '+' }}\n </span>\n } @else {\n <ol-icon\n [name]=\"node().expanded ? 'alt-arrow-down' : 'alt-arrow-right'\"\n size=\"xs\"\n ></ol-icon>\n }\n </button>\n } @else {\n @if (!checkable()) {\n <span class=\"w-5 h-5 flex items-center justify-center shrink-0\">\n <span\n class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand transition-colors\"\n ></span>\n </span>\n }\n }\n <!-- Checkbox option (when checkable) -->\n @if (checkable()) {\n <div class=\"flex items-center justify-center shrink-0\" (click)=\"$event.stopPropagation()\">\n <ol-checkbox\n [standalone]=\"true\"\n [ariaLabel]=\"'Ch\u1ECDn ' + (node().label || 'nh\u00E1nh')\"\n [checked]=\"node().selected ?? false\"\n [indeterminate]=\"indeterminate()\"\n [disabled]=\"node().disabled ?? false\"\n (checkedChange)=\"handleCheck($event)\"\n size=\"base\"\n ></ol-checkbox>\n </div>\n }\n <!--\n Template tu\u1EF3 bi\u1EBFn CH\u1EC8 thay ph\u1EA7n n\u1ED9i dung (icon \u00B7 nh\u00E3n \u00B7 count \u00B7 badge).\n N\u00FAt m\u1EDF r\u1ED9ng, checkbox v\u00E0 th\u1EBB `role=\"treeitem\"` b\u1ECDc ngo\u00E0i v\u1EABn do lib gi\u1EEF \u2014\n cho ng\u01B0\u1EDDi d\u00F9ng v\u1EBD c\u1EA3 h\u00E0ng l\u00E0 m\u1EA5t ARIA \u0111\u00E3 l\u00E0m \u0111\u00FAng chu\u1EA9n \u1EDF BUG-017.\n -->\n @if (nodeTemplate()) {\n <ng-container *ngTemplateOutlet=\"nodeTemplate()!; context: tplContext()\" />\n } @else {\n <!-- Node Icon (if provided) -->\n @if (node().icon) {\n <ol-icon\n [name]=\"node().icon!\"\n size=\"xs\"\n [class.text-fg-brand]=\"node().selected\"\n [class.text-body-subtle]=\"!node().selected\"\n ></ol-icon>\n }\n <!-- Node Label -->\n <span class=\"truncate flex-1\">{{ node().label }}</span>\n <!-- Count or Badge -->\n @if (node().count !== undefined) {\n <span class=\"text-2xs text-body-subtle font-mono\"> ({{ node().count }}) </span>\n }\n @if (node().badge) {\n <ol-badge [color]=\"node().badgeColor || 'brand'\" size=\"xs\" shape=\"pill\">\n {{ node().badge }}\n </ol-badge>\n }\n }\n <!--\n H\u00E0nh \u0111\u1ED9ng cu\u1ED1i h\u00E0ng. \u0110\u1EB7t NGO\u00C0I c\u1EB7p @if/@else \u1EDF tr\u00EAn l\u00E0 c\u00F3 ch\u1EE7 \u00FD: n\u00FAt ph\u1EA3i\n hi\u1EC7n c\u1EA3 khi ng\u01B0\u1EDDi d\u00F9ng t\u1EF1 v\u1EBD n\u1ED9i dung b\u1EB1ng `nodeTemplate`. \u0110\u01B0a v\u00E0o trong\n nh\u00E1nh @else th\u00EC b\u1EADt `nodeTemplate` l\u00E0 n\u00FAt bi\u1EBFn m\u1EA5t \u2014 build v\u1EABn xanh.\n -->\n @if (actionsTemplate()) {\n <span class=\"ms-auto flex shrink-0 items-center gap-1\">\n <ng-container *ngTemplateOutlet=\"actionsTemplate()!; context: tplContext()\" />\n </span>\n }\n </div>\n <!-- Children Nodes (Indented 20px guide line + pl-5 child offset) -->\n @if (node().expanded && node().children && node().children!.length > 0) {\n <div role=\"group\" class=\"ml-5 mt-1 space-y-1 border-l border-default pl-5\">\n @for (child of node().children; track child.id) {\n <ol-tree-node\n [node]=\"child\"\n [nodeTemplate]=\"nodeTemplate()\"\n [actionsTemplate]=\"actionsTemplate()\"\n [checkable]=\"checkable()\"\n [selectionMode]=\"selectionMode()\"\n [toggleIcon]=\"toggleIcon()\"\n [level]=\"level() + 1\"\n [tabbableId]=\"tabbableId()\"\n [focusedId]=\"focusedId()\"\n (nodeFocus)=\"nodeFocus.emit($event)\"\n (nodeClick)=\"nodeClick.emit($event)\"\n (nodeCheck)=\"nodeCheck.emit($event)\"\n (toggleExpand)=\"toggleExpand.emit($event)\"\n ></ol-tree-node>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: OlTreeNodeItem, selector: "ol-tree-node", inputs: ["node", "nodeTemplate", "actionsTemplate", "checkable", "selectionMode", "toggleIcon", "level", "tabbableId", "focusedId"], outputs: ["nodeClick", "nodeCheck", "toggleExpand", "nodeFocus"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "component", type: OlCheckbox, selector: "ol-checkbox", inputs: ["checked", "label", "ariaLabel", "description", "disabled", "size", "indeterminate", "card", "id", "standalone"], outputs: ["checkedChange", "disabledChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15493
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlTreeNodeItem, isStandalone: true, selector: "ol-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, nodeTemplate: { classPropertyName: "nodeTemplate", publicName: "nodeTemplate", isSignal: true, isRequired: false, transformFunction: null }, actionsTemplate: { classPropertyName: "actionsTemplate", publicName: "actionsTemplate", isSignal: true, isRequired: false, transformFunction: null }, checkable: { classPropertyName: "checkable", publicName: "checkable", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, toggleIcon: { classPropertyName: "toggleIcon", publicName: "toggleIcon", isSignal: true, isRequired: false, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, tabbableId: { classPropertyName: "tabbableId", publicName: "tabbableId", isSignal: true, isRequired: false, transformFunction: null }, focusedId: { classPropertyName: "focusedId", publicName: "focusedId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeClick: "nodeClick", nodeCheck: "nodeCheck", toggleExpand: "toggleExpand", nodeFocus: "nodeFocus" }, viewQueries: [{ propertyName: "hang", first: true, predicate: ["hang"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"select-none py-0.5\">\n <!-- Focus v\u1EBD VI\u1EC0N 2px b\u00EAn trong, kh\u00F4ng ph\u1EA3i ring: Figma `ol-tree`\n State=Focus \u0111o \u0111\u01B0\u1EE3c `strokeWeight: 2`, `strokeAlign: INSIDE`, m\u00E0u\n `border/border-brand` #2b4182. Ring v\u1EBD NGO\u00C0I h\u1ED9p n\u00EAn kh\u00E1c h\u00ECnh, v\u00E0 \u1EDF\n c\u00E2y l\u1ED3ng nhau n\u00F3 ch\u1ED3ng l\u00EAn node b\u00EAn c\u1EA1nh. -->\n <div\n #hang\n role=\"treeitem\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-level]=\"ariaLevel()\"\n [attr.aria-expanded]=\"ariaExpanded()\"\n [attr.aria-selected]=\"!!node().selected\"\n [attr.aria-disabled]=\"node().disabled ? true : null\"\n (focus)=\"nodeFocus.emit(node().id)\"\n class=\"group flex cursor-pointer items-center gap-2 rounded-lg border-2 border-transparent p-1 text-sm outline-hidden transition-colors focus:outline-hidden focus-visible:border-brand\"\n [class.bg-brand-softer]=\"node().selected\"\n [class.text-fg-brand]=\"node().selected\"\n [class.font-medium]=\"node().selected\"\n [class.text-heading]=\"!node().selected && !node().disabled\"\n [class.hover:bg-brand-softer]=\"!node().selected && !node().disabled\"\n [class.opacity-50]=\"node().disabled\"\n [class.cursor-not-allowed]=\"node().disabled\"\n (click)=\"handleClick()\"\n >\n <!-- Expand Toggle Button -->\n @if (node().children && node().children!.length > 0) {\n <!--\n N\u00FAt ch\u1EC9 c\u00F3 icon n\u00EAn PH\u1EA2I c\u00F3 t\u00EAn: \u0111o `/tree` ra 18 n\u00FAt g\u1EADp kh\u00F4ng t\u00EAn\n trong c\u00E2y tr\u1EE3 n\u0103ng. `aria-expanded` cho bi\u1EBFt tr\u1EA1ng th\u00E1i, `aria-label`\n cho bi\u1EBFt n\u00FAt n\u00E0y g\u1EADp/m\u1EDF NH\u00C1NH N\u00C0O \u2014 nhi\u1EC1u n\u00FAt gi\u1ED1ng h\u1EC7t nhau tr\u00EAn m\u1ED9t\n trang, ch\u1EC9 \"M\u1EDF r\u1ED9ng\" th\u00F4i th\u00EC v\u1EABn kh\u00F4ng ph\u00E2n bi\u1EC7t \u0111\u01B0\u1EE3c.\n -->\n <button\n type=\"button\"\n (click)=\"handleToggle($event)\"\n [attr.aria-expanded]=\"!!node().expanded\"\n [attr.aria-label]=\"(node().expanded ? 'Thu g\u1ECDn ' : 'M\u1EDF r\u1ED9ng ') + (node().label || 'nh\u00E1nh')\"\n class=\"w-5 h-5 flex items-center justify-center rounded text-body-subtle hover:text-heading transition-colors shrink-0\"\n >\n @if (toggleIcon() === 'plus-minus') {\n <span\n class=\"w-4 h-4 rounded border border-default-strong flex items-center justify-center text-xs font-mono font-bold leading-none text-body\"\n >\n {{ node().expanded ? '\u2212' : '+' }}\n </span>\n } @else {\n <ol-icon\n [name]=\"node().expanded ? 'alt-arrow-down' : 'alt-arrow-right'\"\n size=\"xs\"\n ></ol-icon>\n }\n </button>\n } @else {\n @if (!checkable()) {\n <span class=\"w-5 h-5 flex items-center justify-center shrink-0\">\n <span\n class=\"w-1.5 h-1.5 rounded-full bg-gray-surface group-hover:bg-brand transition-colors\"\n ></span>\n </span>\n }\n }\n <!-- Checkbox option (when checkable) -->\n @if (checkable()) {\n <div class=\"flex items-center justify-center shrink-0\" (click)=\"$event.stopPropagation()\">\n <ol-checkbox\n [standalone]=\"true\"\n [ariaLabel]=\"'Ch\u1ECDn ' + (node().label || 'nh\u00E1nh')\"\n [checked]=\"node().selected ?? false\"\n [indeterminate]=\"indeterminate()\"\n [disabled]=\"node().disabled ?? false\"\n (checkedChange)=\"handleCheck($event)\"\n size=\"base\"\n ></ol-checkbox>\n </div>\n }\n <!--\n Template tu\u1EF3 bi\u1EBFn CH\u1EC8 thay ph\u1EA7n n\u1ED9i dung (icon \u00B7 nh\u00E3n \u00B7 count \u00B7 badge).\n N\u00FAt m\u1EDF r\u1ED9ng, checkbox v\u00E0 th\u1EBB `role=\"treeitem\"` b\u1ECDc ngo\u00E0i v\u1EABn do lib gi\u1EEF \u2014\n cho ng\u01B0\u1EDDi d\u00F9ng v\u1EBD c\u1EA3 h\u00E0ng l\u00E0 m\u1EA5t ARIA \u0111\u00E3 l\u00E0m \u0111\u00FAng chu\u1EA9n \u1EDF BUG-017.\n -->\n @if (nodeTemplate()) {\n <ng-container *ngTemplateOutlet=\"nodeTemplate()!; context: tplContext()\" />\n } @else {\n <!-- Node Icon (if provided) -->\n @if (node().icon) {\n <ol-icon\n [name]=\"node().icon!\"\n size=\"xs\"\n [class.text-fg-brand]=\"node().selected\"\n [class.text-body-subtle]=\"!node().selected\"\n ></ol-icon>\n }\n <!-- Node Label -->\n <span class=\"truncate flex-1\">{{ node().label }}</span>\n <!-- Count or Badge -->\n @if (node().count !== undefined) {\n <span class=\"text-2xs text-body-subtle font-mono\"> ({{ node().count }}) </span>\n }\n @if (node().badge) {\n <ol-badge [color]=\"node().badgeColor || 'brand'\" size=\"xs\" shape=\"pill\">\n {{ node().badge }}\n </ol-badge>\n }\n }\n <!--\n H\u00E0nh \u0111\u1ED9ng cu\u1ED1i h\u00E0ng. \u0110\u1EB7t NGO\u00C0I c\u1EB7p @if/@else \u1EDF tr\u00EAn l\u00E0 c\u00F3 ch\u1EE7 \u00FD: n\u00FAt ph\u1EA3i\n hi\u1EC7n c\u1EA3 khi ng\u01B0\u1EDDi d\u00F9ng t\u1EF1 v\u1EBD n\u1ED9i dung b\u1EB1ng `nodeTemplate`. \u0110\u01B0a v\u00E0o trong\n nh\u00E1nh @else th\u00EC b\u1EADt `nodeTemplate` l\u00E0 n\u00FAt bi\u1EBFn m\u1EA5t \u2014 build v\u1EABn xanh.\n -->\n @if (actionsTemplate()) {\n <span class=\"ms-auto flex shrink-0 items-center gap-1\">\n <ng-container *ngTemplateOutlet=\"actionsTemplate()!; context: tplContext()\" />\n </span>\n }\n </div>\n <!-- Children Nodes (Indented 20px guide line + pl-5 child offset) -->\n @if (node().expanded && node().children && node().children!.length > 0) {\n <div role=\"group\" class=\"ml-5 mt-1 space-y-1 border-l border-default pl-5\">\n @for (child of node().children; track child.id) {\n <ol-tree-node\n [node]=\"child\"\n [nodeTemplate]=\"nodeTemplate()\"\n [actionsTemplate]=\"actionsTemplate()\"\n [checkable]=\"checkable()\"\n [selectionMode]=\"selectionMode()\"\n [toggleIcon]=\"toggleIcon()\"\n [level]=\"level() + 1\"\n [tabbableId]=\"tabbableId()\"\n [focusedId]=\"focusedId()\"\n (nodeFocus)=\"nodeFocus.emit($event)\"\n (nodeClick)=\"nodeClick.emit($event)\"\n (nodeCheck)=\"nodeCheck.emit($event)\"\n (toggleExpand)=\"toggleExpand.emit($event)\"\n ></ol-tree-node>\n }\n </div>\n }\n</div>\n", dependencies: [{ kind: "component", type: OlTreeNodeItem, selector: "ol-tree-node", inputs: ["node", "nodeTemplate", "actionsTemplate", "checkable", "selectionMode", "toggleIcon", "level", "tabbableId", "focusedId"], outputs: ["nodeClick", "nodeCheck", "toggleExpand", "nodeFocus"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "component", type: OlCheckbox, selector: "ol-checkbox", inputs: ["checked", "label", "ariaLabel", "description", "disabled", "size", "indeterminate", "card", "id", "standalone"], outputs: ["checkedChange", "disabledChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15098
15494
|
}
|
|
15099
15495
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlTreeNodeItem, decorators: [{
|
|
15100
15496
|
type: Component,
|
|
@@ -16076,11 +16472,11 @@ class OlChart {
|
|
|
16076
16472
|
}, /* @ts-ignore */
|
|
16077
16473
|
...(ngDevMode ? [{ debugName: "horizontalBarsComputed" }] : /* istanbul ignore next */ []));
|
|
16078
16474
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
16079
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlChart, isStandalone: true, selector: "ol-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, categories: { classPropertyName: "categories", publicName: "categories", isSignal: true, isRequired: false, transformFunction: null }, series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: false, transformFunction: null }, dataPoints: { classPropertyName: "dataPoints", publicName: "dataPoints", isSignal: true, isRequired: false, transformFunction: null }, indicators: { classPropertyName: "indicators", publicName: "indicators", isSignal: true, isRequired: false, transformFunction: null }, scatterSeries: { classPropertyName: "scatterSeries", publicName: "scatterSeries", isSignal: true, isRequired: false, transformFunction: null }, heatmapData: { classPropertyName: "heatmapData", publicName: "heatmapData", isSignal: true, isRequired: false, transformFunction: null }, boxplotData: { classPropertyName: "boxplotData", publicName: "boxplotData", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, xAxisTitle: { classPropertyName: "xAxisTitle", publicName: "xAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, yAxisTitle: { classPropertyName: "yAxisTitle", publicName: "yAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 13/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n\n 38 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n text-gray-700 / dark:text-gray-300 s\u00E1ng \u0111\u1EADm, t\u1ED1i nh\u1EA1t\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
16475
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlChart, isStandalone: true, selector: "ol-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, categories: { classPropertyName: "categories", publicName: "categories", isSignal: true, isRequired: false, transformFunction: null }, series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: false, transformFunction: null }, dataPoints: { classPropertyName: "dataPoints", publicName: "dataPoints", isSignal: true, isRequired: false, transformFunction: null }, indicators: { classPropertyName: "indicators", publicName: "indicators", isSignal: true, isRequired: false, transformFunction: null }, scatterSeries: { classPropertyName: "scatterSeries", publicName: "scatterSeries", isSignal: true, isRequired: false, transformFunction: null }, heatmapData: { classPropertyName: "heatmapData", publicName: "heatmapData", isSignal: true, isRequired: false, transformFunction: null }, boxplotData: { classPropertyName: "boxplotData", publicName: "boxplotData", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, xAxisTitle: { classPropertyName: "xAxisTitle", publicName: "xAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, yAxisTitle: { classPropertyName: "yAxisTitle", publicName: "yAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 16/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n text-gray-700 dark:text-gray-300 -> text-body (3 l\u01B0\u1EE3t, 2026-09-24)\n\n Ba l\u01B0\u1EE3t cu\u1ED1i: ch\u00FA th\u00EDch c\u0169 x\u1EBFp ch\u00FAng v\u00E0o nh\u00F3m \"kh\u00F4ng token n\u00E0o kh\u1EDBp\", \u0111o l\u1EA1i\n th\u00EC `--text-body` \u0111\u1ED5i \u0110\u00DANG chi\u1EC1u #4a5565 -> #d1d5db, v\u00E0 gi\u00E1 tr\u1ECB t\u1ED1i TR\u00D9NG\n KH\u00CDT gray-300 c\u0169. S\u00E1ng nh\u1EA1t h\u01A1n m\u1ED9t b\u1EADc (#364153 -> #4a5565), t\u01B0\u01A1ng ph\u1EA3n\n tr\u00EAn n\u1EC1n tr\u1EAFng v\u1EABn 7,56 \u2014 d\u01B0 chu\u1EA9n AA 4,5.\n\n 35 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
16080
16476
|
}
|
|
16081
16477
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlChart, decorators: [{
|
|
16082
16478
|
type: Component,
|
|
16083
|
-
args: [{ selector: 'ol-chart', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 13/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n\n 38 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n text-gray-700 / dark:text-gray-300 s\u00E1ng \u0111\u1EADm, t\u1ED1i nh\u1EA1t\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-gray-700]=\"!s.hidden\"\n [class.dark:text-gray-300]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"] }]
|
|
16479
|
+
args: [{ selector: 'ol-chart', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 16/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n text-gray-700 dark:text-gray-300 -> text-body (3 l\u01B0\u1EE3t, 2026-09-24)\n\n Ba l\u01B0\u1EE3t cu\u1ED1i: ch\u00FA th\u00EDch c\u0169 x\u1EBFp ch\u00FAng v\u00E0o nh\u00F3m \"kh\u00F4ng token n\u00E0o kh\u1EDBp\", \u0111o l\u1EA1i\n th\u00EC `--text-body` \u0111\u1ED5i \u0110\u00DANG chi\u1EC1u #4a5565 -> #d1d5db, v\u00E0 gi\u00E1 tr\u1ECB t\u1ED1i TR\u00D9NG\n KH\u00CDT gray-300 c\u0169. S\u00E1ng nh\u1EA1t h\u01A1n m\u1ED9t b\u1EADc (#364153 -> #4a5565), t\u01B0\u01A1ng ph\u1EA3n\n tr\u00EAn n\u1EC1n tr\u1EAFng v\u1EABn 7,56 \u2014 d\u01B0 chu\u1EA9n AA 4,5.\n\n 35 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"] }]
|
|
16084
16480
|
}], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], categories: [{ type: i0.Input, args: [{ isSignal: true, alias: "categories", required: false }] }], series: [{ type: i0.Input, args: [{ isSignal: true, alias: "series", required: false }] }], dataPoints: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataPoints", required: false }] }], indicators: [{ type: i0.Input, args: [{ isSignal: true, alias: "indicators", required: false }] }], scatterSeries: [{ type: i0.Input, args: [{ isSignal: true, alias: "scatterSeries", required: false }] }], heatmapData: [{ type: i0.Input, args: [{ isSignal: true, alias: "heatmapData", required: false }] }], boxplotData: [{ type: i0.Input, args: [{ isSignal: true, alias: "boxplotData", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], showGrid: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGrid", required: false }] }], xAxisTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "xAxisTitle", required: false }] }], yAxisTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "yAxisTitle", required: false }] }], unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: false }] }] } });
|
|
16085
16481
|
|
|
16086
16482
|
/** Nhãn hiển thị + icon cho từng trạng thái. */
|
|
@@ -16235,11 +16631,11 @@ class OlJumbotron {
|
|
|
16235
16631
|
}, /* @ts-ignore */
|
|
16236
16632
|
...(ngDevMode ? [{ debugName: "badgeClasses" }] : /* istanbul ignore next */ []));
|
|
16237
16633
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlJumbotron, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
16238
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlJumbotron, isStandalone: true, selector: "ol-jumbotron", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, badgeText: { classPropertyName: "badgeText", publicName: "badgeText", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\
|
|
16634
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlJumbotron, isStandalone: true, selector: "ol-jumbotron", inputs: { headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, badgeText: { classPropertyName: "badgeText", publicName: "badgeText", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\u0169, \u0110\u00C3 G\u1EE0 H\u1EBET (2026-09-24). Ch\u00FA th\u00EDch tr\u01B0\u1EDBc \u0111\u00E2y ghi\n \"kh\u00F4ng token n\u00E0o kh\u1EDBp\" l\u00E0 SAI \u2014 ph\u00E9p \u0111o c\u0169 \u0111\u1ECDc hex t\u1EEB file CSS v\u00E0 l\u1EA5y nh\u1EA7m\n l\u1EA7n khai trong kh\u1ED1i `.dark`, n\u00EAn token n\u00E0o c\u0169ng ra gi\u00E1 tr\u1ECB ch\u1EBF \u0111\u1ED9 t\u1ED1i.\n\n \u0110o l\u1EA1i tr\u00EAn Chrome, \u0111\u1ECDc `getComputedStyle` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 (\u0111\u1ED1i ch\u1EE9ng d\u01B0\u01A1ng:\n `--text-heading` #101828 -> #ffffff), ba token n\u00E0y T\u1EF0 \u0110\u1ED4I theo `.dark`:\n\n --bg-secondary #f9fafb -> #030712\n --text-body #4a5565 -> #d1d5db (tr\u00F9ng kh\u00EDt gray-300 c\u0169)\n --bg-primary-soft #ffffff -> #101828 (tr\u00F9ng kh\u00EDt gray-900 c\u0169)\n\n N\u00EAn `dark:bg-gray-900` v\u00E0 `dark:text-gray-300` ch\u1EC9 \u0110\u00C8 L\u00CAN token \u0111\u00E3 \u0111\u00FAng:\n nh\u00ECn kh\u00F4ng kh\u00E1c g\u00EC, nh\u01B0ng token \u0111\u1ED5i l\u00E0 h\u1ECFng ngay m\u00E0 kh\u00F4ng ai th\u1EA5y.\n-->\n<!-- Figma trang Jumbotron ch\u1EC9 \u0111\u1ECBnh ngh\u0129a bi\u1EBFn th\u1EC3 n\u1EC1n brand; c\u00E1c bi\u1EBFn th\u1EC3\n `gradient`, `dark`, `light` b\u00EAn d\u01B0\u1EDBi l\u00E0 m\u1EDF r\u1ED9ng NGO\u00C0I Figma n\u00EAn gi\u1EEF\n b\u1EADc s\u1ED1 cho t\u1EDBi khi thi\u1EBFt k\u1EBF \u0111\u1ECBnh ngh\u0129a. -->\n<div\n class=\"relative overflow-hidden rounded-3xl p-6 sm:p-8 md:p-12 border shadow-xl\"\n [class.bg-linear-to-tr]=\"variant() === 'gradient'\"\n [class.from-brand-950]=\"variant() === 'gradient'\"\n [class.via-brand-800]=\"variant() === 'gradient'\"\n [class.to-indigo-900]=\"variant() === 'gradient'\"\n [class.text-white]=\"variant() === 'gradient' || variant() === 'brand' || variant() === 'dark'\"\n [class.border-brand/50]=\"variant() === 'gradient'\"\n [class.bg-brand]=\"variant() === 'brand'\"\n [class.border-brand-subtle]=\"variant() === 'brand'\"\n [class.bg-gray-900]=\"variant() === 'dark'\"\n [class.border-gray-800]=\"variant() === 'dark'\"\n [class.bg-neutral-secondary]=\"variant() === 'light'\"\n [class.border-default]=\"variant() === 'light'\"\n [class.dark:border-default]=\"variant() === 'light'\"\n [class.text-heading]=\"variant() === 'light'\"\n [class.dark:text-white]=\"variant() === 'light'\"\n>\n <div class=\"relative z-10 max-w-3xl space-y-4\">\n <!-- Optional Badge Slot or text -->\n @if (badgeText()) {\n <div [class]=\"badgeClasses()\">\n <ng-content select=\"[slot=badge-icon]\"></ng-content>\n <span>{{ badgeText() }}</span>\n </div>\n }\n\n <!-- Title -->\n @if (title()) {\n @switch (headingLevel()) {\n @case (2) {\n <h2 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h2>\n }\n @case (3) {\n <h3 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h3>\n }\n @case (4) {\n <h4 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h4>\n }\n @case (5) {\n <h5 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h5>\n }\n @case (6) {\n <h6 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h6>\n }\n @default {\n <h1 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h1>\n }\n }\n }\n\n <!--\n Subtitle / Description\n\n Bi\u1EBFn th\u1EC3 n\u1EC1n BRAND d\u00F9ng `text-fg-white` + `opacity-90`, KH\u00D4NG\n `text-fg-brand-soft`. T\u00E1ch hai class ch\u1EE9 kh\u00F4ng vi\u1EBFt `text-fg-white/90`:\n c\u00FA ph\u00E1p opacity k\u00E9p kh\u00F4ng sinh CSS \u1EDF d\u1EF1 \u00E1n n\u00E0y, class v\u1EABn n\u1EB1m trong\n `classList` m\u00E0 ph\u1EA7n t\u1EED m\u1EA5t m\u00E0u.\n\n `--text-brand-soft` l\u1EADt theo n\u1EC1n TRANG (brand-100 \u1EDF s\u00E1ng, brand-900 \u1EDF\n t\u1ED1i), nh\u01B0ng jumbotron t\u1EF1 t\u00F4 gradient brand \u0111\u1EADm \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 l\u1EADt\n xong th\u00E0nh xanh \u0111\u1EADm tr\u00EAn xanh \u0111\u1EADm. \u0110o /jumbotron: s\u00E1ng 1,20 \u00B7 t\u1ED1i 1,29,\n \u1EA3nh ch\u1EE5p cho th\u1EA5y d\u00F2ng m\u00F4 t\u1EA3 ch\u00ECm h\u1EB3n (BUG-035).\n\n N\u1EC1n gradient n\u00EAn m\u1ECDi ph\u00E9p \u0111o t\u1EF1 \u0111\u1ED9ng \u0111\u1EC1u sai \u2014 ph\u1EA3i x\u00E1c nh\u1EADn b\u1EB1ng \u1EA3nh.\n\n Bi\u1EBFn th\u1EC3 `light` gi\u1EEF nguy\u00EAn: n\u00F3 \u0111i theo n\u1EC1n trang n\u00EAn `dark:` l\u00E0 \u0111\u00FAng.\n -->\n @if (subtitle()) {\n <p\n class=\"text-sm md:text-base max-w-2xl\"\n [class.text-fg-white]=\"variant() !== 'light'\"\n [class.opacity-90]=\"variant() !== 'light'\"\n [class.text-body]=\"variant() === 'light'\"\n >\n {{ subtitle() }}\n </p>\n }\n\n <!-- Content / CTA Slot -->\n <div class=\"pt-2 flex flex-wrap items-center gap-3\">\n <ng-content select=\"[slot=actions]\"></ng-content>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
16239
16635
|
}
|
|
16240
16636
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlJumbotron, decorators: [{
|
|
16241
16637
|
type: Component,
|
|
16242
|
-
args: [{ selector: 'ol-jumbotron', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\
|
|
16638
|
+
args: [{ selector: 'ol-jumbotron', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Ba class `dark:*` b\u1EADc s\u1ED1 c\u0169, \u0110\u00C3 G\u1EE0 H\u1EBET (2026-09-24). Ch\u00FA th\u00EDch tr\u01B0\u1EDBc \u0111\u00E2y ghi\n \"kh\u00F4ng token n\u00E0o kh\u1EDBp\" l\u00E0 SAI \u2014 ph\u00E9p \u0111o c\u0169 \u0111\u1ECDc hex t\u1EEB file CSS v\u00E0 l\u1EA5y nh\u1EA7m\n l\u1EA7n khai trong kh\u1ED1i `.dark`, n\u00EAn token n\u00E0o c\u0169ng ra gi\u00E1 tr\u1ECB ch\u1EBF \u0111\u1ED9 t\u1ED1i.\n\n \u0110o l\u1EA1i tr\u00EAn Chrome, \u0111\u1ECDc `getComputedStyle` \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 (\u0111\u1ED1i ch\u1EE9ng d\u01B0\u01A1ng:\n `--text-heading` #101828 -> #ffffff), ba token n\u00E0y T\u1EF0 \u0110\u1ED4I theo `.dark`:\n\n --bg-secondary #f9fafb -> #030712\n --text-body #4a5565 -> #d1d5db (tr\u00F9ng kh\u00EDt gray-300 c\u0169)\n --bg-primary-soft #ffffff -> #101828 (tr\u00F9ng kh\u00EDt gray-900 c\u0169)\n\n N\u00EAn `dark:bg-gray-900` v\u00E0 `dark:text-gray-300` ch\u1EC9 \u0110\u00C8 L\u00CAN token \u0111\u00E3 \u0111\u00FAng:\n nh\u00ECn kh\u00F4ng kh\u00E1c g\u00EC, nh\u01B0ng token \u0111\u1ED5i l\u00E0 h\u1ECFng ngay m\u00E0 kh\u00F4ng ai th\u1EA5y.\n-->\n<!-- Figma trang Jumbotron ch\u1EC9 \u0111\u1ECBnh ngh\u0129a bi\u1EBFn th\u1EC3 n\u1EC1n brand; c\u00E1c bi\u1EBFn th\u1EC3\n `gradient`, `dark`, `light` b\u00EAn d\u01B0\u1EDBi l\u00E0 m\u1EDF r\u1ED9ng NGO\u00C0I Figma n\u00EAn gi\u1EEF\n b\u1EADc s\u1ED1 cho t\u1EDBi khi thi\u1EBFt k\u1EBF \u0111\u1ECBnh ngh\u0129a. -->\n<div\n class=\"relative overflow-hidden rounded-3xl p-6 sm:p-8 md:p-12 border shadow-xl\"\n [class.bg-linear-to-tr]=\"variant() === 'gradient'\"\n [class.from-brand-950]=\"variant() === 'gradient'\"\n [class.via-brand-800]=\"variant() === 'gradient'\"\n [class.to-indigo-900]=\"variant() === 'gradient'\"\n [class.text-white]=\"variant() === 'gradient' || variant() === 'brand' || variant() === 'dark'\"\n [class.border-brand/50]=\"variant() === 'gradient'\"\n [class.bg-brand]=\"variant() === 'brand'\"\n [class.border-brand-subtle]=\"variant() === 'brand'\"\n [class.bg-gray-900]=\"variant() === 'dark'\"\n [class.border-gray-800]=\"variant() === 'dark'\"\n [class.bg-neutral-secondary]=\"variant() === 'light'\"\n [class.border-default]=\"variant() === 'light'\"\n [class.dark:border-default]=\"variant() === 'light'\"\n [class.text-heading]=\"variant() === 'light'\"\n [class.dark:text-white]=\"variant() === 'light'\"\n>\n <div class=\"relative z-10 max-w-3xl space-y-4\">\n <!-- Optional Badge Slot or text -->\n @if (badgeText()) {\n <div [class]=\"badgeClasses()\">\n <ng-content select=\"[slot=badge-icon]\"></ng-content>\n <span>{{ badgeText() }}</span>\n </div>\n }\n\n <!-- Title -->\n @if (title()) {\n @switch (headingLevel()) {\n @case (2) {\n <h2 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h2>\n }\n @case (3) {\n <h3 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h3>\n }\n @case (4) {\n <h4 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h4>\n }\n @case (5) {\n <h5 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h5>\n }\n @case (6) {\n <h6 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h6>\n }\n @default {\n <h1 class=\"text-2xl sm:text-3xl md:text-5xl font-black tracking-tight leading-tight\">\n {{ title() }}\n </h1>\n }\n }\n }\n\n <!--\n Subtitle / Description\n\n Bi\u1EBFn th\u1EC3 n\u1EC1n BRAND d\u00F9ng `text-fg-white` + `opacity-90`, KH\u00D4NG\n `text-fg-brand-soft`. T\u00E1ch hai class ch\u1EE9 kh\u00F4ng vi\u1EBFt `text-fg-white/90`:\n c\u00FA ph\u00E1p opacity k\u00E9p kh\u00F4ng sinh CSS \u1EDF d\u1EF1 \u00E1n n\u00E0y, class v\u1EABn n\u1EB1m trong\n `classList` m\u00E0 ph\u1EA7n t\u1EED m\u1EA5t m\u00E0u.\n\n `--text-brand-soft` l\u1EADt theo n\u1EC1n TRANG (brand-100 \u1EDF s\u00E1ng, brand-900 \u1EDF\n t\u1ED1i), nh\u01B0ng jumbotron t\u1EF1 t\u00F4 gradient brand \u0111\u1EADm \u1EDF C\u1EA2 HAI ch\u1EBF \u0111\u1ED9 \u2014 l\u1EADt\n xong th\u00E0nh xanh \u0111\u1EADm tr\u00EAn xanh \u0111\u1EADm. \u0110o /jumbotron: s\u00E1ng 1,20 \u00B7 t\u1ED1i 1,29,\n \u1EA3nh ch\u1EE5p cho th\u1EA5y d\u00F2ng m\u00F4 t\u1EA3 ch\u00ECm h\u1EB3n (BUG-035).\n\n N\u1EC1n gradient n\u00EAn m\u1ECDi ph\u00E9p \u0111o t\u1EF1 \u0111\u1ED9ng \u0111\u1EC1u sai \u2014 ph\u1EA3i x\u00E1c nh\u1EADn b\u1EB1ng \u1EA3nh.\n\n Bi\u1EBFn th\u1EC3 `light` gi\u1EEF nguy\u00EAn: n\u00F3 \u0111i theo n\u1EC1n trang n\u00EAn `dark:` l\u00E0 \u0111\u00FAng.\n -->\n @if (subtitle()) {\n <p\n class=\"text-sm md:text-base max-w-2xl\"\n [class.text-fg-white]=\"variant() !== 'light'\"\n [class.opacity-90]=\"variant() !== 'light'\"\n [class.text-body]=\"variant() === 'light'\"\n >\n {{ subtitle() }}\n </p>\n }\n\n <!-- Content / CTA Slot -->\n <div class=\"pt-2 flex flex-wrap items-center gap-3\">\n <ng-content select=\"[slot=actions]\"></ng-content>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n" }]
|
|
16243
16639
|
}], propDecorators: { headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], badgeText: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeText", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
|
|
16244
16640
|
|
|
16245
16641
|
/**
|
|
@@ -16331,5 +16727,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
16331
16727
|
* Generated bundle index. Do not edit.
|
|
16332
16728
|
*/
|
|
16333
16729
|
|
|
16334
|
-
export { CHAT_BUBBLE_STATUS, MONTHS_FULL_EN, MONTHS_FULL_VI, MONTHS_SHORT_VI, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppNav, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, OlEmpty, OlFileInput, OlFloatingLabel, OlFocusTrapService, OlFooter, OlGallery, OlGalleryItem, OlHelper, OlIcon, OlIconService, OlIconShape, OlInput, OlInputCounter, OlJumbotron, OlKbd, OlLabel, OlListGroup, OlListGroupItem, OlLoadingOverlay, OlModal, OlModalFooter, OlModalHeader, OlMonthView, OlNavDropdown, OlNavbar, OlNavbarItem, OlOtpInput, OlOverlayContainer, OlPagination, OlPopover, OlPopoverContent, OlPositioningService, OlProgress, OlProgressBarService, OlProgressBarTop, OlRadio, OlRange, OlRating, OlRichTextEditor, OlScrollArea, OlSelect, OlSeparator, OlSidebar, OlSidebarGroup, OlSidebarItem, OlSidebarSubmenu, OlSkeleton, OlSpeedDial, OlSpeedDialItem, OlSpinner, OlStepItem, OlStepper, OlTabItem, OlTable, OlTableLoading, OlTableRow, OlTabs, OlTagInput, OlTextarea, OlTimeline, OlTimelineItem, OlTimepicker, OlToast, OlToastContainer, OlToastService, OlToggle, OlToggleGroup, OlToggleInput, OlTooltip, OlTooltipDirective, OlTree, OlTreeNodeItem, OlYearView, WEEKDAYS_SHORT_EN, WEEKDAYS_SHORT_VI, boDau, cloneDate, endOfDay, formatDate, generateMonthMatrix, generateMonthsMatrix, generateYearsMatrix, isBetween, isDateDisabled, isSameDay, isSameMonth, isSameYear, isToday, parseDate, startOfDay, stripDiacritics };
|
|
16730
|
+
export { CHAT_BUBBLE_STATUS, FIELD_ASTERISK, FIELD_DISABLED, FIELD_HEIGHT, FIELD_HELPER, FIELD_HELPER_GAP, FIELD_LABEL, FIELD_LABEL_GAP, FIELD_MIN_HEIGHT, FIELD_PADDING_X, FIELD_RADIUS, FIELD_STATE, FIELD_TEXT, MONTHS_FULL_EN, MONTHS_FULL_VI, MONTHS_SHORT_VI, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppNav, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, OlEmpty, OlFileInput, OlFloatingLabel, OlFocusTrapService, OlFooter, OlGallery, OlGalleryItem, OlHelper, OlIcon, OlIconService, OlIconShape, OlInput, OlInputCounter, OlJumbotron, OlKbd, OlLabel, OlListGroup, OlListGroupItem, OlLoadingOverlay, OlModal, OlModalFooter, OlModalHeader, OlMonthView, OlNavDropdown, OlNavbar, OlNavbarItem, OlOtpInput, OlOverlayContainer, OlPagination, OlPopover, OlPopoverContent, OlPositioningService, OlProgress, OlProgressBarService, OlProgressBarTop, OlRadio, OlRange, OlRating, OlRichTextEditor, OlScrollArea, OlSelect, OlSeparator, OlSidebar, OlSidebarGroup, OlSidebarItem, OlSidebarSubmenu, OlSkeleton, OlSpeedDial, OlSpeedDialItem, OlSpinner, OlStepItem, OlStepper, OlTabItem, OlTable, OlTableLoading, OlTableRow, OlTabs, OlTagInput, OlTextarea, OlTimeline, OlTimelineItem, OlTimepicker, OlToast, OlToastContainer, OlToastService, OlToggle, OlToggleGroup, OlToggleInput, OlTooltip, OlTooltipDirective, OlTree, OlTreeNodeItem, OlYearView, WEEKDAYS_SHORT_EN, WEEKDAYS_SHORT_VI, boDau, cloneDate, endOfDay, fieldSizeClasses, formatDate, generateMonthMatrix, generateMonthsMatrix, generateYearsMatrix, isBetween, isDateDisabled, isSameDay, isSameMonth, isSameYear, isToday, parseDate, startOfDay, stripDiacritics };
|
|
16335
16731
|
//# sourceMappingURL=edmicro-olkit.mjs.map
|