@edmicro/olkit 2.1.1 → 2.1.6
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/LICENSE +21 -0
- package/README.md +271 -332
- package/fesm2022/edmicro-olkit.mjs +450 -156
- package/fesm2022/edmicro-olkit.mjs.map +1 -1
- package/olkit.css +2 -29
- package/package.json +1 -1
- package/types/edmicro-olkit.d.ts +11 -6
- package/types/edmicro-olkit.d.ts.map +1 -1
|
@@ -2,7 +2,7 @@ import * as i0 from "@angular/core";
|
|
|
2
2
|
import { ApplicationRef, ChangeDetectionStrategy, Component, ContentChild, DOCUMENT, DestroyRef, Directive, ElementRef, EnvironmentInjector, HostListener, Injectable, Renderer2, Service, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, ViewEncapsulation, afterNextRender, afterRenderEffect, computed, contentChild, contentChildren, createComponent, effect, forwardRef, inject, input, isDevMode, model, output, signal, untracked, viewChild } from "@angular/core";
|
|
3
3
|
import * as i1$1 from "@angular/common";
|
|
4
4
|
import { CommonModule, NgStyle, NgTemplateOutlet } from "@angular/common";
|
|
5
|
-
import { OlIcon, OlIconShape } from "@edmicro/olkit-icons";
|
|
5
|
+
import { OlIcon, OlIconShape, provideOlIcons } from "@edmicro/olkit-icons";
|
|
6
6
|
import { arrow, autoUpdate, computePosition, flip, offset, shift, size } from "@floating-ui/dom";
|
|
7
7
|
import * as i2 from "@angular/router";
|
|
8
8
|
import { NavigationEnd, Router, RouterLink, RouterModule } from "@angular/router";
|
|
@@ -114,8 +114,9 @@ var OlAccordionItem = class OlAccordionItem {
|
|
|
114
114
|
properties: { "class.opacity-60": "disabled()" },
|
|
115
115
|
classAttribute: "block"
|
|
116
116
|
},
|
|
117
|
+
providers: [provideOlIcons({ "chevron-down": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m6 9l6 6l6-6\"/></svg>" })],
|
|
117
118
|
ngImport: i0,
|
|
118
|
-
template: "<div\r\n class=\"ol-accordion-item\"\r\n [class.border]=\"isSeparated()\"\r\n [class.border-default]=\"isSeparated()\"\r\n [class.rounded-md]=\"isSeparated()\"\r\n [class.overflow-hidden]=\"isSeparated()\"\r\n [class.bg-neutral-primary]=\"isSeparated()\"\r\n [class.shadow-2xs]=\"isSeparated()\"\r\n>\r\n <!-- Header / Trigger -->\r\n <button\r\n type=\"button\"\r\n [id]=\"headerId()\"\r\n [class]=\"headerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"contentId()\"\r\n [attr.aria-disabled]=\"disabled()\"\r\n [disabled]=\"disabled()\"\r\n (click)=\"toggle()\"\r\n >\r\n <span class=\"flex-1 text-left\">{{ title() }}</span>\r\n <!-- Chevron Icon -->\r\n <ol-icon name=\"
|
|
119
|
+
template: "<div\r\n class=\"ol-accordion-item\"\r\n [class.border]=\"isSeparated()\"\r\n [class.border-default]=\"isSeparated()\"\r\n [class.rounded-md]=\"isSeparated()\"\r\n [class.overflow-hidden]=\"isSeparated()\"\r\n [class.bg-neutral-primary]=\"isSeparated()\"\r\n [class.shadow-2xs]=\"isSeparated()\"\r\n>\r\n <!-- Header / Trigger -->\r\n <button\r\n type=\"button\"\r\n [id]=\"headerId()\"\r\n [class]=\"headerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"contentId()\"\r\n [attr.aria-disabled]=\"disabled()\"\r\n [disabled]=\"disabled()\"\r\n (click)=\"toggle()\"\r\n >\r\n <span class=\"flex-1 text-left\">{{ title() }}</span>\r\n <!-- Chevron Icon -->\r\n <ol-icon name=\"chevron_down\" [size]=\"18\" [class]=\"chevronClasses()\" />\r\n </button>\r\n <!-- Content -->\r\n <div\r\n [class]=\"contentClasses()\"\r\n [attr.id]=\"contentId()\"\r\n role=\"region\"\r\n [attr.aria-labelledby]=\"headerId()\"\r\n >\r\n <div class=\"accordion-content-inner\">\r\n @if (isOpen()) {\r\n <div [class]=\"bodyClasses()\">\r\n <ng-content />\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</div>\r\n",
|
|
119
120
|
styles: [".accordion-content{display:grid;grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows .3s cubic-bezier(.4,0,.2,1),opacity .3s cubic-bezier(.4,0,.2,1)}.accordion-content.open{grid-template-rows:1fr;opacity:1}.accordion-content-inner{min-height:0}\n"],
|
|
120
121
|
dependencies: [{
|
|
121
122
|
kind: "ngmodule",
|
|
@@ -151,7 +152,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
151
152
|
class: "block",
|
|
152
153
|
"[class.opacity-60]": "disabled()"
|
|
153
154
|
},
|
|
154
|
-
|
|
155
|
+
providers: [provideOlIcons({ "chevron-down": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m6 9l6 6l6-6\"/></svg>" })],
|
|
156
|
+
template: "<div\r\n class=\"ol-accordion-item\"\r\n [class.border]=\"isSeparated()\"\r\n [class.border-default]=\"isSeparated()\"\r\n [class.rounded-md]=\"isSeparated()\"\r\n [class.overflow-hidden]=\"isSeparated()\"\r\n [class.bg-neutral-primary]=\"isSeparated()\"\r\n [class.shadow-2xs]=\"isSeparated()\"\r\n>\r\n <!-- Header / Trigger -->\r\n <button\r\n type=\"button\"\r\n [id]=\"headerId()\"\r\n [class]=\"headerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"contentId()\"\r\n [attr.aria-disabled]=\"disabled()\"\r\n [disabled]=\"disabled()\"\r\n (click)=\"toggle()\"\r\n >\r\n <span class=\"flex-1 text-left\">{{ title() }}</span>\r\n <!-- Chevron Icon -->\r\n <ol-icon name=\"chevron_down\" [size]=\"18\" [class]=\"chevronClasses()\" />\r\n </button>\r\n <!-- Content -->\r\n <div\r\n [class]=\"contentClasses()\"\r\n [attr.id]=\"contentId()\"\r\n role=\"region\"\r\n [attr.aria-labelledby]=\"headerId()\"\r\n >\r\n <div class=\"accordion-content-inner\">\r\n @if (isOpen()) {\r\n <div [class]=\"bodyClasses()\">\r\n <ng-content />\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</div>\r\n",
|
|
155
157
|
styles: [".accordion-content{display:grid;grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows .3s cubic-bezier(.4,0,.2,1),opacity .3s cubic-bezier(.4,0,.2,1)}.accordion-content.open{grid-template-rows:1fr;opacity:1}.accordion-content-inner{min-height:0}\n"]
|
|
156
158
|
}]
|
|
157
159
|
}],
|
|
@@ -643,6 +645,13 @@ var OlAlert = class OlAlert {
|
|
|
643
645
|
}
|
|
644
646
|
},
|
|
645
647
|
outputs: { onDismiss: "onDismiss" },
|
|
648
|
+
providers: [provideOlIcons({
|
|
649
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
650
|
+
"info-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M12 16V12\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <circle cx=\"12\" cy=\"8.5\" r=\"1\" fill=\"currentColor\"/> </svg>",
|
|
651
|
+
"check-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M8.5 12.5L10.5 14.5L15.5 9.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>",
|
|
652
|
+
"danger-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M12 7V13\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <circle cx=\"12\" cy=\"16\" r=\"1\" fill=\"currentColor\"/> </svg>",
|
|
653
|
+
"danger-triangle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M12 2L2 20H22L12 2Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M12 9V13\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <circle cx=\"12\" cy=\"16\" r=\"1\" fill=\"currentColor\"/> </svg>"
|
|
654
|
+
})],
|
|
646
655
|
ngImport: i0,
|
|
647
656
|
template: "@if (visible()) {\r\n <div\r\n [class]=\"alertClasses()\"\r\n role=\"alert\"\r\n [attr.aria-live]=\"type() === 'danger' ? 'assertive' : 'polite'\"\r\n animate.leave=\"alert-fade-out\"\r\n >\r\n <!-- Mỗi ng-content chỉ được khai MỘT lần và phải nằm ngoài khối điều kiện:\r\n Angular chiếu nội dung đúng một lần lúc khởi tạo, ng-content đặt trong\r\n @if/@switch sẽ không nhận được nội dung. Nên bốn bố cục của Figma đổi\r\n bằng class, không đổi cây DOM. -->\r\n\r\n <!-- Hàng đầu: nhãn tròn (small) · icon · tiêu đề · nội dung · nút đóng -->\r\n <div [class]=\"headerRowClasses()\">\r\n <!-- Nhãn tròn: luôn có mặt để nhận nội dung chiếu, ẩn bằng [hidden]\r\n thay vì @if — ng-content trong khối điều kiện sẽ mất nội dung khi\r\n layout đổi sau lúc khởi tạo. -->\r\n <span [class]=\"badgeClasses()\" [hidden]=\"layout() !== 'small' || !badge()\">\r\n <ng-content select=\"[badge]\">{{ badge() }}</ng-content>\r\n </span>\r\n\r\n @if (layout() !== 'small' && showIcon()) {\r\n <ol-icon [name]=\"iconName()\" [class]=\"iconClasses()\" [size]=\"20\" />\r\n }\r\n\r\n <div [class]=\"bodyClasses()\">\r\n @if (title()) {\r\n @if (layout() === 'complex') {\r\n @switch (headingLevel()) {\r\n @case (1) {\r\n <h1 class=\"text-base font-semibold\">{{ title() }}</h1>\r\n }\r\n @case (2) {\r\n <h2 class=\"text-base font-semibold\">{{ title() }}</h2>\r\n }\r\n @case (4) {\r\n <h4 class=\"text-base font-semibold\">{{ title() }}</h4>\r\n }\r\n @case (5) {\r\n <h5 class=\"text-base font-semibold\">{{ title() }}</h5>\r\n }\r\n @case (6) {\r\n <h6 class=\"text-base font-semibold\">{{ title() }}</h6>\r\n }\r\n @default {\r\n <h3 class=\"text-base font-semibold\">{{ title() }}</h3>\r\n }\r\n }\r\n } @else if (layout() !== 'small') {\r\n @switch (headingLevel()) {\r\n @case (1) {\r\n <h1 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h1>\r\n }\r\n @case (2) {\r\n <h2 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h2>\r\n }\r\n @case (3) {\r\n <h3 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h3>\r\n }\r\n @case (5) {\r\n <h5 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h5>\r\n }\r\n @case (6) {\r\n <h6 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h6>\r\n }\r\n @default {\r\n <h4 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h4>\r\n }\r\n }\r\n }\r\n }\r\n\r\n <div [class]=\"contentClasses()\">\r\n <ng-content />\r\n </div>\r\n </div>\r\n\r\n @if (dismissible()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"closeButtonClasses()\"\r\n (click)=\"dismiss()\"\r\n [attr.aria-label]=\"'Đóng thông báo'\"\r\n >\r\n <span class=\"sr-only\">Đóng</span>\r\n <ol-icon name=\"close\" [size]=\"16\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Hàng nút hành động — luôn có mặt để nhận nội dung chiếu -->\r\n <div [class]=\"actionRowClasses()\">\r\n <ng-content select=\"[alertActions]\" />\r\n </div>\r\n </div>\r\n}\r\n",
|
|
648
657
|
dependencies: [{
|
|
@@ -675,6 +684,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
675
684
|
standalone: true,
|
|
676
685
|
imports: [CommonModule, OlIcon],
|
|
677
686
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
687
|
+
providers: [provideOlIcons({
|
|
688
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
689
|
+
"info-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M12 16V12\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <circle cx=\"12\" cy=\"8.5\" r=\"1\" fill=\"currentColor\"/> </svg>",
|
|
690
|
+
"check-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M8.5 12.5L10.5 14.5L15.5 9.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>",
|
|
691
|
+
"danger-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M12 7V13\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <circle cx=\"12\" cy=\"16\" r=\"1\" fill=\"currentColor\"/> </svg>",
|
|
692
|
+
"danger-triangle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M12 2L2 20H22L12 2Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M12 9V13\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <circle cx=\"12\" cy=\"16\" r=\"1\" fill=\"currentColor\"/> </svg>"
|
|
693
|
+
})],
|
|
678
694
|
template: "@if (visible()) {\r\n <div\r\n [class]=\"alertClasses()\"\r\n role=\"alert\"\r\n [attr.aria-live]=\"type() === 'danger' ? 'assertive' : 'polite'\"\r\n animate.leave=\"alert-fade-out\"\r\n >\r\n <!-- Mỗi ng-content chỉ được khai MỘT lần và phải nằm ngoài khối điều kiện:\r\n Angular chiếu nội dung đúng một lần lúc khởi tạo, ng-content đặt trong\r\n @if/@switch sẽ không nhận được nội dung. Nên bốn bố cục của Figma đổi\r\n bằng class, không đổi cây DOM. -->\r\n\r\n <!-- Hàng đầu: nhãn tròn (small) · icon · tiêu đề · nội dung · nút đóng -->\r\n <div [class]=\"headerRowClasses()\">\r\n <!-- Nhãn tròn: luôn có mặt để nhận nội dung chiếu, ẩn bằng [hidden]\r\n thay vì @if — ng-content trong khối điều kiện sẽ mất nội dung khi\r\n layout đổi sau lúc khởi tạo. -->\r\n <span [class]=\"badgeClasses()\" [hidden]=\"layout() !== 'small' || !badge()\">\r\n <ng-content select=\"[badge]\">{{ badge() }}</ng-content>\r\n </span>\r\n\r\n @if (layout() !== 'small' && showIcon()) {\r\n <ol-icon [name]=\"iconName()\" [class]=\"iconClasses()\" [size]=\"20\" />\r\n }\r\n\r\n <div [class]=\"bodyClasses()\">\r\n @if (title()) {\r\n @if (layout() === 'complex') {\r\n @switch (headingLevel()) {\r\n @case (1) {\r\n <h1 class=\"text-base font-semibold\">{{ title() }}</h1>\r\n }\r\n @case (2) {\r\n <h2 class=\"text-base font-semibold\">{{ title() }}</h2>\r\n }\r\n @case (4) {\r\n <h4 class=\"text-base font-semibold\">{{ title() }}</h4>\r\n }\r\n @case (5) {\r\n <h5 class=\"text-base font-semibold\">{{ title() }}</h5>\r\n }\r\n @case (6) {\r\n <h6 class=\"text-base font-semibold\">{{ title() }}</h6>\r\n }\r\n @default {\r\n <h3 class=\"text-base font-semibold\">{{ title() }}</h3>\r\n }\r\n }\r\n } @else if (layout() !== 'small') {\r\n @switch (headingLevel()) {\r\n @case (1) {\r\n <h1 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h1>\r\n }\r\n @case (2) {\r\n <h2 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h2>\r\n }\r\n @case (3) {\r\n <h3 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h3>\r\n }\r\n @case (5) {\r\n <h5 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h5>\r\n }\r\n @case (6) {\r\n <h6 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h6>\r\n }\r\n @default {\r\n <h4 class=\"font-semibold mb-1 text-sm\">{{ title() }}</h4>\r\n }\r\n }\r\n }\r\n }\r\n\r\n <div [class]=\"contentClasses()\">\r\n <ng-content />\r\n </div>\r\n </div>\r\n\r\n @if (dismissible()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"closeButtonClasses()\"\r\n (click)=\"dismiss()\"\r\n [attr.aria-label]=\"'Đóng thông báo'\"\r\n >\r\n <span class=\"sr-only\">Đóng</span>\r\n <ol-icon name=\"close\" [size]=\"16\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Hàng nút hành động — luôn có mặt để nhận nội dung chiếu -->\r\n <div [class]=\"actionRowClasses()\">\r\n <ng-content select=\"[alertActions]\" />\r\n </div>\r\n </div>\r\n}\r\n"
|
|
679
695
|
}]
|
|
680
696
|
}],
|
|
@@ -1037,7 +1053,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1037
1053
|
decorators: [{ type: Service }]
|
|
1038
1054
|
});
|
|
1039
1055
|
var OlOverlayContainer = class OlOverlayContainer {
|
|
1040
|
-
|
|
1056
|
+
constructor() {
|
|
1057
|
+
this.doc = inject(DOCUMENT);
|
|
1058
|
+
}
|
|
1059
|
+
doc;
|
|
1041
1060
|
host = null;
|
|
1042
1061
|
currentZ = 1e3;
|
|
1043
1062
|
getContainer() {
|
|
@@ -1162,16 +1181,17 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1162
1181
|
decorators: [{
|
|
1163
1182
|
type: Injectable,
|
|
1164
1183
|
args: [{ providedIn: "root" }]
|
|
1165
|
-
}]
|
|
1184
|
+
}],
|
|
1185
|
+
ctorParameters: () => []
|
|
1166
1186
|
});
|
|
1167
1187
|
function stripDiacritics(text) {
|
|
1168
1188
|
return text.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/đ/g, "d");
|
|
1169
1189
|
}
|
|
1170
1190
|
const boDau = stripDiacritics;
|
|
1171
1191
|
var OlCommand = class OlCommand {
|
|
1172
|
-
focusTrap
|
|
1173
|
-
overlayContainer
|
|
1174
|
-
destroyRef
|
|
1192
|
+
focusTrap;
|
|
1193
|
+
overlayContainer;
|
|
1194
|
+
destroyRef;
|
|
1175
1195
|
panel = viewChild("panel", ...ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []);
|
|
1176
1196
|
searchInput = viewChild("searchInput", ...ngDevMode ? [{ debugName: "searchInput" }] : /* istanbul ignore next */ []);
|
|
1177
1197
|
isOpen = model(false, ...ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []);
|
|
@@ -1198,6 +1218,9 @@ var OlCommand = class OlCommand {
|
|
|
1198
1218
|
flatItems = computed(() => this.filteredGroups().map((g) => g.items).reduce((a, b) => a.concat(b), []).filter((it) => !it.disabled), ...ngDevMode ? [{ debugName: "flatItems" }] : /* istanbul ignore next */ []);
|
|
1199
1219
|
activeId = computed(() => this.flatItems()[this.activeIndex()]?.id ?? null, ...ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []);
|
|
1200
1220
|
constructor() {
|
|
1221
|
+
this.focusTrap = inject(OlFocusTrapService);
|
|
1222
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
1223
|
+
this.destroyRef = inject(DestroyRef);
|
|
1201
1224
|
effect((onCleanup) => {
|
|
1202
1225
|
if (!this.isOpen()) return;
|
|
1203
1226
|
onCleanup(this.overlayContainer.register(() => this.close()));
|
|
@@ -1334,6 +1357,10 @@ var OlCommand = class OlCommand {
|
|
|
1334
1357
|
closed: "closed"
|
|
1335
1358
|
},
|
|
1336
1359
|
host: { listeners: { "document:keydown": "onGlobalKeydown($event)" } },
|
|
1360
|
+
providers: [provideOlIcons({
|
|
1361
|
+
"lucide:search": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"/><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" d=\"m21 21l-4.35-4.35\"/></svg>",
|
|
1362
|
+
"lucide:check": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20 6L9 17l-5-5\"/></svg>"
|
|
1363
|
+
})],
|
|
1337
1364
|
viewQueries: [{
|
|
1338
1365
|
propertyName: "panel",
|
|
1339
1366
|
first: true,
|
|
@@ -1386,6 +1413,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1386
1413
|
standalone: true,
|
|
1387
1414
|
imports: [OlIcon, OlEmpty],
|
|
1388
1415
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1416
|
+
providers: [provideOlIcons({
|
|
1417
|
+
"lucide:search": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"/><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" d=\"m21 21l-4.35-4.35\"/></svg>",
|
|
1418
|
+
"lucide:check": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M20 6L9 17l-5-5\"/></svg>"
|
|
1419
|
+
})],
|
|
1389
1420
|
template: "@if (isOpen()) {\r\n <div\r\n (click)=\"onBackdropClick($event)\"\r\n class=\"fixed inset-0 z-modal flex items-start justify-center overflow-y-auto bg-overlay-backdrop p-4 pt-[12vh] backdrop-blur-xs\"\r\n >\r\n <div\r\n #panel\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n aria-label=\"Bảng lệnh\"\r\n tabindex=\"-1\"\r\n class=\"w-full max-w-xl overflow-hidden rounded-lg border border-default bg-neutral-primary shadow-2xl\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n <div class=\"flex items-center gap-2.5 border-b border-default-subtle px-4 py-3\">\r\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\r\n\r\n <input\r\n #searchInput\r\n type=\"text\"\r\n role=\"combobox\"\r\n aria-expanded=\"true\"\r\n aria-controls=\"ol-command-list\"\r\n [attr.aria-activedescendant]=\"activeId() ? 'ol-command-' + activeId() : null\"\r\n autocomplete=\"off\"\r\n [placeholder]=\"placeholder()\"\r\n [value]=\"query()\"\r\n (input)=\"onQueryInput($event)\"\r\n class=\"w-full bg-transparent text-sm text-heading placeholder:text-body-subtle focus:outline-hidden\"\r\n />\r\n\r\n <kbd\r\n class=\"shrink-0 rounded-xs border border-default bg-neutral-secondary px-1.5 py-0.5 text-xs text-body-subtle\"\r\n >\r\n Esc\r\n </kbd>\r\n </div>\r\n\r\n <div\r\n id=\"ol-command-list\"\r\n role=\"listbox\"\r\n class=\"max-h-80 overflow-y-auto overflow-x-hidden p-2\"\r\n >\r\n @for (group of filteredGroups(); track group.label) {\r\n <p class=\"px-2 pb-1 pt-2 text-xs font-medium text-body-subtle\">{{ group.label }}</p>\r\n\r\n @for (item of group.items; track item.id) {\r\n <button\r\n type=\"button\"\r\n role=\"option\"\r\n [id]=\"'ol-command-' + item.id\"\r\n [attr.aria-selected]=\"activeId() === item.id\"\r\n [disabled]=\"item.disabled\"\r\n (click)=\"onSelect(item)\"\r\n (mouseenter)=\"onItemHover(item)\"\r\n [class]=\"\r\n 'flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-start text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50 ' +\r\n (activeId() === item.id ? 'bg-neutral-tertiary text-heading' : 'text-body')\r\n \"\r\n >\r\n @if (item.icon) {\r\n <ol-icon [name]=\"item.icon\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\r\n }\r\n\r\n <span class=\"flex-1 truncate\">{{ item.label }}</span>\r\n\r\n @if (item.hint) {\r\n <span class=\"shrink-0 text-xs text-body-subtle\">{{ item.hint }}</span>\r\n }\r\n </button>\r\n }\r\n }\r\n\r\n @if (flatItems().length === 0) {\r\n <ol-empty size=\"sm\" [title]=\"emptyText()\" />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n"
|
|
1390
1421
|
}]
|
|
1391
1422
|
}],
|
|
@@ -1590,8 +1621,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1590
1621
|
}
|
|
1591
1622
|
});
|
|
1592
1623
|
var OlScrollspy = class OlScrollspy {
|
|
1593
|
-
elementRef
|
|
1594
|
-
destroyRef
|
|
1624
|
+
elementRef;
|
|
1625
|
+
destroyRef;
|
|
1595
1626
|
items = input([], ...ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []);
|
|
1596
1627
|
orientation = input("vertical", ...ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []);
|
|
1597
1628
|
ariaLabel = input("Mục lục trang", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
|
|
@@ -1603,6 +1634,8 @@ var OlScrollspy = class OlScrollspy {
|
|
|
1603
1634
|
activeChange = output();
|
|
1604
1635
|
observer = null;
|
|
1605
1636
|
constructor() {
|
|
1637
|
+
this.elementRef = inject(ElementRef);
|
|
1638
|
+
this.destroyRef = inject(DestroyRef);
|
|
1606
1639
|
effect(() => {
|
|
1607
1640
|
const list = this.items();
|
|
1608
1641
|
const margin = this.rootMargin();
|
|
@@ -2090,11 +2123,11 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
2090
2123
|
decorators: [{ type: Service }]
|
|
2091
2124
|
});
|
|
2092
2125
|
var OlTooltipDirective = class OlTooltipDirective {
|
|
2093
|
-
elementRef
|
|
2094
|
-
renderer
|
|
2095
|
-
appRef
|
|
2096
|
-
injector
|
|
2097
|
-
positioningService
|
|
2126
|
+
elementRef;
|
|
2127
|
+
renderer;
|
|
2128
|
+
appRef;
|
|
2129
|
+
injector;
|
|
2130
|
+
positioningService;
|
|
2098
2131
|
olTooltip = input.required(...ngDevMode ? [{ debugName: "olTooltip" }] : /* istanbul ignore next */ []);
|
|
2099
2132
|
tooltipPosition = input("top", ...ngDevMode ? [{ debugName: "tooltipPosition" }] : /* istanbul ignore next */ []);
|
|
2100
2133
|
tooltipTheme = input("subtle", ...ngDevMode ? [{ debugName: "tooltipTheme" }] : /* istanbul ignore next */ []);
|
|
@@ -2118,6 +2151,11 @@ var OlTooltipDirective = class OlTooltipDirective {
|
|
|
2118
2151
|
touchEndListener;
|
|
2119
2152
|
touchCancelListeners = [];
|
|
2120
2153
|
constructor() {
|
|
2154
|
+
this.elementRef = inject(ElementRef);
|
|
2155
|
+
this.renderer = inject(Renderer2);
|
|
2156
|
+
this.appRef = inject(ApplicationRef);
|
|
2157
|
+
this.injector = inject(EnvironmentInjector);
|
|
2158
|
+
this.positioningService = inject(OlPositioningService);
|
|
2121
2159
|
this.setupListeners();
|
|
2122
2160
|
effect(() => {
|
|
2123
2161
|
if (this.tooltipDisabled()) this.hide(true);
|
|
@@ -2869,6 +2907,7 @@ var OlModalHeader = class OlModalHeader {
|
|
|
2869
2907
|
},
|
|
2870
2908
|
outputs: { closeClick: "closeClick" },
|
|
2871
2909
|
host: { classAttribute: "block" },
|
|
2910
|
+
providers: [provideOlIcons({ "lucide:x": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>" })],
|
|
2872
2911
|
ngImport: i0,
|
|
2873
2912
|
template: `
|
|
2874
2913
|
<!--
|
|
@@ -2917,7 +2956,7 @@ var OlModalHeader = class OlModalHeader {
|
|
|
2917
2956
|
aria-label="Đóng hộp thoại"
|
|
2918
2957
|
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"
|
|
2919
2958
|
>
|
|
2920
|
-
<ol-icon name="
|
|
2959
|
+
<ol-icon name="lucide:x" size="sm" />
|
|
2921
2960
|
</button>
|
|
2922
2961
|
}
|
|
2923
2962
|
</div>
|
|
@@ -3006,13 +3045,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
3006
3045
|
aria-label="Đóng hộp thoại"
|
|
3007
3046
|
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"
|
|
3008
3047
|
>
|
|
3009
|
-
<ol-icon name="
|
|
3048
|
+
<ol-icon name="lucide:x" size="sm" />
|
|
3010
3049
|
</button>
|
|
3011
3050
|
}
|
|
3012
3051
|
</div>
|
|
3013
3052
|
`,
|
|
3014
3053
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3015
|
-
host: { class: "block" }
|
|
3054
|
+
host: { class: "block" },
|
|
3055
|
+
providers: [provideOlIcons({ "lucide:x": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>" })]
|
|
3016
3056
|
}]
|
|
3017
3057
|
}],
|
|
3018
3058
|
propDecorators: {
|
|
@@ -3079,9 +3119,9 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
3079
3119
|
}
|
|
3080
3120
|
});
|
|
3081
3121
|
var OlModal = class OlModal {
|
|
3082
|
-
focusTrap
|
|
3083
|
-
destroyRef
|
|
3084
|
-
overlayContainer
|
|
3122
|
+
focusTrap;
|
|
3123
|
+
destroyRef;
|
|
3124
|
+
overlayContainer;
|
|
3085
3125
|
releaseEscape = null;
|
|
3086
3126
|
panel = viewChild("panel", ...ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []);
|
|
3087
3127
|
headingLevel = input(3, ...ngDevMode ? [{ debugName: "headingLevel" }] : /* istanbul ignore next */ []);
|
|
@@ -3104,6 +3144,9 @@ var OlModal = class OlModal {
|
|
|
3104
3144
|
opened = output();
|
|
3105
3145
|
closed = output();
|
|
3106
3146
|
constructor() {
|
|
3147
|
+
this.focusTrap = inject(OlFocusTrapService);
|
|
3148
|
+
this.destroyRef = inject(DestroyRef);
|
|
3149
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
3107
3150
|
effect(() => {
|
|
3108
3151
|
const el = this.panel()?.nativeElement;
|
|
3109
3152
|
if (this.isOpen() && el) {
|
|
@@ -3426,9 +3469,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
3426
3469
|
}
|
|
3427
3470
|
});
|
|
3428
3471
|
var OlAlertDialog = class OlAlertDialog {
|
|
3429
|
-
overlayContainer
|
|
3472
|
+
overlayContainer;
|
|
3430
3473
|
releaseEscape = null;
|
|
3431
3474
|
constructor() {
|
|
3475
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
3432
3476
|
effect(() => {
|
|
3433
3477
|
if (this.isOpen()) this.releaseEscape ??= this.overlayContainer.register(() => this.onCancel());
|
|
3434
3478
|
else {
|
|
@@ -4088,6 +4132,10 @@ var OlAvatar = class OlAvatar {
|
|
|
4088
4132
|
}
|
|
4089
4133
|
},
|
|
4090
4134
|
outputs: { remove: "remove" },
|
|
4135
|
+
providers: [provideOlIcons({
|
|
4136
|
+
"user": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"8\" r=\"4\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M4 20C4 16.134 7.582 13 12 13C16.418 13 20 16.134 20 20\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> </svg>",
|
|
4137
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>"
|
|
4138
|
+
})],
|
|
4091
4139
|
ngImport: i0,
|
|
4092
4140
|
template: "<div\r\n class=\"inline-flex items-center\"\r\n [class.gap-2]=\"size() === 'xs' || size() === 'sm' || size() === 'base'\"\r\n [ngClass]=\"{\r\n 'gap-1.5': size() === '2xs',\r\n 'gap-2.5': size() === 'lg' || size() === 'xl' || size() === '2xl',\r\n }\"\r\n>\r\n <!-- Avatar Icon/Image Node -->\r\n <div class=\"relative inline-block shrink-0 leading-none select-none\">\r\n @if (src()) {\r\n <img\r\n [src]=\"src()\"\r\n [alt]=\"alt()\"\r\n class=\"object-cover\"\r\n [class.rounded-full]=\"shape() === 'circle'\"\r\n [class.rounded-lg]=\"\r\n shape() === 'rounded' && (size() === 'lg' || size() === 'xl' || size() === '2xl')\r\n \"\r\n [class.rounded-md]=\"\r\n shape() === 'rounded' &&\r\n (size() === '2xs' || size() === 'xs' || size() === 'sm' || size() === 'base')\r\n \"\r\n [class.w-6]=\"size() === 'sm'\"\r\n [class.h-6]=\"size() === 'sm'\"\r\n [class.w-8]=\"size() === 'base'\"\r\n [class.h-8]=\"size() === 'base'\"\r\n [class.w-11]=\"size() === 'lg'\"\r\n [class.h-11]=\"size() === 'lg'\"\r\n [class.w-14]=\"size() === 'xl'\"\r\n [class.h-14]=\"size() === 'xl'\"\r\n [class.w-16]=\"size() === '2xl'\"\r\n [class.h-16]=\"size() === '2xl'\"\r\n [class.ring-2]=\"bordered()\"\r\n [class.ring-white]=\"bordered()\"\r\n [class.ring-buffer]=\"bordered()\"\r\n [ngClass]=\"{\r\n 'w-4.5': size() === 'xs',\r\n 'h-4.5': size() === 'xs',\r\n 'w-3.5': size() === '2xs',\r\n 'h-3.5': size() === '2xs',\r\n }\"\r\n />\r\n } @else if (initials() || text()) {\r\n <div\r\n class=\"relative inline-flex items-center justify-center overflow-hidden bg-neutral-tertiary text-body font-semibold uppercase\"\r\n [class.rounded-full]=\"shape() === 'circle'\"\r\n [class.rounded-lg]=\"\r\n shape() === 'rounded' && (size() === 'lg' || size() === 'xl' || size() === '2xl')\r\n \"\r\n [class.rounded-md]=\"\r\n shape() === 'rounded' &&\r\n (size() === '2xs' || size() === 'xs' || size() === 'sm' || size() === 'base')\r\n \"\r\n [class.text-[8px]]=\"size() === '2xs'\"\r\n [class.text-[9px]]=\"size() === 'xs'\"\r\n [class.w-6]=\"size() === 'sm'\"\r\n [class.h-6]=\"size() === 'sm'\"\r\n [class.text-xs]=\"size() === 'sm'\"\r\n [class.w-8]=\"size() === 'base'\"\r\n [class.h-8]=\"size() === 'base'\"\r\n [class.text-sm]=\"size() === 'base'\"\r\n [class.w-11]=\"size() === 'lg'\"\r\n [class.h-11]=\"size() === 'lg'\"\r\n [class.text-lg]=\"size() === 'lg'\"\r\n [class.w-14]=\"size() === 'xl'\"\r\n [class.h-14]=\"size() === 'xl'\"\r\n [class.w-16]=\"size() === '2xl'\"\r\n [class.h-16]=\"size() === '2xl'\"\r\n [class.text-2xl]=\"size() === 'xl' || size() === '2xl'\"\r\n [class.ring-2]=\"bordered()\"\r\n [class.ring-white]=\"bordered()\"\r\n [class.ring-buffer]=\"bordered()\"\r\n [ngClass]=\"{\r\n 'w-4.5': size() === 'xs',\r\n 'h-4.5': size() === 'xs',\r\n 'w-3.5': size() === '2xs',\r\n 'h-3.5': size() === '2xs',\r\n }\"\r\n >\r\n {{ initials() || text() }}\r\n </div>\r\n } @else {\r\n <div\r\n class=\"relative inline-flex items-center justify-center overflow-hidden bg-neutral-tertiary text-body font-semibold\"\r\n [class.rounded-full]=\"shape() === 'circle'\"\r\n [class.rounded-lg]=\"\r\n shape() === 'rounded' && (size() === 'lg' || size() === 'xl' || size() === '2xl')\r\n \"\r\n [class.rounded-md]=\"\r\n shape() === 'rounded' &&\r\n (size() === '2xs' || size() === 'xs' || size() === 'sm' || size() === 'base')\r\n \"\r\n [class.w-6]=\"size() === 'sm'\"\r\n [class.h-6]=\"size() === 'sm'\"\r\n [class.w-8]=\"size() === 'base'\"\r\n [class.h-8]=\"size() === 'base'\"\r\n [class.w-11]=\"size() === 'lg'\"\r\n [class.h-11]=\"size() === 'lg'\"\r\n [class.w-14]=\"size() === 'xl'\"\r\n [class.h-14]=\"size() === 'xl'\"\r\n [class.w-16]=\"size() === '2xl'\"\r\n [class.h-16]=\"size() === '2xl'\"\r\n [class.ring-2]=\"bordered()\"\r\n [class.ring-white]=\"bordered()\"\r\n [class.ring-buffer]=\"bordered()\"\r\n [ngClass]=\"{\r\n 'w-4.5': size() === 'xs',\r\n 'h-4.5': size() === 'xs',\r\n 'w-3.5': size() === '2xs',\r\n 'h-3.5': size() === '2xs',\r\n }\"\r\n >\r\n <ng-content>\r\n <!-- Default Figma User Placeholder Icon -->\r\n <ol-icon name=\"user\" [size]=\"iconPx()\" />\r\n </ng-content>\r\n </div>\r\n }\r\n\r\n <!-- Status Dot\r\n Figma `ol-avatar-dot` (#1146:5625) có trục Status=[Online|Offline|\r\n Alternative]: Online #007A55 (bg-success-surface), Offline #EC003F\r\n (bg-danger-surface). `busy` và `away` KHÔNG có trong Figma nên giữ\r\n bậc số cho tới khi thiết kế định nghĩa, cùng cách đã áp cho\r\n ol-speed-dial và ol-spinner. -->\r\n @if (status()) {\r\n <!-- offline và busy dùng CHUNG bg-danger-surface. Tách thành hai\r\n [class.bg-danger-surface] là hỏng: Angular gộp binding trùng tên,\r\n cái cuối thắng và cái trước mất im lặng. -->\r\n <!-- `right-0` là class TĨNH: cả hai giá trị statusPosition đều neo mép\r\n phải, chỉ khác trục dọc. Trước đây `right-0` bị buộc vào riêng nhánh\r\n `bottom-right` nên `top-right` thiếu trục ngang và chấm rơi về góc\r\n trên TRÁI (đo DOM 2026-09-16). -->\r\n <span\r\n class=\"absolute right-0 rounded-full ring-2 ring-buffer shrink-0\"\r\n [class.bottom-0]=\"statusPosition() === 'bottom-right'\"\r\n [class.top-0]=\"statusPosition() === 'top-right'\"\r\n [class.w-2]=\"size() === 'base'\"\r\n [class.h-2]=\"size() === 'base'\"\r\n [class.w-3]=\"size() === 'lg'\"\r\n [class.h-3]=\"size() === 'lg'\"\r\n [class.w-4]=\"size() === 'xl' || size() === '2xl'\"\r\n [class.h-4]=\"size() === 'xl' || size() === '2xl'\"\r\n [class.bg-success-surface]=\"status() === 'online'\"\r\n [class.bg-danger-surface]=\"status() === 'offline' || status() === 'busy'\"\r\n [class.bg-gray-surface]=\"status() === 'alternative'\"\r\n [class.bg-warning-surface]=\"status() === 'away'\"\r\n [ngClass]=\"{\r\n 'w-1.5': size() === 'xs',\r\n 'h-1.5': size() === 'xs',\r\n 'w-2.5': size() === 'sm',\r\n 'h-2.5': size() === 'sm',\r\n }\"\r\n ></span>\r\n }\r\n\r\n <!-- Remove Button -->\r\n @if (removable()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"onRemoveClick($event)\"\r\n class=\"absolute -top-1 -right-1 z-10 inline-flex items-center justify-center rounded-full bg-neutral-tertiary-medium hover:bg-neutral-quaternary text-body shadow-sm border border-buffer cursor-pointer transition-transform active:scale-95\"\r\n [class.w-3]=\"size() === 'xs'\"\r\n [class.h-3]=\"size() === 'xs'\"\r\n [class.w-4]=\"size() === 'base'\"\r\n [class.h-4]=\"size() === 'base'\"\r\n [class.w-5]=\"size() === 'lg' || size() === 'xl' || size() === '2xl'\"\r\n [class.h-5]=\"size() === 'lg' || size() === 'xl' || size() === '2xl'\"\r\n aria-label=\"Gỡ ảnh đại diện\"\r\n [ngClass]=\"{ 'w-3.5': size() === 'sm', 'h-3.5': size() === 'sm' }\"\r\n >\r\n <ol-icon name=\"close\" size=\"sm\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Label Details (Name & Helper Text) -->\r\n @if (name()) {\r\n <div class=\"flex flex-col min-w-0\">\r\n <span\r\n class=\"font-medium text-heading truncate\"\r\n [class.text-xs]=\"size() === 'xs' || size() === 'sm'\"\r\n [class.text-base]=\"size() === 'base' || size() === 'lg'\"\r\n [class.text-xl]=\"size() === 'xl' || size() === '2xl'\"\r\n >\r\n {{ name() }}\r\n </span>\r\n @if (helper() && showHelper()) {\r\n <span\r\n class=\"text-body truncate\"\r\n [class.text-2xs]=\"size() === 'xs'\"\r\n [class.text-xs]=\"size() === 'sm'\"\r\n [class.text-sm]=\"size() === 'base'\"\r\n [class.text-base]=\"size() === 'lg'\"\r\n [class.text-lg]=\"size() === 'xl' || size() === '2xl'\"\r\n >\r\n {{ helper() }}\r\n </span>\r\n }\r\n </div>\r\n }\r\n</div>\r\n",
|
|
4093
4141
|
dependencies: [
|
|
@@ -4129,6 +4177,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
4129
4177
|
standalone: true,
|
|
4130
4178
|
imports: [CommonModule, OlIcon],
|
|
4131
4179
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4180
|
+
providers: [provideOlIcons({
|
|
4181
|
+
"user": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"8\" r=\"4\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M4 20C4 16.134 7.582 13 12 13C16.418 13 20 16.134 20 20\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> </svg>",
|
|
4182
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>"
|
|
4183
|
+
})],
|
|
4132
4184
|
template: "<div\r\n class=\"inline-flex items-center\"\r\n [class.gap-2]=\"size() === 'xs' || size() === 'sm' || size() === 'base'\"\r\n [ngClass]=\"{\r\n 'gap-1.5': size() === '2xs',\r\n 'gap-2.5': size() === 'lg' || size() === 'xl' || size() === '2xl',\r\n }\"\r\n>\r\n <!-- Avatar Icon/Image Node -->\r\n <div class=\"relative inline-block shrink-0 leading-none select-none\">\r\n @if (src()) {\r\n <img\r\n [src]=\"src()\"\r\n [alt]=\"alt()\"\r\n class=\"object-cover\"\r\n [class.rounded-full]=\"shape() === 'circle'\"\r\n [class.rounded-lg]=\"\r\n shape() === 'rounded' && (size() === 'lg' || size() === 'xl' || size() === '2xl')\r\n \"\r\n [class.rounded-md]=\"\r\n shape() === 'rounded' &&\r\n (size() === '2xs' || size() === 'xs' || size() === 'sm' || size() === 'base')\r\n \"\r\n [class.w-6]=\"size() === 'sm'\"\r\n [class.h-6]=\"size() === 'sm'\"\r\n [class.w-8]=\"size() === 'base'\"\r\n [class.h-8]=\"size() === 'base'\"\r\n [class.w-11]=\"size() === 'lg'\"\r\n [class.h-11]=\"size() === 'lg'\"\r\n [class.w-14]=\"size() === 'xl'\"\r\n [class.h-14]=\"size() === 'xl'\"\r\n [class.w-16]=\"size() === '2xl'\"\r\n [class.h-16]=\"size() === '2xl'\"\r\n [class.ring-2]=\"bordered()\"\r\n [class.ring-white]=\"bordered()\"\r\n [class.ring-buffer]=\"bordered()\"\r\n [ngClass]=\"{\r\n 'w-4.5': size() === 'xs',\r\n 'h-4.5': size() === 'xs',\r\n 'w-3.5': size() === '2xs',\r\n 'h-3.5': size() === '2xs',\r\n }\"\r\n />\r\n } @else if (initials() || text()) {\r\n <div\r\n class=\"relative inline-flex items-center justify-center overflow-hidden bg-neutral-tertiary text-body font-semibold uppercase\"\r\n [class.rounded-full]=\"shape() === 'circle'\"\r\n [class.rounded-lg]=\"\r\n shape() === 'rounded' && (size() === 'lg' || size() === 'xl' || size() === '2xl')\r\n \"\r\n [class.rounded-md]=\"\r\n shape() === 'rounded' &&\r\n (size() === '2xs' || size() === 'xs' || size() === 'sm' || size() === 'base')\r\n \"\r\n [class.text-[8px]]=\"size() === '2xs'\"\r\n [class.text-[9px]]=\"size() === 'xs'\"\r\n [class.w-6]=\"size() === 'sm'\"\r\n [class.h-6]=\"size() === 'sm'\"\r\n [class.text-xs]=\"size() === 'sm'\"\r\n [class.w-8]=\"size() === 'base'\"\r\n [class.h-8]=\"size() === 'base'\"\r\n [class.text-sm]=\"size() === 'base'\"\r\n [class.w-11]=\"size() === 'lg'\"\r\n [class.h-11]=\"size() === 'lg'\"\r\n [class.text-lg]=\"size() === 'lg'\"\r\n [class.w-14]=\"size() === 'xl'\"\r\n [class.h-14]=\"size() === 'xl'\"\r\n [class.w-16]=\"size() === '2xl'\"\r\n [class.h-16]=\"size() === '2xl'\"\r\n [class.text-2xl]=\"size() === 'xl' || size() === '2xl'\"\r\n [class.ring-2]=\"bordered()\"\r\n [class.ring-white]=\"bordered()\"\r\n [class.ring-buffer]=\"bordered()\"\r\n [ngClass]=\"{\r\n 'w-4.5': size() === 'xs',\r\n 'h-4.5': size() === 'xs',\r\n 'w-3.5': size() === '2xs',\r\n 'h-3.5': size() === '2xs',\r\n }\"\r\n >\r\n {{ initials() || text() }}\r\n </div>\r\n } @else {\r\n <div\r\n class=\"relative inline-flex items-center justify-center overflow-hidden bg-neutral-tertiary text-body font-semibold\"\r\n [class.rounded-full]=\"shape() === 'circle'\"\r\n [class.rounded-lg]=\"\r\n shape() === 'rounded' && (size() === 'lg' || size() === 'xl' || size() === '2xl')\r\n \"\r\n [class.rounded-md]=\"\r\n shape() === 'rounded' &&\r\n (size() === '2xs' || size() === 'xs' || size() === 'sm' || size() === 'base')\r\n \"\r\n [class.w-6]=\"size() === 'sm'\"\r\n [class.h-6]=\"size() === 'sm'\"\r\n [class.w-8]=\"size() === 'base'\"\r\n [class.h-8]=\"size() === 'base'\"\r\n [class.w-11]=\"size() === 'lg'\"\r\n [class.h-11]=\"size() === 'lg'\"\r\n [class.w-14]=\"size() === 'xl'\"\r\n [class.h-14]=\"size() === 'xl'\"\r\n [class.w-16]=\"size() === '2xl'\"\r\n [class.h-16]=\"size() === '2xl'\"\r\n [class.ring-2]=\"bordered()\"\r\n [class.ring-white]=\"bordered()\"\r\n [class.ring-buffer]=\"bordered()\"\r\n [ngClass]=\"{\r\n 'w-4.5': size() === 'xs',\r\n 'h-4.5': size() === 'xs',\r\n 'w-3.5': size() === '2xs',\r\n 'h-3.5': size() === '2xs',\r\n }\"\r\n >\r\n <ng-content>\r\n <!-- Default Figma User Placeholder Icon -->\r\n <ol-icon name=\"user\" [size]=\"iconPx()\" />\r\n </ng-content>\r\n </div>\r\n }\r\n\r\n <!-- Status Dot\r\n Figma `ol-avatar-dot` (#1146:5625) có trục Status=[Online|Offline|\r\n Alternative]: Online #007A55 (bg-success-surface), Offline #EC003F\r\n (bg-danger-surface). `busy` và `away` KHÔNG có trong Figma nên giữ\r\n bậc số cho tới khi thiết kế định nghĩa, cùng cách đã áp cho\r\n ol-speed-dial và ol-spinner. -->\r\n @if (status()) {\r\n <!-- offline và busy dùng CHUNG bg-danger-surface. Tách thành hai\r\n [class.bg-danger-surface] là hỏng: Angular gộp binding trùng tên,\r\n cái cuối thắng và cái trước mất im lặng. -->\r\n <!-- `right-0` là class TĨNH: cả hai giá trị statusPosition đều neo mép\r\n phải, chỉ khác trục dọc. Trước đây `right-0` bị buộc vào riêng nhánh\r\n `bottom-right` nên `top-right` thiếu trục ngang và chấm rơi về góc\r\n trên TRÁI (đo DOM 2026-09-16). -->\r\n <span\r\n class=\"absolute right-0 rounded-full ring-2 ring-buffer shrink-0\"\r\n [class.bottom-0]=\"statusPosition() === 'bottom-right'\"\r\n [class.top-0]=\"statusPosition() === 'top-right'\"\r\n [class.w-2]=\"size() === 'base'\"\r\n [class.h-2]=\"size() === 'base'\"\r\n [class.w-3]=\"size() === 'lg'\"\r\n [class.h-3]=\"size() === 'lg'\"\r\n [class.w-4]=\"size() === 'xl' || size() === '2xl'\"\r\n [class.h-4]=\"size() === 'xl' || size() === '2xl'\"\r\n [class.bg-success-surface]=\"status() === 'online'\"\r\n [class.bg-danger-surface]=\"status() === 'offline' || status() === 'busy'\"\r\n [class.bg-gray-surface]=\"status() === 'alternative'\"\r\n [class.bg-warning-surface]=\"status() === 'away'\"\r\n [ngClass]=\"{\r\n 'w-1.5': size() === 'xs',\r\n 'h-1.5': size() === 'xs',\r\n 'w-2.5': size() === 'sm',\r\n 'h-2.5': size() === 'sm',\r\n }\"\r\n ></span>\r\n }\r\n\r\n <!-- Remove Button -->\r\n @if (removable()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"onRemoveClick($event)\"\r\n class=\"absolute -top-1 -right-1 z-10 inline-flex items-center justify-center rounded-full bg-neutral-tertiary-medium hover:bg-neutral-quaternary text-body shadow-sm border border-buffer cursor-pointer transition-transform active:scale-95\"\r\n [class.w-3]=\"size() === 'xs'\"\r\n [class.h-3]=\"size() === 'xs'\"\r\n [class.w-4]=\"size() === 'base'\"\r\n [class.h-4]=\"size() === 'base'\"\r\n [class.w-5]=\"size() === 'lg' || size() === 'xl' || size() === '2xl'\"\r\n [class.h-5]=\"size() === 'lg' || size() === 'xl' || size() === '2xl'\"\r\n aria-label=\"Gỡ ảnh đại diện\"\r\n [ngClass]=\"{ 'w-3.5': size() === 'sm', 'h-3.5': size() === 'sm' }\"\r\n >\r\n <ol-icon name=\"close\" size=\"sm\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Label Details (Name & Helper Text) -->\r\n @if (name()) {\r\n <div class=\"flex flex-col min-w-0\">\r\n <span\r\n class=\"font-medium text-heading truncate\"\r\n [class.text-xs]=\"size() === 'xs' || size() === 'sm'\"\r\n [class.text-base]=\"size() === 'base' || size() === 'lg'\"\r\n [class.text-xl]=\"size() === 'xl' || size() === '2xl'\"\r\n >\r\n {{ name() }}\r\n </span>\r\n @if (helper() && showHelper()) {\r\n <span\r\n class=\"text-body truncate\"\r\n [class.text-2xs]=\"size() === 'xs'\"\r\n [class.text-xs]=\"size() === 'sm'\"\r\n [class.text-sm]=\"size() === 'base'\"\r\n [class.text-base]=\"size() === 'lg'\"\r\n [class.text-lg]=\"size() === 'xl' || size() === '2xl'\"\r\n >\r\n {{ helper() }}\r\n </span>\r\n }\r\n </div>\r\n }\r\n</div>\r\n"
|
|
4133
4185
|
}]
|
|
4134
4186
|
}],
|
|
@@ -4912,6 +4964,7 @@ var OlBanner = class OlBanner {
|
|
|
4912
4964
|
}
|
|
4913
4965
|
},
|
|
4914
4966
|
outputs: { onDismiss: "onDismiss" },
|
|
4967
|
+
providers: [provideOlIcons({ "close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>" })],
|
|
4915
4968
|
ngImport: i0,
|
|
4916
4969
|
template: "@if (visible()) {\r\n <div\r\n [class]=\"bannerClasses()\"\r\n role=\"banner\"\r\n aria-live=\"polite\"\r\n aria-label=\"Thông báo hệ thống\"\r\n animate.enter=\"slide-down-enter\"\r\n animate.leave=\"slide-down-leave\"\r\n >\r\n <div class=\"container mx-auto px-4 py-3\">\r\n <div\r\n class=\"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4\"\r\n >\r\n <!-- Left: Icon + Badge + Content -->\r\n <div class=\"flex items-center gap-3 flex-1 min-w-0 w-full sm:w-auto\">\r\n <!-- Icon -->\r\n @if (icon()) {\r\n <ol-icon [name]=\"icon()\" [size]=\"20\" class=\"flex-shrink-0\" />\r\n }\r\n\r\n <!-- Badge -->\r\n @if (showBadge()) {\r\n <span [class]=\"badgeClasses()\">\r\n {{ badgeText() }}\r\n </span>\r\n }\r\n\r\n <!-- Content (ng-content) -->\r\n <div class=\"flex-1 min-w-0 text-sm sm:text-base\">\r\n <ng-content />\r\n </div>\r\n </div>\r\n\r\n <!-- Right: CTA + Dismiss -->\r\n <div class=\"flex items-center gap-3 flex-shrink-0 w-full sm:w-auto justify-end\">\r\n <!-- CTA Slot -->\r\n <ng-content select=\"[bannerCta]\" />\r\n\r\n <!-- Dismiss Button -->\r\n @if (dismissible()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"\r\n 'inline-flex items-center justify-center w-8 h-8 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-subtle ' +\r\n dismissButtonClasses()\r\n \"\r\n (click)=\"dismiss()\"\r\n aria-label=\"Đóng banner\"\r\n >\r\n <ol-icon name=\"close\" [size]=\"16\" />\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n",
|
|
4917
4970
|
styles: [".fade-in{animation:fadeIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.fade-in-fast{animation:fadeIn var(--anim-duration-fast, .15s) var(--anim-ease-out, ease-out) forwards}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fade-out{opacity:0;transition:opacity var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.slide-down-enter{animation:slideDown var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1)) forwards}.slide-down-leave{opacity:0;transform:translateY(-100%);transition:all var(--anim-duration-normal, .3s) cubic-bezier(.4,0,.6,1)}@keyframes slideDown{0%{transform:translateY(-100%);opacity:0}to{transform:translateY(0);opacity:1}}.toast-slide-enter{animation:toastSlideIn var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1)) forwards}.toast-slide-leave{opacity:0;transform:translate(100%);transition:all .2s cubic-bezier(.4,0,.6,1)}@keyframes toastSlideIn{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}.slide-in-up{animation:slideInUp var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.slide-in-down{animation:slideInDown var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInDown{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}.slide-in-left{animation:slideInLeft var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInLeft{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.slide-in-right{animation:slideInRight var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInRight{0%{opacity:0;transform:translate(-20px)}to{opacity:1;transform:translate(0)}}.scale-in{animation:scaleIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes scaleIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.scale-out{opacity:0;transform:scale(.95);transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.menu-scale-enter{animation:menuScaleIn var(--anim-duration-fast, .15s) var(--anim-ease-out, cubic-bezier(0, 0, .2, 1)) forwards}.menu-scale-leave{opacity:0;transform:scale(.85);transition:all .1s cubic-bezier(.4,0,1,1)}@keyframes menuScaleIn{0%{opacity:0;transform:scale(.85)}to{opacity:1;transform:scale(1)}}.fade-scale-enter{animation:fadeScaleIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.fade-scale-leave{opacity:0;transform:scale(.95);transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}@keyframes fadeScaleIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.alert-fade-out{opacity:0;transform:translateY(-8px);transition:all var(--anim-duration-normal, .3s) ease-out}.tooltip-fade-enter{animation:fadeIn var(--anim-duration-fast, .15s) var(--anim-ease-out, ease-out) forwards}.backdrop-enter{animation:fadeIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.backdrop-leave{opacity:0;transition:opacity var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.expand-enter{animation:expandIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.collapse-leave{overflow:hidden;height:0;opacity:0;transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}@keyframes expandIn{0%{height:0;opacity:0;overflow:hidden}to{height:auto;opacity:1}}.rotate{transition:transform var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1))}.stagger-delay-1{animation-delay:50ms}.stagger-delay-2{animation-delay:.1s}.stagger-delay-3{animation-delay:.15s}.stagger-delay-4{animation-delay:.2s}.stagger-delay-5{animation-delay:.25s}.anim-forwards{animation-fill-mode:forwards}.anim-backwards{animation-fill-mode:backwards}.anim-both{animation-fill-mode:both}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}\n"],
|
|
@@ -4945,6 +4998,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
4945
4998
|
standalone: true,
|
|
4946
4999
|
imports: [CommonModule, OlIcon],
|
|
4947
5000
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
5001
|
+
providers: [provideOlIcons({ "close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>" })],
|
|
4948
5002
|
template: "@if (visible()) {\r\n <div\r\n [class]=\"bannerClasses()\"\r\n role=\"banner\"\r\n aria-live=\"polite\"\r\n aria-label=\"Thông báo hệ thống\"\r\n animate.enter=\"slide-down-enter\"\r\n animate.leave=\"slide-down-leave\"\r\n >\r\n <div class=\"container mx-auto px-4 py-3\">\r\n <div\r\n class=\"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4\"\r\n >\r\n <!-- Left: Icon + Badge + Content -->\r\n <div class=\"flex items-center gap-3 flex-1 min-w-0 w-full sm:w-auto\">\r\n <!-- Icon -->\r\n @if (icon()) {\r\n <ol-icon [name]=\"icon()\" [size]=\"20\" class=\"flex-shrink-0\" />\r\n }\r\n\r\n <!-- Badge -->\r\n @if (showBadge()) {\r\n <span [class]=\"badgeClasses()\">\r\n {{ badgeText() }}\r\n </span>\r\n }\r\n\r\n <!-- Content (ng-content) -->\r\n <div class=\"flex-1 min-w-0 text-sm sm:text-base\">\r\n <ng-content />\r\n </div>\r\n </div>\r\n\r\n <!-- Right: CTA + Dismiss -->\r\n <div class=\"flex items-center gap-3 flex-shrink-0 w-full sm:w-auto justify-end\">\r\n <!-- CTA Slot -->\r\n <ng-content select=\"[bannerCta]\" />\r\n\r\n <!-- Dismiss Button -->\r\n @if (dismissible()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"\r\n 'inline-flex items-center justify-center w-8 h-8 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-subtle ' +\r\n dismissButtonClasses()\r\n \"\r\n (click)=\"dismiss()\"\r\n aria-label=\"Đóng banner\"\r\n >\r\n <ol-icon name=\"close\" [size]=\"16\" />\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n",
|
|
4949
5003
|
styles: [".fade-in{animation:fadeIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.fade-in-fast{animation:fadeIn var(--anim-duration-fast, .15s) var(--anim-ease-out, ease-out) forwards}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fade-out{opacity:0;transition:opacity var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.slide-down-enter{animation:slideDown var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1)) forwards}.slide-down-leave{opacity:0;transform:translateY(-100%);transition:all var(--anim-duration-normal, .3s) cubic-bezier(.4,0,.6,1)}@keyframes slideDown{0%{transform:translateY(-100%);opacity:0}to{transform:translateY(0);opacity:1}}.toast-slide-enter{animation:toastSlideIn var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1)) forwards}.toast-slide-leave{opacity:0;transform:translate(100%);transition:all .2s cubic-bezier(.4,0,.6,1)}@keyframes toastSlideIn{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}.slide-in-up{animation:slideInUp var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.slide-in-down{animation:slideInDown var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInDown{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}.slide-in-left{animation:slideInLeft var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInLeft{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.slide-in-right{animation:slideInRight var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInRight{0%{opacity:0;transform:translate(-20px)}to{opacity:1;transform:translate(0)}}.scale-in{animation:scaleIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes scaleIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.scale-out{opacity:0;transform:scale(.95);transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.menu-scale-enter{animation:menuScaleIn var(--anim-duration-fast, .15s) var(--anim-ease-out, cubic-bezier(0, 0, .2, 1)) forwards}.menu-scale-leave{opacity:0;transform:scale(.85);transition:all .1s cubic-bezier(.4,0,1,1)}@keyframes menuScaleIn{0%{opacity:0;transform:scale(.85)}to{opacity:1;transform:scale(1)}}.fade-scale-enter{animation:fadeScaleIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.fade-scale-leave{opacity:0;transform:scale(.95);transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}@keyframes fadeScaleIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.alert-fade-out{opacity:0;transform:translateY(-8px);transition:all var(--anim-duration-normal, .3s) ease-out}.tooltip-fade-enter{animation:fadeIn var(--anim-duration-fast, .15s) var(--anim-ease-out, ease-out) forwards}.backdrop-enter{animation:fadeIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.backdrop-leave{opacity:0;transition:opacity var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.expand-enter{animation:expandIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.collapse-leave{overflow:hidden;height:0;opacity:0;transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}@keyframes expandIn{0%{height:0;opacity:0;overflow:hidden}to{height:auto;opacity:1}}.rotate{transition:transform var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1))}.stagger-delay-1{animation-delay:50ms}.stagger-delay-2{animation-delay:.1s}.stagger-delay-3{animation-delay:.15s}.stagger-delay-4{animation-delay:.2s}.stagger-delay-5{animation-delay:.25s}.anim-forwards{animation-fill-mode:forwards}.anim-backwards{animation-fill-mode:backwards}.anim-both{animation-fill-mode:both}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}\n"]
|
|
4950
5004
|
}]
|
|
@@ -5235,8 +5289,9 @@ var OlBreadcrumbItem = class OlBreadcrumbItem {
|
|
|
5235
5289
|
},
|
|
5236
5290
|
outputs: { itemClick: "itemClick" },
|
|
5237
5291
|
host: { classAttribute: "inline-flex" },
|
|
5292
|
+
providers: [provideOlIcons({ "alt-arrow-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m9 5l7 7l-7 7\"/></svg>" })],
|
|
5238
5293
|
ngImport: i0,
|
|
5239
|
-
template: "<ng-template #contentTpl>\r\n @if (icon()) {\r\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\r\n }\r\n @if (label()) {\r\n <span>{{ label() }}</span>\r\n } @else {\r\n <span><ng-content /></span>\r\n }\r\n</ng-template>\r\n\r\n<li class=\"inline-flex items-center gap-1.5\">\r\n @if (isLink()) {\r\n <!--\r\n `[clickable]=\"false\"` phải BỎ HẲN `[routerLink]`, không chỉ\r\n `preventDefault()` trong `(click)`.\r\n\r\n Đã thử cách kia trước: RouterLink tự đăng ký handler click của nó và\r\n KHÔNG kiểm `event.defaultPrevented`, nên nó điều hướng bất kể mình đã\r\n chặn hay chưa — spec `breadcrumb-item-clickable.spec.ts` bắt đúng chỗ này.\r\n\r\n Thay bằng `href` tĩnh dựng từ chính `routerLink()`: thẻ vẫn là `<a>` có\r\n `href` thật nên trình đọc màn hình vẫn đọc là liên kết, vẫn Tab tới được,\r\n vẫn xem được đích đến ở thanh trạng thái — chỉ `preventDefault()` chặn\r\n trình duyệt đi theo. Đó là khác biệt với `disabled`, vốn bỏ luôn thẻ `<a>`.\r\n -->\r\n @if (routerLink() && clickable()) {\r\n <a\r\n [routerLink]=\"routerLink()\"\r\n (click)=\"onClick($event)\"\r\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\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </a>\r\n } @else if (routerLink()) {\r\n <a\r\n [href]=\"staticHref()\"\r\n (click)=\"onClick($event)\"\r\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\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </a>\r\n } @else {\r\n <a\r\n [href]=\"href()\"\r\n (click)=\"onClick($event)\"\r\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\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </a>\r\n }\r\n } @else {\r\n <!--\r\n Thứ tự ba nhánh là CỐ Ý: `disabled` đứng TRƯỚC `isCurrent`. Mục cuối mà\r\n cũng bị khoá thì phải hiện là khoá — ngược lại người dùng thấy nó y hệt\r\n trang hiện tại và không hiểu vì sao bấm không được.\r\n\r\n Màu theo chuẩn disabled của thư viện (`bg-disabled` + `text-fg-disabled`,\r\n đúng Figma State=Disabled, xem button.component.ts). KHÔNG dùng\r\n `opacity-50`: mẫu đó chỉ còn 2 component dùng, và thư viện đang có bốn\r\n bậc opacity song song chưa hội tụ.\r\n\r\n Mục ĐANG ĐỨNG dùng `text-heading` ở CẢ HAI chế độ — LỆCH FIGMA CÓ CHỦ Ý\r\n (2026-09-24, theo yêu cầu người dùng \"cho màu đậm hơn các màu khác\").\r\n\r\n Figma `ol-breadcrumb-item` #1148:31348 `Status=Active` bind\r\n `colors/text/text-body-subtle` = #6B7280. Đo /breadcrumb ở chế độ SÁNG\r\n thì nó cho tương phản **4,83** trong khi các cấp cha (`text-body`\r\n #4A5565) đạt **7,56** — tức trang đang đứng NHẠT HƠN cấp cha nó, ngược\r\n ngữ nghĩa. Nay `text-heading`: sáng #101828 (17,75) · tối #FFFFFF\r\n (20,13), đậm hơn hẳn và tự lật theo `.dark`.\r\n\r\n Bản trước phải viết `text-body-subtle dark:text-heading` vì ở nhánh tối\r\n `--text-body-subtle` bị đặt BẰNG HỆT `--text-body` (cùng gray-300) nên\r\n mục cuối và mục thường trùng màu. Dùng `text-heading` cho cả hai chế độ\r\n thì hết cần ngoại lệ đó.\r\n\r\n Nợ: cập nhật Figma `Status=Active` sang `colors/text/text-heading`.\r\n -->\r\n <span\r\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\r\n [class]=\"\r\n disabled()\r\n ? 'font-medium text-fg-disabled cursor-not-allowed'\r\n : isCurrent()\r\n ? 'font-medium text-heading'\r\n : status() === 'hover'\r\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\r\n : 'font-medium text-body'\r\n \"\r\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\r\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </span>\r\n }\r\n\r\n @if (!isCurrent() && showSeparator()) {\r\n <!--\r\n Dấu ngăn dùng `text-body-subtle`, KHÔNG `text-fg-disabled`.\r\n\r\n `text-fg-disabled` (#99A1AF) trên nền trắng chỉ đạt **2,60** — có\r\n `aria-hidden` nên không vi phạm WCAG, nhưng người nhìn kém khó thấy ranh\r\n giới giữa các cấp. `text-body-subtle` (#6B7280) đạt 4,83 mà vẫn nhạt hơn\r\n chữ của chặng, nên vẫn ra \"dấu ngăn\" chứ không cạnh tranh với nội dung.\r\n\r\n KHÁC dòng trên: chặng `disabled` giữ `text-fg-disabled` — đó là chuẩn\r\n trạng thái khoá của thư viện, và WCAG miễn trừ phần tử không bấm được.\r\n -->\r\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\r\n @switch (effectiveSeparator()) {\r\n @case ('slash') {\r\n <span class=\"text-xs font-normal\">/</span>\r\n }\r\n @case ('arrow') {\r\n <span class=\"text-xs\">→</span>\r\n }\r\n @case ('dot') {\r\n <span class=\"text-xs\">•</span>\r\n }\r\n @case ('chevron') {\r\n <ol-icon name=\"
|
|
5294
|
+
template: "<ng-template #contentTpl>\r\n @if (icon()) {\r\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\r\n }\r\n @if (label()) {\r\n <span>{{ label() }}</span>\r\n } @else {\r\n <span><ng-content /></span>\r\n }\r\n</ng-template>\r\n\r\n<li class=\"inline-flex items-center gap-1.5\">\r\n @if (isLink()) {\r\n <!--\r\n `[clickable]=\"false\"` phải BỎ HẲN `[routerLink]`, không chỉ\r\n `preventDefault()` trong `(click)`.\r\n\r\n Đã thử cách kia trước: RouterLink tự đăng ký handler click của nó và\r\n KHÔNG kiểm `event.defaultPrevented`, nên nó điều hướng bất kể mình đã\r\n chặn hay chưa — spec `breadcrumb-item-clickable.spec.ts` bắt đúng chỗ này.\r\n\r\n Thay bằng `href` tĩnh dựng từ chính `routerLink()`: thẻ vẫn là `<a>` có\r\n `href` thật nên trình đọc màn hình vẫn đọc là liên kết, vẫn Tab tới được,\r\n vẫn xem được đích đến ở thanh trạng thái — chỉ `preventDefault()` chặn\r\n trình duyệt đi theo. Đó là khác biệt với `disabled`, vốn bỏ luôn thẻ `<a>`.\r\n -->\r\n @if (routerLink() && clickable()) {\r\n <a\r\n [routerLink]=\"routerLink()\"\r\n (click)=\"onClick($event)\"\r\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\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </a>\r\n } @else if (routerLink()) {\r\n <a\r\n [href]=\"staticHref()\"\r\n (click)=\"onClick($event)\"\r\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\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </a>\r\n } @else {\r\n <a\r\n [href]=\"href()\"\r\n (click)=\"onClick($event)\"\r\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\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </a>\r\n }\r\n } @else {\r\n <!--\r\n Thứ tự ba nhánh là CỐ Ý: `disabled` đứng TRƯỚC `isCurrent`. Mục cuối mà\r\n cũng bị khoá thì phải hiện là khoá — ngược lại người dùng thấy nó y hệt\r\n trang hiện tại và không hiểu vì sao bấm không được.\r\n\r\n Màu theo chuẩn disabled của thư viện (`bg-disabled` + `text-fg-disabled`,\r\n đúng Figma State=Disabled, xem button.component.ts). KHÔNG dùng\r\n `opacity-50`: mẫu đó chỉ còn 2 component dùng, và thư viện đang có bốn\r\n bậc opacity song song chưa hội tụ.\r\n\r\n Mục ĐANG ĐỨNG dùng `text-heading` ở CẢ HAI chế độ — LỆCH FIGMA CÓ CHỦ Ý\r\n (2026-09-24, theo yêu cầu người dùng \"cho màu đậm hơn các màu khác\").\r\n\r\n Figma `ol-breadcrumb-item` #1148:31348 `Status=Active` bind\r\n `colors/text/text-body-subtle` = #6B7280. Đo /breadcrumb ở chế độ SÁNG\r\n thì nó cho tương phản **4,83** trong khi các cấp cha (`text-body`\r\n #4A5565) đạt **7,56** — tức trang đang đứng NHẠT HƠN cấp cha nó, ngược\r\n ngữ nghĩa. Nay `text-heading`: sáng #101828 (17,75) · tối #FFFFFF\r\n (20,13), đậm hơn hẳn và tự lật theo `.dark`.\r\n\r\n Bản trước phải viết `text-body-subtle dark:text-heading` vì ở nhánh tối\r\n `--text-body-subtle` bị đặt BẰNG HỆT `--text-body` (cùng gray-300) nên\r\n mục cuối và mục thường trùng màu. Dùng `text-heading` cho cả hai chế độ\r\n thì hết cần ngoại lệ đó.\r\n\r\n Nợ: cập nhật Figma `Status=Active` sang `colors/text/text-heading`.\r\n -->\r\n <span\r\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\r\n [class]=\"\r\n disabled()\r\n ? 'font-medium text-fg-disabled cursor-not-allowed'\r\n : isCurrent()\r\n ? 'font-medium text-heading'\r\n : status() === 'hover'\r\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\r\n : 'font-medium text-body'\r\n \"\r\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\r\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </span>\r\n }\r\n\r\n @if (!isCurrent() && showSeparator()) {\r\n <!--\r\n Dấu ngăn dùng `text-body-subtle`, KHÔNG `text-fg-disabled`.\r\n\r\n `text-fg-disabled` (#99A1AF) trên nền trắng chỉ đạt **2,60** — có\r\n `aria-hidden` nên không vi phạm WCAG, nhưng người nhìn kém khó thấy ranh\r\n giới giữa các cấp. `text-body-subtle` (#6B7280) đạt 4,83 mà vẫn nhạt hơn\r\n chữ của chặng, nên vẫn ra \"dấu ngăn\" chứ không cạnh tranh với nội dung.\r\n\r\n KHÁC dòng trên: chặng `disabled` giữ `text-fg-disabled` — đó là chuẩn\r\n trạng thái khoá của thư viện, và WCAG miễn trừ phần tử không bấm được.\r\n -->\r\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\r\n @switch (effectiveSeparator()) {\r\n @case ('slash') {\r\n <span class=\"text-xs font-normal\">/</span>\r\n }\r\n @case ('arrow') {\r\n <span class=\"text-xs\">→</span>\r\n }\r\n @case ('dot') {\r\n <span class=\"text-xs\">•</span>\r\n }\r\n @case ('chevron') {\r\n <ol-icon name=\"alt_arrow_right\" size=\"xs\"></ol-icon>\r\n }\r\n @default {\r\n <ol-icon name=\"alt_arrow_right\" size=\"xs\"></ol-icon>\r\n }\r\n }\r\n </span>\r\n }\r\n</li>\r\n",
|
|
5240
5295
|
dependencies: [
|
|
5241
5296
|
{
|
|
5242
5297
|
kind: "ngmodule",
|
|
@@ -5308,7 +5363,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
5308
5363
|
],
|
|
5309
5364
|
host: { class: "inline-flex" },
|
|
5310
5365
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
5311
|
-
|
|
5366
|
+
providers: [provideOlIcons({ "alt-arrow-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m9 5l7 7l-7 7\"/></svg>" })],
|
|
5367
|
+
template: "<ng-template #contentTpl>\r\n @if (icon()) {\r\n <ol-icon [name]=\"icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\r\n }\r\n @if (label()) {\r\n <span>{{ label() }}</span>\r\n } @else {\r\n <span><ng-content /></span>\r\n }\r\n</ng-template>\r\n\r\n<li class=\"inline-flex items-center gap-1.5\">\r\n @if (isLink()) {\r\n <!--\r\n `[clickable]=\"false\"` phải BỎ HẲN `[routerLink]`, không chỉ\r\n `preventDefault()` trong `(click)`.\r\n\r\n Đã thử cách kia trước: RouterLink tự đăng ký handler click của nó và\r\n KHÔNG kiểm `event.defaultPrevented`, nên nó điều hướng bất kể mình đã\r\n chặn hay chưa — spec `breadcrumb-item-clickable.spec.ts` bắt đúng chỗ này.\r\n\r\n Thay bằng `href` tĩnh dựng từ chính `routerLink()`: thẻ vẫn là `<a>` có\r\n `href` thật nên trình đọc màn hình vẫn đọc là liên kết, vẫn Tab tới được,\r\n vẫn xem được đích đến ở thanh trạng thái — chỉ `preventDefault()` chặn\r\n trình duyệt đi theo. Đó là khác biệt với `disabled`, vốn bỏ luôn thẻ `<a>`.\r\n -->\r\n @if (routerLink() && clickable()) {\r\n <a\r\n [routerLink]=\"routerLink()\"\r\n (click)=\"onClick($event)\"\r\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\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </a>\r\n } @else if (routerLink()) {\r\n <a\r\n [href]=\"staticHref()\"\r\n (click)=\"onClick($event)\"\r\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\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </a>\r\n } @else {\r\n <a\r\n [href]=\"href()\"\r\n (click)=\"onClick($event)\"\r\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\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </a>\r\n }\r\n } @else {\r\n <!--\r\n Thứ tự ba nhánh là CỐ Ý: `disabled` đứng TRƯỚC `isCurrent`. Mục cuối mà\r\n cũng bị khoá thì phải hiện là khoá — ngược lại người dùng thấy nó y hệt\r\n trang hiện tại và không hiểu vì sao bấm không được.\r\n\r\n Màu theo chuẩn disabled của thư viện (`bg-disabled` + `text-fg-disabled`,\r\n đúng Figma State=Disabled, xem button.component.ts). KHÔNG dùng\r\n `opacity-50`: mẫu đó chỉ còn 2 component dùng, và thư viện đang có bốn\r\n bậc opacity song song chưa hội tụ.\r\n\r\n Mục ĐANG ĐỨNG dùng `text-heading` ở CẢ HAI chế độ — LỆCH FIGMA CÓ CHỦ Ý\r\n (2026-09-24, theo yêu cầu người dùng \"cho màu đậm hơn các màu khác\").\r\n\r\n Figma `ol-breadcrumb-item` #1148:31348 `Status=Active` bind\r\n `colors/text/text-body-subtle` = #6B7280. Đo /breadcrumb ở chế độ SÁNG\r\n thì nó cho tương phản **4,83** trong khi các cấp cha (`text-body`\r\n #4A5565) đạt **7,56** — tức trang đang đứng NHẠT HƠN cấp cha nó, ngược\r\n ngữ nghĩa. Nay `text-heading`: sáng #101828 (17,75) · tối #FFFFFF\r\n (20,13), đậm hơn hẳn và tự lật theo `.dark`.\r\n\r\n Bản trước phải viết `text-body-subtle dark:text-heading` vì ở nhánh tối\r\n `--text-body-subtle` bị đặt BẰNG HỆT `--text-body` (cùng gray-300) nên\r\n mục cuối và mục thường trùng màu. Dùng `text-heading` cho cả hai chế độ\r\n thì hết cần ngoại lệ đó.\r\n\r\n Nợ: cập nhật Figma `Status=Active` sang `colors/text/text-heading`.\r\n -->\r\n <span\r\n class=\"inline-flex items-center gap-1.5 text-sm select-none\"\r\n [class]=\"\r\n disabled()\r\n ? 'font-medium text-fg-disabled cursor-not-allowed'\r\n : isCurrent()\r\n ? 'font-medium text-heading'\r\n : status() === 'hover'\r\n ? 'font-medium text-fg-brand dark:text-fg-brand cursor-pointer'\r\n : 'font-medium text-body'\r\n \"\r\n [attr.aria-current]=\"isCurrent() && !disabled() ? 'page' : null\"\r\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\r\n >\r\n <ng-container *ngTemplateOutlet=\"contentTpl\" />\r\n </span>\r\n }\r\n\r\n @if (!isCurrent() && showSeparator()) {\r\n <!--\r\n Dấu ngăn dùng `text-body-subtle`, KHÔNG `text-fg-disabled`.\r\n\r\n `text-fg-disabled` (#99A1AF) trên nền trắng chỉ đạt **2,60** — có\r\n `aria-hidden` nên không vi phạm WCAG, nhưng người nhìn kém khó thấy ranh\r\n giới giữa các cấp. `text-body-subtle` (#6B7280) đạt 4,83 mà vẫn nhạt hơn\r\n chữ của chặng, nên vẫn ra \"dấu ngăn\" chứ không cạnh tranh với nội dung.\r\n\r\n KHÁC dòng trên: chặng `disabled` giữ `text-fg-disabled` — đó là chuẩn\r\n trạng thái khoá của thư viện, và WCAG miễn trừ phần tử không bấm được.\r\n -->\r\n <span class=\"text-body-subtle flex items-center select-none\" aria-hidden=\"true\">\r\n @switch (effectiveSeparator()) {\r\n @case ('slash') {\r\n <span class=\"text-xs font-normal\">/</span>\r\n }\r\n @case ('arrow') {\r\n <span class=\"text-xs\">→</span>\r\n }\r\n @case ('dot') {\r\n <span class=\"text-xs\">•</span>\r\n }\r\n @case ('chevron') {\r\n <ol-icon name=\"alt_arrow_right\" size=\"xs\"></ol-icon>\r\n }\r\n @default {\r\n <ol-icon name=\"alt_arrow_right\" size=\"xs\"></ol-icon>\r\n }\r\n }\r\n </span>\r\n }\r\n</li>\r\n"
|
|
5312
5368
|
}]
|
|
5313
5369
|
}],
|
|
5314
5370
|
propDecorators: {
|
|
@@ -5970,7 +6026,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
5970
6026
|
}
|
|
5971
6027
|
});
|
|
5972
6028
|
var OlCarousel = class OlCarousel {
|
|
5973
|
-
|
|
6029
|
+
constructor() {
|
|
6030
|
+
this.el = inject(ElementRef);
|
|
6031
|
+
}
|
|
6032
|
+
el;
|
|
5974
6033
|
activeIndex = model(0, ...ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []);
|
|
5975
6034
|
slideCountInput = input(0, {
|
|
5976
6035
|
...ngDevMode ? { debugName: "slideCountInput" } : /* istanbul ignore next */ {},
|
|
@@ -6051,8 +6110,12 @@ var OlCarousel = class OlCarousel {
|
|
|
6051
6110
|
},
|
|
6052
6111
|
outputs: { activeIndex: "activeIndexChange" },
|
|
6053
6112
|
host: { classAttribute: "block w-full" },
|
|
6113
|
+
providers: [provideOlIcons({
|
|
6114
|
+
"chevron-left": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m15 6l-6 6l6 6\"/></svg>",
|
|
6115
|
+
"chevron-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m9 18l6-6l-6-6\"/></svg>"
|
|
6116
|
+
})],
|
|
6054
6117
|
ngImport: i0,
|
|
6055
|
-
template: "<!-- Nút điều hướng dùng nền trắng mờ (bg-white/30) để nhìn xuyên qua ảnh.\r\n Figma `ol-carousel-control` vẽ nền xám đặc, nhưng nút ở đó không nằm\r\n trên ảnh — đã chốt giữ trắng mờ và đề nghị Figma bổ sung biến thể\r\n nút-trên-ảnh. Các bậc số gray ở đây thuộc phần đó. -->\r\n<div class=\"relative w-full\">\r\n <div class=\"relative h-56 overflow-hidden rounded-lg md:h-96\">\r\n <!--\r\n Track trượt. Slide do NƠI GỌI chiếu vào nên component không sửa được\r\n class của chúng — ép bề rộng qua biến thể con `[&>*]`, cùng kỹ thuật\r\n `ol-button-group` dùng cho kích thước nút.\r\n\r\n `<ng-content />` chỉ khai MỘT lần và nằm ngoài mọi `@if`: Angular chiếu\r\n nội dung đúng một lần lúc khởi tạo, đặt trong khối điều kiện là mất ruột.\r\n -->\r\n <div\r\n data-track\r\n class=\"flex h-full transition-transform duration-300 ease-out [&>*]:w-full [&>*]:shrink-0\"\r\n [style.transform]=\"'translateX(' + activeIndex() * -100 + '%)'\"\r\n >\r\n <ng-content />\r\n </div>\r\n </div>\r\n @if (indicators()) {\r\n <div\r\n class=\"absolute z-30 flex -translate-x-1/2 bottom-5 left-1/2 space-x-3 rtl:space-x-reverse\"\r\n >\r\n @for (item of indicatorList(); track $index) {\r\n <button\r\n type=\"button\"\r\n (click)=\"goTo($index)\"\r\n [attr.aria-label]=\"'Tới ảnh ' + ($index + 1) + ' trên ' + slideCount()\"\r\n class=\"w-3 h-3 rounded-full\"\r\n [class.bg-white]=\"activeIndex() === $index\"\r\n [class.bg-white/50]=\"activeIndex() !== $index\"\r\n [attr.aria-current]=\"activeIndex() === $index\"\r\n ></button>\r\n }\r\n </div>\r\n }\r\n @if (controls()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n class=\"absolute top-0 inset-s-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-hidden\"\r\n >\r\n <span\r\n class=\"inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-dark/30 group-hover:bg-white/50 dark:group-hover:bg-dark/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-hidden\"\r\n >\r\n <ol-icon name=\"
|
|
6118
|
+
template: "<!-- Nút điều hướng dùng nền trắng mờ (bg-white/30) để nhìn xuyên qua ảnh.\r\n Figma `ol-carousel-control` vẽ nền xám đặc, nhưng nút ở đó không nằm\r\n trên ảnh — đã chốt giữ trắng mờ và đề nghị Figma bổ sung biến thể\r\n nút-trên-ảnh. Các bậc số gray ở đây thuộc phần đó. -->\r\n<div class=\"relative w-full\">\r\n <div class=\"relative h-56 overflow-hidden rounded-lg md:h-96\">\r\n <!--\r\n Track trượt. Slide do NƠI GỌI chiếu vào nên component không sửa được\r\n class của chúng — ép bề rộng qua biến thể con `[&>*]`, cùng kỹ thuật\r\n `ol-button-group` dùng cho kích thước nút.\r\n\r\n `<ng-content />` chỉ khai MỘT lần và nằm ngoài mọi `@if`: Angular chiếu\r\n nội dung đúng một lần lúc khởi tạo, đặt trong khối điều kiện là mất ruột.\r\n -->\r\n <div\r\n data-track\r\n class=\"flex h-full transition-transform duration-300 ease-out [&>*]:w-full [&>*]:shrink-0\"\r\n [style.transform]=\"'translateX(' + activeIndex() * -100 + '%)'\"\r\n >\r\n <ng-content />\r\n </div>\r\n </div>\r\n @if (indicators()) {\r\n <div\r\n class=\"absolute z-30 flex -translate-x-1/2 bottom-5 left-1/2 space-x-3 rtl:space-x-reverse\"\r\n >\r\n @for (item of indicatorList(); track $index) {\r\n <button\r\n type=\"button\"\r\n (click)=\"goTo($index)\"\r\n [attr.aria-label]=\"'Tới ảnh ' + ($index + 1) + ' trên ' + slideCount()\"\r\n class=\"w-3 h-3 rounded-full\"\r\n [class.bg-white]=\"activeIndex() === $index\"\r\n [class.bg-white/50]=\"activeIndex() !== $index\"\r\n [attr.aria-current]=\"activeIndex() === $index\"\r\n ></button>\r\n }\r\n </div>\r\n }\r\n @if (controls()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n class=\"absolute top-0 inset-s-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-hidden\"\r\n >\r\n <span\r\n class=\"inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-dark/30 group-hover:bg-white/50 dark:group-hover:bg-dark/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-hidden\"\r\n >\r\n <ol-icon name=\"chevron_left\" size=\"xs\" />\r\n <span class=\"sr-only\">Ảnh trước</span>\r\n </span>\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n class=\"absolute top-0 inset-e-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-hidden\"\r\n >\r\n <span\r\n class=\"inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-dark/30 group-hover:bg-white/50 dark:group-hover:bg-dark/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-hidden\"\r\n >\r\n <ol-icon name=\"chevron_right\" size=\"xs\" />\r\n <span class=\"sr-only\">Ảnh sau</span>\r\n </span>\r\n </button>\r\n }\r\n</div>\r\n",
|
|
6056
6119
|
dependencies: [{
|
|
6057
6120
|
kind: "component",
|
|
6058
6121
|
type: OlIcon,
|
|
@@ -6081,9 +6144,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
6081
6144
|
imports: [OlIcon],
|
|
6082
6145
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6083
6146
|
host: { class: "block w-full" },
|
|
6084
|
-
|
|
6147
|
+
providers: [provideOlIcons({
|
|
6148
|
+
"chevron-left": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m15 6l-6 6l6 6\"/></svg>",
|
|
6149
|
+
"chevron-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m9 18l6-6l-6-6\"/></svg>"
|
|
6150
|
+
})],
|
|
6151
|
+
template: "<!-- Nút điều hướng dùng nền trắng mờ (bg-white/30) để nhìn xuyên qua ảnh.\r\n Figma `ol-carousel-control` vẽ nền xám đặc, nhưng nút ở đó không nằm\r\n trên ảnh — đã chốt giữ trắng mờ và đề nghị Figma bổ sung biến thể\r\n nút-trên-ảnh. Các bậc số gray ở đây thuộc phần đó. -->\r\n<div class=\"relative w-full\">\r\n <div class=\"relative h-56 overflow-hidden rounded-lg md:h-96\">\r\n <!--\r\n Track trượt. Slide do NƠI GỌI chiếu vào nên component không sửa được\r\n class của chúng — ép bề rộng qua biến thể con `[&>*]`, cùng kỹ thuật\r\n `ol-button-group` dùng cho kích thước nút.\r\n\r\n `<ng-content />` chỉ khai MỘT lần và nằm ngoài mọi `@if`: Angular chiếu\r\n nội dung đúng một lần lúc khởi tạo, đặt trong khối điều kiện là mất ruột.\r\n -->\r\n <div\r\n data-track\r\n class=\"flex h-full transition-transform duration-300 ease-out [&>*]:w-full [&>*]:shrink-0\"\r\n [style.transform]=\"'translateX(' + activeIndex() * -100 + '%)'\"\r\n >\r\n <ng-content />\r\n </div>\r\n </div>\r\n @if (indicators()) {\r\n <div\r\n class=\"absolute z-30 flex -translate-x-1/2 bottom-5 left-1/2 space-x-3 rtl:space-x-reverse\"\r\n >\r\n @for (item of indicatorList(); track $index) {\r\n <button\r\n type=\"button\"\r\n (click)=\"goTo($index)\"\r\n [attr.aria-label]=\"'Tới ảnh ' + ($index + 1) + ' trên ' + slideCount()\"\r\n class=\"w-3 h-3 rounded-full\"\r\n [class.bg-white]=\"activeIndex() === $index\"\r\n [class.bg-white/50]=\"activeIndex() !== $index\"\r\n [attr.aria-current]=\"activeIndex() === $index\"\r\n ></button>\r\n }\r\n </div>\r\n }\r\n @if (controls()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n class=\"absolute top-0 inset-s-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-hidden\"\r\n >\r\n <span\r\n class=\"inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-dark/30 group-hover:bg-white/50 dark:group-hover:bg-dark/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-hidden\"\r\n >\r\n <ol-icon name=\"chevron_left\" size=\"xs\" />\r\n <span class=\"sr-only\">Ảnh trước</span>\r\n </span>\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n class=\"absolute top-0 inset-e-0 z-30 flex items-center justify-center h-full px-4 cursor-pointer group focus:outline-hidden\"\r\n >\r\n <span\r\n class=\"inline-flex items-center justify-center w-10 h-10 rounded-full bg-white/30 dark:bg-dark/30 group-hover:bg-white/50 dark:group-hover:bg-dark/60 group-focus:ring-4 group-focus:ring-white dark:group-focus:ring-gray-800/70 group-focus:outline-hidden\"\r\n >\r\n <ol-icon name=\"chevron_right\" size=\"xs\" />\r\n <span class=\"sr-only\">Ảnh sau</span>\r\n </span>\r\n </button>\r\n }\r\n</div>\r\n"
|
|
6085
6152
|
}]
|
|
6086
6153
|
}],
|
|
6154
|
+
ctorParameters: () => [],
|
|
6087
6155
|
propDecorators: {
|
|
6088
6156
|
activeIndex: [{
|
|
6089
6157
|
type: i0.Input,
|
|
@@ -6949,7 +7017,7 @@ var OlFieldLabel = class OlFieldLabel {
|
|
|
6949
7017
|
class="inline-flex cursor-help items-center text-body-subtle hover:text-heading"
|
|
6950
7018
|
[title]="helpTooltip() || 'Trợ giúp'"
|
|
6951
7019
|
>
|
|
6952
|
-
<ol-icon name="
|
|
7020
|
+
<ol-icon name="lucide:help-circle" size="xs" />
|
|
6953
7021
|
</span>
|
|
6954
7022
|
}
|
|
6955
7023
|
</label>
|
|
@@ -6994,7 +7062,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
6994
7062
|
class="inline-flex cursor-help items-center text-body-subtle hover:text-heading"
|
|
6995
7063
|
[title]="helpTooltip() || 'Trợ giúp'"
|
|
6996
7064
|
>
|
|
6997
|
-
<ol-icon name="
|
|
7065
|
+
<ol-icon name="lucide:help-circle" size="xs" />
|
|
6998
7066
|
</span>
|
|
6999
7067
|
}
|
|
7000
7068
|
</label>
|
|
@@ -7557,8 +7625,12 @@ var OlDatepickerHeader = class OlDatepickerHeader {
|
|
|
7557
7625
|
type: OlDatepickerHeader,
|
|
7558
7626
|
isStandalone: true,
|
|
7559
7627
|
selector: "ol-datepicker-header",
|
|
7628
|
+
providers: [provideOlIcons({
|
|
7629
|
+
"chevron-left": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m15 6l-6 6l6 6\"/></svg>",
|
|
7630
|
+
"chevron-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m9 18l6-6l-6-6\"/></svg>"
|
|
7631
|
+
})],
|
|
7560
7632
|
ngImport: i0,
|
|
7561
|
-
template: "<div class=\"flex items-center justify-between w-full pb-2 mb-2 border-b border-light-medium\">\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigatePrevious()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-default-medium dark:hover:text-white transition-colors cursor-pointer\"\r\n aria-label=\"Trước\"\r\n >\r\n <ol-icon name=\"
|
|
7633
|
+
template: "<div class=\"flex items-center justify-between w-full pb-2 mb-2 border-b border-light-medium\">\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigatePrevious()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-default-medium dark:hover:text-white transition-colors cursor-pointer\"\r\n aria-label=\"Trước\"\r\n >\r\n <ol-icon name=\"chevron_left\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"state.toggleViewMode()\"\r\n class=\"text-sm font-semibold text-heading hover:bg-neutral-tertiary-medium px-3 py-1.5 rounded-lg transition-colors cursor-pointer select-none\"\r\n >\r\n {{ state.headerTitle() }}\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigateNext()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-default-medium dark:hover:text-white transition-colors cursor-pointer\"\r\n aria-label=\"Sau\"\r\n >\r\n <ol-icon name=\"chevron_right\" size=\"sm\" />\r\n </button>\r\n</div>\r\n",
|
|
7562
7634
|
dependencies: [{
|
|
7563
7635
|
kind: "ngmodule",
|
|
7564
7636
|
type: CommonModule
|
|
@@ -7589,7 +7661,11 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
7589
7661
|
standalone: true,
|
|
7590
7662
|
imports: [CommonModule, OlIcon],
|
|
7591
7663
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
7592
|
-
|
|
7664
|
+
providers: [provideOlIcons({
|
|
7665
|
+
"chevron-left": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m15 6l-6 6l6 6\"/></svg>",
|
|
7666
|
+
"chevron-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m9 18l6-6l-6-6\"/></svg>"
|
|
7667
|
+
})],
|
|
7668
|
+
template: "<div class=\"flex items-center justify-between w-full pb-2 mb-2 border-b border-light-medium\">\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigatePrevious()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-default-medium dark:hover:text-white transition-colors cursor-pointer\"\r\n aria-label=\"Trước\"\r\n >\r\n <ol-icon name=\"chevron_left\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"state.toggleViewMode()\"\r\n class=\"text-sm font-semibold text-heading hover:bg-neutral-tertiary-medium px-3 py-1.5 rounded-lg transition-colors cursor-pointer select-none\"\r\n >\r\n {{ state.headerTitle() }}\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigateNext()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading focus:outline-hidden focus:ring-2 focus:ring-default-medium dark:hover:text-white transition-colors cursor-pointer\"\r\n aria-label=\"Sau\"\r\n >\r\n <ol-icon name=\"chevron_right\" size=\"sm\" />\r\n </button>\r\n</div>\r\n"
|
|
7593
7669
|
}]
|
|
7594
7670
|
}]
|
|
7595
7671
|
});
|
|
@@ -8078,7 +8154,7 @@ var OlDatepickerPopup = class OlDatepickerPopup {
|
|
|
8078
8154
|
cancelClick: "cancelClick"
|
|
8079
8155
|
},
|
|
8080
8156
|
ngImport: i0,
|
|
8081
|
-
template: "<!--\r\n Chặn chiều DỌC, đối xứng với `flex-wrap` lo chiều ngang bên dưới.\r\n\r\n Đo /datepicker ở 375×812 (range picker, dualMonth): khi hai lịch xuống dòng,\r\n popup cao **770px**, đáy ở 1257 — **vượt đáy màn 445px**. Tháng thứ hai và\r\n TOÀN BỘ footer (nút \"Xác nhận\") nằm ngoài màn, `overflow-y: visible` nên\r\n không cuộn tới được: ở mobile người dùng **không chốt nổi dải ngày**.\r\n\r\n Giới hạn theo `--ol-dp-max-height` — khoảng trống THỰC TẾ dưới ô nhập, do\r\n component đặt khi mở và khi cuộn/đổi cỡ. Không dùng `85svh`: đó là chiều cao\r\n MÀN, trong khi popup mở dưới ô nhập vốn đã nằm giữa màn — đo lại sau khi thử\r\n `85svh` vẫn còn vượt đáy 365px và footer vẫn khuất.\r\n\r\n `svh` trong giá trị dự phòng chứ không `vh`: trên di động `vh` tính theo khung\r\n khi thanh địa chỉ ĐÃ thu, nên footer vẫn chui xuống dưới thanh đó lúc mới mở.\r\n-->\r\n<div\r\n class=\"flex flex-wrap bg-neutral-primary-medium border border-default-medium rounded-base shadow-xl p-3.5 sm:p-5 select-none transition-all max-w-full\"\r\n [class]=\"wrapperClasses()\"\r\n>\r\n <!-- Optional Quick Presets Sidebar / Topbar on Mobile & Tablet -->\r\n @if (presets().length > 0) {\r\n <div\r\n class=\"flex flex-row md:flex-col flex-wrap md:flex-nowrap gap-1 pb-3 mb-3 border-b md:border-b-0 md:pb-0 md:mb-0 md:pr-4 md:mr-4 md:border-r border-light-medium md:min-w-32.5\"\r\n >\r\n <span\r\n class=\"w-full text-xs font-semibold text-fg-disabled uppercase tracking-wider mb-1 md:mb-2\"\r\n >\r\n Phím tắt\r\n </span>\r\n @for (preset of presets(); track preset.label) {\r\n <button\r\n type=\"button\"\r\n (click)=\"applyPreset(preset)\"\r\n class=\"text-left px-2.5 py-1 sm:py-1.5 text-xs font-medium text-body bg-neutral-tertiary md:bg-transparent hover:bg-neutral-tertiary-medium rounded-lg transition-colors cursor-pointer whitespace-nowrap\"\r\n >\r\n {{ preset.label }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Calendar Content -->\r\n @if (showTwoMonths() && state.viewMode() === 'day') {\r\n <!-- Dual-Month (Stacked vertically on mobile/tablet or narrow containers, side-by-side on wide containers) -->\r\n <!--\r\n `md:w-auto` chứ KHÔNG để `w-full` trần: cha là `flex-wrap`, nên `w-full`\r\n buộc khối lịch chiếm trọn dòng và bị đẩy XUỐNG DƯỚI cột preset thay vì\r\n nằm cạnh. Đo /datepicker ở 1278px (range picker, dualMonth + 4 preset):\r\n preset ở y=21 cao 152px rộng 130px, khối lịch ở **y=173** — xuống dòng,\r\n để trống 544px bên phải. Popup cần 554px trong khi chỗ trống hai phía\r\n chỉ 358px, nên `max-height` cắt mất hàng ngày cuối và footer.\r\n\r\n Giữ `w-full` ở màn hẹp: dưới `md` cột preset là hàng NGANG trên cùng\r\n (`flex-row` + `border-b`), lúc đó khối lịch phải chiếm trọn dòng dưới.\r\n Breakpoint khớp đúng chỗ preset đổi chiều.\r\n -->\r\n <div class=\"flex flex-col w-full md:w-auto {{ calendarBlockClasses() }}\">\r\n <!--\r\n `flex-wrap` chứ KHÔNG phải `sm:flex-row`: hai lịch mỗi cái `w-63` (252px)\r\n cứng, nên xếp ngang cần tối thiểu ~560px kể cả đệm. `sm:` bật từ 640px —\r\n đủ cho khung TRỐNG, nhưng nơi dùng thường có thanh bên. Đo /datepicker ở\r\n 768px: popup rộng 716px trong khi cha `max-w-md` chỉ 334px, mép phải\r\n 1093px — **54 nút ngày nằm ngoài màn, không bấm được**.\r\n\r\n Breakpoint đo BỀ RỘNG MÀN, không đo chỗ còn lại cho component. `flex-wrap`\r\n đo chỗ thật: đủ thì nằm ngang, không đủ thì tự xuống dòng.\r\n -->\r\n <!--\r\n Vùng LỊCH cuộn riêng, footer ở NGOÀI vùng này nên luôn thấy.\r\n\r\n Trước đây trần + `overflow-y-auto` áp cho cả wrapper ngoài, nên footer\r\n cuộn theo: đo /datepicker ở 1278px (dualMonth + 4 preset), nội dung cần\r\n 402px mà chỗ trống hai phía chỉ 358px — footer cao 49px có đáy ở 383\r\n trong khung 350, **tràn 33px**. Nút \"Xác nhận\" phải cuộn mới thấy.\r\n\r\n `min-h-0` là bắt buộc: con của flex mặc định `min-height: auto`, không\r\n co nhỏ hơn nội dung nên `overflow-y-auto` không bao giờ kích hoạt.\r\n -->\r\n <div\r\n class=\"flex flex-wrap items-start justify-center gap-4 sm:gap-6 min-h-0 overflow-y-auto overscroll-contain\"\r\n >\r\n <!-- Month 1 (Left / Top) -->\r\n <div class=\"flex flex-col w-63 max-w-full\">\r\n <div\r\n class=\"flex items-center justify-between w-full pb-2 mb-2 border-b border-light-medium\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigatePrevious()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading transition-colors cursor-pointer\"\r\n aria-label=\"Tháng trước\"\r\n >\r\n <ol-icon name=\"
|
|
8157
|
+
template: "<!--\r\n Chặn chiều DỌC, đối xứng với `flex-wrap` lo chiều ngang bên dưới.\r\n\r\n Đo /datepicker ở 375×812 (range picker, dualMonth): khi hai lịch xuống dòng,\r\n popup cao **770px**, đáy ở 1257 — **vượt đáy màn 445px**. Tháng thứ hai và\r\n TOÀN BỘ footer (nút \"Xác nhận\") nằm ngoài màn, `overflow-y: visible` nên\r\n không cuộn tới được: ở mobile người dùng **không chốt nổi dải ngày**.\r\n\r\n Giới hạn theo `--ol-dp-max-height` — khoảng trống THỰC TẾ dưới ô nhập, do\r\n component đặt khi mở và khi cuộn/đổi cỡ. Không dùng `85svh`: đó là chiều cao\r\n MÀN, trong khi popup mở dưới ô nhập vốn đã nằm giữa màn — đo lại sau khi thử\r\n `85svh` vẫn còn vượt đáy 365px và footer vẫn khuất.\r\n\r\n `svh` trong giá trị dự phòng chứ không `vh`: trên di động `vh` tính theo khung\r\n khi thanh địa chỉ ĐÃ thu, nên footer vẫn chui xuống dưới thanh đó lúc mới mở.\r\n-->\r\n<div\r\n class=\"flex flex-wrap bg-neutral-primary-medium border border-default-medium rounded-base shadow-xl p-3.5 sm:p-5 select-none transition-all max-w-full\"\r\n [class]=\"wrapperClasses()\"\r\n>\r\n <!-- Optional Quick Presets Sidebar / Topbar on Mobile & Tablet -->\r\n @if (presets().length > 0) {\r\n <div\r\n class=\"flex flex-row md:flex-col flex-wrap md:flex-nowrap gap-1 pb-3 mb-3 border-b md:border-b-0 md:pb-0 md:mb-0 md:pr-4 md:mr-4 md:border-r border-light-medium md:min-w-32.5\"\r\n >\r\n <span\r\n class=\"w-full text-xs font-semibold text-fg-disabled uppercase tracking-wider mb-1 md:mb-2\"\r\n >\r\n Phím tắt\r\n </span>\r\n @for (preset of presets(); track preset.label) {\r\n <button\r\n type=\"button\"\r\n (click)=\"applyPreset(preset)\"\r\n class=\"text-left px-2.5 py-1 sm:py-1.5 text-xs font-medium text-body bg-neutral-tertiary md:bg-transparent hover:bg-neutral-tertiary-medium rounded-lg transition-colors cursor-pointer whitespace-nowrap\"\r\n >\r\n {{ preset.label }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Calendar Content -->\r\n @if (showTwoMonths() && state.viewMode() === 'day') {\r\n <!-- Dual-Month (Stacked vertically on mobile/tablet or narrow containers, side-by-side on wide containers) -->\r\n <!--\r\n `md:w-auto` chứ KHÔNG để `w-full` trần: cha là `flex-wrap`, nên `w-full`\r\n buộc khối lịch chiếm trọn dòng và bị đẩy XUỐNG DƯỚI cột preset thay vì\r\n nằm cạnh. Đo /datepicker ở 1278px (range picker, dualMonth + 4 preset):\r\n preset ở y=21 cao 152px rộng 130px, khối lịch ở **y=173** — xuống dòng,\r\n để trống 544px bên phải. Popup cần 554px trong khi chỗ trống hai phía\r\n chỉ 358px, nên `max-height` cắt mất hàng ngày cuối và footer.\r\n\r\n Giữ `w-full` ở màn hẹp: dưới `md` cột preset là hàng NGANG trên cùng\r\n (`flex-row` + `border-b`), lúc đó khối lịch phải chiếm trọn dòng dưới.\r\n Breakpoint khớp đúng chỗ preset đổi chiều.\r\n -->\r\n <div class=\"flex flex-col w-full md:w-auto {{ calendarBlockClasses() }}\">\r\n <!--\r\n `flex-wrap` chứ KHÔNG phải `sm:flex-row`: hai lịch mỗi cái `w-63` (252px)\r\n cứng, nên xếp ngang cần tối thiểu ~560px kể cả đệm. `sm:` bật từ 640px —\r\n đủ cho khung TRỐNG, nhưng nơi dùng thường có thanh bên. Đo /datepicker ở\r\n 768px: popup rộng 716px trong khi cha `max-w-md` chỉ 334px, mép phải\r\n 1093px — **54 nút ngày nằm ngoài màn, không bấm được**.\r\n\r\n Breakpoint đo BỀ RỘNG MÀN, không đo chỗ còn lại cho component. `flex-wrap`\r\n đo chỗ thật: đủ thì nằm ngang, không đủ thì tự xuống dòng.\r\n -->\r\n <!--\r\n Vùng LỊCH cuộn riêng, footer ở NGOÀI vùng này nên luôn thấy.\r\n\r\n Trước đây trần + `overflow-y-auto` áp cho cả wrapper ngoài, nên footer\r\n cuộn theo: đo /datepicker ở 1278px (dualMonth + 4 preset), nội dung cần\r\n 402px mà chỗ trống hai phía chỉ 358px — footer cao 49px có đáy ở 383\r\n trong khung 350, **tràn 33px**. Nút \"Xác nhận\" phải cuộn mới thấy.\r\n\r\n `min-h-0` là bắt buộc: con của flex mặc định `min-height: auto`, không\r\n co nhỏ hơn nội dung nên `overflow-y-auto` không bao giờ kích hoạt.\r\n -->\r\n <div\r\n class=\"flex flex-wrap items-start justify-center gap-4 sm:gap-6 min-h-0 overflow-y-auto overscroll-contain\"\r\n >\r\n <!-- Month 1 (Left / Top) -->\r\n <div class=\"flex flex-col w-63 max-w-full\">\r\n <div\r\n class=\"flex items-center justify-between w-full pb-2 mb-2 border-b border-light-medium\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigatePrevious()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading transition-colors cursor-pointer\"\r\n aria-label=\"Tháng trước\"\r\n >\r\n <ol-icon name=\"chevron_left\" size=\"sm\" />\r\n </button>\r\n <span class=\"text-sm font-semibold text-heading select-none\">\r\n {{ state.headerTitle() }}\r\n </span>\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigateNext()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading transition-colors cursor-pointer\"\r\n aria-label=\"Tháng sau\"\r\n >\r\n <ol-icon name=\"chevron_right\" size=\"sm\" />\r\n </button>\r\n </div>\r\n <ol-day-view\r\n [customMatrix]=\"state.dayMatrix()\"\r\n [fixedRows]=\"true\"\r\n (daySelect)=\"onDaySelected($event)\"\r\n />\r\n </div>\r\n\r\n <!-- Month 2 (Right / Bottom) -->\r\n <div class=\"flex flex-col w-63 max-w-full\">\r\n <div\r\n class=\"flex items-center justify-between w-full pb-2 mb-2 border-b border-light-medium\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigatePrevious()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading transition-colors cursor-pointer\"\r\n aria-label=\"Tháng trước\"\r\n >\r\n <ol-icon name=\"chevron_left\" size=\"sm\" />\r\n </button>\r\n <span class=\"text-sm font-semibold text-heading select-none\">\r\n {{ state.headerTitleNextMonth() }}\r\n </span>\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigateNext()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading transition-colors cursor-pointer\"\r\n aria-label=\"Tháng sau\"\r\n >\r\n <ol-icon name=\"chevron_right\" size=\"sm\" />\r\n </button>\r\n </div>\r\n <ol-day-view\r\n [customMatrix]=\"state.dayMatrixNextMonth()\"\r\n [fixedRows]=\"true\"\r\n (daySelect)=\"onDaySelected($event)\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <!-- Footer Spanning Calendars -->\r\n @if (showFooter()) {\r\n <ol-datepicker-footer\r\n (clearClick)=\"onFooterClear()\"\r\n (todayClick)=\"onFooterToday()\"\r\n (applyClick)=\"onFooterApply()\"\r\n (cancelClick)=\"onFooterCancel()\"\r\n />\r\n }\r\n </div>\r\n } @else {\r\n <!-- Single Calendar Content -->\r\n <div class=\"flex flex-col w-63 max-w-full mx-auto\">\r\n <!-- Header -->\r\n <ol-datepicker-header />\r\n\r\n <!-- Views Switcher -->\r\n @switch (state.viewMode()) {\r\n @case ('day') {\r\n <ol-day-view (daySelect)=\"onDaySelected($event)\" />\r\n }\r\n @case ('month') {\r\n <ol-month-view />\r\n }\r\n @case ('year') {\r\n <ol-year-view />\r\n }\r\n }\r\n\r\n <!-- Footer -->\r\n @if (showFooter()) {\r\n <ol-datepicker-footer\r\n (clearClick)=\"onFooterClear()\"\r\n (todayClick)=\"onFooterToday()\"\r\n (applyClick)=\"onFooterApply()\"\r\n (cancelClick)=\"onFooterCancel()\"\r\n />\r\n }\r\n </div>\r\n }\r\n</div>\r\n",
|
|
8082
8158
|
dependencies: [
|
|
8083
8159
|
{
|
|
8084
8160
|
kind: "ngmodule",
|
|
@@ -8153,7 +8229,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
8153
8229
|
OlIcon
|
|
8154
8230
|
],
|
|
8155
8231
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
8156
|
-
template: "<!--\r\n Chặn chiều DỌC, đối xứng với `flex-wrap` lo chiều ngang bên dưới.\r\n\r\n Đo /datepicker ở 375×812 (range picker, dualMonth): khi hai lịch xuống dòng,\r\n popup cao **770px**, đáy ở 1257 — **vượt đáy màn 445px**. Tháng thứ hai và\r\n TOÀN BỘ footer (nút \"Xác nhận\") nằm ngoài màn, `overflow-y: visible` nên\r\n không cuộn tới được: ở mobile người dùng **không chốt nổi dải ngày**.\r\n\r\n Giới hạn theo `--ol-dp-max-height` — khoảng trống THỰC TẾ dưới ô nhập, do\r\n component đặt khi mở và khi cuộn/đổi cỡ. Không dùng `85svh`: đó là chiều cao\r\n MÀN, trong khi popup mở dưới ô nhập vốn đã nằm giữa màn — đo lại sau khi thử\r\n `85svh` vẫn còn vượt đáy 365px và footer vẫn khuất.\r\n\r\n `svh` trong giá trị dự phòng chứ không `vh`: trên di động `vh` tính theo khung\r\n khi thanh địa chỉ ĐÃ thu, nên footer vẫn chui xuống dưới thanh đó lúc mới mở.\r\n-->\r\n<div\r\n class=\"flex flex-wrap bg-neutral-primary-medium border border-default-medium rounded-base shadow-xl p-3.5 sm:p-5 select-none transition-all max-w-full\"\r\n [class]=\"wrapperClasses()\"\r\n>\r\n <!-- Optional Quick Presets Sidebar / Topbar on Mobile & Tablet -->\r\n @if (presets().length > 0) {\r\n <div\r\n class=\"flex flex-row md:flex-col flex-wrap md:flex-nowrap gap-1 pb-3 mb-3 border-b md:border-b-0 md:pb-0 md:mb-0 md:pr-4 md:mr-4 md:border-r border-light-medium md:min-w-32.5\"\r\n >\r\n <span\r\n class=\"w-full text-xs font-semibold text-fg-disabled uppercase tracking-wider mb-1 md:mb-2\"\r\n >\r\n Phím tắt\r\n </span>\r\n @for (preset of presets(); track preset.label) {\r\n <button\r\n type=\"button\"\r\n (click)=\"applyPreset(preset)\"\r\n class=\"text-left px-2.5 py-1 sm:py-1.5 text-xs font-medium text-body bg-neutral-tertiary md:bg-transparent hover:bg-neutral-tertiary-medium rounded-lg transition-colors cursor-pointer whitespace-nowrap\"\r\n >\r\n {{ preset.label }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Calendar Content -->\r\n @if (showTwoMonths() && state.viewMode() === 'day') {\r\n <!-- Dual-Month (Stacked vertically on mobile/tablet or narrow containers, side-by-side on wide containers) -->\r\n <!--\r\n `md:w-auto` chứ KHÔNG để `w-full` trần: cha là `flex-wrap`, nên `w-full`\r\n buộc khối lịch chiếm trọn dòng và bị đẩy XUỐNG DƯỚI cột preset thay vì\r\n nằm cạnh. Đo /datepicker ở 1278px (range picker, dualMonth + 4 preset):\r\n preset ở y=21 cao 152px rộng 130px, khối lịch ở **y=173** — xuống dòng,\r\n để trống 544px bên phải. Popup cần 554px trong khi chỗ trống hai phía\r\n chỉ 358px, nên `max-height` cắt mất hàng ngày cuối và footer.\r\n\r\n Giữ `w-full` ở màn hẹp: dưới `md` cột preset là hàng NGANG trên cùng\r\n (`flex-row` + `border-b`), lúc đó khối lịch phải chiếm trọn dòng dưới.\r\n Breakpoint khớp đúng chỗ preset đổi chiều.\r\n -->\r\n <div class=\"flex flex-col w-full md:w-auto {{ calendarBlockClasses() }}\">\r\n <!--\r\n `flex-wrap` chứ KHÔNG phải `sm:flex-row`: hai lịch mỗi cái `w-63` (252px)\r\n cứng, nên xếp ngang cần tối thiểu ~560px kể cả đệm. `sm:` bật từ 640px —\r\n đủ cho khung TRỐNG, nhưng nơi dùng thường có thanh bên. Đo /datepicker ở\r\n 768px: popup rộng 716px trong khi cha `max-w-md` chỉ 334px, mép phải\r\n 1093px — **54 nút ngày nằm ngoài màn, không bấm được**.\r\n\r\n Breakpoint đo BỀ RỘNG MÀN, không đo chỗ còn lại cho component. `flex-wrap`\r\n đo chỗ thật: đủ thì nằm ngang, không đủ thì tự xuống dòng.\r\n -->\r\n <!--\r\n Vùng LỊCH cuộn riêng, footer ở NGOÀI vùng này nên luôn thấy.\r\n\r\n Trước đây trần + `overflow-y-auto` áp cho cả wrapper ngoài, nên footer\r\n cuộn theo: đo /datepicker ở 1278px (dualMonth + 4 preset), nội dung cần\r\n 402px mà chỗ trống hai phía chỉ 358px — footer cao 49px có đáy ở 383\r\n trong khung 350, **tràn 33px**. Nút \"Xác nhận\" phải cuộn mới thấy.\r\n\r\n `min-h-0` là bắt buộc: con của flex mặc định `min-height: auto`, không\r\n co nhỏ hơn nội dung nên `overflow-y-auto` không bao giờ kích hoạt.\r\n -->\r\n <div\r\n class=\"flex flex-wrap items-start justify-center gap-4 sm:gap-6 min-h-0 overflow-y-auto overscroll-contain\"\r\n >\r\n <!-- Month 1 (Left / Top) -->\r\n <div class=\"flex flex-col w-63 max-w-full\">\r\n <div\r\n class=\"flex items-center justify-between w-full pb-2 mb-2 border-b border-light-medium\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigatePrevious()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading transition-colors cursor-pointer\"\r\n aria-label=\"Tháng trước\"\r\n >\r\n <ol-icon name=\"
|
|
8232
|
+
template: "<!--\r\n Chặn chiều DỌC, đối xứng với `flex-wrap` lo chiều ngang bên dưới.\r\n\r\n Đo /datepicker ở 375×812 (range picker, dualMonth): khi hai lịch xuống dòng,\r\n popup cao **770px**, đáy ở 1257 — **vượt đáy màn 445px**. Tháng thứ hai và\r\n TOÀN BỘ footer (nút \"Xác nhận\") nằm ngoài màn, `overflow-y: visible` nên\r\n không cuộn tới được: ở mobile người dùng **không chốt nổi dải ngày**.\r\n\r\n Giới hạn theo `--ol-dp-max-height` — khoảng trống THỰC TẾ dưới ô nhập, do\r\n component đặt khi mở và khi cuộn/đổi cỡ. Không dùng `85svh`: đó là chiều cao\r\n MÀN, trong khi popup mở dưới ô nhập vốn đã nằm giữa màn — đo lại sau khi thử\r\n `85svh` vẫn còn vượt đáy 365px và footer vẫn khuất.\r\n\r\n `svh` trong giá trị dự phòng chứ không `vh`: trên di động `vh` tính theo khung\r\n khi thanh địa chỉ ĐÃ thu, nên footer vẫn chui xuống dưới thanh đó lúc mới mở.\r\n-->\r\n<div\r\n class=\"flex flex-wrap bg-neutral-primary-medium border border-default-medium rounded-base shadow-xl p-3.5 sm:p-5 select-none transition-all max-w-full\"\r\n [class]=\"wrapperClasses()\"\r\n>\r\n <!-- Optional Quick Presets Sidebar / Topbar on Mobile & Tablet -->\r\n @if (presets().length > 0) {\r\n <div\r\n class=\"flex flex-row md:flex-col flex-wrap md:flex-nowrap gap-1 pb-3 mb-3 border-b md:border-b-0 md:pb-0 md:mb-0 md:pr-4 md:mr-4 md:border-r border-light-medium md:min-w-32.5\"\r\n >\r\n <span\r\n class=\"w-full text-xs font-semibold text-fg-disabled uppercase tracking-wider mb-1 md:mb-2\"\r\n >\r\n Phím tắt\r\n </span>\r\n @for (preset of presets(); track preset.label) {\r\n <button\r\n type=\"button\"\r\n (click)=\"applyPreset(preset)\"\r\n class=\"text-left px-2.5 py-1 sm:py-1.5 text-xs font-medium text-body bg-neutral-tertiary md:bg-transparent hover:bg-neutral-tertiary-medium rounded-lg transition-colors cursor-pointer whitespace-nowrap\"\r\n >\r\n {{ preset.label }}\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Calendar Content -->\r\n @if (showTwoMonths() && state.viewMode() === 'day') {\r\n <!-- Dual-Month (Stacked vertically on mobile/tablet or narrow containers, side-by-side on wide containers) -->\r\n <!--\r\n `md:w-auto` chứ KHÔNG để `w-full` trần: cha là `flex-wrap`, nên `w-full`\r\n buộc khối lịch chiếm trọn dòng và bị đẩy XUỐNG DƯỚI cột preset thay vì\r\n nằm cạnh. Đo /datepicker ở 1278px (range picker, dualMonth + 4 preset):\r\n preset ở y=21 cao 152px rộng 130px, khối lịch ở **y=173** — xuống dòng,\r\n để trống 544px bên phải. Popup cần 554px trong khi chỗ trống hai phía\r\n chỉ 358px, nên `max-height` cắt mất hàng ngày cuối và footer.\r\n\r\n Giữ `w-full` ở màn hẹp: dưới `md` cột preset là hàng NGANG trên cùng\r\n (`flex-row` + `border-b`), lúc đó khối lịch phải chiếm trọn dòng dưới.\r\n Breakpoint khớp đúng chỗ preset đổi chiều.\r\n -->\r\n <div class=\"flex flex-col w-full md:w-auto {{ calendarBlockClasses() }}\">\r\n <!--\r\n `flex-wrap` chứ KHÔNG phải `sm:flex-row`: hai lịch mỗi cái `w-63` (252px)\r\n cứng, nên xếp ngang cần tối thiểu ~560px kể cả đệm. `sm:` bật từ 640px —\r\n đủ cho khung TRỐNG, nhưng nơi dùng thường có thanh bên. Đo /datepicker ở\r\n 768px: popup rộng 716px trong khi cha `max-w-md` chỉ 334px, mép phải\r\n 1093px — **54 nút ngày nằm ngoài màn, không bấm được**.\r\n\r\n Breakpoint đo BỀ RỘNG MÀN, không đo chỗ còn lại cho component. `flex-wrap`\r\n đo chỗ thật: đủ thì nằm ngang, không đủ thì tự xuống dòng.\r\n -->\r\n <!--\r\n Vùng LỊCH cuộn riêng, footer ở NGOÀI vùng này nên luôn thấy.\r\n\r\n Trước đây trần + `overflow-y-auto` áp cho cả wrapper ngoài, nên footer\r\n cuộn theo: đo /datepicker ở 1278px (dualMonth + 4 preset), nội dung cần\r\n 402px mà chỗ trống hai phía chỉ 358px — footer cao 49px có đáy ở 383\r\n trong khung 350, **tràn 33px**. Nút \"Xác nhận\" phải cuộn mới thấy.\r\n\r\n `min-h-0` là bắt buộc: con của flex mặc định `min-height: auto`, không\r\n co nhỏ hơn nội dung nên `overflow-y-auto` không bao giờ kích hoạt.\r\n -->\r\n <div\r\n class=\"flex flex-wrap items-start justify-center gap-4 sm:gap-6 min-h-0 overflow-y-auto overscroll-contain\"\r\n >\r\n <!-- Month 1 (Left / Top) -->\r\n <div class=\"flex flex-col w-63 max-w-full\">\r\n <div\r\n class=\"flex items-center justify-between w-full pb-2 mb-2 border-b border-light-medium\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigatePrevious()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading transition-colors cursor-pointer\"\r\n aria-label=\"Tháng trước\"\r\n >\r\n <ol-icon name=\"chevron_left\" size=\"sm\" />\r\n </button>\r\n <span class=\"text-sm font-semibold text-heading select-none\">\r\n {{ state.headerTitle() }}\r\n </span>\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigateNext()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading transition-colors cursor-pointer\"\r\n aria-label=\"Tháng sau\"\r\n >\r\n <ol-icon name=\"chevron_right\" size=\"sm\" />\r\n </button>\r\n </div>\r\n <ol-day-view\r\n [customMatrix]=\"state.dayMatrix()\"\r\n [fixedRows]=\"true\"\r\n (daySelect)=\"onDaySelected($event)\"\r\n />\r\n </div>\r\n\r\n <!-- Month 2 (Right / Bottom) -->\r\n <div class=\"flex flex-col w-63 max-w-full\">\r\n <div\r\n class=\"flex items-center justify-between w-full pb-2 mb-2 border-b border-light-medium\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigatePrevious()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading transition-colors cursor-pointer\"\r\n aria-label=\"Tháng trước\"\r\n >\r\n <ol-icon name=\"chevron_left\" size=\"sm\" />\r\n </button>\r\n <span class=\"text-sm font-semibold text-heading select-none\">\r\n {{ state.headerTitleNextMonth() }}\r\n </span>\r\n <button\r\n type=\"button\"\r\n (click)=\"state.navigateNext()\"\r\n class=\"inline-flex items-center justify-center w-8 h-8 text-body-subtle rounded-lg hover:bg-neutral-tertiary-medium hover:text-heading transition-colors cursor-pointer\"\r\n aria-label=\"Tháng sau\"\r\n >\r\n <ol-icon name=\"chevron_right\" size=\"sm\" />\r\n </button>\r\n </div>\r\n <ol-day-view\r\n [customMatrix]=\"state.dayMatrixNextMonth()\"\r\n [fixedRows]=\"true\"\r\n (daySelect)=\"onDaySelected($event)\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <!-- Footer Spanning Calendars -->\r\n @if (showFooter()) {\r\n <ol-datepicker-footer\r\n (clearClick)=\"onFooterClear()\"\r\n (todayClick)=\"onFooterToday()\"\r\n (applyClick)=\"onFooterApply()\"\r\n (cancelClick)=\"onFooterCancel()\"\r\n />\r\n }\r\n </div>\r\n } @else {\r\n <!-- Single Calendar Content -->\r\n <div class=\"flex flex-col w-63 max-w-full mx-auto\">\r\n <!-- Header -->\r\n <ol-datepicker-header />\r\n\r\n <!-- Views Switcher -->\r\n @switch (state.viewMode()) {\r\n @case ('day') {\r\n <ol-day-view (daySelect)=\"onDaySelected($event)\" />\r\n }\r\n @case ('month') {\r\n <ol-month-view />\r\n }\r\n @case ('year') {\r\n <ol-year-view />\r\n }\r\n }\r\n\r\n <!-- Footer -->\r\n @if (showFooter()) {\r\n <ol-datepicker-footer\r\n (clearClick)=\"onFooterClear()\"\r\n (todayClick)=\"onFooterToday()\"\r\n (applyClick)=\"onFooterApply()\"\r\n (cancelClick)=\"onFooterCancel()\"\r\n />\r\n }\r\n </div>\r\n }\r\n</div>\r\n"
|
|
8157
8233
|
}]
|
|
8158
8234
|
}],
|
|
8159
8235
|
ctorParameters: () => [],
|
|
@@ -8243,7 +8319,7 @@ function trackPopupMaxHeight(anchorEl, panelEl) {
|
|
|
8243
8319
|
let uniqueDatepickerId = 0;
|
|
8244
8320
|
var OlDatepicker = class OlDatepicker {
|
|
8245
8321
|
pickerState = inject(OlDatepickerStateService);
|
|
8246
|
-
hostEl
|
|
8322
|
+
hostEl;
|
|
8247
8323
|
id = input("", ...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
|
|
8248
8324
|
generatedId = `ol-datepicker-${++uniqueDatepickerId}`;
|
|
8249
8325
|
inputId = computed(() => this.id() || this.generatedId, ...ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []);
|
|
@@ -8277,7 +8353,7 @@ var OlDatepicker = class OlDatepicker {
|
|
|
8277
8353
|
isoChange = output();
|
|
8278
8354
|
formattedChange = output();
|
|
8279
8355
|
panel = viewChild("panel", ...ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []);
|
|
8280
|
-
overlayContainer
|
|
8356
|
+
overlayContainer;
|
|
8281
8357
|
detachPanel = null;
|
|
8282
8358
|
releaseEscape = null;
|
|
8283
8359
|
stopTrackingHeight = null;
|
|
@@ -8293,6 +8369,8 @@ var OlDatepicker = class OlDatepicker {
|
|
|
8293
8369
|
this.formattedChange.emit(d ? formatDate(d, this.format()) : "");
|
|
8294
8370
|
}
|
|
8295
8371
|
constructor() {
|
|
8372
|
+
this.hostEl = inject(ElementRef);
|
|
8373
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
8296
8374
|
effect(() => {
|
|
8297
8375
|
const open = this.isOpen();
|
|
8298
8376
|
const el = this.panel()?.nativeElement;
|
|
@@ -8616,11 +8694,18 @@ var OlDatepicker = class OlDatepicker {
|
|
|
8616
8694
|
},
|
|
8617
8695
|
classAttribute: "relative"
|
|
8618
8696
|
},
|
|
8619
|
-
providers: [
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8697
|
+
providers: [
|
|
8698
|
+
OlDatepickerStateService,
|
|
8699
|
+
{
|
|
8700
|
+
provide: NG_VALUE_ACCESSOR,
|
|
8701
|
+
useExisting: forwardRef(() => OlDatepicker),
|
|
8702
|
+
multi: true
|
|
8703
|
+
},
|
|
8704
|
+
provideOlIcons({
|
|
8705
|
+
"calendar": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M2 12C2 8.22876 2 6.34315 3.17157 5.17157C4.34315 4 6.22876 4 10 4H14C17.7712 4 19.6569 4 20.8284 5.17157C22 6.34315 22 8.22876 22 12V14C22 17.7712 22 19.6569 20.8284 20.8284C19.6569 22 17.7712 22 14 22H10C6.22876 22 4.34315 22 3.17157 20.8284C2 19.6569 2 17.7712 2 14V12Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M7 4V2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M17 4V2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M2.5 9H21.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M18 17C18 17.5523 17.5523 18 17 18C16.4477 18 16 17.5523 16 17C16 16.4477 16.4477 16 17 16C17.5523 16 18 16.4477 18 17Z\" fill=\"currentColor\"/> <path d=\"M18 13C18 13.5523 17.5523 14 17 14C16.4477 14 16 13.5523 16 13C16 12.4477 16.4477 12 17 12C17.5523 12 18 12.4477 18 13Z\" fill=\"currentColor\"/> <path d=\"M13 17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16C12.5523 16 13 16.4477 13 17Z\" fill=\"currentColor\"/> <path d=\"M13 13C13 13.5523 12.5523 14 12 14C11.4477 14 11 13.5523 11 13C11 12.4477 11.4477 12 12 12C12.5523 12 13 12.4477 13 13Z\" fill=\"currentColor\"/> <path d=\"M8 17C8 17.5523 7.55228 18 7 18C6.44772 18 6 17.5523 6 17C6 16.4477 6.44772 16 7 16C7.55228 16 8 16.4477 8 17Z\" fill=\"currentColor\"/> <path d=\"M8 13C8 13.5523 7.55228 14 7 14C6.44772 14 6 13.5523 6 13C6 12.4477 6.44772 12 7 12C7.55228 12 8 12.4477 8 13Z\" fill=\"currentColor\"/> </svg>",
|
|
8706
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>"
|
|
8707
|
+
})
|
|
8708
|
+
],
|
|
8624
8709
|
viewQueries: [{
|
|
8625
8710
|
propertyName: "panel",
|
|
8626
8711
|
first: true,
|
|
@@ -8725,11 +8810,18 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
8725
8810
|
OlFieldFrame,
|
|
8726
8811
|
OlFieldLabel
|
|
8727
8812
|
],
|
|
8728
|
-
providers: [
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8813
|
+
providers: [
|
|
8814
|
+
OlDatepickerStateService,
|
|
8815
|
+
{
|
|
8816
|
+
provide: NG_VALUE_ACCESSOR,
|
|
8817
|
+
useExisting: forwardRef(() => OlDatepicker),
|
|
8818
|
+
multi: true
|
|
8819
|
+
},
|
|
8820
|
+
provideOlIcons({
|
|
8821
|
+
"calendar": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M2 12C2 8.22876 2 6.34315 3.17157 5.17157C4.34315 4 6.22876 4 10 4H14C17.7712 4 19.6569 4 20.8284 5.17157C22 6.34315 22 8.22876 22 12V14C22 17.7712 22 19.6569 20.8284 20.8284C19.6569 22 17.7712 22 14 22H10C6.22876 22 4.34315 22 3.17157 20.8284C2 19.6569 2 17.7712 2 14V12Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M7 4V2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M17 4V2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M2.5 9H21.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M18 17C18 17.5523 17.5523 18 17 18C16.4477 18 16 17.5523 16 17C16 16.4477 16.4477 16 17 16C17.5523 16 18 16.4477 18 17Z\" fill=\"currentColor\"/> <path d=\"M18 13C18 13.5523 17.5523 14 17 14C16.4477 14 16 13.5523 16 13C16 12.4477 16.4477 12 17 12C17.5523 12 18 12.4477 18 13Z\" fill=\"currentColor\"/> <path d=\"M13 17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16C12.5523 16 13 16.4477 13 17Z\" fill=\"currentColor\"/> <path d=\"M13 13C13 13.5523 12.5523 14 12 14C11.4477 14 11 13.5523 11 13C11 12.4477 11.4477 12 12 12C12.5523 12 13 12.4477 13 13Z\" fill=\"currentColor\"/> <path d=\"M8 17C8 17.5523 7.55228 18 7 18C6.44772 18 6 17.5523 6 17C6 16.4477 6.44772 16 7 16C7.55228 16 8 16.4477 8 17Z\" fill=\"currentColor\"/> <path d=\"M8 13C8 13.5523 7.55228 14 7 14C6.44772 14 6 13.5523 6 13C6 12.4477 6.44772 12 7 12C7.55228 12 8 12.4477 8 13Z\" fill=\"currentColor\"/> </svg>",
|
|
8822
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>"
|
|
8823
|
+
})
|
|
8824
|
+
],
|
|
8733
8825
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
8734
8826
|
host: {
|
|
8735
8827
|
class: "relative",
|
|
@@ -8970,7 +9062,7 @@ const JOINED_TEXT_PAD = {
|
|
|
8970
9062
|
};
|
|
8971
9063
|
var OlDateRangePicker = class OlDateRangePicker {
|
|
8972
9064
|
pickerState = inject(OlDatepickerStateService);
|
|
8973
|
-
hostEl
|
|
9065
|
+
hostEl;
|
|
8974
9066
|
value = model(null, ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
|
|
8975
9067
|
startPlaceholder = input("Ngày bắt đầu", ...ngDevMode ? [{ debugName: "startPlaceholder" }] : /* istanbul ignore next */ []);
|
|
8976
9068
|
endPlaceholder = input("Ngày kết thúc", ...ngDevMode ? [{ debugName: "endPlaceholder" }] : /* istanbul ignore next */ []);
|
|
@@ -9009,7 +9101,7 @@ var OlDateRangePicker = class OlDateRangePicker {
|
|
|
9009
9101
|
clearable = input(true, ...ngDevMode ? [{ debugName: "clearable" }] : /* istanbul ignore next */ []);
|
|
9010
9102
|
rangeChange = output();
|
|
9011
9103
|
panel = viewChild("panel", ...ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []);
|
|
9012
|
-
overlayContainer
|
|
9104
|
+
overlayContainer;
|
|
9013
9105
|
detachPanel = null;
|
|
9014
9106
|
releaseEscape = null;
|
|
9015
9107
|
stopTrackingHeight = null;
|
|
@@ -9020,6 +9112,8 @@ var OlDateRangePicker = class OlDateRangePicker {
|
|
|
9020
9112
|
onChange = () => {};
|
|
9021
9113
|
onTouched = () => {};
|
|
9022
9114
|
constructor() {
|
|
9115
|
+
this.hostEl = inject(ElementRef);
|
|
9116
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
9023
9117
|
effect(() => {
|
|
9024
9118
|
const open = this.isOpen();
|
|
9025
9119
|
const el = this.panel()?.nativeElement;
|
|
@@ -9440,11 +9534,18 @@ var OlDateRangePicker = class OlDateRangePicker {
|
|
|
9440
9534
|
properties: { "class.z-dropdown": "isOpen()" },
|
|
9441
9535
|
classAttribute: "relative inline-block"
|
|
9442
9536
|
},
|
|
9443
|
-
providers: [
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9537
|
+
providers: [
|
|
9538
|
+
OlDatepickerStateService,
|
|
9539
|
+
{
|
|
9540
|
+
provide: NG_VALUE_ACCESSOR,
|
|
9541
|
+
useExisting: forwardRef(() => OlDateRangePicker),
|
|
9542
|
+
multi: true
|
|
9543
|
+
},
|
|
9544
|
+
provideOlIcons({
|
|
9545
|
+
"calendar": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M2 12C2 8.22876 2 6.34315 3.17157 5.17157C4.34315 4 6.22876 4 10 4H14C17.7712 4 19.6569 4 20.8284 5.17157C22 6.34315 22 8.22876 22 12V14C22 17.7712 22 19.6569 20.8284 20.8284C19.6569 22 17.7712 22 14 22H10C6.22876 22 4.34315 22 3.17157 20.8284C2 19.6569 2 17.7712 2 14V12Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M7 4V2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M17 4V2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M2.5 9H21.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M18 17C18 17.5523 17.5523 18 17 18C16.4477 18 16 17.5523 16 17C16 16.4477 16.4477 16 17 16C17.5523 16 18 16.4477 18 17Z\" fill=\"currentColor\"/> <path d=\"M18 13C18 13.5523 17.5523 14 17 14C16.4477 14 16 13.5523 16 13C16 12.4477 16.4477 12 17 12C17.5523 12 18 12.4477 18 13Z\" fill=\"currentColor\"/> <path d=\"M13 17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16C12.5523 16 13 16.4477 13 17Z\" fill=\"currentColor\"/> <path d=\"M13 13C13 13.5523 12.5523 14 12 14C11.4477 14 11 13.5523 11 13C11 12.4477 11.4477 12 12 12C12.5523 12 13 12.4477 13 13Z\" fill=\"currentColor\"/> <path d=\"M8 17C8 17.5523 7.55228 18 7 18C6.44772 18 6 17.5523 6 17C6 16.4477 6.44772 16 7 16C7.55228 16 8 16.4477 8 17Z\" fill=\"currentColor\"/> <path d=\"M8 13C8 13.5523 7.55228 14 7 14C6.44772 14 6 13.5523 6 13C6 12.4477 6.44772 12 7 12C7.55228 12 8 12.4477 8 13Z\" fill=\"currentColor\"/> </svg>",
|
|
9546
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>"
|
|
9547
|
+
})
|
|
9548
|
+
],
|
|
9448
9549
|
viewQueries: [{
|
|
9449
9550
|
propertyName: "panel",
|
|
9450
9551
|
first: true,
|
|
@@ -9549,11 +9650,18 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
9549
9650
|
OlFieldFrame,
|
|
9550
9651
|
OlFieldLabel
|
|
9551
9652
|
],
|
|
9552
|
-
providers: [
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9653
|
+
providers: [
|
|
9654
|
+
OlDatepickerStateService,
|
|
9655
|
+
{
|
|
9656
|
+
provide: NG_VALUE_ACCESSOR,
|
|
9657
|
+
useExisting: forwardRef(() => OlDateRangePicker),
|
|
9658
|
+
multi: true
|
|
9659
|
+
},
|
|
9660
|
+
provideOlIcons({
|
|
9661
|
+
"calendar": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M2 12C2 8.22876 2 6.34315 3.17157 5.17157C4.34315 4 6.22876 4 10 4H14C17.7712 4 19.6569 4 20.8284 5.17157C22 6.34315 22 8.22876 22 12V14C22 17.7712 22 19.6569 20.8284 20.8284C19.6569 22 17.7712 22 14 22H10C6.22876 22 4.34315 22 3.17157 20.8284C2 19.6569 2 17.7712 2 14V12Z\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M7 4V2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M17 4V2.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M2.5 9H21.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <path d=\"M18 17C18 17.5523 17.5523 18 17 18C16.4477 18 16 17.5523 16 17C16 16.4477 16.4477 16 17 16C17.5523 16 18 16.4477 18 17Z\" fill=\"currentColor\"/> <path d=\"M18 13C18 13.5523 17.5523 14 17 14C16.4477 14 16 13.5523 16 13C16 12.4477 16.4477 12 17 12C17.5523 12 18 12.4477 18 13Z\" fill=\"currentColor\"/> <path d=\"M13 17C13 17.5523 12.5523 18 12 18C11.4477 18 11 17.5523 11 17C11 16.4477 11.4477 16 12 16C12.5523 16 13 16.4477 13 17Z\" fill=\"currentColor\"/> <path d=\"M13 13C13 13.5523 12.5523 14 12 14C11.4477 14 11 13.5523 11 13C11 12.4477 11.4477 12 12 12C12.5523 12 13 12.4477 13 13Z\" fill=\"currentColor\"/> <path d=\"M8 17C8 17.5523 7.55228 18 7 18C6.44772 18 6 17.5523 6 17C6 16.4477 6.44772 16 7 16C7.55228 16 8 16.4477 8 17Z\" fill=\"currentColor\"/> <path d=\"M8 13C8 13.5523 7.55228 14 7 14C6.44772 14 6 13.5523 6 13C6 12.4477 6.44772 12 7 12C7.55228 12 8 12.4477 8 13Z\" fill=\"currentColor\"/> </svg>",
|
|
9662
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>"
|
|
9663
|
+
})
|
|
9664
|
+
],
|
|
9557
9665
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
9558
9666
|
host: {
|
|
9559
9667
|
class: "relative inline-block",
|
|
@@ -9813,8 +9921,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
9813
9921
|
}
|
|
9814
9922
|
});
|
|
9815
9923
|
var OlDatepickerDirective = class OlDatepickerDirective {
|
|
9816
|
-
|
|
9817
|
-
|
|
9924
|
+
constructor() {
|
|
9925
|
+
this.el = inject(ElementRef);
|
|
9926
|
+
this.vcr = inject(ViewContainerRef);
|
|
9927
|
+
}
|
|
9928
|
+
el;
|
|
9929
|
+
vcr;
|
|
9818
9930
|
state = inject(OlDatepickerStateService);
|
|
9819
9931
|
format = input("DD/MM/YYYY", ...ngDevMode ? [{ debugName: "format" }] : /* istanbul ignore next */ []);
|
|
9820
9932
|
minDate = input(null, ...ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []);
|
|
@@ -9926,6 +10038,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
9926
10038
|
exportAs: "olDatepicker"
|
|
9927
10039
|
}]
|
|
9928
10040
|
}],
|
|
10041
|
+
ctorParameters: () => [],
|
|
9929
10042
|
propDecorators: {
|
|
9930
10043
|
format: [{
|
|
9931
10044
|
type: i0.Input,
|
|
@@ -10034,6 +10147,7 @@ var OlDrawerHeader = class OlDrawerHeader {
|
|
|
10034
10147
|
},
|
|
10035
10148
|
outputs: { closeClick: "closeClick" },
|
|
10036
10149
|
host: { classAttribute: "block" },
|
|
10150
|
+
providers: [provideOlIcons({ "lucide:x": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>" })],
|
|
10037
10151
|
ngImport: i0,
|
|
10038
10152
|
template: `
|
|
10039
10153
|
<!--
|
|
@@ -10083,7 +10197,7 @@ var OlDrawerHeader = class OlDrawerHeader {
|
|
|
10083
10197
|
aria-label="Đóng"
|
|
10084
10198
|
class="inline-flex h-8 w-8 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"
|
|
10085
10199
|
>
|
|
10086
|
-
<ol-icon name="
|
|
10200
|
+
<ol-icon name="lucide:x" size="xs" />
|
|
10087
10201
|
<span class="sr-only">Đóng</span>
|
|
10088
10202
|
</button>
|
|
10089
10203
|
}
|
|
@@ -10174,14 +10288,15 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10174
10288
|
aria-label="Đóng"
|
|
10175
10289
|
class="inline-flex h-8 w-8 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"
|
|
10176
10290
|
>
|
|
10177
|
-
<ol-icon name="
|
|
10291
|
+
<ol-icon name="lucide:x" size="xs" />
|
|
10178
10292
|
<span class="sr-only">Đóng</span>
|
|
10179
10293
|
</button>
|
|
10180
10294
|
}
|
|
10181
10295
|
</div>
|
|
10182
10296
|
`,
|
|
10183
10297
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10184
|
-
host: { class: "block" }
|
|
10298
|
+
host: { class: "block" },
|
|
10299
|
+
providers: [provideOlIcons({ "lucide:x": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>" })]
|
|
10185
10300
|
}]
|
|
10186
10301
|
}],
|
|
10187
10302
|
propDecorators: {
|
|
@@ -10240,9 +10355,9 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10240
10355
|
}
|
|
10241
10356
|
});
|
|
10242
10357
|
var OlDrawer = class OlDrawer {
|
|
10243
|
-
focusTrap
|
|
10244
|
-
overlayContainer
|
|
10245
|
-
destroyRef
|
|
10358
|
+
focusTrap;
|
|
10359
|
+
overlayContainer;
|
|
10360
|
+
destroyRef;
|
|
10246
10361
|
panel = viewChild("panel", ...ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []);
|
|
10247
10362
|
isOpen = model(false, ...ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []);
|
|
10248
10363
|
placement = input("left", ...ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []);
|
|
@@ -10256,6 +10371,9 @@ var OlDrawer = class OlDrawer {
|
|
|
10256
10371
|
ariaLabel = input("Ngăn kéo", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
|
|
10257
10372
|
closed = output();
|
|
10258
10373
|
constructor() {
|
|
10374
|
+
this.focusTrap = inject(OlFocusTrapService);
|
|
10375
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
10376
|
+
this.destroyRef = inject(DestroyRef);
|
|
10259
10377
|
effect((onCleanup) => {
|
|
10260
10378
|
if (!this.isOpen() || !this.closeOnEscape()) return;
|
|
10261
10379
|
onCleanup(this.overlayContainer.register(() => this.close()));
|
|
@@ -10396,6 +10514,7 @@ var OlDrawer = class OlDrawer {
|
|
|
10396
10514
|
isOpen: "isOpenChange",
|
|
10397
10515
|
closed: "closed"
|
|
10398
10516
|
},
|
|
10517
|
+
providers: [provideOlIcons({ "close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>" })],
|
|
10399
10518
|
queries: [{
|
|
10400
10519
|
propertyName: "header",
|
|
10401
10520
|
first: true,
|
|
@@ -10439,6 +10558,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10439
10558
|
standalone: true,
|
|
10440
10559
|
imports: [OlIcon],
|
|
10441
10560
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10561
|
+
providers: [provideOlIcons({ "close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>" })],
|
|
10442
10562
|
template: "@if (isOpen()) {\r\n @if (backdrop()) {\r\n <div\r\n (click)=\"onBackdropClick()\"\r\n class=\"fixed inset-0 z-drawer-backdrop bg-overlay-backdrop\"\r\n aria-hidden=\"true\"\r\n ></div>\r\n }\r\n <div\r\n #panel\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n [attr.aria-label]=\"ariaLabel()\"\r\n tabindex=\"-1\"\r\n class=\"fixed z-drawer flex flex-col overflow-y-auto bg-neutral-primary border-default p-3 transition-transform duration-300 ease-out\"\r\n [class]=\"panelClasses()\"\r\n >\r\n @if (hienNutDong()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"close()\"\r\n data-ol-dismiss\r\n aria-label=\"Đóng\"\r\n class=\"absolute top-2.5 end-2.5 inline-flex h-8 w-8 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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n <span class=\"sr-only\">Đóng</span>\r\n </button>\r\n }\r\n <ng-content />\r\n </div>\r\n}\r\n"
|
|
10443
10563
|
}]
|
|
10444
10564
|
}],
|
|
@@ -10600,9 +10720,9 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10600
10720
|
let nextDropdownId = 0;
|
|
10601
10721
|
const HOVER_HIDE_DELAY_MS = 150;
|
|
10602
10722
|
var OlDropdown = class OlDropdown {
|
|
10603
|
-
elementRef
|
|
10604
|
-
destroyRef
|
|
10605
|
-
positioning
|
|
10723
|
+
elementRef;
|
|
10724
|
+
destroyRef;
|
|
10725
|
+
positioning;
|
|
10606
10726
|
uid = nextDropdownId++;
|
|
10607
10727
|
buttonId = `ol-dropdown-button-${this.uid}`;
|
|
10608
10728
|
menuId = `ol-dropdown-menu-${this.uid}`;
|
|
@@ -10610,7 +10730,7 @@ var OlDropdown = class OlDropdown {
|
|
|
10610
10730
|
menuRef = viewChild("menu", ...ngDevMode ? [{ debugName: "menuRef" }] : /* istanbul ignore next */ []);
|
|
10611
10731
|
cursorAnchorRef = viewChild("cursorAnchor", ...ngDevMode ? [{ debugName: "cursorAnchorRef" }] : /* istanbul ignore next */ []);
|
|
10612
10732
|
hideTimeoutId = null;
|
|
10613
|
-
overlayContainer
|
|
10733
|
+
overlayContainer;
|
|
10614
10734
|
cleanupAutoUpdate = null;
|
|
10615
10735
|
detachMenu = null;
|
|
10616
10736
|
releaseEscape = null;
|
|
@@ -10628,6 +10748,10 @@ var OlDropdown = class OlDropdown {
|
|
|
10628
10748
|
opened = output();
|
|
10629
10749
|
closed = output();
|
|
10630
10750
|
constructor() {
|
|
10751
|
+
this.elementRef = inject(ElementRef);
|
|
10752
|
+
this.destroyRef = inject(DestroyRef);
|
|
10753
|
+
this.positioning = inject(OlPositioningService);
|
|
10754
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
10631
10755
|
effect(() => {
|
|
10632
10756
|
const open = this.isOpen();
|
|
10633
10757
|
const reference = this.anchorElement();
|
|
@@ -10886,6 +11010,7 @@ var OlDropdown = class OlDropdown {
|
|
|
10886
11010
|
listeners: { "document:click": "onDocumentClick($event)" },
|
|
10887
11011
|
classAttribute: "relative inline-block text-left"
|
|
10888
11012
|
},
|
|
11013
|
+
providers: [provideOlIcons({ "chevron-down": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m6 9l6 6l6-6\"/></svg>" })],
|
|
10889
11014
|
viewQueries: [
|
|
10890
11015
|
{
|
|
10891
11016
|
propertyName: "triggerButtonRef",
|
|
@@ -10910,7 +11035,7 @@ var OlDropdown = class OlDropdown {
|
|
|
10910
11035
|
}
|
|
10911
11036
|
],
|
|
10912
11037
|
ngImport: i0,
|
|
10913
|
-
template: "<div\r\n (mouseenter)=\"onMouseEnter()\"\r\n (mouseleave)=\"onMouseLeave()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <!-- Custom Slot Trigger or Default Button -->\r\n <div (click)=\"onTriggerClick()\" (keydown)=\"onTriggerKeydown($event)\">\r\n <ng-content select=\"[slot=trigger], [trigger]\" />\r\n @if (label()) {\r\n <button\r\n #triggerButton\r\n type=\"button\"\r\n [id]=\"buttonId\"\r\n [disabled]=\"disabled()\"\r\n aria-haspopup=\"true\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"menuId\"\r\n [class]=\"\r\n triggerClass()\r\n ? 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium cursor-pointer ' +\r\n triggerClass()\r\n : 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-body bg-neutral-primary border border-default rounded-md hover:bg-neutral-tertiary focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed'\r\n \"\r\n >\r\n <ng-content select=\"[triggerIcon]\" />\r\n <span>{{ label() }}</span>\r\n <ol-icon\r\n name=\"
|
|
11038
|
+
template: "<div\r\n (mouseenter)=\"onMouseEnter()\"\r\n (mouseleave)=\"onMouseLeave()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <!-- Custom Slot Trigger or Default Button -->\r\n <div (click)=\"onTriggerClick()\" (keydown)=\"onTriggerKeydown($event)\">\r\n <ng-content select=\"[slot=trigger], [trigger]\" />\r\n @if (label()) {\r\n <button\r\n #triggerButton\r\n type=\"button\"\r\n [id]=\"buttonId\"\r\n [disabled]=\"disabled()\"\r\n aria-haspopup=\"true\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"menuId\"\r\n [class]=\"\r\n triggerClass()\r\n ? 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium cursor-pointer ' +\r\n triggerClass()\r\n : 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-body bg-neutral-primary border border-default rounded-md hover:bg-neutral-tertiary focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed'\r\n \"\r\n >\r\n <ng-content select=\"[triggerIcon]\" />\r\n <span>{{ label() }}</span>\r\n <ol-icon\r\n name=\"chevron_down\"\r\n size=\"xs\"\r\n class=\"transition-transform duration-200\"\r\n [class.rotate-180]=\"isOpen()\"\r\n ></ol-icon>\r\n </button>\r\n } @else if (!label() && triggerClass()) {\r\n <button\r\n #triggerButton\r\n type=\"button\"\r\n [id]=\"buttonId\"\r\n [disabled]=\"disabled()\"\r\n aria-haspopup=\"true\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"menuId\"\r\n [class]=\"'inline-flex items-center justify-center p-2.5 cursor-pointer ' + triggerClass()\"\r\n >\r\n <ng-content select=\"[triggerIcon]\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (trigger() === 'context') {\r\n <span\r\n #cursorAnchor\r\n aria-hidden=\"true\"\r\n class=\"pointer-events-none fixed h-0 w-0\"\r\n [style.left.px]=\"cursorX()\"\r\n [style.top.px]=\"cursorY()\"\r\n ></span>\r\n }\r\n\r\n <div\r\n #menu\r\n [id]=\"menuId\"\r\n role=\"menu\"\r\n [attr.aria-labelledby]=\"buttonId\"\r\n [class]=\"\r\n 'z-dropdown absolute top-0 left-0 min-w-48 bg-neutral-primary rounded-md shadow-lg border border-default p-2 space-y-1.5 focus:outline-hidden transition-opacity duration-150 ' +\r\n (menuClass() ? ' ' + menuClass() : '')\r\n \"\r\n [class.hidden]=\"!isOpen()\"\r\n >\r\n <ng-content />\r\n </div>\r\n</div>\r\n",
|
|
10914
11039
|
dependencies: [{
|
|
10915
11040
|
kind: "ngmodule",
|
|
10916
11041
|
type: CommonModule
|
|
@@ -10942,7 +11067,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
10942
11067
|
imports: [CommonModule, OlIcon],
|
|
10943
11068
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10944
11069
|
host: { class: "relative inline-block text-left" },
|
|
10945
|
-
|
|
11070
|
+
providers: [provideOlIcons({ "chevron-down": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m6 9l6 6l6-6\"/></svg>" })],
|
|
11071
|
+
template: "<div\r\n (mouseenter)=\"onMouseEnter()\"\r\n (mouseleave)=\"onMouseLeave()\"\r\n (contextmenu)=\"onContextMenu($event)\"\r\n>\r\n <!-- Custom Slot Trigger or Default Button -->\r\n <div (click)=\"onTriggerClick()\" (keydown)=\"onTriggerKeydown($event)\">\r\n <ng-content select=\"[slot=trigger], [trigger]\" />\r\n @if (label()) {\r\n <button\r\n #triggerButton\r\n type=\"button\"\r\n [id]=\"buttonId\"\r\n [disabled]=\"disabled()\"\r\n aria-haspopup=\"true\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"menuId\"\r\n [class]=\"\r\n triggerClass()\r\n ? 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium cursor-pointer ' +\r\n triggerClass()\r\n : 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-body bg-neutral-primary border border-default rounded-md hover:bg-neutral-tertiary focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed'\r\n \"\r\n >\r\n <ng-content select=\"[triggerIcon]\" />\r\n <span>{{ label() }}</span>\r\n <ol-icon\r\n name=\"chevron_down\"\r\n size=\"xs\"\r\n class=\"transition-transform duration-200\"\r\n [class.rotate-180]=\"isOpen()\"\r\n ></ol-icon>\r\n </button>\r\n } @else if (!label() && triggerClass()) {\r\n <button\r\n #triggerButton\r\n type=\"button\"\r\n [id]=\"buttonId\"\r\n [disabled]=\"disabled()\"\r\n aria-haspopup=\"true\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"menuId\"\r\n [class]=\"'inline-flex items-center justify-center p-2.5 cursor-pointer ' + triggerClass()\"\r\n >\r\n <ng-content select=\"[triggerIcon]\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (trigger() === 'context') {\r\n <span\r\n #cursorAnchor\r\n aria-hidden=\"true\"\r\n class=\"pointer-events-none fixed h-0 w-0\"\r\n [style.left.px]=\"cursorX()\"\r\n [style.top.px]=\"cursorY()\"\r\n ></span>\r\n }\r\n\r\n <div\r\n #menu\r\n [id]=\"menuId\"\r\n role=\"menu\"\r\n [attr.aria-labelledby]=\"buttonId\"\r\n [class]=\"\r\n 'z-dropdown absolute top-0 left-0 min-w-48 bg-neutral-primary rounded-md shadow-lg border border-default p-2 space-y-1.5 focus:outline-hidden transition-opacity duration-150 ' +\r\n (menuClass() ? ' ' + menuClass() : '')\r\n \"\r\n [class.hidden]=\"!isOpen()\"\r\n >\r\n <ng-content />\r\n </div>\r\n</div>\r\n"
|
|
10946
11072
|
}]
|
|
10947
11073
|
}],
|
|
10948
11074
|
ctorParameters: () => [],
|
|
@@ -11725,9 +11851,13 @@ var OlInput = class OlInput {
|
|
|
11725
11851
|
provide: NG_VALUE_ACCESSOR,
|
|
11726
11852
|
useExisting: forwardRef(() => OlInput),
|
|
11727
11853
|
multi: true
|
|
11728
|
-
}
|
|
11854
|
+
}, provideOlIcons({
|
|
11855
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
11856
|
+
"info-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M12 16V12\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <circle cx=\"12\" cy=\"8.5\" r=\"1\" fill=\"currentColor\"/> </svg>",
|
|
11857
|
+
"check-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M8.5 12.5L10.5 14.5L15.5 9.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>"
|
|
11858
|
+
})],
|
|
11729
11859
|
ngImport: i0,
|
|
11730
|
-
template: "<div class=\"w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"size()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [helpIcon]=\"labelIcon()\"\r\n [helpTooltip]=\"labelTooltip()\"\r\n />\r\n }\r\n\r\n <!--\r\n Icon đầu chiếu MỘT lần vào ng-template, rồi dựng lại ở nhánh cần hiện.\r\n Bản cũ khai hai <ng-content select=\"[leftIcon]\"> (một trong nhánh addOn):\r\n Angular chiếu nội dung vào slot ĐẦU TIÊN khớp theo thứ tự template — tức\r\n slot nằm trong @if (addOnIcon()) đang tắt — nên icon [leftIcon] không bao\r\n giờ hiện (đo /floating-label 2026-09-28: 0/28 ô có icon).\r\n -->\r\n <ng-template #leadIcon><ng-content select=\"[addOnIcon], [leftIcon]\" /></ng-template>\r\n\r\n <!--\r\n Khung nhãn nổi dùng chung: tắt (floatingLabel = false) thì trong suốt, giữ\r\n nguyên giao diện cũ. Bật thì nhãn nằm trong khung, nổi khi focus/có chữ.\r\n -->\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"size()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasValue()\"\r\n >\r\n <!-- Main Unified Input Container (Sử dụng Semantic Token Classes 100%) -->\r\n <div\r\n class=\"ol-field-box relative flex items-center w-full border shadow-xs overflow-hidden rounded-md\"\r\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.focus-within:ring-2]=\"\r\n (!disabled() && !readonly()) ||\r\n (state() === 'success' && !disabled()) ||\r\n (state() === 'error' && !disabled())\r\n \"\r\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\r\n [class.bg-disabled]=\"readonly() || disabled()\"\r\n [class.cursor-default]=\"readonly()\"\r\n [class.opacity-60]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n >\r\n <!-- Add-on Icon Box -->\r\n @if (addOnIcon()) {\r\n <span\r\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\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"leadIcon\" />\r\n </span>\r\n }\r\n\r\n <!-- Prefix Add-on Text -->\r\n @if (prefixText()) {\r\n <span\r\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\"\r\n >\r\n {{ prefixText() }}\r\n </span>\r\n }\r\n\r\n <!-- Left Icon (Inside Input) -->\r\n @if (hasLeadingIcon()) {\r\n <div [class]=\"leadIconClasses()\">\r\n <ng-container [ngTemplateOutlet]=\"leadIcon\" />\r\n </div>\r\n }\r\n\r\n <!-- Real HTML Input -->\r\n <input\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.aria-required]=\"required() ? 'true' : null\"\r\n [type]=\"type()\"\r\n [disabled]=\"disabled()\"\r\n [readonly]=\"readonly()\"\r\n [placeholder]=\"placeholder()\"\r\n [ngModel]=\"value()\"\r\n (ngModelChange)=\"emitChange($event)\"\r\n (blur)=\"markTouched()\"\r\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\r\n [class]=\"paddingClasses()\"\r\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\r\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\r\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.cursor-default]=\"readonly()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n />\r\n\r\n <!--\r\n NÚT XOÁ — đặt TRƯỚC icon trạng thái.\r\n\r\n Icon lỗi/thành công chỉ để đọc, nút xoá thì bấm được; đặt nút sau thì ở\r\n trạng thái lỗi nó biến mất, đúng lúc người dùng muốn xoá đi gõ lại.\r\n\r\n Ẩn khi `readonly` hoặc `disabled`: ô không sửa được thì nút xoá là mời\r\n bấm vào chỗ không làm gì.\r\n -->\r\n @if (showClear()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"clearClasses()\"\r\n [attr.aria-label]=\"clearAriaLabel()\"\r\n (click)=\"clearValue($event)\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n\r\n <!-- Right Icon / Status Icon -->\r\n @if (state() === 'error') {\r\n <div class=\"text-fg-danger\" [class]=\"trailIconClasses()\">\r\n <ol-icon name=\"
|
|
11860
|
+
template: "<div class=\"w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"size()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [helpIcon]=\"labelIcon()\"\r\n [helpTooltip]=\"labelTooltip()\"\r\n />\r\n }\r\n\r\n <!--\r\n Icon đầu chiếu MỘT lần vào ng-template, rồi dựng lại ở nhánh cần hiện.\r\n Bản cũ khai hai <ng-content select=\"[leftIcon]\"> (một trong nhánh addOn):\r\n Angular chiếu nội dung vào slot ĐẦU TIÊN khớp theo thứ tự template — tức\r\n slot nằm trong @if (addOnIcon()) đang tắt — nên icon [leftIcon] không bao\r\n giờ hiện (đo /floating-label 2026-09-28: 0/28 ô có icon).\r\n -->\r\n <ng-template #leadIcon><ng-content select=\"[addOnIcon], [leftIcon]\" /></ng-template>\r\n\r\n <!--\r\n Khung nhãn nổi dùng chung: tắt (floatingLabel = false) thì trong suốt, giữ\r\n nguyên giao diện cũ. Bật thì nhãn nằm trong khung, nổi khi focus/có chữ.\r\n -->\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"size()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasValue()\"\r\n >\r\n <!-- Main Unified Input Container (Sử dụng Semantic Token Classes 100%) -->\r\n <div\r\n class=\"ol-field-box relative flex items-center w-full border shadow-xs overflow-hidden rounded-md\"\r\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.focus-within:ring-2]=\"\r\n (!disabled() && !readonly()) ||\r\n (state() === 'success' && !disabled()) ||\r\n (state() === 'error' && !disabled())\r\n \"\r\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\r\n [class.bg-disabled]=\"readonly() || disabled()\"\r\n [class.cursor-default]=\"readonly()\"\r\n [class.opacity-60]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n >\r\n <!-- Add-on Icon Box -->\r\n @if (addOnIcon()) {\r\n <span\r\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\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"leadIcon\" />\r\n </span>\r\n }\r\n\r\n <!-- Prefix Add-on Text -->\r\n @if (prefixText()) {\r\n <span\r\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\"\r\n >\r\n {{ prefixText() }}\r\n </span>\r\n }\r\n\r\n <!-- Left Icon (Inside Input) -->\r\n @if (hasLeadingIcon()) {\r\n <div [class]=\"leadIconClasses()\">\r\n <ng-container [ngTemplateOutlet]=\"leadIcon\" />\r\n </div>\r\n }\r\n\r\n <!-- Real HTML Input -->\r\n <input\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.aria-required]=\"required() ? 'true' : null\"\r\n [type]=\"type()\"\r\n [disabled]=\"disabled()\"\r\n [readonly]=\"readonly()\"\r\n [placeholder]=\"placeholder()\"\r\n [ngModel]=\"value()\"\r\n (ngModelChange)=\"emitChange($event)\"\r\n (blur)=\"markTouched()\"\r\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\r\n [class]=\"paddingClasses()\"\r\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\r\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\r\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.cursor-default]=\"readonly()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n />\r\n\r\n <!--\r\n NÚT XOÁ — đặt TRƯỚC icon trạng thái.\r\n\r\n Icon lỗi/thành công chỉ để đọc, nút xoá thì bấm được; đặt nút sau thì ở\r\n trạng thái lỗi nó biến mất, đúng lúc người dùng muốn xoá đi gõ lại.\r\n\r\n Ẩn khi `readonly` hoặc `disabled`: ô không sửa được thì nút xoá là mời\r\n bấm vào chỗ không làm gì.\r\n -->\r\n @if (showClear()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"clearClasses()\"\r\n [attr.aria-label]=\"clearAriaLabel()\"\r\n (click)=\"clearValue($event)\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n\r\n <!-- Right Icon / Status Icon -->\r\n @if (state() === 'error') {\r\n <div class=\"text-fg-danger\" [class]=\"trailIconClasses()\">\r\n <ol-icon name=\"info_circle\" size=\"sm\" />\r\n </div>\r\n } @else if (state() === 'success') {\r\n <div class=\"text-fg-success\" [class]=\"trailIconClasses()\">\r\n <ol-icon name=\"check_circle\" size=\"sm\" />\r\n </div>\r\n } @else if (rightIcon()) {\r\n <div class=\"text-body-subtle\" [class]=\"trailIconClasses()\">\r\n <ng-content select=\"[rightIcon]\" />\r\n </div>\r\n }\r\n\r\n <!-- Suffix Add-on Text -->\r\n @if (suffixText()) {\r\n <span\r\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\"\r\n >\r\n {{ suffixText() }}\r\n </span>\r\n }\r\n </div>\r\n </ol-field-frame>\r\n\r\n <!-- Helper Text & Right Helper Action -->\r\n @if (helperText() || rightHelperText()) {\r\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\r\n @if (helperText()) {\r\n <p\r\n [id]=\"helperId()\"\r\n [attr.role]=\"state() === 'error' ? 'alert' : null\"\r\n [class.text-body]=\"state() === 'default'\"\r\n [class.text-fg-danger]=\"state() === 'error'\"\r\n [class.text-fg-success]=\"state() === 'success'\"\r\n >\r\n {{ helperText() }}\r\n </p>\r\n }\r\n @if (rightHelperText()) {\r\n <span class=\"text-xs text-fg-brand hover:underline cursor-pointer ms-auto font-medium\">\r\n {{ rightHelperText() }}\r\n </span>\r\n }\r\n <ng-content select=\"[rightHelperAction]\" />\r\n </div>\r\n }\r\n</div>\r\n",
|
|
11731
11861
|
dependencies: [
|
|
11732
11862
|
{
|
|
11733
11863
|
kind: "ngmodule",
|
|
@@ -11840,8 +11970,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
11840
11970
|
provide: NG_VALUE_ACCESSOR,
|
|
11841
11971
|
useExisting: forwardRef(() => OlInput),
|
|
11842
11972
|
multi: true
|
|
11843
|
-
}
|
|
11844
|
-
|
|
11973
|
+
}, provideOlIcons({
|
|
11974
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
11975
|
+
"info-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M12 16V12\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/> <circle cx=\"12\" cy=\"8.5\" r=\"1\" fill=\"currentColor\"/> </svg>",
|
|
11976
|
+
"check-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/> <path d=\"M8.5 12.5L10.5 14.5L15.5 9.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>"
|
|
11977
|
+
})],
|
|
11978
|
+
template: "<div class=\"w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"size()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [helpIcon]=\"labelIcon()\"\r\n [helpTooltip]=\"labelTooltip()\"\r\n />\r\n }\r\n\r\n <!--\r\n Icon đầu chiếu MỘT lần vào ng-template, rồi dựng lại ở nhánh cần hiện.\r\n Bản cũ khai hai <ng-content select=\"[leftIcon]\"> (một trong nhánh addOn):\r\n Angular chiếu nội dung vào slot ĐẦU TIÊN khớp theo thứ tự template — tức\r\n slot nằm trong @if (addOnIcon()) đang tắt — nên icon [leftIcon] không bao\r\n giờ hiện (đo /floating-label 2026-09-28: 0/28 ô có icon).\r\n -->\r\n <ng-template #leadIcon><ng-content select=\"[addOnIcon], [leftIcon]\" /></ng-template>\r\n\r\n <!--\r\n Khung nhãn nổi dùng chung: tắt (floatingLabel = false) thì trong suốt, giữ\r\n nguyên giao diện cũ. Bật thì nhãn nằm trong khung, nổi khi focus/có chữ.\r\n -->\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"size()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasValue()\"\r\n >\r\n <!-- Main Unified Input Container (Sử dụng Semantic Token Classes 100%) -->\r\n <div\r\n class=\"ol-field-box relative flex items-center w-full border shadow-xs overflow-hidden rounded-md\"\r\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.focus-within:border-brand]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.focus-within:ring-2]=\"\r\n (!disabled() && !readonly()) ||\r\n (state() === 'success' && !disabled()) ||\r\n (state() === 'error' && !disabled())\r\n \"\r\n [class.focus-within:ring-brand-subtle]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.dark:focus-within:ring-brand-950]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus-within:border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus-within:ring-success-medium]=\"state() === 'success' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus-within:border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus-within:ring-danger-medium]=\"state() === 'error' && !disabled()\"\r\n [class.bg-disabled]=\"readonly() || disabled()\"\r\n [class.cursor-default]=\"readonly()\"\r\n [class.opacity-60]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n >\r\n <!-- Add-on Icon Box -->\r\n @if (addOnIcon()) {\r\n <span\r\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\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"leadIcon\" />\r\n </span>\r\n }\r\n\r\n <!-- Prefix Add-on Text -->\r\n @if (prefixText()) {\r\n <span\r\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\"\r\n >\r\n {{ prefixText() }}\r\n </span>\r\n }\r\n\r\n <!-- Left Icon (Inside Input) -->\r\n @if (hasLeadingIcon()) {\r\n <div [class]=\"leadIconClasses()\">\r\n <ng-container [ngTemplateOutlet]=\"leadIcon\" />\r\n </div>\r\n }\r\n\r\n <!-- Real HTML Input -->\r\n <input\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.aria-required]=\"required() ? 'true' : null\"\r\n [type]=\"type()\"\r\n [disabled]=\"disabled()\"\r\n [readonly]=\"readonly()\"\r\n [placeholder]=\"placeholder()\"\r\n [ngModel]=\"value()\"\r\n (ngModelChange)=\"emitChange($event)\"\r\n (blur)=\"markTouched()\"\r\n class=\"block w-full h-full bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal\"\r\n [class]=\"paddingClasses()\"\r\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\r\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\r\n [class.text-heading]=\"(state() === 'default' && !disabled() && !readonly()) || readonly()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.cursor-default]=\"readonly()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n />\r\n\r\n <!--\r\n NÚT XOÁ — đặt TRƯỚC icon trạng thái.\r\n\r\n Icon lỗi/thành công chỉ để đọc, nút xoá thì bấm được; đặt nút sau thì ở\r\n trạng thái lỗi nó biến mất, đúng lúc người dùng muốn xoá đi gõ lại.\r\n\r\n Ẩn khi `readonly` hoặc `disabled`: ô không sửa được thì nút xoá là mời\r\n bấm vào chỗ không làm gì.\r\n -->\r\n @if (showClear()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"clearClasses()\"\r\n [attr.aria-label]=\"clearAriaLabel()\"\r\n (click)=\"clearValue($event)\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n\r\n <!-- Right Icon / Status Icon -->\r\n @if (state() === 'error') {\r\n <div class=\"text-fg-danger\" [class]=\"trailIconClasses()\">\r\n <ol-icon name=\"info_circle\" size=\"sm\" />\r\n </div>\r\n } @else if (state() === 'success') {\r\n <div class=\"text-fg-success\" [class]=\"trailIconClasses()\">\r\n <ol-icon name=\"check_circle\" size=\"sm\" />\r\n </div>\r\n } @else if (rightIcon()) {\r\n <div class=\"text-body-subtle\" [class]=\"trailIconClasses()\">\r\n <ng-content select=\"[rightIcon]\" />\r\n </div>\r\n }\r\n\r\n <!-- Suffix Add-on Text -->\r\n @if (suffixText()) {\r\n <span\r\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\"\r\n >\r\n {{ suffixText() }}\r\n </span>\r\n }\r\n </div>\r\n </ol-field-frame>\r\n\r\n <!-- Helper Text & Right Helper Action -->\r\n @if (helperText() || rightHelperText()) {\r\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\r\n @if (helperText()) {\r\n <p\r\n [id]=\"helperId()\"\r\n [attr.role]=\"state() === 'error' ? 'alert' : null\"\r\n [class.text-body]=\"state() === 'default'\"\r\n [class.text-fg-danger]=\"state() === 'error'\"\r\n [class.text-fg-success]=\"state() === 'success'\"\r\n >\r\n {{ helperText() }}\r\n </p>\r\n }\r\n @if (rightHelperText()) {\r\n <span class=\"text-xs text-fg-brand hover:underline cursor-pointer ms-auto font-medium\">\r\n {{ rightHelperText() }}\r\n </span>\r\n }\r\n <ng-content select=\"[rightHelperAction]\" />\r\n </div>\r\n }\r\n</div>\r\n"
|
|
11845
11979
|
}]
|
|
11846
11980
|
}],
|
|
11847
11981
|
propDecorators: {
|
|
@@ -14078,9 +14212,14 @@ var OlTextarea = class OlTextarea {
|
|
|
14078
14212
|
provide: NG_VALUE_ACCESSOR,
|
|
14079
14213
|
useExisting: forwardRef(() => OlTextarea),
|
|
14080
14214
|
multi: true
|
|
14081
|
-
}
|
|
14082
|
-
|
|
14083
|
-
|
|
14215
|
+
}, provideOlIcons({
|
|
14216
|
+
"link": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>",
|
|
14217
|
+
"photo": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><rect fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"8\" cy=\"9\" r=\"1\" fill=\"currentColor\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M21 15l-5.197-5.197a2 2 0 0 0-2.828 0l-5.422 5.422a2 2 0 0 1-2.828 0L3 12\"/></svg>",
|
|
14218
|
+
"arrow-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m5 12h14m-7-7l7 7-7 7\"/></svg>",
|
|
14219
|
+
"mynaui:smile": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\" d=\"M8 11c0 .57.383 1 .857 1h6.286c.474 0 .857-.43.857-1\"/><circle cx=\"9.5\" cy=\"8\" r=\"1\" fill=\"currentColor\"/><circle cx=\"14.5\" cy=\"8\" r=\"1\" fill=\"currentColor\"/></svg>"
|
|
14220
|
+
})],
|
|
14221
|
+
ngImport: i0,
|
|
14222
|
+
template: "<div class=\"w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && (!floatingLabel() || commentBox())) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n @if (!commentBox()) {\r\n <!-- Standard Textarea Input -->\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [variant]=\"floatingVariant()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"!!value()\"\r\n [floatStart]=\"14\"\r\n >\r\n <div class=\"relative w-full\">\r\n <textarea\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [rows]=\"rows()\"\r\n [disabled]=\"disabled()\"\r\n [placeholder]=\"placeholder()\"\r\n [maxLength]=\"maxLength() ?? 5000\"\r\n [ngModel]=\"value()\"\r\n (ngModelChange)=\"emitChange($event)\"\r\n (blur)=\"markTouched()\"\r\n class=\"ol-field-box block w-full p-3.5 text-sm rounded-base outline-none border font-normal shadow-xs\"\r\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\r\n [class.text-body]=\"state() === 'default' && !disabled()\"\r\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\r\n [class.focus:ring-2]=\"!disabled()\"\r\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\r\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n ></textarea>\r\n </div>\r\n </ol-field-frame>\r\n } @else {\r\n <!--\r\n Figma Ready-to-use Comment / Reply Textarea Card (`Type=Chatroom`).\r\n\r\n `rounded-md` (12px) chứ không `rounded-lg` (16px): đọc Figma\r\n `ol-textarea` biến thể `Type=Chatroom, State=Initial` ra **radius 12** —\r\n cùng bậc với `ol-input` và mọi ô nhập khác trong họ. Đo trang /forms\r\n 2026-09-17: `ol-textarea` là component DUY NHẤT trong họ có hai giá trị\r\n bo góc (12px và 16px), lệch với 9 component còn lại.\r\n -->\r\n <div\r\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\"\r\n >\r\n <textarea\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [rows]=\"rows()\"\r\n [disabled]=\"disabled()\"\r\n [placeholder]=\"placeholder() || 'Viết bình luận hoặc phản hồi...'\"\r\n [ngModel]=\"value()\"\r\n (ngModelChange)=\"emitChange($event)\"\r\n (blur)=\"markTouched()\"\r\n class=\"block w-full p-3.5 text-sm text-body bg-transparent border-0 outline-none resize-none placeholder:text-body-subtle\"\r\n ></textarea>\r\n <!-- Bottom Action Bar -->\r\n <div\r\n class=\"flex items-center justify-between px-3 py-2 border-t border-default-medium bg-neutral-secondary-medium\"\r\n >\r\n <div class=\"flex items-center gap-1 text-body-subtle\">\r\n <button\r\n type=\"button\"\r\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\r\n title=\"Đính kèm tệp\"\r\n >\r\n <ol-icon name=\"link\" size=\"sm\" />\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\r\n title=\"Chèn vị trí/hình ảnh\"\r\n >\r\n <ol-icon name=\"photo\" size=\"sm\" />\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\r\n title=\"Biểu tượng cảm xúc\"\r\n >\r\n <ol-icon name=\"mynaui_smile\" size=\"sm\" />\r\n </button>\r\n </div>\r\n <button\r\n type=\"button\"\r\n (click)=\"onSubmit()\"\r\n [disabled]=\"disabled() || !value().trim()\"\r\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\"\r\n >\r\n <span>Gửi</span>\r\n <ol-icon name=\"arrow_right\" size=\"xs\" />\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n @if (helperText() || showCharCount()) {\r\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\r\n @if (helperText()) {\r\n <p\r\n [class.text-body]=\"state() === 'default'\"\r\n [class.text-fg-danger-strong]=\"state() === 'error'\"\r\n [class.text-fg-success-strong]=\"state() === 'success'\"\r\n >\r\n {{ helperText() }}\r\n </p>\r\n }\r\n @if (showCharCount()) {\r\n <span class=\"text-body-subtle ms-auto\">\r\n {{ value().length }}{{ maxLength() ? '/' + maxLength() : '' }} ký tự\r\n </span>\r\n }\r\n </div>\r\n }\r\n</div>\r\n",
|
|
14084
14223
|
dependencies: [
|
|
14085
14224
|
{
|
|
14086
14225
|
kind: "ngmodule",
|
|
@@ -14182,8 +14321,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
14182
14321
|
provide: NG_VALUE_ACCESSOR,
|
|
14183
14322
|
useExisting: forwardRef(() => OlTextarea),
|
|
14184
14323
|
multi: true
|
|
14185
|
-
}
|
|
14186
|
-
|
|
14324
|
+
}, provideOlIcons({
|
|
14325
|
+
"link": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>",
|
|
14326
|
+
"photo": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><rect fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"8\" cy=\"9\" r=\"1\" fill=\"currentColor\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M21 15l-5.197-5.197a2 2 0 0 0-2.828 0l-5.422 5.422a2 2 0 0 1-2.828 0L3 12\"/></svg>",
|
|
14327
|
+
"arrow-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m5 12h14m-7-7l7 7-7 7\"/></svg>",
|
|
14328
|
+
"mynaui:smile": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\" d=\"M8 11c0 .57.383 1 .857 1h6.286c.474 0 .857-.43.857-1\"/><circle cx=\"9.5\" cy=\"8\" r=\"1\" fill=\"currentColor\"/><circle cx=\"14.5\" cy=\"8\" r=\"1\" fill=\"currentColor\"/></svg>"
|
|
14329
|
+
})],
|
|
14330
|
+
template: "<div class=\"w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && (!floatingLabel() || commentBox())) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n @if (!commentBox()) {\r\n <!-- Standard Textarea Input -->\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [variant]=\"floatingVariant()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"!!value()\"\r\n [floatStart]=\"14\"\r\n >\r\n <div class=\"relative w-full\">\r\n <textarea\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [rows]=\"rows()\"\r\n [disabled]=\"disabled()\"\r\n [placeholder]=\"placeholder()\"\r\n [maxLength]=\"maxLength() ?? 5000\"\r\n [ngModel]=\"value()\"\r\n (ngModelChange)=\"emitChange($event)\"\r\n (blur)=\"markTouched()\"\r\n class=\"ol-field-box block w-full p-3.5 text-sm rounded-base outline-none border font-normal shadow-xs\"\r\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\r\n [class.text-body]=\"state() === 'default' && !disabled()\"\r\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\r\n [class.focus:ring-2]=\"!disabled()\"\r\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\r\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n ></textarea>\r\n </div>\r\n </ol-field-frame>\r\n } @else {\r\n <!--\r\n Figma Ready-to-use Comment / Reply Textarea Card (`Type=Chatroom`).\r\n\r\n `rounded-md` (12px) chứ không `rounded-lg` (16px): đọc Figma\r\n `ol-textarea` biến thể `Type=Chatroom, State=Initial` ra **radius 12** —\r\n cùng bậc với `ol-input` và mọi ô nhập khác trong họ. Đo trang /forms\r\n 2026-09-17: `ol-textarea` là component DUY NHẤT trong họ có hai giá trị\r\n bo góc (12px và 16px), lệch với 9 component còn lại.\r\n -->\r\n <div\r\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\"\r\n >\r\n <textarea\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [rows]=\"rows()\"\r\n [disabled]=\"disabled()\"\r\n [placeholder]=\"placeholder() || 'Viết bình luận hoặc phản hồi...'\"\r\n [ngModel]=\"value()\"\r\n (ngModelChange)=\"emitChange($event)\"\r\n (blur)=\"markTouched()\"\r\n class=\"block w-full p-3.5 text-sm text-body bg-transparent border-0 outline-none resize-none placeholder:text-body-subtle\"\r\n ></textarea>\r\n <!-- Bottom Action Bar -->\r\n <div\r\n class=\"flex items-center justify-between px-3 py-2 border-t border-default-medium bg-neutral-secondary-medium\"\r\n >\r\n <div class=\"flex items-center gap-1 text-body-subtle\">\r\n <button\r\n type=\"button\"\r\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\r\n title=\"Đính kèm tệp\"\r\n >\r\n <ol-icon name=\"link\" size=\"sm\" />\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\r\n title=\"Chèn vị trí/hình ảnh\"\r\n >\r\n <ol-icon name=\"photo\" size=\"sm\" />\r\n </button>\r\n <button\r\n type=\"button\"\r\n class=\"p-1.5 hover:bg-neutral-secondary-medium rounded-lg text-body-subtle hover:text-body\"\r\n title=\"Biểu tượng cảm xúc\"\r\n >\r\n <ol-icon name=\"mynaui_smile\" size=\"sm\" />\r\n </button>\r\n </div>\r\n <button\r\n type=\"button\"\r\n (click)=\"onSubmit()\"\r\n [disabled]=\"disabled() || !value().trim()\"\r\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\"\r\n >\r\n <span>Gửi</span>\r\n <ol-icon name=\"arrow_right\" size=\"xs\" />\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n @if (helperText() || showCharCount()) {\r\n <div class=\"flex items-center justify-between mt-1.5 text-xs\">\r\n @if (helperText()) {\r\n <p\r\n [class.text-body]=\"state() === 'default'\"\r\n [class.text-fg-danger-strong]=\"state() === 'error'\"\r\n [class.text-fg-success-strong]=\"state() === 'success'\"\r\n >\r\n {{ helperText() }}\r\n </p>\r\n }\r\n @if (showCharCount()) {\r\n <span class=\"text-body-subtle ms-auto\">\r\n {{ value().length }}{{ maxLength() ? '/' + maxLength() : '' }} ký tự\r\n </span>\r\n }\r\n </div>\r\n }\r\n</div>\r\n"
|
|
14187
14331
|
}]
|
|
14188
14332
|
}],
|
|
14189
14333
|
propDecorators: {
|
|
@@ -14732,15 +14876,18 @@ var OlSelect = class OlSelect {
|
|
|
14732
14876
|
controlPadClass = computed(() => FIELD_PADDING_END[this.resolvedSize()], ...ngDevMode ? [{ debugName: "controlPadClass" }] : /* istanbul ignore next */ []);
|
|
14733
14877
|
onChange = () => {};
|
|
14734
14878
|
onTouched = () => {};
|
|
14735
|
-
destroyRef
|
|
14736
|
-
elementRef
|
|
14737
|
-
overlayContainer
|
|
14879
|
+
destroyRef;
|
|
14880
|
+
elementRef;
|
|
14881
|
+
overlayContainer;
|
|
14738
14882
|
detachPanel = null;
|
|
14739
14883
|
releaseEscape = null;
|
|
14740
14884
|
stopTracking = null;
|
|
14741
14885
|
wasOpen = false;
|
|
14742
14886
|
hengio = null;
|
|
14743
14887
|
constructor() {
|
|
14888
|
+
this.destroyRef = inject(DestroyRef);
|
|
14889
|
+
this.elementRef = inject(ElementRef);
|
|
14890
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
14744
14891
|
effect(() => {
|
|
14745
14892
|
const open = this.isOpen();
|
|
14746
14893
|
if (open === this.wasOpen) return;
|
|
@@ -15515,7 +15662,11 @@ var OlSelect = class OlSelect {
|
|
|
15515
15662
|
provide: NG_VALUE_ACCESSOR,
|
|
15516
15663
|
useExisting: forwardRef(() => OlSelect),
|
|
15517
15664
|
multi: true
|
|
15518
|
-
}
|
|
15665
|
+
}, provideOlIcons({
|
|
15666
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
15667
|
+
"chevron-down": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m6 9l6 6l6-6\"/></svg>",
|
|
15668
|
+
"magnifier": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\" d=\"m20 20l-3-3\"/></svg>"
|
|
15669
|
+
})],
|
|
15519
15670
|
queries: [{
|
|
15520
15671
|
propertyName: "optionTemplate",
|
|
15521
15672
|
first: true,
|
|
@@ -15558,7 +15709,7 @@ var OlSelect = class OlSelect {
|
|
|
15558
15709
|
}
|
|
15559
15710
|
],
|
|
15560
15711
|
ngImport: i0,
|
|
15561
|
-
template: "<div class=\"w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"resolvedSize()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <div class=\"relative w-full\">\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"resolvedSize()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasValue()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <!-- Icon trái neo theo KHUNG NHẬP, không theo khung nổi (có hàng nhãn 20px). -->\r\n <div class=\"relative\">\r\n @if (leftIcon()) {\r\n <div\r\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center pointer-events-none text-body-subtle\"\r\n [class]=\"leftIconPadClass()\"\r\n >\r\n <ng-content select=\"[leftIcon]\" />\r\n </div>\r\n }\r\n\r\n <!--\r\n Ô ĐIỀU KHIỂN — giữ NGUYÊN bảng class của bản `<select>` cũ để giao diện\r\n Figma không đổi một pixel: cùng viền, nền, bo góc, thang cỡ (h-9/h-10/h-12)\r\n và cùng bảng màu theo `state`.\r\n\r\n `dark:focus:ring-brand-950` GIỮ bậc số: `brand` là bảng Figma CÓ nên bậc\r\n số hợp lệ, còn `ring-brand-softer` (token duy nhất trỏ brand-950 ở chế độ\r\n tối) chưa nơi nào dùng nên Tailwind không sinh CSS — thay vào là class\r\n CHẾT mà build vẫn xanh (đo 2026-09-15).\r\n\r\n Khác bản cũ ở chỗ đây là `<div role=\"combobox\">` chứ không phải `<select>`\r\n gốc: thẻ gốc không cho tìm kiếm, đa chọn hay tự vẽ từng dòng.\r\n -->\r\n <div\r\n [id]=\"inputId()\"\r\n role=\"combobox\"\r\n tabindex=\"0\"\r\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\r\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.aria-required]=\"required() ? 'true' : null\"\r\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\r\n aria-haspopup=\"listbox\"\r\n (click)=\"toggle()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n class=\"ol-field-box relative flex w-full items-center gap-1.5 border shadow-xs outline-none font-normal\"\r\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\r\n [class.rounded-base]=\"true\"\r\n [class]=\"paddingStartClass()\"\r\n [class.flex-wrap]=\"multiple()\"\r\n [class.content-center]=\"multiple()\"\r\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\r\n [class.min-h-10]=\"resolvedSize() === 'base'\"\r\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\r\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\r\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\r\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\r\n [class.text-heading]=\"state() === 'default' && !disabled()\"\r\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\r\n [class.focus:ring-2]=\"!disabled()\"\r\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\r\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n >\r\n <!-- Thẻ của các giá trị đã chọn (đa chọn) -->\r\n @if (multiple()) {\r\n @for (val of selectedValues(); track val) {\r\n <!-- `h-6` cố định: để thẻ co theo chữ thì mỗi thẻ cao một kiểu và ô\r\n bị đẩy theo thẻ cao nhất. Cùng cách đã chốt ở `ol-autocomplete`. -->\r\n <!-- Chip khoá dùng bảng màu TRUNG TÍNH: mất nút × là dấu hiệu quá\r\n nhẹ, người dùng tưởng nút chưa hiện. Đổi màu cho thấy ngay đây\r\n là loại chip khác. -->\r\n <span\r\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\r\n [class.border-brand-subtle]=\"!isLocked(val)\"\r\n [class.bg-brand-softer]=\"!isLocked(val)\"\r\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\r\n [class.border-default]=\"isLocked(val)\"\r\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\r\n [class.text-body]=\"isLocked(val)\"\r\n >\r\n @if (isLocked(val)) {\r\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\r\n } @else if (optionOf(val)?.icon) {\r\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\r\n } @else if (optionOf(val)?.prefix) {\r\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\r\n optionOf(val)?.prefix\r\n }}</span>\r\n }\r\n <span>{{ getDisplayLabel(val) }}</span>\r\n @if (!disabled() && !readonly() && !isLocked(val)) {\r\n <button\r\n type=\"button\"\r\n (click)=\"removeValue(val, $event)\"\r\n [attr.aria-label]=\"'Gỡ ' + getDisplayLabel(val)\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n }\r\n\r\n <!--\r\n PHẦN CỐ ĐỊNH của ô đóng — icon và nhãn LUÔN hiện, dù đã chọn hay chưa,\r\n dù danh sách đang mở hay đóng. Chỉ phần GIÁ TRỊ bên phải mới đổi.\r\n\r\n Đặt NGOÀI cả ba nhánh dưới (ô tìm / nhãn đã chọn / placeholder) chính\r\n là điều làm nó cố định — nằm trong nhánh nào thì nó biến mất ở hai\r\n nhánh kia.\r\n\r\n Khác `icon` của MỤC: cái đó đổi theo mục đang chọn. Khai cả hai thì\r\n `triggerIcon` thắng, vì nhãn cố định mà nhảy theo mục thì hết cố định.\r\n -->\r\n @if (triggerIcon()) {\r\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 me-0.5 text-body-subtle\" />\r\n }\r\n @if (triggerLabel()) {\r\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\r\n triggerLabel()\r\n }}</span>\r\n }\r\n\r\n <!--\r\n Ô GÕ TÌM chỉ hiện khi danh sách ĐANG MỞ. Đóng lại thì hiện nhãn tĩnh —\r\n nếu để ô nhập luôn đó, người dùng tưởng gõ thẳng vào được, mà giá trị\r\n lại không đổi theo chữ họ gõ.\r\n -->\r\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\r\n <input\r\n #inlineSearch\r\n type=\"text\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchInput($event)\"\r\n (click)=\"$event.stopPropagation()\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [attr.aria-label]=\"searchPlaceholder()\"\r\n autocomplete=\"off\"\r\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\r\n />\r\n } @else if (!multiple() && hasValue()) {\r\n @if (labelTemplate) {\r\n <ng-container\r\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\r\n />\r\n } @else {\r\n <!-- Ô đóng hiện CẢ phần đầu của mục đang chọn, không chỉ chữ — đây\r\n là điểm khác của combobox reui so với ô chọn thường. -->\r\n @if (selectedItem()?.icon && !triggerIcon()) {\r\n <ol-icon\r\n [name]=\"$any(selectedItem()).icon\"\r\n size=\"sm\"\r\n class=\"shrink-0 me-0.5\"\r\n [class]=\"selectedItem()?.prefixClass || ''\"\r\n />\r\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\r\n <span\r\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\r\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\r\n >{{ selectedItem()?.prefix }}</span\r\n >\r\n }\r\n <span class=\"truncate\">{{ singleLabel() }}</span>\r\n }\r\n } @else if (!hasValue()) {\r\n <!--\r\n Ở trạng thái LỖI, nền đổi sang `bg-danger-soft` (#fef0f2) và\r\n `text-body-subtle` chỉ còn **4,36** — dưới chuẩn 4,5. Nền `success`\r\n (#ecfdf5) cho 4,59, vừa đủ, nên chỉ nhánh lỗi cần đậm hơn.\r\n `text-body` trên nền đó ra 6,82 (sáng) và 10,66 (tối).\r\n -->\r\n <span\r\n class=\"truncate\"\r\n data-ol-field-text\r\n data-ol-field-placeholder\r\n [class.text-body-subtle]=\"state() !== 'error'\"\r\n [class.text-body]=\"state() === 'error'\"\r\n >{{ placeholder() }}</span\r\n >\r\n }\r\n\r\n <!--\r\n MỘT ô điều khiển, ĐỔI VAI theo trạng thái — không xếp cạnh nhau.\r\n Bản đầu của tôi để nút xoá và `chevron-down` CÙNG hiện: đo 2026-09-17\r\n trên `/forms` đếm **2 icon bên phải ở MỌI trạng thái**, kể cả khi danh\r\n sách đang mở. Cụm bên phải chật, hai icon 16px chen nhau trông rối.\r\n\r\n `ol-autocomplete` đã chốt lối này từ trước (ô `w-7` đổi vai\r\n spinner / xoá / chevron); `ol-select` phải theo cho nhất quán.\r\n\r\n Cụm này `absolute` nên BUNG KHỎI LUỒNG: lề phải lấy theo cỡ\r\n (`controlPadClass`), còn khung chừa chỗ bằng `FIELD_ICON_RESERVE_END`\r\n để chữ dài dừng cách icon đúng 8px. Nút xoá bọc `size-4` bằng icon, đo\r\n theo mép SVG — thứ mắt người nhìn thấy.\r\n -->\r\n <div\r\n class=\"absolute inset-y-0 inset-e-0 flex items-center justify-end\"\r\n [class]=\"controlPadClass()\"\r\n >\r\n @if (loading()) {\r\n <ol-spinner size=\"xs\" color=\"brand\" />\r\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"clear($event)\"\r\n aria-label=\"Xoá lựa chọn\"\r\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\r\n >\r\n <ol-icon name=\"close\" size=\"sm\" />\r\n </button>\r\n } @else {\r\n <ol-icon\r\n name=\"chevron-down\"\r\n size=\"sm\"\r\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\r\n [class.rotate-180]=\"isOpen()\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n <!--\r\n DANH SÁCH. Token đọc thẳng từ Figma `ol-autocomplete-menu` (#1149:53147),\r\n biến thể `Type=Default` — đó là dropdown chuẩn của design system:\r\n\r\n bo góc 12px -> rounded-md\r\n đệm 8px -> p-2 (trước là p-1.5 = 6px)\r\n nền bg-neutral-primary-medium (trước là bg-neutral-primary)\r\n viền border-default-medium (trước là border-default)\r\n\r\n Hai token nền/viền TRÙNG NHAU ở chế độ sáng (#ffffff / #e5e7eb) nên đổi\r\n hay không nhìn cũng thế — nhưng CHẾ ĐỘ TỐI thì khác hẳn:\r\n\r\n bg-primary gray-950 ← đang dùng, gần đen, panel CHÌM vào nền\r\n bg-primary-medium gray-800 ← Figma, panel nổi lên\r\n border-default gray-800\r\n border-default-medium gray-700\r\n\r\n Đây đúng loại lỗi chỉ lộ ở chế độ tối mà chế độ sáng không bao giờ thấy.\r\n -->\r\n @if (isOpen()) {\r\n <!--\r\n Ô TÌM Ở ĐẦU PANEL (`searchPosition=\"panel\"`).\r\n Đặt NGOÀI thẻ `role=\"listbox\"`: một `<input>` nằm trong listbox là sai\r\n ARIA — mọi con của listbox phải là `option` hoặc `group`. Đây là lối\r\n `ol-command` và combobox của reui dùng.\r\n -->\r\n <!--\r\n HỘP BỌC mang `#panel` và `position: absolute`.\r\n\r\n `#panel` phải ở ĐÂY chứ không ở thẻ `role=\"listbox\"`: `appendTo=\"body\"`\r\n đẩy chính phần tử `#panel` ra `<body>`. Để nó ở listbox thì ô tìm ở lại\r\n trong cây DOM cũ còn danh sách bay đi — hộp thoại vỡ làm đôi.\r\n -->\r\n <div\r\n #panel\r\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)]\"\r\n [class.w-full]=\"matchTriggerWidth()\"\r\n [class]=\"menuClass()\"\r\n >\r\n @if (searchable() && searchPosition() === 'panel') {\r\n <div class=\"border-b border-default-subtle p-2\">\r\n <!-- Icon/chữ đứng đầu ô TÌM — của việc TÌM, không phải của mục\r\n đang chọn. Bọc trong khung có viền để icon nằm TRONG ô, không\r\n đứng rời bên ngoài như một nhãn. -->\r\n <div\r\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\r\n >\r\n @if (searchIcon()) {\r\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\r\n } @else if (searchPrefix()) {\r\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\r\n searchPrefix()\r\n }}</span>\r\n }\r\n <input\r\n #oTimPanel\r\n type=\"text\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchInput($event)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [attr.aria-label]=\"searchPlaceholder()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n autocomplete=\"off\"\r\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\r\n />\r\n </div>\r\n </div>\r\n }\r\n <!--\r\n `overflow-x-hidden` KHAI RÕ: khai `overflow-y-auto` một mình thì trình\r\n duyệt tự tính `overflow-x` thành `auto` (spec CSS: một trục `visible`\r\n còn trục kia khác `visible` là không hợp lệ). Đo được `scrollWidth\r\n 765 > 620` khi option có `white-space: nowrap`.\r\n\r\n Chỉ chặn ở PANEL CHỌN có bề rộng cố định — nội dung phải vừa bề ngang.\r\n KHÔNG chặn ở khung chứa nội dung tự do (`device-mockup`,\r\n `chat-container`, `drawer`): ở đó người dùng chiếu nội dung bất kỳ\r\n vào, chặn là cắt mất phần rộng của họ.\r\n -->\r\n <div\r\n #listboxRef\r\n role=\"listbox\"\r\n [id]=\"listboxId()\"\r\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\r\n (scroll)=\"onPanelScroll($event)\"\r\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\r\n >\r\n <!--\r\n Bốn trạng thái của danh sách lấy từ máy chủ (Figma `ol-dropdown`\r\n 5266:29107): Đang tải · Chưa gõ đủ · Rỗng/Không có kết quả · có mục.\r\n Đang tải đứng TRƯỚC rỗng: bản cũ báo \"không tìm thấy\" trong lúc\r\n câu trả lời còn chưa về — sai sự thật.\r\n -->\r\n @if (loading()) {\r\n <ol-dropdown-state type=\"loading\" />\r\n } @else if (waitingForTerm()) {\r\n <!-- Chưa gõ đủ `minTermLength`: \"không tìm thấy\" là sai sự thật, vì\r\n chưa tìm gì cả. Cùng lối `typeToSearchText` của ng-select. -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\r\n } @else if (rows().length === 0) {\r\n <div class=\"text-center\">\r\n <ol-dropdown-state\r\n [type]=\"searchQuery().trim() ? 'no-results' : 'empty'\"\r\n [query]=\"searchQuery().trim()\"\r\n [heading]=\"emptyText()\"\r\n />\r\n @if (canCreate()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"createOption()\"\r\n class=\"-mt-2 mb-4 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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" />\r\n <span>{{ createLabel() }}</span>\r\n </button>\r\n }\r\n </div>\r\n } @else {\r\n <!-- Đệm trên: giữ thanh cuộn dài đúng như khi vẽ hết danh sách. -->\r\n @if (viewport().padTop > 0) {\r\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\r\n @if (row.kind === 'group') {\r\n <!-- `dauTien` tính khi dựng danh sách, KHÔNG dùng `$first` của\r\n `@for`: khi `virtualScroll` bật, `$first` là dòng đầu của\r\n khung nhìn nên cuộn xuống là mất đường ngăn. -->\r\n @if (groupSeparator() && !row.isFirstGroup) {\r\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\r\n }\r\n <div\r\n role=\"presentation\"\r\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\r\n >\r\n {{ row.label }}\r\n </div>\r\n } @else {\r\n <!--\r\n `rounded-sm` (6px) chứ không `rounded-md` (12px): Figma vẽ\r\n `ol-autocomplete-item` bo **8px**, mà thang token của lib không\r\n có bậc 8 (xs 4 · sm 6 · md/base 12 · lg 16). Lấy bậc GẦN NHẤT\r\n thay vì viết `rounded-[8px]` — giá trị thô nằm ngoài hệ token\r\n thì đổi theme không theo được.\r\n\r\n `ol-dropdown-item` cũng đã chốt `rounded-sm` cho mục menu, nên\r\n đây còn là giữ nhất quán trong lib.\r\n -->\r\n <!--\r\n `min-w-0` cho phép hàng CO lại, điều kiện cần để `truncate`\r\n của người dùng chạy. Nhưng nó KHÔNG tự chặn tràn: đo bằng cách\r\n tiêm `white-space: nowrap` vào hàng — panel vẫn `scrollWidth\r\n 1763 > 831`. Chỉ `overflow: hidden` đặt trên CHÍNH phần tử nội\r\n dung mới chặn được, mà phần tử đó do người dùng chiếu vào qua\r\n `optionTemplate` nên lib không với tới. `overflow-x-hidden` ở\r\n panel là lớp chặn cuối.\r\n -->\r\n <div\r\n role=\"option\"\r\n [id]=\"optionId(row.index)\"\r\n [attr.data-option-index]=\"row.index\"\r\n [attr.aria-selected]=\"isSelected(row.item)\"\r\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\r\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\r\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\r\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\r\n [class.cursor-pointer]=\"!row.item?.disabled\"\r\n [class.cursor-not-allowed]=\"row.item?.disabled\"\r\n [class.bg-neutral-tertiary-medium]=\"\r\n activeIndex() === row.index && !row.item?.disabled\r\n \"\r\n [class.hover:bg-neutral-tertiary-medium]=\"\r\n !row.item?.disabled && activeIndex() !== row.index\r\n \"\r\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\r\n [class.hover:text-heading]=\"!row.item?.disabled\"\r\n [class.font-medium]=\"isSelected(row.item)\"\r\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\r\n [class.text-fg-disabled]=\"row.item?.disabled\"\r\n >\r\n @if (optionTemplate) {\r\n <ng-container\r\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\r\n />\r\n } @else {\r\n <!-- Phần đầu: `icon` ưu tiên, không có thì `prefix` (chữ).\r\n `prefixClass` nhận CLASS chứ không nhận mã màu — mã màu\r\n cứng không đổi theo theme và theo chế độ tối. -->\r\n @if (row.item?.icon) {\r\n <ol-icon\r\n [name]=\"row.item.icon\"\r\n size=\"sm\"\r\n class=\"shrink-0\"\r\n [class]=\"row.item.prefixClass || ''\"\r\n />\r\n } @else if (row.item?.prefix) {\r\n <span\r\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\r\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\r\n >{{ row.item.prefix }}</span\r\n >\r\n }\r\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\r\n }\r\n @if (isSelected(row.item)) {\r\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Đệm dưới — xem lý do ở đệm trên. -->\r\n @if (viewport().padBottom > 0) {\r\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n <!-- Danh sách CÒN mục nhưng chữ đang gõ chưa trùng nhãn nào: vẫn phải\r\n cho tạo. Ví dụ đã có \"Toán 10\" mà người dùng gõ \"Toán 11\" —\r\n nhánh rỗng ở trên không chạy vì bộ lọc vẫn ra một mục. -->\r\n @if (canCreate()) {\r\n <div class=\"mt-1 border-t border-default-subtle p-2\">\r\n <button\r\n type=\"button\"\r\n (click)=\"createOption()\"\r\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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\r\n <span class=\"truncate\">{{ createLabel() }}</span>\r\n </button>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (helperText()) {\r\n <p\r\n class=\"mt-1.5 text-xs\"\r\n [class.text-body]=\"state() === 'default'\"\r\n [class.text-fg-danger-strong]=\"state() === 'error'\"\r\n [class.text-fg-success-strong]=\"state() === 'success'\"\r\n >\r\n {{ helperText() }}\r\n </p>\r\n }\r\n</div>\r\n",
|
|
15712
|
+
template: "<div class=\"w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"resolvedSize()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <div class=\"relative w-full\">\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"resolvedSize()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasValue()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <!-- Icon trái neo theo KHUNG NHẬP, không theo khung nổi (có hàng nhãn 20px). -->\r\n <div class=\"relative\">\r\n @if (leftIcon()) {\r\n <div\r\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center pointer-events-none text-body-subtle\"\r\n [class]=\"leftIconPadClass()\"\r\n >\r\n <ng-content select=\"[leftIcon]\" />\r\n </div>\r\n }\r\n\r\n <!--\r\n Ô ĐIỀU KHIỂN — giữ NGUYÊN bảng class của bản `<select>` cũ để giao diện\r\n Figma không đổi một pixel: cùng viền, nền, bo góc, thang cỡ (h-9/h-10/h-12)\r\n và cùng bảng màu theo `state`.\r\n\r\n `dark:focus:ring-brand-950` GIỮ bậc số: `brand` là bảng Figma CÓ nên bậc\r\n số hợp lệ, còn `ring-brand-softer` (token duy nhất trỏ brand-950 ở chế độ\r\n tối) chưa nơi nào dùng nên Tailwind không sinh CSS — thay vào là class\r\n CHẾT mà build vẫn xanh (đo 2026-09-15).\r\n\r\n Khác bản cũ ở chỗ đây là `<div role=\"combobox\">` chứ không phải `<select>`\r\n gốc: thẻ gốc không cho tìm kiếm, đa chọn hay tự vẽ từng dòng.\r\n -->\r\n <div\r\n [id]=\"inputId()\"\r\n role=\"combobox\"\r\n tabindex=\"0\"\r\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\r\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.aria-required]=\"required() ? 'true' : null\"\r\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\r\n aria-haspopup=\"listbox\"\r\n (click)=\"toggle()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n class=\"ol-field-box relative flex w-full items-center gap-1.5 border shadow-xs outline-none font-normal\"\r\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\r\n [class.rounded-base]=\"true\"\r\n [class]=\"paddingStartClass()\"\r\n [class.flex-wrap]=\"multiple()\"\r\n [class.content-center]=\"multiple()\"\r\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\r\n [class.min-h-10]=\"resolvedSize() === 'base'\"\r\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\r\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\r\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\r\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\r\n [class.text-heading]=\"state() === 'default' && !disabled()\"\r\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\r\n [class.focus:ring-2]=\"!disabled()\"\r\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\r\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n >\r\n <!-- Thẻ của các giá trị đã chọn (đa chọn) -->\r\n @if (multiple()) {\r\n @for (val of selectedValues(); track val) {\r\n <!-- `h-6` cố định: để thẻ co theo chữ thì mỗi thẻ cao một kiểu và ô\r\n bị đẩy theo thẻ cao nhất. Cùng cách đã chốt ở `ol-autocomplete`. -->\r\n <!-- Chip khoá dùng bảng màu TRUNG TÍNH: mất nút × là dấu hiệu quá\r\n nhẹ, người dùng tưởng nút chưa hiện. Đổi màu cho thấy ngay đây\r\n là loại chip khác. -->\r\n <span\r\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\r\n [class.border-brand-subtle]=\"!isLocked(val)\"\r\n [class.bg-brand-softer]=\"!isLocked(val)\"\r\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\r\n [class.border-default]=\"isLocked(val)\"\r\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\r\n [class.text-body]=\"isLocked(val)\"\r\n >\r\n @if (isLocked(val)) {\r\n <ol-icon name=\"lucide_lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\r\n } @else if (optionOf(val)?.icon) {\r\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\r\n } @else if (optionOf(val)?.prefix) {\r\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\r\n optionOf(val)?.prefix\r\n }}</span>\r\n }\r\n <span>{{ getDisplayLabel(val) }}</span>\r\n @if (!disabled() && !readonly() && !isLocked(val)) {\r\n <button\r\n type=\"button\"\r\n (click)=\"removeValue(val, $event)\"\r\n [attr.aria-label]=\"'Gỡ ' + getDisplayLabel(val)\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n }\r\n\r\n <!--\r\n PHẦN CỐ ĐỊNH của ô đóng — icon và nhãn LUÔN hiện, dù đã chọn hay chưa,\r\n dù danh sách đang mở hay đóng. Chỉ phần GIÁ TRỊ bên phải mới đổi.\r\n\r\n Đặt NGOÀI cả ba nhánh dưới (ô tìm / nhãn đã chọn / placeholder) chính\r\n là điều làm nó cố định — nằm trong nhánh nào thì nó biến mất ở hai\r\n nhánh kia.\r\n\r\n Khác `icon` của MỤC: cái đó đổi theo mục đang chọn. Khai cả hai thì\r\n `triggerIcon` thắng, vì nhãn cố định mà nhảy theo mục thì hết cố định.\r\n -->\r\n @if (triggerIcon()) {\r\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 me-0.5 text-body-subtle\" />\r\n }\r\n @if (triggerLabel()) {\r\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\r\n triggerLabel()\r\n }}</span>\r\n }\r\n\r\n <!--\r\n Ô GÕ TÌM chỉ hiện khi danh sách ĐANG MỞ. Đóng lại thì hiện nhãn tĩnh —\r\n nếu để ô nhập luôn đó, người dùng tưởng gõ thẳng vào được, mà giá trị\r\n lại không đổi theo chữ họ gõ.\r\n -->\r\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\r\n <input\r\n #inlineSearch\r\n type=\"text\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchInput($event)\"\r\n (click)=\"$event.stopPropagation()\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [attr.aria-label]=\"searchPlaceholder()\"\r\n autocomplete=\"off\"\r\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\r\n />\r\n } @else if (!multiple() && hasValue()) {\r\n @if (labelTemplate) {\r\n <ng-container\r\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\r\n />\r\n } @else {\r\n <!-- Ô đóng hiện CẢ phần đầu của mục đang chọn, không chỉ chữ — đây\r\n là điểm khác của combobox reui so với ô chọn thường. -->\r\n @if (selectedItem()?.icon && !triggerIcon()) {\r\n <ol-icon\r\n [name]=\"$any(selectedItem()).icon\"\r\n size=\"sm\"\r\n class=\"shrink-0 me-0.5\"\r\n [class]=\"selectedItem()?.prefixClass || ''\"\r\n />\r\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\r\n <span\r\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\r\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\r\n >{{ selectedItem()?.prefix }}</span\r\n >\r\n }\r\n <span class=\"truncate\">{{ singleLabel() }}</span>\r\n }\r\n } @else if (!hasValue()) {\r\n <!--\r\n Ở trạng thái LỖI, nền đổi sang `bg-danger-soft` (#fef0f2) và\r\n `text-body-subtle` chỉ còn **4,36** — dưới chuẩn 4,5. Nền `success`\r\n (#ecfdf5) cho 4,59, vừa đủ, nên chỉ nhánh lỗi cần đậm hơn.\r\n `text-body` trên nền đó ra 6,82 (sáng) và 10,66 (tối).\r\n -->\r\n <span\r\n class=\"truncate\"\r\n data-ol-field-text\r\n data-ol-field-placeholder\r\n [class.text-body-subtle]=\"state() !== 'error'\"\r\n [class.text-body]=\"state() === 'error'\"\r\n >{{ placeholder() }}</span\r\n >\r\n }\r\n\r\n <!--\r\n MỘT ô điều khiển, ĐỔI VAI theo trạng thái — không xếp cạnh nhau.\r\n Bản đầu của tôi để nút xoá và `chevron-down` CÙNG hiện: đo 2026-09-17\r\n trên `/forms` đếm **2 icon bên phải ở MỌI trạng thái**, kể cả khi danh\r\n sách đang mở. Cụm bên phải chật, hai icon 16px chen nhau trông rối.\r\n\r\n `ol-autocomplete` đã chốt lối này từ trước (ô `w-7` đổi vai\r\n spinner / xoá / chevron); `ol-select` phải theo cho nhất quán.\r\n\r\n Cụm này `absolute` nên BUNG KHỎI LUỒNG: lề phải lấy theo cỡ\r\n (`controlPadClass`), còn khung chừa chỗ bằng `FIELD_ICON_RESERVE_END`\r\n để chữ dài dừng cách icon đúng 8px. Nút xoá bọc `size-4` bằng icon, đo\r\n theo mép SVG — thứ mắt người nhìn thấy.\r\n -->\r\n <div\r\n class=\"absolute inset-y-0 inset-e-0 flex items-center justify-end\"\r\n [class]=\"controlPadClass()\"\r\n >\r\n @if (loading()) {\r\n <ol-spinner size=\"xs\" color=\"brand\" />\r\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"clear($event)\"\r\n aria-label=\"Xoá lựa chọn\"\r\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\r\n >\r\n <ol-icon name=\"close\" size=\"sm\" />\r\n </button>\r\n } @else {\r\n <ol-icon\r\n name=\"chevron_down\"\r\n size=\"sm\"\r\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\r\n [class.rotate-180]=\"isOpen()\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n <!--\r\n DANH SÁCH. Token đọc thẳng từ Figma `ol-autocomplete-menu` (#1149:53147),\r\n biến thể `Type=Default` — đó là dropdown chuẩn của design system:\r\n\r\n bo góc 12px -> rounded-md\r\n đệm 8px -> p-2 (trước là p-1.5 = 6px)\r\n nền bg-neutral-primary-medium (trước là bg-neutral-primary)\r\n viền border-default-medium (trước là border-default)\r\n\r\n Hai token nền/viền TRÙNG NHAU ở chế độ sáng (#ffffff / #e5e7eb) nên đổi\r\n hay không nhìn cũng thế — nhưng CHẾ ĐỘ TỐI thì khác hẳn:\r\n\r\n bg-primary gray-950 ← đang dùng, gần đen, panel CHÌM vào nền\r\n bg-primary-medium gray-800 ← Figma, panel nổi lên\r\n border-default gray-800\r\n border-default-medium gray-700\r\n\r\n Đây đúng loại lỗi chỉ lộ ở chế độ tối mà chế độ sáng không bao giờ thấy.\r\n -->\r\n @if (isOpen()) {\r\n <!--\r\n Ô TÌM Ở ĐẦU PANEL (`searchPosition=\"panel\"`).\r\n Đặt NGOÀI thẻ `role=\"listbox\"`: một `<input>` nằm trong listbox là sai\r\n ARIA — mọi con của listbox phải là `option` hoặc `group`. Đây là lối\r\n `ol-command` và combobox của reui dùng.\r\n -->\r\n <!--\r\n HỘP BỌC mang `#panel` và `position: absolute`.\r\n\r\n `#panel` phải ở ĐÂY chứ không ở thẻ `role=\"listbox\"`: `appendTo=\"body\"`\r\n đẩy chính phần tử `#panel` ra `<body>`. Để nó ở listbox thì ô tìm ở lại\r\n trong cây DOM cũ còn danh sách bay đi — hộp thoại vỡ làm đôi.\r\n -->\r\n <div\r\n #panel\r\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)]\"\r\n [class.w-full]=\"matchTriggerWidth()\"\r\n [class]=\"menuClass()\"\r\n >\r\n @if (searchable() && searchPosition() === 'panel') {\r\n <div class=\"border-b border-default-subtle p-2\">\r\n <!-- Icon/chữ đứng đầu ô TÌM — của việc TÌM, không phải của mục\r\n đang chọn. Bọc trong khung có viền để icon nằm TRONG ô, không\r\n đứng rời bên ngoài như một nhãn. -->\r\n <div\r\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\r\n >\r\n @if (searchIcon()) {\r\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\r\n } @else if (searchPrefix()) {\r\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\r\n searchPrefix()\r\n }}</span>\r\n }\r\n <input\r\n #oTimPanel\r\n type=\"text\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchInput($event)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [attr.aria-label]=\"searchPlaceholder()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n autocomplete=\"off\"\r\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\r\n />\r\n </div>\r\n </div>\r\n }\r\n <!--\r\n `overflow-x-hidden` KHAI RÕ: khai `overflow-y-auto` một mình thì trình\r\n duyệt tự tính `overflow-x` thành `auto` (spec CSS: một trục `visible`\r\n còn trục kia khác `visible` là không hợp lệ). Đo được `scrollWidth\r\n 765 > 620` khi option có `white-space: nowrap`.\r\n\r\n Chỉ chặn ở PANEL CHỌN có bề rộng cố định — nội dung phải vừa bề ngang.\r\n KHÔNG chặn ở khung chứa nội dung tự do (`device-mockup`,\r\n `chat-container`, `drawer`): ở đó người dùng chiếu nội dung bất kỳ\r\n vào, chặn là cắt mất phần rộng của họ.\r\n -->\r\n <div\r\n #listboxRef\r\n role=\"listbox\"\r\n [id]=\"listboxId()\"\r\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\r\n (scroll)=\"onPanelScroll($event)\"\r\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\r\n >\r\n <!--\r\n Bốn trạng thái của danh sách lấy từ máy chủ (Figma `ol-dropdown`\r\n 5266:29107): Đang tải · Chưa gõ đủ · Rỗng/Không có kết quả · có mục.\r\n Đang tải đứng TRƯỚC rỗng: bản cũ báo \"không tìm thấy\" trong lúc\r\n câu trả lời còn chưa về — sai sự thật.\r\n -->\r\n @if (loading()) {\r\n <ol-dropdown-state type=\"loading\" />\r\n } @else if (waitingForTerm()) {\r\n <!-- Chưa gõ đủ `minTermLength`: \"không tìm thấy\" là sai sự thật, vì\r\n chưa tìm gì cả. Cùng lối `typeToSearchText` của ng-select. -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\r\n } @else if (rows().length === 0) {\r\n <div class=\"text-center\">\r\n <ol-dropdown-state\r\n [type]=\"searchQuery().trim() ? 'no-results' : 'empty'\"\r\n [query]=\"searchQuery().trim()\"\r\n [heading]=\"emptyText()\"\r\n />\r\n @if (canCreate()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"createOption()\"\r\n class=\"-mt-2 mb-4 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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" />\r\n <span>{{ createLabel() }}</span>\r\n </button>\r\n }\r\n </div>\r\n } @else {\r\n <!-- Đệm trên: giữ thanh cuộn dài đúng như khi vẽ hết danh sách. -->\r\n @if (viewport().padTop > 0) {\r\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\r\n @if (row.kind === 'group') {\r\n <!-- `dauTien` tính khi dựng danh sách, KHÔNG dùng `$first` của\r\n `@for`: khi `virtualScroll` bật, `$first` là dòng đầu của\r\n khung nhìn nên cuộn xuống là mất đường ngăn. -->\r\n @if (groupSeparator() && !row.isFirstGroup) {\r\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\r\n }\r\n <div\r\n role=\"presentation\"\r\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\r\n >\r\n {{ row.label }}\r\n </div>\r\n } @else {\r\n <!--\r\n `rounded-sm` (6px) chứ không `rounded-md` (12px): Figma vẽ\r\n `ol-autocomplete-item` bo **8px**, mà thang token của lib không\r\n có bậc 8 (xs 4 · sm 6 · md/base 12 · lg 16). Lấy bậc GẦN NHẤT\r\n thay vì viết `rounded-[8px]` — giá trị thô nằm ngoài hệ token\r\n thì đổi theme không theo được.\r\n\r\n `ol-dropdown-item` cũng đã chốt `rounded-sm` cho mục menu, nên\r\n đây còn là giữ nhất quán trong lib.\r\n -->\r\n <!--\r\n `min-w-0` cho phép hàng CO lại, điều kiện cần để `truncate`\r\n của người dùng chạy. Nhưng nó KHÔNG tự chặn tràn: đo bằng cách\r\n tiêm `white-space: nowrap` vào hàng — panel vẫn `scrollWidth\r\n 1763 > 831`. Chỉ `overflow: hidden` đặt trên CHÍNH phần tử nội\r\n dung mới chặn được, mà phần tử đó do người dùng chiếu vào qua\r\n `optionTemplate` nên lib không với tới. `overflow-x-hidden` ở\r\n panel là lớp chặn cuối.\r\n -->\r\n <div\r\n role=\"option\"\r\n [id]=\"optionId(row.index)\"\r\n [attr.data-option-index]=\"row.index\"\r\n [attr.aria-selected]=\"isSelected(row.item)\"\r\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\r\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\r\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\r\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\r\n [class.cursor-pointer]=\"!row.item?.disabled\"\r\n [class.cursor-not-allowed]=\"row.item?.disabled\"\r\n [class.bg-neutral-tertiary-medium]=\"\r\n activeIndex() === row.index && !row.item?.disabled\r\n \"\r\n [class.hover:bg-neutral-tertiary-medium]=\"\r\n !row.item?.disabled && activeIndex() !== row.index\r\n \"\r\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\r\n [class.hover:text-heading]=\"!row.item?.disabled\"\r\n [class.font-medium]=\"isSelected(row.item)\"\r\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\r\n [class.text-fg-disabled]=\"row.item?.disabled\"\r\n >\r\n @if (optionTemplate) {\r\n <ng-container\r\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\r\n />\r\n } @else {\r\n <!-- Phần đầu: `icon` ưu tiên, không có thì `prefix` (chữ).\r\n `prefixClass` nhận CLASS chứ không nhận mã màu — mã màu\r\n cứng không đổi theo theme và theo chế độ tối. -->\r\n @if (row.item?.icon) {\r\n <ol-icon\r\n [name]=\"row.item.icon\"\r\n size=\"sm\"\r\n class=\"shrink-0\"\r\n [class]=\"row.item.prefixClass || ''\"\r\n />\r\n } @else if (row.item?.prefix) {\r\n <span\r\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\r\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\r\n >{{ row.item.prefix }}</span\r\n >\r\n }\r\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\r\n }\r\n @if (isSelected(row.item)) {\r\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Đệm dưới — xem lý do ở đệm trên. -->\r\n @if (viewport().padBottom > 0) {\r\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n <!-- Danh sách CÒN mục nhưng chữ đang gõ chưa trùng nhãn nào: vẫn phải\r\n cho tạo. Ví dụ đã có \"Toán 10\" mà người dùng gõ \"Toán 11\" —\r\n nhánh rỗng ở trên không chạy vì bộ lọc vẫn ra một mục. -->\r\n @if (canCreate()) {\r\n <div class=\"mt-1 border-t border-default-subtle p-2\">\r\n <button\r\n type=\"button\"\r\n (click)=\"createOption()\"\r\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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\r\n <span class=\"truncate\">{{ createLabel() }}</span>\r\n </button>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (helperText()) {\r\n <p\r\n class=\"mt-1.5 text-xs\"\r\n [class.text-body]=\"state() === 'default'\"\r\n [class.text-fg-danger-strong]=\"state() === 'error'\"\r\n [class.text-fg-success-strong]=\"state() === 'success'\"\r\n >\r\n {{ helperText() }}\r\n </p>\r\n }\r\n</div>\r\n",
|
|
15562
15713
|
dependencies: [
|
|
15563
15714
|
{
|
|
15564
15715
|
kind: "ngmodule",
|
|
@@ -15699,8 +15850,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
15699
15850
|
provide: NG_VALUE_ACCESSOR,
|
|
15700
15851
|
useExisting: forwardRef(() => OlSelect),
|
|
15701
15852
|
multi: true
|
|
15702
|
-
}
|
|
15703
|
-
template: "<div class=\"w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"resolvedSize()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <div class=\"relative w-full\">\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"resolvedSize()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasValue()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <!-- Icon trái neo theo KHUNG NHẬP, không theo khung nổi (có hàng nhãn 20px). -->\r\n <div class=\"relative\">\r\n @if (leftIcon()) {\r\n <div\r\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center pointer-events-none text-body-subtle\"\r\n [class]=\"leftIconPadClass()\"\r\n >\r\n <ng-content select=\"[leftIcon]\" />\r\n </div>\r\n }\r\n\r\n <!--\r\n Ô ĐIỀU KHIỂN — giữ NGUYÊN bảng class của bản `<select>` cũ để giao diện\r\n Figma không đổi một pixel: cùng viền, nền, bo góc, thang cỡ (h-9/h-10/h-12)\r\n và cùng bảng màu theo `state`.\r\n\r\n `dark:focus:ring-brand-950` GIỮ bậc số: `brand` là bảng Figma CÓ nên bậc\r\n số hợp lệ, còn `ring-brand-softer` (token duy nhất trỏ brand-950 ở chế độ\r\n tối) chưa nơi nào dùng nên Tailwind không sinh CSS — thay vào là class\r\n CHẾT mà build vẫn xanh (đo 2026-09-15).\r\n\r\n Khác bản cũ ở chỗ đây là `<div role=\"combobox\">` chứ không phải `<select>`\r\n gốc: thẻ gốc không cho tìm kiếm, đa chọn hay tự vẽ từng dòng.\r\n -->\r\n <div\r\n [id]=\"inputId()\"\r\n role=\"combobox\"\r\n tabindex=\"0\"\r\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\r\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.aria-required]=\"required() ? 'true' : null\"\r\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\r\n aria-haspopup=\"listbox\"\r\n (click)=\"toggle()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n class=\"ol-field-box relative flex w-full items-center gap-1.5 border shadow-xs outline-none font-normal\"\r\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\r\n [class.rounded-base]=\"true\"\r\n [class]=\"paddingStartClass()\"\r\n [class.flex-wrap]=\"multiple()\"\r\n [class.content-center]=\"multiple()\"\r\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\r\n [class.min-h-10]=\"resolvedSize() === 'base'\"\r\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\r\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\r\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\r\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\r\n [class.text-heading]=\"state() === 'default' && !disabled()\"\r\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\r\n [class.focus:ring-2]=\"!disabled()\"\r\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\r\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n >\r\n <!-- Thẻ của các giá trị đã chọn (đa chọn) -->\r\n @if (multiple()) {\r\n @for (val of selectedValues(); track val) {\r\n <!-- `h-6` cố định: để thẻ co theo chữ thì mỗi thẻ cao một kiểu và ô\r\n bị đẩy theo thẻ cao nhất. Cùng cách đã chốt ở `ol-autocomplete`. -->\r\n <!-- Chip khoá dùng bảng màu TRUNG TÍNH: mất nút × là dấu hiệu quá\r\n nhẹ, người dùng tưởng nút chưa hiện. Đổi màu cho thấy ngay đây\r\n là loại chip khác. -->\r\n <span\r\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\r\n [class.border-brand-subtle]=\"!isLocked(val)\"\r\n [class.bg-brand-softer]=\"!isLocked(val)\"\r\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\r\n [class.border-default]=\"isLocked(val)\"\r\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\r\n [class.text-body]=\"isLocked(val)\"\r\n >\r\n @if (isLocked(val)) {\r\n <ol-icon name=\"lucide:lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\r\n } @else if (optionOf(val)?.icon) {\r\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\r\n } @else if (optionOf(val)?.prefix) {\r\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\r\n optionOf(val)?.prefix\r\n }}</span>\r\n }\r\n <span>{{ getDisplayLabel(val) }}</span>\r\n @if (!disabled() && !readonly() && !isLocked(val)) {\r\n <button\r\n type=\"button\"\r\n (click)=\"removeValue(val, $event)\"\r\n [attr.aria-label]=\"'Gỡ ' + getDisplayLabel(val)\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n }\r\n\r\n <!--\r\n PHẦN CỐ ĐỊNH của ô đóng — icon và nhãn LUÔN hiện, dù đã chọn hay chưa,\r\n dù danh sách đang mở hay đóng. Chỉ phần GIÁ TRỊ bên phải mới đổi.\r\n\r\n Đặt NGOÀI cả ba nhánh dưới (ô tìm / nhãn đã chọn / placeholder) chính\r\n là điều làm nó cố định — nằm trong nhánh nào thì nó biến mất ở hai\r\n nhánh kia.\r\n\r\n Khác `icon` của MỤC: cái đó đổi theo mục đang chọn. Khai cả hai thì\r\n `triggerIcon` thắng, vì nhãn cố định mà nhảy theo mục thì hết cố định.\r\n -->\r\n @if (triggerIcon()) {\r\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 me-0.5 text-body-subtle\" />\r\n }\r\n @if (triggerLabel()) {\r\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\r\n triggerLabel()\r\n }}</span>\r\n }\r\n\r\n <!--\r\n Ô GÕ TÌM chỉ hiện khi danh sách ĐANG MỞ. Đóng lại thì hiện nhãn tĩnh —\r\n nếu để ô nhập luôn đó, người dùng tưởng gõ thẳng vào được, mà giá trị\r\n lại không đổi theo chữ họ gõ.\r\n -->\r\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\r\n <input\r\n #inlineSearch\r\n type=\"text\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchInput($event)\"\r\n (click)=\"$event.stopPropagation()\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [attr.aria-label]=\"searchPlaceholder()\"\r\n autocomplete=\"off\"\r\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\r\n />\r\n } @else if (!multiple() && hasValue()) {\r\n @if (labelTemplate) {\r\n <ng-container\r\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\r\n />\r\n } @else {\r\n <!-- Ô đóng hiện CẢ phần đầu của mục đang chọn, không chỉ chữ — đây\r\n là điểm khác của combobox reui so với ô chọn thường. -->\r\n @if (selectedItem()?.icon && !triggerIcon()) {\r\n <ol-icon\r\n [name]=\"$any(selectedItem()).icon\"\r\n size=\"sm\"\r\n class=\"shrink-0 me-0.5\"\r\n [class]=\"selectedItem()?.prefixClass || ''\"\r\n />\r\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\r\n <span\r\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\r\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\r\n >{{ selectedItem()?.prefix }}</span\r\n >\r\n }\r\n <span class=\"truncate\">{{ singleLabel() }}</span>\r\n }\r\n } @else if (!hasValue()) {\r\n <!--\r\n Ở trạng thái LỖI, nền đổi sang `bg-danger-soft` (#fef0f2) và\r\n `text-body-subtle` chỉ còn **4,36** — dưới chuẩn 4,5. Nền `success`\r\n (#ecfdf5) cho 4,59, vừa đủ, nên chỉ nhánh lỗi cần đậm hơn.\r\n `text-body` trên nền đó ra 6,82 (sáng) và 10,66 (tối).\r\n -->\r\n <span\r\n class=\"truncate\"\r\n data-ol-field-text\r\n data-ol-field-placeholder\r\n [class.text-body-subtle]=\"state() !== 'error'\"\r\n [class.text-body]=\"state() === 'error'\"\r\n >{{ placeholder() }}</span\r\n >\r\n }\r\n\r\n <!--\r\n MỘT ô điều khiển, ĐỔI VAI theo trạng thái — không xếp cạnh nhau.\r\n Bản đầu của tôi để nút xoá và `chevron-down` CÙNG hiện: đo 2026-09-17\r\n trên `/forms` đếm **2 icon bên phải ở MỌI trạng thái**, kể cả khi danh\r\n sách đang mở. Cụm bên phải chật, hai icon 16px chen nhau trông rối.\r\n\r\n `ol-autocomplete` đã chốt lối này từ trước (ô `w-7` đổi vai\r\n spinner / xoá / chevron); `ol-select` phải theo cho nhất quán.\r\n\r\n Cụm này `absolute` nên BUNG KHỎI LUỒNG: lề phải lấy theo cỡ\r\n (`controlPadClass`), còn khung chừa chỗ bằng `FIELD_ICON_RESERVE_END`\r\n để chữ dài dừng cách icon đúng 8px. Nút xoá bọc `size-4` bằng icon, đo\r\n theo mép SVG — thứ mắt người nhìn thấy.\r\n -->\r\n <div\r\n class=\"absolute inset-y-0 inset-e-0 flex items-center justify-end\"\r\n [class]=\"controlPadClass()\"\r\n >\r\n @if (loading()) {\r\n <ol-spinner size=\"xs\" color=\"brand\" />\r\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"clear($event)\"\r\n aria-label=\"Xoá lựa chọn\"\r\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\r\n >\r\n <ol-icon name=\"close\" size=\"sm\" />\r\n </button>\r\n } @else {\r\n <ol-icon\r\n name=\"chevron-down\"\r\n size=\"sm\"\r\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\r\n [class.rotate-180]=\"isOpen()\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n <!--\r\n DANH SÁCH. Token đọc thẳng từ Figma `ol-autocomplete-menu` (#1149:53147),\r\n biến thể `Type=Default` — đó là dropdown chuẩn của design system:\r\n\r\n bo góc 12px -> rounded-md\r\n đệm 8px -> p-2 (trước là p-1.5 = 6px)\r\n nền bg-neutral-primary-medium (trước là bg-neutral-primary)\r\n viền border-default-medium (trước là border-default)\r\n\r\n Hai token nền/viền TRÙNG NHAU ở chế độ sáng (#ffffff / #e5e7eb) nên đổi\r\n hay không nhìn cũng thế — nhưng CHẾ ĐỘ TỐI thì khác hẳn:\r\n\r\n bg-primary gray-950 ← đang dùng, gần đen, panel CHÌM vào nền\r\n bg-primary-medium gray-800 ← Figma, panel nổi lên\r\n border-default gray-800\r\n border-default-medium gray-700\r\n\r\n Đây đúng loại lỗi chỉ lộ ở chế độ tối mà chế độ sáng không bao giờ thấy.\r\n -->\r\n @if (isOpen()) {\r\n <!--\r\n Ô TÌM Ở ĐẦU PANEL (`searchPosition=\"panel\"`).\r\n Đặt NGOÀI thẻ `role=\"listbox\"`: một `<input>` nằm trong listbox là sai\r\n ARIA — mọi con của listbox phải là `option` hoặc `group`. Đây là lối\r\n `ol-command` và combobox của reui dùng.\r\n -->\r\n <!--\r\n HỘP BỌC mang `#panel` và `position: absolute`.\r\n\r\n `#panel` phải ở ĐÂY chứ không ở thẻ `role=\"listbox\"`: `appendTo=\"body\"`\r\n đẩy chính phần tử `#panel` ra `<body>`. Để nó ở listbox thì ô tìm ở lại\r\n trong cây DOM cũ còn danh sách bay đi — hộp thoại vỡ làm đôi.\r\n -->\r\n <div\r\n #panel\r\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)]\"\r\n [class.w-full]=\"matchTriggerWidth()\"\r\n [class]=\"menuClass()\"\r\n >\r\n @if (searchable() && searchPosition() === 'panel') {\r\n <div class=\"border-b border-default-subtle p-2\">\r\n <!-- Icon/chữ đứng đầu ô TÌM — của việc TÌM, không phải của mục\r\n đang chọn. Bọc trong khung có viền để icon nằm TRONG ô, không\r\n đứng rời bên ngoài như một nhãn. -->\r\n <div\r\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\r\n >\r\n @if (searchIcon()) {\r\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\r\n } @else if (searchPrefix()) {\r\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\r\n searchPrefix()\r\n }}</span>\r\n }\r\n <input\r\n #oTimPanel\r\n type=\"text\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchInput($event)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [attr.aria-label]=\"searchPlaceholder()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n autocomplete=\"off\"\r\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\r\n />\r\n </div>\r\n </div>\r\n }\r\n <!--\r\n `overflow-x-hidden` KHAI RÕ: khai `overflow-y-auto` một mình thì trình\r\n duyệt tự tính `overflow-x` thành `auto` (spec CSS: một trục `visible`\r\n còn trục kia khác `visible` là không hợp lệ). Đo được `scrollWidth\r\n 765 > 620` khi option có `white-space: nowrap`.\r\n\r\n Chỉ chặn ở PANEL CHỌN có bề rộng cố định — nội dung phải vừa bề ngang.\r\n KHÔNG chặn ở khung chứa nội dung tự do (`device-mockup`,\r\n `chat-container`, `drawer`): ở đó người dùng chiếu nội dung bất kỳ\r\n vào, chặn là cắt mất phần rộng của họ.\r\n -->\r\n <div\r\n #listboxRef\r\n role=\"listbox\"\r\n [id]=\"listboxId()\"\r\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\r\n (scroll)=\"onPanelScroll($event)\"\r\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\r\n >\r\n <!--\r\n Bốn trạng thái của danh sách lấy từ máy chủ (Figma `ol-dropdown`\r\n 5266:29107): Đang tải · Chưa gõ đủ · Rỗng/Không có kết quả · có mục.\r\n Đang tải đứng TRƯỚC rỗng: bản cũ báo \"không tìm thấy\" trong lúc\r\n câu trả lời còn chưa về — sai sự thật.\r\n -->\r\n @if (loading()) {\r\n <ol-dropdown-state type=\"loading\" />\r\n } @else if (waitingForTerm()) {\r\n <!-- Chưa gõ đủ `minTermLength`: \"không tìm thấy\" là sai sự thật, vì\r\n chưa tìm gì cả. Cùng lối `typeToSearchText` của ng-select. -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\r\n } @else if (rows().length === 0) {\r\n <div class=\"text-center\">\r\n <ol-dropdown-state\r\n [type]=\"searchQuery().trim() ? 'no-results' : 'empty'\"\r\n [query]=\"searchQuery().trim()\"\r\n [heading]=\"emptyText()\"\r\n />\r\n @if (canCreate()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"createOption()\"\r\n class=\"-mt-2 mb-4 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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" />\r\n <span>{{ createLabel() }}</span>\r\n </button>\r\n }\r\n </div>\r\n } @else {\r\n <!-- Đệm trên: giữ thanh cuộn dài đúng như khi vẽ hết danh sách. -->\r\n @if (viewport().padTop > 0) {\r\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\r\n @if (row.kind === 'group') {\r\n <!-- `dauTien` tính khi dựng danh sách, KHÔNG dùng `$first` của\r\n `@for`: khi `virtualScroll` bật, `$first` là dòng đầu của\r\n khung nhìn nên cuộn xuống là mất đường ngăn. -->\r\n @if (groupSeparator() && !row.isFirstGroup) {\r\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\r\n }\r\n <div\r\n role=\"presentation\"\r\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\r\n >\r\n {{ row.label }}\r\n </div>\r\n } @else {\r\n <!--\r\n `rounded-sm` (6px) chứ không `rounded-md` (12px): Figma vẽ\r\n `ol-autocomplete-item` bo **8px**, mà thang token của lib không\r\n có bậc 8 (xs 4 · sm 6 · md/base 12 · lg 16). Lấy bậc GẦN NHẤT\r\n thay vì viết `rounded-[8px]` — giá trị thô nằm ngoài hệ token\r\n thì đổi theme không theo được.\r\n\r\n `ol-dropdown-item` cũng đã chốt `rounded-sm` cho mục menu, nên\r\n đây còn là giữ nhất quán trong lib.\r\n -->\r\n <!--\r\n `min-w-0` cho phép hàng CO lại, điều kiện cần để `truncate`\r\n của người dùng chạy. Nhưng nó KHÔNG tự chặn tràn: đo bằng cách\r\n tiêm `white-space: nowrap` vào hàng — panel vẫn `scrollWidth\r\n 1763 > 831`. Chỉ `overflow: hidden` đặt trên CHÍNH phần tử nội\r\n dung mới chặn được, mà phần tử đó do người dùng chiếu vào qua\r\n `optionTemplate` nên lib không với tới. `overflow-x-hidden` ở\r\n panel là lớp chặn cuối.\r\n -->\r\n <div\r\n role=\"option\"\r\n [id]=\"optionId(row.index)\"\r\n [attr.data-option-index]=\"row.index\"\r\n [attr.aria-selected]=\"isSelected(row.item)\"\r\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\r\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\r\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\r\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\r\n [class.cursor-pointer]=\"!row.item?.disabled\"\r\n [class.cursor-not-allowed]=\"row.item?.disabled\"\r\n [class.bg-neutral-tertiary-medium]=\"\r\n activeIndex() === row.index && !row.item?.disabled\r\n \"\r\n [class.hover:bg-neutral-tertiary-medium]=\"\r\n !row.item?.disabled && activeIndex() !== row.index\r\n \"\r\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\r\n [class.hover:text-heading]=\"!row.item?.disabled\"\r\n [class.font-medium]=\"isSelected(row.item)\"\r\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\r\n [class.text-fg-disabled]=\"row.item?.disabled\"\r\n >\r\n @if (optionTemplate) {\r\n <ng-container\r\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\r\n />\r\n } @else {\r\n <!-- Phần đầu: `icon` ưu tiên, không có thì `prefix` (chữ).\r\n `prefixClass` nhận CLASS chứ không nhận mã màu — mã màu\r\n cứng không đổi theo theme và theo chế độ tối. -->\r\n @if (row.item?.icon) {\r\n <ol-icon\r\n [name]=\"row.item.icon\"\r\n size=\"sm\"\r\n class=\"shrink-0\"\r\n [class]=\"row.item.prefixClass || ''\"\r\n />\r\n } @else if (row.item?.prefix) {\r\n <span\r\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\r\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\r\n >{{ row.item.prefix }}</span\r\n >\r\n }\r\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\r\n }\r\n @if (isSelected(row.item)) {\r\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Đệm dưới — xem lý do ở đệm trên. -->\r\n @if (viewport().padBottom > 0) {\r\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n <!-- Danh sách CÒN mục nhưng chữ đang gõ chưa trùng nhãn nào: vẫn phải\r\n cho tạo. Ví dụ đã có \"Toán 10\" mà người dùng gõ \"Toán 11\" —\r\n nhánh rỗng ở trên không chạy vì bộ lọc vẫn ra một mục. -->\r\n @if (canCreate()) {\r\n <div class=\"mt-1 border-t border-default-subtle p-2\">\r\n <button\r\n type=\"button\"\r\n (click)=\"createOption()\"\r\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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\r\n <span class=\"truncate\">{{ createLabel() }}</span>\r\n </button>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (helperText()) {\r\n <p\r\n class=\"mt-1.5 text-xs\"\r\n [class.text-body]=\"state() === 'default'\"\r\n [class.text-fg-danger-strong]=\"state() === 'error'\"\r\n [class.text-fg-success-strong]=\"state() === 'success'\"\r\n >\r\n {{ helperText() }}\r\n </p>\r\n }\r\n</div>\r\n"
|
|
15853
|
+
}, provideOlIcons({
|
|
15854
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
15855
|
+
"chevron-down": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m6 9l6 6l6-6\"/></svg>",
|
|
15856
|
+
"magnifier": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\" d=\"m20 20l-3-3\"/></svg>"
|
|
15857
|
+
})],
|
|
15858
|
+
template: "<div class=\"w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"resolvedSize()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <div class=\"relative w-full\">\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"resolvedSize()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasValue()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <!-- Icon trái neo theo KHUNG NHẬP, không theo khung nổi (có hàng nhãn 20px). -->\r\n <div class=\"relative\">\r\n @if (leftIcon()) {\r\n <div\r\n class=\"absolute inset-y-0 inset-s-0 z-10 flex items-center pointer-events-none text-body-subtle\"\r\n [class]=\"leftIconPadClass()\"\r\n >\r\n <ng-content select=\"[leftIcon]\" />\r\n </div>\r\n }\r\n\r\n <!--\r\n Ô ĐIỀU KHIỂN — giữ NGUYÊN bảng class của bản `<select>` cũ để giao diện\r\n Figma không đổi một pixel: cùng viền, nền, bo góc, thang cỡ (h-9/h-10/h-12)\r\n và cùng bảng màu theo `state`.\r\n\r\n `dark:focus:ring-brand-950` GIỮ bậc số: `brand` là bảng Figma CÓ nên bậc\r\n số hợp lệ, còn `ring-brand-softer` (token duy nhất trỏ brand-950 ở chế độ\r\n tối) chưa nơi nào dùng nên Tailwind không sinh CSS — thay vào là class\r\n CHẾT mà build vẫn xanh (đo 2026-09-15).\r\n\r\n Khác bản cũ ở chỗ đây là `<div role=\"combobox\">` chứ không phải `<select>`\r\n gốc: thẻ gốc không cho tìm kiếm, đa chọn hay tự vẽ từng dòng.\r\n -->\r\n <div\r\n [id]=\"inputId()\"\r\n role=\"combobox\"\r\n tabindex=\"0\"\r\n [attr.aria-labelledby]=\"label() ? labelId() : triggerLabel() ? triggerLabelId() : null\"\r\n [attr.aria-label]=\"label() || triggerLabel() ? null : ariaLabel() || null\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.aria-required]=\"required() ? 'true' : null\"\r\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\r\n aria-haspopup=\"listbox\"\r\n (click)=\"toggle()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n class=\"ol-field-box relative flex w-full items-center gap-1.5 border shadow-xs outline-none font-normal\"\r\n [class.border-default-medium]=\"state() === 'default' || disabled()\"\r\n [class.rounded-base]=\"true\"\r\n [class]=\"paddingStartClass()\"\r\n [class.flex-wrap]=\"multiple()\"\r\n [class.content-center]=\"multiple()\"\r\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\r\n [class.min-h-10]=\"resolvedSize() === 'base'\"\r\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\r\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\r\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\r\n [class.cursor-pointer]=\"!disabled() && !readonly()\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled()\"\r\n [class.text-heading]=\"state() === 'default' && !disabled()\"\r\n [class.focus:border-brand]=\"state() === 'default' && !disabled()\"\r\n [class.focus:ring-2]=\"!disabled()\"\r\n [class.focus:ring-brand-subtle]=\"state() === 'default' && !disabled()\"\r\n [class.dark:focus:ring-brand-950]=\"state() === 'default' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.focus:border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.focus:ring-danger-medium]=\"state() === 'error' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.focus:border-success]=\"state() === 'success' && !disabled()\"\r\n [class.focus:ring-success-medium]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n >\r\n <!-- Thẻ của các giá trị đã chọn (đa chọn) -->\r\n @if (multiple()) {\r\n @for (val of selectedValues(); track val) {\r\n <!-- `h-6` cố định: để thẻ co theo chữ thì mỗi thẻ cao một kiểu và ô\r\n bị đẩy theo thẻ cao nhất. Cùng cách đã chốt ở `ol-autocomplete`. -->\r\n <!-- Chip khoá dùng bảng màu TRUNG TÍNH: mất nút × là dấu hiệu quá\r\n nhẹ, người dùng tưởng nút chưa hiện. Đổi màu cho thấy ngay đây\r\n là loại chip khác. -->\r\n <span\r\n class=\"inline-flex h-6 shrink-0 items-center gap-1 rounded-md border px-2 text-xs font-medium\"\r\n [class.border-brand-subtle]=\"!isLocked(val)\"\r\n [class.bg-brand-softer]=\"!isLocked(val)\"\r\n [class.text-fg-brand-strong]=\"!isLocked(val)\"\r\n [class.border-default]=\"isLocked(val)\"\r\n [class.bg-neutral-tertiary]=\"isLocked(val)\"\r\n [class.text-body]=\"isLocked(val)\"\r\n >\r\n @if (isLocked(val)) {\r\n <ol-icon name=\"lucide_lock\" size=\"xs\" class=\"shrink-0 opacity-70\" />\r\n } @else if (optionOf(val)?.icon) {\r\n <ol-icon [name]=\"$any(optionOf(val)).icon\" size=\"xs\" class=\"shrink-0\" />\r\n } @else if (optionOf(val)?.prefix) {\r\n <span class=\"shrink-0 text-2xs font-semibold tabular-nums\">{{\r\n optionOf(val)?.prefix\r\n }}</span>\r\n }\r\n <span>{{ getDisplayLabel(val) }}</span>\r\n @if (!disabled() && !readonly() && !isLocked(val)) {\r\n <button\r\n type=\"button\"\r\n (click)=\"removeValue(val, $event)\"\r\n [attr.aria-label]=\"'Gỡ ' + getDisplayLabel(val)\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n }\r\n\r\n <!--\r\n PHẦN CỐ ĐỊNH của ô đóng — icon và nhãn LUÔN hiện, dù đã chọn hay chưa,\r\n dù danh sách đang mở hay đóng. Chỉ phần GIÁ TRỊ bên phải mới đổi.\r\n\r\n Đặt NGOÀI cả ba nhánh dưới (ô tìm / nhãn đã chọn / placeholder) chính\r\n là điều làm nó cố định — nằm trong nhánh nào thì nó biến mất ở hai\r\n nhánh kia.\r\n\r\n Khác `icon` của MỤC: cái đó đổi theo mục đang chọn. Khai cả hai thì\r\n `triggerIcon` thắng, vì nhãn cố định mà nhảy theo mục thì hết cố định.\r\n -->\r\n @if (triggerIcon()) {\r\n <ol-icon [name]=\"triggerIcon()\" size=\"sm\" class=\"shrink-0 me-0.5 text-body-subtle\" />\r\n }\r\n @if (triggerLabel()) {\r\n <span [id]=\"triggerLabelId()\" class=\"shrink-0 text-sm font-medium text-body\">{{\r\n triggerLabel()\r\n }}</span>\r\n }\r\n\r\n <!--\r\n Ô GÕ TÌM chỉ hiện khi danh sách ĐANG MỞ. Đóng lại thì hiện nhãn tĩnh —\r\n nếu để ô nhập luôn đó, người dùng tưởng gõ thẳng vào được, mà giá trị\r\n lại không đổi theo chữ họ gõ.\r\n -->\r\n @if (searchable() && isOpen() && searchPosition() === 'inline') {\r\n <input\r\n #inlineSearch\r\n type=\"text\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchInput($event)\"\r\n (click)=\"$event.stopPropagation()\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [attr.aria-label]=\"searchPlaceholder()\"\r\n autocomplete=\"off\"\r\n class=\"min-w-0 flex-1 border-none bg-transparent p-0 text-inherit outline-none placeholder:text-body-subtle\"\r\n />\r\n } @else if (!multiple() && hasValue()) {\r\n @if (labelTemplate) {\r\n <ng-container\r\n *ngTemplateOutlet=\"labelTemplate; context: { $implicit: selectedValues()[0] }\"\r\n />\r\n } @else {\r\n <!-- Ô đóng hiện CẢ phần đầu của mục đang chọn, không chỉ chữ — đây\r\n là điểm khác của combobox reui so với ô chọn thường. -->\r\n @if (selectedItem()?.icon && !triggerIcon()) {\r\n <ol-icon\r\n [name]=\"$any(selectedItem()).icon\"\r\n size=\"sm\"\r\n class=\"shrink-0 me-0.5\"\r\n [class]=\"selectedItem()?.prefixClass || ''\"\r\n />\r\n } @else if (selectedItem()?.prefix && !triggerIcon()) {\r\n <span\r\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\r\n [class]=\"selectedItem()?.prefixClass || 'text-body-subtle'\"\r\n >{{ selectedItem()?.prefix }}</span\r\n >\r\n }\r\n <span class=\"truncate\">{{ singleLabel() }}</span>\r\n }\r\n } @else if (!hasValue()) {\r\n <!--\r\n Ở trạng thái LỖI, nền đổi sang `bg-danger-soft` (#fef0f2) và\r\n `text-body-subtle` chỉ còn **4,36** — dưới chuẩn 4,5. Nền `success`\r\n (#ecfdf5) cho 4,59, vừa đủ, nên chỉ nhánh lỗi cần đậm hơn.\r\n `text-body` trên nền đó ra 6,82 (sáng) và 10,66 (tối).\r\n -->\r\n <span\r\n class=\"truncate\"\r\n data-ol-field-text\r\n data-ol-field-placeholder\r\n [class.text-body-subtle]=\"state() !== 'error'\"\r\n [class.text-body]=\"state() === 'error'\"\r\n >{{ placeholder() }}</span\r\n >\r\n }\r\n\r\n <!--\r\n MỘT ô điều khiển, ĐỔI VAI theo trạng thái — không xếp cạnh nhau.\r\n Bản đầu của tôi để nút xoá và `chevron-down` CÙNG hiện: đo 2026-09-17\r\n trên `/forms` đếm **2 icon bên phải ở MỌI trạng thái**, kể cả khi danh\r\n sách đang mở. Cụm bên phải chật, hai icon 16px chen nhau trông rối.\r\n\r\n `ol-autocomplete` đã chốt lối này từ trước (ô `w-7` đổi vai\r\n spinner / xoá / chevron); `ol-select` phải theo cho nhất quán.\r\n\r\n Cụm này `absolute` nên BUNG KHỎI LUỒNG: lề phải lấy theo cỡ\r\n (`controlPadClass`), còn khung chừa chỗ bằng `FIELD_ICON_RESERVE_END`\r\n để chữ dài dừng cách icon đúng 8px. Nút xoá bọc `size-4` bằng icon, đo\r\n theo mép SVG — thứ mắt người nhìn thấy.\r\n -->\r\n <div\r\n class=\"absolute inset-y-0 inset-e-0 flex items-center justify-end\"\r\n [class]=\"controlPadClass()\"\r\n >\r\n @if (loading()) {\r\n <ol-spinner size=\"xs\" color=\"brand\" />\r\n } @else if (clearable() && hasContent() && !disabled() && !readonly()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"clear($event)\"\r\n aria-label=\"Xoá lựa chọn\"\r\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\r\n >\r\n <ol-icon name=\"close\" size=\"sm\" />\r\n </button>\r\n } @else {\r\n <ol-icon\r\n name=\"chevron_down\"\r\n size=\"sm\"\r\n class=\"pointer-events-none shrink-0 text-body-subtle transition-transform\"\r\n [class.rotate-180]=\"isOpen()\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n <!--\r\n DANH SÁCH. Token đọc thẳng từ Figma `ol-autocomplete-menu` (#1149:53147),\r\n biến thể `Type=Default` — đó là dropdown chuẩn của design system:\r\n\r\n bo góc 12px -> rounded-md\r\n đệm 8px -> p-2 (trước là p-1.5 = 6px)\r\n nền bg-neutral-primary-medium (trước là bg-neutral-primary)\r\n viền border-default-medium (trước là border-default)\r\n\r\n Hai token nền/viền TRÙNG NHAU ở chế độ sáng (#ffffff / #e5e7eb) nên đổi\r\n hay không nhìn cũng thế — nhưng CHẾ ĐỘ TỐI thì khác hẳn:\r\n\r\n bg-primary gray-950 ← đang dùng, gần đen, panel CHÌM vào nền\r\n bg-primary-medium gray-800 ← Figma, panel nổi lên\r\n border-default gray-800\r\n border-default-medium gray-700\r\n\r\n Đây đúng loại lỗi chỉ lộ ở chế độ tối mà chế độ sáng không bao giờ thấy.\r\n -->\r\n @if (isOpen()) {\r\n <!--\r\n Ô TÌM Ở ĐẦU PANEL (`searchPosition=\"panel\"`).\r\n Đặt NGOÀI thẻ `role=\"listbox\"`: một `<input>` nằm trong listbox là sai\r\n ARIA — mọi con của listbox phải là `option` hoặc `group`. Đây là lối\r\n `ol-command` và combobox của reui dùng.\r\n -->\r\n <!--\r\n HỘP BỌC mang `#panel` và `position: absolute`.\r\n\r\n `#panel` phải ở ĐÂY chứ không ở thẻ `role=\"listbox\"`: `appendTo=\"body\"`\r\n đẩy chính phần tử `#panel` ra `<body>`. Để nó ở listbox thì ô tìm ở lại\r\n trong cây DOM cũ còn danh sách bay đi — hộp thoại vỡ làm đôi.\r\n -->\r\n <div\r\n #panel\r\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)]\"\r\n [class.w-full]=\"matchTriggerWidth()\"\r\n [class]=\"menuClass()\"\r\n >\r\n @if (searchable() && searchPosition() === 'panel') {\r\n <div class=\"border-b border-default-subtle p-2\">\r\n <!-- Icon/chữ đứng đầu ô TÌM — của việc TÌM, không phải của mục\r\n đang chọn. Bọc trong khung có viền để icon nằm TRONG ô, không\r\n đứng rời bên ngoài như một nhãn. -->\r\n <div\r\n class=\"flex items-center gap-2 rounded border border-default bg-neutral-primary px-2.5 py-1.5 focus-within:border-brand\"\r\n >\r\n @if (searchIcon()) {\r\n <ol-icon [name]=\"searchIcon()\" size=\"sm\" class=\"shrink-0 text-body-subtle\" />\r\n } @else if (searchPrefix()) {\r\n <span class=\"shrink-0 text-xs font-medium text-body-subtle\">{{\r\n searchPrefix()\r\n }}</span>\r\n }\r\n <input\r\n #oTimPanel\r\n type=\"text\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchInput($event)\"\r\n (click)=\"$event.stopPropagation()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [attr.aria-label]=\"searchPlaceholder()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n autocomplete=\"off\"\r\n class=\"w-full min-w-0 border-none bg-transparent p-0 text-sm outline-none placeholder:text-body-subtle\"\r\n />\r\n </div>\r\n </div>\r\n }\r\n <!--\r\n `overflow-x-hidden` KHAI RÕ: khai `overflow-y-auto` một mình thì trình\r\n duyệt tự tính `overflow-x` thành `auto` (spec CSS: một trục `visible`\r\n còn trục kia khác `visible` là không hợp lệ). Đo được `scrollWidth\r\n 765 > 620` khi option có `white-space: nowrap`.\r\n\r\n Chỉ chặn ở PANEL CHỌN có bề rộng cố định — nội dung phải vừa bề ngang.\r\n KHÔNG chặn ở khung chứa nội dung tự do (`device-mockup`,\r\n `chat-container`, `drawer`): ở đó người dùng chiếu nội dung bất kỳ\r\n vào, chặn là cắt mất phần rộng của họ.\r\n -->\r\n <div\r\n #listboxRef\r\n role=\"listbox\"\r\n [id]=\"listboxId()\"\r\n [attr.aria-multiselectable]=\"multiple() ? 'true' : null\"\r\n (scroll)=\"onPanelScroll($event)\"\r\n class=\"flex-1 min-h-0 overflow-y-auto overflow-x-hidden p-2\"\r\n >\r\n <!--\r\n Bốn trạng thái của danh sách lấy từ máy chủ (Figma `ol-dropdown`\r\n 5266:29107): Đang tải · Chưa gõ đủ · Rỗng/Không có kết quả · có mục.\r\n Đang tải đứng TRƯỚC rỗng: bản cũ báo \"không tìm thấy\" trong lúc\r\n câu trả lời còn chưa về — sai sự thật.\r\n -->\r\n @if (loading()) {\r\n <ol-dropdown-state type=\"loading\" />\r\n } @else if (waitingForTerm()) {\r\n <!-- Chưa gõ đủ `minTermLength`: \"không tìm thấy\" là sai sự thật, vì\r\n chưa tìm gì cả. Cùng lối `typeToSearchText` của ng-select. -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">{{ typeToSearchText() }}</div>\r\n } @else if (rows().length === 0) {\r\n <div class=\"text-center\">\r\n <ol-dropdown-state\r\n [type]=\"searchQuery().trim() ? 'no-results' : 'empty'\"\r\n [query]=\"searchQuery().trim()\"\r\n [heading]=\"emptyText()\"\r\n />\r\n @if (canCreate()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"createOption()\"\r\n class=\"-mt-2 mb-4 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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" />\r\n <span>{{ createLabel() }}</span>\r\n </button>\r\n }\r\n </div>\r\n } @else {\r\n <!-- Đệm trên: giữ thanh cuộn dài đúng như khi vẽ hết danh sách. -->\r\n @if (viewport().padTop > 0) {\r\n <div [style.height.px]=\"viewport().padTop\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n @for (row of viewport().rows; track row.kind + '-' + row.index + '-' + row.label) {\r\n @if (row.kind === 'group') {\r\n <!-- `dauTien` tính khi dựng danh sách, KHÔNG dùng `$first` của\r\n `@for`: khi `virtualScroll` bật, `$first` là dòng đầu của\r\n khung nhìn nên cuộn xuống là mất đường ngăn. -->\r\n @if (groupSeparator() && !row.isFirstGroup) {\r\n <div role=\"presentation\" class=\"my-1 border-t border-default-subtle\"></div>\r\n }\r\n <div\r\n role=\"presentation\"\r\n class=\"px-2.5 pt-2 pb-0 text-2xs font-semibold uppercase tracking-wide text-body-subtle\"\r\n >\r\n {{ row.label }}\r\n </div>\r\n } @else {\r\n <!--\r\n `rounded-sm` (6px) chứ không `rounded-md` (12px): Figma vẽ\r\n `ol-autocomplete-item` bo **8px**, mà thang token của lib không\r\n có bậc 8 (xs 4 · sm 6 · md/base 12 · lg 16). Lấy bậc GẦN NHẤT\r\n thay vì viết `rounded-[8px]` — giá trị thô nằm ngoài hệ token\r\n thì đổi theme không theo được.\r\n\r\n `ol-dropdown-item` cũng đã chốt `rounded-sm` cho mục menu, nên\r\n đây còn là giữ nhất quán trong lib.\r\n -->\r\n <!--\r\n `min-w-0` cho phép hàng CO lại, điều kiện cần để `truncate`\r\n của người dùng chạy. Nhưng nó KHÔNG tự chặn tràn: đo bằng cách\r\n tiêm `white-space: nowrap` vào hàng — panel vẫn `scrollWidth\r\n 1763 > 831`. Chỉ `overflow: hidden` đặt trên CHÍNH phần tử nội\r\n dung mới chặn được, mà phần tử đó do người dùng chiếu vào qua\r\n `optionTemplate` nên lib không với tới. `overflow-x-hidden` ở\r\n panel là lớp chặn cuối.\r\n -->\r\n <div\r\n role=\"option\"\r\n [id]=\"optionId(row.index)\"\r\n [attr.data-option-index]=\"row.index\"\r\n [attr.aria-selected]=\"isSelected(row.item)\"\r\n [attr.aria-disabled]=\"row.item?.disabled ? 'true' : null\"\r\n (click)=\"selectItem(row.item); $event.stopPropagation()\"\r\n [style.height.px]=\"virtualScroll() ? itemHeight() : null\"\r\n class=\"flex min-w-0 items-center gap-2 rounded-sm px-3 py-2 text-sm transition-colors\"\r\n [class.cursor-pointer]=\"!row.item?.disabled\"\r\n [class.cursor-not-allowed]=\"row.item?.disabled\"\r\n [class.bg-neutral-tertiary-medium]=\"\r\n activeIndex() === row.index && !row.item?.disabled\r\n \"\r\n [class.hover:bg-neutral-tertiary-medium]=\"\r\n !row.item?.disabled && activeIndex() !== row.index\r\n \"\r\n [class.text-heading]=\"isSelected(row.item) || activeIndex() === row.index\"\r\n [class.hover:text-heading]=\"!row.item?.disabled\"\r\n [class.font-medium]=\"isSelected(row.item)\"\r\n [class.text-body]=\"!isSelected(row.item) && activeIndex() !== row.index\"\r\n [class.text-fg-disabled]=\"row.item?.disabled\"\r\n >\r\n @if (optionTemplate) {\r\n <ng-container\r\n *ngTemplateOutlet=\"optionTemplate; context: { $implicit: row.item }\"\r\n />\r\n } @else {\r\n <!-- Phần đầu: `icon` ưu tiên, không có thì `prefix` (chữ).\r\n `prefixClass` nhận CLASS chứ không nhận mã màu — mã màu\r\n cứng không đổi theo theme và theo chế độ tối. -->\r\n @if (row.item?.icon) {\r\n <ol-icon\r\n [name]=\"row.item.icon\"\r\n size=\"sm\"\r\n class=\"shrink-0\"\r\n [class]=\"row.item.prefixClass || ''\"\r\n />\r\n } @else if (row.item?.prefix) {\r\n <span\r\n class=\"shrink-0 text-2xs font-semibold tabular-nums\"\r\n [class]=\"row.item.prefixClass || 'text-body-subtle'\"\r\n >{{ row.item.prefix }}</span\r\n >\r\n }\r\n <span class=\"min-w-0 flex-1 truncate\">{{ row.label }}</span>\r\n }\r\n @if (isSelected(row.item)) {\r\n <ol-icon name=\"check\" size=\"sm\" class=\"shrink-0 text-fg-brand\" />\r\n }\r\n </div>\r\n }\r\n }\r\n\r\n <!-- Đệm dưới — xem lý do ở đệm trên. -->\r\n @if (viewport().padBottom > 0) {\r\n <div [style.height.px]=\"viewport().padBottom\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n <!-- Danh sách CÒN mục nhưng chữ đang gõ chưa trùng nhãn nào: vẫn phải\r\n cho tạo. Ví dụ đã có \"Toán 10\" mà người dùng gõ \"Toán 11\" —\r\n nhánh rỗng ở trên không chạy vì bộ lọc vẫn ra một mục. -->\r\n @if (canCreate()) {\r\n <div class=\"mt-1 border-t border-default-subtle p-2\">\r\n <button\r\n type=\"button\"\r\n (click)=\"createOption()\"\r\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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" class=\"shrink-0\" />\r\n <span class=\"truncate\">{{ createLabel() }}</span>\r\n </button>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (helperText()) {\r\n <p\r\n class=\"mt-1.5 text-xs\"\r\n [class.text-body]=\"state() === 'default'\"\r\n [class.text-fg-danger-strong]=\"state() === 'error'\"\r\n [class.text-fg-success-strong]=\"state() === 'success'\"\r\n >\r\n {{ helperText() }}\r\n </p>\r\n }\r\n</div>\r\n"
|
|
15704
15859
|
}]
|
|
15705
15860
|
}],
|
|
15706
15861
|
ctorParameters: () => [],
|
|
@@ -16296,7 +16451,10 @@ var OlCheckbox = class OlCheckbox {
|
|
|
16296
16451
|
provide: NG_VALUE_ACCESSOR,
|
|
16297
16452
|
useExisting: forwardRef(() => OlCheckbox),
|
|
16298
16453
|
multi: true
|
|
16299
|
-
}
|
|
16454
|
+
}, provideOlIcons({
|
|
16455
|
+
"check": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M20 6L9 17l-5-5\"/></svg>",
|
|
16456
|
+
"minus": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M6 12h12\"/></svg>"
|
|
16457
|
+
})],
|
|
16300
16458
|
viewQueries: [{
|
|
16301
16459
|
propertyName: "nativeInput",
|
|
16302
16460
|
first: true,
|
|
@@ -16374,7 +16532,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
16374
16532
|
provide: NG_VALUE_ACCESSOR,
|
|
16375
16533
|
useExisting: forwardRef(() => OlCheckbox),
|
|
16376
16534
|
multi: true
|
|
16377
|
-
}
|
|
16535
|
+
}, provideOlIcons({
|
|
16536
|
+
"check": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M20 6L9 17l-5-5\"/></svg>",
|
|
16537
|
+
"minus": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M6 12h12\"/></svg>"
|
|
16538
|
+
})],
|
|
16378
16539
|
template: "@if (standalone()) {\r\n <!-- Standalone Checkbox (no label wrapper) -->\r\n <input\r\n #nativeInput\r\n [id]=\"inputId()\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n type=\"checkbox\"\r\n [ngModel]=\"checked()\"\r\n (ngModelChange)=\"onModelChange($event)\"\r\n [disabled]=\"disabled()\"\r\n class=\"sr-only\"\r\n />\r\n <div\r\n class=\"rounded border transition-colors duration-150 flex items-center justify-center shrink-0 cursor-pointer\"\r\n [style.border-radius.px]=\"4\"\r\n [class.w-4]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.h-4]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.w-5]=\"resolvedSize() === 'lg'\"\r\n [class.h-5]=\"resolvedSize() === 'lg'\"\r\n [class.bg-brand]=\"(checked() || indeterminate()) && !disabled()\"\r\n [class.border-brand]=\"(checked() || indeterminate()) && !disabled()\"\r\n [class.bg-white]=\"!checked() && !indeterminate() && !disabled()\"\r\n [class.border-default-strong]=\"(!checked() && !indeterminate() && !disabled()) || disabled()\"\r\n [class.bg-neutral-secondary-strong]=\"disabled()\"\r\n [class.opacity-50]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n (click)=\"!disabled() && onModelChange(!checked())\"\r\n >\r\n <!--\r\n Ba trạng thái loại trừ nhau. Trước đây `check` render VÔ ĐIỀU KIỆN rồi\r\n `minus` chồng thêm khi indeterminate: đo /checkbox-radio thấy hai icon\r\n 14px nằm CẠNH nhau (x=371 và x=385) trong ô 16px — ô \"một phần\" hiện cả\r\n tick lẫn gạch và tràn ra ngoài. Ô chưa chọn cũng luôn có tick trắng, chỉ\r\n tình cờ trùng màu nền trắng nên không ai thấy.\r\n -->\r\n @if (indeterminate()) {\r\n <ol-icon name=\"minus\" size=\"xs\" color=\"white\" />\r\n } @else if (checked()) {\r\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" />\r\n }\r\n </div>\r\n} @else if (card()) {\r\n <!-- Card Checkbox Layout -->\r\n <!--\r\n Nền THẺ dùng `bg-neutral-primary`, KHÔNG phải `bg-white`: `bg-white` ghim\r\n #FFFFFF ở cả hai chế độ, nên ở chế độ tối thẻ trắng còn nhãn\r\n `text-heading` cũng trắng — tương phản **1,00**. Đo `/checkbox-radio` mục\r\n \"Gói dịch vụ\". Ô VUÔNG của checkbox vẫn giữ `bg-white`, nó phải trắng ở\r\n cả hai chế độ.\r\n -->\r\n <label\r\n [for]=\"inputId()\"\r\n class=\"relative flex items-start gap-3.5 p-4 border rounded-md cursor-pointer shadow-xs select-none transition-colors duration-150\"\r\n [class.bg-brand-softer]=\"checked() && !disabled()\"\r\n [class.border-brand]=\"checked() && !disabled()\"\r\n [class.bg-neutral-primary]=\"!checked() && !disabled()\"\r\n [class.border-default-strong]=\"!checked() && !disabled()\"\r\n [class.hover:border-default-strong]=\"!checked() && !disabled()\"\r\n [class.opacity-60]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n [class.bg-neutral-secondary-strong]=\"disabled()\"\r\n >\r\n <!-- Hidden native input for form binding & accessibility -->\r\n <input\r\n #nativeInput\r\n [id]=\"inputId()\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n type=\"checkbox\"\r\n [ngModel]=\"checked()\"\r\n (ngModelChange)=\"onModelChange($event)\"\r\n [disabled]=\"disabled()\"\r\n class=\"sr-only\"\r\n />\r\n\r\n <!-- Custom styled checkbox box -->\r\n <div class=\"flex items-center h-5 mt-0.5 shrink-0\">\r\n <div\r\n class=\"rounded-xs border transition-colors duration-150 flex items-center justify-center shrink-0\"\r\n [class.w-4]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.h-4]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.w-5]=\"resolvedSize() === 'lg'\"\r\n [class.h-5]=\"resolvedSize() === 'lg'\"\r\n [class.bg-brand]=\"(checked() || indeterminate()) && !disabled()\"\r\n [class.border-brand]=\"(checked() || indeterminate()) && !disabled()\"\r\n [class.bg-white]=\"!checked() && !indeterminate() && !disabled()\"\r\n [class.border-default-strong]=\"!checked() && !indeterminate() && !disabled()\"\r\n [class.bg-neutral-secondary-strong]=\"disabled()\"\r\n [class.border-border-light]=\"disabled()\"\r\n >\r\n <!--\r\n Ba trạng thái loại trừ nhau. Trước đây `check` render VÔ ĐIỀU KIỆN rồi\r\n `minus` chồng thêm khi indeterminate: đo /checkbox-radio thấy hai icon\r\n 14px nằm CẠNH nhau (x=371 và x=385) trong ô 16px — ô \"một phần\" hiện cả\r\n tick lẫn gạch và tràn ra ngoài. Ô chưa chọn cũng luôn có tick trắng, chỉ\r\n tình cờ trùng màu nền trắng nên không ai thấy.\r\n -->\r\n @if (indeterminate()) {\r\n <ol-icon name=\"minus\" size=\"xs\" color=\"white\" />\r\n } @else if (checked()) {\r\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" />\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex-1 min-w-0\">\r\n <ng-content />\r\n @if (label()) {\r\n <span\r\n class=\"font-medium block select-none\"\r\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.text-base]=\"resolvedSize() === 'lg'\"\r\n [class.text-heading]=\"!disabled()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n >\r\n {{ label() }}\r\n </span>\r\n }\r\n @if (description()) {\r\n <p class=\"text-xs text-body-subtle mt-0.5 select-none\">{{ description() }}</p>\r\n }\r\n </div>\r\n </label>\r\n} @else {\r\n <!-- Standard Checkbox Layout -->\r\n <label\r\n [for]=\"inputId()\"\r\n class=\"inline-flex items-start cursor-pointer select-none group\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n >\r\n <!-- Hidden native input -->\r\n <input\r\n #nativeInput\r\n [id]=\"inputId()\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n type=\"checkbox\"\r\n [ngModel]=\"checked()\"\r\n (ngModelChange)=\"onModelChange($event)\"\r\n [disabled]=\"disabled()\"\r\n class=\"sr-only\"\r\n />\r\n\r\n <!-- Custom styled checkbox box -->\r\n <div class=\"flex items-center h-5 shrink-0\">\r\n <div\r\n class=\"rounded-xs border transition-colors duration-150 flex items-center justify-center shrink-0 group-hover:border-brand\"\r\n [class.w-4]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.h-4]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.w-5]=\"resolvedSize() === 'lg'\"\r\n [class.h-5]=\"resolvedSize() === 'lg'\"\r\n [class.bg-brand]=\"(checked() || indeterminate()) && !disabled()\"\r\n [class.border-brand]=\"(checked() || indeterminate()) && !disabled()\"\r\n [class.bg-white]=\"!checked() && !indeterminate() && !disabled()\"\r\n [class.border-default-strong]=\"!checked() && !indeterminate() && !disabled()\"\r\n [class.bg-neutral-secondary-strong]=\"disabled()\"\r\n [class.border-border-light]=\"disabled()\"\r\n [class.opacity-50]=\"disabled()\"\r\n >\r\n <!--\r\n Ba trạng thái loại trừ nhau. Trước đây `check` render VÔ ĐIỀU KIỆN rồi\r\n `minus` chồng thêm khi indeterminate: đo /checkbox-radio thấy hai icon\r\n 14px nằm CẠNH nhau (x=371 và x=385) trong ô 16px — ô \"một phần\" hiện cả\r\n tick lẫn gạch và tràn ra ngoài. Ô chưa chọn cũng luôn có tick trắng, chỉ\r\n tình cờ trùng màu nền trắng nên không ai thấy.\r\n -->\r\n @if (indeterminate()) {\r\n <ol-icon name=\"minus\" size=\"xs\" color=\"white\" />\r\n } @else if (checked()) {\r\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" />\r\n }\r\n </div>\r\n </div>\r\n\r\n @if (label() || description()) {\r\n <div class=\"ms-2.5\">\r\n @if (label()) {\r\n <span\r\n class=\"font-medium block transition-colors group-hover:text-fg-brand\"\r\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.text-base]=\"resolvedSize() === 'lg'\"\r\n [class.text-heading]=\"!disabled()\"\r\n [class.text-fg-disabled]=\"disabled()\"\r\n >\r\n {{ label() }}\r\n </span>\r\n }\r\n @if (description()) {\r\n <p class=\"text-xs text-body-subtle mt-0.5\">\r\n {{ description() }}\r\n </p>\r\n }\r\n </div>\r\n }\r\n </label>\r\n}\r\n"
|
|
16379
16540
|
}]
|
|
16380
16541
|
}],
|
|
@@ -17580,13 +17741,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
17580
17741
|
selector: "ol-segmented",
|
|
17581
17742
|
standalone: true,
|
|
17582
17743
|
imports: [OlIcon],
|
|
17583
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17584
|
-
host: { "[class]": "hostClasses()" },
|
|
17585
17744
|
providers: [{
|
|
17586
17745
|
provide: NG_VALUE_ACCESSOR,
|
|
17587
17746
|
useExisting: forwardRef(() => OlSegmented),
|
|
17588
17747
|
multi: true
|
|
17589
17748
|
}],
|
|
17749
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17750
|
+
host: { "[class]": "hostClasses()" },
|
|
17590
17751
|
template: "<div\r\n role=\"radiogroup\"\r\n [attr.aria-label]=\"ariaLabel() || null\"\r\n [attr.aria-disabled]=\"disabled() || null\"\r\n [class]=\"trackClasses()\"\r\n>\r\n @for (option of options(); track option.value; let i = $index) {\r\n <button\r\n type=\"button\"\r\n role=\"radio\"\r\n [attr.aria-checked]=\"option.value === value()\"\r\n [disabled]=\"option.disabled || disabled()\"\r\n [tabindex]=\"i === focusIndex() ? 0 : -1\"\r\n [class]=\"itemClasses(option)\"\r\n (click)=\"select(option)\"\r\n (keydown)=\"onKeydown($event, i)\"\r\n >\r\n @if (option.icon) {\r\n <ol-icon [name]=\"option.icon\" size=\"sm\" class=\"shrink-0\" />\r\n }\r\n <span class=\"truncate\">{{ option.label }}</span>\r\n </button>\r\n }\r\n</div>\r\n"
|
|
17591
17752
|
}]
|
|
17592
17753
|
}],
|
|
@@ -18312,8 +18473,16 @@ var OlFileItem = class OlFileItem {
|
|
|
18312
18473
|
retry: "retry"
|
|
18313
18474
|
},
|
|
18314
18475
|
host: { classAttribute: "block min-w-0" },
|
|
18315
|
-
|
|
18316
|
-
|
|
18476
|
+
providers: [provideOlIcons({
|
|
18477
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
18478
|
+
"solar:eye-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 5C7 5 2.73 8.11 1 12.5c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5M12 15c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3\"/></svg>",
|
|
18479
|
+
"solar:refresh-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M4 12a8 8 0 0 1 15.5-1m0 10a8 8 0 0 1-15.5 1M9 21h6M9 3h6\"/></svg>",
|
|
18480
|
+
"solar:trash-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M20 6h-16m2-2h12a1 1 0 0 1 1 1v14a2 2 0 0 1-2 2h-10a2 2 0 0 1-2-2v-14a1 1 0 0 1 1-1M9 11v6M15 11v6\"/></svg>",
|
|
18481
|
+
"check-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M8.5 12.5L10.5 14.5L15.5 9.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>",
|
|
18482
|
+
"danger-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M12 7V13\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/><circle cx=\"12\" cy=\"16\" r=\"1\" fill=\"currentColor\"/></svg>"
|
|
18483
|
+
})],
|
|
18484
|
+
ngImport: i0,
|
|
18485
|
+
template: "@switch (variant()) {\r\n @case ('chip') {\r\n <span\r\n class=\"inline-flex max-w-full items-center gap-1 rounded-md border border-default bg-neutral-primary py-0.5 ps-1 pe-1 text-sm text-heading\"\r\n >\r\n @if (previewable()) {\r\n <button\r\n type=\"button\"\r\n class=\"inline-flex size-6 shrink-0 items-center justify-center rounded-sm text-fg-brand hover:bg-neutral-tertiary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xem ' + file().name\"\r\n (click)=\"preview.emit(file())\"\r\n >\r\n <ol-icon name=\"solar:eye_bold\" [size]=\"14\" />\r\n </button>\r\n } @else {\r\n <ol-icon [name]=\"meta().icon\" [size]=\"14\" class=\"ms-1 shrink-0 text-body-subtle\" />\r\n }\r\n <span class=\"min-w-0 truncate\" [title]=\"file().name\">{{ file().name }}</span>\r\n @if (removable()) {\r\n <button\r\n type=\"button\"\r\n class=\"inline-flex size-6 shrink-0 items-center justify-center rounded-sm text-body-subtle hover:bg-neutral-tertiary hover:text-heading focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xoá ' + file().name\"\r\n (click)=\"remove.emit(file())\"\r\n >\r\n <ol-icon name=\"close\" [size]=\"12\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n\r\n @case ('card') {\r\n <div\r\n class=\"relative flex min-h-19 items-center justify-center gap-2 rounded-md border border-default bg-neutral-primary px-10 py-4\"\r\n >\r\n <ol-icon [name]=\"meta().icon\" [size]=\"20\" class=\"shrink-0 text-fg-brand-strong\" />\r\n <span class=\"min-w-0 truncate text-sm font-medium text-fg-brand-strong\" [title]=\"file().name\">\r\n {{ file().name }}\r\n </span>\r\n @if (removable()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"actionClass + ' absolute end-1.5 top-1.5'\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xoá ' + file().name\"\r\n (click)=\"remove.emit(file())\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @default {\r\n <div [class]=\"rowClass()\">\r\n <ol-icon-shape [icon]=\"meta().icon\" [color]=\"meta().color\" type=\"square\" size=\"sm\" />\r\n\r\n <div class=\"min-w-0 flex-1\">\r\n <div class=\"flex items-center gap-1.5\">\r\n <span class=\"min-w-0 truncate text-sm font-medium text-heading\" [title]=\"file().name\">\r\n {{ file().name }}\r\n </span>\r\n @if (status() === 'success') {\r\n <ol-icon name=\"solar:check_circle_bold\" [size]=\"16\" class=\"shrink-0 text-fg-success\" />\r\n } @else if (status() === 'error') {\r\n <ol-icon name=\"solar:danger_circle_bold\" [size]=\"16\" class=\"shrink-0 text-fg-danger\" />\r\n }\r\n </div>\r\n <!-- aria-live: trình đọc màn hình đọc \"Đã tải lên\" / lỗi khi trạng thái đổi. -->\r\n <p [class]=\"metaClass()\" aria-live=\"polite\">{{ metaText() }}</p>\r\n @if (status() === 'uploading') {\r\n <ol-progress\r\n class=\"mt-1.5 block\"\r\n size=\"sm\"\r\n [progress]=\"progress()\"\r\n [attr.aria-label]=\"'Tiến trình tải ' + file().name\"\r\n />\r\n }\r\n </div>\r\n\r\n <div class=\"flex shrink-0 items-center gap-0.5\">\r\n @if (previewable() && status() !== 'uploading') {\r\n <button\r\n type=\"button\"\r\n [class]=\"actionClass\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xem ' + file().name\"\r\n (click)=\"preview.emit(file())\"\r\n >\r\n <ol-icon name=\"solar:eye_bold\" size=\"sm\" />\r\n </button>\r\n }\r\n @if (status() === 'error') {\r\n <button\r\n type=\"button\"\r\n [class]=\"actionClass\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Thử tải lại ' + file().name\"\r\n (click)=\"retry.emit(file())\"\r\n >\r\n <ol-icon name=\"solar:refresh_bold\" size=\"sm\" />\r\n </button>\r\n }\r\n @if (removable()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"actionClass\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"(status() === 'uploading' ? 'Huỷ tải ' : 'Xoá ') + file().name\"\r\n (click)=\"remove.emit(file())\"\r\n >\r\n <ol-icon\r\n [name]=\"status() === 'uploading' ? 'close' : 'solar:trash-bin-trash-bold'\"\r\n size=\"sm\"\r\n />\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n}\r\n",
|
|
18317
18486
|
dependencies: [
|
|
18318
18487
|
{
|
|
18319
18488
|
kind: "component",
|
|
@@ -18372,7 +18541,15 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
18372
18541
|
],
|
|
18373
18542
|
host: { class: "block min-w-0" },
|
|
18374
18543
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
18375
|
-
|
|
18544
|
+
providers: [provideOlIcons({
|
|
18545
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
18546
|
+
"solar:eye-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 5C7 5 2.73 8.11 1 12.5c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5M12 15c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3\"/></svg>",
|
|
18547
|
+
"solar:refresh-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M4 12a8 8 0 0 1 15.5-1m0 10a8 8 0 0 1-15.5 1M9 21h6M9 3h6\"/></svg>",
|
|
18548
|
+
"solar:trash-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M20 6h-16m2-2h12a1 1 0 0 1 1 1v14a2 2 0 0 1-2 2h-10a2 2 0 0 1-2-2v-14a1 1 0 0 1 1-1M9 11v6M15 11v6\"/></svg>",
|
|
18549
|
+
"check-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M8.5 12.5L10.5 14.5L15.5 9.5\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>",
|
|
18550
|
+
"danger-circle": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path d=\"M12 7V13\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/><circle cx=\"12\" cy=\"16\" r=\"1\" fill=\"currentColor\"/></svg>"
|
|
18551
|
+
})],
|
|
18552
|
+
template: "@switch (variant()) {\r\n @case ('chip') {\r\n <span\r\n class=\"inline-flex max-w-full items-center gap-1 rounded-md border border-default bg-neutral-primary py-0.5 ps-1 pe-1 text-sm text-heading\"\r\n >\r\n @if (previewable()) {\r\n <button\r\n type=\"button\"\r\n class=\"inline-flex size-6 shrink-0 items-center justify-center rounded-sm text-fg-brand hover:bg-neutral-tertiary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xem ' + file().name\"\r\n (click)=\"preview.emit(file())\"\r\n >\r\n <ol-icon name=\"solar:eye_bold\" [size]=\"14\" />\r\n </button>\r\n } @else {\r\n <ol-icon [name]=\"meta().icon\" [size]=\"14\" class=\"ms-1 shrink-0 text-body-subtle\" />\r\n }\r\n <span class=\"min-w-0 truncate\" [title]=\"file().name\">{{ file().name }}</span>\r\n @if (removable()) {\r\n <button\r\n type=\"button\"\r\n class=\"inline-flex size-6 shrink-0 items-center justify-center rounded-sm text-body-subtle hover:bg-neutral-tertiary hover:text-heading focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xoá ' + file().name\"\r\n (click)=\"remove.emit(file())\"\r\n >\r\n <ol-icon name=\"close\" [size]=\"12\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n\r\n @case ('card') {\r\n <div\r\n class=\"relative flex min-h-19 items-center justify-center gap-2 rounded-md border border-default bg-neutral-primary px-10 py-4\"\r\n >\r\n <ol-icon [name]=\"meta().icon\" [size]=\"20\" class=\"shrink-0 text-fg-brand-strong\" />\r\n <span class=\"min-w-0 truncate text-sm font-medium text-fg-brand-strong\" [title]=\"file().name\">\r\n {{ file().name }}\r\n </span>\r\n @if (removable()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"actionClass + ' absolute end-1.5 top-1.5'\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xoá ' + file().name\"\r\n (click)=\"remove.emit(file())\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </div>\r\n }\r\n\r\n @default {\r\n <div [class]=\"rowClass()\">\r\n <ol-icon-shape [icon]=\"meta().icon\" [color]=\"meta().color\" type=\"square\" size=\"sm\" />\r\n\r\n <div class=\"min-w-0 flex-1\">\r\n <div class=\"flex items-center gap-1.5\">\r\n <span class=\"min-w-0 truncate text-sm font-medium text-heading\" [title]=\"file().name\">\r\n {{ file().name }}\r\n </span>\r\n @if (status() === 'success') {\r\n <ol-icon name=\"solar:check_circle_bold\" [size]=\"16\" class=\"shrink-0 text-fg-success\" />\r\n } @else if (status() === 'error') {\r\n <ol-icon name=\"solar:danger_circle_bold\" [size]=\"16\" class=\"shrink-0 text-fg-danger\" />\r\n }\r\n </div>\r\n <!-- aria-live: trình đọc màn hình đọc \"Đã tải lên\" / lỗi khi trạng thái đổi. -->\r\n <p [class]=\"metaClass()\" aria-live=\"polite\">{{ metaText() }}</p>\r\n @if (status() === 'uploading') {\r\n <ol-progress\r\n class=\"mt-1.5 block\"\r\n size=\"sm\"\r\n [progress]=\"progress()\"\r\n [attr.aria-label]=\"'Tiến trình tải ' + file().name\"\r\n />\r\n }\r\n </div>\r\n\r\n <div class=\"flex shrink-0 items-center gap-0.5\">\r\n @if (previewable() && status() !== 'uploading') {\r\n <button\r\n type=\"button\"\r\n [class]=\"actionClass\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xem ' + file().name\"\r\n (click)=\"preview.emit(file())\"\r\n >\r\n <ol-icon name=\"solar:eye_bold\" size=\"sm\" />\r\n </button>\r\n }\r\n @if (status() === 'error') {\r\n <button\r\n type=\"button\"\r\n [class]=\"actionClass\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Thử tải lại ' + file().name\"\r\n (click)=\"retry.emit(file())\"\r\n >\r\n <ol-icon name=\"solar:refresh_bold\" size=\"sm\" />\r\n </button>\r\n }\r\n @if (removable()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"actionClass\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"(status() === 'uploading' ? 'Huỷ tải ' : 'Xoá ') + file().name\"\r\n (click)=\"remove.emit(file())\"\r\n >\r\n <ol-icon\r\n [name]=\"status() === 'uploading' ? 'close' : 'solar:trash-bin-trash-bold'\"\r\n size=\"sm\"\r\n />\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n}\r\n"
|
|
18376
18553
|
}]
|
|
18377
18554
|
}],
|
|
18378
18555
|
propDecorators: {
|
|
@@ -18869,8 +19046,8 @@ var OlImagePreview = class OlImagePreview {
|
|
|
18869
19046
|
pinchStart = null;
|
|
18870
19047
|
moved = 0;
|
|
18871
19048
|
suppressClick = false;
|
|
18872
|
-
focusTrap
|
|
18873
|
-
overlayContainer
|
|
19049
|
+
focusTrap;
|
|
19050
|
+
overlayContainer;
|
|
18874
19051
|
releaseEscape = null;
|
|
18875
19052
|
count = computed(() => this.images().length, ...ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []);
|
|
18876
19053
|
current = computed(() => {
|
|
@@ -18879,6 +19056,8 @@ var OlImagePreview = class OlImagePreview {
|
|
|
18879
19056
|
}, ...ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []);
|
|
18880
19057
|
image = computed(() => this.images()[this.current()], ...ngDevMode ? [{ debugName: "image" }] : /* istanbul ignore next */ []);
|
|
18881
19058
|
constructor() {
|
|
19059
|
+
this.focusTrap = inject(OlFocusTrapService);
|
|
19060
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
18882
19061
|
effect(() => {
|
|
18883
19062
|
const el = this.panel()?.nativeElement;
|
|
18884
19063
|
if (this.open() && el && this.count()) {
|
|
@@ -19080,6 +19259,11 @@ var OlImagePreview = class OlImagePreview {
|
|
|
19080
19259
|
index: "indexChange",
|
|
19081
19260
|
closed: "closed"
|
|
19082
19261
|
},
|
|
19262
|
+
providers: [provideOlIcons({
|
|
19263
|
+
"lucide:close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
19264
|
+
"lucide:zoom-in": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"2\" d=\"m21 21l-4.35-4.35M11 8v6m3 0h-6\"/></svg>",
|
|
19265
|
+
"lucide:zoom-out": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"2\" d=\"m21 21l-4.35-4.35M8 11h6\"/></svg>"
|
|
19266
|
+
})],
|
|
19083
19267
|
viewQueries: [
|
|
19084
19268
|
{
|
|
19085
19269
|
propertyName: "panel",
|
|
@@ -19104,7 +19288,7 @@ var OlImagePreview = class OlImagePreview {
|
|
|
19104
19288
|
}
|
|
19105
19289
|
],
|
|
19106
19290
|
ngImport: i0,
|
|
19107
|
-
template: "@if (open() && image(); as img) {\r\n <div\r\n #panel\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'Xem ảnh ' + (current() + 1) + ' / ' + count()\"\r\n class=\"fixed inset-0 z-modal flex flex-col bg-overlay-backdrop backdrop-blur-xs outline-hidden\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n <!-- Thanh trên: số thứ tự · phóng/thu · đóng. -->\r\n <div class=\"flex items-center justify-between gap-3 px-4 py-3 text-fg-white\">\r\n <span class=\"text-sm font-medium tabular-nums\" aria-live=\"polite\">\r\n @if (count() > 1) {\r\n {{ current() + 1 }} / {{ count() }}\r\n }\r\n </span>\r\n <div\r\n class=\"ms-auto flex items-center gap-1 rounded-full bg-neutral-primary p-1 text-heading shadow-lg\"\r\n >\r\n <button\r\n type=\"button\"\r\n aria-label=\"Thu nhỏ\"\r\n class=\"inline-flex size-8 items-center justify-center rounded-full transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle disabled:cursor-not-allowed disabled:opacity-40\"\r\n [disabled]=\"!canZoomOut()\"\r\n (click)=\"zoomOut()\"\r\n >\r\n <ol-icon name=\"lucide:
|
|
19291
|
+
template: "@if (open() && image(); as img) {\r\n <div\r\n #panel\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'Xem ảnh ' + (current() + 1) + ' / ' + count()\"\r\n class=\"fixed inset-0 z-modal flex flex-col bg-overlay-backdrop backdrop-blur-xs outline-hidden\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n <!-- Thanh trên: số thứ tự · phóng/thu · đóng. -->\r\n <div class=\"flex items-center justify-between gap-3 px-4 py-3 text-fg-white\">\r\n <span class=\"text-sm font-medium tabular-nums\" aria-live=\"polite\">\r\n @if (count() > 1) {\r\n {{ current() + 1 }} / {{ count() }}\r\n }\r\n </span>\r\n <div\r\n class=\"ms-auto flex items-center gap-1 rounded-full bg-neutral-primary p-1 text-heading shadow-lg\"\r\n >\r\n <button\r\n type=\"button\"\r\n aria-label=\"Thu nhỏ\"\r\n class=\"inline-flex size-8 items-center justify-center rounded-full transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle disabled:cursor-not-allowed disabled:opacity-40\"\r\n [disabled]=\"!canZoomOut()\"\r\n (click)=\"zoomOut()\"\r\n >\r\n <ol-icon name=\"lucide:zoom_out\" size=\"sm\" />\r\n </button>\r\n <button\r\n type=\"button\"\r\n aria-label=\"Về cỡ gốc\"\r\n class=\"min-w-12 rounded-full px-1 text-center text-xs font-semibold tabular-nums transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n (click)=\"resetZoom()\"\r\n >\r\n {{ zoomPercent() }}%\r\n </button>\r\n <button\r\n type=\"button\"\r\n aria-label=\"Phóng to\"\r\n class=\"inline-flex size-8 items-center justify-center rounded-full transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle disabled:cursor-not-allowed disabled:opacity-40\"\r\n [disabled]=\"!canZoomIn()\"\r\n (click)=\"zoomIn()\"\r\n >\r\n <ol-icon name=\"lucide:zoom_in\" size=\"sm\" />\r\n </button>\r\n </div>\r\n <button\r\n type=\"button\"\r\n data-ol-dismiss\r\n aria-label=\"Đóng xem ảnh\"\r\n class=\"inline-flex size-10 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-lg transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n (click)=\"close()\"\r\n >\r\n <ol-icon name=\"close\" size=\"base\" />\r\n </button>\r\n </div>\r\n\r\n <!--\r\n Ảnh: vừa khung, không cắt (object-contain). Bấm nền tối quanh ảnh để đóng.\r\n `overflow-hidden`: ảnh phóng to không được tràn đè lên thanh trên và dải\r\n ảnh nhỏ — đo 2026-09-28, ảnh 364% che nút \"Phóng to\", không bấm được.\r\n `tabindex=\"-1\"` ở khung ngoài: bấm vào ảnh (không focus được) thì tiêu\r\n điểm ở lại trình xem, phím + − 0 ← → vẫn chạy.\r\n -->\r\n <div\r\n class=\"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden px-4 pb-4\"\r\n (click)=\"onBackdropClick($event)\"\r\n >\r\n <img\r\n #mainImage\r\n [src]=\"img.src\"\r\n [alt]=\"img.alt || ''\"\r\n [class]=\"imageClasses()\"\r\n [style]=\"imageStyle()\"\r\n draggable=\"false\"\r\n tabindex=\"0\"\r\n (click)=\"onImageClick($event)\"\r\n (keydown.enter)=\"toggleZoom(); $event.preventDefault()\"\r\n (keydown.space)=\"toggleZoom(); $event.preventDefault()\"\r\n (wheel)=\"onWheel($event)\"\r\n (pointerdown)=\"onPointerDown($event)\"\r\n (pointermove)=\"onPointerMove($event)\"\r\n (pointerup)=\"onPointerUp($event)\"\r\n (pointercancel)=\"onPointerUp($event)\"\r\n />\r\n\r\n @if (count() > 1) {\r\n <button\r\n type=\"button\"\r\n aria-label=\"Ảnh trước\"\r\n class=\"absolute start-3 top-1/2 inline-flex size-11 -translate-y-1/2 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-lg transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n (click)=\"prev()\"\r\n >\r\n <ol-icon name=\"chevron_left\" size=\"base\" />\r\n </button>\r\n <button\r\n type=\"button\"\r\n aria-label=\"Ảnh sau\"\r\n class=\"absolute end-3 top-1/2 inline-flex size-11 -translate-y-1/2 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-lg transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n (click)=\"next()\"\r\n >\r\n <ol-icon name=\"chevron_right\" size=\"base\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (img.alt) {\r\n <p class=\"truncate px-4 pb-3 text-center text-sm text-fg-white\">{{ img.alt }}</p>\r\n }\r\n\r\n <!--\r\n Dải ảnh nhỏ: thấy các ảnh còn lại, bấm để nhảy tới; ảnh đang xem có viền\r\n và tự cuộn vào tầm nhìn. Cuộn ngang khi dài (điện thoại), căn giữa khi\r\n ngắn — bằng ms-auto/me-auto ở ô đầu/cuối, KHÔNG justify-center: nó cắt\r\n mất các ô đầu khi dải tràn và không cuộn tới được. Lề dưới max() với\r\n safe-area: chừa vạch Home của iPhone.\r\n -->\r\n @if (count() > 1) {\r\n <div\r\n #thumbStrip\r\n class=\"flex shrink-0 gap-2 overflow-x-auto px-4 pt-1 pb-[max(1rem,env(safe-area-inset-bottom))] [scrollbar-width:none] [&>:first-child]:ms-auto [&>:last-child]:me-auto\"\r\n role=\"group\"\r\n aria-label=\"Danh sách ảnh\"\r\n >\r\n @for (thumb of images(); track $index; let i = $index) {\r\n <button\r\n type=\"button\"\r\n [attr.aria-label]=\"'Xem ảnh ' + (i + 1) + (thumb.alt ? ': ' + thumb.alt : '')\"\r\n [attr.aria-current]=\"i === current() ? 'true' : null\"\r\n [class]=\"thumbClasses(i)\"\r\n (click)=\"index.set(i)\"\r\n >\r\n <img [src]=\"thumb.src\" alt=\"\" class=\"size-full object-cover\" loading=\"lazy\" />\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n}\r\n",
|
|
19108
19292
|
dependencies: [{
|
|
19109
19293
|
kind: "component",
|
|
19110
19294
|
type: OlIcon,
|
|
@@ -19132,7 +19316,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
19132
19316
|
standalone: true,
|
|
19133
19317
|
imports: [OlIcon],
|
|
19134
19318
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19135
|
-
|
|
19319
|
+
providers: [provideOlIcons({
|
|
19320
|
+
"lucide:close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
19321
|
+
"lucide:zoom-in": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"2\" d=\"m21 21l-4.35-4.35M11 8v6m3 0h-6\"/></svg>",
|
|
19322
|
+
"lucide:zoom-out": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"2\" d=\"m21 21l-4.35-4.35M8 11h6\"/></svg>"
|
|
19323
|
+
})],
|
|
19324
|
+
template: "@if (open() && image(); as img) {\r\n <div\r\n #panel\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n tabindex=\"-1\"\r\n [attr.aria-label]=\"'Xem ảnh ' + (current() + 1) + ' / ' + count()\"\r\n class=\"fixed inset-0 z-modal flex flex-col bg-overlay-backdrop backdrop-blur-xs outline-hidden\"\r\n (keydown)=\"onKeydown($event)\"\r\n >\r\n <!-- Thanh trên: số thứ tự · phóng/thu · đóng. -->\r\n <div class=\"flex items-center justify-between gap-3 px-4 py-3 text-fg-white\">\r\n <span class=\"text-sm font-medium tabular-nums\" aria-live=\"polite\">\r\n @if (count() > 1) {\r\n {{ current() + 1 }} / {{ count() }}\r\n }\r\n </span>\r\n <div\r\n class=\"ms-auto flex items-center gap-1 rounded-full bg-neutral-primary p-1 text-heading shadow-lg\"\r\n >\r\n <button\r\n type=\"button\"\r\n aria-label=\"Thu nhỏ\"\r\n class=\"inline-flex size-8 items-center justify-center rounded-full transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle disabled:cursor-not-allowed disabled:opacity-40\"\r\n [disabled]=\"!canZoomOut()\"\r\n (click)=\"zoomOut()\"\r\n >\r\n <ol-icon name=\"lucide:zoom_out\" size=\"sm\" />\r\n </button>\r\n <button\r\n type=\"button\"\r\n aria-label=\"Về cỡ gốc\"\r\n class=\"min-w-12 rounded-full px-1 text-center text-xs font-semibold tabular-nums transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n (click)=\"resetZoom()\"\r\n >\r\n {{ zoomPercent() }}%\r\n </button>\r\n <button\r\n type=\"button\"\r\n aria-label=\"Phóng to\"\r\n class=\"inline-flex size-8 items-center justify-center rounded-full transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle disabled:cursor-not-allowed disabled:opacity-40\"\r\n [disabled]=\"!canZoomIn()\"\r\n (click)=\"zoomIn()\"\r\n >\r\n <ol-icon name=\"lucide:zoom_in\" size=\"sm\" />\r\n </button>\r\n </div>\r\n <button\r\n type=\"button\"\r\n data-ol-dismiss\r\n aria-label=\"Đóng xem ảnh\"\r\n class=\"inline-flex size-10 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-lg transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n (click)=\"close()\"\r\n >\r\n <ol-icon name=\"close\" size=\"base\" />\r\n </button>\r\n </div>\r\n\r\n <!--\r\n Ảnh: vừa khung, không cắt (object-contain). Bấm nền tối quanh ảnh để đóng.\r\n `overflow-hidden`: ảnh phóng to không được tràn đè lên thanh trên và dải\r\n ảnh nhỏ — đo 2026-09-28, ảnh 364% che nút \"Phóng to\", không bấm được.\r\n `tabindex=\"-1\"` ở khung ngoài: bấm vào ảnh (không focus được) thì tiêu\r\n điểm ở lại trình xem, phím + − 0 ← → vẫn chạy.\r\n -->\r\n <div\r\n class=\"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden px-4 pb-4\"\r\n (click)=\"onBackdropClick($event)\"\r\n >\r\n <img\r\n #mainImage\r\n [src]=\"img.src\"\r\n [alt]=\"img.alt || ''\"\r\n [class]=\"imageClasses()\"\r\n [style]=\"imageStyle()\"\r\n draggable=\"false\"\r\n tabindex=\"0\"\r\n (click)=\"onImageClick($event)\"\r\n (keydown.enter)=\"toggleZoom(); $event.preventDefault()\"\r\n (keydown.space)=\"toggleZoom(); $event.preventDefault()\"\r\n (wheel)=\"onWheel($event)\"\r\n (pointerdown)=\"onPointerDown($event)\"\r\n (pointermove)=\"onPointerMove($event)\"\r\n (pointerup)=\"onPointerUp($event)\"\r\n (pointercancel)=\"onPointerUp($event)\"\r\n />\r\n\r\n @if (count() > 1) {\r\n <button\r\n type=\"button\"\r\n aria-label=\"Ảnh trước\"\r\n class=\"absolute start-3 top-1/2 inline-flex size-11 -translate-y-1/2 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-lg transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n (click)=\"prev()\"\r\n >\r\n <ol-icon name=\"chevron_left\" size=\"base\" />\r\n </button>\r\n <button\r\n type=\"button\"\r\n aria-label=\"Ảnh sau\"\r\n class=\"absolute end-3 top-1/2 inline-flex size-11 -translate-y-1/2 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-lg transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n (click)=\"next()\"\r\n >\r\n <ol-icon name=\"chevron_right\" size=\"base\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n @if (img.alt) {\r\n <p class=\"truncate px-4 pb-3 text-center text-sm text-fg-white\">{{ img.alt }}</p>\r\n }\r\n\r\n <!--\r\n Dải ảnh nhỏ: thấy các ảnh còn lại, bấm để nhảy tới; ảnh đang xem có viền\r\n và tự cuộn vào tầm nhìn. Cuộn ngang khi dài (điện thoại), căn giữa khi\r\n ngắn — bằng ms-auto/me-auto ở ô đầu/cuối, KHÔNG justify-center: nó cắt\r\n mất các ô đầu khi dải tràn và không cuộn tới được. Lề dưới max() với\r\n safe-area: chừa vạch Home của iPhone.\r\n -->\r\n @if (count() > 1) {\r\n <div\r\n #thumbStrip\r\n class=\"flex shrink-0 gap-2 overflow-x-auto px-4 pt-1 pb-[max(1rem,env(safe-area-inset-bottom))] [scrollbar-width:none] [&>:first-child]:ms-auto [&>:last-child]:me-auto\"\r\n role=\"group\"\r\n aria-label=\"Danh sách ảnh\"\r\n >\r\n @for (thumb of images(); track $index; let i = $index) {\r\n <button\r\n type=\"button\"\r\n [attr.aria-label]=\"'Xem ảnh ' + (i + 1) + (thumb.alt ? ': ' + thumb.alt : '')\"\r\n [attr.aria-current]=\"i === current() ? 'true' : null\"\r\n [class]=\"thumbClasses(i)\"\r\n (click)=\"index.set(i)\"\r\n >\r\n <img [src]=\"thumb.src\" alt=\"\" class=\"size-full object-cover\" loading=\"lazy\" />\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n}\r\n"
|
|
19136
19325
|
}]
|
|
19137
19326
|
}],
|
|
19138
19327
|
ctorParameters: () => [],
|
|
@@ -19420,6 +19609,11 @@ var OlFileUpload = class OlFileUpload {
|
|
|
19420
19609
|
listeners: { "paste": "onPaste($event)" },
|
|
19421
19610
|
classAttribute: "block w-full"
|
|
19422
19611
|
},
|
|
19612
|
+
providers: [provideOlIcons({
|
|
19613
|
+
"solar:upload-minimalistic-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 13V3m0 0L7 8m5-5l5 5M3 13v5a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5\"/></svg>",
|
|
19614
|
+
"solar:gallery-add-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><rect fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"8\" cy=\"8\" r=\"1\" fill=\"currentColor\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M21 15l-5.197-5.197a2 2 0 0 0-2.828 0l-5.422 5.422a2 2 0 0 1-2.828 0L3 12\"/></svg>",
|
|
19615
|
+
"lucide:zoom-in": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"2\" d=\"m21 21l-4.35-4.35M11 8v6m3 0h-6\"/></svg>"
|
|
19616
|
+
})],
|
|
19423
19617
|
viewQueries: [{
|
|
19424
19618
|
propertyName: "fileInput",
|
|
19425
19619
|
first: true,
|
|
@@ -19428,7 +19622,7 @@ var OlFileUpload = class OlFileUpload {
|
|
|
19428
19622
|
isSignal: true
|
|
19429
19623
|
}],
|
|
19430
19624
|
ngImport: i0,
|
|
19431
|
-
template: "<div\r\n class=\"overflow-hidden rounded-md border bg-neutral-primary\"\r\n [class]=\"errors().length ? 'border-danger' : 'border-default'\"\r\n [attr.aria-disabled]=\"disabled() || null\"\r\n>\r\n <!-- Đầu khung: tiêu đề + nút (Figma \"Button header\"). Slot [actions] thay nút. -->\r\n <div class=\"flex items-center justify-between gap-3 border-b border-default px-3 py-2\">\r\n <span class=\"min-w-0 truncate text-sm font-medium text-fg-brand-strong\" [id]=\"uid + '-title'\">\r\n {{ title() }}\r\n @if (required()) {\r\n <span class=\"text-fg-danger\" aria-hidden=\"true\">*</span>\r\n }\r\n </span>\r\n <div class=\"flex shrink-0 items-center gap-2\">\r\n <ng-content select=\"[actions]\" />\r\n <ol-button\r\n size=\"sm\"\r\n [pill]=\"true\"\r\n [disabled]=\"disabled() || limitReached()\"\r\n [ariaLabel]=\"resolvedButtonLabel() + (title() ? ' — ' + title() : '')\"\r\n (btnClick)=\"openPicker()\"\r\n >\r\n <ol-icon name=\"solar:
|
|
19625
|
+
template: "<div\r\n class=\"overflow-hidden rounded-md border bg-neutral-primary\"\r\n [class]=\"errors().length ? 'border-danger' : 'border-default'\"\r\n [attr.aria-disabled]=\"disabled() || null\"\r\n>\r\n <!-- Đầu khung: tiêu đề + nút (Figma \"Button header\"). Slot [actions] thay nút. -->\r\n <div class=\"flex items-center justify-between gap-3 border-b border-default px-3 py-2\">\r\n <span class=\"min-w-0 truncate text-sm font-medium text-fg-brand-strong\" [id]=\"uid + '-title'\">\r\n {{ title() }}\r\n @if (required()) {\r\n <span class=\"text-fg-danger\" aria-hidden=\"true\">*</span>\r\n }\r\n </span>\r\n <div class=\"flex shrink-0 items-center gap-2\">\r\n <ng-content select=\"[actions]\" />\r\n <ol-button\r\n size=\"sm\"\r\n [pill]=\"true\"\r\n [disabled]=\"disabled() || limitReached()\"\r\n [ariaLabel]=\"resolvedButtonLabel() + (title() ? ' — ' + title() : '')\"\r\n (btnClick)=\"openPicker()\"\r\n >\r\n <ol-icon name=\"solar:upload_minimalistic_linear\" [size]=\"16\" />\r\n {{ resolvedButtonLabel() }}\r\n </ol-button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"bg-neutral-secondary p-3\">\r\n @if (!hasFiles()) {\r\n <ol-file-dropzone\r\n [compact]=\"true\"\r\n [hint]=\"hintText()\"\r\n [icon]=\"isImageMode() ? 'solar:gallery-add-linear' : 'solar:upload-minimalistic-linear'\"\r\n [title]=\"isImageMode() ? 'Nhấn để chọn ảnh hoặc kéo thả' : 'Nhấn để tải lên hoặc kéo thả'\"\r\n [disabled]=\"disabled()\"\r\n [state]=\"errors().length ? 'error' : 'default'\"\r\n [describedBy]=\"describedBy()\"\r\n (pick)=\"openPicker()\"\r\n (filesDropped)=\"addFiles($event)\"\r\n />\r\n } @else {\r\n @switch (mode()) {\r\n @case ('image') {\r\n <div class=\"relative overflow-hidden rounded-md bg-neutral-tertiary\">\r\n @if (imageUrls()[files()[0].id]; as src) {\r\n <!-- Cả ảnh là nút: bấm để xem phóng to. -->\r\n <button\r\n type=\"button\"\r\n class=\"group block w-full cursor-zoom-in focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle focus-visible:ring-inset\"\r\n [attr.aria-label]=\"'Xem ảnh ' + files()[0].name\"\r\n (click)=\"openPreview(files()[0])\"\r\n >\r\n <img [src]=\"src\" [alt]=\"files()[0].name\" class=\"block h-44 w-full object-cover\" />\r\n <span\r\n class=\"absolute start-2 bottom-2 inline-flex items-center gap-1 rounded-full bg-neutral-primary px-2.5 py-1 text-xs font-medium text-heading shadow-sm\"\r\n aria-hidden=\"true\"\r\n >\r\n <ol-icon name=\"lucide:zoom_in\" size=\"xs\" />\r\n Xem ảnh\r\n </span>\r\n </button>\r\n } @else {\r\n <div class=\"flex h-44 items-center justify-center text-sm text-body-subtle\">\r\n {{ files()[0].name }}\r\n </div>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"absolute end-2 top-2 inline-flex size-8 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-sm hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xoá ảnh ' + files()[0].name\"\r\n (click)=\"removeFile(files()[0])\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n </div>\r\n }\r\n @case ('images') {\r\n <!--\r\n Lưới ô vuông. Bấm ô để xem, trong trình xem lướt ‹ › qua mọi ảnh.\r\n Ô cuối \"+\" thêm ảnh (ẩn khi đủ maxFiles). Thả tệp vào lưới vẫn nhận.\r\n -->\r\n <ul\r\n class=\"grid grid-cols-3 gap-2 sm:grid-cols-4\"\r\n [attr.aria-labelledby]=\"uid + '-title'\"\r\n (dragover)=\"$event.preventDefault()\"\r\n (drop)=\"onListDrop($event)\"\r\n >\r\n @for (f of files(); track f.id) {\r\n <li class=\"relative aspect-square overflow-hidden rounded-md bg-neutral-tertiary\">\r\n @if (imageUrls()[f.id]; as src) {\r\n <button\r\n type=\"button\"\r\n class=\"block size-full cursor-zoom-in focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle focus-visible:ring-inset\"\r\n [attr.aria-label]=\"'Xem ảnh ' + f.name\"\r\n (click)=\"openPreview(f)\"\r\n >\r\n <img [src]=\"src\" [alt]=\"f.name\" class=\"size-full object-cover\" />\r\n </button>\r\n } @else {\r\n <span\r\n class=\"flex size-full items-center justify-center p-2 text-center text-xs break-all text-body-subtle\"\r\n >\r\n {{ f.name }}\r\n </span>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"absolute end-1 top-1 inline-flex size-7 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-sm hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xoá ảnh ' + f.name\"\r\n (click)=\"removeFile(f)\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n </li>\r\n }\r\n @if (!limitReached()) {\r\n <li class=\"aspect-square\">\r\n <button\r\n type=\"button\"\r\n class=\"flex size-full flex-col items-center justify-center gap-1 rounded-md border border-dashed border-default text-xs font-medium text-body-subtle transition-colors hover:border-brand hover:text-fg-brand focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle disabled:cursor-not-allowed disabled:opacity-50\"\r\n [disabled]=\"disabled()\"\r\n (click)=\"openPicker()\"\r\n >\r\n <ol-icon name=\"solar:gallery_add_linear\" size=\"base\" />\r\n Thêm ảnh\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n }\r\n @case ('single') {\r\n <ol-file-item\r\n variant=\"card\"\r\n [file]=\"files()[0]\"\r\n [disabled]=\"disabled()\"\r\n (remove)=\"removeFile($event)\"\r\n />\r\n }\r\n @case ('multiple') {\r\n <!--\r\n Thả thêm tệp vào vùng chip vẫn nhận — kéo thả không bắt buộc phải\r\n nhắm đúng ô trống. max-h + cuộn dọc: danh sách dài không đẩy form.\r\n -->\r\n <ul\r\n class=\"flex max-h-36 flex-wrap gap-1.5 overflow-y-auto\"\r\n [attr.aria-labelledby]=\"uid + '-title'\"\r\n (dragover)=\"$event.preventDefault()\"\r\n (drop)=\"onListDrop($event)\"\r\n >\r\n @for (f of files(); track f.id) {\r\n <li class=\"min-w-0 max-w-full\">\r\n <ol-file-item\r\n variant=\"chip\"\r\n [file]=\"f\"\r\n [previewable]=\"previewable()\"\r\n [disabled]=\"disabled()\"\r\n (remove)=\"removeFile($event)\"\r\n (preview)=\"filePreview.emit($event)\"\r\n />\r\n </li>\r\n }\r\n </ul>\r\n }\r\n @default {\r\n <ul class=\"flex flex-col gap-2\" [attr.aria-labelledby]=\"uid + '-title'\">\r\n @for (f of files(); track f.id) {\r\n <li>\r\n <ol-file-item\r\n [file]=\"f\"\r\n [previewable]=\"previewable()\"\r\n [disabled]=\"disabled()\"\r\n (remove)=\"removeFile($event)\"\r\n (preview)=\"filePreview.emit($event)\"\r\n (retry)=\"fileRetry.emit($event)\"\r\n />\r\n </li>\r\n }\r\n </ul>\r\n }\r\n }\r\n }\r\n </div>\r\n</div>\r\n\r\n<!--\r\n `sr-only` chứ KHÔNG `hidden`: `display: none` loại ô khỏi cây trợ năng. Mở\r\n bằng nút đầu khung hoặc vùng kéo thả; tabindex -1 để Tab không dừng hai lần.\r\n-->\r\n<input\r\n #fileInput\r\n type=\"file\"\r\n class=\"sr-only\"\r\n tabindex=\"-1\"\r\n aria-hidden=\"true\"\r\n [accept]=\"accept()\"\r\n [multiple]=\"isMultiple()\"\r\n [disabled]=\"disabled()\"\r\n (change)=\"onInputChange($event)\"\r\n/>\r\n\r\n@if (errors().length) {\r\n <ul [id]=\"uid + '-err'\" class=\"mt-1.5 space-y-0.5 text-xs text-fg-danger\" role=\"alert\">\r\n @for (msg of errors(); track msg) {\r\n <li>{{ msg }}</li>\r\n }\r\n </ul>\r\n}\r\n<ol-image-preview [images]=\"previewImages()\" [(open)]=\"previewOpen\" [(index)]=\"previewIndex\" />\r\n\r\n@if (helperText()) {\r\n <p [id]=\"uid + '-help'\" class=\"mt-1.5 text-xs text-body-subtle\">{{ helperText() }}</p>\r\n}\r\n",
|
|
19432
19626
|
dependencies: [
|
|
19433
19627
|
{
|
|
19434
19628
|
kind: "component",
|
|
@@ -19537,7 +19731,12 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
19537
19731
|
"(paste)": "onPaste($event)"
|
|
19538
19732
|
},
|
|
19539
19733
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19540
|
-
|
|
19734
|
+
providers: [provideOlIcons({
|
|
19735
|
+
"solar:upload-minimalistic-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 13V3m0 0L7 8m5-5l5 5M3 13v5a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-5\"/></svg>",
|
|
19736
|
+
"solar:gallery-add-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><rect fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><circle cx=\"8\" cy=\"8\" r=\"1\" fill=\"currentColor\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M21 15l-5.197-5.197a2 2 0 0 0-2.828 0l-5.422 5.422a2 2 0 0 1-2.828 0L3 12\"/></svg>",
|
|
19737
|
+
"lucide:zoom-in": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"2\" d=\"m21 21l-4.35-4.35M11 8v6m3 0h-6\"/></svg>"
|
|
19738
|
+
})],
|
|
19739
|
+
template: "<div\r\n class=\"overflow-hidden rounded-md border bg-neutral-primary\"\r\n [class]=\"errors().length ? 'border-danger' : 'border-default'\"\r\n [attr.aria-disabled]=\"disabled() || null\"\r\n>\r\n <!-- Đầu khung: tiêu đề + nút (Figma \"Button header\"). Slot [actions] thay nút. -->\r\n <div class=\"flex items-center justify-between gap-3 border-b border-default px-3 py-2\">\r\n <span class=\"min-w-0 truncate text-sm font-medium text-fg-brand-strong\" [id]=\"uid + '-title'\">\r\n {{ title() }}\r\n @if (required()) {\r\n <span class=\"text-fg-danger\" aria-hidden=\"true\">*</span>\r\n }\r\n </span>\r\n <div class=\"flex shrink-0 items-center gap-2\">\r\n <ng-content select=\"[actions]\" />\r\n <ol-button\r\n size=\"sm\"\r\n [pill]=\"true\"\r\n [disabled]=\"disabled() || limitReached()\"\r\n [ariaLabel]=\"resolvedButtonLabel() + (title() ? ' — ' + title() : '')\"\r\n (btnClick)=\"openPicker()\"\r\n >\r\n <ol-icon name=\"solar:upload_minimalistic_linear\" [size]=\"16\" />\r\n {{ resolvedButtonLabel() }}\r\n </ol-button>\r\n </div>\r\n </div>\r\n\r\n <div class=\"bg-neutral-secondary p-3\">\r\n @if (!hasFiles()) {\r\n <ol-file-dropzone\r\n [compact]=\"true\"\r\n [hint]=\"hintText()\"\r\n [icon]=\"isImageMode() ? 'solar:gallery-add-linear' : 'solar:upload-minimalistic-linear'\"\r\n [title]=\"isImageMode() ? 'Nhấn để chọn ảnh hoặc kéo thả' : 'Nhấn để tải lên hoặc kéo thả'\"\r\n [disabled]=\"disabled()\"\r\n [state]=\"errors().length ? 'error' : 'default'\"\r\n [describedBy]=\"describedBy()\"\r\n (pick)=\"openPicker()\"\r\n (filesDropped)=\"addFiles($event)\"\r\n />\r\n } @else {\r\n @switch (mode()) {\r\n @case ('image') {\r\n <div class=\"relative overflow-hidden rounded-md bg-neutral-tertiary\">\r\n @if (imageUrls()[files()[0].id]; as src) {\r\n <!-- Cả ảnh là nút: bấm để xem phóng to. -->\r\n <button\r\n type=\"button\"\r\n class=\"group block w-full cursor-zoom-in focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle focus-visible:ring-inset\"\r\n [attr.aria-label]=\"'Xem ảnh ' + files()[0].name\"\r\n (click)=\"openPreview(files()[0])\"\r\n >\r\n <img [src]=\"src\" [alt]=\"files()[0].name\" class=\"block h-44 w-full object-cover\" />\r\n <span\r\n class=\"absolute start-2 bottom-2 inline-flex items-center gap-1 rounded-full bg-neutral-primary px-2.5 py-1 text-xs font-medium text-heading shadow-sm\"\r\n aria-hidden=\"true\"\r\n >\r\n <ol-icon name=\"lucide:zoom_in\" size=\"xs\" />\r\n Xem ảnh\r\n </span>\r\n </button>\r\n } @else {\r\n <div class=\"flex h-44 items-center justify-center text-sm text-body-subtle\">\r\n {{ files()[0].name }}\r\n </div>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"absolute end-2 top-2 inline-flex size-8 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-sm hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xoá ảnh ' + files()[0].name\"\r\n (click)=\"removeFile(files()[0])\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n </div>\r\n }\r\n @case ('images') {\r\n <!--\r\n Lưới ô vuông. Bấm ô để xem, trong trình xem lướt ‹ › qua mọi ảnh.\r\n Ô cuối \"+\" thêm ảnh (ẩn khi đủ maxFiles). Thả tệp vào lưới vẫn nhận.\r\n -->\r\n <ul\r\n class=\"grid grid-cols-3 gap-2 sm:grid-cols-4\"\r\n [attr.aria-labelledby]=\"uid + '-title'\"\r\n (dragover)=\"$event.preventDefault()\"\r\n (drop)=\"onListDrop($event)\"\r\n >\r\n @for (f of files(); track f.id) {\r\n <li class=\"relative aspect-square overflow-hidden rounded-md bg-neutral-tertiary\">\r\n @if (imageUrls()[f.id]; as src) {\r\n <button\r\n type=\"button\"\r\n class=\"block size-full cursor-zoom-in focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle focus-visible:ring-inset\"\r\n [attr.aria-label]=\"'Xem ảnh ' + f.name\"\r\n (click)=\"openPreview(f)\"\r\n >\r\n <img [src]=\"src\" [alt]=\"f.name\" class=\"size-full object-cover\" />\r\n </button>\r\n } @else {\r\n <span\r\n class=\"flex size-full items-center justify-center p-2 text-center text-xs break-all text-body-subtle\"\r\n >\r\n {{ f.name }}\r\n </span>\r\n }\r\n <button\r\n type=\"button\"\r\n class=\"absolute end-1 top-1 inline-flex size-7 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-sm hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\r\n [disabled]=\"disabled()\"\r\n [attr.aria-label]=\"'Xoá ảnh ' + f.name\"\r\n (click)=\"removeFile(f)\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n </li>\r\n }\r\n @if (!limitReached()) {\r\n <li class=\"aspect-square\">\r\n <button\r\n type=\"button\"\r\n class=\"flex size-full flex-col items-center justify-center gap-1 rounded-md border border-dashed border-default text-xs font-medium text-body-subtle transition-colors hover:border-brand hover:text-fg-brand focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle disabled:cursor-not-allowed disabled:opacity-50\"\r\n [disabled]=\"disabled()\"\r\n (click)=\"openPicker()\"\r\n >\r\n <ol-icon name=\"solar:gallery_add_linear\" size=\"base\" />\r\n Thêm ảnh\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n }\r\n @case ('single') {\r\n <ol-file-item\r\n variant=\"card\"\r\n [file]=\"files()[0]\"\r\n [disabled]=\"disabled()\"\r\n (remove)=\"removeFile($event)\"\r\n />\r\n }\r\n @case ('multiple') {\r\n <!--\r\n Thả thêm tệp vào vùng chip vẫn nhận — kéo thả không bắt buộc phải\r\n nhắm đúng ô trống. max-h + cuộn dọc: danh sách dài không đẩy form.\r\n -->\r\n <ul\r\n class=\"flex max-h-36 flex-wrap gap-1.5 overflow-y-auto\"\r\n [attr.aria-labelledby]=\"uid + '-title'\"\r\n (dragover)=\"$event.preventDefault()\"\r\n (drop)=\"onListDrop($event)\"\r\n >\r\n @for (f of files(); track f.id) {\r\n <li class=\"min-w-0 max-w-full\">\r\n <ol-file-item\r\n variant=\"chip\"\r\n [file]=\"f\"\r\n [previewable]=\"previewable()\"\r\n [disabled]=\"disabled()\"\r\n (remove)=\"removeFile($event)\"\r\n (preview)=\"filePreview.emit($event)\"\r\n />\r\n </li>\r\n }\r\n </ul>\r\n }\r\n @default {\r\n <ul class=\"flex flex-col gap-2\" [attr.aria-labelledby]=\"uid + '-title'\">\r\n @for (f of files(); track f.id) {\r\n <li>\r\n <ol-file-item\r\n [file]=\"f\"\r\n [previewable]=\"previewable()\"\r\n [disabled]=\"disabled()\"\r\n (remove)=\"removeFile($event)\"\r\n (preview)=\"filePreview.emit($event)\"\r\n (retry)=\"fileRetry.emit($event)\"\r\n />\r\n </li>\r\n }\r\n </ul>\r\n }\r\n }\r\n }\r\n </div>\r\n</div>\r\n\r\n<!--\r\n `sr-only` chứ KHÔNG `hidden`: `display: none` loại ô khỏi cây trợ năng. Mở\r\n bằng nút đầu khung hoặc vùng kéo thả; tabindex -1 để Tab không dừng hai lần.\r\n-->\r\n<input\r\n #fileInput\r\n type=\"file\"\r\n class=\"sr-only\"\r\n tabindex=\"-1\"\r\n aria-hidden=\"true\"\r\n [accept]=\"accept()\"\r\n [multiple]=\"isMultiple()\"\r\n [disabled]=\"disabled()\"\r\n (change)=\"onInputChange($event)\"\r\n/>\r\n\r\n@if (errors().length) {\r\n <ul [id]=\"uid + '-err'\" class=\"mt-1.5 space-y-0.5 text-xs text-fg-danger\" role=\"alert\">\r\n @for (msg of errors(); track msg) {\r\n <li>{{ msg }}</li>\r\n }\r\n </ul>\r\n}\r\n<ol-image-preview [images]=\"previewImages()\" [(open)]=\"previewOpen\" [(index)]=\"previewIndex\" />\r\n\r\n@if (helperText()) {\r\n <p [id]=\"uid + '-help'\" class=\"mt-1.5 text-xs text-body-subtle\">{{ helperText() }}</p>\r\n}\r\n"
|
|
19541
19740
|
}]
|
|
19542
19741
|
}],
|
|
19543
19742
|
ctorParameters: () => [],
|
|
@@ -20270,7 +20469,10 @@ var OlInputCounter = class OlInputCounter {
|
|
|
20270
20469
|
provide: NG_VALUE_ACCESSOR,
|
|
20271
20470
|
useExisting: forwardRef(() => OlInputCounter),
|
|
20272
20471
|
multi: true
|
|
20273
|
-
}
|
|
20472
|
+
}, provideOlIcons({
|
|
20473
|
+
"minus": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M6 12h12\"/></svg>",
|
|
20474
|
+
"plus": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 5v14m-7-7h14\"/></svg>"
|
|
20475
|
+
})],
|
|
20274
20476
|
ngImport: i0,
|
|
20275
20477
|
template: "<div [class]=\"wrapperClasses()\">\r\n @if (label()) {\r\n <label [for]=\"inputId()\" [class]=\"labelClasses()\">\r\n {{ label() }}\r\n </label>\r\n }\r\n\r\n <div [class]=\"groupClasses()\">\r\n @if (!oneButton()) {\r\n <button\r\n type=\"button\"\r\n [disabled]=\"disabled() || (value() ?? 0) <= min()\"\r\n (click)=\"decrement()\"\r\n aria-label=\"Giảm giá trị\"\r\n class=\"inline-flex items-center justify-center text-heading bg-neutral-tertiary hover:bg-neutral-tertiary-medium disabled:opacity-40 disabled:cursor-not-allowed border-e border-default\"\r\n [class]=\"ghostNut()\"\r\n [class.w-9]=\"resolvedSize() === 'sm'\"\r\n [class.h-9]=\"resolvedSize() === 'sm'\"\r\n [class.w-10]=\"resolvedSize() === 'base'\"\r\n [class.h-10]=\"resolvedSize() === 'base'\"\r\n [class.w-12]=\"resolvedSize() === 'lg'\"\r\n [class.w-13]=\"resolvedSize() === 'xl'\"\r\n [class.h-12]=\"resolvedSize() === 'lg'\"\r\n [class.h-13]=\"resolvedSize() === 'xl'\"\r\n >\r\n <ol-icon name=\"minus\" size=\"xs\" />\r\n </button>\r\n }\r\n\r\n <input\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n type=\"text\"\r\n [value]=\"value()\"\r\n [disabled]=\"disabled()\"\r\n class=\"bg-transparent border-0 text-center text-heading font-medium outline-none\"\r\n [class.w-14]=\"resolvedSize() === 'sm'\"\r\n [class.h-9]=\"resolvedSize() === 'sm'\"\r\n [class.w-16]=\"resolvedSize() === 'base'\"\r\n [class.h-10]=\"resolvedSize() === 'base'\"\r\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.w-20]=\"resolvedSize() === 'lg'\"\r\n [class.w-24]=\"resolvedSize() === 'xl'\"\r\n [class.h-12]=\"resolvedSize() === 'lg'\"\r\n [class.h-13]=\"resolvedSize() === 'xl'\"\r\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\r\n readonly\r\n />\r\n\r\n <button\r\n type=\"button\"\r\n [disabled]=\"disabled() || (value() ?? 0) >= max()\"\r\n (click)=\"increment()\"\r\n aria-label=\"Tăng giá trị\"\r\n class=\"inline-flex items-center justify-center text-heading bg-neutral-tertiary hover:bg-neutral-tertiary-medium disabled:opacity-40 disabled:cursor-not-allowed border-s border-default\"\r\n [class]=\"ghostNut()\"\r\n [class.w-9]=\"resolvedSize() === 'sm'\"\r\n [class.h-9]=\"resolvedSize() === 'sm'\"\r\n [class.w-10]=\"resolvedSize() === 'base'\"\r\n [class.h-10]=\"resolvedSize() === 'base'\"\r\n [class.w-12]=\"resolvedSize() === 'lg'\"\r\n [class.w-13]=\"resolvedSize() === 'xl'\"\r\n [class.h-12]=\"resolvedSize() === 'lg'\"\r\n [class.h-13]=\"resolvedSize() === 'xl'\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" />\r\n </button>\r\n </div>\r\n\r\n @if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n",
|
|
20276
20478
|
dependencies: [{
|
|
@@ -20305,7 +20507,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
20305
20507
|
provide: NG_VALUE_ACCESSOR,
|
|
20306
20508
|
useExisting: forwardRef(() => OlInputCounter),
|
|
20307
20509
|
multi: true
|
|
20308
|
-
}
|
|
20510
|
+
}, provideOlIcons({
|
|
20511
|
+
"minus": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M6 12h12\"/></svg>",
|
|
20512
|
+
"plus": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 5v14m-7-7h14\"/></svg>"
|
|
20513
|
+
})],
|
|
20309
20514
|
template: "<div [class]=\"wrapperClasses()\">\r\n @if (label()) {\r\n <label [for]=\"inputId()\" [class]=\"labelClasses()\">\r\n {{ label() }}\r\n </label>\r\n }\r\n\r\n <div [class]=\"groupClasses()\">\r\n @if (!oneButton()) {\r\n <button\r\n type=\"button\"\r\n [disabled]=\"disabled() || (value() ?? 0) <= min()\"\r\n (click)=\"decrement()\"\r\n aria-label=\"Giảm giá trị\"\r\n class=\"inline-flex items-center justify-center text-heading bg-neutral-tertiary hover:bg-neutral-tertiary-medium disabled:opacity-40 disabled:cursor-not-allowed border-e border-default\"\r\n [class]=\"ghostNut()\"\r\n [class.w-9]=\"resolvedSize() === 'sm'\"\r\n [class.h-9]=\"resolvedSize() === 'sm'\"\r\n [class.w-10]=\"resolvedSize() === 'base'\"\r\n [class.h-10]=\"resolvedSize() === 'base'\"\r\n [class.w-12]=\"resolvedSize() === 'lg'\"\r\n [class.w-13]=\"resolvedSize() === 'xl'\"\r\n [class.h-12]=\"resolvedSize() === 'lg'\"\r\n [class.h-13]=\"resolvedSize() === 'xl'\"\r\n >\r\n <ol-icon name=\"minus\" size=\"xs\" />\r\n </button>\r\n }\r\n\r\n <input\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n type=\"text\"\r\n [value]=\"value()\"\r\n [disabled]=\"disabled()\"\r\n class=\"bg-transparent border-0 text-center text-heading font-medium outline-none\"\r\n [class.w-14]=\"resolvedSize() === 'sm'\"\r\n [class.h-9]=\"resolvedSize() === 'sm'\"\r\n [class.w-16]=\"resolvedSize() === 'base'\"\r\n [class.h-10]=\"resolvedSize() === 'base'\"\r\n [class.text-sm]=\"resolvedSize() === 'sm' || resolvedSize() === 'base'\"\r\n [class.w-20]=\"resolvedSize() === 'lg'\"\r\n [class.w-24]=\"resolvedSize() === 'xl'\"\r\n [class.h-12]=\"resolvedSize() === 'lg'\"\r\n [class.h-13]=\"resolvedSize() === 'xl'\"\r\n [class.text-base]=\"resolvedSize() === 'lg' || resolvedSize() === 'xl'\"\r\n readonly\r\n />\r\n\r\n <button\r\n type=\"button\"\r\n [disabled]=\"disabled() || (value() ?? 0) >= max()\"\r\n (click)=\"increment()\"\r\n aria-label=\"Tăng giá trị\"\r\n class=\"inline-flex items-center justify-center text-heading bg-neutral-tertiary hover:bg-neutral-tertiary-medium disabled:opacity-40 disabled:cursor-not-allowed border-s border-default\"\r\n [class]=\"ghostNut()\"\r\n [class.w-9]=\"resolvedSize() === 'sm'\"\r\n [class.h-9]=\"resolvedSize() === 'sm'\"\r\n [class.w-10]=\"resolvedSize() === 'base'\"\r\n [class.h-10]=\"resolvedSize() === 'base'\"\r\n [class.w-12]=\"resolvedSize() === 'lg'\"\r\n [class.w-13]=\"resolvedSize() === 'xl'\"\r\n [class.h-12]=\"resolvedSize() === 'lg'\"\r\n [class.h-13]=\"resolvedSize() === 'xl'\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" />\r\n </button>\r\n </div>\r\n\r\n @if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n"
|
|
20310
20515
|
}]
|
|
20311
20516
|
}],
|
|
@@ -20927,7 +21132,7 @@ var OlTagInput = class OlTagInput {
|
|
|
20927
21132
|
provide: NG_VALUE_ACCESSOR,
|
|
20928
21133
|
useExisting: forwardRef(() => OlTagInput),
|
|
20929
21134
|
multi: true
|
|
20930
|
-
}],
|
|
21135
|
+
}, provideOlIcons({ "close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M18 6L6 18M6 6l12 12\"/></svg>" })],
|
|
20931
21136
|
viewQueries: [{
|
|
20932
21137
|
propertyName: "tagInputEl",
|
|
20933
21138
|
first: true,
|
|
@@ -21042,7 +21247,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
21042
21247
|
provide: NG_VALUE_ACCESSOR,
|
|
21043
21248
|
useExisting: forwardRef(() => OlTagInput),
|
|
21044
21249
|
multi: true
|
|
21045
|
-
}],
|
|
21250
|
+
}, provideOlIcons({ "close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M18 6L6 18M6 6l12 12\"/></svg>" })],
|
|
21046
21251
|
template: "<!--\r\n Bậc số `brand-*` ở đây GIỮ có chủ đích — `brand` là bảng Figma CÓ:\r\n dark:focus-within:ring-brand-950 token ring-brand-softer không sinh CSS\r\n dark:bg-brand-950/50 token không mang được opacity /50\r\n dark:border-brand-800/60 cùng lý do\r\n-->\r\n<div class=\"w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label [beside]=\"labelBeside()\" [size]=\"size()\" [text]=\"label()\" [forId]=\"inputId()\" />\r\n }\r\n\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"size()\"\r\n [disabled]=\"disabled()\"\r\n [hasValue]=\"tags().length > 0 || inputValue() !== ''\"\r\n >\r\n <!-- Cùng nền với ol-input (#F9FAFB) — xem lý do ở ol-otp-input. -->\r\n <div\r\n (click)=\"focusInput()\"\r\n class=\"ol-field-box flex flex-wrap items-center gap-1.5 border border-default-medium rounded-md shadow-xs focus-within:ring-2 focus-within:ring-brand-subtle dark:focus-within:ring-brand-950 focus-within:border-brand cursor-text\"\r\n [class]=\"boxSizeClass()\"\r\n [class.bg-neutral-secondary-medium]=\"!disabled()\"\r\n [class.bg-neutral-tertiary]=\"disabled()\"\r\n [class.dark:bg-dark]=\"disabled()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n [class.opacity-60]=\"disabled()\"\r\n >\r\n @for (tag of tags(); track tag; let i = $index) {\r\n <span\r\n class=\"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium bg-brand-softer text-fg-brand-medium dark:bg-brand-950/50 dark:text-fg-brand border border-brand-subtle dark:border-brand-800/60\"\r\n >\r\n <span>{{ tag }}</span>\r\n @if (!disabled()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"removeTag(i); $event.stopPropagation()\"\r\n [attr.aria-label]=\"'Gỡ ' + tag\"\r\n class=\"text-fg-brand hover:text-fg-brand-medium dark:text-fg-brand dark:hover:text-fg-brand-subtle p-0.5 rounded-md hover:bg-brand-soft dark:hover:bg-brand-900/60\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n\r\n <input\r\n #tagInputField\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n type=\"text\"\r\n [disabled]=\"disabled()\"\r\n [placeholder]=\"tags().length === 0 ? placeholder() : ''\"\r\n [ngModel]=\"inputValue()\"\r\n (ngModelChange)=\"inputValue.set($event)\"\r\n (keydown)=\"onKeyDown($event)\"\r\n (blur)=\"markTouched()\"\r\n class=\"flex-1 min-w-30 bg-transparent border-0 text-heading outline-hidden py-1 px-1 placeholder:text-body-subtle\"\r\n [class]=\"fieldTextClass()\"\r\n />\r\n </div>\r\n </ol-field-frame>\r\n\r\n @if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n"
|
|
21047
21252
|
}]
|
|
21048
21253
|
}],
|
|
@@ -21311,7 +21516,15 @@ var OlRichTextEditor = class OlRichTextEditor {
|
|
|
21311
21516
|
provide: NG_VALUE_ACCESSOR,
|
|
21312
21517
|
useExisting: forwardRef(() => OlRichTextEditor),
|
|
21313
21518
|
multi: true
|
|
21314
|
-
}
|
|
21519
|
+
}, provideOlIcons({
|
|
21520
|
+
"minus": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M6 12h12\"/></svg>",
|
|
21521
|
+
"link": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>",
|
|
21522
|
+
"list": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M9 6h12M9 12h12M9 18h12M5 6v.01M5 12v.01M5 18v.01\"/></svg>",
|
|
21523
|
+
"menu-dots": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"5\" cy=\"12\" r=\"1\" fill=\"currentColor\"/><circle cx=\"12\" cy=\"12\" r=\"1\" fill=\"currentColor\"/><circle cx=\"19\" cy=\"12\" r=\"1\" fill=\"currentColor\"/></svg>",
|
|
21524
|
+
"lucide:bold": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6zM6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"/></svg>",
|
|
21525
|
+
"lucide:italic": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 4H5m14 0l-8 16M5 20l8-16\"/></svg>",
|
|
21526
|
+
"lucide:code": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M16 18l4-4-4-4M8 6l-4 4 4 4\"/></svg>"
|
|
21527
|
+
})],
|
|
21315
21528
|
viewQueries: [{
|
|
21316
21529
|
propertyName: "editorArea",
|
|
21317
21530
|
first: true,
|
|
@@ -21319,7 +21532,7 @@ var OlRichTextEditor = class OlRichTextEditor {
|
|
|
21319
21532
|
descendants: true
|
|
21320
21533
|
}],
|
|
21321
21534
|
ngImport: i0,
|
|
21322
|
-
template: "<div class=\"w-full\">\r\n @if (label()) {\r\n <label [for]=\"inputId()\" class=\"block mb-1.5 text-sm font-medium text-heading\">\r\n {{ label() }}\r\n </label>\r\n }\r\n\r\n <div\r\n class=\"border border-default 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\"\r\n >\r\n <!-- Formatting Toolbar (Figma #1149:49749) -->\r\n <div\r\n class=\"flex flex-wrap items-center gap-1 p-2 border-b border-default bg-neutral-secondary-medium/70\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"formatText('**', '**', 'chữ in đậm')\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"In đậm (Bold)\"\r\n >\r\n <ol-icon name=\"
|
|
21535
|
+
template: "<div class=\"w-full\">\r\n @if (label()) {\r\n <label [for]=\"inputId()\" class=\"block mb-1.5 text-sm font-medium text-heading\">\r\n {{ label() }}\r\n </label>\r\n }\r\n\r\n <div\r\n class=\"border border-default 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\"\r\n >\r\n <!-- Formatting Toolbar (Figma #1149:49749) -->\r\n <div\r\n class=\"flex flex-wrap items-center gap-1 p-2 border-b border-default bg-neutral-secondary-medium/70\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"formatText('**', '**', 'chữ in đậm')\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"In đậm (Bold)\"\r\n >\r\n <ol-icon name=\"lucide_bold\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"formatText('*', '*', 'chữ in nghiêng')\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"In nghiêng (Italic)\"\r\n >\r\n <ol-icon name=\"lucide_italic\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"formatText('~~', '~~', 'chữ gạch ngang')\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Gạch ngang (Strikethrough)\"\r\n >\r\n <ol-icon name=\"minus\" size=\"sm\" />\r\n </button>\r\n\r\n <div class=\"w-px h-5 bg-neutral-quaternary mx-1\"></div>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"insertLink()\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Chèn liên kết (Link)\"\r\n >\r\n <ol-icon name=\"link\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"insertCodeBlock()\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Khối mã (Code)\"\r\n >\r\n <ol-icon name=\"lucide_code\" size=\"sm\" />\r\n </button>\r\n\r\n <div class=\"w-px h-5 bg-neutral-quaternary mx-1\"></div>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"insertList(false)\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Danh sách dấu chấm (Bullet List)\"\r\n >\r\n <ol-icon name=\"list\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"insertList(true)\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Danh sách đánh số (Numbered List)\"\r\n >\r\n <ol-icon name=\"list\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"formatText('> ', '', 'Trích dẫn quan trọng...')\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Trích dẫn (Quote)\"\r\n >\r\n <ol-icon name=\"menu_dots\" size=\"sm\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Textarea Area -->\r\n <textarea\r\n #editorArea\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.disabled]=\"disabled() ? '' : null\"\r\n [placeholder]=\"placeholder()\"\r\n [ngModel]=\"currentValue\"\r\n (ngModelChange)=\"onModelChange($event)\"\r\n class=\"block w-full p-3.5 text-sm text-heading bg-transparent border-0 outline-none resize-y placeholder:text-body-subtle font-sans\"\r\n [style.min-height]=\"minHeight()\"\r\n ></textarea>\r\n </div>\r\n\r\n @if (helperText()) {\r\n <p [id]=\"helperId()\" class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n",
|
|
21323
21536
|
dependencies: [
|
|
21324
21537
|
{
|
|
21325
21538
|
kind: "ngmodule",
|
|
@@ -21388,8 +21601,16 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
21388
21601
|
provide: NG_VALUE_ACCESSOR,
|
|
21389
21602
|
useExisting: forwardRef(() => OlRichTextEditor),
|
|
21390
21603
|
multi: true
|
|
21391
|
-
}
|
|
21392
|
-
|
|
21604
|
+
}, provideOlIcons({
|
|
21605
|
+
"minus": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M6 12h12\"/></svg>",
|
|
21606
|
+
"link": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>",
|
|
21607
|
+
"list": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M9 6h12M9 12h12M9 18h12M5 6v.01M5 12v.01M5 18v.01\"/></svg>",
|
|
21608
|
+
"menu-dots": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"5\" cy=\"12\" r=\"1\" fill=\"currentColor\"/><circle cx=\"12\" cy=\"12\" r=\"1\" fill=\"currentColor\"/><circle cx=\"19\" cy=\"12\" r=\"1\" fill=\"currentColor\"/></svg>",
|
|
21609
|
+
"lucide:bold": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6zM6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"/></svg>",
|
|
21610
|
+
"lucide:italic": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 4H5m14 0l-8 16M5 20l8-16\"/></svg>",
|
|
21611
|
+
"lucide:code": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M16 18l4-4-4-4M8 6l-4 4 4 4\"/></svg>"
|
|
21612
|
+
})],
|
|
21613
|
+
template: "<div class=\"w-full\">\r\n @if (label()) {\r\n <label [for]=\"inputId()\" class=\"block mb-1.5 text-sm font-medium text-heading\">\r\n {{ label() }}\r\n </label>\r\n }\r\n\r\n <div\r\n class=\"border border-default 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\"\r\n >\r\n <!-- Formatting Toolbar (Figma #1149:49749) -->\r\n <div\r\n class=\"flex flex-wrap items-center gap-1 p-2 border-b border-default bg-neutral-secondary-medium/70\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"formatText('**', '**', 'chữ in đậm')\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"In đậm (Bold)\"\r\n >\r\n <ol-icon name=\"lucide_bold\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"formatText('*', '*', 'chữ in nghiêng')\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"In nghiêng (Italic)\"\r\n >\r\n <ol-icon name=\"lucide_italic\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"formatText('~~', '~~', 'chữ gạch ngang')\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Gạch ngang (Strikethrough)\"\r\n >\r\n <ol-icon name=\"minus\" size=\"sm\" />\r\n </button>\r\n\r\n <div class=\"w-px h-5 bg-neutral-quaternary mx-1\"></div>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"insertLink()\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Chèn liên kết (Link)\"\r\n >\r\n <ol-icon name=\"link\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"insertCodeBlock()\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Khối mã (Code)\"\r\n >\r\n <ol-icon name=\"lucide_code\" size=\"sm\" />\r\n </button>\r\n\r\n <div class=\"w-px h-5 bg-neutral-quaternary mx-1\"></div>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"insertList(false)\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Danh sách dấu chấm (Bullet List)\"\r\n >\r\n <ol-icon name=\"list\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"insertList(true)\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Danh sách đánh số (Numbered List)\"\r\n >\r\n <ol-icon name=\"list\" size=\"sm\" />\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"formatText('> ', '', 'Trích dẫn quan trọng...')\"\r\n [disabled]=\"disabled()\"\r\n class=\"p-1.5 rounded-lg text-body hover:bg-neutral-tertiary-medium disabled:cursor-not-allowed disabled:opacity-40\"\r\n title=\"Trích dẫn (Quote)\"\r\n >\r\n <ol-icon name=\"menu_dots\" size=\"sm\" />\r\n </button>\r\n </div>\r\n\r\n <!-- Textarea Area -->\r\n <textarea\r\n #editorArea\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.disabled]=\"disabled() ? '' : null\"\r\n [placeholder]=\"placeholder()\"\r\n [ngModel]=\"currentValue\"\r\n (ngModelChange)=\"onModelChange($event)\"\r\n class=\"block w-full p-3.5 text-sm text-heading bg-transparent border-0 outline-none resize-y placeholder:text-body-subtle font-sans\"\r\n [style.min-height]=\"minHeight()\"\r\n ></textarea>\r\n </div>\r\n\r\n @if (helperText()) {\r\n <p [id]=\"helperId()\" class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n"
|
|
21393
21614
|
}]
|
|
21394
21615
|
}],
|
|
21395
21616
|
ctorParameters: () => [],
|
|
@@ -21513,7 +21734,7 @@ var OlTimepicker = class OlTimepicker {
|
|
|
21513
21734
|
describedBy = computed(() => this.helperText() ? this.helperId() : null, ...ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []);
|
|
21514
21735
|
ariaInvalid = computed(() => this.state() === "error" ? "true" : null, ...ngDevMode ? [{ debugName: "ariaInvalid" }] : /* istanbul ignore next */ []);
|
|
21515
21736
|
panel = viewChild("panel", ...ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []);
|
|
21516
|
-
overlayContainer
|
|
21737
|
+
overlayContainer;
|
|
21517
21738
|
detachPanel = null;
|
|
21518
21739
|
stopTracking = null;
|
|
21519
21740
|
isOpen = signal(false, ...ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []);
|
|
@@ -21579,6 +21800,7 @@ var OlTimepicker = class OlTimepicker {
|
|
|
21579
21800
|
}
|
|
21580
21801
|
constructor(elementRef) {
|
|
21581
21802
|
this.elementRef = elementRef;
|
|
21803
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
21582
21804
|
effect(() => {
|
|
21583
21805
|
const open = this.isOpen();
|
|
21584
21806
|
const el = this.panel()?.nativeElement;
|
|
@@ -21914,7 +22136,7 @@ var OlTimepicker = class OlTimepicker {
|
|
|
21914
22136
|
provide: NG_VALUE_ACCESSOR,
|
|
21915
22137
|
useExisting: forwardRef(() => OlTimepicker),
|
|
21916
22138
|
multi: true
|
|
21917
|
-
}],
|
|
22139
|
+
}, provideOlIcons({ "clock-circle": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 7v5l4 2\"/></svg>" })],
|
|
21918
22140
|
viewQueries: [
|
|
21919
22141
|
{
|
|
21920
22142
|
propertyName: "panel",
|
|
@@ -21939,7 +22161,7 @@ var OlTimepicker = class OlTimepicker {
|
|
|
21939
22161
|
}
|
|
21940
22162
|
],
|
|
21941
22163
|
ngImport: i0,
|
|
21942
|
-
template: "<!-- Bo góc: Figma `ol-timepicker` (#1149:52243) dùng 12px cho CẢ BỐN size\r\n (rounded-md). Code trước đây dùng rounded-lg 16px cho sm/base và\r\n rounded-md 24px cho lg — cùng lỗi đã sửa ở `ol-input`. -->\r\n<div class=\"relative w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"size()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <!-- Trigger Box Container with Add-on Support -->\r\n <!-- Nhãn nổi kiểu filled/standard thêm hàng 20px phía trên khung: addon\r\n phải bám ĐÁY cho thẳng hàng với khung nhập, không bám hàng nhãn. -->\r\n <div\r\n class=\"flex w-full shadow-xs\"\r\n [class.items-stretch]=\"!floatingLabel()\"\r\n [class.items-end]=\"floatingLabel()\"\r\n >\r\n @if (addonType() === 'left') {\r\n <div\r\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\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n >\r\n @if (addonText()) {\r\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\r\n } @else {\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n\r\n <ol-field-frame\r\n class=\"min-w-0 flex-1\"\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"size()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"!!value()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <div\r\n [id]=\"inputId()\"\r\n role=\"combobox\"\r\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\r\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n aria-haspopup=\"listbox\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.tabindex]=\"disabled() ? -1 : 0\"\r\n (click)=\"toggleDropdown()\"\r\n (keydown.enter)=\"toggleDropdown()\"\r\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\r\n class=\"ol-field-box 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\"\r\n [ngClass]=\"fieldTextClasses()\"\r\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\r\n [class.rounded-md]=\"addonType() === 'none'\"\r\n [class.rounded-e-md]=\"addonType() === 'left'\"\r\n [class.rounded-s-md]=\"addonType() === 'right'\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.border-brand]=\"isOpen() && state() === 'default'\"\r\n [class.ring-2]=\"isOpen()\"\r\n [class.ring-brand-subtle]=\"isOpen()\"\r\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled() || readonly()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n [class.opacity-60]=\"disabled()\"\r\n >\r\n <div class=\"flex items-center gap-2 overflow-hidden min-w-0\">\r\n @if (addonType() === 'none') {\r\n <!-- Icon trong ô khi không có addon; `addonIcon` đổi được. -->\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n <span\r\n class=\"truncate font-normal\"\r\n data-ol-field-text\r\n [attr.data-ol-field-placeholder]=\"value() ? null : ''\"\r\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\r\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\r\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\r\n [class.text-body]=\"!value() && state() === 'error'\"\r\n [class.text-heading]=\"!!value()\"\r\n >\r\n {{ value() || placeholder() }}\r\n </span>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\r\n @if (value() && !disabled() && !readonly()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"clear($event)\"\r\n aria-label=\"Xoá giờ đã chọn\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n <ol-icon name=\"chevron-down\" size=\"sm\" />\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n @if (addonType() === 'right') {\r\n <div\r\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\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n >\r\n @if (addonText()) {\r\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\r\n } @else {\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Popover Panel -->\r\n @if (isOpen()) {\r\n <div\r\n #panel\r\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\"\r\n >\r\n @if (showPresets()) {\r\n <!-- Preset quick times -->\r\n <div class=\"mb-3 pb-2 border-b border-default\">\r\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">Mốc giờ nhanh</span>\r\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\r\n @for (preset of presetTimes; track preset) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPreset(preset)\"\r\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\r\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\r\n [class.bg-brand-softer]=\"value() === preset\"\r\n [class.text-fg-brand]=\"value() === preset\"\r\n [class.text-body]=\"value() !== preset\"\r\n >\r\n {{ preset }}\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Columns Time Picker -->\r\n <div class=\"flex items-center justify-center gap-2 py-2\">\r\n <!-- Hour Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Giờ</span>\r\n <div\r\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\r\n >\r\n <!-- Top Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n\r\n <ol-scroll-area\r\n #hourArea\r\n scrollbar=\"hidden\"\r\n [smooth]=\"true\"\r\n innerClass=\"p-1 py-4 space-y-1\"\r\n >\r\n @if (format() === '12h') {\r\n @for (h of hours12; track h) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectHour(h)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedHour() === h\"\r\n [class.text-white]=\"selectedHour() === h\"\r\n [class.font-semibold]=\"selectedHour() === h\"\r\n [class.scale-105]=\"selectedHour() === h\"\r\n [class.shadow-xs]=\"selectedHour() === h\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\r\n [class.text-heading]=\"selectedHour() !== h\"\r\n >\r\n {{ pad(h) }}\r\n </button>\r\n }\r\n } @else {\r\n @for (h of hours24; track h) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectHour(h)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedHour() === h\"\r\n [class.text-white]=\"selectedHour() === h\"\r\n [class.font-semibold]=\"selectedHour() === h\"\r\n [class.scale-105]=\"selectedHour() === h\"\r\n [class.shadow-xs]=\"selectedHour() === h\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\r\n [class.text-heading]=\"selectedHour() !== h\"\r\n >\r\n {{ pad(h) }}\r\n </button>\r\n }\r\n }\r\n </ol-scroll-area>\r\n\r\n <!-- Bottom Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n </div>\r\n </div>\r\n\r\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\r\n\r\n <!-- Minute Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Phút</span>\r\n <div\r\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\r\n >\r\n <!-- Top Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n\r\n <ol-scroll-area\r\n #minuteArea\r\n scrollbar=\"hidden\"\r\n [smooth]=\"true\"\r\n innerClass=\"p-1 py-4 space-y-1\"\r\n >\r\n @for (m of minutes; track m) {\r\n @if (m % minuteStep() === 0) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectMinute(m)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedMinute() === m\"\r\n [class.text-white]=\"selectedMinute() === m\"\r\n [class.font-semibold]=\"selectedMinute() === m\"\r\n [class.scale-105]=\"selectedMinute() === m\"\r\n [class.shadow-xs]=\"selectedMinute() === m\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\r\n [class.text-heading]=\"selectedMinute() !== m\"\r\n >\r\n {{ pad(m) }}\r\n </button>\r\n }\r\n }\r\n </ol-scroll-area>\r\n\r\n <!-- Bottom Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n </div>\r\n </div>\r\n\r\n @if (format() === '12h') {\r\n <!-- AM/PM Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Buổi</span>\r\n <div\r\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPeriod('AM')\"\r\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\r\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\r\n [class.text-white]=\"selectedPeriod() === 'AM'\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\r\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\r\n >\r\n AM\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPeriod('PM')\"\r\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\r\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\r\n [class.text-white]=\"selectedPeriod() === 'PM'\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\r\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\r\n >\r\n PM\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Action Footer -->\r\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\r\n <button\r\n type=\"button\"\r\n (click)=\"cancel()\"\r\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\r\n >\r\n Hủy\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"apply()\"\r\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\"\r\n >\r\n Áp dụng\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Helper Text & Error Text -->\r\n @if (state() === 'error' && errorText()) {\r\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\r\n } @else if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n",
|
|
22164
|
+
template: "<!-- Bo góc: Figma `ol-timepicker` (#1149:52243) dùng 12px cho CẢ BỐN size\r\n (rounded-md). Code trước đây dùng rounded-lg 16px cho sm/base và\r\n rounded-md 24px cho lg — cùng lỗi đã sửa ở `ol-input`. -->\r\n<div class=\"relative w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"size()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <!-- Trigger Box Container with Add-on Support -->\r\n <!-- Nhãn nổi kiểu filled/standard thêm hàng 20px phía trên khung: addon\r\n phải bám ĐÁY cho thẳng hàng với khung nhập, không bám hàng nhãn. -->\r\n <div\r\n class=\"flex w-full shadow-xs\"\r\n [class.items-stretch]=\"!floatingLabel()\"\r\n [class.items-end]=\"floatingLabel()\"\r\n >\r\n @if (addonType() === 'left') {\r\n <div\r\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\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n >\r\n @if (addonText()) {\r\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\r\n } @else {\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n\r\n <ol-field-frame\r\n class=\"min-w-0 flex-1\"\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"size()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"!!value()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <div\r\n [id]=\"inputId()\"\r\n role=\"combobox\"\r\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\r\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n aria-haspopup=\"listbox\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.tabindex]=\"disabled() ? -1 : 0\"\r\n (click)=\"toggleDropdown()\"\r\n (keydown.enter)=\"toggleDropdown()\"\r\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\r\n class=\"ol-field-box 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\"\r\n [ngClass]=\"fieldTextClasses()\"\r\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\r\n [class.rounded-md]=\"addonType() === 'none'\"\r\n [class.rounded-e-md]=\"addonType() === 'left'\"\r\n [class.rounded-s-md]=\"addonType() === 'right'\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.border-brand]=\"isOpen() && state() === 'default'\"\r\n [class.ring-2]=\"isOpen()\"\r\n [class.ring-brand-subtle]=\"isOpen()\"\r\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled() || readonly()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n [class.opacity-60]=\"disabled()\"\r\n >\r\n <div class=\"flex items-center gap-2 overflow-hidden min-w-0\">\r\n @if (addonType() === 'none') {\r\n <!-- Icon trong ô khi không có addon; `addonIcon` đổi được. -->\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n <span\r\n class=\"truncate font-normal\"\r\n data-ol-field-text\r\n [attr.data-ol-field-placeholder]=\"value() ? null : ''\"\r\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\r\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\r\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\r\n [class.text-body]=\"!value() && state() === 'error'\"\r\n [class.text-heading]=\"!!value()\"\r\n >\r\n {{ value() || placeholder() }}\r\n </span>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\r\n @if (value() && !disabled() && !readonly()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"clear($event)\"\r\n aria-label=\"Xoá giờ đã chọn\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n <ol-icon name=\"chevron_down\" size=\"sm\" />\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n @if (addonType() === 'right') {\r\n <div\r\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\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n >\r\n @if (addonText()) {\r\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\r\n } @else {\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Popover Panel -->\r\n @if (isOpen()) {\r\n <div\r\n #panel\r\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\"\r\n >\r\n @if (showPresets()) {\r\n <!-- Preset quick times -->\r\n <div class=\"mb-3 pb-2 border-b border-default\">\r\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">Mốc giờ nhanh</span>\r\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\r\n @for (preset of presetTimes; track preset) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPreset(preset)\"\r\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\r\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\r\n [class.bg-brand-softer]=\"value() === preset\"\r\n [class.text-fg-brand]=\"value() === preset\"\r\n [class.text-body]=\"value() !== preset\"\r\n >\r\n {{ preset }}\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Columns Time Picker -->\r\n <div class=\"flex items-center justify-center gap-2 py-2\">\r\n <!-- Hour Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Giờ</span>\r\n <div\r\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\r\n >\r\n <!-- Top Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n\r\n <ol-scroll-area\r\n #hourArea\r\n scrollbar=\"hidden\"\r\n [smooth]=\"true\"\r\n innerClass=\"p-1 py-4 space-y-1\"\r\n >\r\n @if (format() === '12h') {\r\n @for (h of hours12; track h) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectHour(h)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedHour() === h\"\r\n [class.text-white]=\"selectedHour() === h\"\r\n [class.font-semibold]=\"selectedHour() === h\"\r\n [class.scale-105]=\"selectedHour() === h\"\r\n [class.shadow-xs]=\"selectedHour() === h\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\r\n [class.text-heading]=\"selectedHour() !== h\"\r\n >\r\n {{ pad(h) }}\r\n </button>\r\n }\r\n } @else {\r\n @for (h of hours24; track h) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectHour(h)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedHour() === h\"\r\n [class.text-white]=\"selectedHour() === h\"\r\n [class.font-semibold]=\"selectedHour() === h\"\r\n [class.scale-105]=\"selectedHour() === h\"\r\n [class.shadow-xs]=\"selectedHour() === h\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\r\n [class.text-heading]=\"selectedHour() !== h\"\r\n >\r\n {{ pad(h) }}\r\n </button>\r\n }\r\n }\r\n </ol-scroll-area>\r\n\r\n <!-- Bottom Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n </div>\r\n </div>\r\n\r\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\r\n\r\n <!-- Minute Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Phút</span>\r\n <div\r\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\r\n >\r\n <!-- Top Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n\r\n <ol-scroll-area\r\n #minuteArea\r\n scrollbar=\"hidden\"\r\n [smooth]=\"true\"\r\n innerClass=\"p-1 py-4 space-y-1\"\r\n >\r\n @for (m of minutes; track m) {\r\n @if (m % minuteStep() === 0) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectMinute(m)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedMinute() === m\"\r\n [class.text-white]=\"selectedMinute() === m\"\r\n [class.font-semibold]=\"selectedMinute() === m\"\r\n [class.scale-105]=\"selectedMinute() === m\"\r\n [class.shadow-xs]=\"selectedMinute() === m\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\r\n [class.text-heading]=\"selectedMinute() !== m\"\r\n >\r\n {{ pad(m) }}\r\n </button>\r\n }\r\n }\r\n </ol-scroll-area>\r\n\r\n <!-- Bottom Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n </div>\r\n </div>\r\n\r\n @if (format() === '12h') {\r\n <!-- AM/PM Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Buổi</span>\r\n <div\r\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPeriod('AM')\"\r\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\r\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\r\n [class.text-white]=\"selectedPeriod() === 'AM'\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\r\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\r\n >\r\n AM\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPeriod('PM')\"\r\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\r\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\r\n [class.text-white]=\"selectedPeriod() === 'PM'\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\r\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\r\n >\r\n PM\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Action Footer -->\r\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\r\n <button\r\n type=\"button\"\r\n (click)=\"cancel()\"\r\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\r\n >\r\n Hủy\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"apply()\"\r\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\"\r\n >\r\n Áp dụng\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Helper Text & Error Text -->\r\n @if (state() === 'error' && errorText()) {\r\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\r\n } @else if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n",
|
|
21943
22165
|
dependencies: [
|
|
21944
22166
|
{
|
|
21945
22167
|
kind: "ngmodule",
|
|
@@ -22041,8 +22263,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
22041
22263
|
provide: NG_VALUE_ACCESSOR,
|
|
22042
22264
|
useExisting: forwardRef(() => OlTimepicker),
|
|
22043
22265
|
multi: true
|
|
22044
|
-
}],
|
|
22045
|
-
template: "<!-- Bo góc: Figma `ol-timepicker` (#1149:52243) dùng 12px cho CẢ BỐN size\r\n (rounded-md). Code trước đây dùng rounded-lg 16px cho sm/base và\r\n rounded-md 24px cho lg — cùng lỗi đã sửa ở `ol-input`. -->\r\n<div class=\"relative w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"size()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <!-- Trigger Box Container with Add-on Support -->\r\n <!-- Nhãn nổi kiểu filled/standard thêm hàng 20px phía trên khung: addon\r\n phải bám ĐÁY cho thẳng hàng với khung nhập, không bám hàng nhãn. -->\r\n <div\r\n class=\"flex w-full shadow-xs\"\r\n [class.items-stretch]=\"!floatingLabel()\"\r\n [class.items-end]=\"floatingLabel()\"\r\n >\r\n @if (addonType() === 'left') {\r\n <div\r\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\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n >\r\n @if (addonText()) {\r\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\r\n } @else {\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n\r\n <ol-field-frame\r\n class=\"min-w-0 flex-1\"\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"size()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"!!value()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <div\r\n [id]=\"inputId()\"\r\n role=\"combobox\"\r\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\r\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n aria-haspopup=\"listbox\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.tabindex]=\"disabled() ? -1 : 0\"\r\n (click)=\"toggleDropdown()\"\r\n (keydown.enter)=\"toggleDropdown()\"\r\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\r\n class=\"ol-field-box 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\"\r\n [ngClass]=\"fieldTextClasses()\"\r\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\r\n [class.rounded-md]=\"addonType() === 'none'\"\r\n [class.rounded-e-md]=\"addonType() === 'left'\"\r\n [class.rounded-s-md]=\"addonType() === 'right'\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.border-brand]=\"isOpen() && state() === 'default'\"\r\n [class.ring-2]=\"isOpen()\"\r\n [class.ring-brand-subtle]=\"isOpen()\"\r\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled() || readonly()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n [class.opacity-60]=\"disabled()\"\r\n >\r\n <div class=\"flex items-center gap-2 overflow-hidden min-w-0\">\r\n @if (addonType() === 'none') {\r\n <!-- Icon trong ô khi không có addon; `addonIcon` đổi được. -->\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n <span\r\n class=\"truncate font-normal\"\r\n data-ol-field-text\r\n [attr.data-ol-field-placeholder]=\"value() ? null : ''\"\r\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\r\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\r\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\r\n [class.text-body]=\"!value() && state() === 'error'\"\r\n [class.text-heading]=\"!!value()\"\r\n >\r\n {{ value() || placeholder() }}\r\n </span>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\r\n @if (value() && !disabled() && !readonly()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"clear($event)\"\r\n aria-label=\"Xoá giờ đã chọn\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n <ol-icon name=\"chevron-down\" size=\"sm\" />\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n @if (addonType() === 'right') {\r\n <div\r\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\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n >\r\n @if (addonText()) {\r\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\r\n } @else {\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Popover Panel -->\r\n @if (isOpen()) {\r\n <div\r\n #panel\r\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\"\r\n >\r\n @if (showPresets()) {\r\n <!-- Preset quick times -->\r\n <div class=\"mb-3 pb-2 border-b border-default\">\r\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">Mốc giờ nhanh</span>\r\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\r\n @for (preset of presetTimes; track preset) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPreset(preset)\"\r\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\r\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\r\n [class.bg-brand-softer]=\"value() === preset\"\r\n [class.text-fg-brand]=\"value() === preset\"\r\n [class.text-body]=\"value() !== preset\"\r\n >\r\n {{ preset }}\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Columns Time Picker -->\r\n <div class=\"flex items-center justify-center gap-2 py-2\">\r\n <!-- Hour Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Giờ</span>\r\n <div\r\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\r\n >\r\n <!-- Top Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n\r\n <ol-scroll-area\r\n #hourArea\r\n scrollbar=\"hidden\"\r\n [smooth]=\"true\"\r\n innerClass=\"p-1 py-4 space-y-1\"\r\n >\r\n @if (format() === '12h') {\r\n @for (h of hours12; track h) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectHour(h)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedHour() === h\"\r\n [class.text-white]=\"selectedHour() === h\"\r\n [class.font-semibold]=\"selectedHour() === h\"\r\n [class.scale-105]=\"selectedHour() === h\"\r\n [class.shadow-xs]=\"selectedHour() === h\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\r\n [class.text-heading]=\"selectedHour() !== h\"\r\n >\r\n {{ pad(h) }}\r\n </button>\r\n }\r\n } @else {\r\n @for (h of hours24; track h) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectHour(h)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedHour() === h\"\r\n [class.text-white]=\"selectedHour() === h\"\r\n [class.font-semibold]=\"selectedHour() === h\"\r\n [class.scale-105]=\"selectedHour() === h\"\r\n [class.shadow-xs]=\"selectedHour() === h\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\r\n [class.text-heading]=\"selectedHour() !== h\"\r\n >\r\n {{ pad(h) }}\r\n </button>\r\n }\r\n }\r\n </ol-scroll-area>\r\n\r\n <!-- Bottom Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n </div>\r\n </div>\r\n\r\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\r\n\r\n <!-- Minute Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Phút</span>\r\n <div\r\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\r\n >\r\n <!-- Top Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n\r\n <ol-scroll-area\r\n #minuteArea\r\n scrollbar=\"hidden\"\r\n [smooth]=\"true\"\r\n innerClass=\"p-1 py-4 space-y-1\"\r\n >\r\n @for (m of minutes; track m) {\r\n @if (m % minuteStep() === 0) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectMinute(m)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedMinute() === m\"\r\n [class.text-white]=\"selectedMinute() === m\"\r\n [class.font-semibold]=\"selectedMinute() === m\"\r\n [class.scale-105]=\"selectedMinute() === m\"\r\n [class.shadow-xs]=\"selectedMinute() === m\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\r\n [class.text-heading]=\"selectedMinute() !== m\"\r\n >\r\n {{ pad(m) }}\r\n </button>\r\n }\r\n }\r\n </ol-scroll-area>\r\n\r\n <!-- Bottom Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n </div>\r\n </div>\r\n\r\n @if (format() === '12h') {\r\n <!-- AM/PM Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Buổi</span>\r\n <div\r\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPeriod('AM')\"\r\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\r\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\r\n [class.text-white]=\"selectedPeriod() === 'AM'\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\r\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\r\n >\r\n AM\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPeriod('PM')\"\r\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\r\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\r\n [class.text-white]=\"selectedPeriod() === 'PM'\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\r\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\r\n >\r\n PM\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Action Footer -->\r\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\r\n <button\r\n type=\"button\"\r\n (click)=\"cancel()\"\r\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\r\n >\r\n Hủy\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"apply()\"\r\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\"\r\n >\r\n Áp dụng\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Helper Text & Error Text -->\r\n @if (state() === 'error' && errorText()) {\r\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\r\n } @else if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n"
|
|
22266
|
+
}, provideOlIcons({ "clock-circle": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 7v5l4 2\"/></svg>" })],
|
|
22267
|
+
template: "<!-- Bo góc: Figma `ol-timepicker` (#1149:52243) dùng 12px cho CẢ BỐN size\r\n (rounded-md). Code trước đây dùng rounded-lg 16px cho sm/base và\r\n rounded-md 24px cho lg — cùng lỗi đã sửa ở `ol-input`. -->\r\n<div class=\"relative w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"size()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <!-- Trigger Box Container with Add-on Support -->\r\n <!-- Nhãn nổi kiểu filled/standard thêm hàng 20px phía trên khung: addon\r\n phải bám ĐÁY cho thẳng hàng với khung nhập, không bám hàng nhãn. -->\r\n <div\r\n class=\"flex w-full shadow-xs\"\r\n [class.items-stretch]=\"!floatingLabel()\"\r\n [class.items-end]=\"floatingLabel()\"\r\n >\r\n @if (addonType() === 'left') {\r\n <div\r\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\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n >\r\n @if (addonText()) {\r\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\r\n } @else {\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n\r\n <ol-field-frame\r\n class=\"min-w-0 flex-1\"\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [labelId]=\"labelId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"size()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"!!value()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <div\r\n [id]=\"inputId()\"\r\n role=\"combobox\"\r\n [attr.aria-labelledby]=\"label() ? labelId() : null\"\r\n [attr.aria-label]=\"label() ? null : ariaLabel() || null\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n aria-haspopup=\"listbox\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n [attr.tabindex]=\"disabled() ? -1 : 0\"\r\n (click)=\"toggleDropdown()\"\r\n (keydown.enter)=\"toggleDropdown()\"\r\n (keydown.space)=\"toggleDropdown(); $event.preventDefault()\"\r\n class=\"ol-field-box 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\"\r\n [ngClass]=\"fieldTextClasses()\"\r\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\r\n [class.rounded-md]=\"addonType() === 'none'\"\r\n [class.rounded-e-md]=\"addonType() === 'left'\"\r\n [class.rounded-s-md]=\"addonType() === 'right'\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.border-brand]=\"isOpen() && state() === 'default'\"\r\n [class.ring-2]=\"isOpen()\"\r\n [class.ring-brand-subtle]=\"isOpen()\"\r\n [class.border-danger-subtle]=\"state() === 'error' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.text-fg-danger-strong]=\"state() === 'error' && !disabled()\"\r\n [class.border-success-subtle]=\"state() === 'success' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.text-fg-success-strong]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled() || readonly()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n [class.opacity-60]=\"disabled()\"\r\n >\r\n <div class=\"flex items-center gap-2 overflow-hidden min-w-0\">\r\n @if (addonType() === 'none') {\r\n <!-- Icon trong ô khi không có addon; `addonIcon` đổi được. -->\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n <span\r\n class=\"truncate font-normal\"\r\n data-ol-field-text\r\n [attr.data-ol-field-placeholder]=\"value() ? null : ''\"\r\n [class.text-sm]=\"size() === 'sm' || size() === 'base'\"\r\n [class.text-base]=\"size() === 'lg' || size() === 'xl'\"\r\n [class.text-body-subtle]=\"!value() && state() !== 'error'\"\r\n [class.text-body]=\"!value() && state() === 'error'\"\r\n [class.text-heading]=\"!!value()\"\r\n >\r\n {{ value() || placeholder() }}\r\n </span>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-1.5 shrink-0 ms-1\">\r\n @if (value() && !disabled() && !readonly()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"clear($event)\"\r\n aria-label=\"Xoá giờ đã chọn\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n <ol-icon name=\"chevron_down\" size=\"sm\" />\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n @if (addonType() === 'right') {\r\n <div\r\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\"\r\n [class.h-9]=\"size() === 'sm'\"\r\n [class.h-10]=\"size() === 'base'\"\r\n [class.h-12]=\"size() === 'lg'\"\r\n [class.h-13]=\"size() === 'xl'\"\r\n >\r\n @if (addonText()) {\r\n <span class=\"text-xs font-medium\">{{ addonText() }}</span>\r\n } @else {\r\n <ol-icon [name]=\"addonIcon() || 'clock-circle'\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Popover Panel -->\r\n @if (isOpen()) {\r\n <div\r\n #panel\r\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\"\r\n >\r\n @if (showPresets()) {\r\n <!-- Preset quick times -->\r\n <div class=\"mb-3 pb-2 border-b border-default\">\r\n <span class=\"text-xs font-semibold text-body-subtle block mb-1.5\">Mốc giờ nhanh</span>\r\n <div class=\"grid grid-cols-4 gap-1 max-h-24 overflow-y-auto overflow-x-hidden pr-1\">\r\n @for (preset of presetTimes; track preset) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPreset(preset)\"\r\n class=\"px-2 py-1 text-xs rounded-md text-center transition-colors font-medium\"\r\n [class.hover:bg-neutral-tertiary]=\"value() !== preset\"\r\n [class.bg-brand-softer]=\"value() === preset\"\r\n [class.text-fg-brand]=\"value() === preset\"\r\n [class.text-body]=\"value() !== preset\"\r\n >\r\n {{ preset }}\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Columns Time Picker -->\r\n <div class=\"flex items-center justify-center gap-2 py-2\">\r\n <!-- Hour Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Giờ</span>\r\n <div\r\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\r\n >\r\n <!-- Top Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n\r\n <ol-scroll-area\r\n #hourArea\r\n scrollbar=\"hidden\"\r\n [smooth]=\"true\"\r\n innerClass=\"p-1 py-4 space-y-1\"\r\n >\r\n @if (format() === '12h') {\r\n @for (h of hours12; track h) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectHour(h)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedHour() === h\"\r\n [class.text-white]=\"selectedHour() === h\"\r\n [class.font-semibold]=\"selectedHour() === h\"\r\n [class.scale-105]=\"selectedHour() === h\"\r\n [class.shadow-xs]=\"selectedHour() === h\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\r\n [class.text-heading]=\"selectedHour() !== h\"\r\n >\r\n {{ pad(h) }}\r\n </button>\r\n }\r\n } @else {\r\n @for (h of hours24; track h) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectHour(h)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedHour() === h\"\r\n [class.text-white]=\"selectedHour() === h\"\r\n [class.font-semibold]=\"selectedHour() === h\"\r\n [class.scale-105]=\"selectedHour() === h\"\r\n [class.shadow-xs]=\"selectedHour() === h\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedHour() !== h\"\r\n [class.text-heading]=\"selectedHour() !== h\"\r\n >\r\n {{ pad(h) }}\r\n </button>\r\n }\r\n }\r\n </ol-scroll-area>\r\n\r\n <!-- Bottom Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n </div>\r\n </div>\r\n\r\n <span class=\"text-lg font-bold text-heading mt-4\">:</span>\r\n\r\n <!-- Minute Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Phút</span>\r\n <div\r\n class=\"relative group/col w-16 h-44 rounded-lg overflow-hidden border border-default\"\r\n >\r\n <!-- Top Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 top-0 h-6 bg-gradient-to-b from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n\r\n <ol-scroll-area\r\n #minuteArea\r\n scrollbar=\"hidden\"\r\n [smooth]=\"true\"\r\n innerClass=\"p-1 py-4 space-y-1\"\r\n >\r\n @for (m of minutes; track m) {\r\n @if (m % minuteStep() === 0) {\r\n <button\r\n type=\"button\"\r\n (click)=\"selectMinute(m)\"\r\n class=\"w-full py-1 text-center text-sm rounded-md transition-all font-medium\"\r\n [class.bg-brand]=\"selectedMinute() === m\"\r\n [class.text-white]=\"selectedMinute() === m\"\r\n [class.font-semibold]=\"selectedMinute() === m\"\r\n [class.scale-105]=\"selectedMinute() === m\"\r\n [class.shadow-xs]=\"selectedMinute() === m\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedMinute() !== m\"\r\n [class.text-heading]=\"selectedMinute() !== m\"\r\n >\r\n {{ pad(m) }}\r\n </button>\r\n }\r\n }\r\n </ol-scroll-area>\r\n\r\n <!-- Bottom Fade Gradient -->\r\n <div\r\n class=\"pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-gradient-to-t from-neutral-primary to-transparent z-10\"\r\n ></div>\r\n </div>\r\n </div>\r\n\r\n @if (format() === '12h') {\r\n <!-- AM/PM Column -->\r\n <div class=\"flex flex-col items-center\">\r\n <span class=\"text-xs font-semibold text-body-subtle mb-1\">Buổi</span>\r\n <div\r\n class=\"h-44 w-16 flex flex-col justify-center gap-2 border border-default rounded-lg p-1\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPeriod('AM')\"\r\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\r\n [class.bg-brand]=\"selectedPeriod() === 'AM'\"\r\n [class.text-white]=\"selectedPeriod() === 'AM'\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'AM'\"\r\n [class.text-heading]=\"selectedPeriod() !== 'AM'\"\r\n >\r\n AM\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"selectPeriod('PM')\"\r\n class=\"w-full py-2 text-center text-xs rounded-md transition-colors font-semibold\"\r\n [class.bg-brand]=\"selectedPeriod() === 'PM'\"\r\n [class.text-white]=\"selectedPeriod() === 'PM'\"\r\n [class.hover:bg-neutral-tertiary]=\"selectedPeriod() !== 'PM'\"\r\n [class.text-heading]=\"selectedPeriod() !== 'PM'\"\r\n >\r\n PM\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Action Footer -->\r\n <div class=\"flex items-center justify-between pt-3 mt-2 border-t border-default\">\r\n <button\r\n type=\"button\"\r\n (click)=\"cancel()\"\r\n class=\"px-3 py-1.5 text-xs font-medium text-body hover:text-heading hover:bg-neutral-tertiary rounded-lg transition-colors\"\r\n >\r\n Hủy\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"apply()\"\r\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\"\r\n >\r\n Áp dụng\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Helper Text & Error Text -->\r\n @if (state() === 'error' && errorText()) {\r\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\r\n } @else if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n"
|
|
22046
22268
|
}]
|
|
22047
22269
|
}],
|
|
22048
22270
|
ctorParameters: () => [{ type: i0.ElementRef }],
|
|
@@ -22362,15 +22584,17 @@ var OlAutocomplete = class OlAutocomplete {
|
|
|
22362
22584
|
onChange = () => {};
|
|
22363
22585
|
onTouched = () => {};
|
|
22364
22586
|
panel = viewChild("panel", ...ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []);
|
|
22365
|
-
overlayContainer
|
|
22587
|
+
overlayContainer;
|
|
22366
22588
|
detachPanel = null;
|
|
22367
22589
|
releaseEscape = null;
|
|
22368
22590
|
stopTracking = null;
|
|
22369
|
-
destroyRef
|
|
22591
|
+
destroyRef;
|
|
22370
22592
|
wasOpen = false;
|
|
22371
22593
|
hengio = null;
|
|
22372
22594
|
constructor(elementRef) {
|
|
22373
22595
|
this.elementRef = elementRef;
|
|
22596
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
22597
|
+
this.destroyRef = inject(DestroyRef);
|
|
22374
22598
|
effect(() => {
|
|
22375
22599
|
const open = this.isOpen();
|
|
22376
22600
|
if (open === this.wasOpen) return;
|
|
@@ -22867,7 +23091,10 @@ var OlAutocomplete = class OlAutocomplete {
|
|
|
22867
23091
|
provide: NG_VALUE_ACCESSOR,
|
|
22868
23092
|
useExisting: forwardRef(() => OlAutocomplete),
|
|
22869
23093
|
multi: true
|
|
22870
|
-
}
|
|
23094
|
+
}, provideOlIcons({
|
|
23095
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
23096
|
+
"magnifier": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\" d=\"m20 20l-3-3\"/></svg>"
|
|
23097
|
+
})],
|
|
22871
23098
|
queries: [{
|
|
22872
23099
|
propertyName: "optionTemplate",
|
|
22873
23100
|
first: true,
|
|
@@ -22882,7 +23109,7 @@ var OlAutocomplete = class OlAutocomplete {
|
|
|
22882
23109
|
isSignal: true
|
|
22883
23110
|
}],
|
|
22884
23111
|
ngImport: i0,
|
|
22885
|
-
template: "<div class=\"relative w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"resolvedSize()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"resolvedSize()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasFloatValue()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <!--\r\n Main Input Box Container.\r\n\r\n `py-1` chứ KHÔNG phải `p-1.5` cho chế độ multiple: `p-1.5` đệm MỌI PHÍA 6px,\r\n cộng vào nội dung 34px thành 46px — vượt `min-h-10` (40px). Đo 2026-09-17:\r\n ô đa chọn cao **46px** trong khi ô đơn chọn ngay cạnh cao **40px**, hai ô\r\n cùng một hàng lưới nên lệch thấy rõ. Đệm dọc 4px vừa đủ cho thẻ cao 24px\r\n (4 + 24 + 4 = 32 < 40) mà vẫn còn chỗ khi thẻ xuống dòng thứ hai.\r\n\r\n `content-center` đi kèm `items-center`: với `flex-wrap`, `items-center` chỉ\r\n căn giữa TRONG TỪNG DÒNG, cả khối nhiều dòng vẫn dính mép trên.\r\n -->\r\n <div\r\n class=\"ol-field-box relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\r\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\r\n [class.rounded-md]=\"true\"\r\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\r\n [class.min-h-10]=\"resolvedSize() === 'base'\"\r\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\r\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.border-brand]=\"isOpen() && state() === 'default'\"\r\n [class.ring-2]=\"isOpen()\"\r\n [class.ring-brand-subtle]=\"isOpen()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled() || readonly()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n [class.opacity-60]=\"disabled()\"\r\n [ngClass]=\"boxPaddingClass()\"\r\n >\r\n <!--\r\n Icon kính lúp LUÔN hiện. Bản cũ ẩn nó khi `multiple` đã có giá trị\r\n (`@if (!multiple() || !value() || value()?.length === 0)`) — chọn một thẻ\r\n là icon biến mất, xoá hết thẻ thì nó quay lại. Đo 2026-09-17 trên\r\n `/autocomplete`: ô đơn chọn 1 icon `magnifer`, ô đa chọn **0 icon** khi\r\n có 2 thẻ.\r\n\r\n Figma không có cơ sở cho việc ẩn: đọc `ol-autocomplete-examples`\r\n (#1149:53126) thì **6/6 Type đều có `search` trong `ol-input`**, không\r\n Type nào bỏ. Figma cũng không vẽ biến thể đa chọn có thẻ, nên đây là lựa\r\n chọn của code chứ không phải thiết kế.\r\n\r\n `shrink-0` để icon không bị các thẻ ép bẹp khi ô chật.\r\n -->\r\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0 me-0.5\" />\r\n\r\n <!-- Multiple Selected Tags / Chips -->\r\n @if (multiple() && value() && value()?.length > 0) {\r\n @for (itemVal of value(); track itemVal) {\r\n <!--\r\n `h-6` cố định (24px): để thẻ tự co theo nội dung thì mỗi thẻ cao một\r\n kiểu và ô nhập bị đẩy theo thẻ cao nhất. 24px + đệm dọc 4×2 = 32px,\r\n vẫn nằm trong `min-h-10` (40px) nên ô đa chọn cao đúng bằng ô đơn chọn.\r\n -->\r\n <span\r\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\"\r\n >\r\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\r\n @if (!disabled() && !readonly()) {\r\n <!-- Vuông như nút xoá chung — xem lý do ở đó. -->\r\n <button\r\n type=\"button\"\r\n (click)=\"removeTag(itemVal, $event)\"\r\n [attr.aria-label]=\"'Gỡ ' + getDisplayLabelForValue(itemVal)\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n }\r\n\r\n <!-- Input text field -->\r\n <input\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n role=\"combobox\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n type=\"text\"\r\n [disabled]=\"disabled()\"\r\n [readonly]=\"readonly()\"\r\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchChange($event)\"\r\n (focus)=\"onInputFocus()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n class=\"flex-1 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\r\n [ngClass]=\"fieldTextClass() + ' ' + (hasChips() ? 'min-w-0 focus:min-w-16' : 'min-w-20')\"\r\n />\r\n\r\n <!--\r\n MỘT ô điều khiển, đổi vai theo trạng thái — nút xoá, chevron hoặc\r\n spinner, không xếp cạnh nhau (bản cũ hiện cùng lúc 3–4 phần tử). Ô rộng\r\n đúng bằng icon (`controlClass`): chữ ↔ icon 8px như Figma.\r\n -->\r\n <!--\r\n Chọn nhiều: cụm nút ra khỏi luồng (absolute, như ol-select) và khung\r\n chừa pe-9. Để trong luồng thì cụm nút + ô gõ không vừa phần còn lại của\r\n dòng chip cuối, cùng rớt xuống một dòng mới chỉ chứa khoảng trống — đo\r\n 2026-09-29: dư 31–36px ở mọi kiểu và cỡ.\r\n -->\r\n <div [class]=\"controlClass()\">\r\n @if (loading()) {\r\n <ol-spinner size=\"xs\" color=\"brand\" />\r\n } @else if (\r\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\r\n !disabled() &&\r\n !readonly()\r\n ) {\r\n <!--\r\n Nút bọc `size-4` ĐÚNG BẰNG icon, không `size-6`.\r\n\r\n `size-6` (24px) bọc SVG 16px thì dư 4px mỗi bên, đẩy icon vào trong:\r\n đo 2026-09-17 thấy ô đang hiện nút xoá có lề phải **19px** trong khi\r\n ô hiện chevron trần chỉ 15px — cùng một component mà hai trạng thái\r\n lệch nhau 4px, đúng chỗ anh chỉ trong ảnh.\r\n\r\n Vùng bấm vẫn đủ lớn nhờ `p-1` ở NGOÀI khung đo: `size-4` + đệm\r\n không cộng vào bề rộng chiếm chỗ vì nút nằm trong ô `w-7` cố định.\r\n -->\r\n <button\r\n type=\"button\"\r\n (click)=\"clearAll($event)\"\r\n aria-label=\"Xoá toàn bộ lựa chọn\"\r\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\r\n >\r\n <ol-icon name=\"close\" size=\"sm\" />\r\n </button>\r\n } @else {\r\n <ol-icon\r\n name=\"chevron-down\"\r\n size=\"sm\"\r\n class=\"transition-transform\"\r\n [class.rotate-180]=\"isOpen()\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n <!--\r\n Dropdown Suggestions Menu.\r\n\r\n Token đọc từ Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\r\n bo góc 12px · đệm 8px · nền `bg-neutral-primary-medium` · viền\r\n `border-default-medium`.\r\n\r\n Hai token nền/viền TRÙNG NHAU ở chế độ sáng nên đổi hay không nhìn cũng\r\n thế — nhưng CHẾ ĐỘ TỐI khác hẳn: `bg-primary` là gray-950 (gần đen, panel\r\n chìm vào nền) còn `bg-primary-medium` là gray-800 (panel nổi lên).\r\n `ol-select` cùng lệch này, sửa chung một lượt.\r\n -->\r\n @if (isOpen()) {\r\n <div\r\n #panel\r\n [id]=\"listboxId()\"\r\n role=\"listbox\"\r\n [attr.aria-label]=\"label() || 'Gợi ý'\"\r\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\"\r\n [class.w-full]=\"matchTriggerWidth()\"\r\n [class]=\"menuClass()\"\r\n >\r\n @if (filteredOptions().length > 0) {\r\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\r\n <!-- Tiêu đề nhóm chèn giữa vòng lặp phẳng, xem `groupNameAt`. -->\r\n @if (groupNameAt(idx); as groupName) {\r\n <div\r\n role=\"presentation\"\r\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\r\n >\r\n {{ groupName }}\r\n </div>\r\n }\r\n <div\r\n [id]=\"optionId(idx)\"\r\n role=\"option\"\r\n [attr.aria-selected]=\"isSelected(item)\"\r\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\r\n (click)=\"selectOption(item)\"\r\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\r\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\r\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\r\n [class.opacity-50]=\"item?.disabled\"\r\n [class.cursor-not-allowed]=\"item?.disabled\"\r\n >\r\n @if (optionTemplate) {\r\n <!-- Custom Option Template -->\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n optionTemplate;\r\n context: { $implicit: item, selected: isSelected(item) }\r\n \"\r\n />\r\n } @else {\r\n <!-- Default Option Display -->\r\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\r\n @if (item?.avatar) {\r\n <img\r\n [src]=\"item.avatar\"\r\n alt=\"Ảnh đại diện\"\r\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\r\n />\r\n }\r\n <div class=\"truncate\">\r\n <span class=\"font-medium text-heading block truncate\">{{\r\n getItemLabel(item)\r\n }}</span>\r\n @if (item?.subLabel) {\r\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\r\n @if (item?.badge) {\r\n <span\r\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\r\n >\r\n {{ item.badge }}\r\n </span>\r\n }\r\n @if (isSelected(item)) {\r\n <ol-icon name=\"check\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n } @else if (waitingForTerm()) {\r\n <!--\r\n Chế độ LỌC TỪ XA, chưa gõ đủ `minTermLength`. Phải là thông báo RIÊNG\r\n chứ không dùng `emptyText`: \"Không tìm thấy kết quả\" là sai sự thật —\r\n chưa tìm gì cả. Cùng lối với `typeToSearchText` của ng-select.\r\n -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">\r\n <p>{{ typeToSearchText() }}</p>\r\n </div>\r\n } @else {\r\n <!-- Empty State & Allow Create Option -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">\r\n <p>{{ emptyText() }}</p>\r\n @if (allowCreate() && searchQuery().trim()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"onCreateNew()\"\r\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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" />\r\n <span>Thêm mới: \"{{ searchQuery().trim() }}\"</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Helper Text & Error Text -->\r\n @if (state() === 'error' && errorText()) {\r\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\r\n } @else if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n",
|
|
23112
|
+
template: "<div class=\"relative w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"resolvedSize()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"resolvedSize()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasFloatValue()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <!--\r\n Main Input Box Container.\r\n\r\n `py-1` chứ KHÔNG phải `p-1.5` cho chế độ multiple: `p-1.5` đệm MỌI PHÍA 6px,\r\n cộng vào nội dung 34px thành 46px — vượt `min-h-10` (40px). Đo 2026-09-17:\r\n ô đa chọn cao **46px** trong khi ô đơn chọn ngay cạnh cao **40px**, hai ô\r\n cùng một hàng lưới nên lệch thấy rõ. Đệm dọc 4px vừa đủ cho thẻ cao 24px\r\n (4 + 24 + 4 = 32 < 40) mà vẫn còn chỗ khi thẻ xuống dòng thứ hai.\r\n\r\n `content-center` đi kèm `items-center`: với `flex-wrap`, `items-center` chỉ\r\n căn giữa TRONG TỪNG DÒNG, cả khối nhiều dòng vẫn dính mép trên.\r\n -->\r\n <div\r\n class=\"ol-field-box relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\r\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\r\n [class.rounded-md]=\"true\"\r\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\r\n [class.min-h-10]=\"resolvedSize() === 'base'\"\r\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\r\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.border-brand]=\"isOpen() && state() === 'default'\"\r\n [class.ring-2]=\"isOpen()\"\r\n [class.ring-brand-subtle]=\"isOpen()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled() || readonly()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n [class.opacity-60]=\"disabled()\"\r\n [ngClass]=\"boxPaddingClass()\"\r\n >\r\n <!--\r\n Icon kính lúp LUÔN hiện. Bản cũ ẩn nó khi `multiple` đã có giá trị\r\n (`@if (!multiple() || !value() || value()?.length === 0)`) — chọn một thẻ\r\n là icon biến mất, xoá hết thẻ thì nó quay lại. Đo 2026-09-17 trên\r\n `/autocomplete`: ô đơn chọn 1 icon `magnifer`, ô đa chọn **0 icon** khi\r\n có 2 thẻ.\r\n\r\n Figma không có cơ sở cho việc ẩn: đọc `ol-autocomplete-examples`\r\n (#1149:53126) thì **6/6 Type đều có `search` trong `ol-input`**, không\r\n Type nào bỏ. Figma cũng không vẽ biến thể đa chọn có thẻ, nên đây là lựa\r\n chọn của code chứ không phải thiết kế.\r\n\r\n `shrink-0` để icon không bị các thẻ ép bẹp khi ô chật.\r\n -->\r\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0 me-0.5\" />\r\n\r\n <!-- Multiple Selected Tags / Chips -->\r\n @if (multiple() && value() && value()?.length > 0) {\r\n @for (itemVal of value(); track itemVal) {\r\n <!--\r\n `h-6` cố định (24px): để thẻ tự co theo nội dung thì mỗi thẻ cao một\r\n kiểu và ô nhập bị đẩy theo thẻ cao nhất. 24px + đệm dọc 4×2 = 32px,\r\n vẫn nằm trong `min-h-10` (40px) nên ô đa chọn cao đúng bằng ô đơn chọn.\r\n -->\r\n <span\r\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\"\r\n >\r\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\r\n @if (!disabled() && !readonly()) {\r\n <!-- Vuông như nút xoá chung — xem lý do ở đó. -->\r\n <button\r\n type=\"button\"\r\n (click)=\"removeTag(itemVal, $event)\"\r\n [attr.aria-label]=\"'Gỡ ' + getDisplayLabelForValue(itemVal)\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n }\r\n\r\n <!-- Input text field -->\r\n <input\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n role=\"combobox\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n type=\"text\"\r\n [disabled]=\"disabled()\"\r\n [readonly]=\"readonly()\"\r\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchChange($event)\"\r\n (focus)=\"onInputFocus()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n class=\"flex-1 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\r\n [ngClass]=\"fieldTextClass() + ' ' + (hasChips() ? 'min-w-0 focus:min-w-16' : 'min-w-20')\"\r\n />\r\n\r\n <!--\r\n MỘT ô điều khiển, đổi vai theo trạng thái — nút xoá, chevron hoặc\r\n spinner, không xếp cạnh nhau (bản cũ hiện cùng lúc 3–4 phần tử). Ô rộng\r\n đúng bằng icon (`controlClass`): chữ ↔ icon 8px như Figma.\r\n -->\r\n <!--\r\n Chọn nhiều: cụm nút ra khỏi luồng (absolute, như ol-select) và khung\r\n chừa pe-9. Để trong luồng thì cụm nút + ô gõ không vừa phần còn lại của\r\n dòng chip cuối, cùng rớt xuống một dòng mới chỉ chứa khoảng trống — đo\r\n 2026-09-29: dư 31–36px ở mọi kiểu và cỡ.\r\n -->\r\n <div [class]=\"controlClass()\">\r\n @if (loading()) {\r\n <ol-spinner size=\"xs\" color=\"brand\" />\r\n } @else if (\r\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\r\n !disabled() &&\r\n !readonly()\r\n ) {\r\n <!--\r\n Nút bọc `size-4` ĐÚNG BẰNG icon, không `size-6`.\r\n\r\n `size-6` (24px) bọc SVG 16px thì dư 4px mỗi bên, đẩy icon vào trong:\r\n đo 2026-09-17 thấy ô đang hiện nút xoá có lề phải **19px** trong khi\r\n ô hiện chevron trần chỉ 15px — cùng một component mà hai trạng thái\r\n lệch nhau 4px, đúng chỗ anh chỉ trong ảnh.\r\n\r\n Vùng bấm vẫn đủ lớn nhờ `p-1` ở NGOÀI khung đo: `size-4` + đệm\r\n không cộng vào bề rộng chiếm chỗ vì nút nằm trong ô `w-7` cố định.\r\n -->\r\n <button\r\n type=\"button\"\r\n (click)=\"clearAll($event)\"\r\n aria-label=\"Xoá toàn bộ lựa chọn\"\r\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\r\n >\r\n <ol-icon name=\"close\" size=\"sm\" />\r\n </button>\r\n } @else {\r\n <ol-icon\r\n name=\"chevron_down\"\r\n size=\"sm\"\r\n class=\"transition-transform\"\r\n [class.rotate-180]=\"isOpen()\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n <!--\r\n Dropdown Suggestions Menu.\r\n\r\n Token đọc từ Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\r\n bo góc 12px · đệm 8px · nền `bg-neutral-primary-medium` · viền\r\n `border-default-medium`.\r\n\r\n Hai token nền/viền TRÙNG NHAU ở chế độ sáng nên đổi hay không nhìn cũng\r\n thế — nhưng CHẾ ĐỘ TỐI khác hẳn: `bg-primary` là gray-950 (gần đen, panel\r\n chìm vào nền) còn `bg-primary-medium` là gray-800 (panel nổi lên).\r\n `ol-select` cùng lệch này, sửa chung một lượt.\r\n -->\r\n @if (isOpen()) {\r\n <div\r\n #panel\r\n [id]=\"listboxId()\"\r\n role=\"listbox\"\r\n [attr.aria-label]=\"label() || 'Gợi ý'\"\r\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\"\r\n [class.w-full]=\"matchTriggerWidth()\"\r\n [class]=\"menuClass()\"\r\n >\r\n @if (filteredOptions().length > 0) {\r\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\r\n <!-- Tiêu đề nhóm chèn giữa vòng lặp phẳng, xem `groupNameAt`. -->\r\n @if (groupNameAt(idx); as groupName) {\r\n <div\r\n role=\"presentation\"\r\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\r\n >\r\n {{ groupName }}\r\n </div>\r\n }\r\n <div\r\n [id]=\"optionId(idx)\"\r\n role=\"option\"\r\n [attr.aria-selected]=\"isSelected(item)\"\r\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\r\n (click)=\"selectOption(item)\"\r\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\r\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\r\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\r\n [class.opacity-50]=\"item?.disabled\"\r\n [class.cursor-not-allowed]=\"item?.disabled\"\r\n >\r\n @if (optionTemplate) {\r\n <!-- Custom Option Template -->\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n optionTemplate;\r\n context: { $implicit: item, selected: isSelected(item) }\r\n \"\r\n />\r\n } @else {\r\n <!-- Default Option Display -->\r\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\r\n @if (item?.avatar) {\r\n <img\r\n [src]=\"item.avatar\"\r\n alt=\"Ảnh đại diện\"\r\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\r\n />\r\n }\r\n <div class=\"truncate\">\r\n <span class=\"font-medium text-heading block truncate\">{{\r\n getItemLabel(item)\r\n }}</span>\r\n @if (item?.subLabel) {\r\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\r\n @if (item?.badge) {\r\n <span\r\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\r\n >\r\n {{ item.badge }}\r\n </span>\r\n }\r\n @if (isSelected(item)) {\r\n <ol-icon name=\"check\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n } @else if (waitingForTerm()) {\r\n <!--\r\n Chế độ LỌC TỪ XA, chưa gõ đủ `minTermLength`. Phải là thông báo RIÊNG\r\n chứ không dùng `emptyText`: \"Không tìm thấy kết quả\" là sai sự thật —\r\n chưa tìm gì cả. Cùng lối với `typeToSearchText` của ng-select.\r\n -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">\r\n <p>{{ typeToSearchText() }}</p>\r\n </div>\r\n } @else {\r\n <!-- Empty State & Allow Create Option -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">\r\n <p>{{ emptyText() }}</p>\r\n @if (allowCreate() && searchQuery().trim()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"onCreateNew()\"\r\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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" />\r\n <span>Thêm mới: \"{{ searchQuery().trim() }}\"</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Helper Text & Error Text -->\r\n @if (state() === 'error' && errorText()) {\r\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\r\n } @else if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n",
|
|
22886
23113
|
dependencies: [
|
|
22887
23114
|
{
|
|
22888
23115
|
kind: "ngmodule",
|
|
@@ -23017,8 +23244,11 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
23017
23244
|
provide: NG_VALUE_ACCESSOR,
|
|
23018
23245
|
useExisting: forwardRef(() => OlAutocomplete),
|
|
23019
23246
|
multi: true
|
|
23020
|
-
}
|
|
23021
|
-
template: "<div class=\"relative w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"resolvedSize()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"resolvedSize()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasFloatValue()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <!--\r\n Main Input Box Container.\r\n\r\n `py-1` chứ KHÔNG phải `p-1.5` cho chế độ multiple: `p-1.5` đệm MỌI PHÍA 6px,\r\n cộng vào nội dung 34px thành 46px — vượt `min-h-10` (40px). Đo 2026-09-17:\r\n ô đa chọn cao **46px** trong khi ô đơn chọn ngay cạnh cao **40px**, hai ô\r\n cùng một hàng lưới nên lệch thấy rõ. Đệm dọc 4px vừa đủ cho thẻ cao 24px\r\n (4 + 24 + 4 = 32 < 40) mà vẫn còn chỗ khi thẻ xuống dòng thứ hai.\r\n\r\n `content-center` đi kèm `items-center`: với `flex-wrap`, `items-center` chỉ\r\n căn giữa TRONG TỪNG DÒNG, cả khối nhiều dòng vẫn dính mép trên.\r\n -->\r\n <div\r\n class=\"ol-field-box relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\r\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\r\n [class.rounded-md]=\"true\"\r\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\r\n [class.min-h-10]=\"resolvedSize() === 'base'\"\r\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\r\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.border-brand]=\"isOpen() && state() === 'default'\"\r\n [class.ring-2]=\"isOpen()\"\r\n [class.ring-brand-subtle]=\"isOpen()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled() || readonly()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n [class.opacity-60]=\"disabled()\"\r\n [ngClass]=\"boxPaddingClass()\"\r\n >\r\n <!--\r\n Icon kính lúp LUÔN hiện. Bản cũ ẩn nó khi `multiple` đã có giá trị\r\n (`@if (!multiple() || !value() || value()?.length === 0)`) — chọn một thẻ\r\n là icon biến mất, xoá hết thẻ thì nó quay lại. Đo 2026-09-17 trên\r\n `/autocomplete`: ô đơn chọn 1 icon `magnifer`, ô đa chọn **0 icon** khi\r\n có 2 thẻ.\r\n\r\n Figma không có cơ sở cho việc ẩn: đọc `ol-autocomplete-examples`\r\n (#1149:53126) thì **6/6 Type đều có `search` trong `ol-input`**, không\r\n Type nào bỏ. Figma cũng không vẽ biến thể đa chọn có thẻ, nên đây là lựa\r\n chọn của code chứ không phải thiết kế.\r\n\r\n `shrink-0` để icon không bị các thẻ ép bẹp khi ô chật.\r\n -->\r\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0 me-0.5\" />\r\n\r\n <!-- Multiple Selected Tags / Chips -->\r\n @if (multiple() && value() && value()?.length > 0) {\r\n @for (itemVal of value(); track itemVal) {\r\n <!--\r\n `h-6` cố định (24px): để thẻ tự co theo nội dung thì mỗi thẻ cao một\r\n kiểu và ô nhập bị đẩy theo thẻ cao nhất. 24px + đệm dọc 4×2 = 32px,\r\n vẫn nằm trong `min-h-10` (40px) nên ô đa chọn cao đúng bằng ô đơn chọn.\r\n -->\r\n <span\r\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\"\r\n >\r\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\r\n @if (!disabled() && !readonly()) {\r\n <!-- Vuông như nút xoá chung — xem lý do ở đó. -->\r\n <button\r\n type=\"button\"\r\n (click)=\"removeTag(itemVal, $event)\"\r\n [attr.aria-label]=\"'Gỡ ' + getDisplayLabelForValue(itemVal)\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n }\r\n\r\n <!-- Input text field -->\r\n <input\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n role=\"combobox\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n type=\"text\"\r\n [disabled]=\"disabled()\"\r\n [readonly]=\"readonly()\"\r\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchChange($event)\"\r\n (focus)=\"onInputFocus()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n class=\"flex-1 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\r\n [ngClass]=\"fieldTextClass() + ' ' + (hasChips() ? 'min-w-0 focus:min-w-16' : 'min-w-20')\"\r\n />\r\n\r\n <!--\r\n MỘT ô điều khiển, đổi vai theo trạng thái — nút xoá, chevron hoặc\r\n spinner, không xếp cạnh nhau (bản cũ hiện cùng lúc 3–4 phần tử). Ô rộng\r\n đúng bằng icon (`controlClass`): chữ ↔ icon 8px như Figma.\r\n -->\r\n <!--\r\n Chọn nhiều: cụm nút ra khỏi luồng (absolute, như ol-select) và khung\r\n chừa pe-9. Để trong luồng thì cụm nút + ô gõ không vừa phần còn lại của\r\n dòng chip cuối, cùng rớt xuống một dòng mới chỉ chứa khoảng trống — đo\r\n 2026-09-29: dư 31–36px ở mọi kiểu và cỡ.\r\n -->\r\n <div [class]=\"controlClass()\">\r\n @if (loading()) {\r\n <ol-spinner size=\"xs\" color=\"brand\" />\r\n } @else if (\r\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\r\n !disabled() &&\r\n !readonly()\r\n ) {\r\n <!--\r\n Nút bọc `size-4` ĐÚNG BẰNG icon, không `size-6`.\r\n\r\n `size-6` (24px) bọc SVG 16px thì dư 4px mỗi bên, đẩy icon vào trong:\r\n đo 2026-09-17 thấy ô đang hiện nút xoá có lề phải **19px** trong khi\r\n ô hiện chevron trần chỉ 15px — cùng một component mà hai trạng thái\r\n lệch nhau 4px, đúng chỗ anh chỉ trong ảnh.\r\n\r\n Vùng bấm vẫn đủ lớn nhờ `p-1` ở NGOÀI khung đo: `size-4` + đệm\r\n không cộng vào bề rộng chiếm chỗ vì nút nằm trong ô `w-7` cố định.\r\n -->\r\n <button\r\n type=\"button\"\r\n (click)=\"clearAll($event)\"\r\n aria-label=\"Xoá toàn bộ lựa chọn\"\r\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\r\n >\r\n <ol-icon name=\"close\" size=\"sm\" />\r\n </button>\r\n } @else {\r\n <ol-icon\r\n name=\"chevron-down\"\r\n size=\"sm\"\r\n class=\"transition-transform\"\r\n [class.rotate-180]=\"isOpen()\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n <!--\r\n Dropdown Suggestions Menu.\r\n\r\n Token đọc từ Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\r\n bo góc 12px · đệm 8px · nền `bg-neutral-primary-medium` · viền\r\n `border-default-medium`.\r\n\r\n Hai token nền/viền TRÙNG NHAU ở chế độ sáng nên đổi hay không nhìn cũng\r\n thế — nhưng CHẾ ĐỘ TỐI khác hẳn: `bg-primary` là gray-950 (gần đen, panel\r\n chìm vào nền) còn `bg-primary-medium` là gray-800 (panel nổi lên).\r\n `ol-select` cùng lệch này, sửa chung một lượt.\r\n -->\r\n @if (isOpen()) {\r\n <div\r\n #panel\r\n [id]=\"listboxId()\"\r\n role=\"listbox\"\r\n [attr.aria-label]=\"label() || 'Gợi ý'\"\r\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\"\r\n [class.w-full]=\"matchTriggerWidth()\"\r\n [class]=\"menuClass()\"\r\n >\r\n @if (filteredOptions().length > 0) {\r\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\r\n <!-- Tiêu đề nhóm chèn giữa vòng lặp phẳng, xem `groupNameAt`. -->\r\n @if (groupNameAt(idx); as groupName) {\r\n <div\r\n role=\"presentation\"\r\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\r\n >\r\n {{ groupName }}\r\n </div>\r\n }\r\n <div\r\n [id]=\"optionId(idx)\"\r\n role=\"option\"\r\n [attr.aria-selected]=\"isSelected(item)\"\r\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\r\n (click)=\"selectOption(item)\"\r\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\r\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\r\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\r\n [class.opacity-50]=\"item?.disabled\"\r\n [class.cursor-not-allowed]=\"item?.disabled\"\r\n >\r\n @if (optionTemplate) {\r\n <!-- Custom Option Template -->\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n optionTemplate;\r\n context: { $implicit: item, selected: isSelected(item) }\r\n \"\r\n />\r\n } @else {\r\n <!-- Default Option Display -->\r\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\r\n @if (item?.avatar) {\r\n <img\r\n [src]=\"item.avatar\"\r\n alt=\"Ảnh đại diện\"\r\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\r\n />\r\n }\r\n <div class=\"truncate\">\r\n <span class=\"font-medium text-heading block truncate\">{{\r\n getItemLabel(item)\r\n }}</span>\r\n @if (item?.subLabel) {\r\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\r\n @if (item?.badge) {\r\n <span\r\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\r\n >\r\n {{ item.badge }}\r\n </span>\r\n }\r\n @if (isSelected(item)) {\r\n <ol-icon name=\"check\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n } @else if (waitingForTerm()) {\r\n <!--\r\n Chế độ LỌC TỪ XA, chưa gõ đủ `minTermLength`. Phải là thông báo RIÊNG\r\n chứ không dùng `emptyText`: \"Không tìm thấy kết quả\" là sai sự thật —\r\n chưa tìm gì cả. Cùng lối với `typeToSearchText` của ng-select.\r\n -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">\r\n <p>{{ typeToSearchText() }}</p>\r\n </div>\r\n } @else {\r\n <!-- Empty State & Allow Create Option -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">\r\n <p>{{ emptyText() }}</p>\r\n @if (allowCreate() && searchQuery().trim()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"onCreateNew()\"\r\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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" />\r\n <span>Thêm mới: \"{{ searchQuery().trim() }}\"</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Helper Text & Error Text -->\r\n @if (state() === 'error' && errorText()) {\r\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\r\n } @else if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n"
|
|
23247
|
+
}, provideOlIcons({
|
|
23248
|
+
"close": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M18 6L6 18M6 6l12 12\"/></svg>",
|
|
23249
|
+
"magnifier": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\" d=\"m20 20l-3-3\"/></svg>"
|
|
23250
|
+
})],
|
|
23251
|
+
template: "<div class=\"relative w-full\" [class]=\"labelLayout()\" [style.--ol-label-w]=\"labelWidth() || null\">\r\n @if (label() && !floatingLabel()) {\r\n <ol-field-label\r\n [beside]=\"labelBeside()\"\r\n [size]=\"resolvedSize()\"\r\n [text]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [required]=\"required() || showAsterisk()\"\r\n />\r\n }\r\n\r\n <ol-field-frame\r\n [enabled]=\"floatingLabel()\"\r\n [label]=\"label()\"\r\n [forId]=\"inputId()\"\r\n [variant]=\"floatingVariant()\"\r\n [size]=\"resolvedSize()\"\r\n [state]=\"state()\"\r\n [disabled]=\"disabled()\"\r\n [required]=\"required() || showAsterisk()\"\r\n [hasValue]=\"hasFloatValue()\"\r\n [open]=\"isOpen()\"\r\n >\r\n <!--\r\n Main Input Box Container.\r\n\r\n `py-1` chứ KHÔNG phải `p-1.5` cho chế độ multiple: `p-1.5` đệm MỌI PHÍA 6px,\r\n cộng vào nội dung 34px thành 46px — vượt `min-h-10` (40px). Đo 2026-09-17:\r\n ô đa chọn cao **46px** trong khi ô đơn chọn ngay cạnh cao **40px**, hai ô\r\n cùng một hàng lưới nên lệch thấy rõ. Đệm dọc 4px vừa đủ cho thẻ cao 24px\r\n (4 + 24 + 4 = 32 < 40) mà vẫn còn chỗ khi thẻ xuống dòng thứ hai.\r\n\r\n `content-center` đi kèm `items-center`: với `flex-wrap`, `items-center` chỉ\r\n căn giữa TRONG TỪNG DÒNG, cả khối nhiều dòng vẫn dính mép trên.\r\n -->\r\n <div\r\n class=\"ol-field-box relative flex items-center content-center flex-wrap gap-1.5 w-full border shadow-xs\"\r\n [class.border-default-medium]=\"!isOpen() && state() === 'default'\"\r\n [class.rounded-md]=\"true\"\r\n [class.min-h-9]=\"resolvedSize() === 'sm'\"\r\n [class.min-h-10]=\"resolvedSize() === 'base'\"\r\n [class.min-h-12]=\"resolvedSize() === 'lg'\"\r\n [class.min-h-13]=\"resolvedSize() === 'xl'\"\r\n [class.bg-neutral-secondary-medium]=\"state() === 'default' && !disabled() && !readonly()\"\r\n [class.border-brand]=\"isOpen() && state() === 'default'\"\r\n [class.ring-2]=\"isOpen()\"\r\n [class.ring-brand-subtle]=\"isOpen()\"\r\n [class.border-danger]=\"state() === 'error' && !disabled()\"\r\n [class.bg-danger-soft]=\"state() === 'error' && !disabled()\"\r\n [class.border-success]=\"state() === 'success' && !disabled()\"\r\n [class.bg-success-soft]=\"state() === 'success' && !disabled()\"\r\n [class.bg-disabled]=\"disabled() || readonly()\"\r\n [class.cursor-not-allowed]=\"disabled()\"\r\n [class.opacity-60]=\"disabled()\"\r\n [ngClass]=\"boxPaddingClass()\"\r\n >\r\n <!--\r\n Icon kính lúp LUÔN hiện. Bản cũ ẩn nó khi `multiple` đã có giá trị\r\n (`@if (!multiple() || !value() || value()?.length === 0)`) — chọn một thẻ\r\n là icon biến mất, xoá hết thẻ thì nó quay lại. Đo 2026-09-17 trên\r\n `/autocomplete`: ô đơn chọn 1 icon `magnifer`, ô đa chọn **0 icon** khi\r\n có 2 thẻ.\r\n\r\n Figma không có cơ sở cho việc ẩn: đọc `ol-autocomplete-examples`\r\n (#1149:53126) thì **6/6 Type đều có `search` trong `ol-input`**, không\r\n Type nào bỏ. Figma cũng không vẽ biến thể đa chọn có thẻ, nên đây là lựa\r\n chọn của code chứ không phải thiết kế.\r\n\r\n `shrink-0` để icon không bị các thẻ ép bẹp khi ô chật.\r\n -->\r\n <ol-icon name=\"magnifer\" size=\"sm\" class=\"shrink-0 me-0.5\" />\r\n\r\n <!-- Multiple Selected Tags / Chips -->\r\n @if (multiple() && value() && value()?.length > 0) {\r\n @for (itemVal of value(); track itemVal) {\r\n <!--\r\n `h-6` cố định (24px): để thẻ tự co theo nội dung thì mỗi thẻ cao một\r\n kiểu và ô nhập bị đẩy theo thẻ cao nhất. 24px + đệm dọc 4×2 = 32px,\r\n vẫn nằm trong `min-h-10` (40px) nên ô đa chọn cao đúng bằng ô đơn chọn.\r\n -->\r\n <span\r\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\"\r\n >\r\n <span>{{ getDisplayLabelForValue(itemVal) }}</span>\r\n @if (!disabled() && !readonly()) {\r\n <!-- Vuông như nút xoá chung — xem lý do ở đó. -->\r\n <button\r\n type=\"button\"\r\n (click)=\"removeTag(itemVal, $event)\"\r\n [attr.aria-label]=\"'Gỡ ' + getDisplayLabelForValue(itemVal)\"\r\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\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </span>\r\n }\r\n }\r\n\r\n <!-- Input text field -->\r\n <input\r\n [id]=\"inputId()\"\r\n [attr.aria-invalid]=\"ariaInvalid()\"\r\n [attr.aria-describedby]=\"describedBy()\"\r\n role=\"combobox\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-controls]=\"listboxId()\"\r\n [attr.aria-activedescendant]=\"activeOptionId()\"\r\n type=\"text\"\r\n [disabled]=\"disabled()\"\r\n [readonly]=\"readonly()\"\r\n [placeholder]=\"multiple() && value()?.length > 0 ? '' : placeholder()\"\r\n [ngModel]=\"searchQuery()\"\r\n (ngModelChange)=\"onSearchChange($event)\"\r\n (focus)=\"onInputFocus()\"\r\n (keydown)=\"onKeyDown($event)\"\r\n class=\"flex-1 bg-transparent border-0 outline-none placeholder:text-body-subtle font-normal text-heading\"\r\n [ngClass]=\"fieldTextClass() + ' ' + (hasChips() ? 'min-w-0 focus:min-w-16' : 'min-w-20')\"\r\n />\r\n\r\n <!--\r\n MỘT ô điều khiển, đổi vai theo trạng thái — nút xoá, chevron hoặc\r\n spinner, không xếp cạnh nhau (bản cũ hiện cùng lúc 3–4 phần tử). Ô rộng\r\n đúng bằng icon (`controlClass`): chữ ↔ icon 8px như Figma.\r\n -->\r\n <!--\r\n Chọn nhiều: cụm nút ra khỏi luồng (absolute, như ol-select) và khung\r\n chừa pe-9. Để trong luồng thì cụm nút + ô gõ không vừa phần còn lại của\r\n dòng chip cuối, cùng rớt xuống một dòng mới chỉ chứa khoảng trống — đo\r\n 2026-09-29: dư 31–36px ở mọi kiểu và cỡ.\r\n -->\r\n <div [class]=\"controlClass()\">\r\n @if (loading()) {\r\n <ol-spinner size=\"xs\" color=\"brand\" />\r\n } @else if (\r\n (searchQuery() || (multiple() && value()?.length > 0) || (!multiple() && value())) &&\r\n !disabled() &&\r\n !readonly()\r\n ) {\r\n <!--\r\n Nút bọc `size-4` ĐÚNG BẰNG icon, không `size-6`.\r\n\r\n `size-6` (24px) bọc SVG 16px thì dư 4px mỗi bên, đẩy icon vào trong:\r\n đo 2026-09-17 thấy ô đang hiện nút xoá có lề phải **19px** trong khi\r\n ô hiện chevron trần chỉ 15px — cùng một component mà hai trạng thái\r\n lệch nhau 4px, đúng chỗ anh chỉ trong ảnh.\r\n\r\n Vùng bấm vẫn đủ lớn nhờ `p-1` ở NGOÀI khung đo: `size-4` + đệm\r\n không cộng vào bề rộng chiếm chỗ vì nút nằm trong ô `w-7` cố định.\r\n -->\r\n <button\r\n type=\"button\"\r\n (click)=\"clearAll($event)\"\r\n aria-label=\"Xoá toàn bộ lựa chọn\"\r\n class=\"inline-flex size-4 shrink-0 items-center justify-center rounded-full text-body-subtle transition-colors hover:text-heading\"\r\n >\r\n <ol-icon name=\"close\" size=\"sm\" />\r\n </button>\r\n } @else {\r\n <ol-icon\r\n name=\"chevron_down\"\r\n size=\"sm\"\r\n class=\"transition-transform\"\r\n [class.rotate-180]=\"isOpen()\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n </ol-field-frame>\r\n\r\n <!--\r\n Dropdown Suggestions Menu.\r\n\r\n Token đọc từ Figma `ol-autocomplete-menu` (#1149:53147, Type=Default):\r\n bo góc 12px · đệm 8px · nền `bg-neutral-primary-medium` · viền\r\n `border-default-medium`.\r\n\r\n Hai token nền/viền TRÙNG NHAU ở chế độ sáng nên đổi hay không nhìn cũng\r\n thế — nhưng CHẾ ĐỘ TỐI khác hẳn: `bg-primary` là gray-950 (gần đen, panel\r\n chìm vào nền) còn `bg-primary-medium` là gray-800 (panel nổi lên).\r\n `ol-select` cùng lệch này, sửa chung một lượt.\r\n -->\r\n @if (isOpen()) {\r\n <div\r\n #panel\r\n [id]=\"listboxId()\"\r\n role=\"listbox\"\r\n [attr.aria-label]=\"label() || 'Gợi ý'\"\r\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\"\r\n [class.w-full]=\"matchTriggerWidth()\"\r\n [class]=\"menuClass()\"\r\n >\r\n @if (filteredOptions().length > 0) {\r\n @for (item of filteredOptions(); track getItemValue(item); let idx = $index) {\r\n <!-- Tiêu đề nhóm chèn giữa vòng lặp phẳng, xem `groupNameAt`. -->\r\n @if (groupNameAt(idx); as groupName) {\r\n <div\r\n role=\"presentation\"\r\n class=\"px-3 pt-2 pb-1 text-xs font-semibold uppercase tracking-wide text-body-subtle\"\r\n >\r\n {{ groupName }}\r\n </div>\r\n }\r\n <div\r\n [id]=\"optionId(idx)\"\r\n role=\"option\"\r\n [attr.aria-selected]=\"isSelected(item)\"\r\n [attr.aria-disabled]=\"item?.disabled ? 'true' : null\"\r\n (click)=\"selectOption(item)\"\r\n class=\"flex items-center justify-between px-3 py-2 text-sm rounded-lg cursor-pointer transition-colors\"\r\n [class.bg-brand-softer]=\"isSelected(item) || activeIndex() === idx\"\r\n [class.hover:bg-neutral-tertiary-medium]=\"!isSelected(item) && activeIndex() !== idx\"\r\n [class.opacity-50]=\"item?.disabled\"\r\n [class.cursor-not-allowed]=\"item?.disabled\"\r\n >\r\n @if (optionTemplate) {\r\n <!-- Custom Option Template -->\r\n <ng-container\r\n *ngTemplateOutlet=\"\r\n optionTemplate;\r\n context: { $implicit: item, selected: isSelected(item) }\r\n \"\r\n />\r\n } @else {\r\n <!-- Default Option Display -->\r\n <div class=\"flex items-center gap-2.5 overflow-hidden\">\r\n @if (item?.avatar) {\r\n <img\r\n [src]=\"item.avatar\"\r\n alt=\"Ảnh đại diện\"\r\n class=\"w-7 h-7 rounded-full object-cover shrink-0\"\r\n />\r\n }\r\n <div class=\"truncate\">\r\n <span class=\"font-medium text-heading block truncate\">{{\r\n getItemLabel(item)\r\n }}</span>\r\n @if (item?.subLabel) {\r\n <span class=\"text-xs text-body-subtle block truncate\">{{ item.subLabel }}</span>\r\n }\r\n </div>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-2 shrink-0 ms-2\">\r\n @if (item?.badge) {\r\n <span\r\n class=\"px-1.5 py-0.5 text-2xs font-medium rounded bg-neutral-tertiary-medium text-body\"\r\n >\r\n {{ item.badge }}\r\n </span>\r\n }\r\n @if (isSelected(item)) {\r\n <ol-icon name=\"check\" size=\"sm\" />\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n } @else if (waitingForTerm()) {\r\n <!--\r\n Chế độ LỌC TỪ XA, chưa gõ đủ `minTermLength`. Phải là thông báo RIÊNG\r\n chứ không dùng `emptyText`: \"Không tìm thấy kết quả\" là sai sự thật —\r\n chưa tìm gì cả. Cùng lối với `typeToSearchText` của ng-select.\r\n -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">\r\n <p>{{ typeToSearchText() }}</p>\r\n </div>\r\n } @else {\r\n <!-- Empty State & Allow Create Option -->\r\n <div class=\"p-3 text-center text-sm text-body-subtle\">\r\n <p>{{ emptyText() }}</p>\r\n @if (allowCreate() && searchQuery().trim()) {\r\n <button\r\n type=\"button\"\r\n (click)=\"onCreateNew()\"\r\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\"\r\n >\r\n <ol-icon name=\"plus\" size=\"xs\" />\r\n <span>Thêm mới: \"{{ searchQuery().trim() }}\"</span>\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Helper Text & Error Text -->\r\n @if (state() === 'error' && errorText()) {\r\n <p class=\"mt-1.5 text-xs text-fg-danger\">{{ errorText() }}</p>\r\n } @else if (helperText()) {\r\n <p class=\"mt-1.5 text-xs text-body\">{{ helperText() }}</p>\r\n }\r\n</div>\r\n"
|
|
23022
23252
|
}]
|
|
23023
23253
|
}],
|
|
23024
23254
|
ctorParameters: () => [{ type: i0.ElementRef }],
|
|
@@ -23556,7 +23786,10 @@ const ART = {
|
|
|
23556
23786
|
};
|
|
23557
23787
|
let nextId$1 = 0;
|
|
23558
23788
|
var OlLogo = class OlLogo {
|
|
23559
|
-
|
|
23789
|
+
constructor() {
|
|
23790
|
+
this.sanitizer = inject(DomSanitizer);
|
|
23791
|
+
}
|
|
23792
|
+
sanitizer;
|
|
23560
23793
|
uid = `ol-logo-${nextId$1++}`;
|
|
23561
23794
|
variant = input("full", ...ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []);
|
|
23562
23795
|
tone = input("auto", ...ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []);
|
|
@@ -23647,6 +23880,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
23647
23880
|
template: "<!--\r\n `auto` vẽ cả hai bản: bản màu ẩn trong `.dark`, bản trắng chỉ hiện trong\r\n `.dark`. Hai bản có id gradient khác nhau nên bản bị ẩn không kéo bản đang\r\n hiện mất màu.\r\n-->\r\n@if (colorSvg(); as svg) {\r\n <span [class]=\"colorClass()\" data-tone=\"color\" [innerHTML]=\"svg\"></span>\r\n}\r\n@if (whiteSvg(); as svg) {\r\n <span [class]=\"whiteClass()\" data-tone=\"white\" [innerHTML]=\"svg\"></span>\r\n}\r\n"
|
|
23648
23881
|
}]
|
|
23649
23882
|
}],
|
|
23883
|
+
ctorParameters: () => [],
|
|
23650
23884
|
propDecorators: {
|
|
23651
23885
|
variant: [{
|
|
23652
23886
|
type: i0.Input,
|
|
@@ -23807,13 +24041,15 @@ var OlNavbar = class OlNavbar {
|
|
|
23807
24041
|
onSearch = output();
|
|
23808
24042
|
onToggleMobileMenu = output();
|
|
23809
24043
|
searchQuery = "";
|
|
23810
|
-
elementRef
|
|
24044
|
+
elementRef;
|
|
23811
24045
|
mobilePanel = viewChild("mobilePanel", ...ngDevMode ? [{ debugName: "mobilePanel" }] : /* istanbul ignore next */ []);
|
|
23812
24046
|
navWrap = viewChild("navWrap", ...ngDevMode ? [{ debugName: "navWrap" }] : /* istanbul ignore next */ []);
|
|
23813
|
-
destroyRef
|
|
24047
|
+
destroyRef;
|
|
23814
24048
|
hasMobileLinks = signal(false, ...ngDevMode ? [{ debugName: "hasMobileLinks" }] : /* istanbul ignore next */ []);
|
|
23815
24049
|
hasPanelContent = signal(false, ...ngDevMode ? [{ debugName: "hasPanelContent" }] : /* istanbul ignore next */ []);
|
|
23816
24050
|
constructor() {
|
|
24051
|
+
this.elementRef = inject(ElementRef);
|
|
24052
|
+
this.destroyRef = inject(DestroyRef);
|
|
23817
24053
|
afterNextRender(() => {
|
|
23818
24054
|
this.detectMobileSlots();
|
|
23819
24055
|
this.observeStackHeight();
|
|
@@ -24445,9 +24681,11 @@ var OlNavDropdown = class OlNavDropdown {
|
|
|
24445
24681
|
4: "md:w-[56rem]"
|
|
24446
24682
|
}[cols]} min-w-64`;
|
|
24447
24683
|
}, ...ngDevMode ? [{ debugName: "panelClasses" }] : /* istanbul ignore next */ []);
|
|
24448
|
-
elementRef
|
|
24449
|
-
overlayContainer
|
|
24684
|
+
elementRef;
|
|
24685
|
+
overlayContainer;
|
|
24450
24686
|
constructor() {
|
|
24687
|
+
this.elementRef = inject(ElementRef);
|
|
24688
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
24451
24689
|
effect((onCleanup) => {
|
|
24452
24690
|
if (!this.isOpen()) return;
|
|
24453
24691
|
onCleanup(this.overlayContainer.register(() => this.close()));
|
|
@@ -24522,7 +24760,7 @@ var OlNavDropdown = class OlNavDropdown {
|
|
|
24522
24760
|
},
|
|
24523
24761
|
host: { listeners: { "document:click": "handleClickOutside($event)" } },
|
|
24524
24762
|
ngImport: i0,
|
|
24525
|
-
template: "<div class=\"relative inline-block text-left\">\r\n <!-- Trigger -->\r\n <div (click)=\"toggle()\">\r\n <ng-content select=\"[slot=trigger], [trigger]\" />\r\n @if (label() || icon()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"triggerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-current]=\"active() ? 'page' : null\"\r\n [attr.title]=\"icon() ? label() : null\"\r\n aria-haspopup=\"true\"\r\n >\r\n @if (icon()) {\r\n <ol-icon\r\n [name]=\"icon()!\"\r\n size=\"sm\"\r\n class=\"shrink-0\"\r\n [class.ol-nav-icon]=\"!!label()\"\r\n ></ol-icon>\r\n }\r\n <span class=\"whitespace-nowrap\" [class.ol-nav-label]=\"!!icon()\">{{ label() }}</span>\r\n <ol-icon\r\n name=\"
|
|
24763
|
+
template: "<div class=\"relative inline-block text-left\">\r\n <!-- Trigger -->\r\n <div (click)=\"toggle()\">\r\n <ng-content select=\"[slot=trigger], [trigger]\" />\r\n @if (label() || icon()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"triggerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-current]=\"active() ? 'page' : null\"\r\n [attr.title]=\"icon() ? label() : null\"\r\n aria-haspopup=\"true\"\r\n >\r\n @if (icon()) {\r\n <ol-icon\r\n [name]=\"icon()!\"\r\n size=\"sm\"\r\n class=\"shrink-0\"\r\n [class.ol-nav-icon]=\"!!label()\"\r\n ></ol-icon>\r\n }\r\n <span class=\"whitespace-nowrap\" [class.ol-nav-label]=\"!!icon()\">{{ label() }}</span>\r\n <ol-icon\r\n name=\"chevron_down\"\r\n size=\"xs\"\r\n class=\"transition-transform duration-200 shrink-0\"\r\n [class.rotate-180]=\"isOpen()\"\r\n ></ol-icon>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!--\r\n Panel. Class gom trong `panelClasses()` vì `columns` đổi cả cách xếp, bề\r\n ngang lẫn cách neo — ba thứ đó phải quyết cùng nhau, tách thành nhiều\r\n `[class.x]` rời thì mỗi tổ hợp là một luật CSS chọi nhau.\r\n -->\r\n <div [class]=\"panelClasses()\" [class.hidden]=\"!isOpen()\" role=\"menu\">\r\n <ng-content />\r\n </div>\r\n</div>\r\n",
|
|
24526
24764
|
dependencies: [{
|
|
24527
24765
|
kind: "ngmodule",
|
|
24528
24766
|
type: CommonModule
|
|
@@ -24553,7 +24791,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
24553
24791
|
standalone: true,
|
|
24554
24792
|
imports: [CommonModule, OlIcon],
|
|
24555
24793
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
24556
|
-
template: "<div class=\"relative inline-block text-left\">\r\n <!-- Trigger -->\r\n <div (click)=\"toggle()\">\r\n <ng-content select=\"[slot=trigger], [trigger]\" />\r\n @if (label() || icon()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"triggerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-current]=\"active() ? 'page' : null\"\r\n [attr.title]=\"icon() ? label() : null\"\r\n aria-haspopup=\"true\"\r\n >\r\n @if (icon()) {\r\n <ol-icon\r\n [name]=\"icon()!\"\r\n size=\"sm\"\r\n class=\"shrink-0\"\r\n [class.ol-nav-icon]=\"!!label()\"\r\n ></ol-icon>\r\n }\r\n <span class=\"whitespace-nowrap\" [class.ol-nav-label]=\"!!icon()\">{{ label() }}</span>\r\n <ol-icon\r\n name=\"
|
|
24794
|
+
template: "<div class=\"relative inline-block text-left\">\r\n <!-- Trigger -->\r\n <div (click)=\"toggle()\">\r\n <ng-content select=\"[slot=trigger], [trigger]\" />\r\n @if (label() || icon()) {\r\n <button\r\n type=\"button\"\r\n [class]=\"triggerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n [attr.aria-current]=\"active() ? 'page' : null\"\r\n [attr.title]=\"icon() ? label() : null\"\r\n aria-haspopup=\"true\"\r\n >\r\n @if (icon()) {\r\n <ol-icon\r\n [name]=\"icon()!\"\r\n size=\"sm\"\r\n class=\"shrink-0\"\r\n [class.ol-nav-icon]=\"!!label()\"\r\n ></ol-icon>\r\n }\r\n <span class=\"whitespace-nowrap\" [class.ol-nav-label]=\"!!icon()\">{{ label() }}</span>\r\n <ol-icon\r\n name=\"chevron_down\"\r\n size=\"xs\"\r\n class=\"transition-transform duration-200 shrink-0\"\r\n [class.rotate-180]=\"isOpen()\"\r\n ></ol-icon>\r\n </button>\r\n }\r\n </div>\r\n\r\n <!--\r\n Panel. Class gom trong `panelClasses()` vì `columns` đổi cả cách xếp, bề\r\n ngang lẫn cách neo — ba thứ đó phải quyết cùng nhau, tách thành nhiều\r\n `[class.x]` rời thì mỗi tổ hợp là một luật CSS chọi nhau.\r\n -->\r\n <div [class]=\"panelClasses()\" [class.hidden]=\"!isOpen()\" role=\"menu\">\r\n <ng-content />\r\n </div>\r\n</div>\r\n"
|
|
24557
24795
|
}]
|
|
24558
24796
|
}],
|
|
24559
24797
|
ctorParameters: () => [],
|
|
@@ -24613,9 +24851,9 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
24613
24851
|
}
|
|
24614
24852
|
});
|
|
24615
24853
|
var OlMenubar = class OlMenubar {
|
|
24616
|
-
elementRef
|
|
24617
|
-
destroyRef
|
|
24618
|
-
overlayContainer
|
|
24854
|
+
elementRef;
|
|
24855
|
+
destroyRef;
|
|
24856
|
+
overlayContainer;
|
|
24619
24857
|
menus = input([], ...ngDevMode ? [{ debugName: "menus" }] : /* istanbul ignore next */ []);
|
|
24620
24858
|
ariaLabel = input("Thanh menu ứng dụng", ...ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []);
|
|
24621
24859
|
openId = signal("", ...ngDevMode ? [{ debugName: "openId" }] : /* istanbul ignore next */ []);
|
|
@@ -24623,6 +24861,9 @@ var OlMenubar = class OlMenubar {
|
|
|
24623
24861
|
itemSelect = output();
|
|
24624
24862
|
releaseEscape = null;
|
|
24625
24863
|
constructor() {
|
|
24864
|
+
this.elementRef = inject(ElementRef);
|
|
24865
|
+
this.destroyRef = inject(DestroyRef);
|
|
24866
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
24626
24867
|
this.destroyRef.onDestroy(() => this.closeAll());
|
|
24627
24868
|
}
|
|
24628
24869
|
toggle(menu) {
|
|
@@ -25017,8 +25258,15 @@ var OlPagination = class OlPagination {
|
|
|
25017
25258
|
pageChange: "pageChange"
|
|
25018
25259
|
},
|
|
25019
25260
|
host: { classAttribute: "block w-full" },
|
|
25020
|
-
|
|
25021
|
-
template: "<div class=\"flex items-center gap-4 flex-wrap max-w-full\">\r\n <!-- 1. Type = Default (Figma #1149:81851) -->\r\n @if (layoutType() === 'default') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <!-- Prev Button -->\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt-arrow-left\" size=\"xs\"></ol-icon>\r\n </button>\r\n\r\n <!-- Pages Array -->\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Next Button -->\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 2. Type = Arrows (Figma #1149:81864) -->\r\n @if (layoutType() === 'arrows') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt-arrow-left\" size=\"xs\"></ol-icon>\r\n </button>\r\n\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 text-sm font-medium text-body bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n >\r\n {{ effectivePageIndex() }} trên {{ calculatedTotalPages() }}\r\n </span>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 3. Type = Dropdown & buttons (Figma #1149:81868) -->\r\n @if (layoutType() === 'dropdown-buttons') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <!-- Dropdown button -->\r\n <div class=\"relative inline-flex items-center\">\r\n <!--\r\n `[ngModel]` chứ KHÔNG phải `[value]`: `[value]` trên <select> đặt\r\n thuộc tính DOM NGAY, lúc các <option> của @for chưa tồn tại, nên trình\r\n duyệt hoàn nguyên về option đầu. Đo 2026-09-17: trang hiện tại là 3 mà\r\n ô chọn vẫn hiện \"01\". Build xanh, không cảnh báo nào.\r\n -->\r\n <select\r\n [ngModel]=\"effectivePageIndex()\"\r\n (ngModelChange)=\"goToPage(+$event)\"\r\n aria-label=\"Chọn trang\"\r\n class=\"appearance-none h-9 px-3.5 py-0 bg-neutral-tertiary border border-default rounded-md text-sm font-medium text-body pr-8 shadow-xs cursor-pointer focus:outline-hidden focus:ring-1 focus:ring-brand-subtle whitespace-nowrap\"\r\n >\r\n @for (p of pageList(); track p) {\r\n <option [value]=\"p\">{{ p < 10 ? '0' + p : p }}</option>\r\n }\r\n </select>\r\n <ol-icon\r\n name=\"alt-arrow-down\"\r\n size=\"xs\"\r\n class=\"pointer-events-none absolute right-2.5 text-body\"\r\n />\r\n </div>\r\n\r\n <!-- Helper info -->\r\n <span\r\n class=\"inline-flex items-center h-9 px-3.5 bg-neutral-tertiary/60 rounded-md text-sm font-medium text-fg-disabled select-none whitespace-nowrap\"\r\n >\r\n của {{ calculatedTotalPages() }} trang\r\n </span>\r\n\r\n <!-- Buttons -->\r\n <div class=\"inline-flex -space-x-px rounded-md shadow-xs isolate shrink-0\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt-arrow-left\" size=\"xs\"></ol-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 4. Type = Text buttons (Figma #1149:81874) -->\r\n @if (layoutType() === 'text-buttons') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none max-w-full overflow-x-auto\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <!-- Trang trước -->\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-l-md text-sm font-medium border border-default disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft whitespace-nowrap shrink-0\"\r\n >\r\n Trang trước\r\n </button>\r\n\r\n <!-- Pages -->\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Trang sau -->\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-r-md text-sm font-medium border border-default disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft whitespace-nowrap shrink-0\"\r\n >\r\n Trang sau\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 5. Type = Two buttons (Figma #1149:81887) -->\r\n @if (layoutType() === 'two-buttons') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"inline-flex items-center gap-1.5 px-3.5 py-2 bg-neutral-primary rounded-md border border-default text-sm font-medium text-body shadow-2xs hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n <ol-icon name=\"arrow-left\" size=\"xs\" />\r\n <span>Trang trước</span>\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"inline-flex items-center gap-1.5 px-3.5 py-2 bg-neutral-primary rounded-md border border-default text-sm font-medium text-body shadow-2xs hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n <span>Trang sau</span>\r\n <ol-icon name=\"arrow-right\" size=\"xs\" />\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- 6. Type = Buttons & helper text (Figma #1149:80890) -->\r\n @if (layoutType() === 'buttons-helper') {\r\n <div class=\"flex flex-col items-center gap-3 select-none\">\r\n @if (showText()) {\r\n <p class=\"text-sm font-medium text-body text-center whitespace-nowrap\">\r\n Hiển thị từ <strong class=\"text-heading font-semibold\">{{ startRecord() }}</strong> đến\r\n <strong class=\"text-heading font-semibold\">{{ endRecord() }}</strong> trong\r\n <strong class=\"text-heading font-semibold\">{{ total() }}</strong> dữ liệu\r\n </p>\r\n }\r\n <div class=\"inline-flex -space-x-px rounded-md shadow-xs isolate shrink-0\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 rounded-l-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n Trước\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 rounded-r-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n Sau\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 8. Type = With input (Figma #1149:81910) -->\r\n @if (layoutType() === 'with-input') {\r\n <div class=\"inline-flex items-center gap-4 select-none flex-wrap\">\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate max-w-full overflow-x-auto\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-l-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap shrink-0\"\r\n >\r\n Trang trước\r\n </button>\r\n\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-r-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap shrink-0\"\r\n >\r\n Trang sau\r\n </button>\r\n </nav>\r\n\r\n <!-- Input Jump -->\r\n <div class=\"flex items-center gap-2 text-sm text-body whitespace-nowrap shrink-0\">\r\n <span>Đi tới</span>\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [max]=\"calculatedTotalPages()\"\r\n [ngModel]=\"jumpPageValue()\"\r\n (ngModelChange)=\"jumpPageValue.set($event)\"\r\n (keydown.enter)=\"handleJump()\"\r\n placeholder=\"199\"\r\n class=\"w-16 px-2.5 py-1.5 text-center text-sm bg-neutral-tertiary border border-default rounded-md focus:outline-hidden focus:ring-1 focus:ring-brand-subtle text-heading shadow-xs\"\r\n />\r\n <span>trang</span>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 9. Type = Simple with input (Figma #1149:81928) -->\r\n @if (layoutType() === 'simple-input') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <span class=\"text-sm font-medium text-body whitespace-nowrap\">Đi tới trang</span>\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [max]=\"calculatedTotalPages()\"\r\n [ngModel]=\"jumpPageValue()\"\r\n (ngModelChange)=\"jumpPageValue.set($event)\"\r\n (keydown.enter)=\"handleJump()\"\r\n placeholder=\"20\"\r\n class=\"w-16 px-2.5 py-1.5 text-center text-sm bg-neutral-tertiary border border-default rounded-md focus:outline-hidden focus:ring-1 focus:ring-brand-subtle text-heading shadow-xs\"\r\n />\r\n <button\r\n type=\"button\"\r\n (click)=\"handleJump()\"\r\n class=\"px-4 py-1.5 bg-brand hover:bg-brand-strong text-white text-sm font-medium rounded-md shadow-xs cursor-pointer transition-colors whitespace-nowrap\"\r\n >\r\n Đi\r\n </button>\r\n </div>\r\n }\r\n @if (hasPageSizeDropdown()) {\r\n <!--\r\n Ô chọn cỡ trang — CỜ RIÊNG `withDropdown`, gắn được vào mọi type (Figma\r\n `With dropdown` #1149:81895 là nơi nó xuất hiện đầu tiên). Viết MỘT chỗ ở\r\n đây thay vì chép vào từng type.\r\n -->\r\n <div class=\"relative inline-flex items-center shrink-0\">\r\n <!-- `[ngModel]` — xem lý do ở ô chọn trang của Type `Dropdown & buttons`. -->\r\n <select\r\n [ngModel]=\"pageSize()\"\r\n (ngModelChange)=\"onPageSizeChange(+$event)\"\r\n aria-label=\"Số dòng mỗi trang\"\r\n class=\"appearance-none h-9 px-3.5 py-0 bg-neutral-tertiary border border-default rounded-md text-sm font-medium text-body pr-8 shadow-xs cursor-pointer focus:outline-hidden focus:ring-1 focus:ring-brand-subtle whitespace-nowrap\"\r\n >\r\n @for (opt of pageSizeOptions(); track opt) {\r\n <option [value]=\"opt\">{{ opt }} mỗi trang</option>\r\n }\r\n </select>\r\n <ol-icon\r\n name=\"alt-arrow-down\"\r\n size=\"xs\"\r\n class=\"pointer-events-none absolute right-2.5 text-body\"\r\n />\r\n </div>\r\n }\r\n</div>\r\n",
|
|
25261
|
+
providers: [provideOlIcons({
|
|
25262
|
+
"alt-arrow-left": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M15 5L9 12L15 19\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>",
|
|
25263
|
+
"alt-arrow-right": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M9 5L15 12L9 19\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>",
|
|
25264
|
+
"arrow-left": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M20 12H4M10 18L4 12L10 6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>",
|
|
25265
|
+
"arrow-right": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M4 12H20M14 18L20 12L14 6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>",
|
|
25266
|
+
"alt-arrow-down": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M19 9L12 15L5 9\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>"
|
|
25267
|
+
})],
|
|
25268
|
+
ngImport: i0,
|
|
25269
|
+
template: "<div class=\"flex items-center gap-4 flex-wrap max-w-full\">\r\n <!-- 1. Type = Default (Figma #1149:81851) -->\r\n @if (layoutType() === 'default') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <!-- Prev Button -->\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt_arrow_left\" size=\"xs\"></ol-icon>\r\n </button>\r\n\r\n <!-- Pages Array -->\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Next Button -->\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt_arrow_right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 2. Type = Arrows (Figma #1149:81864) -->\r\n @if (layoutType() === 'arrows') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt_arrow_left\" size=\"xs\"></ol-icon>\r\n </button>\r\n\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 text-sm font-medium text-body bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n >\r\n {{ effectivePageIndex() }} trên {{ calculatedTotalPages() }}\r\n </span>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt_arrow_right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 3. Type = Dropdown & buttons (Figma #1149:81868) -->\r\n @if (layoutType() === 'dropdown-buttons') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <!-- Dropdown button -->\r\n <div class=\"relative inline-flex items-center\">\r\n <!--\r\n `[ngModel]` chứ KHÔNG phải `[value]`: `[value]` trên <select> đặt\r\n thuộc tính DOM NGAY, lúc các <option> của @for chưa tồn tại, nên trình\r\n duyệt hoàn nguyên về option đầu. Đo 2026-09-17: trang hiện tại là 3 mà\r\n ô chọn vẫn hiện \"01\". Build xanh, không cảnh báo nào.\r\n -->\r\n <select\r\n [ngModel]=\"effectivePageIndex()\"\r\n (ngModelChange)=\"goToPage(+$event)\"\r\n aria-label=\"Chọn trang\"\r\n class=\"appearance-none h-9 px-3.5 py-0 bg-neutral-tertiary border border-default rounded-md text-sm font-medium text-body pr-8 shadow-xs cursor-pointer focus:outline-hidden focus:ring-1 focus:ring-brand-subtle whitespace-nowrap\"\r\n >\r\n @for (p of pageList(); track p) {\r\n <option [value]=\"p\">{{ p < 10 ? '0' + p : p }}</option>\r\n }\r\n </select>\r\n <ol-icon\r\n name=\"alt_arrow_down\"\r\n size=\"xs\"\r\n class=\"pointer-events-none absolute right-2.5 text-body\"\r\n />\r\n </div>\r\n\r\n <!-- Helper info -->\r\n <span\r\n class=\"inline-flex items-center h-9 px-3.5 bg-neutral-tertiary/60 rounded-md text-sm font-medium text-fg-disabled select-none whitespace-nowrap\"\r\n >\r\n của {{ calculatedTotalPages() }} trang\r\n </span>\r\n\r\n <!-- Buttons -->\r\n <div class=\"inline-flex -space-x-px rounded-md shadow-xs isolate shrink-0\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt_arrow_left\" size=\"xs\"></ol-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt_arrow_right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 4. Type = Text buttons (Figma #1149:81874) -->\r\n @if (layoutType() === 'text-buttons') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none max-w-full overflow-x-auto\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <!-- Trang trước -->\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-l-md text-sm font-medium border border-default disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft whitespace-nowrap shrink-0\"\r\n >\r\n Trang trước\r\n </button>\r\n\r\n <!-- Pages -->\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Trang sau -->\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-r-md text-sm font-medium border border-default disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft whitespace-nowrap shrink-0\"\r\n >\r\n Trang sau\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 5. Type = Two buttons (Figma #1149:81887) -->\r\n @if (layoutType() === 'two-buttons') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"inline-flex items-center gap-1.5 px-3.5 py-2 bg-neutral-primary rounded-md border border-default text-sm font-medium text-body shadow-2xs hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n <ol-icon name=\"arrow_left\" size=\"xs\" />\r\n <span>Trang trước</span>\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"inline-flex items-center gap-1.5 px-3.5 py-2 bg-neutral-primary rounded-md border border-default text-sm font-medium text-body shadow-2xs hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n <span>Trang sau</span>\r\n <ol-icon name=\"arrow_right\" size=\"xs\" />\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- 6. Type = Buttons & helper text (Figma #1149:80890) -->\r\n @if (layoutType() === 'buttons-helper') {\r\n <div class=\"flex flex-col items-center gap-3 select-none\">\r\n @if (showText()) {\r\n <p class=\"text-sm font-medium text-body text-center whitespace-nowrap\">\r\n Hiển thị từ <strong class=\"text-heading font-semibold\">{{ startRecord() }}</strong> đến\r\n <strong class=\"text-heading font-semibold\">{{ endRecord() }}</strong> trong\r\n <strong class=\"text-heading font-semibold\">{{ total() }}</strong> dữ liệu\r\n </p>\r\n }\r\n <div class=\"inline-flex -space-x-px rounded-md shadow-xs isolate shrink-0\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 rounded-l-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n Trước\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 rounded-r-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n Sau\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 8. Type = With input (Figma #1149:81910) -->\r\n @if (layoutType() === 'with-input') {\r\n <div class=\"inline-flex items-center gap-4 select-none flex-wrap\">\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate max-w-full overflow-x-auto\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-l-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap shrink-0\"\r\n >\r\n Trang trước\r\n </button>\r\n\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-r-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap shrink-0\"\r\n >\r\n Trang sau\r\n </button>\r\n </nav>\r\n\r\n <!-- Input Jump -->\r\n <div class=\"flex items-center gap-2 text-sm text-body whitespace-nowrap shrink-0\">\r\n <span>Đi tới</span>\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [max]=\"calculatedTotalPages()\"\r\n [ngModel]=\"jumpPageValue()\"\r\n (ngModelChange)=\"jumpPageValue.set($event)\"\r\n (keydown.enter)=\"handleJump()\"\r\n placeholder=\"199\"\r\n class=\"w-16 px-2.5 py-1.5 text-center text-sm bg-neutral-tertiary border border-default rounded-md focus:outline-hidden focus:ring-1 focus:ring-brand-subtle text-heading shadow-xs\"\r\n />\r\n <span>trang</span>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 9. Type = Simple with input (Figma #1149:81928) -->\r\n @if (layoutType() === 'simple-input') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <span class=\"text-sm font-medium text-body whitespace-nowrap\">Đi tới trang</span>\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [max]=\"calculatedTotalPages()\"\r\n [ngModel]=\"jumpPageValue()\"\r\n (ngModelChange)=\"jumpPageValue.set($event)\"\r\n (keydown.enter)=\"handleJump()\"\r\n placeholder=\"20\"\r\n class=\"w-16 px-2.5 py-1.5 text-center text-sm bg-neutral-tertiary border border-default rounded-md focus:outline-hidden focus:ring-1 focus:ring-brand-subtle text-heading shadow-xs\"\r\n />\r\n <button\r\n type=\"button\"\r\n (click)=\"handleJump()\"\r\n class=\"px-4 py-1.5 bg-brand hover:bg-brand-strong text-white text-sm font-medium rounded-md shadow-xs cursor-pointer transition-colors whitespace-nowrap\"\r\n >\r\n Đi\r\n </button>\r\n </div>\r\n }\r\n @if (hasPageSizeDropdown()) {\r\n <!--\r\n Ô chọn cỡ trang — CỜ RIÊNG `withDropdown`, gắn được vào mọi type (Figma\r\n `With dropdown` #1149:81895 là nơi nó xuất hiện đầu tiên). Viết MỘT chỗ ở\r\n đây thay vì chép vào từng type.\r\n -->\r\n <div class=\"relative inline-flex items-center shrink-0\">\r\n <!-- `[ngModel]` — xem lý do ở ô chọn trang của Type `Dropdown & buttons`. -->\r\n <select\r\n [ngModel]=\"pageSize()\"\r\n (ngModelChange)=\"onPageSizeChange(+$event)\"\r\n aria-label=\"Số dòng mỗi trang\"\r\n class=\"appearance-none h-9 px-3.5 py-0 bg-neutral-tertiary border border-default rounded-md text-sm font-medium text-body pr-8 shadow-xs cursor-pointer focus:outline-hidden focus:ring-1 focus:ring-brand-subtle whitespace-nowrap\"\r\n >\r\n @for (opt of pageSizeOptions(); track opt) {\r\n <option [value]=\"opt\">{{ opt }} mỗi trang</option>\r\n }\r\n </select>\r\n <ol-icon\r\n name=\"alt_arrow_down\"\r\n size=\"xs\"\r\n class=\"pointer-events-none absolute right-2.5 text-body\"\r\n />\r\n </div>\r\n }\r\n</div>\r\n",
|
|
25022
25270
|
dependencies: [
|
|
25023
25271
|
{
|
|
25024
25272
|
kind: "ngmodule",
|
|
@@ -25119,7 +25367,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
25119
25367
|
],
|
|
25120
25368
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
25121
25369
|
host: { class: "block w-full" },
|
|
25122
|
-
template: "<div class=\"flex items-center gap-4 flex-wrap max-w-full\">\r\n <!-- 1. Type = Default (Figma #1149:81851) -->\r\n @if (layoutType() === 'default') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <!-- Prev Button -->\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt-arrow-left\" size=\"xs\"></ol-icon>\r\n </button>\r\n\r\n <!-- Pages Array -->\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Next Button -->\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 2. Type = Arrows (Figma #1149:81864) -->\r\n @if (layoutType() === 'arrows') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt-arrow-left\" size=\"xs\"></ol-icon>\r\n </button>\r\n\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 text-sm font-medium text-body bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n >\r\n {{ effectivePageIndex() }} trên {{ calculatedTotalPages() }}\r\n </span>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 3. Type = Dropdown & buttons (Figma #1149:81868) -->\r\n @if (layoutType() === 'dropdown-buttons') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <!-- Dropdown button -->\r\n <div class=\"relative inline-flex items-center\">\r\n <!--\r\n `[ngModel]` chứ KHÔNG phải `[value]`: `[value]` trên <select> đặt\r\n thuộc tính DOM NGAY, lúc các <option> của @for chưa tồn tại, nên trình\r\n duyệt hoàn nguyên về option đầu. Đo 2026-09-17: trang hiện tại là 3 mà\r\n ô chọn vẫn hiện \"01\". Build xanh, không cảnh báo nào.\r\n -->\r\n <select\r\n [ngModel]=\"effectivePageIndex()\"\r\n (ngModelChange)=\"goToPage(+$event)\"\r\n aria-label=\"Chọn trang\"\r\n class=\"appearance-none h-9 px-3.5 py-0 bg-neutral-tertiary border border-default rounded-md text-sm font-medium text-body pr-8 shadow-xs cursor-pointer focus:outline-hidden focus:ring-1 focus:ring-brand-subtle whitespace-nowrap\"\r\n >\r\n @for (p of pageList(); track p) {\r\n <option [value]=\"p\">{{ p < 10 ? '0' + p : p }}</option>\r\n }\r\n </select>\r\n <ol-icon\r\n name=\"alt-arrow-down\"\r\n size=\"xs\"\r\n class=\"pointer-events-none absolute right-2.5 text-body\"\r\n />\r\n </div>\r\n\r\n <!-- Helper info -->\r\n <span\r\n class=\"inline-flex items-center h-9 px-3.5 bg-neutral-tertiary/60 rounded-md text-sm font-medium text-fg-disabled select-none whitespace-nowrap\"\r\n >\r\n của {{ calculatedTotalPages() }} trang\r\n </span>\r\n\r\n <!-- Buttons -->\r\n <div class=\"inline-flex -space-x-px rounded-md shadow-xs isolate shrink-0\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt-arrow-left\" size=\"xs\"></ol-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt-arrow-right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 4. Type = Text buttons (Figma #1149:81874) -->\r\n @if (layoutType() === 'text-buttons') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none max-w-full overflow-x-auto\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <!-- Trang trước -->\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-l-md text-sm font-medium border border-default disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft whitespace-nowrap shrink-0\"\r\n >\r\n Trang trước\r\n </button>\r\n\r\n <!-- Pages -->\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Trang sau -->\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-r-md text-sm font-medium border border-default disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft whitespace-nowrap shrink-0\"\r\n >\r\n Trang sau\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 5. Type = Two buttons (Figma #1149:81887) -->\r\n @if (layoutType() === 'two-buttons') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"inline-flex items-center gap-1.5 px-3.5 py-2 bg-neutral-primary rounded-md border border-default text-sm font-medium text-body shadow-2xs hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n <ol-icon name=\"arrow-left\" size=\"xs\" />\r\n <span>Trang trước</span>\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"inline-flex items-center gap-1.5 px-3.5 py-2 bg-neutral-primary rounded-md border border-default text-sm font-medium text-body shadow-2xs hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n <span>Trang sau</span>\r\n <ol-icon name=\"arrow-right\" size=\"xs\" />\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- 6. Type = Buttons & helper text (Figma #1149:80890) -->\r\n @if (layoutType() === 'buttons-helper') {\r\n <div class=\"flex flex-col items-center gap-3 select-none\">\r\n @if (showText()) {\r\n <p class=\"text-sm font-medium text-body text-center whitespace-nowrap\">\r\n Hiển thị từ <strong class=\"text-heading font-semibold\">{{ startRecord() }}</strong> đến\r\n <strong class=\"text-heading font-semibold\">{{ endRecord() }}</strong> trong\r\n <strong class=\"text-heading font-semibold\">{{ total() }}</strong> dữ liệu\r\n </p>\r\n }\r\n <div class=\"inline-flex -space-x-px rounded-md shadow-xs isolate shrink-0\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 rounded-l-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n Trước\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 rounded-r-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n Sau\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 8. Type = With input (Figma #1149:81910) -->\r\n @if (layoutType() === 'with-input') {\r\n <div class=\"inline-flex items-center gap-4 select-none flex-wrap\">\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate max-w-full overflow-x-auto\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-l-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap shrink-0\"\r\n >\r\n Trang trước\r\n </button>\r\n\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-r-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap shrink-0\"\r\n >\r\n Trang sau\r\n </button>\r\n </nav>\r\n\r\n <!-- Input Jump -->\r\n <div class=\"flex items-center gap-2 text-sm text-body whitespace-nowrap shrink-0\">\r\n <span>Đi tới</span>\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [max]=\"calculatedTotalPages()\"\r\n [ngModel]=\"jumpPageValue()\"\r\n (ngModelChange)=\"jumpPageValue.set($event)\"\r\n (keydown.enter)=\"handleJump()\"\r\n placeholder=\"199\"\r\n class=\"w-16 px-2.5 py-1.5 text-center text-sm bg-neutral-tertiary border border-default rounded-md focus:outline-hidden focus:ring-1 focus:ring-brand-subtle text-heading shadow-xs\"\r\n />\r\n <span>trang</span>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 9. Type = Simple with input (Figma #1149:81928) -->\r\n @if (layoutType() === 'simple-input') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <span class=\"text-sm font-medium text-body whitespace-nowrap\">Đi tới trang</span>\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [max]=\"calculatedTotalPages()\"\r\n [ngModel]=\"jumpPageValue()\"\r\n (ngModelChange)=\"jumpPageValue.set($event)\"\r\n (keydown.enter)=\"handleJump()\"\r\n placeholder=\"20\"\r\n class=\"w-16 px-2.5 py-1.5 text-center text-sm bg-neutral-tertiary border border-default rounded-md focus:outline-hidden focus:ring-1 focus:ring-brand-subtle text-heading shadow-xs\"\r\n />\r\n <button\r\n type=\"button\"\r\n (click)=\"handleJump()\"\r\n class=\"px-4 py-1.5 bg-brand hover:bg-brand-strong text-white text-sm font-medium rounded-md shadow-xs cursor-pointer transition-colors whitespace-nowrap\"\r\n >\r\n Đi\r\n </button>\r\n </div>\r\n }\r\n @if (hasPageSizeDropdown()) {\r\n <!--\r\n Ô chọn cỡ trang — CỜ RIÊNG `withDropdown`, gắn được vào mọi type (Figma\r\n `With dropdown` #1149:81895 là nơi nó xuất hiện đầu tiên). Viết MỘT chỗ ở\r\n đây thay vì chép vào từng type.\r\n -->\r\n <div class=\"relative inline-flex items-center shrink-0\">\r\n <!-- `[ngModel]` — xem lý do ở ô chọn trang của Type `Dropdown & buttons`. -->\r\n <select\r\n [ngModel]=\"pageSize()\"\r\n (ngModelChange)=\"onPageSizeChange(+$event)\"\r\n aria-label=\"Số dòng mỗi trang\"\r\n class=\"appearance-none h-9 px-3.5 py-0 bg-neutral-tertiary border border-default rounded-md text-sm font-medium text-body pr-8 shadow-xs cursor-pointer focus:outline-hidden focus:ring-1 focus:ring-brand-subtle whitespace-nowrap\"\r\n >\r\n @for (opt of pageSizeOptions(); track opt) {\r\n <option [value]=\"opt\">{{ opt }} mỗi trang</option>\r\n }\r\n </select>\r\n <ol-icon\r\n name=\"alt-arrow-down\"\r\n size=\"xs\"\r\n class=\"pointer-events-none absolute right-2.5 text-body\"\r\n />\r\n </div>\r\n }\r\n</div>\r\n"
|
|
25370
|
+
providers: [provideOlIcons({
|
|
25371
|
+
"alt-arrow-left": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M15 5L9 12L15 19\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>",
|
|
25372
|
+
"alt-arrow-right": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M9 5L15 12L9 19\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>",
|
|
25373
|
+
"arrow-left": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M20 12H4M10 18L4 12L10 6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>",
|
|
25374
|
+
"arrow-right": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M4 12H20M14 18L20 12L14 6\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>",
|
|
25375
|
+
"alt-arrow-down": "<svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"> <path d=\"M19 9L12 15L5 9\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> </svg>"
|
|
25376
|
+
})],
|
|
25377
|
+
template: "<div class=\"flex items-center gap-4 flex-wrap max-w-full\">\r\n <!-- 1. Type = Default (Figma #1149:81851) -->\r\n @if (layoutType() === 'default') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <!-- Prev Button -->\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt_arrow_left\" size=\"xs\"></ol-icon>\r\n </button>\r\n\r\n <!-- Pages Array -->\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Next Button -->\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt_arrow_right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 2. Type = Arrows (Figma #1149:81864) -->\r\n @if (layoutType() === 'arrows') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt_arrow_left\" size=\"xs\"></ol-icon>\r\n </button>\r\n\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 text-sm font-medium text-body bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n >\r\n {{ effectivePageIndex() }} trên {{ calculatedTotalPages() }}\r\n </span>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default transition-colors disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt_arrow_right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 3. Type = Dropdown & buttons (Figma #1149:81868) -->\r\n @if (layoutType() === 'dropdown-buttons') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <!-- Dropdown button -->\r\n <div class=\"relative inline-flex items-center\">\r\n <!--\r\n `[ngModel]` chứ KHÔNG phải `[value]`: `[value]` trên <select> đặt\r\n thuộc tính DOM NGAY, lúc các <option> của @for chưa tồn tại, nên trình\r\n duyệt hoàn nguyên về option đầu. Đo 2026-09-17: trang hiện tại là 3 mà\r\n ô chọn vẫn hiện \"01\". Build xanh, không cảnh báo nào.\r\n -->\r\n <select\r\n [ngModel]=\"effectivePageIndex()\"\r\n (ngModelChange)=\"goToPage(+$event)\"\r\n aria-label=\"Chọn trang\"\r\n class=\"appearance-none h-9 px-3.5 py-0 bg-neutral-tertiary border border-default rounded-md text-sm font-medium text-body pr-8 shadow-xs cursor-pointer focus:outline-hidden focus:ring-1 focus:ring-brand-subtle whitespace-nowrap\"\r\n >\r\n @for (p of pageList(); track p) {\r\n <option [value]=\"p\">{{ p < 10 ? '0' + p : p }}</option>\r\n }\r\n </select>\r\n <ol-icon\r\n name=\"alt_arrow_down\"\r\n size=\"xs\"\r\n class=\"pointer-events-none absolute right-2.5 text-body\"\r\n />\r\n </div>\r\n\r\n <!-- Helper info -->\r\n <span\r\n class=\"inline-flex items-center h-9 px-3.5 bg-neutral-tertiary/60 rounded-md text-sm font-medium text-fg-disabled select-none whitespace-nowrap\"\r\n >\r\n của {{ calculatedTotalPages() }} trang\r\n </span>\r\n\r\n <!-- Buttons -->\r\n <div class=\"inline-flex -space-x-px rounded-md shadow-xs isolate shrink-0\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-l-md border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer shrink-0\"\r\n aria-label=\"Trang trước\"\r\n >\r\n <ol-icon name=\"alt_arrow_left\" size=\"xs\"></ol-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center w-9 h-9 rounded-r-md border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer shrink-0\"\r\n aria-label=\"Trang sau\"\r\n >\r\n <ol-icon name=\"alt_arrow_right\" size=\"xs\"></ol-icon>\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 4. Type = Text buttons (Figma #1149:81874) -->\r\n @if (layoutType() === 'text-buttons') {\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate select-none max-w-full overflow-x-auto\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <!-- Trang trước -->\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-l-md text-sm font-medium border border-default disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft whitespace-nowrap shrink-0\"\r\n >\r\n Trang trước\r\n </button>\r\n\r\n <!-- Pages -->\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <!-- Trang sau -->\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-r-md text-sm font-medium border border-default disabled:opacity-40 disabled:cursor-not-allowed disabled:bg-disabled cursor-pointer bg-neutral-primary text-body hover:bg-neutral-secondary-soft whitespace-nowrap shrink-0\"\r\n >\r\n Trang sau\r\n </button>\r\n </nav>\r\n }\r\n\r\n <!-- 5. Type = Two buttons (Figma #1149:81887) -->\r\n @if (layoutType() === 'two-buttons') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"inline-flex items-center gap-1.5 px-3.5 py-2 bg-neutral-primary rounded-md border border-default text-sm font-medium text-body shadow-2xs hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n <ol-icon name=\"arrow_left\" size=\"xs\" />\r\n <span>Trang trước</span>\r\n </button>\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"inline-flex items-center gap-1.5 px-3.5 py-2 bg-neutral-primary rounded-md border border-default text-sm font-medium text-body shadow-2xs hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n <span>Trang sau</span>\r\n <ol-icon name=\"arrow_right\" size=\"xs\" />\r\n </button>\r\n </div>\r\n }\r\n\r\n <!-- 6. Type = Buttons & helper text (Figma #1149:80890) -->\r\n @if (layoutType() === 'buttons-helper') {\r\n <div class=\"flex flex-col items-center gap-3 select-none\">\r\n @if (showText()) {\r\n <p class=\"text-sm font-medium text-body text-center whitespace-nowrap\">\r\n Hiển thị từ <strong class=\"text-heading font-semibold\">{{ startRecord() }}</strong> đến\r\n <strong class=\"text-heading font-semibold\">{{ endRecord() }}</strong> trong\r\n <strong class=\"text-heading font-semibold\">{{ total() }}</strong> dữ liệu\r\n </p>\r\n }\r\n <div class=\"inline-flex -space-x-px rounded-md shadow-xs isolate shrink-0\">\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 rounded-l-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n Trước\r\n </button>\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-4 h-9 rounded-r-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap\"\r\n >\r\n Sau\r\n </button>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 8. Type = With input (Figma #1149:81910) -->\r\n @if (layoutType() === 'with-input') {\r\n <div class=\"inline-flex items-center gap-4 select-none flex-wrap\">\r\n <nav\r\n class=\"inline-flex -space-x-px rounded-md shadow-xs isolate max-w-full overflow-x-auto\"\r\n aria-label=\"Phân trang\"\r\n >\r\n <button\r\n type=\"button\"\r\n (click)=\"prev()\"\r\n [disabled]=\"effectivePageIndex() <= 1\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-l-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap shrink-0\"\r\n >\r\n Trang trước\r\n </button>\r\n\r\n @for (page of visiblePages(); track trackPage($index, page)) {\r\n @if (page === 'ellipsis') {\r\n <span\r\n class=\"relative inline-flex items-center justify-center px-3 h-9 text-sm text-fg-disabled bg-neutral-primary border border-default select-none whitespace-nowrap shrink-0\"\r\n aria-hidden=\"true\"\r\n >\r\n ...\r\n </span>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n (click)=\"goToPage(page)\"\r\n [attr.aria-current]=\"page === effectivePageIndex() ? 'page' : null\"\r\n [attr.aria-label]=\"'Trang ' + page\"\r\n [class]=\"\r\n 'relative inline-flex items-center justify-center min-w-9 h-9 px-3 text-sm transition-colors cursor-pointer whitespace-nowrap shrink-0 ' +\r\n (page === effectivePageIndex()\r\n ? 'z-10 font-semibold border border-brand bg-brand text-white'\r\n : 'font-medium border border-default bg-neutral-primary-soft text-body hover:bg-neutral-secondary-soft')\r\n \"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n }\r\n\r\n <button\r\n type=\"button\"\r\n (click)=\"next()\"\r\n [disabled]=\"effectivePageIndex() >= calculatedTotalPages()\"\r\n class=\"relative inline-flex items-center justify-center px-3.5 h-9 rounded-r-md text-sm font-medium border border-default bg-neutral-primary text-body hover:bg-neutral-secondary-soft disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer whitespace-nowrap shrink-0\"\r\n >\r\n Trang sau\r\n </button>\r\n </nav>\r\n\r\n <!-- Input Jump -->\r\n <div class=\"flex items-center gap-2 text-sm text-body whitespace-nowrap shrink-0\">\r\n <span>Đi tới</span>\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [max]=\"calculatedTotalPages()\"\r\n [ngModel]=\"jumpPageValue()\"\r\n (ngModelChange)=\"jumpPageValue.set($event)\"\r\n (keydown.enter)=\"handleJump()\"\r\n placeholder=\"199\"\r\n class=\"w-16 px-2.5 py-1.5 text-center text-sm bg-neutral-tertiary border border-default rounded-md focus:outline-hidden focus:ring-1 focus:ring-brand-subtle text-heading shadow-xs\"\r\n />\r\n <span>trang</span>\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- 9. Type = Simple with input (Figma #1149:81928) -->\r\n @if (layoutType() === 'simple-input') {\r\n <div class=\"inline-flex items-center gap-3 select-none flex-wrap\">\r\n <span class=\"text-sm font-medium text-body whitespace-nowrap\">Đi tới trang</span>\r\n <input\r\n type=\"number\"\r\n min=\"1\"\r\n [max]=\"calculatedTotalPages()\"\r\n [ngModel]=\"jumpPageValue()\"\r\n (ngModelChange)=\"jumpPageValue.set($event)\"\r\n (keydown.enter)=\"handleJump()\"\r\n placeholder=\"20\"\r\n class=\"w-16 px-2.5 py-1.5 text-center text-sm bg-neutral-tertiary border border-default rounded-md focus:outline-hidden focus:ring-1 focus:ring-brand-subtle text-heading shadow-xs\"\r\n />\r\n <button\r\n type=\"button\"\r\n (click)=\"handleJump()\"\r\n class=\"px-4 py-1.5 bg-brand hover:bg-brand-strong text-white text-sm font-medium rounded-md shadow-xs cursor-pointer transition-colors whitespace-nowrap\"\r\n >\r\n Đi\r\n </button>\r\n </div>\r\n }\r\n @if (hasPageSizeDropdown()) {\r\n <!--\r\n Ô chọn cỡ trang — CỜ RIÊNG `withDropdown`, gắn được vào mọi type (Figma\r\n `With dropdown` #1149:81895 là nơi nó xuất hiện đầu tiên). Viết MỘT chỗ ở\r\n đây thay vì chép vào từng type.\r\n -->\r\n <div class=\"relative inline-flex items-center shrink-0\">\r\n <!-- `[ngModel]` — xem lý do ở ô chọn trang của Type `Dropdown & buttons`. -->\r\n <select\r\n [ngModel]=\"pageSize()\"\r\n (ngModelChange)=\"onPageSizeChange(+$event)\"\r\n aria-label=\"Số dòng mỗi trang\"\r\n class=\"appearance-none h-9 px-3.5 py-0 bg-neutral-tertiary border border-default rounded-md text-sm font-medium text-body pr-8 shadow-xs cursor-pointer focus:outline-hidden focus:ring-1 focus:ring-brand-subtle whitespace-nowrap\"\r\n >\r\n @for (opt of pageSizeOptions(); track opt) {\r\n <option [value]=\"opt\">{{ opt }} mỗi trang</option>\r\n }\r\n </select>\r\n <ol-icon\r\n name=\"alt_arrow_down\"\r\n size=\"xs\"\r\n class=\"pointer-events-none absolute right-2.5 text-body\"\r\n />\r\n </div>\r\n }\r\n</div>\r\n"
|
|
25123
25378
|
}]
|
|
25124
25379
|
}],
|
|
25125
25380
|
propDecorators: {
|
|
@@ -25251,9 +25506,9 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
25251
25506
|
}
|
|
25252
25507
|
});
|
|
25253
25508
|
var OlPopover = class OlPopover {
|
|
25254
|
-
positioning
|
|
25255
|
-
destroyRef
|
|
25256
|
-
host
|
|
25509
|
+
positioning;
|
|
25510
|
+
destroyRef;
|
|
25511
|
+
host;
|
|
25257
25512
|
hoverTimer = null;
|
|
25258
25513
|
triggerRef = viewChild("triggerEl", ...ngDevMode ? [{ debugName: "triggerRef" }] : /* istanbul ignore next */ []);
|
|
25259
25514
|
panelRef = viewChild("panel", ...ngDevMode ? [{ debugName: "panelRef" }] : /* istanbul ignore next */ []);
|
|
@@ -25270,7 +25525,7 @@ var OlPopover = class OlPopover {
|
|
|
25270
25525
|
closeDelay = input(0, ...ngDevMode ? [{ debugName: "closeDelay" }] : /* istanbul ignore next */ []);
|
|
25271
25526
|
opened = output();
|
|
25272
25527
|
closed = output();
|
|
25273
|
-
overlayContainer
|
|
25528
|
+
overlayContainer;
|
|
25274
25529
|
cleanupAutoUpdate = null;
|
|
25275
25530
|
detachPanel = null;
|
|
25276
25531
|
static FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
@@ -25283,6 +25538,10 @@ var OlPopover = class OlPopover {
|
|
|
25283
25538
|
return index === -1 ? null : all[index + 1] ?? null;
|
|
25284
25539
|
}
|
|
25285
25540
|
constructor() {
|
|
25541
|
+
this.positioning = inject(OlPositioningService);
|
|
25542
|
+
this.destroyRef = inject(DestroyRef);
|
|
25543
|
+
this.host = inject(ElementRef);
|
|
25544
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
25286
25545
|
effect(() => {
|
|
25287
25546
|
const open = this.isOpen();
|
|
25288
25547
|
const reference = this.triggerRef()?.nativeElement;
|
|
@@ -26366,8 +26625,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
26366
26625
|
});
|
|
26367
26626
|
let uniqueRatingId = 0;
|
|
26368
26627
|
var OlResizable = class OlResizable {
|
|
26369
|
-
elementRef
|
|
26370
|
-
destroyRef
|
|
26628
|
+
elementRef;
|
|
26629
|
+
destroyRef;
|
|
26371
26630
|
direction = input("horizontal", ...ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []);
|
|
26372
26631
|
ratio = model(50, ...ngDevMode ? [{ debugName: "ratio" }] : /* istanbul ignore next */ []);
|
|
26373
26632
|
minRatio = input(15, ...ngDevMode ? [{ debugName: "minRatio" }] : /* istanbul ignore next */ []);
|
|
@@ -26379,6 +26638,8 @@ var OlResizable = class OlResizable {
|
|
|
26379
26638
|
dragging = signal(false, ...ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []);
|
|
26380
26639
|
releaseDrag = null;
|
|
26381
26640
|
constructor() {
|
|
26641
|
+
this.elementRef = inject(ElementRef);
|
|
26642
|
+
this.destroyRef = inject(DestroyRef);
|
|
26382
26643
|
this.destroyRef.onDestroy(() => this.stopDrag());
|
|
26383
26644
|
}
|
|
26384
26645
|
clamp(value) {
|
|
@@ -26847,12 +27108,15 @@ const APP_NAV_DEFAULT_USER_ACTIONS = [
|
|
|
26847
27108
|
];
|
|
26848
27109
|
let nextId = 0;
|
|
26849
27110
|
var OlAppNav = class OlAppNav {
|
|
26850
|
-
overlayContainer
|
|
26851
|
-
router
|
|
26852
|
-
currentUrl
|
|
27111
|
+
overlayContainer;
|
|
27112
|
+
router;
|
|
27113
|
+
currentUrl;
|
|
26853
27114
|
expandedIds = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "expandedIds" }] : /* istanbul ignore next */ []);
|
|
26854
27115
|
releaseEscape = null;
|
|
26855
27116
|
constructor() {
|
|
27117
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
27118
|
+
this.router = inject(Router);
|
|
27119
|
+
this.currentUrl = toSignal(this.router.events.pipe(filter((e) => e instanceof NavigationEnd), map((e) => e.urlAfterRedirects)), { initialValue: this.router.url });
|
|
26856
27120
|
effect(() => {
|
|
26857
27121
|
this.currentUrl();
|
|
26858
27122
|
const ids = this.activeAncestorIds(this.allGroups().map((g) => g.items).reduce((a, b) => a.concat(b), []));
|
|
@@ -27232,6 +27496,10 @@ var OlAppNav = class OlAppNav {
|
|
|
27232
27496
|
mobileMenuOpen: "mobileMenuOpenChange"
|
|
27233
27497
|
},
|
|
27234
27498
|
host: { classAttribute: "block" },
|
|
27499
|
+
providers: [provideOlIcons({
|
|
27500
|
+
"solar:hamburger-menu-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\" d=\"M3 6h18M3 12h18M3 18h18\"/></svg>",
|
|
27501
|
+
"alt-arrow-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m9 5l7 7l-7 7\"/></svg>"
|
|
27502
|
+
})],
|
|
27235
27503
|
queries: [
|
|
27236
27504
|
{
|
|
27237
27505
|
propertyName: "itemDef",
|
|
@@ -27277,7 +27545,7 @@ var OlAppNav = class OlAppNav {
|
|
|
27277
27545
|
}
|
|
27278
27546
|
],
|
|
27279
27547
|
ngImport: i0,
|
|
27280
|
-
template: "<!--\r\n Menu điều hướng Ôn Luyện — Figma `ol-app-nav` (#5303:30109).\r\n\r\n Màu lấy từ TOKEN, không ghim hex:\r\n\r\n --bg-app-nav-from điểm đầu gradient (--color-navy-from)\r\n --bg-app-nav-to điểm cuối (--color-navy-to)\r\n --bg-app-nav-hover nền mục khi rê chuột (--color-brand-600)\r\n\r\n Bản đầu tôi ghim thẳng `#445a9a` và `#0f2977` vào đây với lý do \"bản sắc của\r\n một sản phẩm, không phải bậc màu dùng chung\". Lý do đó sai chỗ: primitives là\r\n nơi MỌI màu gốc nằm, kể cả màu chỉ một component dùng. Ghim trong template\r\n thì `check:tokens` không thấy (guard chỉ quét class Tailwind bậc số, không\r\n quét `style=`), và đổi màu phải sửa từng nơi dùng.\r\n\r\n Giá trị dự phòng giữ lại để component vẫn vẽ đúng khi nơi dùng quên nhập\r\n `tokens.css` — đã xảy ra tới bản 1.0.14, gói publish không có file CSS nào.\r\n\r\n Bố cục `justify-between` chia ba vùng như Figma: phần trên co theo nội dung,\r\n phần dưới dính đáy, khoảng trống ở giữa.\r\n-->\r\n<nav\r\n [class]=\"\r\n 'relative h-full flex-col justify-between rounded-2xl pt-5 transition-[width] duration-200 ' +\r\n railDisplay()\r\n \"\r\n style=\"\r\n background-image: linear-gradient(\r\n 164.52deg,\r\n var(--bg-app-nav-from, #445a9a) 0%,\r\n var(--bg-app-nav-to, #0f2977) 100%\r\n );\r\n \"\r\n [style.width]=\"navWidth()\"\r\n [attr.aria-label]=\"'Menu điều hướng'\"\r\n>\r\n <!--\r\n THANH KÉO (rail) — thu gọn / mở lại bằng một cú bấm ở mép phải.\r\n\r\n Dựng theo đúng hành vi mẫu người dùng đưa:\r\n\r\n · hai vạch dọc 8×2px, bo hai đầu ngoài\r\n · hover thì xoay ±40° thành hình mũi tên, và đậm lên\r\n · tooltip trượt từ trái sang, chỉ hiện khi hover\r\n · vùng bấm rộng 28px dù vạch chỉ 2px — ngón tay và chuột cần chỗ\r\n\r\n Đặt TRONG `<nav>` với `absolute` chứ không `fixed` như mẫu: `fixed` neo\r\n theo màn hình nên menu đặt ở đâu nó cũng nhảy về mép trái. `absolute` bám\r\n đúng mép phải của chính menu, và `group/rail` cho hover đi xuyên xuống hai\r\n vạch con.\r\n\r\n `hidden md:flex`: dưới 768px menu thường chuyển sang dạng ngăn kéo, thanh\r\n kéo không còn nghĩa.\r\n -->\r\n @if (showRail()) {\r\n <button\r\n type=\"button\"\r\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\"\r\n [attr.aria-label]=\"railLabel() + ' menu'\"\r\n [attr.aria-expanded]=\"!collapsed()\"\r\n (click)=\"toggle()\"\r\n >\r\n <span class=\"flex flex-col items-center\">\r\n <span\r\n aria-hidden=\"true\"\r\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\"\r\n ></span>\r\n <span\r\n aria-hidden=\"true\"\r\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\"\r\n ></span>\r\n </span>\r\n\r\n <!--\r\n Tooltip tự dựng, không dùng `olTooltip`: directive đó treo panel ra\r\n `.ol-overlay-container` nên không chạy được hiệu ứng trượt gắn với\r\n `group-hover` của chính nút.\r\n -->\r\n <span\r\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\"\r\n >\r\n {{ railLabel() }}\r\n </span>\r\n </button>\r\n }\r\n <!-- ── Phần trên: logo + menu chính ───────────────────────────────── -->\r\n <!--\r\n Đệm ngang 15px khi thu gọn: `<ul>` là flex-col nên `<li>` kéo giãn mục\r\n con hết bề rộng, `size-10` một mình không thắng được. 70 − 15×2 = 40, đúng\r\n ô vuông bằng chiều cao mục (40px — bản gọn 2026-10-06, Figma 44px / 13px).\r\n\r\n `overflow-hidden` đặt ở VÙNG CON, không ở `<nav>`: `<nav>` phải cho tooltip\r\n của thanh kéo tràn ra ngoài mép, nhưng nội dung vẫn cần bị cắt theo bo góc\r\n 24px — nhãn dài ở bề rộng 70px sẽ thò ra khỏi khung bo.\r\n -->\r\n <div\r\n class=\"flex min-h-0 flex-1 flex-col gap-5 overflow-hidden rounded-t-2xl\"\r\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\r\n >\r\n <div class=\"flex items-center justify-center\">\r\n @if (logoDef(); as logo) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"logo.template\"\r\n [ngTemplateOutletContext]=\"{ $implicit: collapsed(), collapsed: collapsed() }\"\r\n />\r\n } @else if (logoSrc()) {\r\n <img\r\n [src]=\"collapsed() && logoMarkSrc() ? logoMarkSrc() : logoSrc()\"\r\n alt=\"Ôn Luyện\"\r\n [class]=\"collapsed() ? 'size-10 object-contain' : 'h-10 w-auto max-w-full object-contain'\"\r\n />\r\n } @else {\r\n <!--\r\n Logo của kit (Figma `Logo ol` #6603:1269). Thu gọn: bản VUÔNG 40px —\r\n bản dài rộng ~164px, ở khung 70px tràn hẳn ra ngoài. Mở: bản dài cao\r\n 32px (~164px, Figma app-nav đặt logo rộng 155px).\r\n -->\r\n @if (collapsed()) {\r\n <ol-logo variant=\"mark\" [tone]=\"logoTone()\" [height]=\"40\" />\r\n } @else {\r\n <ol-logo variant=\"full\" [tone]=\"logoTone()\" [height]=\"32\" />\r\n }\r\n }\r\n </div>\r\n\r\n @if (headerDef(); as header) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"header.template\"\r\n [ngTemplateOutletContext]=\"slotContext('rail')\"\r\n />\r\n }\r\n\r\n <!--\r\n Vùng mục CUỘN ĐƯỢC: menu nhiều nhóm (quản trị trường: 5 nhóm, menu con 3\r\n cấp) cao hơn màn hình. Logo và thẻ tài khoản đứng yên, chỉ danh sách cuộn.\r\n\r\n Thanh cuộn MẢNH, trắng mờ — KHÔNG ẩn hẳn: đo menu giáo viên (10 + 5 mục)\r\n ở màn cao 900px, 3 mục cuối nằm dưới mép mà không có dấu hiệu gì cho\r\n biết còn nữa. Thanh mặc định xám đặc thì chướng trên nền navy.\r\n -->\r\n <div\r\n class=\"-me-3 flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto pe-2 [scrollbar-color:rgb(255_255_255/0.3)_transparent] [scrollbar-width:thin]\"\r\n >\r\n @for (group of mainGroups(); track $index) {\r\n <div class=\"flex flex-col gap-1\">\r\n @if (group.label && !collapsed()) {\r\n <p class=\"px-2.5 pt-1 pb-0.5 text-xs font-bold uppercase tracking-wide text-white/70\">\r\n {{ group.label }}\r\n </p>\r\n }\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (footerDef(); as footer) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"footer.template\"\r\n [ngTemplateOutletContext]=\"slotContext('rail')\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- ── Phần dưới: menu phụ + thẻ tài khoản ────────────────────────── -->\r\n <div\r\n class=\"flex shrink-0 flex-col gap-1 overflow-hidden rounded-b-2xl pt-2 pb-4\"\r\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\r\n >\r\n @if (showDivider()) {\r\n <!-- `aria-hidden`: đường kẻ là dấu hiệu nhìn, trình đọc màn hình không cần -->\r\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: footerItems(), level: 1 }\"\r\n />\r\n\r\n <!--\r\n THẺ TÀI KHOẢN — bấm mở menu tài khoản (popup nổi `ol-dropdown`).\r\n\r\n Popup chứ không đổi danh sách tại chỗ: ở bề rộng 70px (thu gọn) không có\r\n chỗ cho ba dòng chữ; popup nổi ra ngoài menu thì đọc được ở cả hai trạng\r\n thái. Luôn mở SANG PHẢI, cạnh sidebar (`right-end`: mép dưới popup thẳng\r\n mép dưới thẻ, popup mọc lên trên vì thẻ nằm sát đáy) — người dùng yêu\r\n cầu 2026-10-06, bản trước mở lên trên đè lên chính menu khi mở rộng.\r\n\r\n `user.actions = []` thì không có popup — bấm thẻ chỉ phát `userClick`\r\n như các bản trước.\r\n\r\n `autoClose=\"outside\"`: bấm TRONG popup không tự đóng — ruột tuỳ biến có\r\n thể chứa điều khiển (bộ chọn Sáng/Tối) mà người dùng cần thấy kết quả.\r\n Mục hành động tự đóng qua `select()` / `close()` của ngữ cảnh.\r\n -->\r\n @if (config().user; as user) {\r\n <div class=\"mt-2 flex\" [class]=\"collapsed() ? 'justify-center' : ''\">\r\n @if (userActions().length > 0 || userMenuDef()) {\r\n <ol-dropdown\r\n class=\"min-w-0\"\r\n [class]=\"collapsed() ? '' : 'w-full'\"\r\n placement=\"right-end\"\r\n [offset]=\"userMenuOffset()\"\r\n menuClass=\"w-72\"\r\n autoClose=\"outside\"\r\n [(isOpen)]=\"userMenuOpen\"\r\n >\r\n <button\r\n trigger\r\n type=\"button\"\r\n [class]=\"userCardClasses()\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"userMenuOpen()\"\r\n [attr.aria-label]=\"collapsed() ? 'Tài khoản ' + user.name : null\"\r\n (click)=\"userClick.emit()\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: collapsed(),\r\n initials: userInitials(),\r\n variant: 'rail',\r\n }\"\r\n />\r\n </button>\r\n\r\n <!--\r\n Ruột popup chỉ dựng khi mở: `ol-dropdown` ẩn panel bằng class\r\n `hidden`, nên dựng sẵn thì tên + email nằm trong DOM cả khi menu\r\n thu gọn — trình đọc màn hình và tìm kiếm trang vẫn thấy chúng.\r\n -->\r\n @if (userMenuOpen()) {\r\n @if (userMenuDef(); as menu) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"menu.template\"\r\n [ngTemplateOutletContext]=\"userMenuPopupContext()\"\r\n />\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"userMenuBody\" />\r\n }\r\n }\r\n </ol-dropdown>\r\n } @else {\r\n <button type=\"button\" [class]=\"userCardClasses()\" (click)=\"userClick.emit()\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: collapsed(),\r\n initials: userInitials(),\r\n variant: 'rail',\r\n }\"\r\n />\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</nav>\r\n\r\n<!--\r\n ── MOBILE: THANH ĐÁY + KHAY MENU (`layout=\"auto\"` dưới `md`, hoặc `\"mobile\"`) ──\r\n\r\n Chuyển từ phần tự dựng của onluyenv4 vào thư viện (2026-10-06):\r\n\r\n · thanh đáy `mobilePrimaryCount` mục đầu nhóm chính + nút Menu ở ô cuối\r\n · khay trượt lên từ đáy (nút mở nằm ở đáy phải — đi theo ngón tay), chứa\r\n thẻ tài khoản, các mục KHÔNG lên thanh đáy, và menu tài khoản\r\n · `fixed` theo màn hình; `pb-[env(safe-area-inset-bottom)]` chừa thanh vuốt iPhone\r\n\r\n Khay dùng `@if` chứ không ẩn bằng class: ẩn bằng `hidden` thì nút bên trong\r\n vẫn tab tới được dù không nhìn thấy. Lớp phủ là `<button>` để đóng được\r\n bằng bàn phím.\r\n-->\r\n@if (hasMobile()) {\r\n <nav\r\n class=\"fixed inset-x-0 bottom-0 z-50 flex h-[calc(3.5rem+env(safe-area-inset-bottom))] items-stretch border-t border-default bg-neutral-primary pb-[env(safe-area-inset-bottom)]\"\r\n [class]=\"mobileDisplay()\"\r\n aria-label=\"Điều hướng chính\"\r\n >\r\n @for (item of primaryItems(); track item.id) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"barItemClasses(item)\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n [attr.aria-label]=\"item.label\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? barBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'bar')\"\r\n />\r\n </a>\r\n }\r\n\r\n <button\r\n type=\"button\"\r\n class=\"flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 py-2 transition-colors\"\r\n [class]=\"mobileMenuOpen() ? 'text-fg-brand' : 'text-body-subtle hover:text-heading'\"\r\n [attr.aria-expanded]=\"mobileMenuOpen()\"\r\n [attr.aria-controls]=\"sheetId\"\r\n (click)=\"toggleMobileMenu()\"\r\n >\r\n <ol-icon name=\"solar:hamburger-menu-linear\" size=\"base\" />\r\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\r\n {{ mobileMenuLabel() }}\r\n </span>\r\n </button>\r\n </nav>\r\n\r\n <!--\r\n Khay và lớp phủ DỪNG trên thanh đáy (cao 56px + vùng vuốt iPhone): thanh\r\n đáy luôn thấy được và bấm được khi khay mở — nút \"Menu\" bấm lần nữa thì\r\n đóng, các mục khác chuyển trang luôn (người dùng yêu cầu 2026-10-06). Thanh\r\n đáy z-50, trên lớp phủ z-40.\r\n -->\r\n @if (mobileMenuOpen()) {\r\n <button\r\n type=\"button\"\r\n class=\"fixed inset-x-0 top-0 bottom-[calc(3.5rem+env(safe-area-inset-bottom))] z-40 cursor-default bg-black/40 transition-opacity duration-200 starting:opacity-0\"\r\n animate.leave=\"opacity-0\"\r\n [class]=\"mobileDisplay()\"\r\n aria-label=\"Đóng menu\"\r\n (click)=\"mobileMenuOpen.set(false)\"\r\n ></button>\r\n\r\n <!--\r\n Khung như bản cũ: kéo hết bề ngang, dính mép trên thanh đáy, bo 2 góc\r\n trên (người dùng 2026-10-06 — không thẻ nổi cách mép). Bên trong `px-2`\r\n để mỗi dòng bo tròn riêng. Mở: trượt LÊN từ đáy (`starting:` =\r\n `@starting-style`). Đóng: trượt XUỐNG — `animate.leave` gắn\r\n `translate-y-full opacity-0`, Angular chờ transition 200ms xong mới gỡ\r\n khỏi DOM (`@if` gỡ ngay thì không kịp chạy).\r\n\r\n Hai phần: ĐẦU đứng yên (vạch kéo, thẻ tài khoản + Đăng xuất, và phần `top`\r\n của menu tài khoản — Trang cá nhân, Đổi mật khẩu, Giao diện…), THÂN cuộn\r\n (khe, nhóm mục, phần `bottom`). Menu dài thì chỉ thân cuộn (người dùng\r\n 2026-10-06).\r\n -->\r\n <div\r\n [id]=\"sheetId\"\r\n class=\"fixed inset-x-0 bottom-[calc(3.5rem+env(safe-area-inset-bottom))] z-40 flex max-h-[calc(100dvh-3.5rem-env(safe-area-inset-bottom)-4rem)] flex-col rounded-t-lg bg-neutral-primary px-2 pb-2 shadow-2xl transition-[opacity,translate] duration-200 ease-out starting:translate-y-full starting:opacity-0\"\r\n animate.leave=\"translate-y-full opacity-0\"\r\n [class]=\"mobileDisplay()\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n [attr.aria-label]=\"mobileMenuLabel()\"\r\n >\r\n <div class=\"shrink-0\">\r\n <div class=\"flex justify-center pt-2.5\">\r\n <span class=\"h-1 w-10 rounded-full bg-neutral-tertiary\" aria-hidden=\"true\"></span>\r\n </div>\r\n\r\n @if (config().user; as user) {\r\n <div class=\"flex items-center gap-3 px-2 pt-2 pb-2\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? sheetUserBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: false,\r\n initials: userInitials(),\r\n variant: 'sheet',\r\n }\"\r\n />\r\n <!--\r\n `split`: mục danger (Đăng xuất) thành nút CHỈ ICON viền đỏ bên phải thẻ\r\n tài khoản — một chạm, không phải cuộn xuống cuối khay (người dùng\r\n 2026-10-06). Tên đọc + tooltip (nhấn giữ) từ `label`.\r\n -->\r\n @if (mobileUserMenu() === 'split') {\r\n @for (action of cardActions(); track action.id) {\r\n <ol-button\r\n class=\"ms-auto shrink-0\"\r\n color=\"danger\"\r\n [outline]=\"true\"\r\n size=\"sm\"\r\n [iconOnly]=\"true\"\r\n [ariaLabel]=\"action.label\"\r\n (btnClick)=\"onUserAction(action)\"\r\n >\r\n <ol-icon leftIcon [name]=\"action.icon\" size=\"base\" />\r\n </ol-button>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!--\r\n `mobileUserMenu=\"split\"`: việc thường của tài khoản (Trang cá nhân, Đổi\r\n mật khẩu, cài đặt…) lên ĐẦU khay, ngay dưới thẻ tài khoản.\r\n -->\r\n @if (mobileUserMenu() === 'split') {\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetAccount\"\r\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetTopContext() }\"\r\n />\r\n }\r\n </div>\r\n\r\n <div class=\"-mx-2 min-h-0 flex-1 overflow-y-auto overscroll-contain px-2\">\r\n <!--\r\n `empty:hidden`: template khe có thể không vẽ gì (bộ chọn vai chỉ hiện\r\n với tài khoản hai vai). Không có nó, khay còn một dải trống 24px kẹp\r\n giữa hai đường kẻ. `:empty` bỏ qua nút chú thích Angular chèn vào.\r\n -->\r\n @if (headerDef(); as header) {\r\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"header.template\"\r\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\r\n />\r\n </div>\r\n }\r\n\r\n @for (group of sheetGroups(); track $index) {\r\n <div class=\"mt-1 border-t border-default pt-1\">\r\n @if (group.label) {\r\n <p class=\"px-2 pt-2 pb-1 text-xs font-bold uppercase tracking-wide text-body-subtle\">\r\n {{ group.label }}\r\n </p>\r\n }\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (footerDef(); as footer) {\r\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"footer.template\"\r\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\r\n />\r\n </div>\r\n }\r\n\r\n <!-- Menu tài khoản (cuối khay): trọn menu, hoặc chỉ phần `bottom` khi `mobileUserMenu=\"split\"` -->\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetAccount\"\r\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetContext() }\"\r\n />\r\n </div>\r\n </div>\r\n }\r\n}\r\n\r\n<!--\r\n Khối menu tài khoản trong khay — vẽ ở đầu (`part: 'top'`) và/hoặc cuối khay.\r\n Template tuỳ biến nếu có, không thì danh sách mục mặc định của phần đó.\r\n `empty:hidden`: phần không có mục nào thì không để lại dải kẻ.\r\n-->\r\n<ng-template #sheetAccount let-ctx>\r\n @if (ctx) {\r\n @if (userMenuDef(); as menu) {\r\n <!--\r\n `px-2`: ruột tuỳ biến (ol-dropdown-item…) không tự có lề ngang như dòng khay.\r\n `min-h-10` cho mục menu: ol-dropdown-item cao 36px, dòng khay 40px — ép\r\n bằng nhau để cả khay một nhịp.\r\n -->\r\n <div\r\n class=\"mt-1 border-t border-default pt-1 empty:hidden [&_[role=menuitem]]:min-h-10 [&_[role=menuitem]]:rounded-md\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"menu.template\" [ngTemplateOutletContext]=\"ctx\" />\r\n </div>\r\n } @else if (sheetActionsFor(ctx.part).length > 0) {\r\n <ul class=\"mt-1 border-t border-default pt-1\">\r\n @for (action of sheetActionsFor(ctx.part); track action.id) {\r\n @if (action.dividerBefore && !$first) {\r\n <li class=\"my-1 h-px bg-neutral-tertiary\" aria-hidden=\"true\"></li>\r\n }\r\n <li>\r\n @if (action.route) {\r\n <a\r\n [routerLink]=\"action.route\"\r\n [class]=\"sheetActionClasses(action)\"\r\n (click)=\"onUserAction(action)\"\r\n >\r\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\r\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"sheetActionClasses(action)\"\r\n (click)=\"onUserAction(action)\"\r\n >\r\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\r\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\r\n </button>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Cây mục (đệ quy) — cấp 1 · 2 · 3. Nhánh con chỉ vẽ khi mở và menu không\r\n thu gọn; thu gọn thì menu con hiện NỔI cạnh mục cha (xem `#navItem`).\r\n-->\r\n<ng-template #navTree let-items let-level=\"level\">\r\n <ul class=\"flex flex-col gap-1\" [class]=\"level === 1 ? '' : 'mt-1'\">\r\n @for (item of items; track item.id) {\r\n <li>\r\n <ng-container\r\n [ngTemplateOutlet]=\"navItem\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item, level: level }\"\r\n />\r\n @if (item.children?.length && isExpanded(item) && !collapsed() && level < 3) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\r\n />\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</ng-template>\r\n\r\n<!--\r\n Một mục menu. Ba loại hành vi, không gộp được:\r\n có con → `<button aria-expanded>` mở/đóng nhánh, không điều hướng\r\n có `route` → `<a routerLink>`; mục đang chọn tính bằng `isItemActive`\r\n (route · activeRoutes · activeId), không `routerLinkActive`\r\n còn lại → `<button>` chỉ phát `itemClick` (disabled lo trong class)\r\n-->\r\n<ng-template #navItem let-item let-level=\"level\">\r\n @if (item.children?.length && collapsed() && level === 1) {\r\n <!--\r\n THU GỌN + mục cha: rê chuột / bấm / Enter mở menu con NỔI bên phải\r\n (`ol-dropdown`). Không tooltip — đầu panel đã ghi tên mục.\r\n -->\r\n <div\r\n class=\"flex justify-center\"\r\n (mouseenter)=\"openFlyout(item.id)\"\r\n (mouseleave)=\"scheduleFlyoutClose()\"\r\n >\r\n <ol-dropdown\r\n placement=\"right\"\r\n menuClass=\"w-64\"\r\n [isOpen]=\"flyoutId() === item.id\"\r\n (isOpenChange)=\"onFlyoutOpenChange(item.id, $event)\"\r\n >\r\n <button\r\n trigger\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level)\"\r\n [attr.aria-label]=\"item.label\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"flyoutId() === item.id\"\r\n [disabled]=\"item.disabled ?? false\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n\r\n @if (flyoutId() === item.id) {\r\n <div (mouseenter)=\"cancelFlyoutClose()\" (mouseleave)=\"scheduleFlyoutClose()\">\r\n <p class=\"px-2 pt-1 pb-1.5 text-xs font-bold uppercase tracking-wide text-body-subtle\">\r\n {{ item.label }}\r\n </p>\r\n <ng-container\r\n [ngTemplateOutlet]=\"flyoutTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: 2 }\"\r\n />\r\n </div>\r\n }\r\n </ol-dropdown>\r\n </div>\r\n } @else if (item.children?.length) {\r\n <button\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [attr.aria-expanded]=\"isExpanded(item)\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n } @else if (item.route) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"itemClasses(item, level)\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Ruột menu nổi: con là liên kết (`ol-dropdown-item`); con có cháu thì thành\r\n nhãn nhóm, cháu thụt vào dưới nó. Mục đang chọn mang `selected`.\r\n-->\r\n<ng-template #flyoutTree let-items let-level=\"level\">\r\n @for (child of items; track child.id) {\r\n @if (child.children?.length) {\r\n <p class=\"px-2 pt-2 pb-1 text-sm font-semibold text-heading\" [class.ps-5]=\"level > 2\">\r\n {{ child.label }}\r\n </p>\r\n <div class=\"ps-3\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"flyoutTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: child.children, level: level + 1 }\"\r\n />\r\n </div>\r\n } @else {\r\n <ol-dropdown-item\r\n [label]=\"child.label\"\r\n [routerLink]=\"child.route ?? null\"\r\n [selected]=\"isItemActive(child)\"\r\n [disabled]=\"child.disabled ?? false\"\r\n (itemClick)=\"onFlyoutSelect(child)\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Ruột một mục ở menu dọc: icon 24px (chỉ cấp 1) + nhãn + nhãn phụ + huy hiệu\r\n + mũi tên (mục cha). Nhãn ẩn khi thu gọn.\r\n-->\r\n<ng-template #itemBody let-item let-level=\"level\" let-expanded=\"expanded\">\r\n @if (level === 1) {\r\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\r\n }\r\n\r\n @if (!collapsed() || level > 1) {\r\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\r\n\r\n @if (item.extraText) {\r\n <span\r\n class=\"shrink-0 rounded-sm border border-white px-1 text-2xs leading-tight text-white\"\r\n >{{ item.extraText }}</span\r\n >\r\n }\r\n @if (item.badge) {\r\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\r\n }\r\n @if (item.children?.length) {\r\n <ol-icon\r\n name=\"alt-arrow-right\"\r\n size=\"xs\"\r\n class=\"shrink-0 text-white transition-transform duration-200\"\r\n [class.rotate-90]=\"expanded\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!-- Ruột một mục ở thanh đáy mobile: icon + nhãn ngắn (đọc nhãn đầy đủ qua aria-label) -->\r\n<ng-template #barBody let-item>\r\n <ol-icon [name]=\"item.icon\" size=\"base\" />\r\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\r\n {{ item.shortLabel ?? item.label }}\r\n </span>\r\n</ng-template>\r\n\r\n<!-- Cây mục trong khay mobile — cùng luật mở/đóng với menu dọc -->\r\n<ng-template #sheetTree let-items let-level=\"level\">\r\n <ul>\r\n @for (item of items; track item.id) {\r\n <li>\r\n @if (item.route && !item.children?.length) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"sheetItemClasses(item, level)\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\r\n />\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"sheetItemClasses(item, level)\"\r\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item) : null\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\r\n />\r\n </button>\r\n }\r\n @if (item.children?.length && isExpanded(item) && level < 3) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\r\n />\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</ng-template>\r\n\r\n<!-- Ruột một dòng trong khay mobile: nền sáng nên chữ theo token, không trắng -->\r\n<ng-template #sheetBody let-item let-level=\"level\" let-expanded=\"expanded\">\r\n @if (level === 1) {\r\n <ol-icon [name]=\"item.icon\" size=\"sm\" class=\"shrink-0\" />\r\n }\r\n <span class=\"min-w-0 flex-1 truncate\">{{ item.label }}</span>\r\n @if (item.extraText) {\r\n <span class=\"shrink-0 rounded-sm border border-default px-1 text-2xs leading-tight\">{{\r\n item.extraText\r\n }}</span>\r\n }\r\n @if (item.badge) {\r\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\r\n }\r\n @if (item.children?.length) {\r\n <ol-icon\r\n name=\"alt-arrow-right\"\r\n size=\"xs\"\r\n class=\"shrink-0 transition-transform duration-200\"\r\n [class.rotate-90]=\"expanded\"\r\n />\r\n }\r\n</ng-template>\r\n\r\n<!-- Ruột thẻ tài khoản ở đáy menu dọc: avatar + tên/email (ẩn khi thu gọn) + mũi tên -->\r\n<ng-template #userCardBody let-user>\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n @if (!collapsed()) {\r\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\r\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"truncate text-xs text-white/80\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n <!--\r\n Mũi tên CHỈ SANG PHẢI — menu tài khoản mở sang phải cạnh sidebar\r\n (`right-end`); mũi tên xuống của bản trước chỉ sai hướng. Rê chuột / đang\r\n mở thì nhích 2px sang phải báo hướng mở.\r\n -->\r\n <ol-icon\r\n name=\"alt-arrow-right\"\r\n size=\"xs\"\r\n class=\"shrink-0 text-white/80 transition-transform duration-200 group-hover/user:translate-x-0.5\"\r\n [class.translate-x-0.5]=\"userMenuOpen()\"\r\n />\r\n }\r\n</ng-template>\r\n\r\n<!-- Khối tài khoản đầu khay mobile -->\r\n<ng-template #sheetUserBody let-user>\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n</ng-template>\r\n\r\n<!--\r\n Ruột popup tài khoản mặc định: đầu là avatar + tên + email để người dùng\r\n biết đang ở tài khoản nào, dưới là các mục `userActions()`.\r\n-->\r\n<ng-template #userMenuBody>\r\n @if (config().user; as user) {\r\n <div class=\"flex items-center gap-3 px-2 py-1.5\">\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n </div>\r\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\r\n @for (action of userActions(); track action.id) {\r\n @if (action.dividerBefore && !$first) {\r\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\r\n }\r\n <ol-dropdown-item\r\n [icon]=\"action.icon\"\r\n [label]=\"action.label\"\r\n [routerLink]=\"action.route ?? null\"\r\n [danger]=\"action.danger ?? false\"\r\n (itemClick)=\"onUserAction(action)\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n",
|
|
27548
|
+
template: "<!--\r\n Menu điều hướng Ôn Luyện — Figma `ol-app-nav` (#5303:30109).\r\n\r\n Màu lấy từ TOKEN, không ghim hex:\r\n\r\n --bg-app-nav-from điểm đầu gradient (--color-navy-from)\r\n --bg-app-nav-to điểm cuối (--color-navy-to)\r\n --bg-app-nav-hover nền mục khi rê chuột (--color-brand-600)\r\n\r\n Bản đầu tôi ghim thẳng `#445a9a` và `#0f2977` vào đây với lý do \"bản sắc của\r\n một sản phẩm, không phải bậc màu dùng chung\". Lý do đó sai chỗ: primitives là\r\n nơi MỌI màu gốc nằm, kể cả màu chỉ một component dùng. Ghim trong template\r\n thì `check:tokens` không thấy (guard chỉ quét class Tailwind bậc số, không\r\n quét `style=`), và đổi màu phải sửa từng nơi dùng.\r\n\r\n Giá trị dự phòng giữ lại để component vẫn vẽ đúng khi nơi dùng quên nhập\r\n `tokens.css` — đã xảy ra tới bản 1.0.14, gói publish không có file CSS nào.\r\n\r\n Bố cục `justify-between` chia ba vùng như Figma: phần trên co theo nội dung,\r\n phần dưới dính đáy, khoảng trống ở giữa.\r\n-->\r\n<nav\r\n [class]=\"\r\n 'relative h-full flex-col justify-between rounded-2xl pt-5 transition-[width] duration-200 ' +\r\n railDisplay()\r\n \"\r\n style=\"\r\n background-image: linear-gradient(\r\n 164.52deg,\r\n var(--bg-app-nav-from, #445a9a) 0%,\r\n var(--bg-app-nav-to, #0f2977) 100%\r\n );\r\n \"\r\n [style.width]=\"navWidth()\"\r\n [attr.aria-label]=\"'Menu điều hướng'\"\r\n>\r\n <!--\r\n THANH KÉO (rail) — thu gọn / mở lại bằng một cú bấm ở mép phải.\r\n\r\n Dựng theo đúng hành vi mẫu người dùng đưa:\r\n\r\n · hai vạch dọc 8×2px, bo hai đầu ngoài\r\n · hover thì xoay ±40° thành hình mũi tên, và đậm lên\r\n · tooltip trượt từ trái sang, chỉ hiện khi hover\r\n · vùng bấm rộng 28px dù vạch chỉ 2px — ngón tay và chuột cần chỗ\r\n\r\n Đặt TRONG `<nav>` với `absolute` chứ không `fixed` như mẫu: `fixed` neo\r\n theo màn hình nên menu đặt ở đâu nó cũng nhảy về mép trái. `absolute` bám\r\n đúng mép phải của chính menu, và `group/rail` cho hover đi xuyên xuống hai\r\n vạch con.\r\n\r\n `hidden md:flex`: dưới 768px menu thường chuyển sang dạng ngăn kéo, thanh\r\n kéo không còn nghĩa.\r\n -->\r\n @if (showRail()) {\r\n <button\r\n type=\"button\"\r\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\"\r\n [attr.aria-label]=\"railLabel() + ' menu'\"\r\n [attr.aria-expanded]=\"!collapsed()\"\r\n (click)=\"toggle()\"\r\n >\r\n <span class=\"flex flex-col items-center\">\r\n <span\r\n aria-hidden=\"true\"\r\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\"\r\n ></span>\r\n <span\r\n aria-hidden=\"true\"\r\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\"\r\n ></span>\r\n </span>\r\n\r\n <!--\r\n Tooltip tự dựng, không dùng `olTooltip`: directive đó treo panel ra\r\n `.ol-overlay-container` nên không chạy được hiệu ứng trượt gắn với\r\n `group-hover` của chính nút.\r\n -->\r\n <span\r\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\"\r\n >\r\n {{ railLabel() }}\r\n </span>\r\n </button>\r\n }\r\n <!-- ── Phần trên: logo + menu chính ───────────────────────────────── -->\r\n <!--\r\n Đệm ngang 15px khi thu gọn: `<ul>` là flex-col nên `<li>` kéo giãn mục\r\n con hết bề rộng, `size-10` một mình không thắng được. 70 − 15×2 = 40, đúng\r\n ô vuông bằng chiều cao mục (40px — bản gọn 2026-10-06, Figma 44px / 13px).\r\n\r\n `overflow-hidden` đặt ở VÙNG CON, không ở `<nav>`: `<nav>` phải cho tooltip\r\n của thanh kéo tràn ra ngoài mép, nhưng nội dung vẫn cần bị cắt theo bo góc\r\n 24px — nhãn dài ở bề rộng 70px sẽ thò ra khỏi khung bo.\r\n -->\r\n <div\r\n class=\"flex min-h-0 flex-1 flex-col gap-5 overflow-hidden rounded-t-2xl\"\r\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\r\n >\r\n <div class=\"flex items-center justify-center\">\r\n @if (logoDef(); as logo) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"logo.template\"\r\n [ngTemplateOutletContext]=\"{ $implicit: collapsed(), collapsed: collapsed() }\"\r\n />\r\n } @else if (logoSrc()) {\r\n <img\r\n [src]=\"collapsed() && logoMarkSrc() ? logoMarkSrc() : logoSrc()\"\r\n alt=\"Ôn Luyện\"\r\n [class]=\"collapsed() ? 'size-10 object-contain' : 'h-10 w-auto max-w-full object-contain'\"\r\n />\r\n } @else {\r\n <!--\r\n Logo của kit (Figma `Logo ol` #6603:1269). Thu gọn: bản VUÔNG 40px —\r\n bản dài rộng ~164px, ở khung 70px tràn hẳn ra ngoài. Mở: bản dài cao\r\n 32px (~164px, Figma app-nav đặt logo rộng 155px).\r\n -->\r\n @if (collapsed()) {\r\n <ol-logo variant=\"mark\" [tone]=\"logoTone()\" [height]=\"40\" />\r\n } @else {\r\n <ol-logo variant=\"full\" [tone]=\"logoTone()\" [height]=\"32\" />\r\n }\r\n }\r\n </div>\r\n\r\n @if (headerDef(); as header) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"header.template\"\r\n [ngTemplateOutletContext]=\"slotContext('rail')\"\r\n />\r\n }\r\n\r\n <!--\r\n Vùng mục CUỘN ĐƯỢC: menu nhiều nhóm (quản trị trường: 5 nhóm, menu con 3\r\n cấp) cao hơn màn hình. Logo và thẻ tài khoản đứng yên, chỉ danh sách cuộn.\r\n\r\n Thanh cuộn MẢNH, trắng mờ — KHÔNG ẩn hẳn: đo menu giáo viên (10 + 5 mục)\r\n ở màn cao 900px, 3 mục cuối nằm dưới mép mà không có dấu hiệu gì cho\r\n biết còn nữa. Thanh mặc định xám đặc thì chướng trên nền navy.\r\n -->\r\n <div\r\n class=\"-me-3 flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto pe-2 [scrollbar-color:rgb(255_255_255/0.3)_transparent] [scrollbar-width:thin]\"\r\n >\r\n @for (group of mainGroups(); track $index) {\r\n <div class=\"flex flex-col gap-1\">\r\n @if (group.label && !collapsed()) {\r\n <p class=\"px-2.5 pt-1 pb-0.5 text-xs font-bold uppercase tracking-wide text-white/70\">\r\n {{ group.label }}\r\n </p>\r\n }\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (footerDef(); as footer) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"footer.template\"\r\n [ngTemplateOutletContext]=\"slotContext('rail')\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- ── Phần dưới: menu phụ + thẻ tài khoản ────────────────────────── -->\r\n <div\r\n class=\"flex shrink-0 flex-col gap-1 overflow-hidden rounded-b-2xl pt-2 pb-4\"\r\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\r\n >\r\n @if (showDivider()) {\r\n <!-- `aria-hidden`: đường kẻ là dấu hiệu nhìn, trình đọc màn hình không cần -->\r\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: footerItems(), level: 1 }\"\r\n />\r\n\r\n <!--\r\n THẺ TÀI KHOẢN — bấm mở menu tài khoản (popup nổi `ol-dropdown`).\r\n\r\n Popup chứ không đổi danh sách tại chỗ: ở bề rộng 70px (thu gọn) không có\r\n chỗ cho ba dòng chữ; popup nổi ra ngoài menu thì đọc được ở cả hai trạng\r\n thái. Luôn mở SANG PHẢI, cạnh sidebar (`right-end`: mép dưới popup thẳng\r\n mép dưới thẻ, popup mọc lên trên vì thẻ nằm sát đáy) — người dùng yêu\r\n cầu 2026-10-06, bản trước mở lên trên đè lên chính menu khi mở rộng.\r\n\r\n `user.actions = []` thì không có popup — bấm thẻ chỉ phát `userClick`\r\n như các bản trước.\r\n\r\n `autoClose=\"outside\"`: bấm TRONG popup không tự đóng — ruột tuỳ biến có\r\n thể chứa điều khiển (bộ chọn Sáng/Tối) mà người dùng cần thấy kết quả.\r\n Mục hành động tự đóng qua `select()` / `close()` của ngữ cảnh.\r\n -->\r\n @if (config().user; as user) {\r\n <div class=\"mt-2 flex\" [class]=\"collapsed() ? 'justify-center' : ''\">\r\n @if (userActions().length > 0 || userMenuDef()) {\r\n <ol-dropdown\r\n class=\"min-w-0\"\r\n [class]=\"collapsed() ? '' : 'w-full'\"\r\n placement=\"right-end\"\r\n [offset]=\"userMenuOffset()\"\r\n menuClass=\"w-72\"\r\n autoClose=\"outside\"\r\n [(isOpen)]=\"userMenuOpen\"\r\n >\r\n <button\r\n trigger\r\n type=\"button\"\r\n [class]=\"userCardClasses()\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"userMenuOpen()\"\r\n [attr.aria-label]=\"collapsed() ? 'Tài khoản ' + user.name : null\"\r\n (click)=\"userClick.emit()\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: collapsed(),\r\n initials: userInitials(),\r\n variant: 'rail',\r\n }\"\r\n />\r\n </button>\r\n\r\n <!--\r\n Ruột popup chỉ dựng khi mở: `ol-dropdown` ẩn panel bằng class\r\n `hidden`, nên dựng sẵn thì tên + email nằm trong DOM cả khi menu\r\n thu gọn — trình đọc màn hình và tìm kiếm trang vẫn thấy chúng.\r\n -->\r\n @if (userMenuOpen()) {\r\n @if (userMenuDef(); as menu) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"menu.template\"\r\n [ngTemplateOutletContext]=\"userMenuPopupContext()\"\r\n />\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"userMenuBody\" />\r\n }\r\n }\r\n </ol-dropdown>\r\n } @else {\r\n <button type=\"button\" [class]=\"userCardClasses()\" (click)=\"userClick.emit()\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: collapsed(),\r\n initials: userInitials(),\r\n variant: 'rail',\r\n }\"\r\n />\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</nav>\r\n\r\n<!--\r\n ── MOBILE: THANH ĐÁY + KHAY MENU (`layout=\"auto\"` dưới `md`, hoặc `\"mobile\"`) ──\r\n\r\n Chuyển từ phần tự dựng của onluyenv4 vào thư viện (2026-10-06):\r\n\r\n · thanh đáy `mobilePrimaryCount` mục đầu nhóm chính + nút Menu ở ô cuối\r\n · khay trượt lên từ đáy (nút mở nằm ở đáy phải — đi theo ngón tay), chứa\r\n thẻ tài khoản, các mục KHÔNG lên thanh đáy, và menu tài khoản\r\n · `fixed` theo màn hình; `pb-[env(safe-area-inset-bottom)]` chừa thanh vuốt iPhone\r\n\r\n Khay dùng `@if` chứ không ẩn bằng class: ẩn bằng `hidden` thì nút bên trong\r\n vẫn tab tới được dù không nhìn thấy. Lớp phủ là `<button>` để đóng được\r\n bằng bàn phím.\r\n-->\r\n@if (hasMobile()) {\r\n <nav\r\n class=\"fixed inset-x-0 bottom-0 z-50 flex h-[calc(3.5rem+env(safe-area-inset-bottom))] items-stretch border-t border-default bg-neutral-primary pb-[env(safe-area-inset-bottom)]\"\r\n [class]=\"mobileDisplay()\"\r\n aria-label=\"Điều hướng chính\"\r\n >\r\n @for (item of primaryItems(); track item.id) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"barItemClasses(item)\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n [attr.aria-label]=\"item.label\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? barBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'bar')\"\r\n />\r\n </a>\r\n }\r\n\r\n <button\r\n type=\"button\"\r\n class=\"flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 py-2 transition-colors\"\r\n [class]=\"mobileMenuOpen() ? 'text-fg-brand' : 'text-body-subtle hover:text-heading'\"\r\n [attr.aria-expanded]=\"mobileMenuOpen()\"\r\n [attr.aria-controls]=\"sheetId\"\r\n (click)=\"toggleMobileMenu()\"\r\n >\r\n <ol-icon name=\"solar:hamburger_menu_linear\" size=\"base\" />\r\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\r\n {{ mobileMenuLabel() }}\r\n </span>\r\n </button>\r\n </nav>\r\n\r\n <!--\r\n Khay và lớp phủ DỪNG trên thanh đáy (cao 56px + vùng vuốt iPhone): thanh\r\n đáy luôn thấy được và bấm được khi khay mở — nút \"Menu\" bấm lần nữa thì\r\n đóng, các mục khác chuyển trang luôn (người dùng yêu cầu 2026-10-06). Thanh\r\n đáy z-50, trên lớp phủ z-40.\r\n -->\r\n @if (mobileMenuOpen()) {\r\n <button\r\n type=\"button\"\r\n class=\"fixed inset-x-0 top-0 bottom-[calc(3.5rem+env(safe-area-inset-bottom))] z-40 cursor-default bg-black/40 transition-opacity duration-200 starting:opacity-0\"\r\n animate.leave=\"opacity-0\"\r\n [class]=\"mobileDisplay()\"\r\n aria-label=\"Đóng menu\"\r\n (click)=\"mobileMenuOpen.set(false)\"\r\n ></button>\r\n\r\n <!--\r\n Khung như bản cũ: kéo hết bề ngang, dính mép trên thanh đáy, bo 2 góc\r\n trên (người dùng 2026-10-06 — không thẻ nổi cách mép). Bên trong `px-2`\r\n để mỗi dòng bo tròn riêng. Mở: trượt LÊN từ đáy (`starting:` =\r\n `@starting-style`). Đóng: trượt XUỐNG — `animate.leave` gắn\r\n `translate-y-full opacity-0`, Angular chờ transition 200ms xong mới gỡ\r\n khỏi DOM (`@if` gỡ ngay thì không kịp chạy).\r\n\r\n Hai phần: ĐẦU đứng yên (vạch kéo, thẻ tài khoản + Đăng xuất, và phần `top`\r\n của menu tài khoản — Trang cá nhân, Đổi mật khẩu, Giao diện…), THÂN cuộn\r\n (khe, nhóm mục, phần `bottom`). Menu dài thì chỉ thân cuộn (người dùng\r\n 2026-10-06).\r\n -->\r\n <div\r\n [id]=\"sheetId\"\r\n class=\"fixed inset-x-0 bottom-[calc(3.5rem+env(safe-area-inset-bottom))] z-40 flex max-h-[calc(100dvh-3.5rem-env(safe-area-inset-bottom)-4rem)] flex-col rounded-t-lg bg-neutral-primary px-2 pb-2 shadow-2xl transition-[opacity,translate] duration-200 ease-out starting:translate-y-full starting:opacity-0\"\r\n animate.leave=\"translate-y-full opacity-0\"\r\n [class]=\"mobileDisplay()\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n [attr.aria-label]=\"mobileMenuLabel()\"\r\n >\r\n <div class=\"shrink-0\">\r\n <div class=\"flex justify-center pt-2.5\">\r\n <span class=\"h-1 w-10 rounded-full bg-neutral-tertiary\" aria-hidden=\"true\"></span>\r\n </div>\r\n\r\n @if (config().user; as user) {\r\n <div class=\"flex items-center gap-3 px-2 pt-2 pb-2\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? sheetUserBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: false,\r\n initials: userInitials(),\r\n variant: 'sheet',\r\n }\"\r\n />\r\n <!--\r\n `split`: mục danger (Đăng xuất) thành nút CHỈ ICON viền đỏ bên phải thẻ\r\n tài khoản — một chạm, không phải cuộn xuống cuối khay (người dùng\r\n 2026-10-06). Tên đọc + tooltip (nhấn giữ) từ `label`.\r\n -->\r\n @if (mobileUserMenu() === 'split') {\r\n @for (action of cardActions(); track action.id) {\r\n <ol-button\r\n class=\"ms-auto shrink-0\"\r\n color=\"danger\"\r\n [outline]=\"true\"\r\n size=\"sm\"\r\n [iconOnly]=\"true\"\r\n [ariaLabel]=\"action.label\"\r\n (btnClick)=\"onUserAction(action)\"\r\n >\r\n <ol-icon leftIcon [name]=\"action.icon\" size=\"base\" />\r\n </ol-button>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!--\r\n `mobileUserMenu=\"split\"`: việc thường của tài khoản (Trang cá nhân, Đổi\r\n mật khẩu, cài đặt…) lên ĐẦU khay, ngay dưới thẻ tài khoản.\r\n -->\r\n @if (mobileUserMenu() === 'split') {\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetAccount\"\r\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetTopContext() }\"\r\n />\r\n }\r\n </div>\r\n\r\n <div class=\"-mx-2 min-h-0 flex-1 overflow-y-auto overscroll-contain px-2\">\r\n <!--\r\n `empty:hidden`: template khe có thể không vẽ gì (bộ chọn vai chỉ hiện\r\n với tài khoản hai vai). Không có nó, khay còn một dải trống 24px kẹp\r\n giữa hai đường kẻ. `:empty` bỏ qua nút chú thích Angular chèn vào.\r\n -->\r\n @if (headerDef(); as header) {\r\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"header.template\"\r\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\r\n />\r\n </div>\r\n }\r\n\r\n @for (group of sheetGroups(); track $index) {\r\n <div class=\"mt-1 border-t border-default pt-1\">\r\n @if (group.label) {\r\n <p class=\"px-2 pt-2 pb-1 text-xs font-bold uppercase tracking-wide text-body-subtle\">\r\n {{ group.label }}\r\n </p>\r\n }\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (footerDef(); as footer) {\r\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"footer.template\"\r\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\r\n />\r\n </div>\r\n }\r\n\r\n <!-- Menu tài khoản (cuối khay): trọn menu, hoặc chỉ phần `bottom` khi `mobileUserMenu=\"split\"` -->\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetAccount\"\r\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetContext() }\"\r\n />\r\n </div>\r\n </div>\r\n }\r\n}\r\n\r\n<!--\r\n Khối menu tài khoản trong khay — vẽ ở đầu (`part: 'top'`) và/hoặc cuối khay.\r\n Template tuỳ biến nếu có, không thì danh sách mục mặc định của phần đó.\r\n `empty:hidden`: phần không có mục nào thì không để lại dải kẻ.\r\n-->\r\n<ng-template #sheetAccount let-ctx>\r\n @if (ctx) {\r\n @if (userMenuDef(); as menu) {\r\n <!--\r\n `px-2`: ruột tuỳ biến (ol-dropdown-item…) không tự có lề ngang như dòng khay.\r\n `min-h-10` cho mục menu: ol-dropdown-item cao 36px, dòng khay 40px — ép\r\n bằng nhau để cả khay một nhịp.\r\n -->\r\n <div\r\n class=\"mt-1 border-t border-default pt-1 empty:hidden [&_[role=menuitem]]:min-h-10 [&_[role=menuitem]]:rounded-md\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"menu.template\" [ngTemplateOutletContext]=\"ctx\" />\r\n </div>\r\n } @else if (sheetActionsFor(ctx.part).length > 0) {\r\n <ul class=\"mt-1 border-t border-default pt-1\">\r\n @for (action of sheetActionsFor(ctx.part); track action.id) {\r\n @if (action.dividerBefore && !$first) {\r\n <li class=\"my-1 h-px bg-neutral-tertiary\" aria-hidden=\"true\"></li>\r\n }\r\n <li>\r\n @if (action.route) {\r\n <a\r\n [routerLink]=\"action.route\"\r\n [class]=\"sheetActionClasses(action)\"\r\n (click)=\"onUserAction(action)\"\r\n >\r\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\r\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"sheetActionClasses(action)\"\r\n (click)=\"onUserAction(action)\"\r\n >\r\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\r\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\r\n </button>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Cây mục (đệ quy) — cấp 1 · 2 · 3. Nhánh con chỉ vẽ khi mở và menu không\r\n thu gọn; thu gọn thì menu con hiện NỔI cạnh mục cha (xem `#navItem`).\r\n-->\r\n<ng-template #navTree let-items let-level=\"level\">\r\n <ul class=\"flex flex-col gap-1\" [class]=\"level === 1 ? '' : 'mt-1'\">\r\n @for (item of items; track item.id) {\r\n <li>\r\n <ng-container\r\n [ngTemplateOutlet]=\"navItem\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item, level: level }\"\r\n />\r\n @if (item.children?.length && isExpanded(item) && !collapsed() && level < 3) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\r\n />\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</ng-template>\r\n\r\n<!--\r\n Một mục menu. Ba loại hành vi, không gộp được:\r\n có con → `<button aria-expanded>` mở/đóng nhánh, không điều hướng\r\n có `route` → `<a routerLink>`; mục đang chọn tính bằng `isItemActive`\r\n (route · activeRoutes · activeId), không `routerLinkActive`\r\n còn lại → `<button>` chỉ phát `itemClick` (disabled lo trong class)\r\n-->\r\n<ng-template #navItem let-item let-level=\"level\">\r\n @if (item.children?.length && collapsed() && level === 1) {\r\n <!--\r\n THU GỌN + mục cha: rê chuột / bấm / Enter mở menu con NỔI bên phải\r\n (`ol-dropdown`). Không tooltip — đầu panel đã ghi tên mục.\r\n -->\r\n <div\r\n class=\"flex justify-center\"\r\n (mouseenter)=\"openFlyout(item.id)\"\r\n (mouseleave)=\"scheduleFlyoutClose()\"\r\n >\r\n <ol-dropdown\r\n placement=\"right\"\r\n menuClass=\"w-64\"\r\n [isOpen]=\"flyoutId() === item.id\"\r\n (isOpenChange)=\"onFlyoutOpenChange(item.id, $event)\"\r\n >\r\n <button\r\n trigger\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level)\"\r\n [attr.aria-label]=\"item.label\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"flyoutId() === item.id\"\r\n [disabled]=\"item.disabled ?? false\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n\r\n @if (flyoutId() === item.id) {\r\n <div (mouseenter)=\"cancelFlyoutClose()\" (mouseleave)=\"scheduleFlyoutClose()\">\r\n <p class=\"px-2 pt-1 pb-1.5 text-xs font-bold uppercase tracking-wide text-body-subtle\">\r\n {{ item.label }}\r\n </p>\r\n <ng-container\r\n [ngTemplateOutlet]=\"flyoutTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: 2 }\"\r\n />\r\n </div>\r\n }\r\n </ol-dropdown>\r\n </div>\r\n } @else if (item.children?.length) {\r\n <button\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [attr.aria-expanded]=\"isExpanded(item)\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n } @else if (item.route) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"itemClasses(item, level)\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Ruột menu nổi: con là liên kết (`ol-dropdown-item`); con có cháu thì thành\r\n nhãn nhóm, cháu thụt vào dưới nó. Mục đang chọn mang `selected`.\r\n-->\r\n<ng-template #flyoutTree let-items let-level=\"level\">\r\n @for (child of items; track child.id) {\r\n @if (child.children?.length) {\r\n <p class=\"px-2 pt-2 pb-1 text-sm font-semibold text-heading\" [class.ps-5]=\"level > 2\">\r\n {{ child.label }}\r\n </p>\r\n <div class=\"ps-3\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"flyoutTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: child.children, level: level + 1 }\"\r\n />\r\n </div>\r\n } @else {\r\n <ol-dropdown-item\r\n [label]=\"child.label\"\r\n [routerLink]=\"child.route ?? null\"\r\n [selected]=\"isItemActive(child)\"\r\n [disabled]=\"child.disabled ?? false\"\r\n (itemClick)=\"onFlyoutSelect(child)\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Ruột một mục ở menu dọc: icon 24px (chỉ cấp 1) + nhãn + nhãn phụ + huy hiệu\r\n + mũi tên (mục cha). Nhãn ẩn khi thu gọn.\r\n-->\r\n<ng-template #itemBody let-item let-level=\"level\" let-expanded=\"expanded\">\r\n @if (level === 1) {\r\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\r\n }\r\n\r\n @if (!collapsed() || level > 1) {\r\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\r\n\r\n @if (item.extraText) {\r\n <span\r\n class=\"shrink-0 rounded-sm border border-white px-1 text-2xs leading-tight text-white\"\r\n >{{ item.extraText }}</span\r\n >\r\n }\r\n @if (item.badge) {\r\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\r\n }\r\n @if (item.children?.length) {\r\n <ol-icon\r\n name=\"alt_arrow_right\"\r\n size=\"xs\"\r\n class=\"shrink-0 text-white transition-transform duration-200\"\r\n [class.rotate-90]=\"expanded\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!-- Ruột một mục ở thanh đáy mobile: icon + nhãn ngắn (đọc nhãn đầy đủ qua aria-label) -->\r\n<ng-template #barBody let-item>\r\n <ol-icon [name]=\"item.icon\" size=\"base\" />\r\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\r\n {{ item.shortLabel ?? item.label }}\r\n </span>\r\n</ng-template>\r\n\r\n<!-- Cây mục trong khay mobile — cùng luật mở/đóng với menu dọc -->\r\n<ng-template #sheetTree let-items let-level=\"level\">\r\n <ul>\r\n @for (item of items; track item.id) {\r\n <li>\r\n @if (item.route && !item.children?.length) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"sheetItemClasses(item, level)\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\r\n />\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"sheetItemClasses(item, level)\"\r\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item) : null\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\r\n />\r\n </button>\r\n }\r\n @if (item.children?.length && isExpanded(item) && level < 3) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\r\n />\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</ng-template>\r\n\r\n<!-- Ruột một dòng trong khay mobile: nền sáng nên chữ theo token, không trắng -->\r\n<ng-template #sheetBody let-item let-level=\"level\" let-expanded=\"expanded\">\r\n @if (level === 1) {\r\n <ol-icon [name]=\"item.icon\" size=\"sm\" class=\"shrink-0\" />\r\n }\r\n <span class=\"min-w-0 flex-1 truncate\">{{ item.label }}</span>\r\n @if (item.extraText) {\r\n <span class=\"shrink-0 rounded-sm border border-default px-1 text-2xs leading-tight\">{{\r\n item.extraText\r\n }}</span>\r\n }\r\n @if (item.badge) {\r\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\r\n }\r\n @if (item.children?.length) {\r\n <ol-icon\r\n name=\"alt_arrow_right\"\r\n size=\"xs\"\r\n class=\"shrink-0 transition-transform duration-200\"\r\n [class.rotate-90]=\"expanded\"\r\n />\r\n }\r\n</ng-template>\r\n\r\n<!-- Ruột thẻ tài khoản ở đáy menu dọc: avatar + tên/email (ẩn khi thu gọn) + mũi tên -->\r\n<ng-template #userCardBody let-user>\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n @if (!collapsed()) {\r\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\r\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"truncate text-xs text-white/80\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n <!--\r\n Mũi tên CHỈ SANG PHẢI — menu tài khoản mở sang phải cạnh sidebar\r\n (`right-end`); mũi tên xuống của bản trước chỉ sai hướng. Rê chuột / đang\r\n mở thì nhích 2px sang phải báo hướng mở.\r\n -->\r\n <ol-icon\r\n name=\"alt_arrow_right\"\r\n size=\"xs\"\r\n class=\"shrink-0 text-white/80 transition-transform duration-200 group-hover/user:translate-x-0.5\"\r\n [class.translate-x-0.5]=\"userMenuOpen()\"\r\n />\r\n }\r\n</ng-template>\r\n\r\n<!-- Khối tài khoản đầu khay mobile -->\r\n<ng-template #sheetUserBody let-user>\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n</ng-template>\r\n\r\n<!--\r\n Ruột popup tài khoản mặc định: đầu là avatar + tên + email để người dùng\r\n biết đang ở tài khoản nào, dưới là các mục `userActions()`.\r\n-->\r\n<ng-template #userMenuBody>\r\n @if (config().user; as user) {\r\n <div class=\"flex items-center gap-3 px-2 py-1.5\">\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n </div>\r\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\r\n @for (action of userActions(); track action.id) {\r\n @if (action.dividerBefore && !$first) {\r\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\r\n }\r\n <ol-dropdown-item\r\n [icon]=\"action.icon\"\r\n [label]=\"action.label\"\r\n [routerLink]=\"action.route ?? null\"\r\n [danger]=\"action.danger ?? false\"\r\n (itemClick)=\"onUserAction(action)\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n",
|
|
27281
27549
|
dependencies: [
|
|
27282
27550
|
{
|
|
27283
27551
|
kind: "directive",
|
|
@@ -27478,7 +27746,11 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
27478
27746
|
],
|
|
27479
27747
|
host: { class: "block" },
|
|
27480
27748
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
27481
|
-
template: "<!--\r\n Menu điều hướng Ôn Luyện — Figma `ol-app-nav` (#5303:30109).\r\n\r\n Màu lấy từ TOKEN, không ghim hex:\r\n\r\n --bg-app-nav-from điểm đầu gradient (--color-navy-from)\r\n --bg-app-nav-to điểm cuối (--color-navy-to)\r\n --bg-app-nav-hover nền mục khi rê chuột (--color-brand-600)\r\n\r\n Bản đầu tôi ghim thẳng `#445a9a` và `#0f2977` vào đây với lý do \"bản sắc của\r\n một sản phẩm, không phải bậc màu dùng chung\". Lý do đó sai chỗ: primitives là\r\n nơi MỌI màu gốc nằm, kể cả màu chỉ một component dùng. Ghim trong template\r\n thì `check:tokens` không thấy (guard chỉ quét class Tailwind bậc số, không\r\n quét `style=`), và đổi màu phải sửa từng nơi dùng.\r\n\r\n Giá trị dự phòng giữ lại để component vẫn vẽ đúng khi nơi dùng quên nhập\r\n `tokens.css` — đã xảy ra tới bản 1.0.14, gói publish không có file CSS nào.\r\n\r\n Bố cục `justify-between` chia ba vùng như Figma: phần trên co theo nội dung,\r\n phần dưới dính đáy, khoảng trống ở giữa.\r\n-->\r\n<nav\r\n [class]=\"\r\n 'relative h-full flex-col justify-between rounded-2xl pt-5 transition-[width] duration-200 ' +\r\n railDisplay()\r\n \"\r\n style=\"\r\n background-image: linear-gradient(\r\n 164.52deg,\r\n var(--bg-app-nav-from, #445a9a) 0%,\r\n var(--bg-app-nav-to, #0f2977) 100%\r\n );\r\n \"\r\n [style.width]=\"navWidth()\"\r\n [attr.aria-label]=\"'Menu điều hướng'\"\r\n>\r\n <!--\r\n THANH KÉO (rail) — thu gọn / mở lại bằng một cú bấm ở mép phải.\r\n\r\n Dựng theo đúng hành vi mẫu người dùng đưa:\r\n\r\n · hai vạch dọc 8×2px, bo hai đầu ngoài\r\n · hover thì xoay ±40° thành hình mũi tên, và đậm lên\r\n · tooltip trượt từ trái sang, chỉ hiện khi hover\r\n · vùng bấm rộng 28px dù vạch chỉ 2px — ngón tay và chuột cần chỗ\r\n\r\n Đặt TRONG `<nav>` với `absolute` chứ không `fixed` như mẫu: `fixed` neo\r\n theo màn hình nên menu đặt ở đâu nó cũng nhảy về mép trái. `absolute` bám\r\n đúng mép phải của chính menu, và `group/rail` cho hover đi xuyên xuống hai\r\n vạch con.\r\n\r\n `hidden md:flex`: dưới 768px menu thường chuyển sang dạng ngăn kéo, thanh\r\n kéo không còn nghĩa.\r\n -->\r\n @if (showRail()) {\r\n <button\r\n type=\"button\"\r\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\"\r\n [attr.aria-label]=\"railLabel() + ' menu'\"\r\n [attr.aria-expanded]=\"!collapsed()\"\r\n (click)=\"toggle()\"\r\n >\r\n <span class=\"flex flex-col items-center\">\r\n <span\r\n aria-hidden=\"true\"\r\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\"\r\n ></span>\r\n <span\r\n aria-hidden=\"true\"\r\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\"\r\n ></span>\r\n </span>\r\n\r\n <!--\r\n Tooltip tự dựng, không dùng `olTooltip`: directive đó treo panel ra\r\n `.ol-overlay-container` nên không chạy được hiệu ứng trượt gắn với\r\n `group-hover` của chính nút.\r\n -->\r\n <span\r\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\"\r\n >\r\n {{ railLabel() }}\r\n </span>\r\n </button>\r\n }\r\n <!-- ── Phần trên: logo + menu chính ───────────────────────────────── -->\r\n <!--\r\n Đệm ngang 15px khi thu gọn: `<ul>` là flex-col nên `<li>` kéo giãn mục\r\n con hết bề rộng, `size-10` một mình không thắng được. 70 − 15×2 = 40, đúng\r\n ô vuông bằng chiều cao mục (40px — bản gọn 2026-10-06, Figma 44px / 13px).\r\n\r\n `overflow-hidden` đặt ở VÙNG CON, không ở `<nav>`: `<nav>` phải cho tooltip\r\n của thanh kéo tràn ra ngoài mép, nhưng nội dung vẫn cần bị cắt theo bo góc\r\n 24px — nhãn dài ở bề rộng 70px sẽ thò ra khỏi khung bo.\r\n -->\r\n <div\r\n class=\"flex min-h-0 flex-1 flex-col gap-5 overflow-hidden rounded-t-2xl\"\r\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\r\n >\r\n <div class=\"flex items-center justify-center\">\r\n @if (logoDef(); as logo) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"logo.template\"\r\n [ngTemplateOutletContext]=\"{ $implicit: collapsed(), collapsed: collapsed() }\"\r\n />\r\n } @else if (logoSrc()) {\r\n <img\r\n [src]=\"collapsed() && logoMarkSrc() ? logoMarkSrc() : logoSrc()\"\r\n alt=\"Ôn Luyện\"\r\n [class]=\"collapsed() ? 'size-10 object-contain' : 'h-10 w-auto max-w-full object-contain'\"\r\n />\r\n } @else {\r\n <!--\r\n Logo của kit (Figma `Logo ol` #6603:1269). Thu gọn: bản VUÔNG 40px —\r\n bản dài rộng ~164px, ở khung 70px tràn hẳn ra ngoài. Mở: bản dài cao\r\n 32px (~164px, Figma app-nav đặt logo rộng 155px).\r\n -->\r\n @if (collapsed()) {\r\n <ol-logo variant=\"mark\" [tone]=\"logoTone()\" [height]=\"40\" />\r\n } @else {\r\n <ol-logo variant=\"full\" [tone]=\"logoTone()\" [height]=\"32\" />\r\n }\r\n }\r\n </div>\r\n\r\n @if (headerDef(); as header) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"header.template\"\r\n [ngTemplateOutletContext]=\"slotContext('rail')\"\r\n />\r\n }\r\n\r\n <!--\r\n Vùng mục CUỘN ĐƯỢC: menu nhiều nhóm (quản trị trường: 5 nhóm, menu con 3\r\n cấp) cao hơn màn hình. Logo và thẻ tài khoản đứng yên, chỉ danh sách cuộn.\r\n\r\n Thanh cuộn MẢNH, trắng mờ — KHÔNG ẩn hẳn: đo menu giáo viên (10 + 5 mục)\r\n ở màn cao 900px, 3 mục cuối nằm dưới mép mà không có dấu hiệu gì cho\r\n biết còn nữa. Thanh mặc định xám đặc thì chướng trên nền navy.\r\n -->\r\n <div\r\n class=\"-me-3 flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto pe-2 [scrollbar-color:rgb(255_255_255/0.3)_transparent] [scrollbar-width:thin]\"\r\n >\r\n @for (group of mainGroups(); track $index) {\r\n <div class=\"flex flex-col gap-1\">\r\n @if (group.label && !collapsed()) {\r\n <p class=\"px-2.5 pt-1 pb-0.5 text-xs font-bold uppercase tracking-wide text-white/70\">\r\n {{ group.label }}\r\n </p>\r\n }\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (footerDef(); as footer) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"footer.template\"\r\n [ngTemplateOutletContext]=\"slotContext('rail')\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- ── Phần dưới: menu phụ + thẻ tài khoản ────────────────────────── -->\r\n <div\r\n class=\"flex shrink-0 flex-col gap-1 overflow-hidden rounded-b-2xl pt-2 pb-4\"\r\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\r\n >\r\n @if (showDivider()) {\r\n <!-- `aria-hidden`: đường kẻ là dấu hiệu nhìn, trình đọc màn hình không cần -->\r\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: footerItems(), level: 1 }\"\r\n />\r\n\r\n <!--\r\n THẺ TÀI KHOẢN — bấm mở menu tài khoản (popup nổi `ol-dropdown`).\r\n\r\n Popup chứ không đổi danh sách tại chỗ: ở bề rộng 70px (thu gọn) không có\r\n chỗ cho ba dòng chữ; popup nổi ra ngoài menu thì đọc được ở cả hai trạng\r\n thái. Luôn mở SANG PHẢI, cạnh sidebar (`right-end`: mép dưới popup thẳng\r\n mép dưới thẻ, popup mọc lên trên vì thẻ nằm sát đáy) — người dùng yêu\r\n cầu 2026-10-06, bản trước mở lên trên đè lên chính menu khi mở rộng.\r\n\r\n `user.actions = []` thì không có popup — bấm thẻ chỉ phát `userClick`\r\n như các bản trước.\r\n\r\n `autoClose=\"outside\"`: bấm TRONG popup không tự đóng — ruột tuỳ biến có\r\n thể chứa điều khiển (bộ chọn Sáng/Tối) mà người dùng cần thấy kết quả.\r\n Mục hành động tự đóng qua `select()` / `close()` của ngữ cảnh.\r\n -->\r\n @if (config().user; as user) {\r\n <div class=\"mt-2 flex\" [class]=\"collapsed() ? 'justify-center' : ''\">\r\n @if (userActions().length > 0 || userMenuDef()) {\r\n <ol-dropdown\r\n class=\"min-w-0\"\r\n [class]=\"collapsed() ? '' : 'w-full'\"\r\n placement=\"right-end\"\r\n [offset]=\"userMenuOffset()\"\r\n menuClass=\"w-72\"\r\n autoClose=\"outside\"\r\n [(isOpen)]=\"userMenuOpen\"\r\n >\r\n <button\r\n trigger\r\n type=\"button\"\r\n [class]=\"userCardClasses()\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"userMenuOpen()\"\r\n [attr.aria-label]=\"collapsed() ? 'Tài khoản ' + user.name : null\"\r\n (click)=\"userClick.emit()\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: collapsed(),\r\n initials: userInitials(),\r\n variant: 'rail',\r\n }\"\r\n />\r\n </button>\r\n\r\n <!--\r\n Ruột popup chỉ dựng khi mở: `ol-dropdown` ẩn panel bằng class\r\n `hidden`, nên dựng sẵn thì tên + email nằm trong DOM cả khi menu\r\n thu gọn — trình đọc màn hình và tìm kiếm trang vẫn thấy chúng.\r\n -->\r\n @if (userMenuOpen()) {\r\n @if (userMenuDef(); as menu) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"menu.template\"\r\n [ngTemplateOutletContext]=\"userMenuPopupContext()\"\r\n />\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"userMenuBody\" />\r\n }\r\n }\r\n </ol-dropdown>\r\n } @else {\r\n <button type=\"button\" [class]=\"userCardClasses()\" (click)=\"userClick.emit()\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: collapsed(),\r\n initials: userInitials(),\r\n variant: 'rail',\r\n }\"\r\n />\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</nav>\r\n\r\n<!--\r\n ── MOBILE: THANH ĐÁY + KHAY MENU (`layout=\"auto\"` dưới `md`, hoặc `\"mobile\"`) ──\r\n\r\n Chuyển từ phần tự dựng của onluyenv4 vào thư viện (2026-10-06):\r\n\r\n · thanh đáy `mobilePrimaryCount` mục đầu nhóm chính + nút Menu ở ô cuối\r\n · khay trượt lên từ đáy (nút mở nằm ở đáy phải — đi theo ngón tay), chứa\r\n thẻ tài khoản, các mục KHÔNG lên thanh đáy, và menu tài khoản\r\n · `fixed` theo màn hình; `pb-[env(safe-area-inset-bottom)]` chừa thanh vuốt iPhone\r\n\r\n Khay dùng `@if` chứ không ẩn bằng class: ẩn bằng `hidden` thì nút bên trong\r\n vẫn tab tới được dù không nhìn thấy. Lớp phủ là `<button>` để đóng được\r\n bằng bàn phím.\r\n-->\r\n@if (hasMobile()) {\r\n <nav\r\n class=\"fixed inset-x-0 bottom-0 z-50 flex h-[calc(3.5rem+env(safe-area-inset-bottom))] items-stretch border-t border-default bg-neutral-primary pb-[env(safe-area-inset-bottom)]\"\r\n [class]=\"mobileDisplay()\"\r\n aria-label=\"Điều hướng chính\"\r\n >\r\n @for (item of primaryItems(); track item.id) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"barItemClasses(item)\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n [attr.aria-label]=\"item.label\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? barBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'bar')\"\r\n />\r\n </a>\r\n }\r\n\r\n <button\r\n type=\"button\"\r\n class=\"flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 py-2 transition-colors\"\r\n [class]=\"mobileMenuOpen() ? 'text-fg-brand' : 'text-body-subtle hover:text-heading'\"\r\n [attr.aria-expanded]=\"mobileMenuOpen()\"\r\n [attr.aria-controls]=\"sheetId\"\r\n (click)=\"toggleMobileMenu()\"\r\n >\r\n <ol-icon name=\"solar:hamburger-menu-linear\" size=\"base\" />\r\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\r\n {{ mobileMenuLabel() }}\r\n </span>\r\n </button>\r\n </nav>\r\n\r\n <!--\r\n Khay và lớp phủ DỪNG trên thanh đáy (cao 56px + vùng vuốt iPhone): thanh\r\n đáy luôn thấy được và bấm được khi khay mở — nút \"Menu\" bấm lần nữa thì\r\n đóng, các mục khác chuyển trang luôn (người dùng yêu cầu 2026-10-06). Thanh\r\n đáy z-50, trên lớp phủ z-40.\r\n -->\r\n @if (mobileMenuOpen()) {\r\n <button\r\n type=\"button\"\r\n class=\"fixed inset-x-0 top-0 bottom-[calc(3.5rem+env(safe-area-inset-bottom))] z-40 cursor-default bg-black/40 transition-opacity duration-200 starting:opacity-0\"\r\n animate.leave=\"opacity-0\"\r\n [class]=\"mobileDisplay()\"\r\n aria-label=\"Đóng menu\"\r\n (click)=\"mobileMenuOpen.set(false)\"\r\n ></button>\r\n\r\n <!--\r\n Khung như bản cũ: kéo hết bề ngang, dính mép trên thanh đáy, bo 2 góc\r\n trên (người dùng 2026-10-06 — không thẻ nổi cách mép). Bên trong `px-2`\r\n để mỗi dòng bo tròn riêng. Mở: trượt LÊN từ đáy (`starting:` =\r\n `@starting-style`). Đóng: trượt XUỐNG — `animate.leave` gắn\r\n `translate-y-full opacity-0`, Angular chờ transition 200ms xong mới gỡ\r\n khỏi DOM (`@if` gỡ ngay thì không kịp chạy).\r\n\r\n Hai phần: ĐẦU đứng yên (vạch kéo, thẻ tài khoản + Đăng xuất, và phần `top`\r\n của menu tài khoản — Trang cá nhân, Đổi mật khẩu, Giao diện…), THÂN cuộn\r\n (khe, nhóm mục, phần `bottom`). Menu dài thì chỉ thân cuộn (người dùng\r\n 2026-10-06).\r\n -->\r\n <div\r\n [id]=\"sheetId\"\r\n class=\"fixed inset-x-0 bottom-[calc(3.5rem+env(safe-area-inset-bottom))] z-40 flex max-h-[calc(100dvh-3.5rem-env(safe-area-inset-bottom)-4rem)] flex-col rounded-t-lg bg-neutral-primary px-2 pb-2 shadow-2xl transition-[opacity,translate] duration-200 ease-out starting:translate-y-full starting:opacity-0\"\r\n animate.leave=\"translate-y-full opacity-0\"\r\n [class]=\"mobileDisplay()\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n [attr.aria-label]=\"mobileMenuLabel()\"\r\n >\r\n <div class=\"shrink-0\">\r\n <div class=\"flex justify-center pt-2.5\">\r\n <span class=\"h-1 w-10 rounded-full bg-neutral-tertiary\" aria-hidden=\"true\"></span>\r\n </div>\r\n\r\n @if (config().user; as user) {\r\n <div class=\"flex items-center gap-3 px-2 pt-2 pb-2\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? sheetUserBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: false,\r\n initials: userInitials(),\r\n variant: 'sheet',\r\n }\"\r\n />\r\n <!--\r\n `split`: mục danger (Đăng xuất) thành nút CHỈ ICON viền đỏ bên phải thẻ\r\n tài khoản — một chạm, không phải cuộn xuống cuối khay (người dùng\r\n 2026-10-06). Tên đọc + tooltip (nhấn giữ) từ `label`.\r\n -->\r\n @if (mobileUserMenu() === 'split') {\r\n @for (action of cardActions(); track action.id) {\r\n <ol-button\r\n class=\"ms-auto shrink-0\"\r\n color=\"danger\"\r\n [outline]=\"true\"\r\n size=\"sm\"\r\n [iconOnly]=\"true\"\r\n [ariaLabel]=\"action.label\"\r\n (btnClick)=\"onUserAction(action)\"\r\n >\r\n <ol-icon leftIcon [name]=\"action.icon\" size=\"base\" />\r\n </ol-button>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!--\r\n `mobileUserMenu=\"split\"`: việc thường của tài khoản (Trang cá nhân, Đổi\r\n mật khẩu, cài đặt…) lên ĐẦU khay, ngay dưới thẻ tài khoản.\r\n -->\r\n @if (mobileUserMenu() === 'split') {\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetAccount\"\r\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetTopContext() }\"\r\n />\r\n }\r\n </div>\r\n\r\n <div class=\"-mx-2 min-h-0 flex-1 overflow-y-auto overscroll-contain px-2\">\r\n <!--\r\n `empty:hidden`: template khe có thể không vẽ gì (bộ chọn vai chỉ hiện\r\n với tài khoản hai vai). Không có nó, khay còn một dải trống 24px kẹp\r\n giữa hai đường kẻ. `:empty` bỏ qua nút chú thích Angular chèn vào.\r\n -->\r\n @if (headerDef(); as header) {\r\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"header.template\"\r\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\r\n />\r\n </div>\r\n }\r\n\r\n @for (group of sheetGroups(); track $index) {\r\n <div class=\"mt-1 border-t border-default pt-1\">\r\n @if (group.label) {\r\n <p class=\"px-2 pt-2 pb-1 text-xs font-bold uppercase tracking-wide text-body-subtle\">\r\n {{ group.label }}\r\n </p>\r\n }\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (footerDef(); as footer) {\r\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"footer.template\"\r\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\r\n />\r\n </div>\r\n }\r\n\r\n <!-- Menu tài khoản (cuối khay): trọn menu, hoặc chỉ phần `bottom` khi `mobileUserMenu=\"split\"` -->\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetAccount\"\r\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetContext() }\"\r\n />\r\n </div>\r\n </div>\r\n }\r\n}\r\n\r\n<!--\r\n Khối menu tài khoản trong khay — vẽ ở đầu (`part: 'top'`) và/hoặc cuối khay.\r\n Template tuỳ biến nếu có, không thì danh sách mục mặc định của phần đó.\r\n `empty:hidden`: phần không có mục nào thì không để lại dải kẻ.\r\n-->\r\n<ng-template #sheetAccount let-ctx>\r\n @if (ctx) {\r\n @if (userMenuDef(); as menu) {\r\n <!--\r\n `px-2`: ruột tuỳ biến (ol-dropdown-item…) không tự có lề ngang như dòng khay.\r\n `min-h-10` cho mục menu: ol-dropdown-item cao 36px, dòng khay 40px — ép\r\n bằng nhau để cả khay một nhịp.\r\n -->\r\n <div\r\n class=\"mt-1 border-t border-default pt-1 empty:hidden [&_[role=menuitem]]:min-h-10 [&_[role=menuitem]]:rounded-md\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"menu.template\" [ngTemplateOutletContext]=\"ctx\" />\r\n </div>\r\n } @else if (sheetActionsFor(ctx.part).length > 0) {\r\n <ul class=\"mt-1 border-t border-default pt-1\">\r\n @for (action of sheetActionsFor(ctx.part); track action.id) {\r\n @if (action.dividerBefore && !$first) {\r\n <li class=\"my-1 h-px bg-neutral-tertiary\" aria-hidden=\"true\"></li>\r\n }\r\n <li>\r\n @if (action.route) {\r\n <a\r\n [routerLink]=\"action.route\"\r\n [class]=\"sheetActionClasses(action)\"\r\n (click)=\"onUserAction(action)\"\r\n >\r\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\r\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"sheetActionClasses(action)\"\r\n (click)=\"onUserAction(action)\"\r\n >\r\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\r\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\r\n </button>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Cây mục (đệ quy) — cấp 1 · 2 · 3. Nhánh con chỉ vẽ khi mở và menu không\r\n thu gọn; thu gọn thì menu con hiện NỔI cạnh mục cha (xem `#navItem`).\r\n-->\r\n<ng-template #navTree let-items let-level=\"level\">\r\n <ul class=\"flex flex-col gap-1\" [class]=\"level === 1 ? '' : 'mt-1'\">\r\n @for (item of items; track item.id) {\r\n <li>\r\n <ng-container\r\n [ngTemplateOutlet]=\"navItem\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item, level: level }\"\r\n />\r\n @if (item.children?.length && isExpanded(item) && !collapsed() && level < 3) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\r\n />\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</ng-template>\r\n\r\n<!--\r\n Một mục menu. Ba loại hành vi, không gộp được:\r\n có con → `<button aria-expanded>` mở/đóng nhánh, không điều hướng\r\n có `route` → `<a routerLink>`; mục đang chọn tính bằng `isItemActive`\r\n (route · activeRoutes · activeId), không `routerLinkActive`\r\n còn lại → `<button>` chỉ phát `itemClick` (disabled lo trong class)\r\n-->\r\n<ng-template #navItem let-item let-level=\"level\">\r\n @if (item.children?.length && collapsed() && level === 1) {\r\n <!--\r\n THU GỌN + mục cha: rê chuột / bấm / Enter mở menu con NỔI bên phải\r\n (`ol-dropdown`). Không tooltip — đầu panel đã ghi tên mục.\r\n -->\r\n <div\r\n class=\"flex justify-center\"\r\n (mouseenter)=\"openFlyout(item.id)\"\r\n (mouseleave)=\"scheduleFlyoutClose()\"\r\n >\r\n <ol-dropdown\r\n placement=\"right\"\r\n menuClass=\"w-64\"\r\n [isOpen]=\"flyoutId() === item.id\"\r\n (isOpenChange)=\"onFlyoutOpenChange(item.id, $event)\"\r\n >\r\n <button\r\n trigger\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level)\"\r\n [attr.aria-label]=\"item.label\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"flyoutId() === item.id\"\r\n [disabled]=\"item.disabled ?? false\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n\r\n @if (flyoutId() === item.id) {\r\n <div (mouseenter)=\"cancelFlyoutClose()\" (mouseleave)=\"scheduleFlyoutClose()\">\r\n <p class=\"px-2 pt-1 pb-1.5 text-xs font-bold uppercase tracking-wide text-body-subtle\">\r\n {{ item.label }}\r\n </p>\r\n <ng-container\r\n [ngTemplateOutlet]=\"flyoutTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: 2 }\"\r\n />\r\n </div>\r\n }\r\n </ol-dropdown>\r\n </div>\r\n } @else if (item.children?.length) {\r\n <button\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [attr.aria-expanded]=\"isExpanded(item)\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n } @else if (item.route) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"itemClasses(item, level)\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Ruột menu nổi: con là liên kết (`ol-dropdown-item`); con có cháu thì thành\r\n nhãn nhóm, cháu thụt vào dưới nó. Mục đang chọn mang `selected`.\r\n-->\r\n<ng-template #flyoutTree let-items let-level=\"level\">\r\n @for (child of items; track child.id) {\r\n @if (child.children?.length) {\r\n <p class=\"px-2 pt-2 pb-1 text-sm font-semibold text-heading\" [class.ps-5]=\"level > 2\">\r\n {{ child.label }}\r\n </p>\r\n <div class=\"ps-3\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"flyoutTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: child.children, level: level + 1 }\"\r\n />\r\n </div>\r\n } @else {\r\n <ol-dropdown-item\r\n [label]=\"child.label\"\r\n [routerLink]=\"child.route ?? null\"\r\n [selected]=\"isItemActive(child)\"\r\n [disabled]=\"child.disabled ?? false\"\r\n (itemClick)=\"onFlyoutSelect(child)\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Ruột một mục ở menu dọc: icon 24px (chỉ cấp 1) + nhãn + nhãn phụ + huy hiệu\r\n + mũi tên (mục cha). Nhãn ẩn khi thu gọn.\r\n-->\r\n<ng-template #itemBody let-item let-level=\"level\" let-expanded=\"expanded\">\r\n @if (level === 1) {\r\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\r\n }\r\n\r\n @if (!collapsed() || level > 1) {\r\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\r\n\r\n @if (item.extraText) {\r\n <span\r\n class=\"shrink-0 rounded-sm border border-white px-1 text-2xs leading-tight text-white\"\r\n >{{ item.extraText }}</span\r\n >\r\n }\r\n @if (item.badge) {\r\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\r\n }\r\n @if (item.children?.length) {\r\n <ol-icon\r\n name=\"alt-arrow-right\"\r\n size=\"xs\"\r\n class=\"shrink-0 text-white transition-transform duration-200\"\r\n [class.rotate-90]=\"expanded\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!-- Ruột một mục ở thanh đáy mobile: icon + nhãn ngắn (đọc nhãn đầy đủ qua aria-label) -->\r\n<ng-template #barBody let-item>\r\n <ol-icon [name]=\"item.icon\" size=\"base\" />\r\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\r\n {{ item.shortLabel ?? item.label }}\r\n </span>\r\n</ng-template>\r\n\r\n<!-- Cây mục trong khay mobile — cùng luật mở/đóng với menu dọc -->\r\n<ng-template #sheetTree let-items let-level=\"level\">\r\n <ul>\r\n @for (item of items; track item.id) {\r\n <li>\r\n @if (item.route && !item.children?.length) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"sheetItemClasses(item, level)\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\r\n />\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"sheetItemClasses(item, level)\"\r\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item) : null\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\r\n />\r\n </button>\r\n }\r\n @if (item.children?.length && isExpanded(item) && level < 3) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\r\n />\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</ng-template>\r\n\r\n<!-- Ruột một dòng trong khay mobile: nền sáng nên chữ theo token, không trắng -->\r\n<ng-template #sheetBody let-item let-level=\"level\" let-expanded=\"expanded\">\r\n @if (level === 1) {\r\n <ol-icon [name]=\"item.icon\" size=\"sm\" class=\"shrink-0\" />\r\n }\r\n <span class=\"min-w-0 flex-1 truncate\">{{ item.label }}</span>\r\n @if (item.extraText) {\r\n <span class=\"shrink-0 rounded-sm border border-default px-1 text-2xs leading-tight\">{{\r\n item.extraText\r\n }}</span>\r\n }\r\n @if (item.badge) {\r\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\r\n }\r\n @if (item.children?.length) {\r\n <ol-icon\r\n name=\"alt-arrow-right\"\r\n size=\"xs\"\r\n class=\"shrink-0 transition-transform duration-200\"\r\n [class.rotate-90]=\"expanded\"\r\n />\r\n }\r\n</ng-template>\r\n\r\n<!-- Ruột thẻ tài khoản ở đáy menu dọc: avatar + tên/email (ẩn khi thu gọn) + mũi tên -->\r\n<ng-template #userCardBody let-user>\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n @if (!collapsed()) {\r\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\r\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"truncate text-xs text-white/80\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n <!--\r\n Mũi tên CHỈ SANG PHẢI — menu tài khoản mở sang phải cạnh sidebar\r\n (`right-end`); mũi tên xuống của bản trước chỉ sai hướng. Rê chuột / đang\r\n mở thì nhích 2px sang phải báo hướng mở.\r\n -->\r\n <ol-icon\r\n name=\"alt-arrow-right\"\r\n size=\"xs\"\r\n class=\"shrink-0 text-white/80 transition-transform duration-200 group-hover/user:translate-x-0.5\"\r\n [class.translate-x-0.5]=\"userMenuOpen()\"\r\n />\r\n }\r\n</ng-template>\r\n\r\n<!-- Khối tài khoản đầu khay mobile -->\r\n<ng-template #sheetUserBody let-user>\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n</ng-template>\r\n\r\n<!--\r\n Ruột popup tài khoản mặc định: đầu là avatar + tên + email để người dùng\r\n biết đang ở tài khoản nào, dưới là các mục `userActions()`.\r\n-->\r\n<ng-template #userMenuBody>\r\n @if (config().user; as user) {\r\n <div class=\"flex items-center gap-3 px-2 py-1.5\">\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n </div>\r\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\r\n @for (action of userActions(); track action.id) {\r\n @if (action.dividerBefore && !$first) {\r\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\r\n }\r\n <ol-dropdown-item\r\n [icon]=\"action.icon\"\r\n [label]=\"action.label\"\r\n [routerLink]=\"action.route ?? null\"\r\n [danger]=\"action.danger ?? false\"\r\n (itemClick)=\"onUserAction(action)\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n"
|
|
27749
|
+
providers: [provideOlIcons({
|
|
27750
|
+
"solar:hamburger-menu-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\" d=\"M3 6h18M3 12h18M3 18h18\"/></svg>",
|
|
27751
|
+
"alt-arrow-right": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"m9 5l7 7l-7 7\"/></svg>"
|
|
27752
|
+
})],
|
|
27753
|
+
template: "<!--\r\n Menu điều hướng Ôn Luyện — Figma `ol-app-nav` (#5303:30109).\r\n\r\n Màu lấy từ TOKEN, không ghim hex:\r\n\r\n --bg-app-nav-from điểm đầu gradient (--color-navy-from)\r\n --bg-app-nav-to điểm cuối (--color-navy-to)\r\n --bg-app-nav-hover nền mục khi rê chuột (--color-brand-600)\r\n\r\n Bản đầu tôi ghim thẳng `#445a9a` và `#0f2977` vào đây với lý do \"bản sắc của\r\n một sản phẩm, không phải bậc màu dùng chung\". Lý do đó sai chỗ: primitives là\r\n nơi MỌI màu gốc nằm, kể cả màu chỉ một component dùng. Ghim trong template\r\n thì `check:tokens` không thấy (guard chỉ quét class Tailwind bậc số, không\r\n quét `style=`), và đổi màu phải sửa từng nơi dùng.\r\n\r\n Giá trị dự phòng giữ lại để component vẫn vẽ đúng khi nơi dùng quên nhập\r\n `tokens.css` — đã xảy ra tới bản 1.0.14, gói publish không có file CSS nào.\r\n\r\n Bố cục `justify-between` chia ba vùng như Figma: phần trên co theo nội dung,\r\n phần dưới dính đáy, khoảng trống ở giữa.\r\n-->\r\n<nav\r\n [class]=\"\r\n 'relative h-full flex-col justify-between rounded-2xl pt-5 transition-[width] duration-200 ' +\r\n railDisplay()\r\n \"\r\n style=\"\r\n background-image: linear-gradient(\r\n 164.52deg,\r\n var(--bg-app-nav-from, #445a9a) 0%,\r\n var(--bg-app-nav-to, #0f2977) 100%\r\n );\r\n \"\r\n [style.width]=\"navWidth()\"\r\n [attr.aria-label]=\"'Menu điều hướng'\"\r\n>\r\n <!--\r\n THANH KÉO (rail) — thu gọn / mở lại bằng một cú bấm ở mép phải.\r\n\r\n Dựng theo đúng hành vi mẫu người dùng đưa:\r\n\r\n · hai vạch dọc 8×2px, bo hai đầu ngoài\r\n · hover thì xoay ±40° thành hình mũi tên, và đậm lên\r\n · tooltip trượt từ trái sang, chỉ hiện khi hover\r\n · vùng bấm rộng 28px dù vạch chỉ 2px — ngón tay và chuột cần chỗ\r\n\r\n Đặt TRONG `<nav>` với `absolute` chứ không `fixed` như mẫu: `fixed` neo\r\n theo màn hình nên menu đặt ở đâu nó cũng nhảy về mép trái. `absolute` bám\r\n đúng mép phải của chính menu, và `group/rail` cho hover đi xuyên xuống hai\r\n vạch con.\r\n\r\n `hidden md:flex`: dưới 768px menu thường chuyển sang dạng ngăn kéo, thanh\r\n kéo không còn nghĩa.\r\n -->\r\n @if (showRail()) {\r\n <button\r\n type=\"button\"\r\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\"\r\n [attr.aria-label]=\"railLabel() + ' menu'\"\r\n [attr.aria-expanded]=\"!collapsed()\"\r\n (click)=\"toggle()\"\r\n >\r\n <span class=\"flex flex-col items-center\">\r\n <span\r\n aria-hidden=\"true\"\r\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\"\r\n ></span>\r\n <span\r\n aria-hidden=\"true\"\r\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\"\r\n ></span>\r\n </span>\r\n\r\n <!--\r\n Tooltip tự dựng, không dùng `olTooltip`: directive đó treo panel ra\r\n `.ol-overlay-container` nên không chạy được hiệu ứng trượt gắn với\r\n `group-hover` của chính nút.\r\n -->\r\n <span\r\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\"\r\n >\r\n {{ railLabel() }}\r\n </span>\r\n </button>\r\n }\r\n <!-- ── Phần trên: logo + menu chính ───────────────────────────────── -->\r\n <!--\r\n Đệm ngang 15px khi thu gọn: `<ul>` là flex-col nên `<li>` kéo giãn mục\r\n con hết bề rộng, `size-10` một mình không thắng được. 70 − 15×2 = 40, đúng\r\n ô vuông bằng chiều cao mục (40px — bản gọn 2026-10-06, Figma 44px / 13px).\r\n\r\n `overflow-hidden` đặt ở VÙNG CON, không ở `<nav>`: `<nav>` phải cho tooltip\r\n của thanh kéo tràn ra ngoài mép, nhưng nội dung vẫn cần bị cắt theo bo góc\r\n 24px — nhãn dài ở bề rộng 70px sẽ thò ra khỏi khung bo.\r\n -->\r\n <div\r\n class=\"flex min-h-0 flex-1 flex-col gap-5 overflow-hidden rounded-t-2xl\"\r\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\r\n >\r\n <div class=\"flex items-center justify-center\">\r\n @if (logoDef(); as logo) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"logo.template\"\r\n [ngTemplateOutletContext]=\"{ $implicit: collapsed(), collapsed: collapsed() }\"\r\n />\r\n } @else if (logoSrc()) {\r\n <img\r\n [src]=\"collapsed() && logoMarkSrc() ? logoMarkSrc() : logoSrc()\"\r\n alt=\"Ôn Luyện\"\r\n [class]=\"collapsed() ? 'size-10 object-contain' : 'h-10 w-auto max-w-full object-contain'\"\r\n />\r\n } @else {\r\n <!--\r\n Logo của kit (Figma `Logo ol` #6603:1269). Thu gọn: bản VUÔNG 40px —\r\n bản dài rộng ~164px, ở khung 70px tràn hẳn ra ngoài. Mở: bản dài cao\r\n 32px (~164px, Figma app-nav đặt logo rộng 155px).\r\n -->\r\n @if (collapsed()) {\r\n <ol-logo variant=\"mark\" [tone]=\"logoTone()\" [height]=\"40\" />\r\n } @else {\r\n <ol-logo variant=\"full\" [tone]=\"logoTone()\" [height]=\"32\" />\r\n }\r\n }\r\n </div>\r\n\r\n @if (headerDef(); as header) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"header.template\"\r\n [ngTemplateOutletContext]=\"slotContext('rail')\"\r\n />\r\n }\r\n\r\n <!--\r\n Vùng mục CUỘN ĐƯỢC: menu nhiều nhóm (quản trị trường: 5 nhóm, menu con 3\r\n cấp) cao hơn màn hình. Logo và thẻ tài khoản đứng yên, chỉ danh sách cuộn.\r\n\r\n Thanh cuộn MẢNH, trắng mờ — KHÔNG ẩn hẳn: đo menu giáo viên (10 + 5 mục)\r\n ở màn cao 900px, 3 mục cuối nằm dưới mép mà không có dấu hiệu gì cho\r\n biết còn nữa. Thanh mặc định xám đặc thì chướng trên nền navy.\r\n -->\r\n <div\r\n class=\"-me-3 flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto pe-2 [scrollbar-color:rgb(255_255_255/0.3)_transparent] [scrollbar-width:thin]\"\r\n >\r\n @for (group of mainGroups(); track $index) {\r\n <div class=\"flex flex-col gap-1\">\r\n @if (group.label && !collapsed()) {\r\n <p class=\"px-2.5 pt-1 pb-0.5 text-xs font-bold uppercase tracking-wide text-white/70\">\r\n {{ group.label }}\r\n </p>\r\n }\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (footerDef(); as footer) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"footer.template\"\r\n [ngTemplateOutletContext]=\"slotContext('rail')\"\r\n />\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- ── Phần dưới: menu phụ + thẻ tài khoản ────────────────────────── -->\r\n <div\r\n class=\"flex shrink-0 flex-col gap-1 overflow-hidden rounded-b-2xl pt-2 pb-4\"\r\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\r\n >\r\n @if (showDivider()) {\r\n <!-- `aria-hidden`: đường kẻ là dấu hiệu nhìn, trình đọc màn hình không cần -->\r\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\r\n }\r\n\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: footerItems(), level: 1 }\"\r\n />\r\n\r\n <!--\r\n THẺ TÀI KHOẢN — bấm mở menu tài khoản (popup nổi `ol-dropdown`).\r\n\r\n Popup chứ không đổi danh sách tại chỗ: ở bề rộng 70px (thu gọn) không có\r\n chỗ cho ba dòng chữ; popup nổi ra ngoài menu thì đọc được ở cả hai trạng\r\n thái. Luôn mở SANG PHẢI, cạnh sidebar (`right-end`: mép dưới popup thẳng\r\n mép dưới thẻ, popup mọc lên trên vì thẻ nằm sát đáy) — người dùng yêu\r\n cầu 2026-10-06, bản trước mở lên trên đè lên chính menu khi mở rộng.\r\n\r\n `user.actions = []` thì không có popup — bấm thẻ chỉ phát `userClick`\r\n như các bản trước.\r\n\r\n `autoClose=\"outside\"`: bấm TRONG popup không tự đóng — ruột tuỳ biến có\r\n thể chứa điều khiển (bộ chọn Sáng/Tối) mà người dùng cần thấy kết quả.\r\n Mục hành động tự đóng qua `select()` / `close()` của ngữ cảnh.\r\n -->\r\n @if (config().user; as user) {\r\n <div class=\"mt-2 flex\" [class]=\"collapsed() ? 'justify-center' : ''\">\r\n @if (userActions().length > 0 || userMenuDef()) {\r\n <ol-dropdown\r\n class=\"min-w-0\"\r\n [class]=\"collapsed() ? '' : 'w-full'\"\r\n placement=\"right-end\"\r\n [offset]=\"userMenuOffset()\"\r\n menuClass=\"w-72\"\r\n autoClose=\"outside\"\r\n [(isOpen)]=\"userMenuOpen\"\r\n >\r\n <button\r\n trigger\r\n type=\"button\"\r\n [class]=\"userCardClasses()\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"userMenuOpen()\"\r\n [attr.aria-label]=\"collapsed() ? 'Tài khoản ' + user.name : null\"\r\n (click)=\"userClick.emit()\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: collapsed(),\r\n initials: userInitials(),\r\n variant: 'rail',\r\n }\"\r\n />\r\n </button>\r\n\r\n <!--\r\n Ruột popup chỉ dựng khi mở: `ol-dropdown` ẩn panel bằng class\r\n `hidden`, nên dựng sẵn thì tên + email nằm trong DOM cả khi menu\r\n thu gọn — trình đọc màn hình và tìm kiếm trang vẫn thấy chúng.\r\n -->\r\n @if (userMenuOpen()) {\r\n @if (userMenuDef(); as menu) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"menu.template\"\r\n [ngTemplateOutletContext]=\"userMenuPopupContext()\"\r\n />\r\n } @else {\r\n <ng-container [ngTemplateOutlet]=\"userMenuBody\" />\r\n }\r\n }\r\n </ol-dropdown>\r\n } @else {\r\n <button type=\"button\" [class]=\"userCardClasses()\" (click)=\"userClick.emit()\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: collapsed(),\r\n initials: userInitials(),\r\n variant: 'rail',\r\n }\"\r\n />\r\n </button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n</nav>\r\n\r\n<!--\r\n ── MOBILE: THANH ĐÁY + KHAY MENU (`layout=\"auto\"` dưới `md`, hoặc `\"mobile\"`) ──\r\n\r\n Chuyển từ phần tự dựng của onluyenv4 vào thư viện (2026-10-06):\r\n\r\n · thanh đáy `mobilePrimaryCount` mục đầu nhóm chính + nút Menu ở ô cuối\r\n · khay trượt lên từ đáy (nút mở nằm ở đáy phải — đi theo ngón tay), chứa\r\n thẻ tài khoản, các mục KHÔNG lên thanh đáy, và menu tài khoản\r\n · `fixed` theo màn hình; `pb-[env(safe-area-inset-bottom)]` chừa thanh vuốt iPhone\r\n\r\n Khay dùng `@if` chứ không ẩn bằng class: ẩn bằng `hidden` thì nút bên trong\r\n vẫn tab tới được dù không nhìn thấy. Lớp phủ là `<button>` để đóng được\r\n bằng bàn phím.\r\n-->\r\n@if (hasMobile()) {\r\n <nav\r\n class=\"fixed inset-x-0 bottom-0 z-50 flex h-[calc(3.5rem+env(safe-area-inset-bottom))] items-stretch border-t border-default bg-neutral-primary pb-[env(safe-area-inset-bottom)]\"\r\n [class]=\"mobileDisplay()\"\r\n aria-label=\"Điều hướng chính\"\r\n >\r\n @for (item of primaryItems(); track item.id) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"barItemClasses(item)\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n [attr.aria-label]=\"item.label\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? barBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'bar')\"\r\n />\r\n </a>\r\n }\r\n\r\n <button\r\n type=\"button\"\r\n class=\"flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 py-2 transition-colors\"\r\n [class]=\"mobileMenuOpen() ? 'text-fg-brand' : 'text-body-subtle hover:text-heading'\"\r\n [attr.aria-expanded]=\"mobileMenuOpen()\"\r\n [attr.aria-controls]=\"sheetId\"\r\n (click)=\"toggleMobileMenu()\"\r\n >\r\n <ol-icon name=\"solar:hamburger_menu_linear\" size=\"base\" />\r\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\r\n {{ mobileMenuLabel() }}\r\n </span>\r\n </button>\r\n </nav>\r\n\r\n <!--\r\n Khay và lớp phủ DỪNG trên thanh đáy (cao 56px + vùng vuốt iPhone): thanh\r\n đáy luôn thấy được và bấm được khi khay mở — nút \"Menu\" bấm lần nữa thì\r\n đóng, các mục khác chuyển trang luôn (người dùng yêu cầu 2026-10-06). Thanh\r\n đáy z-50, trên lớp phủ z-40.\r\n -->\r\n @if (mobileMenuOpen()) {\r\n <button\r\n type=\"button\"\r\n class=\"fixed inset-x-0 top-0 bottom-[calc(3.5rem+env(safe-area-inset-bottom))] z-40 cursor-default bg-black/40 transition-opacity duration-200 starting:opacity-0\"\r\n animate.leave=\"opacity-0\"\r\n [class]=\"mobileDisplay()\"\r\n aria-label=\"Đóng menu\"\r\n (click)=\"mobileMenuOpen.set(false)\"\r\n ></button>\r\n\r\n <!--\r\n Khung như bản cũ: kéo hết bề ngang, dính mép trên thanh đáy, bo 2 góc\r\n trên (người dùng 2026-10-06 — không thẻ nổi cách mép). Bên trong `px-2`\r\n để mỗi dòng bo tròn riêng. Mở: trượt LÊN từ đáy (`starting:` =\r\n `@starting-style`). Đóng: trượt XUỐNG — `animate.leave` gắn\r\n `translate-y-full opacity-0`, Angular chờ transition 200ms xong mới gỡ\r\n khỏi DOM (`@if` gỡ ngay thì không kịp chạy).\r\n\r\n Hai phần: ĐẦU đứng yên (vạch kéo, thẻ tài khoản + Đăng xuất, và phần `top`\r\n của menu tài khoản — Trang cá nhân, Đổi mật khẩu, Giao diện…), THÂN cuộn\r\n (khe, nhóm mục, phần `bottom`). Menu dài thì chỉ thân cuộn (người dùng\r\n 2026-10-06).\r\n -->\r\n <div\r\n [id]=\"sheetId\"\r\n class=\"fixed inset-x-0 bottom-[calc(3.5rem+env(safe-area-inset-bottom))] z-40 flex max-h-[calc(100dvh-3.5rem-env(safe-area-inset-bottom)-4rem)] flex-col rounded-t-lg bg-neutral-primary px-2 pb-2 shadow-2xl transition-[opacity,translate] duration-200 ease-out starting:translate-y-full starting:opacity-0\"\r\n animate.leave=\"translate-y-full opacity-0\"\r\n [class]=\"mobileDisplay()\"\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n [attr.aria-label]=\"mobileMenuLabel()\"\r\n >\r\n <div class=\"shrink-0\">\r\n <div class=\"flex justify-center pt-2.5\">\r\n <span class=\"h-1 w-10 rounded-full bg-neutral-tertiary\" aria-hidden=\"true\"></span>\r\n </div>\r\n\r\n @if (config().user; as user) {\r\n <div class=\"flex items-center gap-3 px-2 pt-2 pb-2\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"userDef()?.template ?? sheetUserBody\"\r\n [ngTemplateOutletContext]=\"{\r\n $implicit: user,\r\n collapsed: false,\r\n initials: userInitials(),\r\n variant: 'sheet',\r\n }\"\r\n />\r\n <!--\r\n `split`: mục danger (Đăng xuất) thành nút CHỈ ICON viền đỏ bên phải thẻ\r\n tài khoản — một chạm, không phải cuộn xuống cuối khay (người dùng\r\n 2026-10-06). Tên đọc + tooltip (nhấn giữ) từ `label`.\r\n -->\r\n @if (mobileUserMenu() === 'split') {\r\n @for (action of cardActions(); track action.id) {\r\n <ol-button\r\n class=\"ms-auto shrink-0\"\r\n color=\"danger\"\r\n [outline]=\"true\"\r\n size=\"sm\"\r\n [iconOnly]=\"true\"\r\n [ariaLabel]=\"action.label\"\r\n (btnClick)=\"onUserAction(action)\"\r\n >\r\n <ol-icon leftIcon [name]=\"action.icon\" size=\"base\" />\r\n </ol-button>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!--\r\n `mobileUserMenu=\"split\"`: việc thường của tài khoản (Trang cá nhân, Đổi\r\n mật khẩu, cài đặt…) lên ĐẦU khay, ngay dưới thẻ tài khoản.\r\n -->\r\n @if (mobileUserMenu() === 'split') {\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetAccount\"\r\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetTopContext() }\"\r\n />\r\n }\r\n </div>\r\n\r\n <div class=\"-mx-2 min-h-0 flex-1 overflow-y-auto overscroll-contain px-2\">\r\n <!--\r\n `empty:hidden`: template khe có thể không vẽ gì (bộ chọn vai chỉ hiện\r\n với tài khoản hai vai). Không có nó, khay còn một dải trống 24px kẹp\r\n giữa hai đường kẻ. `:empty` bỏ qua nút chú thích Angular chèn vào.\r\n -->\r\n @if (headerDef(); as header) {\r\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"header.template\"\r\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\r\n />\r\n </div>\r\n }\r\n\r\n @for (group of sheetGroups(); track $index) {\r\n <div class=\"mt-1 border-t border-default pt-1\">\r\n @if (group.label) {\r\n <p class=\"px-2 pt-2 pb-1 text-xs font-bold uppercase tracking-wide text-body-subtle\">\r\n {{ group.label }}\r\n </p>\r\n }\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\r\n />\r\n </div>\r\n }\r\n\r\n @if (footerDef(); as footer) {\r\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"footer.template\"\r\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\r\n />\r\n </div>\r\n }\r\n\r\n <!-- Menu tài khoản (cuối khay): trọn menu, hoặc chỉ phần `bottom` khi `mobileUserMenu=\"split\"` -->\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetAccount\"\r\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetContext() }\"\r\n />\r\n </div>\r\n </div>\r\n }\r\n}\r\n\r\n<!--\r\n Khối menu tài khoản trong khay — vẽ ở đầu (`part: 'top'`) và/hoặc cuối khay.\r\n Template tuỳ biến nếu có, không thì danh sách mục mặc định của phần đó.\r\n `empty:hidden`: phần không có mục nào thì không để lại dải kẻ.\r\n-->\r\n<ng-template #sheetAccount let-ctx>\r\n @if (ctx) {\r\n @if (userMenuDef(); as menu) {\r\n <!--\r\n `px-2`: ruột tuỳ biến (ol-dropdown-item…) không tự có lề ngang như dòng khay.\r\n `min-h-10` cho mục menu: ol-dropdown-item cao 36px, dòng khay 40px — ép\r\n bằng nhau để cả khay một nhịp.\r\n -->\r\n <div\r\n class=\"mt-1 border-t border-default pt-1 empty:hidden [&_[role=menuitem]]:min-h-10 [&_[role=menuitem]]:rounded-md\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"menu.template\" [ngTemplateOutletContext]=\"ctx\" />\r\n </div>\r\n } @else if (sheetActionsFor(ctx.part).length > 0) {\r\n <ul class=\"mt-1 border-t border-default pt-1\">\r\n @for (action of sheetActionsFor(ctx.part); track action.id) {\r\n @if (action.dividerBefore && !$first) {\r\n <li class=\"my-1 h-px bg-neutral-tertiary\" aria-hidden=\"true\"></li>\r\n }\r\n <li>\r\n @if (action.route) {\r\n <a\r\n [routerLink]=\"action.route\"\r\n [class]=\"sheetActionClasses(action)\"\r\n (click)=\"onUserAction(action)\"\r\n >\r\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\r\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"sheetActionClasses(action)\"\r\n (click)=\"onUserAction(action)\"\r\n >\r\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\r\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\r\n </button>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Cây mục (đệ quy) — cấp 1 · 2 · 3. Nhánh con chỉ vẽ khi mở và menu không\r\n thu gọn; thu gọn thì menu con hiện NỔI cạnh mục cha (xem `#navItem`).\r\n-->\r\n<ng-template #navTree let-items let-level=\"level\">\r\n <ul class=\"flex flex-col gap-1\" [class]=\"level === 1 ? '' : 'mt-1'\">\r\n @for (item of items; track item.id) {\r\n <li>\r\n <ng-container\r\n [ngTemplateOutlet]=\"navItem\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item, level: level }\"\r\n />\r\n @if (item.children?.length && isExpanded(item) && !collapsed() && level < 3) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"navTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\r\n />\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</ng-template>\r\n\r\n<!--\r\n Một mục menu. Ba loại hành vi, không gộp được:\r\n có con → `<button aria-expanded>` mở/đóng nhánh, không điều hướng\r\n có `route` → `<a routerLink>`; mục đang chọn tính bằng `isItemActive`\r\n (route · activeRoutes · activeId), không `routerLinkActive`\r\n còn lại → `<button>` chỉ phát `itemClick` (disabled lo trong class)\r\n-->\r\n<ng-template #navItem let-item let-level=\"level\">\r\n @if (item.children?.length && collapsed() && level === 1) {\r\n <!--\r\n THU GỌN + mục cha: rê chuột / bấm / Enter mở menu con NỔI bên phải\r\n (`ol-dropdown`). Không tooltip — đầu panel đã ghi tên mục.\r\n -->\r\n <div\r\n class=\"flex justify-center\"\r\n (mouseenter)=\"openFlyout(item.id)\"\r\n (mouseleave)=\"scheduleFlyoutClose()\"\r\n >\r\n <ol-dropdown\r\n placement=\"right\"\r\n menuClass=\"w-64\"\r\n [isOpen]=\"flyoutId() === item.id\"\r\n (isOpenChange)=\"onFlyoutOpenChange(item.id, $event)\"\r\n >\r\n <button\r\n trigger\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level)\"\r\n [attr.aria-label]=\"item.label\"\r\n aria-haspopup=\"menu\"\r\n [attr.aria-expanded]=\"flyoutId() === item.id\"\r\n [disabled]=\"item.disabled ?? false\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n\r\n @if (flyoutId() === item.id) {\r\n <div (mouseenter)=\"cancelFlyoutClose()\" (mouseleave)=\"scheduleFlyoutClose()\">\r\n <p class=\"px-2 pt-1 pb-1.5 text-xs font-bold uppercase tracking-wide text-body-subtle\">\r\n {{ item.label }}\r\n </p>\r\n <ng-container\r\n [ngTemplateOutlet]=\"flyoutTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: 2 }\"\r\n />\r\n </div>\r\n }\r\n </ol-dropdown>\r\n </div>\r\n } @else if (item.children?.length) {\r\n <button\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [attr.aria-expanded]=\"isExpanded(item)\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n } @else if (item.route) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"itemClasses(item, level)\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\r\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\r\n tooltipPosition=\"right\"\r\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\r\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\r\n />\r\n </button>\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Ruột menu nổi: con là liên kết (`ol-dropdown-item`); con có cháu thì thành\r\n nhãn nhóm, cháu thụt vào dưới nó. Mục đang chọn mang `selected`.\r\n-->\r\n<ng-template #flyoutTree let-items let-level=\"level\">\r\n @for (child of items; track child.id) {\r\n @if (child.children?.length) {\r\n <p class=\"px-2 pt-2 pb-1 text-sm font-semibold text-heading\" [class.ps-5]=\"level > 2\">\r\n {{ child.label }}\r\n </p>\r\n <div class=\"ps-3\">\r\n <ng-container\r\n [ngTemplateOutlet]=\"flyoutTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: child.children, level: level + 1 }\"\r\n />\r\n </div>\r\n } @else {\r\n <ol-dropdown-item\r\n [label]=\"child.label\"\r\n [routerLink]=\"child.route ?? null\"\r\n [selected]=\"isItemActive(child)\"\r\n [disabled]=\"child.disabled ?? false\"\r\n (itemClick)=\"onFlyoutSelect(child)\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!--\r\n Ruột một mục ở menu dọc: icon 24px (chỉ cấp 1) + nhãn + nhãn phụ + huy hiệu\r\n + mũi tên (mục cha). Nhãn ẩn khi thu gọn.\r\n-->\r\n<ng-template #itemBody let-item let-level=\"level\" let-expanded=\"expanded\">\r\n @if (level === 1) {\r\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\r\n }\r\n\r\n @if (!collapsed() || level > 1) {\r\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\r\n\r\n @if (item.extraText) {\r\n <span\r\n class=\"shrink-0 rounded-sm border border-white px-1 text-2xs leading-tight text-white\"\r\n >{{ item.extraText }}</span\r\n >\r\n }\r\n @if (item.badge) {\r\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\r\n }\r\n @if (item.children?.length) {\r\n <ol-icon\r\n name=\"alt_arrow_right\"\r\n size=\"xs\"\r\n class=\"shrink-0 text-white transition-transform duration-200\"\r\n [class.rotate-90]=\"expanded\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n\r\n<!-- Ruột một mục ở thanh đáy mobile: icon + nhãn ngắn (đọc nhãn đầy đủ qua aria-label) -->\r\n<ng-template #barBody let-item>\r\n <ol-icon [name]=\"item.icon\" size=\"base\" />\r\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\r\n {{ item.shortLabel ?? item.label }}\r\n </span>\r\n</ng-template>\r\n\r\n<!-- Cây mục trong khay mobile — cùng luật mở/đóng với menu dọc -->\r\n<ng-template #sheetTree let-items let-level=\"level\">\r\n <ul>\r\n @for (item of items; track item.id) {\r\n <li>\r\n @if (item.route && !item.children?.length) {\r\n <a\r\n [routerLink]=\"item.route\"\r\n [class]=\"sheetItemClasses(item, level)\"\r\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\r\n />\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n [class]=\"sheetItemClasses(item, level)\"\r\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item) : null\"\r\n [disabled]=\"item.disabled ?? false\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <ng-container\r\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\r\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\r\n />\r\n </button>\r\n }\r\n @if (item.children?.length && isExpanded(item) && level < 3) {\r\n <ng-container\r\n [ngTemplateOutlet]=\"sheetTree\"\r\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\r\n />\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</ng-template>\r\n\r\n<!-- Ruột một dòng trong khay mobile: nền sáng nên chữ theo token, không trắng -->\r\n<ng-template #sheetBody let-item let-level=\"level\" let-expanded=\"expanded\">\r\n @if (level === 1) {\r\n <ol-icon [name]=\"item.icon\" size=\"sm\" class=\"shrink-0\" />\r\n }\r\n <span class=\"min-w-0 flex-1 truncate\">{{ item.label }}</span>\r\n @if (item.extraText) {\r\n <span class=\"shrink-0 rounded-sm border border-default px-1 text-2xs leading-tight\">{{\r\n item.extraText\r\n }}</span>\r\n }\r\n @if (item.badge) {\r\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\r\n }\r\n @if (item.children?.length) {\r\n <ol-icon\r\n name=\"alt_arrow_right\"\r\n size=\"xs\"\r\n class=\"shrink-0 transition-transform duration-200\"\r\n [class.rotate-90]=\"expanded\"\r\n />\r\n }\r\n</ng-template>\r\n\r\n<!-- Ruột thẻ tài khoản ở đáy menu dọc: avatar + tên/email (ẩn khi thu gọn) + mũi tên -->\r\n<ng-template #userCardBody let-user>\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n @if (!collapsed()) {\r\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\r\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"truncate text-xs text-white/80\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n <!--\r\n Mũi tên CHỈ SANG PHẢI — menu tài khoản mở sang phải cạnh sidebar\r\n (`right-end`); mũi tên xuống của bản trước chỉ sai hướng. Rê chuột / đang\r\n mở thì nhích 2px sang phải báo hướng mở.\r\n -->\r\n <ol-icon\r\n name=\"alt_arrow_right\"\r\n size=\"xs\"\r\n class=\"shrink-0 text-white/80 transition-transform duration-200 group-hover/user:translate-x-0.5\"\r\n [class.translate-x-0.5]=\"userMenuOpen()\"\r\n />\r\n }\r\n</ng-template>\r\n\r\n<!-- Khối tài khoản đầu khay mobile -->\r\n<ng-template #sheetUserBody let-user>\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n</ng-template>\r\n\r\n<!--\r\n Ruột popup tài khoản mặc định: đầu là avatar + tên + email để người dùng\r\n biết đang ở tài khoản nào, dưới là các mục `userActions()`.\r\n-->\r\n<ng-template #userMenuBody>\r\n @if (config().user; as user) {\r\n <div class=\"flex items-center gap-3 px-2 py-1.5\">\r\n <ol-avatar\r\n size=\"base\"\r\n [src]=\"user.avatarSrc ?? ''\"\r\n [initials]=\"userInitials()\"\r\n [alt]=\"user.name\"\r\n class=\"shrink-0\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\r\n @if (user.subtitle) {\r\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\r\n }\r\n </span>\r\n </div>\r\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\r\n @for (action of userActions(); track action.id) {\r\n @if (action.dividerBefore && !$first) {\r\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\r\n }\r\n <ol-dropdown-item\r\n [icon]=\"action.icon\"\r\n [label]=\"action.label\"\r\n [routerLink]=\"action.route ?? null\"\r\n [danger]=\"action.danger ?? false\"\r\n (itemClick)=\"onUserAction(action)\"\r\n />\r\n }\r\n }\r\n</ng-template>\r\n"
|
|
27482
27754
|
}]
|
|
27483
27755
|
}],
|
|
27484
27756
|
ctorParameters: () => [],
|
|
@@ -27765,7 +28037,10 @@ const OL_APP_HEADER_TEMPLATES = [
|
|
|
27765
28037
|
OlAppHeaderContentDef
|
|
27766
28038
|
];
|
|
27767
28039
|
var OlAppHeader = class OlAppHeader {
|
|
27768
|
-
|
|
28040
|
+
constructor() {
|
|
28041
|
+
this.router = inject(Router, { optional: true });
|
|
28042
|
+
}
|
|
28043
|
+
router;
|
|
27769
28044
|
variant = input("plain", ...ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []);
|
|
27770
28045
|
size = input("base", ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
|
|
27771
28046
|
mobileSolid = input(true, ...ngDevMode ? [{ debugName: "mobileSolid" }] : /* istanbul ignore next */ []);
|
|
@@ -27957,6 +28232,10 @@ var OlAppHeader = class OlAppHeader {
|
|
|
27957
28232
|
search: "search"
|
|
27958
28233
|
},
|
|
27959
28234
|
host: { classAttribute: "contents" },
|
|
28235
|
+
providers: [provideOlIcons({
|
|
28236
|
+
"solar:arrow-left-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M15 6l-6 6l6 6\"/></svg>",
|
|
28237
|
+
"solar:magnifier-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\" d=\"m20 20l-3-3\"/></svg>"
|
|
28238
|
+
})],
|
|
27960
28239
|
queries: [
|
|
27961
28240
|
{
|
|
27962
28241
|
propertyName: "titleDef",
|
|
@@ -27981,7 +28260,7 @@ var OlAppHeader = class OlAppHeader {
|
|
|
27981
28260
|
}
|
|
27982
28261
|
],
|
|
27983
28262
|
ngImport: i0,
|
|
27984
|
-
template: "<!--\r\n Thanh trên của trang trong app. Khung (`headerClasses`) luôn của component;\r\n ruột mặc định là [←] [tiêu đề] [ô tìm] [hành động], hoặc trọn khe\r\n `olAppHeaderContent`.\r\n-->\r\n<header [class]=\"headerClasses()\">\r\n @if (contentDef(); as content) {\r\n <div class=\"flex min-w-0 flex-1 items-center gap-3\">\r\n <ng-container [ngTemplateOutlet]=\"content.template\" />\r\n </div>\r\n } @else {\r\n @if (hasBack()) {\r\n <ol-button\r\n class=\"shrink-0\"\r\n color=\"ghost\"\r\n [iconOnly]=\"true\"\r\n [ariaLabel]=\"backAriaLabel()\"\r\n (btnClick)=\"onBack()\"\r\n >\r\n <ol-icon leftIcon name=\"solar:
|
|
28263
|
+
template: "<!--\r\n Thanh trên của trang trong app. Khung (`headerClasses`) luôn của component;\r\n ruột mặc định là [←] [tiêu đề] [ô tìm] [hành động], hoặc trọn khe\r\n `olAppHeaderContent`.\r\n-->\r\n<header [class]=\"headerClasses()\">\r\n @if (contentDef(); as content) {\r\n <div class=\"flex min-w-0 flex-1 items-center gap-3\">\r\n <ng-container [ngTemplateOutlet]=\"content.template\" />\r\n </div>\r\n } @else {\r\n @if (hasBack()) {\r\n <ol-button\r\n class=\"shrink-0\"\r\n color=\"ghost\"\r\n [iconOnly]=\"true\"\r\n [ariaLabel]=\"backAriaLabel()\"\r\n (btnClick)=\"onBack()\"\r\n >\r\n <ol-icon leftIcon name=\"solar:arrow_left_linear\" size=\"base\" />\r\n </ol-button>\r\n }\r\n\r\n <div [class]=\"titleClasses()\">\r\n @if (titleDef(); as titleTpl) {\r\n <ng-container [ngTemplateOutlet]=\"titleTpl.template\" />\r\n } @else {\r\n @if (showBreadcrumbs()) {\r\n <!-- Đường dẫn: mục cuối là trang hiện tại; chữ xs để tiêu đề nổi hơn; ẩn dưới sm -->\r\n <ol-breadcrumb class=\"hidden sm:block [&_a]:text-xs [&_span]:text-xs\" separator=\"chevron\">\r\n @for (crumb of breadcrumbs(); track $index) {\r\n <ol-breadcrumb-item\r\n [label]=\"crumb.label\"\r\n [routerLink]=\"$last ? null : (crumb.link ?? null)\"\r\n [current]=\"$last\"\r\n />\r\n }\r\n </ol-breadcrumb>\r\n } @else if (subtitle()) {\r\n <p\r\n class=\"hidden truncate text-2xs font-medium uppercase tracking-wide text-body-subtle sm:block\"\r\n >\r\n {{ subtitle() }}\r\n </p>\r\n }\r\n @if (title()) {\r\n <h1 class=\"truncate text-base font-semibold leading-tight text-heading\">\r\n {{ title() }}\r\n </h1>\r\n }\r\n }\r\n </div>\r\n\r\n @if (showSearch()) {\r\n <!-- Ô tìm đầy đủ — từ mốc `searchCollapseAt`, giữa hàng, co theo chỗ -->\r\n <form [class]=\"searchInlineClasses()\" role=\"search\" (submit)=\"submitSearch($event)\">\r\n <ol-input\r\n class=\"block\"\r\n [ariaLabel]=\"searchAriaLabel()\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [rightIcon]=\"true\"\r\n [(value)]=\"searchValue\"\r\n >\r\n <ol-icon rightIcon name=\"solar:magnifier_linear\" size=\"base\" class=\"text-body-subtle\" />\r\n </ol-input>\r\n </form>\r\n }\r\n\r\n <div class=\"ms-auto flex shrink-0 items-center gap-2\">\r\n @if (showSearch()) {\r\n <!-- Dưới mốc: nút kính lúp mở ô tìm trong bảng thả xuống -->\r\n <ol-dropdown [class]=\"searchToggleClasses()\" placement=\"bottom-end\" menuClass=\"w-80\">\r\n <ol-button trigger color=\"ghost\" [iconOnly]=\"true\" [ariaLabel]=\"searchAriaLabel()\">\r\n <ol-icon leftIcon name=\"solar:magnifier_linear\" size=\"base\" />\r\n </ol-button>\r\n <form class=\"p-1\" role=\"search\" (submit)=\"submitSearch($event)\">\r\n <ol-input\r\n [ariaLabel]=\"searchAriaLabel()\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [(value)]=\"searchValue\"\r\n />\r\n </form>\r\n </ol-dropdown>\r\n }\r\n @if (actionsDef(); as actions) {\r\n <ng-container [ngTemplateOutlet]=\"actions.template\" />\r\n }\r\n </div>\r\n }\r\n</header>\r\n",
|
|
27985
28264
|
dependencies: [
|
|
27986
28265
|
{
|
|
27987
28266
|
kind: "directive",
|
|
@@ -28140,9 +28419,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28140
28419
|
],
|
|
28141
28420
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28142
28421
|
host: { class: "contents" },
|
|
28143
|
-
|
|
28422
|
+
providers: [provideOlIcons({
|
|
28423
|
+
"solar:arrow-left-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M15 6l-6 6l6 6\"/></svg>",
|
|
28424
|
+
"solar:magnifier-linear": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"11\" cy=\"11\" r=\"8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-width=\"1.5\" d=\"m20 20l-3-3\"/></svg>"
|
|
28425
|
+
})],
|
|
28426
|
+
template: "<!--\r\n Thanh trên của trang trong app. Khung (`headerClasses`) luôn của component;\r\n ruột mặc định là [←] [tiêu đề] [ô tìm] [hành động], hoặc trọn khe\r\n `olAppHeaderContent`.\r\n-->\r\n<header [class]=\"headerClasses()\">\r\n @if (contentDef(); as content) {\r\n <div class=\"flex min-w-0 flex-1 items-center gap-3\">\r\n <ng-container [ngTemplateOutlet]=\"content.template\" />\r\n </div>\r\n } @else {\r\n @if (hasBack()) {\r\n <ol-button\r\n class=\"shrink-0\"\r\n color=\"ghost\"\r\n [iconOnly]=\"true\"\r\n [ariaLabel]=\"backAriaLabel()\"\r\n (btnClick)=\"onBack()\"\r\n >\r\n <ol-icon leftIcon name=\"solar:arrow_left_linear\" size=\"base\" />\r\n </ol-button>\r\n }\r\n\r\n <div [class]=\"titleClasses()\">\r\n @if (titleDef(); as titleTpl) {\r\n <ng-container [ngTemplateOutlet]=\"titleTpl.template\" />\r\n } @else {\r\n @if (showBreadcrumbs()) {\r\n <!-- Đường dẫn: mục cuối là trang hiện tại; chữ xs để tiêu đề nổi hơn; ẩn dưới sm -->\r\n <ol-breadcrumb class=\"hidden sm:block [&_a]:text-xs [&_span]:text-xs\" separator=\"chevron\">\r\n @for (crumb of breadcrumbs(); track $index) {\r\n <ol-breadcrumb-item\r\n [label]=\"crumb.label\"\r\n [routerLink]=\"$last ? null : (crumb.link ?? null)\"\r\n [current]=\"$last\"\r\n />\r\n }\r\n </ol-breadcrumb>\r\n } @else if (subtitle()) {\r\n <p\r\n class=\"hidden truncate text-2xs font-medium uppercase tracking-wide text-body-subtle sm:block\"\r\n >\r\n {{ subtitle() }}\r\n </p>\r\n }\r\n @if (title()) {\r\n <h1 class=\"truncate text-base font-semibold leading-tight text-heading\">\r\n {{ title() }}\r\n </h1>\r\n }\r\n }\r\n </div>\r\n\r\n @if (showSearch()) {\r\n <!-- Ô tìm đầy đủ — từ mốc `searchCollapseAt`, giữa hàng, co theo chỗ -->\r\n <form [class]=\"searchInlineClasses()\" role=\"search\" (submit)=\"submitSearch($event)\">\r\n <ol-input\r\n class=\"block\"\r\n [ariaLabel]=\"searchAriaLabel()\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [rightIcon]=\"true\"\r\n [(value)]=\"searchValue\"\r\n >\r\n <ol-icon rightIcon name=\"solar:magnifier_linear\" size=\"base\" class=\"text-body-subtle\" />\r\n </ol-input>\r\n </form>\r\n }\r\n\r\n <div class=\"ms-auto flex shrink-0 items-center gap-2\">\r\n @if (showSearch()) {\r\n <!-- Dưới mốc: nút kính lúp mở ô tìm trong bảng thả xuống -->\r\n <ol-dropdown [class]=\"searchToggleClasses()\" placement=\"bottom-end\" menuClass=\"w-80\">\r\n <ol-button trigger color=\"ghost\" [iconOnly]=\"true\" [ariaLabel]=\"searchAriaLabel()\">\r\n <ol-icon leftIcon name=\"solar:magnifier_linear\" size=\"base\" />\r\n </ol-button>\r\n <form class=\"p-1\" role=\"search\" (submit)=\"submitSearch($event)\">\r\n <ol-input\r\n [ariaLabel]=\"searchAriaLabel()\"\r\n [placeholder]=\"searchPlaceholder()\"\r\n [(value)]=\"searchValue\"\r\n />\r\n </form>\r\n </ol-dropdown>\r\n }\r\n @if (actionsDef(); as actions) {\r\n <ng-container [ngTemplateOutlet]=\"actions.template\" />\r\n }\r\n </div>\r\n }\r\n</header>\r\n"
|
|
28144
28427
|
}]
|
|
28145
28428
|
}],
|
|
28429
|
+
ctorParameters: () => [],
|
|
28146
28430
|
propDecorators: {
|
|
28147
28431
|
variant: [{
|
|
28148
28432
|
type: i0.Input,
|
|
@@ -29039,7 +29323,7 @@ var OlSidebarSubmenu = class OlSidebarSubmenu {
|
|
|
29039
29323
|
},
|
|
29040
29324
|
outputs: { isOpen: "isOpenChange" },
|
|
29041
29325
|
ngImport: i0,
|
|
29042
|
-
template: "<div>\r\n <button\r\n type=\"button\"\r\n (click)=\"toggle()\"\r\n [class]=\"triggerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n >\r\n <div class=\"flex items-center gap-3 min-w-0\">\r\n @if (icon()) {\r\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\r\n }\r\n <span class=\"truncate\">{{ label() }}</span>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-2\">\r\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\r\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\r\n }\r\n <ol-icon\r\n name=\"
|
|
29326
|
+
template: "<div>\r\n <button\r\n type=\"button\"\r\n (click)=\"toggle()\"\r\n [class]=\"triggerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n >\r\n <div class=\"flex items-center gap-3 min-w-0\">\r\n @if (icon()) {\r\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\r\n }\r\n <span class=\"truncate\">{{ label() }}</span>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-2\">\r\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\r\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\r\n }\r\n <ol-icon\r\n name=\"chevron_right\"\r\n size=\"xs\"\r\n class=\"text-body-subtle transition-transform duration-200 shrink-0\"\r\n [class.rotate-90]=\"isOpen()\"\r\n ></ol-icon>\r\n </div>\r\n </button>\r\n\r\n <div\r\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\"\r\n [class.hidden]=\"!isOpen()\"\r\n >\r\n <ng-content />\r\n </div>\r\n</div>\r\n",
|
|
29043
29327
|
dependencies: [
|
|
29044
29328
|
{
|
|
29045
29329
|
kind: "ngmodule",
|
|
@@ -29101,7 +29385,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
29101
29385
|
OlBadge
|
|
29102
29386
|
],
|
|
29103
29387
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
29104
|
-
template: "<div>\r\n <button\r\n type=\"button\"\r\n (click)=\"toggle()\"\r\n [class]=\"triggerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n >\r\n <div class=\"flex items-center gap-3 min-w-0\">\r\n @if (icon()) {\r\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\r\n }\r\n <span class=\"truncate\">{{ label() }}</span>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-2\">\r\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\r\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\r\n }\r\n <ol-icon\r\n name=\"
|
|
29388
|
+
template: "<div>\r\n <button\r\n type=\"button\"\r\n (click)=\"toggle()\"\r\n [class]=\"triggerClasses()\"\r\n [attr.aria-expanded]=\"isOpen()\"\r\n >\r\n <div class=\"flex items-center gap-3 min-w-0\">\r\n @if (icon()) {\r\n <ol-icon [name]=\"icon()!\" size=\"base\" class=\"shrink-0 transition-colors\"></ol-icon>\r\n }\r\n <span class=\"truncate\">{{ label() }}</span>\r\n </div>\r\n\r\n <div class=\"flex items-center gap-2\">\r\n @if (badge() !== null && badge() !== undefined && badge() !== '') {\r\n <ol-badge [color]=\"badgeColor()\" size=\"xs\" shape=\"pill\">{{ badge() }}</ol-badge>\r\n }\r\n <ol-icon\r\n name=\"chevron_right\"\r\n size=\"xs\"\r\n class=\"text-body-subtle transition-transform duration-200 shrink-0\"\r\n [class.rotate-90]=\"isOpen()\"\r\n ></ol-icon>\r\n </div>\r\n </button>\r\n\r\n <div\r\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\"\r\n [class.hidden]=\"!isOpen()\"\r\n >\r\n <ng-content />\r\n </div>\r\n</div>\r\n"
|
|
29105
29389
|
}]
|
|
29106
29390
|
}],
|
|
29107
29391
|
propDecorators: {
|
|
@@ -29386,10 +29670,12 @@ const TRIGGER_COLOR_CLASSES = {
|
|
|
29386
29670
|
warning: "bg-yellow-400 text-black hover:bg-yellow-500 active:bg-yellow-600 focus:ring-yellow-300 dark:bg-yellow-500 dark:hover:bg-yellow-600 dark:focus:ring-yellow-800"
|
|
29387
29671
|
};
|
|
29388
29672
|
var OlSpeedDial = class OlSpeedDial {
|
|
29389
|
-
elementRef
|
|
29390
|
-
overlayContainer
|
|
29673
|
+
elementRef;
|
|
29674
|
+
overlayContainer;
|
|
29391
29675
|
releaseEscape = null;
|
|
29392
29676
|
constructor() {
|
|
29677
|
+
this.elementRef = inject(ElementRef);
|
|
29678
|
+
this.overlayContainer = inject(OlOverlayContainer);
|
|
29393
29679
|
effect(() => {
|
|
29394
29680
|
if (this.isOpen() && this.closeOnEscape()) this.releaseEscape ??= this.overlayContainer.register(() => this.close());
|
|
29395
29681
|
else {
|
|
@@ -30912,7 +31198,7 @@ var OlTabs = class OlTabs {
|
|
|
30912
31198
|
isSignal: true
|
|
30913
31199
|
}],
|
|
30914
31200
|
ngImport: i0,
|
|
30915
|
-
template: "<!--\r\n MỘT `<ng-content>` duy nhất cho mọi nhánh.\r\n\r\n Khai nhiều `<ng-content>` không `select` — kể cả ở các nhánh `@if` loại trừ\r\n nhau — thì Angular chỉ chiếu nội dung lồng vào MỘT chỗ, và thực tế là mất ở\r\n mọi nhánh. Hỏng im lặng: build xanh, khung vẫn vẽ, chỉ ruột trống. Gói vào một\r\n `ng-template` rồi mỗi nhánh chiếu lại bằng `ngTemplateOutlet`.\r\n-->\r\n<ng-template #noiDungLong><ng-content /></ng-template>\r\n\r\n<!-- 1. Mobile Dropdown View (Type=Default / Pills / Border bottom, Mobile=True - Figma #1149:80537, #1149:80539, #1149:80541) -->\r\n@if (isMobileDropdown()) {\r\n <div class=\"w-full max-w-md\">\r\n <div class=\"relative\">\r\n <!--\r\n `[ngModel]` chứ KHÔNG phải `[value]`: `[value]` trên <select> đặt thuộc\r\n tính DOM NGAY, lúc các <option> của @for chưa tồn tại, nên trình duyệt\r\n hoàn nguyên về mục đầu. Đo 2026-09-17 với `[selectedIndex]=\"2\"`: tab\r\n đang chọn là \"Ba\" mà ô chọn hiện \"Một\". Build xanh, không cảnh báo.\r\n\r\n Và `selectTab($event)` chứ không phải `.selectedIndex` — cái sau là vị\r\n trí trong danh sách DOM, trùng với chỉ số tab chỉ khi mọi tab đều được\r\n vẽ; lọc bớt một tab là lệch ngay. `[value]` của option mới là nguồn.\r\n -->\r\n <select\r\n [ngModel]=\"currentIndex()\"\r\n (ngModelChange)=\"selectTab(+$event)\"\r\n aria-label=\"Chọn tab\"\r\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\"\r\n >\r\n @for (item of items(); track item.tabId(); let i = $index) {\r\n <option [value]=\"i\" [disabled]=\"item.disabled()\">\r\n {{ item.displayLabel() }}\r\n </option>\r\n }\r\n </select>\r\n <div\r\n class=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-body-subtle\"\r\n >\r\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" />\r\n </div>\r\n </div>\r\n\r\n <!-- Tab Content Area -->\r\n <div class=\"pt-4 w-full\">\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n} @else {\r\n <!-- 2. Standard / Desktop / Vertical Stack View -->\r\n <div\r\n [class]=\"\r\n isVertical()\r\n ? 'flex flex-col md:flex-row gap-6 items-start w-full'\r\n : 'flex w-full flex-col min-h-0'\r\n \"\r\n >\r\n <!-- Tab Headers Navigation -->\r\n <div\r\n [class]=\"\r\n mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs w-full max-w-md'\r\n : mobile() && variant() === 'button-group'\r\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs divide-y divide-default overflow-hidden w-full max-w-md'\r\n : isVertical()\r\n ? variant() === 'vertical-list'\r\n ? 'w-full md:w-64 shrink-0'\r\n : 'w-full md:w-44 shrink-0'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs inline-flex max-w-full overflow-x-auto'\r\n : variant() === 'button-group'\r\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'\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? 'border-b border-default w-full'\r\n : 'w-full'\r\n \"\r\n >\r\n <div\r\n [class]=\"\r\n mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? 'flex flex-col gap-1 w-full'\r\n : mobile() && variant() === 'button-group'\r\n ? 'flex flex-col w-full'\r\n : isVertical()\r\n ? variant() === 'vertical-list'\r\n ? 'flex flex-col gap-1 p-2 bg-neutral-primary rounded-md border border-default shadow-xs'\r\n : 'flex flex-col gap-2.5'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? 'flex items-center gap-1 ' + (fullWidth() ? 'w-full' : '')\r\n : variant() === 'button-group'\r\n ? 'flex items-center ' + (fullWidth() ? 'w-full' : '')\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? 'flex items-center gap-6 overflow-x-auto -mb-px ' +\r\n (fullWidth() ? 'justify-between' : '')\r\n : 'flex items-center gap-6 overflow-x-auto ' +\r\n (fullWidth() ? 'justify-between' : '')\r\n \"\r\n role=\"tablist\"\r\n >\r\n @for (item of items(); track item.tabId(); let i = $index) {\r\n <button\r\n type=\"button\"\r\n role=\"tab\"\r\n [id]=\"item.tabId()\"\r\n [disabled]=\"item.disabled()\"\r\n [attr.aria-selected]=\"i === currentIndex()\"\r\n (click)=\"selectTab(i)\"\r\n [class]=\"\r\n 'inline-flex items-center font-medium transition-all duration-150 select-none whitespace-nowrap ' +\r\n sizeClasses() +\r\n ' ' +\r\n (fullWidth() || mobile() ? 'w-full justify-start ' : '') +\r\n (item.disabled() ? 'opacity-40 cursor-not-allowed ' : 'cursor-pointer ') +\r\n (mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\r\n : mobile() && variant() === 'button-group'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\r\n : variant() === 'default'\r\n ? i === currentIndex()\r\n ? 'text-fg-brand font-semibold'\r\n : 'text-body hover:text-heading'\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? i === currentIndex()\r\n ? 'border-b-2 border-brand text-fg-brand font-semibold pb-3 -mb-px'\r\n : 'border-b-2 border-transparent text-body hover:text-heading hover:border-default-strong pb-3 -mb-px'\r\n : variant() === 'pills'\r\n ? i === currentIndex()\r\n ? 'bg-brand text-white font-medium rounded-full shadow-xs'\r\n : 'text-body hover:bg-neutral-tertiary rounded-full'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\r\n : variant() === 'button-group'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\r\n : variant() === 'vertical-pills' || variant() === 'vertical'\r\n ? i === currentIndex()\r\n ? 'bg-brand text-white font-medium rounded-md shadow-xs justify-start w-full'\r\n : 'text-body hover:bg-neutral-tertiary rounded-md justify-start w-full'\r\n : variant() === 'vertical-list'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-fg-brand font-semibold rounded-md justify-between w-full'\r\n : 'text-body hover:bg-neutral-secondary-medium rounded-md justify-between w-full'\r\n : '')\r\n \"\r\n >\r\n <!-- User select format with Avatar (Figma #1149:80606) -->\r\n @if (item.avatar()) {\r\n <div class=\"inline-flex items-center gap-3 min-w-0 text-left\">\r\n <img\r\n [src]=\"item.avatar()!\"\r\n alt=\"\"\r\n class=\"w-8 h-8 rounded-full object-cover shrink-0 border border-default\"\r\n />\r\n <div class=\"flex flex-col min-w-0\">\r\n <span class=\"truncate font-medium text-xs leading-tight text-heading\">{{\r\n item.displayLabel()\r\n }}</span>\r\n <!--\r\n Đọc `displaySubtitle()` chứ không phải `item.helper()`:\r\n `subtitle` và `helper` là bí danh của nhau (chú thích ngay\r\n chỗ khai trong tab-item), nhưng render thẳng `helper` thì\r\n đặt `subtitle=\"...\"` không hiện gì.\r\n -->\r\n @if (item.displaySubtitle()) {\r\n <span class=\"truncate text-[11px] text-body-subtle font-normal leading-tight\">{{\r\n item.displaySubtitle()\r\n }}</span>\r\n }\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"inline-flex items-center gap-2 min-w-0\">\r\n @if (item.icon()) {\r\n <ol-icon [name]=\"item.icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\r\n }\r\n <span class=\"truncate\">{{ item.displayLabel() }}</span>\r\n </div>\r\n }\r\n\r\n @if (item.badge() !== null && item.badge() !== undefined && item.badge() !== '') {\r\n <ol-badge\r\n [color]=\"\r\n i === currentIndex() && (variant() === 'pills' || variant() === 'vertical-pills')\r\n ? 'white'\r\n : item.badgeColor()\r\n \"\r\n size=\"xs\"\r\n shape=\"pill\"\r\n class=\"ml-auto\"\r\n >\r\n {{ item.badge() }}\r\n </ol-badge>\r\n }\r\n\r\n @if (variant() === 'vertical-list' && !item.avatar()) {\r\n @if (i === currentIndex()) {\r\n <ol-icon\r\n name=\"check-circle\"\r\n size=\"xs\"\r\n class=\"text-fg-brand shrink-0 ml-auto\"\r\n ></ol-icon>\r\n } @else {\r\n <ol-icon\r\n name=\"alt-arrow-down\"\r\n size=\"xs\"\r\n class=\"text-body-subtle shrink-0 -rotate-90 ml-auto\"\r\n ></ol-icon>\r\n }\r\n }\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Tab Content Area -->\r\n <div\r\n [class]=\"isVertical() ? 'flex-1 min-w-0 w-full' : 'flex min-h-0 flex-1 flex-col pt-4 w-full'\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n}\r\n",
|
|
31201
|
+
template: "<!--\r\n MỘT `<ng-content>` duy nhất cho mọi nhánh.\r\n\r\n Khai nhiều `<ng-content>` không `select` — kể cả ở các nhánh `@if` loại trừ\r\n nhau — thì Angular chỉ chiếu nội dung lồng vào MỘT chỗ, và thực tế là mất ở\r\n mọi nhánh. Hỏng im lặng: build xanh, khung vẫn vẽ, chỉ ruột trống. Gói vào một\r\n `ng-template` rồi mỗi nhánh chiếu lại bằng `ngTemplateOutlet`.\r\n-->\r\n<ng-template #noiDungLong><ng-content /></ng-template>\r\n\r\n<!-- 1. Mobile Dropdown View (Type=Default / Pills / Border bottom, Mobile=True - Figma #1149:80537, #1149:80539, #1149:80541) -->\r\n@if (isMobileDropdown()) {\r\n <div class=\"w-full max-w-md\">\r\n <div class=\"relative\">\r\n <!--\r\n `[ngModel]` chứ KHÔNG phải `[value]`: `[value]` trên <select> đặt thuộc\r\n tính DOM NGAY, lúc các <option> của @for chưa tồn tại, nên trình duyệt\r\n hoàn nguyên về mục đầu. Đo 2026-09-17 với `[selectedIndex]=\"2\"`: tab\r\n đang chọn là \"Ba\" mà ô chọn hiện \"Một\". Build xanh, không cảnh báo.\r\n\r\n Và `selectTab($event)` chứ không phải `.selectedIndex` — cái sau là vị\r\n trí trong danh sách DOM, trùng với chỉ số tab chỉ khi mọi tab đều được\r\n vẽ; lọc bớt một tab là lệch ngay. `[value]` của option mới là nguồn.\r\n -->\r\n <select\r\n [ngModel]=\"currentIndex()\"\r\n (ngModelChange)=\"selectTab(+$event)\"\r\n aria-label=\"Chọn tab\"\r\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\"\r\n >\r\n @for (item of items(); track item.tabId(); let i = $index) {\r\n <option [value]=\"i\" [disabled]=\"item.disabled()\">\r\n {{ item.displayLabel() }}\r\n </option>\r\n }\r\n </select>\r\n <div\r\n class=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-body-subtle\"\r\n >\r\n <ol-icon name=\"alt_arrow_down\" size=\"xs\" />\r\n </div>\r\n </div>\r\n\r\n <!-- Tab Content Area -->\r\n <div class=\"pt-4 w-full\">\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n} @else {\r\n <!-- 2. Standard / Desktop / Vertical Stack View -->\r\n <div\r\n [class]=\"\r\n isVertical()\r\n ? 'flex flex-col md:flex-row gap-6 items-start w-full'\r\n : 'flex w-full flex-col min-h-0'\r\n \"\r\n >\r\n <!-- Tab Headers Navigation -->\r\n <div\r\n [class]=\"\r\n mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs w-full max-w-md'\r\n : mobile() && variant() === 'button-group'\r\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs divide-y divide-default overflow-hidden w-full max-w-md'\r\n : isVertical()\r\n ? variant() === 'vertical-list'\r\n ? 'w-full md:w-64 shrink-0'\r\n : 'w-full md:w-44 shrink-0'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs inline-flex max-w-full overflow-x-auto'\r\n : variant() === 'button-group'\r\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'\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? 'border-b border-default w-full'\r\n : 'w-full'\r\n \"\r\n >\r\n <div\r\n [class]=\"\r\n mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? 'flex flex-col gap-1 w-full'\r\n : mobile() && variant() === 'button-group'\r\n ? 'flex flex-col w-full'\r\n : isVertical()\r\n ? variant() === 'vertical-list'\r\n ? 'flex flex-col gap-1 p-2 bg-neutral-primary rounded-md border border-default shadow-xs'\r\n : 'flex flex-col gap-2.5'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? 'flex items-center gap-1 ' + (fullWidth() ? 'w-full' : '')\r\n : variant() === 'button-group'\r\n ? 'flex items-center ' + (fullWidth() ? 'w-full' : '')\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? 'flex items-center gap-6 overflow-x-auto -mb-px ' +\r\n (fullWidth() ? 'justify-between' : '')\r\n : 'flex items-center gap-6 overflow-x-auto ' +\r\n (fullWidth() ? 'justify-between' : '')\r\n \"\r\n role=\"tablist\"\r\n >\r\n @for (item of items(); track item.tabId(); let i = $index) {\r\n <button\r\n type=\"button\"\r\n role=\"tab\"\r\n [id]=\"item.tabId()\"\r\n [disabled]=\"item.disabled()\"\r\n [attr.aria-selected]=\"i === currentIndex()\"\r\n (click)=\"selectTab(i)\"\r\n [class]=\"\r\n 'inline-flex items-center font-medium transition-all duration-150 select-none whitespace-nowrap ' +\r\n sizeClasses() +\r\n ' ' +\r\n (fullWidth() || mobile() ? 'w-full justify-start ' : '') +\r\n (item.disabled() ? 'opacity-40 cursor-not-allowed ' : 'cursor-pointer ') +\r\n (mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\r\n : mobile() && variant() === 'button-group'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\r\n : variant() === 'default'\r\n ? i === currentIndex()\r\n ? 'text-fg-brand font-semibold'\r\n : 'text-body hover:text-heading'\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? i === currentIndex()\r\n ? 'border-b-2 border-brand text-fg-brand font-semibold pb-3 -mb-px'\r\n : 'border-b-2 border-transparent text-body hover:text-heading hover:border-default-strong pb-3 -mb-px'\r\n : variant() === 'pills'\r\n ? i === currentIndex()\r\n ? 'bg-brand text-white font-medium rounded-full shadow-xs'\r\n : 'text-body hover:bg-neutral-tertiary rounded-full'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\r\n : variant() === 'button-group'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\r\n : variant() === 'vertical-pills' || variant() === 'vertical'\r\n ? i === currentIndex()\r\n ? 'bg-brand text-white font-medium rounded-md shadow-xs justify-start w-full'\r\n : 'text-body hover:bg-neutral-tertiary rounded-md justify-start w-full'\r\n : variant() === 'vertical-list'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-fg-brand font-semibold rounded-md justify-between w-full'\r\n : 'text-body hover:bg-neutral-secondary-medium rounded-md justify-between w-full'\r\n : '')\r\n \"\r\n >\r\n <!-- User select format with Avatar (Figma #1149:80606) -->\r\n @if (item.avatar()) {\r\n <div class=\"inline-flex items-center gap-3 min-w-0 text-left\">\r\n <img\r\n [src]=\"item.avatar()!\"\r\n alt=\"\"\r\n class=\"w-8 h-8 rounded-full object-cover shrink-0 border border-default\"\r\n />\r\n <div class=\"flex flex-col min-w-0\">\r\n <span class=\"truncate font-medium text-xs leading-tight text-heading\">{{\r\n item.displayLabel()\r\n }}</span>\r\n <!--\r\n Đọc `displaySubtitle()` chứ không phải `item.helper()`:\r\n `subtitle` và `helper` là bí danh của nhau (chú thích ngay\r\n chỗ khai trong tab-item), nhưng render thẳng `helper` thì\r\n đặt `subtitle=\"...\"` không hiện gì.\r\n -->\r\n @if (item.displaySubtitle()) {\r\n <span class=\"truncate text-[11px] text-body-subtle font-normal leading-tight\">{{\r\n item.displaySubtitle()\r\n }}</span>\r\n }\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"inline-flex items-center gap-2 min-w-0\">\r\n @if (item.icon()) {\r\n <ol-icon [name]=\"item.icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\r\n }\r\n <span class=\"truncate\">{{ item.displayLabel() }}</span>\r\n </div>\r\n }\r\n\r\n @if (item.badge() !== null && item.badge() !== undefined && item.badge() !== '') {\r\n <ol-badge\r\n [color]=\"\r\n i === currentIndex() && (variant() === 'pills' || variant() === 'vertical-pills')\r\n ? 'white'\r\n : item.badgeColor()\r\n \"\r\n size=\"xs\"\r\n shape=\"pill\"\r\n class=\"ml-auto\"\r\n >\r\n {{ item.badge() }}\r\n </ol-badge>\r\n }\r\n\r\n @if (variant() === 'vertical-list' && !item.avatar()) {\r\n @if (i === currentIndex()) {\r\n <ol-icon\r\n name=\"check_circle\"\r\n size=\"xs\"\r\n class=\"text-fg-brand shrink-0 ml-auto\"\r\n ></ol-icon>\r\n } @else {\r\n <ol-icon\r\n name=\"alt_arrow_down\"\r\n size=\"xs\"\r\n class=\"text-body-subtle shrink-0 -rotate-90 ml-auto\"\r\n ></ol-icon>\r\n }\r\n }\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Tab Content Area -->\r\n <div\r\n [class]=\"isVertical() ? 'flex-1 min-w-0 w-full' : 'flex min-h-0 flex-1 flex-col pt-4 w-full'\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n}\r\n",
|
|
30916
31202
|
dependencies: [
|
|
30917
31203
|
{
|
|
30918
31204
|
kind: "ngmodule",
|
|
@@ -31027,7 +31313,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
31027
31313
|
],
|
|
31028
31314
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
31029
31315
|
host: { class: "block w-full" },
|
|
31030
|
-
template: "<!--\r\n MỘT `<ng-content>` duy nhất cho mọi nhánh.\r\n\r\n Khai nhiều `<ng-content>` không `select` — kể cả ở các nhánh `@if` loại trừ\r\n nhau — thì Angular chỉ chiếu nội dung lồng vào MỘT chỗ, và thực tế là mất ở\r\n mọi nhánh. Hỏng im lặng: build xanh, khung vẫn vẽ, chỉ ruột trống. Gói vào một\r\n `ng-template` rồi mỗi nhánh chiếu lại bằng `ngTemplateOutlet`.\r\n-->\r\n<ng-template #noiDungLong><ng-content /></ng-template>\r\n\r\n<!-- 1. Mobile Dropdown View (Type=Default / Pills / Border bottom, Mobile=True - Figma #1149:80537, #1149:80539, #1149:80541) -->\r\n@if (isMobileDropdown()) {\r\n <div class=\"w-full max-w-md\">\r\n <div class=\"relative\">\r\n <!--\r\n `[ngModel]` chứ KHÔNG phải `[value]`: `[value]` trên <select> đặt thuộc\r\n tính DOM NGAY, lúc các <option> của @for chưa tồn tại, nên trình duyệt\r\n hoàn nguyên về mục đầu. Đo 2026-09-17 với `[selectedIndex]=\"2\"`: tab\r\n đang chọn là \"Ba\" mà ô chọn hiện \"Một\". Build xanh, không cảnh báo.\r\n\r\n Và `selectTab($event)` chứ không phải `.selectedIndex` — cái sau là vị\r\n trí trong danh sách DOM, trùng với chỉ số tab chỉ khi mọi tab đều được\r\n vẽ; lọc bớt một tab là lệch ngay. `[value]` của option mới là nguồn.\r\n -->\r\n <select\r\n [ngModel]=\"currentIndex()\"\r\n (ngModelChange)=\"selectTab(+$event)\"\r\n aria-label=\"Chọn tab\"\r\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\"\r\n >\r\n @for (item of items(); track item.tabId(); let i = $index) {\r\n <option [value]=\"i\" [disabled]=\"item.disabled()\">\r\n {{ item.displayLabel() }}\r\n </option>\r\n }\r\n </select>\r\n <div\r\n class=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-body-subtle\"\r\n >\r\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" />\r\n </div>\r\n </div>\r\n\r\n <!-- Tab Content Area -->\r\n <div class=\"pt-4 w-full\">\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n} @else {\r\n <!-- 2. Standard / Desktop / Vertical Stack View -->\r\n <div\r\n [class]=\"\r\n isVertical()\r\n ? 'flex flex-col md:flex-row gap-6 items-start w-full'\r\n : 'flex w-full flex-col min-h-0'\r\n \"\r\n >\r\n <!-- Tab Headers Navigation -->\r\n <div\r\n [class]=\"\r\n mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs w-full max-w-md'\r\n : mobile() && variant() === 'button-group'\r\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs divide-y divide-default overflow-hidden w-full max-w-md'\r\n : isVertical()\r\n ? variant() === 'vertical-list'\r\n ? 'w-full md:w-64 shrink-0'\r\n : 'w-full md:w-44 shrink-0'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs inline-flex max-w-full overflow-x-auto'\r\n : variant() === 'button-group'\r\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'\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? 'border-b border-default w-full'\r\n : 'w-full'\r\n \"\r\n >\r\n <div\r\n [class]=\"\r\n mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? 'flex flex-col gap-1 w-full'\r\n : mobile() && variant() === 'button-group'\r\n ? 'flex flex-col w-full'\r\n : isVertical()\r\n ? variant() === 'vertical-list'\r\n ? 'flex flex-col gap-1 p-2 bg-neutral-primary rounded-md border border-default shadow-xs'\r\n : 'flex flex-col gap-2.5'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? 'flex items-center gap-1 ' + (fullWidth() ? 'w-full' : '')\r\n : variant() === 'button-group'\r\n ? 'flex items-center ' + (fullWidth() ? 'w-full' : '')\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? 'flex items-center gap-6 overflow-x-auto -mb-px ' +\r\n (fullWidth() ? 'justify-between' : '')\r\n : 'flex items-center gap-6 overflow-x-auto ' +\r\n (fullWidth() ? 'justify-between' : '')\r\n \"\r\n role=\"tablist\"\r\n >\r\n @for (item of items(); track item.tabId(); let i = $index) {\r\n <button\r\n type=\"button\"\r\n role=\"tab\"\r\n [id]=\"item.tabId()\"\r\n [disabled]=\"item.disabled()\"\r\n [attr.aria-selected]=\"i === currentIndex()\"\r\n (click)=\"selectTab(i)\"\r\n [class]=\"\r\n 'inline-flex items-center font-medium transition-all duration-150 select-none whitespace-nowrap ' +\r\n sizeClasses() +\r\n ' ' +\r\n (fullWidth() || mobile() ? 'w-full justify-start ' : '') +\r\n (item.disabled() ? 'opacity-40 cursor-not-allowed ' : 'cursor-pointer ') +\r\n (mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\r\n : mobile() && variant() === 'button-group'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\r\n : variant() === 'default'\r\n ? i === currentIndex()\r\n ? 'text-fg-brand font-semibold'\r\n : 'text-body hover:text-heading'\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? i === currentIndex()\r\n ? 'border-b-2 border-brand text-fg-brand font-semibold pb-3 -mb-px'\r\n : 'border-b-2 border-transparent text-body hover:text-heading hover:border-default-strong pb-3 -mb-px'\r\n : variant() === 'pills'\r\n ? i === currentIndex()\r\n ? 'bg-brand text-white font-medium rounded-full shadow-xs'\r\n : 'text-body hover:bg-neutral-tertiary rounded-full'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\r\n : variant() === 'button-group'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\r\n : variant() === 'vertical-pills' || variant() === 'vertical'\r\n ? i === currentIndex()\r\n ? 'bg-brand text-white font-medium rounded-md shadow-xs justify-start w-full'\r\n : 'text-body hover:bg-neutral-tertiary rounded-md justify-start w-full'\r\n : variant() === 'vertical-list'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-fg-brand font-semibold rounded-md justify-between w-full'\r\n : 'text-body hover:bg-neutral-secondary-medium rounded-md justify-between w-full'\r\n : '')\r\n \"\r\n >\r\n <!-- User select format with Avatar (Figma #1149:80606) -->\r\n @if (item.avatar()) {\r\n <div class=\"inline-flex items-center gap-3 min-w-0 text-left\">\r\n <img\r\n [src]=\"item.avatar()!\"\r\n alt=\"\"\r\n class=\"w-8 h-8 rounded-full object-cover shrink-0 border border-default\"\r\n />\r\n <div class=\"flex flex-col min-w-0\">\r\n <span class=\"truncate font-medium text-xs leading-tight text-heading\">{{\r\n item.displayLabel()\r\n }}</span>\r\n <!--\r\n Đọc `displaySubtitle()` chứ không phải `item.helper()`:\r\n `subtitle` và `helper` là bí danh của nhau (chú thích ngay\r\n chỗ khai trong tab-item), nhưng render thẳng `helper` thì\r\n đặt `subtitle=\"...\"` không hiện gì.\r\n -->\r\n @if (item.displaySubtitle()) {\r\n <span class=\"truncate text-[11px] text-body-subtle font-normal leading-tight\">{{\r\n item.displaySubtitle()\r\n }}</span>\r\n }\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"inline-flex items-center gap-2 min-w-0\">\r\n @if (item.icon()) {\r\n <ol-icon [name]=\"item.icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\r\n }\r\n <span class=\"truncate\">{{ item.displayLabel() }}</span>\r\n </div>\r\n }\r\n\r\n @if (item.badge() !== null && item.badge() !== undefined && item.badge() !== '') {\r\n <ol-badge\r\n [color]=\"\r\n i === currentIndex() && (variant() === 'pills' || variant() === 'vertical-pills')\r\n ? 'white'\r\n : item.badgeColor()\r\n \"\r\n size=\"xs\"\r\n shape=\"pill\"\r\n class=\"ml-auto\"\r\n >\r\n {{ item.badge() }}\r\n </ol-badge>\r\n }\r\n\r\n @if (variant() === 'vertical-list' && !item.avatar()) {\r\n @if (i === currentIndex()) {\r\n <ol-icon\r\n name=\"check-circle\"\r\n size=\"xs\"\r\n class=\"text-fg-brand shrink-0 ml-auto\"\r\n ></ol-icon>\r\n } @else {\r\n <ol-icon\r\n name=\"alt-arrow-down\"\r\n size=\"xs\"\r\n class=\"text-body-subtle shrink-0 -rotate-90 ml-auto\"\r\n ></ol-icon>\r\n }\r\n }\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Tab Content Area -->\r\n <div\r\n [class]=\"isVertical() ? 'flex-1 min-w-0 w-full' : 'flex min-h-0 flex-1 flex-col pt-4 w-full'\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n}\r\n"
|
|
31316
|
+
template: "<!--\r\n MỘT `<ng-content>` duy nhất cho mọi nhánh.\r\n\r\n Khai nhiều `<ng-content>` không `select` — kể cả ở các nhánh `@if` loại trừ\r\n nhau — thì Angular chỉ chiếu nội dung lồng vào MỘT chỗ, và thực tế là mất ở\r\n mọi nhánh. Hỏng im lặng: build xanh, khung vẫn vẽ, chỉ ruột trống. Gói vào một\r\n `ng-template` rồi mỗi nhánh chiếu lại bằng `ngTemplateOutlet`.\r\n-->\r\n<ng-template #noiDungLong><ng-content /></ng-template>\r\n\r\n<!-- 1. Mobile Dropdown View (Type=Default / Pills / Border bottom, Mobile=True - Figma #1149:80537, #1149:80539, #1149:80541) -->\r\n@if (isMobileDropdown()) {\r\n <div class=\"w-full max-w-md\">\r\n <div class=\"relative\">\r\n <!--\r\n `[ngModel]` chứ KHÔNG phải `[value]`: `[value]` trên <select> đặt thuộc\r\n tính DOM NGAY, lúc các <option> của @for chưa tồn tại, nên trình duyệt\r\n hoàn nguyên về mục đầu. Đo 2026-09-17 với `[selectedIndex]=\"2\"`: tab\r\n đang chọn là \"Ba\" mà ô chọn hiện \"Một\". Build xanh, không cảnh báo.\r\n\r\n Và `selectTab($event)` chứ không phải `.selectedIndex` — cái sau là vị\r\n trí trong danh sách DOM, trùng với chỉ số tab chỉ khi mọi tab đều được\r\n vẽ; lọc bớt một tab là lệch ngay. `[value]` của option mới là nguồn.\r\n -->\r\n <select\r\n [ngModel]=\"currentIndex()\"\r\n (ngModelChange)=\"selectTab(+$event)\"\r\n aria-label=\"Chọn tab\"\r\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\"\r\n >\r\n @for (item of items(); track item.tabId(); let i = $index) {\r\n <option [value]=\"i\" [disabled]=\"item.disabled()\">\r\n {{ item.displayLabel() }}\r\n </option>\r\n }\r\n </select>\r\n <div\r\n class=\"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3 text-body-subtle\"\r\n >\r\n <ol-icon name=\"alt_arrow_down\" size=\"xs\" />\r\n </div>\r\n </div>\r\n\r\n <!-- Tab Content Area -->\r\n <div class=\"pt-4 w-full\">\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n} @else {\r\n <!-- 2. Standard / Desktop / Vertical Stack View -->\r\n <div\r\n [class]=\"\r\n isVertical()\r\n ? 'flex flex-col md:flex-row gap-6 items-start w-full'\r\n : 'flex w-full flex-col min-h-0'\r\n \"\r\n >\r\n <!-- Tab Headers Navigation -->\r\n <div\r\n [class]=\"\r\n mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs w-full max-w-md'\r\n : mobile() && variant() === 'button-group'\r\n ? 'bg-neutral-primary rounded-md border border-default shadow-xs divide-y divide-default overflow-hidden w-full max-w-md'\r\n : isVertical()\r\n ? variant() === 'vertical-list'\r\n ? 'w-full md:w-64 shrink-0'\r\n : 'w-full md:w-44 shrink-0'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? 'p-3 bg-neutral-primary rounded-md border border-default shadow-xs inline-flex max-w-full overflow-x-auto'\r\n : variant() === 'button-group'\r\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'\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? 'border-b border-default w-full'\r\n : 'w-full'\r\n \"\r\n >\r\n <div\r\n [class]=\"\r\n mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? 'flex flex-col gap-1 w-full'\r\n : mobile() && variant() === 'button-group'\r\n ? 'flex flex-col w-full'\r\n : isVertical()\r\n ? variant() === 'vertical-list'\r\n ? 'flex flex-col gap-1 p-2 bg-neutral-primary rounded-md border border-default shadow-xs'\r\n : 'flex flex-col gap-2.5'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? 'flex items-center gap-1 ' + (fullWidth() ? 'w-full' : '')\r\n : variant() === 'button-group'\r\n ? 'flex items-center ' + (fullWidth() ? 'w-full' : '')\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? 'flex items-center gap-6 overflow-x-auto -mb-px ' +\r\n (fullWidth() ? 'justify-between' : '')\r\n : 'flex items-center gap-6 overflow-x-auto ' +\r\n (fullWidth() ? 'justify-between' : '')\r\n \"\r\n role=\"tablist\"\r\n >\r\n @for (item of items(); track item.tabId(); let i = $index) {\r\n <button\r\n type=\"button\"\r\n role=\"tab\"\r\n [id]=\"item.tabId()\"\r\n [disabled]=\"item.disabled()\"\r\n [attr.aria-selected]=\"i === currentIndex()\"\r\n (click)=\"selectTab(i)\"\r\n [class]=\"\r\n 'inline-flex items-center font-medium transition-all duration-150 select-none whitespace-nowrap ' +\r\n sizeClasses() +\r\n ' ' +\r\n (fullWidth() || mobile() ? 'w-full justify-start ' : '') +\r\n (item.disabled() ? 'opacity-40 cursor-not-allowed ' : 'cursor-pointer ') +\r\n (mobile() && (variant() === 'button' || variant() === 'boxed')\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\r\n : mobile() && variant() === 'button-group'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\r\n : variant() === 'default'\r\n ? i === currentIndex()\r\n ? 'text-fg-brand font-semibold'\r\n : 'text-body hover:text-heading'\r\n : variant() === 'border-bottom' || variant() === 'underline'\r\n ? i === currentIndex()\r\n ? 'border-b-2 border-brand text-fg-brand font-semibold pb-3 -mb-px'\r\n : 'border-b-2 border-transparent text-body hover:text-heading hover:border-default-strong pb-3 -mb-px'\r\n : variant() === 'pills'\r\n ? i === currentIndex()\r\n ? 'bg-brand text-white font-medium rounded-full shadow-xs'\r\n : 'text-body hover:bg-neutral-tertiary rounded-full'\r\n : variant() === 'button' || variant() === 'boxed'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium rounded-md shadow-2xs'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium rounded-md'\r\n : variant() === 'button-group'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-heading font-medium'\r\n : 'text-body hover:text-heading hover:bg-neutral-secondary-medium'\r\n : variant() === 'vertical-pills' || variant() === 'vertical'\r\n ? i === currentIndex()\r\n ? 'bg-brand text-white font-medium rounded-md shadow-xs justify-start w-full'\r\n : 'text-body hover:bg-neutral-tertiary rounded-md justify-start w-full'\r\n : variant() === 'vertical-list'\r\n ? i === currentIndex()\r\n ? 'bg-neutral-secondary-medium text-fg-brand font-semibold rounded-md justify-between w-full'\r\n : 'text-body hover:bg-neutral-secondary-medium rounded-md justify-between w-full'\r\n : '')\r\n \"\r\n >\r\n <!-- User select format with Avatar (Figma #1149:80606) -->\r\n @if (item.avatar()) {\r\n <div class=\"inline-flex items-center gap-3 min-w-0 text-left\">\r\n <img\r\n [src]=\"item.avatar()!\"\r\n alt=\"\"\r\n class=\"w-8 h-8 rounded-full object-cover shrink-0 border border-default\"\r\n />\r\n <div class=\"flex flex-col min-w-0\">\r\n <span class=\"truncate font-medium text-xs leading-tight text-heading\">{{\r\n item.displayLabel()\r\n }}</span>\r\n <!--\r\n Đọc `displaySubtitle()` chứ không phải `item.helper()`:\r\n `subtitle` và `helper` là bí danh của nhau (chú thích ngay\r\n chỗ khai trong tab-item), nhưng render thẳng `helper` thì\r\n đặt `subtitle=\"...\"` không hiện gì.\r\n -->\r\n @if (item.displaySubtitle()) {\r\n <span class=\"truncate text-[11px] text-body-subtle font-normal leading-tight\">{{\r\n item.displaySubtitle()\r\n }}</span>\r\n }\r\n </div>\r\n </div>\r\n } @else {\r\n <div class=\"inline-flex items-center gap-2 min-w-0\">\r\n @if (item.icon()) {\r\n <ol-icon [name]=\"item.icon()!\" size=\"sm\" class=\"shrink-0\"></ol-icon>\r\n }\r\n <span class=\"truncate\">{{ item.displayLabel() }}</span>\r\n </div>\r\n }\r\n\r\n @if (item.badge() !== null && item.badge() !== undefined && item.badge() !== '') {\r\n <ol-badge\r\n [color]=\"\r\n i === currentIndex() && (variant() === 'pills' || variant() === 'vertical-pills')\r\n ? 'white'\r\n : item.badgeColor()\r\n \"\r\n size=\"xs\"\r\n shape=\"pill\"\r\n class=\"ml-auto\"\r\n >\r\n {{ item.badge() }}\r\n </ol-badge>\r\n }\r\n\r\n @if (variant() === 'vertical-list' && !item.avatar()) {\r\n @if (i === currentIndex()) {\r\n <ol-icon\r\n name=\"check_circle\"\r\n size=\"xs\"\r\n class=\"text-fg-brand shrink-0 ml-auto\"\r\n ></ol-icon>\r\n } @else {\r\n <ol-icon\r\n name=\"alt_arrow_down\"\r\n size=\"xs\"\r\n class=\"text-body-subtle shrink-0 -rotate-90 ml-auto\"\r\n ></ol-icon>\r\n }\r\n }\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Tab Content Area -->\r\n <div\r\n [class]=\"isVertical() ? 'flex-1 min-w-0 w-full' : 'flex min-h-0 flex-1 flex-col pt-4 w-full'\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n}\r\n"
|
|
31031
31317
|
}]
|
|
31032
31318
|
}],
|
|
31033
31319
|
propDecorators: {
|
|
@@ -31441,7 +31727,7 @@ var OlToast = class OlToast {
|
|
|
31441
31727
|
},
|
|
31442
31728
|
outputs: { dismiss: "dismiss" },
|
|
31443
31729
|
ngImport: i0,
|
|
31444
|
-
template: "<div\r\n [class]=\"toastClasses()\"\r\n animate.enter=\"toast-slide-enter\"\r\n animate.leave=\"toast-slide-leave\"\r\n role=\"alert\"\r\n [attr.aria-live]=\"config().type === 'danger' ? 'assertive' : 'polite'\"\r\n (mouseenter)=\"onMouseEnter()\"\r\n (mouseleave)=\"onMouseLeave()\"\r\n>\r\n <div class=\"flex items-start gap-3 p-4\">\r\n <!-- Avatar or Icon -->\r\n @if (config().avatar) {\r\n <img [src]=\"config().avatar\" class=\"w-10 h-10 rounded-full object-cover shrink-0\" alt=\"\" />\r\n } @else if (showIcon()) {\r\n <!--\r\n Tên icon phải lấy từ `iconName()` — nó đọc `AUTO_ICONS[type]`, nên toast\r\n lỗi ra hình cảnh báo còn toast thành công ra dấu tích. Khoá cứng\r\n `name=\"
|
|
31730
|
+
template: "<div\r\n [class]=\"toastClasses()\"\r\n animate.enter=\"toast-slide-enter\"\r\n animate.leave=\"toast-slide-leave\"\r\n role=\"alert\"\r\n [attr.aria-live]=\"config().type === 'danger' ? 'assertive' : 'polite'\"\r\n (mouseenter)=\"onMouseEnter()\"\r\n (mouseleave)=\"onMouseLeave()\"\r\n>\r\n <div class=\"flex items-start gap-3 p-4\">\r\n <!-- Avatar or Icon -->\r\n @if (config().avatar) {\r\n <img [src]=\"config().avatar\" class=\"w-10 h-10 rounded-full object-cover shrink-0\" alt=\"\" />\r\n } @else if (showIcon()) {\r\n <!--\r\n Tên icon phải lấy từ `iconName()` — nó đọc `AUTO_ICONS[type]`, nên toast\r\n lỗi ra hình cảnh báo còn toast thành công ra dấu tích. Khoá cứng\r\n `name=\"check_circle\"` làm MỌI loại hiện dấu tích: đo /toast\r\n (2026-09-16) thấy toast `type: 'danger'` mang icon dấu tích — sai\r\n nghĩa, tệ hơn cả không có icon.\r\n -->\r\n <ol-icon [name]=\"iconName()\" size=\"sm\" [class]=\"iconClasses()\" />\r\n }\r\n\r\n <!-- Content -->\r\n <div class=\"flex-1 min-w-0\">\r\n @if (config().title) {\r\n @switch (headingLevel()) {\r\n @case (1) {\r\n <h1 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h1>\r\n }\r\n @case (2) {\r\n <h2 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h2>\r\n }\r\n @case (3) {\r\n <h3 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h3>\r\n }\r\n @case (5) {\r\n <h5 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h5>\r\n }\r\n @case (6) {\r\n <h6 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h6>\r\n }\r\n @default {\r\n <h4 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h4>\r\n }\r\n }\r\n }\r\n <p class=\"text-sm\">{{ config().message }}</p>\r\n\r\n @if (config().actionLabel) {\r\n <button\r\n type=\"button\"\r\n class=\"mt-2 text-sm font-medium underline hover:no-underline focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-current rounded\"\r\n (click)=\"handleAction()\"\r\n >\r\n {{ config().actionLabel }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Dismiss Button -->\r\n @if (config().dismissible) {\r\n <button\r\n type=\"button\"\r\n class=\"shrink-0 rounded-md p-1 hover:bg-black/5 dark:hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-current transition-colors\"\r\n (click)=\"handleDismiss()\"\r\n [attr.aria-label]=\"'Đóng thông báo'\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Progress Bar -->\r\n @if (config().showProgress && (config().duration ?? 0) > 0) {\r\n <div class=\"absolute bottom-0 inset-x-0 h-0.5 bg-black/5 dark:bg-white/5 pointer-events-none\">\r\n <div\r\n class=\"h-full transition-all duration-75 ease-linear rounded-r-full\"\r\n [class]=\"progressBarClasses()\"\r\n [style.width.%]=\"progress()\"\r\n ></div>\r\n </div>\r\n }\r\n</div>\r\n",
|
|
31445
31731
|
styles: [".fade-in{animation:fadeIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.fade-in-fast{animation:fadeIn var(--anim-duration-fast, .15s) var(--anim-ease-out, ease-out) forwards}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fade-out{opacity:0;transition:opacity var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.slide-down-enter{animation:slideDown var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1)) forwards}.slide-down-leave{opacity:0;transform:translateY(-100%);transition:all var(--anim-duration-normal, .3s) cubic-bezier(.4,0,.6,1)}@keyframes slideDown{0%{transform:translateY(-100%);opacity:0}to{transform:translateY(0);opacity:1}}.toast-slide-enter{animation:toastSlideIn var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1)) forwards}.toast-slide-leave{opacity:0;transform:translate(100%);transition:all .2s cubic-bezier(.4,0,.6,1)}@keyframes toastSlideIn{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}.slide-in-up{animation:slideInUp var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.slide-in-down{animation:slideInDown var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInDown{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}.slide-in-left{animation:slideInLeft var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInLeft{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.slide-in-right{animation:slideInRight var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInRight{0%{opacity:0;transform:translate(-20px)}to{opacity:1;transform:translate(0)}}.scale-in{animation:scaleIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes scaleIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.scale-out{opacity:0;transform:scale(.95);transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.menu-scale-enter{animation:menuScaleIn var(--anim-duration-fast, .15s) var(--anim-ease-out, cubic-bezier(0, 0, .2, 1)) forwards}.menu-scale-leave{opacity:0;transform:scale(.85);transition:all .1s cubic-bezier(.4,0,1,1)}@keyframes menuScaleIn{0%{opacity:0;transform:scale(.85)}to{opacity:1;transform:scale(1)}}.fade-scale-enter{animation:fadeScaleIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.fade-scale-leave{opacity:0;transform:scale(.95);transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}@keyframes fadeScaleIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.alert-fade-out{opacity:0;transform:translateY(-8px);transition:all var(--anim-duration-normal, .3s) ease-out}.tooltip-fade-enter{animation:fadeIn var(--anim-duration-fast, .15s) var(--anim-ease-out, ease-out) forwards}.backdrop-enter{animation:fadeIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.backdrop-leave{opacity:0;transition:opacity var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.expand-enter{animation:expandIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.collapse-leave{overflow:hidden;height:0;opacity:0;transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}@keyframes expandIn{0%{height:0;opacity:0;overflow:hidden}to{height:auto;opacity:1}}.rotate{transition:transform var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1))}.stagger-delay-1{animation-delay:50ms}.stagger-delay-2{animation-delay:.1s}.stagger-delay-3{animation-delay:.15s}.stagger-delay-4{animation-delay:.2s}.stagger-delay-5{animation-delay:.25s}.anim-forwards{animation-fill-mode:forwards}.anim-backwards{animation-fill-mode:backwards}.anim-both{animation-fill-mode:both}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}\n"],
|
|
31446
31732
|
dependencies: [{
|
|
31447
31733
|
kind: "ngmodule",
|
|
@@ -31473,7 +31759,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
31473
31759
|
standalone: true,
|
|
31474
31760
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
31475
31761
|
imports: [CommonModule, OlIcon],
|
|
31476
|
-
template: "<div\r\n [class]=\"toastClasses()\"\r\n animate.enter=\"toast-slide-enter\"\r\n animate.leave=\"toast-slide-leave\"\r\n role=\"alert\"\r\n [attr.aria-live]=\"config().type === 'danger' ? 'assertive' : 'polite'\"\r\n (mouseenter)=\"onMouseEnter()\"\r\n (mouseleave)=\"onMouseLeave()\"\r\n>\r\n <div class=\"flex items-start gap-3 p-4\">\r\n <!-- Avatar or Icon -->\r\n @if (config().avatar) {\r\n <img [src]=\"config().avatar\" class=\"w-10 h-10 rounded-full object-cover shrink-0\" alt=\"\" />\r\n } @else if (showIcon()) {\r\n <!--\r\n Tên icon phải lấy từ `iconName()` — nó đọc `AUTO_ICONS[type]`, nên toast\r\n lỗi ra hình cảnh báo còn toast thành công ra dấu tích. Khoá cứng\r\n `name=\"
|
|
31762
|
+
template: "<div\r\n [class]=\"toastClasses()\"\r\n animate.enter=\"toast-slide-enter\"\r\n animate.leave=\"toast-slide-leave\"\r\n role=\"alert\"\r\n [attr.aria-live]=\"config().type === 'danger' ? 'assertive' : 'polite'\"\r\n (mouseenter)=\"onMouseEnter()\"\r\n (mouseleave)=\"onMouseLeave()\"\r\n>\r\n <div class=\"flex items-start gap-3 p-4\">\r\n <!-- Avatar or Icon -->\r\n @if (config().avatar) {\r\n <img [src]=\"config().avatar\" class=\"w-10 h-10 rounded-full object-cover shrink-0\" alt=\"\" />\r\n } @else if (showIcon()) {\r\n <!--\r\n Tên icon phải lấy từ `iconName()` — nó đọc `AUTO_ICONS[type]`, nên toast\r\n lỗi ra hình cảnh báo còn toast thành công ra dấu tích. Khoá cứng\r\n `name=\"check_circle\"` làm MỌI loại hiện dấu tích: đo /toast\r\n (2026-09-16) thấy toast `type: 'danger'` mang icon dấu tích — sai\r\n nghĩa, tệ hơn cả không có icon.\r\n -->\r\n <ol-icon [name]=\"iconName()\" size=\"sm\" [class]=\"iconClasses()\" />\r\n }\r\n\r\n <!-- Content -->\r\n <div class=\"flex-1 min-w-0\">\r\n @if (config().title) {\r\n @switch (headingLevel()) {\r\n @case (1) {\r\n <h1 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h1>\r\n }\r\n @case (2) {\r\n <h2 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h2>\r\n }\r\n @case (3) {\r\n <h3 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h3>\r\n }\r\n @case (5) {\r\n <h5 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h5>\r\n }\r\n @case (6) {\r\n <h6 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h6>\r\n }\r\n @default {\r\n <h4 class=\"font-semibold text-sm mb-1\">{{ config().title }}</h4>\r\n }\r\n }\r\n }\r\n <p class=\"text-sm\">{{ config().message }}</p>\r\n\r\n @if (config().actionLabel) {\r\n <button\r\n type=\"button\"\r\n class=\"mt-2 text-sm font-medium underline hover:no-underline focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-current rounded\"\r\n (click)=\"handleAction()\"\r\n >\r\n {{ config().actionLabel }}\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Dismiss Button -->\r\n @if (config().dismissible) {\r\n <button\r\n type=\"button\"\r\n class=\"shrink-0 rounded-md p-1 hover:bg-black/5 dark:hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-current transition-colors\"\r\n (click)=\"handleDismiss()\"\r\n [attr.aria-label]=\"'Đóng thông báo'\"\r\n >\r\n <ol-icon name=\"close\" size=\"xs\" />\r\n </button>\r\n }\r\n </div>\r\n\r\n <!-- Progress Bar -->\r\n @if (config().showProgress && (config().duration ?? 0) > 0) {\r\n <div class=\"absolute bottom-0 inset-x-0 h-0.5 bg-black/5 dark:bg-white/5 pointer-events-none\">\r\n <div\r\n class=\"h-full transition-all duration-75 ease-linear rounded-r-full\"\r\n [class]=\"progressBarClasses()\"\r\n [style.width.%]=\"progress()\"\r\n ></div>\r\n </div>\r\n }\r\n</div>\r\n",
|
|
31477
31763
|
styles: [".fade-in{animation:fadeIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.fade-in-fast{animation:fadeIn var(--anim-duration-fast, .15s) var(--anim-ease-out, ease-out) forwards}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fade-out{opacity:0;transition:opacity var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.slide-down-enter{animation:slideDown var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1)) forwards}.slide-down-leave{opacity:0;transform:translateY(-100%);transition:all var(--anim-duration-normal, .3s) cubic-bezier(.4,0,.6,1)}@keyframes slideDown{0%{transform:translateY(-100%);opacity:0}to{transform:translateY(0);opacity:1}}.toast-slide-enter{animation:toastSlideIn var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1)) forwards}.toast-slide-leave{opacity:0;transform:translate(100%);transition:all .2s cubic-bezier(.4,0,.6,1)}@keyframes toastSlideIn{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}.slide-in-up{animation:slideInUp var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.slide-in-down{animation:slideInDown var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInDown{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}.slide-in-left{animation:slideInLeft var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInLeft{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.slide-in-right{animation:slideInRight var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes slideInRight{0%{opacity:0;transform:translate(-20px)}to{opacity:1;transform:translate(0)}}.scale-in{animation:scaleIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}@keyframes scaleIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.scale-out{opacity:0;transform:scale(.95);transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.menu-scale-enter{animation:menuScaleIn var(--anim-duration-fast, .15s) var(--anim-ease-out, cubic-bezier(0, 0, .2, 1)) forwards}.menu-scale-leave{opacity:0;transform:scale(.85);transition:all .1s cubic-bezier(.4,0,1,1)}@keyframes menuScaleIn{0%{opacity:0;transform:scale(.85)}to{opacity:1;transform:scale(1)}}.fade-scale-enter{animation:fadeScaleIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.fade-scale-leave{opacity:0;transform:scale(.95);transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}@keyframes fadeScaleIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.alert-fade-out{opacity:0;transform:translateY(-8px);transition:all var(--anim-duration-normal, .3s) ease-out}.tooltip-fade-enter{animation:fadeIn var(--anim-duration-fast, .15s) var(--anim-ease-out, ease-out) forwards}.backdrop-enter{animation:fadeIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.backdrop-leave{opacity:0;transition:opacity var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}.expand-enter{animation:expandIn var(--anim-duration-normal, .3s) var(--anim-ease-out, ease-out) forwards}.collapse-leave{overflow:hidden;height:0;opacity:0;transition:all var(--anim-duration-normal, .3s) var(--anim-ease-in, ease-in)}@keyframes expandIn{0%{height:0;opacity:0;overflow:hidden}to{height:auto;opacity:1}}.rotate{transition:transform var(--anim-duration-normal, .3s) var(--anim-ease-in-out, cubic-bezier(.4, 0, .2, 1))}.stagger-delay-1{animation-delay:50ms}.stagger-delay-2{animation-delay:.1s}.stagger-delay-3{animation-delay:.15s}.stagger-delay-4{animation-delay:.2s}.stagger-delay-5{animation-delay:.25s}.anim-forwards{animation-fill-mode:forwards}.anim-backwards{animation-fill-mode:backwards}.anim-both{animation-fill-mode:both}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}\n"]
|
|
31478
31764
|
}]
|
|
31479
31765
|
}],
|
|
@@ -32571,7 +32857,7 @@ var OlStepper = class OlStepper {
|
|
|
32571
32857
|
isSignal: true
|
|
32572
32858
|
}],
|
|
32573
32859
|
ngImport: i0,
|
|
32574
|
-
template: "<div [class]=\"isVertical() ? 'flex flex-col md:flex-row gap-8 w-full' : 'w-full'\">\r\n <!-- Stepper Header / Navigation Area -->\r\n <div [class]=\"isVertical() ? 'w-full md:w-80 shrink-0' : 'w-full mb-6'\">\r\n @if (isMobile()) {\r\n <!-- Mobile Layout -->\r\n <div class=\"p-4 rounded-md bg-neutral-tertiary/80 border border-default\">\r\n <div class=\"flex items-center justify-between mb-2\">\r\n <span class=\"text-xs font-semibold text-fg-brand uppercase tracking-wider\">\r\n Bước {{ currentStep() + 1 }} / {{ steps().length }}\r\n </span>\r\n @if (steps()[currentStep()]; as activeStep) {\r\n <span class=\"text-xs text-body-subtle font-medium\">\r\n {{ activeStep.title() }}\r\n </span>\r\n }\r\n </div>\r\n <!-- Progress Bar -->\r\n <div\r\n class=\"w-full bg-neutral-tertiary-medium h-2 rounded-full overflow-hidden flex gap-1 p-0.5\"\r\n >\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div\r\n class=\"h-full flex-1 rounded-full transition-all duration-300\"\r\n [class.bg-brand]=\"i <= currentStep()\"\r\n [class.bg-neutral-tertiary-medium]=\"i > currentStep()\"\r\n ></div>\r\n }\r\n </div>\r\n </div>\r\n } @else if (variant() === 'cards') {\r\n <!-- Cards Variant (Vertical Stack) -->\r\n <div class=\"flex flex-col gap-3\" role=\"tablist\" aria-label=\"Tiến trình các bước\">\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <!--\r\n `<button role=\"tab\">` chứ KHÔNG phải `<div (click)>`. Khung ngoài\r\n khai `role=\"tablist\"` nhưng đo 2026-09-17 bên trong có **0\r\n `role=\"tab\"` và 0 phần tử tab tới được** — `tablist` rỗng vai trò,\r\n và bàn phím không chọn nổi bước nào. Ba biến thể stepper khác đã\r\n dùng `<button>` sẵn; chỉ riêng `cards` lệch.\r\n\r\n `w-full` vì button không tự giãn như div.\r\n -->\r\n <button\r\n type=\"button\"\r\n role=\"tab\"\r\n [attr.aria-selected]=\"step.effectiveState() === 'active'\"\r\n [disabled]=\"step.effectiveState() === 'disabled'\"\r\n (click)=\"goToStep(i)\"\r\n [class]=\"\r\n 'w-full flex items-center justify-between p-4 rounded-md border transition-all duration-200 text-left ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 cursor-not-allowed bg-neutral-secondary-soft border-default text-fg-disabled'\r\n : step.effectiveState() === 'active'\r\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer shadow-xs ring-2 ring-brand-subtle'\r\n : step.effectiveState() === 'completed'\r\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer'\r\n : step.effectiveState() === 'error'\r\n ? 'cursor-pointer hover:shadow-xs border-danger-subtle bg-danger-soft'\r\n : 'cursor-pointer hover:shadow-xs border-default-medium bg-neutral-secondary-medium')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n <div class=\"flex items-center gap-3 min-w-0\">\r\n <!-- Left Icon / Step Badge -->\r\n <div\r\n [class]=\"\r\n 'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 font-bold text-xs transition-colors ' +\r\n (step.effectiveState() === 'completed'\r\n ? 'bg-brand-softer text-fg-brand-strong'\r\n : step.effectiveState() === 'active'\r\n ? 'bg-brand-softer text-fg-brand-strong'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft text-fg-danger-strong'\r\n : 'bg-neutral-tertiary text-body')\r\n \"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info-circle\" size=\"xs\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </div>\r\n\r\n <!-- Title & Subtitle -->\r\n <div class=\"min-w-0\">\r\n <div\r\n [class]=\"\r\n 'text-sm truncate ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand-strong '\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled truncate\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Right Status Icon -->\r\n <div class=\"shrink-0 ml-3\">\r\n @if (step.effectiveState() === 'completed') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\r\n <ol-icon name=\"check-circle\" size=\"xs\" color=\"brand\"></ol-icon>\r\n <span class=\"hidden sm:inline\">Hoàn tất</span>\r\n </span>\r\n } @else if (step.effectiveState() === 'active') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\r\n <span class=\"w-2 h-2 rounded-full bg-brand animate-pulse\"></span>\r\n <span class=\"hidden sm:inline\">Đang thực hiện</span>\r\n </span>\r\n } @else if (step.effectiveState() === 'error') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-danger\">\r\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n <span class=\"hidden sm:inline\">Lỗi</span>\r\n </span>\r\n } @else {\r\n <ol-icon name=\"chevron-right\" size=\"xs\" class=\"text-fg-disabled\"></ol-icon>\r\n }\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n } @else if (isVertical()) {\r\n <!-- Vertical Variant -->\r\n <div class=\"flex flex-col relative\" role=\"tablist\" aria-label=\"Tiến trình các bước\">\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div class=\"flex items-start gap-4 relative pb-8 last:pb-0\">\r\n <!-- Connecting Vertical Line -->\r\n @if (i < steps().length - 1) {\r\n <div\r\n class=\"absolute left-4.25 top-9 bottom-0 w-0.5 transition-colors duration-200\"\r\n [class.bg-brand]=\"i < currentStep()\"\r\n [class.bg-neutral-tertiary-medium]=\"i >= currentStep()\"\r\n ></div>\r\n }\r\n\r\n <!-- Step Circle / Icon Shape -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\n 'w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 z-10 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 cursor-not-allowed bg-neutral-tertiary text-fg-disabled border border-default'\r\n : step.effectiveState() === 'completed'\r\n ? 'cursor-pointer bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'cursor-pointer border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'cursor-pointer bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'cursor-pointer bg-neutral-primary text-body border border-default-strong')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n\r\n <!-- Step Title & Subtitle -->\r\n <div class=\"text-left pt-1 cursor-pointer\" (click)=\"goToStep(i)\">\r\n <div\r\n [class]=\"\r\n 'text-sm transition-colors ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand'\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body-subtle')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled mt-0.5\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <!--\r\n Horizontal Variants: Default | Only Icons | Icons & Text | With Dots\r\n\r\n `overflow-x-auto` trên khung ngoài: `breakpoint` là input THỦ CÔNG mặc\r\n định `desktop`, không tự theo bề rộng màn. Nơi gọi để nguyên mặc định mà\r\n người dùng mở trên điện thoại thì bố cục ngang bốn bước đo được\r\n **x=402 trên viewport 375** — bước cuối nằm hẳn ngoài mép phải và\r\n `<main>` của trang (`overflow-x: hidden`) CẮT MẤT nó, không cuộn tới\r\n được.\r\n\r\n Cho dải này tự cuộn ngang thì bước cuối vẫn tới được bằng cách kéo, và\r\n trên desktop `justify-between` vẫn dàn đều như cũ vì nội dung vừa khung.\r\n Kèm `pb-1` để thanh cuộn không che đường kẻ nối giữa các bước.\r\n -->\r\n <div\r\n class=\"flex items-center justify-between w-full relative overflow-x-auto pb-1\"\r\n role=\"tablist\"\r\n aria-label=\"Tiến trình các bước\"\r\n >\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div class=\"flex items-center flex-1 last:flex-none relative\">\r\n <div class=\"flex items-center gap-3\">\r\n @if (variant() === 'with-dots') {\r\n <!-- With Dots Variant -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\n 'flex items-center justify-center transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'w-3.5 h-3.5 rounded-full opacity-50 bg-neutral-primary text-fg-disabled border-2 border-default-medium'\r\n : step.effectiveState() === 'completed'\r\n ? 'w-4 h-4 rounded-full bg-brand text-white'\r\n : step.effectiveState() === 'active'\r\n ? 'w-4 h-4 rounded-full bg-neutral-primary border-2 border-brand-light ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'w-4 h-4 rounded-full bg-danger-surface ring-4 ring-danger-subtle'\r\n : 'w-3.5 h-3.5 rounded-full bg-neutral-primary border-2 border-default-medium')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" class=\"scale-75\"></ol-icon>\r\n }\r\n </button>\r\n } @else if (variant() === 'only-icons' || variant() === 'icons-text') {\r\n <!-- Icon Shape Variant -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\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 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 bg-neutral-tertiary text-fg-disabled'\r\n : step.effectiveState() === 'completed'\r\n ? 'bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'bg-neutral-tertiary text-body border border-default')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"sm\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info-circle\" size=\"sm\" color=\"danger\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"sm\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n } @else {\r\n <!-- Default Variant (Numbered Circle) -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\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 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 bg-neutral-primary text-fg-disabled border border-default'\r\n : step.effectiveState() === 'completed'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'bg-neutral-primary text-body border border-default-strong')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n }\r\n\r\n <!-- Text Title & Subtitle (unless only-icons) -->\r\n @if (variant() !== 'only-icons') {\r\n <div class=\"text-left cursor-pointer\" (click)=\"goToStep(i)\">\r\n <div\r\n [class]=\"\r\n 'text-sm transition-colors ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand'\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body-subtle')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled hidden md:block\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Connecting Line (Horizontal) -->\r\n @if (i < steps().length - 1) {\r\n <div class=\"flex-1 mx-4 h-0.5 bg-neutral-tertiary-medium relative overflow-hidden\">\r\n <div\r\n class=\"h-full bg-brand transition-all duration-300\"\r\n [class.w-full]=\"i < currentStep()\"\r\n [class.w-0]=\"i >= currentStep()\"\r\n ></div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Stepper Body Content Area -->\r\n <div [class]=\"isVertical() ? 'flex-1 min-w-0' : 'w-full'\">\r\n <ng-content />\r\n </div>\r\n</div>\r\n",
|
|
32860
|
+
template: "<div [class]=\"isVertical() ? 'flex flex-col md:flex-row gap-8 w-full' : 'w-full'\">\r\n <!-- Stepper Header / Navigation Area -->\r\n <div [class]=\"isVertical() ? 'w-full md:w-80 shrink-0' : 'w-full mb-6'\">\r\n @if (isMobile()) {\r\n <!-- Mobile Layout -->\r\n <div class=\"p-4 rounded-md bg-neutral-tertiary/80 border border-default\">\r\n <div class=\"flex items-center justify-between mb-2\">\r\n <span class=\"text-xs font-semibold text-fg-brand uppercase tracking-wider\">\r\n Bước {{ currentStep() + 1 }} / {{ steps().length }}\r\n </span>\r\n @if (steps()[currentStep()]; as activeStep) {\r\n <span class=\"text-xs text-body-subtle font-medium\">\r\n {{ activeStep.title() }}\r\n </span>\r\n }\r\n </div>\r\n <!-- Progress Bar -->\r\n <div\r\n class=\"w-full bg-neutral-tertiary-medium h-2 rounded-full overflow-hidden flex gap-1 p-0.5\"\r\n >\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div\r\n class=\"h-full flex-1 rounded-full transition-all duration-300\"\r\n [class.bg-brand]=\"i <= currentStep()\"\r\n [class.bg-neutral-tertiary-medium]=\"i > currentStep()\"\r\n ></div>\r\n }\r\n </div>\r\n </div>\r\n } @else if (variant() === 'cards') {\r\n <!-- Cards Variant (Vertical Stack) -->\r\n <div class=\"flex flex-col gap-3\" role=\"tablist\" aria-label=\"Tiến trình các bước\">\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <!--\r\n `<button role=\"tab\">` chứ KHÔNG phải `<div (click)>`. Khung ngoài\r\n khai `role=\"tablist\"` nhưng đo 2026-09-17 bên trong có **0\r\n `role=\"tab\"` và 0 phần tử tab tới được** — `tablist` rỗng vai trò,\r\n và bàn phím không chọn nổi bước nào. Ba biến thể stepper khác đã\r\n dùng `<button>` sẵn; chỉ riêng `cards` lệch.\r\n\r\n `w-full` vì button không tự giãn như div.\r\n -->\r\n <button\r\n type=\"button\"\r\n role=\"tab\"\r\n [attr.aria-selected]=\"step.effectiveState() === 'active'\"\r\n [disabled]=\"step.effectiveState() === 'disabled'\"\r\n (click)=\"goToStep(i)\"\r\n [class]=\"\r\n 'w-full flex items-center justify-between p-4 rounded-md border transition-all duration-200 text-left ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 cursor-not-allowed bg-neutral-secondary-soft border-default text-fg-disabled'\r\n : step.effectiveState() === 'active'\r\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer shadow-xs ring-2 ring-brand-subtle'\r\n : step.effectiveState() === 'completed'\r\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer'\r\n : step.effectiveState() === 'error'\r\n ? 'cursor-pointer hover:shadow-xs border-danger-subtle bg-danger-soft'\r\n : 'cursor-pointer hover:shadow-xs border-default-medium bg-neutral-secondary-medium')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n <div class=\"flex items-center gap-3 min-w-0\">\r\n <!-- Left Icon / Step Badge -->\r\n <div\r\n [class]=\"\r\n 'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 font-bold text-xs transition-colors ' +\r\n (step.effectiveState() === 'completed'\r\n ? 'bg-brand-softer text-fg-brand-strong'\r\n : step.effectiveState() === 'active'\r\n ? 'bg-brand-softer text-fg-brand-strong'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft text-fg-danger-strong'\r\n : 'bg-neutral-tertiary text-body')\r\n \"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info_circle\" size=\"xs\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </div>\r\n\r\n <!-- Title & Subtitle -->\r\n <div class=\"min-w-0\">\r\n <div\r\n [class]=\"\r\n 'text-sm truncate ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand-strong '\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled truncate\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Right Status Icon -->\r\n <div class=\"shrink-0 ml-3\">\r\n @if (step.effectiveState() === 'completed') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\r\n <ol-icon name=\"check_circle\" size=\"xs\" color=\"brand\"></ol-icon>\r\n <span class=\"hidden sm:inline\">Hoàn tất</span>\r\n </span>\r\n } @else if (step.effectiveState() === 'active') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\r\n <span class=\"w-2 h-2 rounded-full bg-brand animate-pulse\"></span>\r\n <span class=\"hidden sm:inline\">Đang thực hiện</span>\r\n </span>\r\n } @else if (step.effectiveState() === 'error') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-danger\">\r\n <ol-icon name=\"info_circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n <span class=\"hidden sm:inline\">Lỗi</span>\r\n </span>\r\n } @else {\r\n <ol-icon name=\"chevron_right\" size=\"xs\" class=\"text-fg-disabled\"></ol-icon>\r\n }\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n } @else if (isVertical()) {\r\n <!-- Vertical Variant -->\r\n <div class=\"flex flex-col relative\" role=\"tablist\" aria-label=\"Tiến trình các bước\">\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div class=\"flex items-start gap-4 relative pb-8 last:pb-0\">\r\n <!-- Connecting Vertical Line -->\r\n @if (i < steps().length - 1) {\r\n <div\r\n class=\"absolute left-4.25 top-9 bottom-0 w-0.5 transition-colors duration-200\"\r\n [class.bg-brand]=\"i < currentStep()\"\r\n [class.bg-neutral-tertiary-medium]=\"i >= currentStep()\"\r\n ></div>\r\n }\r\n\r\n <!-- Step Circle / Icon Shape -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\n 'w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 z-10 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 cursor-not-allowed bg-neutral-tertiary text-fg-disabled border border-default'\r\n : step.effectiveState() === 'completed'\r\n ? 'cursor-pointer bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'cursor-pointer border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'cursor-pointer bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'cursor-pointer bg-neutral-primary text-body border border-default-strong')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info_circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n\r\n <!-- Step Title & Subtitle -->\r\n <div class=\"text-left pt-1 cursor-pointer\" (click)=\"goToStep(i)\">\r\n <div\r\n [class]=\"\r\n 'text-sm transition-colors ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand'\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body-subtle')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled mt-0.5\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <!--\r\n Horizontal Variants: Default | Only Icons | Icons & Text | With Dots\r\n\r\n `overflow-x-auto` trên khung ngoài: `breakpoint` là input THỦ CÔNG mặc\r\n định `desktop`, không tự theo bề rộng màn. Nơi gọi để nguyên mặc định mà\r\n người dùng mở trên điện thoại thì bố cục ngang bốn bước đo được\r\n **x=402 trên viewport 375** — bước cuối nằm hẳn ngoài mép phải và\r\n `<main>` của trang (`overflow-x: hidden`) CẮT MẤT nó, không cuộn tới\r\n được.\r\n\r\n Cho dải này tự cuộn ngang thì bước cuối vẫn tới được bằng cách kéo, và\r\n trên desktop `justify-between` vẫn dàn đều như cũ vì nội dung vừa khung.\r\n Kèm `pb-1` để thanh cuộn không che đường kẻ nối giữa các bước.\r\n -->\r\n <div\r\n class=\"flex items-center justify-between w-full relative overflow-x-auto pb-1\"\r\n role=\"tablist\"\r\n aria-label=\"Tiến trình các bước\"\r\n >\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div class=\"flex items-center flex-1 last:flex-none relative\">\r\n <div class=\"flex items-center gap-3\">\r\n @if (variant() === 'with-dots') {\r\n <!-- With Dots Variant -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\n 'flex items-center justify-center transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'w-3.5 h-3.5 rounded-full opacity-50 bg-neutral-primary text-fg-disabled border-2 border-default-medium'\r\n : step.effectiveState() === 'completed'\r\n ? 'w-4 h-4 rounded-full bg-brand text-white'\r\n : step.effectiveState() === 'active'\r\n ? 'w-4 h-4 rounded-full bg-neutral-primary border-2 border-brand-light ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'w-4 h-4 rounded-full bg-danger-surface ring-4 ring-danger-subtle'\r\n : 'w-3.5 h-3.5 rounded-full bg-neutral-primary border-2 border-default-medium')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" class=\"scale-75\"></ol-icon>\r\n }\r\n </button>\r\n } @else if (variant() === 'only-icons' || variant() === 'icons-text') {\r\n <!-- Icon Shape Variant -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\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 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 bg-neutral-tertiary text-fg-disabled'\r\n : step.effectiveState() === 'completed'\r\n ? 'bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'bg-neutral-tertiary text-body border border-default')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"sm\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info_circle\" size=\"sm\" color=\"danger\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"sm\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n } @else {\r\n <!-- Default Variant (Numbered Circle) -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\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 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 bg-neutral-primary text-fg-disabled border border-default'\r\n : step.effectiveState() === 'completed'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'bg-neutral-primary text-body border border-default-strong')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info_circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n }\r\n\r\n <!-- Text Title & Subtitle (unless only-icons) -->\r\n @if (variant() !== 'only-icons') {\r\n <div class=\"text-left cursor-pointer\" (click)=\"goToStep(i)\">\r\n <div\r\n [class]=\"\r\n 'text-sm transition-colors ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand'\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body-subtle')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled hidden md:block\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Connecting Line (Horizontal) -->\r\n @if (i < steps().length - 1) {\r\n <div class=\"flex-1 mx-4 h-0.5 bg-neutral-tertiary-medium relative overflow-hidden\">\r\n <div\r\n class=\"h-full bg-brand transition-all duration-300\"\r\n [class.w-full]=\"i < currentStep()\"\r\n [class.w-0]=\"i >= currentStep()\"\r\n ></div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Stepper Body Content Area -->\r\n <div [class]=\"isVertical() ? 'flex-1 min-w-0' : 'w-full'\">\r\n <ng-content />\r\n </div>\r\n</div>\r\n",
|
|
32575
32861
|
dependencies: [
|
|
32576
32862
|
{
|
|
32577
32863
|
kind: "ngmodule",
|
|
@@ -32634,7 +32920,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32634
32920
|
],
|
|
32635
32921
|
host: { class: "block w-full" },
|
|
32636
32922
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
32637
|
-
template: "<div [class]=\"isVertical() ? 'flex flex-col md:flex-row gap-8 w-full' : 'w-full'\">\r\n <!-- Stepper Header / Navigation Area -->\r\n <div [class]=\"isVertical() ? 'w-full md:w-80 shrink-0' : 'w-full mb-6'\">\r\n @if (isMobile()) {\r\n <!-- Mobile Layout -->\r\n <div class=\"p-4 rounded-md bg-neutral-tertiary/80 border border-default\">\r\n <div class=\"flex items-center justify-between mb-2\">\r\n <span class=\"text-xs font-semibold text-fg-brand uppercase tracking-wider\">\r\n Bước {{ currentStep() + 1 }} / {{ steps().length }}\r\n </span>\r\n @if (steps()[currentStep()]; as activeStep) {\r\n <span class=\"text-xs text-body-subtle font-medium\">\r\n {{ activeStep.title() }}\r\n </span>\r\n }\r\n </div>\r\n <!-- Progress Bar -->\r\n <div\r\n class=\"w-full bg-neutral-tertiary-medium h-2 rounded-full overflow-hidden flex gap-1 p-0.5\"\r\n >\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div\r\n class=\"h-full flex-1 rounded-full transition-all duration-300\"\r\n [class.bg-brand]=\"i <= currentStep()\"\r\n [class.bg-neutral-tertiary-medium]=\"i > currentStep()\"\r\n ></div>\r\n }\r\n </div>\r\n </div>\r\n } @else if (variant() === 'cards') {\r\n <!-- Cards Variant (Vertical Stack) -->\r\n <div class=\"flex flex-col gap-3\" role=\"tablist\" aria-label=\"Tiến trình các bước\">\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <!--\r\n `<button role=\"tab\">` chứ KHÔNG phải `<div (click)>`. Khung ngoài\r\n khai `role=\"tablist\"` nhưng đo 2026-09-17 bên trong có **0\r\n `role=\"tab\"` và 0 phần tử tab tới được** — `tablist` rỗng vai trò,\r\n và bàn phím không chọn nổi bước nào. Ba biến thể stepper khác đã\r\n dùng `<button>` sẵn; chỉ riêng `cards` lệch.\r\n\r\n `w-full` vì button không tự giãn như div.\r\n -->\r\n <button\r\n type=\"button\"\r\n role=\"tab\"\r\n [attr.aria-selected]=\"step.effectiveState() === 'active'\"\r\n [disabled]=\"step.effectiveState() === 'disabled'\"\r\n (click)=\"goToStep(i)\"\r\n [class]=\"\r\n 'w-full flex items-center justify-between p-4 rounded-md border transition-all duration-200 text-left ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 cursor-not-allowed bg-neutral-secondary-soft border-default text-fg-disabled'\r\n : step.effectiveState() === 'active'\r\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer shadow-xs ring-2 ring-brand-subtle'\r\n : step.effectiveState() === 'completed'\r\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer'\r\n : step.effectiveState() === 'error'\r\n ? 'cursor-pointer hover:shadow-xs border-danger-subtle bg-danger-soft'\r\n : 'cursor-pointer hover:shadow-xs border-default-medium bg-neutral-secondary-medium')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n <div class=\"flex items-center gap-3 min-w-0\">\r\n <!-- Left Icon / Step Badge -->\r\n <div\r\n [class]=\"\r\n 'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 font-bold text-xs transition-colors ' +\r\n (step.effectiveState() === 'completed'\r\n ? 'bg-brand-softer text-fg-brand-strong'\r\n : step.effectiveState() === 'active'\r\n ? 'bg-brand-softer text-fg-brand-strong'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft text-fg-danger-strong'\r\n : 'bg-neutral-tertiary text-body')\r\n \"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info-circle\" size=\"xs\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </div>\r\n\r\n <!-- Title & Subtitle -->\r\n <div class=\"min-w-0\">\r\n <div\r\n [class]=\"\r\n 'text-sm truncate ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand-strong '\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled truncate\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Right Status Icon -->\r\n <div class=\"shrink-0 ml-3\">\r\n @if (step.effectiveState() === 'completed') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\r\n <ol-icon name=\"check-circle\" size=\"xs\" color=\"brand\"></ol-icon>\r\n <span class=\"hidden sm:inline\">Hoàn tất</span>\r\n </span>\r\n } @else if (step.effectiveState() === 'active') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\r\n <span class=\"w-2 h-2 rounded-full bg-brand animate-pulse\"></span>\r\n <span class=\"hidden sm:inline\">Đang thực hiện</span>\r\n </span>\r\n } @else if (step.effectiveState() === 'error') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-danger\">\r\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n <span class=\"hidden sm:inline\">Lỗi</span>\r\n </span>\r\n } @else {\r\n <ol-icon name=\"chevron-right\" size=\"xs\" class=\"text-fg-disabled\"></ol-icon>\r\n }\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n } @else if (isVertical()) {\r\n <!-- Vertical Variant -->\r\n <div class=\"flex flex-col relative\" role=\"tablist\" aria-label=\"Tiến trình các bước\">\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div class=\"flex items-start gap-4 relative pb-8 last:pb-0\">\r\n <!-- Connecting Vertical Line -->\r\n @if (i < steps().length - 1) {\r\n <div\r\n class=\"absolute left-4.25 top-9 bottom-0 w-0.5 transition-colors duration-200\"\r\n [class.bg-brand]=\"i < currentStep()\"\r\n [class.bg-neutral-tertiary-medium]=\"i >= currentStep()\"\r\n ></div>\r\n }\r\n\r\n <!-- Step Circle / Icon Shape -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\n 'w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 z-10 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 cursor-not-allowed bg-neutral-tertiary text-fg-disabled border border-default'\r\n : step.effectiveState() === 'completed'\r\n ? 'cursor-pointer bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'cursor-pointer border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'cursor-pointer bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'cursor-pointer bg-neutral-primary text-body border border-default-strong')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n\r\n <!-- Step Title & Subtitle -->\r\n <div class=\"text-left pt-1 cursor-pointer\" (click)=\"goToStep(i)\">\r\n <div\r\n [class]=\"\r\n 'text-sm transition-colors ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand'\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body-subtle')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled mt-0.5\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <!--\r\n Horizontal Variants: Default | Only Icons | Icons & Text | With Dots\r\n\r\n `overflow-x-auto` trên khung ngoài: `breakpoint` là input THỦ CÔNG mặc\r\n định `desktop`, không tự theo bề rộng màn. Nơi gọi để nguyên mặc định mà\r\n người dùng mở trên điện thoại thì bố cục ngang bốn bước đo được\r\n **x=402 trên viewport 375** — bước cuối nằm hẳn ngoài mép phải và\r\n `<main>` của trang (`overflow-x: hidden`) CẮT MẤT nó, không cuộn tới\r\n được.\r\n\r\n Cho dải này tự cuộn ngang thì bước cuối vẫn tới được bằng cách kéo, và\r\n trên desktop `justify-between` vẫn dàn đều như cũ vì nội dung vừa khung.\r\n Kèm `pb-1` để thanh cuộn không che đường kẻ nối giữa các bước.\r\n -->\r\n <div\r\n class=\"flex items-center justify-between w-full relative overflow-x-auto pb-1\"\r\n role=\"tablist\"\r\n aria-label=\"Tiến trình các bước\"\r\n >\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div class=\"flex items-center flex-1 last:flex-none relative\">\r\n <div class=\"flex items-center gap-3\">\r\n @if (variant() === 'with-dots') {\r\n <!-- With Dots Variant -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\n 'flex items-center justify-center transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'w-3.5 h-3.5 rounded-full opacity-50 bg-neutral-primary text-fg-disabled border-2 border-default-medium'\r\n : step.effectiveState() === 'completed'\r\n ? 'w-4 h-4 rounded-full bg-brand text-white'\r\n : step.effectiveState() === 'active'\r\n ? 'w-4 h-4 rounded-full bg-neutral-primary border-2 border-brand-light ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'w-4 h-4 rounded-full bg-danger-surface ring-4 ring-danger-subtle'\r\n : 'w-3.5 h-3.5 rounded-full bg-neutral-primary border-2 border-default-medium')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" class=\"scale-75\"></ol-icon>\r\n }\r\n </button>\r\n } @else if (variant() === 'only-icons' || variant() === 'icons-text') {\r\n <!-- Icon Shape Variant -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\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 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 bg-neutral-tertiary text-fg-disabled'\r\n : step.effectiveState() === 'completed'\r\n ? 'bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'bg-neutral-tertiary text-body border border-default')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"sm\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info-circle\" size=\"sm\" color=\"danger\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"sm\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n } @else {\r\n <!-- Default Variant (Numbered Circle) -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\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 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 bg-neutral-primary text-fg-disabled border border-default'\r\n : step.effectiveState() === 'completed'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'bg-neutral-primary text-body border border-default-strong')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info-circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n }\r\n\r\n <!-- Text Title & Subtitle (unless only-icons) -->\r\n @if (variant() !== 'only-icons') {\r\n <div class=\"text-left cursor-pointer\" (click)=\"goToStep(i)\">\r\n <div\r\n [class]=\"\r\n 'text-sm transition-colors ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand'\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body-subtle')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled hidden md:block\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Connecting Line (Horizontal) -->\r\n @if (i < steps().length - 1) {\r\n <div class=\"flex-1 mx-4 h-0.5 bg-neutral-tertiary-medium relative overflow-hidden\">\r\n <div\r\n class=\"h-full bg-brand transition-all duration-300\"\r\n [class.w-full]=\"i < currentStep()\"\r\n [class.w-0]=\"i >= currentStep()\"\r\n ></div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Stepper Body Content Area -->\r\n <div [class]=\"isVertical() ? 'flex-1 min-w-0' : 'w-full'\">\r\n <ng-content />\r\n </div>\r\n</div>\r\n"
|
|
32923
|
+
template: "<div [class]=\"isVertical() ? 'flex flex-col md:flex-row gap-8 w-full' : 'w-full'\">\r\n <!-- Stepper Header / Navigation Area -->\r\n <div [class]=\"isVertical() ? 'w-full md:w-80 shrink-0' : 'w-full mb-6'\">\r\n @if (isMobile()) {\r\n <!-- Mobile Layout -->\r\n <div class=\"p-4 rounded-md bg-neutral-tertiary/80 border border-default\">\r\n <div class=\"flex items-center justify-between mb-2\">\r\n <span class=\"text-xs font-semibold text-fg-brand uppercase tracking-wider\">\r\n Bước {{ currentStep() + 1 }} / {{ steps().length }}\r\n </span>\r\n @if (steps()[currentStep()]; as activeStep) {\r\n <span class=\"text-xs text-body-subtle font-medium\">\r\n {{ activeStep.title() }}\r\n </span>\r\n }\r\n </div>\r\n <!-- Progress Bar -->\r\n <div\r\n class=\"w-full bg-neutral-tertiary-medium h-2 rounded-full overflow-hidden flex gap-1 p-0.5\"\r\n >\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div\r\n class=\"h-full flex-1 rounded-full transition-all duration-300\"\r\n [class.bg-brand]=\"i <= currentStep()\"\r\n [class.bg-neutral-tertiary-medium]=\"i > currentStep()\"\r\n ></div>\r\n }\r\n </div>\r\n </div>\r\n } @else if (variant() === 'cards') {\r\n <!-- Cards Variant (Vertical Stack) -->\r\n <div class=\"flex flex-col gap-3\" role=\"tablist\" aria-label=\"Tiến trình các bước\">\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <!--\r\n `<button role=\"tab\">` chứ KHÔNG phải `<div (click)>`. Khung ngoài\r\n khai `role=\"tablist\"` nhưng đo 2026-09-17 bên trong có **0\r\n `role=\"tab\"` và 0 phần tử tab tới được** — `tablist` rỗng vai trò,\r\n và bàn phím không chọn nổi bước nào. Ba biến thể stepper khác đã\r\n dùng `<button>` sẵn; chỉ riêng `cards` lệch.\r\n\r\n `w-full` vì button không tự giãn như div.\r\n -->\r\n <button\r\n type=\"button\"\r\n role=\"tab\"\r\n [attr.aria-selected]=\"step.effectiveState() === 'active'\"\r\n [disabled]=\"step.effectiveState() === 'disabled'\"\r\n (click)=\"goToStep(i)\"\r\n [class]=\"\r\n 'w-full flex items-center justify-between p-4 rounded-md border transition-all duration-200 text-left ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 cursor-not-allowed bg-neutral-secondary-soft border-default text-fg-disabled'\r\n : step.effectiveState() === 'active'\r\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer shadow-xs ring-2 ring-brand-subtle'\r\n : step.effectiveState() === 'completed'\r\n ? 'cursor-pointer hover:shadow-xs border-brand-subtle bg-brand-softer'\r\n : step.effectiveState() === 'error'\r\n ? 'cursor-pointer hover:shadow-xs border-danger-subtle bg-danger-soft'\r\n : 'cursor-pointer hover:shadow-xs border-default-medium bg-neutral-secondary-medium')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n <div class=\"flex items-center gap-3 min-w-0\">\r\n <!-- Left Icon / Step Badge -->\r\n <div\r\n [class]=\"\r\n 'w-9 h-9 rounded-lg flex items-center justify-center shrink-0 font-bold text-xs transition-colors ' +\r\n (step.effectiveState() === 'completed'\r\n ? 'bg-brand-softer text-fg-brand-strong'\r\n : step.effectiveState() === 'active'\r\n ? 'bg-brand-softer text-fg-brand-strong'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft text-fg-danger-strong'\r\n : 'bg-neutral-tertiary text-body')\r\n \"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info_circle\" size=\"xs\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </div>\r\n\r\n <!-- Title & Subtitle -->\r\n <div class=\"min-w-0\">\r\n <div\r\n [class]=\"\r\n 'text-sm truncate ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand-strong '\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled truncate\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n\r\n <!-- Right Status Icon -->\r\n <div class=\"shrink-0 ml-3\">\r\n @if (step.effectiveState() === 'completed') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\r\n <ol-icon name=\"check_circle\" size=\"xs\" color=\"brand\"></ol-icon>\r\n <span class=\"hidden sm:inline\">Hoàn tất</span>\r\n </span>\r\n } @else if (step.effectiveState() === 'active') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-brand\">\r\n <span class=\"w-2 h-2 rounded-full bg-brand animate-pulse\"></span>\r\n <span class=\"hidden sm:inline\">Đang thực hiện</span>\r\n </span>\r\n } @else if (step.effectiveState() === 'error') {\r\n <span class=\"inline-flex items-center gap-1 text-xs font-semibold text-fg-danger\">\r\n <ol-icon name=\"info_circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n <span class=\"hidden sm:inline\">Lỗi</span>\r\n </span>\r\n } @else {\r\n <ol-icon name=\"chevron_right\" size=\"xs\" class=\"text-fg-disabled\"></ol-icon>\r\n }\r\n </div>\r\n </button>\r\n }\r\n </div>\r\n } @else if (isVertical()) {\r\n <!-- Vertical Variant -->\r\n <div class=\"flex flex-col relative\" role=\"tablist\" aria-label=\"Tiến trình các bước\">\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div class=\"flex items-start gap-4 relative pb-8 last:pb-0\">\r\n <!-- Connecting Vertical Line -->\r\n @if (i < steps().length - 1) {\r\n <div\r\n class=\"absolute left-4.25 top-9 bottom-0 w-0.5 transition-colors duration-200\"\r\n [class.bg-brand]=\"i < currentStep()\"\r\n [class.bg-neutral-tertiary-medium]=\"i >= currentStep()\"\r\n ></div>\r\n }\r\n\r\n <!-- Step Circle / Icon Shape -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\n 'w-9 h-9 rounded-full flex items-center justify-center font-bold text-xs transition-all duration-200 shrink-0 z-10 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 cursor-not-allowed bg-neutral-tertiary text-fg-disabled border border-default'\r\n : step.effectiveState() === 'completed'\r\n ? 'cursor-pointer bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'cursor-pointer border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'cursor-pointer bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'cursor-pointer bg-neutral-primary text-body border border-default-strong')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info_circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"xs\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n\r\n <!-- Step Title & Subtitle -->\r\n <div class=\"text-left pt-1 cursor-pointer\" (click)=\"goToStep(i)\">\r\n <div\r\n [class]=\"\r\n 'text-sm transition-colors ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand'\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body-subtle')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled mt-0.5\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n } @else {\r\n <!--\r\n Horizontal Variants: Default | Only Icons | Icons & Text | With Dots\r\n\r\n `overflow-x-auto` trên khung ngoài: `breakpoint` là input THỦ CÔNG mặc\r\n định `desktop`, không tự theo bề rộng màn. Nơi gọi để nguyên mặc định mà\r\n người dùng mở trên điện thoại thì bố cục ngang bốn bước đo được\r\n **x=402 trên viewport 375** — bước cuối nằm hẳn ngoài mép phải và\r\n `<main>` của trang (`overflow-x: hidden`) CẮT MẤT nó, không cuộn tới\r\n được.\r\n\r\n Cho dải này tự cuộn ngang thì bước cuối vẫn tới được bằng cách kéo, và\r\n trên desktop `justify-between` vẫn dàn đều như cũ vì nội dung vừa khung.\r\n Kèm `pb-1` để thanh cuộn không che đường kẻ nối giữa các bước.\r\n -->\r\n <div\r\n class=\"flex items-center justify-between w-full relative overflow-x-auto pb-1\"\r\n role=\"tablist\"\r\n aria-label=\"Tiến trình các bước\"\r\n >\r\n @for (step of steps(); track step.stepId(); let i = $index) {\r\n <div class=\"flex items-center flex-1 last:flex-none relative\">\r\n <div class=\"flex items-center gap-3\">\r\n @if (variant() === 'with-dots') {\r\n <!-- With Dots Variant -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\n 'flex items-center justify-center transition-all duration-200 shrink-0 cursor-pointer disabled:cursor-not-allowed ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'w-3.5 h-3.5 rounded-full opacity-50 bg-neutral-primary text-fg-disabled border-2 border-default-medium'\r\n : step.effectiveState() === 'completed'\r\n ? 'w-4 h-4 rounded-full bg-brand text-white'\r\n : step.effectiveState() === 'active'\r\n ? 'w-4 h-4 rounded-full bg-neutral-primary border-2 border-brand-light ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'w-4 h-4 rounded-full bg-danger-surface ring-4 ring-danger-subtle'\r\n : 'w-3.5 h-3.5 rounded-full bg-neutral-primary border-2 border-default-medium')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\" color=\"white\" class=\"scale-75\"></ol-icon>\r\n }\r\n </button>\r\n } @else if (variant() === 'only-icons' || variant() === 'icons-text') {\r\n <!-- Icon Shape Variant -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\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 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 bg-neutral-tertiary text-fg-disabled'\r\n : step.effectiveState() === 'completed'\r\n ? 'bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand ring-4 ring-brand-subtle'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'bg-neutral-tertiary text-body border border-default')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"sm\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info_circle\" size=\"sm\" color=\"danger\"></ol-icon>\r\n } @else if (step.icon()) {\r\n <ol-icon [name]=\"step.icon()!\" size=\"sm\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n } @else {\r\n <!-- Default Variant (Numbered Circle) -->\r\n <button\r\n type=\"button\"\r\n (click)=\"goToStep(i)\"\r\n [disabled]=\"\r\n step.effectiveState() === 'disabled' || (linear() && i > currentStep() + 1)\r\n \"\r\n [class]=\"\r\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 ' +\r\n (step.effectiveState() === 'disabled'\r\n ? 'opacity-50 bg-neutral-primary text-fg-disabled border border-default'\r\n : step.effectiveState() === 'completed'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'active'\r\n ? 'border-2 border-brand bg-brand-softer text-fg-brand shadow-xs'\r\n : step.effectiveState() === 'error'\r\n ? 'bg-danger-soft border-2 border-danger text-fg-danger-strong'\r\n : 'bg-neutral-primary text-body border border-default-strong')\r\n \"\r\n [attr.aria-label]=\"stepAriaLabel(i)\"\r\n [attr.aria-current]=\"step.effectiveState() === 'active' ? 'step' : null\"\r\n >\r\n @if (step.effectiveState() === 'completed') {\r\n <ol-icon name=\"check\" size=\"xs\"></ol-icon>\r\n } @else if (step.effectiveState() === 'error') {\r\n <ol-icon name=\"info_circle\" size=\"xs\" color=\"danger\"></ol-icon>\r\n } @else {\r\n <span>{{ i + 1 }}</span>\r\n }\r\n </button>\r\n }\r\n\r\n <!-- Text Title & Subtitle (unless only-icons) -->\r\n @if (variant() !== 'only-icons') {\r\n <div class=\"text-left cursor-pointer\" (click)=\"goToStep(i)\">\r\n <div\r\n [class]=\"\r\n 'text-sm transition-colors ' +\r\n (step.effectiveState() === 'active'\r\n ? 'font-bold text-fg-brand'\r\n : step.effectiveState() === 'completed'\r\n ? 'font-semibold text-heading'\r\n : step.effectiveState() === 'error'\r\n ? 'font-semibold text-fg-danger'\r\n : 'font-medium text-body-subtle')\r\n \"\r\n >\r\n {{ step.title() }}\r\n </div>\r\n @if (step.badge()) {\r\n <ol-badge size=\"sm\" color=\"brand\" class=\"shrink-0\">{{ step.badge() }}</ol-badge>\r\n }\r\n @if (step.effectiveSubtitle()) {\r\n <div class=\"text-xs text-fg-disabled hidden md:block\">\r\n {{ step.effectiveSubtitle() }}\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Connecting Line (Horizontal) -->\r\n @if (i < steps().length - 1) {\r\n <div class=\"flex-1 mx-4 h-0.5 bg-neutral-tertiary-medium relative overflow-hidden\">\r\n <div\r\n class=\"h-full bg-brand transition-all duration-300\"\r\n [class.w-full]=\"i < currentStep()\"\r\n [class.w-0]=\"i >= currentStep()\"\r\n ></div>\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <!-- Stepper Body Content Area -->\r\n <div [class]=\"isVertical() ? 'flex-1 min-w-0' : 'w-full'\">\r\n <ng-content />\r\n </div>\r\n</div>\r\n"
|
|
32638
32924
|
}]
|
|
32639
32925
|
}],
|
|
32640
32926
|
propDecorators: {
|
|
@@ -34579,6 +34865,10 @@ var OlChatBubble = class OlChatBubble {
|
|
|
34579
34865
|
transformFunction: null
|
|
34580
34866
|
}
|
|
34581
34867
|
},
|
|
34868
|
+
providers: [provideOlIcons({
|
|
34869
|
+
"clock-circle": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 7v5l4 2\"/></svg>",
|
|
34870
|
+
"check-circle": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M8 12l2 2 4-4\"/></svg>"
|
|
34871
|
+
})],
|
|
34582
34872
|
ngImport: i0,
|
|
34583
34873
|
template: "<!--\r\n MỘT `<ng-content>` duy nhất cho mọi nhánh.\r\n\r\n Khai nhiều `<ng-content>` không `select` — kể cả ở các nhánh `@if` loại trừ\r\n nhau — thì Angular chỉ chiếu nội dung lồng vào MỘT chỗ, và thực tế là mất ở\r\n mọi nhánh. Hỏng im lặng: build xanh, khung vẫn vẽ, chỉ ruột trống. Gói vào một\r\n `ng-template` rồi mỗi nhánh chiếu lại bằng `ngTemplateOutlet`.\r\n-->\r\n<ng-template #noiDungLong><ng-content /></ng-template>\r\n\r\n<!-- 1. USER SENDER (Outgoing Bubble - Right aligned) -->\r\n@if (sender() === 'user') {\r\n <div class=\"flex items-start justify-end gap-2.5 max-w-lg ml-auto\">\r\n <div class=\"space-y-1 text-right flex-1 flex flex-col items-end\">\r\n <!-- Header info -->\r\n <div class=\"flex items-center gap-1.5 text-2xs text-body-subtle font-medium\">\r\n @if (time()) {\r\n <span>{{ time() }}</span>\r\n }\r\n @if (senderName()) {\r\n <span class=\"font-bold text-body\">{{ senderName() }}</span>\r\n }\r\n </div>\r\n\r\n <!-- Bubble message content -->\r\n <div\r\n class=\"p-3.5 rounded-md rounded-tr-xs bg-brand text-white text-xs leading-relaxed text-left shadow-xs max-w-md\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n\r\n <!-- Message status icon -->\r\n @if (status()) {\r\n <div class=\"flex items-center gap-1 text-2xs text-fg-brand\">\r\n <ol-icon [name]=\"statusInfo().icon\" size=\"xs\" />\r\n <span class=\"text-2xs\">{{ statusInfo().nhan }}</span>\r\n </div>\r\n }\r\n </div>\r\n\r\n <ol-avatar\r\n [src]=\"avatar()\"\r\n [text]=\"avatarText() || 'U'\"\r\n size=\"xs\"\r\n color=\"brand\"\r\n class=\"shrink-0 mt-1\"\r\n ></ol-avatar>\r\n </div>\r\n}\r\n\r\n<!-- 2. BOT / AI SENDER (Incoming Bubble - Left aligned) -->\r\n@if (sender() === 'bot') {\r\n <div class=\"flex items-start gap-2.5 max-w-lg mr-auto\">\r\n <div\r\n class=\"w-7 h-7 rounded-md bg-brand-softer text-fg-brand flex items-center justify-center shrink-0 mt-1\"\r\n >\r\n <ol-icon name=\"sparkles\" size=\"xs\"></ol-icon>\r\n </div>\r\n\r\n <div class=\"space-y-1 flex-1\">\r\n <div class=\"flex items-center gap-1.5 text-2xs text-body-subtle font-medium\">\r\n <span class=\"font-bold text-heading\">{{ senderName() || 'Trợ lý AI Ôn Luyện' }}</span>\r\n @if (time()) {\r\n <span>• {{ time() }}</span>\r\n }\r\n </div>\r\n\r\n <div\r\n class=\"p-3.5 rounded-md rounded-tl-xs bg-neutral-tertiary text-body text-xs leading-relaxed shadow-xs max-w-md border border-default\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n\r\n<!-- 3. OTHER USER SENDER (Teacher / Peer) -->\r\n@if (sender() === 'other') {\r\n <div class=\"flex items-start gap-2.5 max-w-lg mr-auto\">\r\n <ol-avatar\r\n [src]=\"avatar()\"\r\n [text]=\"avatarText() || 'T'\"\r\n size=\"xs\"\r\n color=\"green\"\r\n class=\"shrink-0 mt-1\"\r\n ></ol-avatar>\r\n\r\n <div class=\"space-y-1 flex-1\">\r\n <div class=\"flex items-center gap-1.5 text-2xs text-body-subtle font-medium\">\r\n <span class=\"font-bold text-heading\">{{ senderName() || 'Giáo viên' }}</span>\r\n @if (time()) {\r\n <span>• {{ time() }}</span>\r\n }\r\n </div>\r\n\r\n <div\r\n class=\"p-3.5 rounded-md rounded-tl-xs bg-neutral-tertiary text-body text-xs leading-relaxed shadow-xs max-w-md\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n",
|
|
34584
34874
|
dependencies: [
|
|
@@ -34650,6 +34940,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
34650
34940
|
NgTemplateOutlet
|
|
34651
34941
|
],
|
|
34652
34942
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
34943
|
+
providers: [provideOlIcons({
|
|
34944
|
+
"clock-circle": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M12 7v5l4 2\"/></svg>",
|
|
34945
|
+
"check-circle": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"/><path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\" d=\"M8 12l2 2 4-4\"/></svg>"
|
|
34946
|
+
})],
|
|
34653
34947
|
template: "<!--\r\n MỘT `<ng-content>` duy nhất cho mọi nhánh.\r\n\r\n Khai nhiều `<ng-content>` không `select` — kể cả ở các nhánh `@if` loại trừ\r\n nhau — thì Angular chỉ chiếu nội dung lồng vào MỘT chỗ, và thực tế là mất ở\r\n mọi nhánh. Hỏng im lặng: build xanh, khung vẫn vẽ, chỉ ruột trống. Gói vào một\r\n `ng-template` rồi mỗi nhánh chiếu lại bằng `ngTemplateOutlet`.\r\n-->\r\n<ng-template #noiDungLong><ng-content /></ng-template>\r\n\r\n<!-- 1. USER SENDER (Outgoing Bubble - Right aligned) -->\r\n@if (sender() === 'user') {\r\n <div class=\"flex items-start justify-end gap-2.5 max-w-lg ml-auto\">\r\n <div class=\"space-y-1 text-right flex-1 flex flex-col items-end\">\r\n <!-- Header info -->\r\n <div class=\"flex items-center gap-1.5 text-2xs text-body-subtle font-medium\">\r\n @if (time()) {\r\n <span>{{ time() }}</span>\r\n }\r\n @if (senderName()) {\r\n <span class=\"font-bold text-body\">{{ senderName() }}</span>\r\n }\r\n </div>\r\n\r\n <!-- Bubble message content -->\r\n <div\r\n class=\"p-3.5 rounded-md rounded-tr-xs bg-brand text-white text-xs leading-relaxed text-left shadow-xs max-w-md\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n\r\n <!-- Message status icon -->\r\n @if (status()) {\r\n <div class=\"flex items-center gap-1 text-2xs text-fg-brand\">\r\n <ol-icon [name]=\"statusInfo().icon\" size=\"xs\" />\r\n <span class=\"text-2xs\">{{ statusInfo().nhan }}</span>\r\n </div>\r\n }\r\n </div>\r\n\r\n <ol-avatar\r\n [src]=\"avatar()\"\r\n [text]=\"avatarText() || 'U'\"\r\n size=\"xs\"\r\n color=\"brand\"\r\n class=\"shrink-0 mt-1\"\r\n ></ol-avatar>\r\n </div>\r\n}\r\n\r\n<!-- 2. BOT / AI SENDER (Incoming Bubble - Left aligned) -->\r\n@if (sender() === 'bot') {\r\n <div class=\"flex items-start gap-2.5 max-w-lg mr-auto\">\r\n <div\r\n class=\"w-7 h-7 rounded-md bg-brand-softer text-fg-brand flex items-center justify-center shrink-0 mt-1\"\r\n >\r\n <ol-icon name=\"sparkles\" size=\"xs\"></ol-icon>\r\n </div>\r\n\r\n <div class=\"space-y-1 flex-1\">\r\n <div class=\"flex items-center gap-1.5 text-2xs text-body-subtle font-medium\">\r\n <span class=\"font-bold text-heading\">{{ senderName() || 'Trợ lý AI Ôn Luyện' }}</span>\r\n @if (time()) {\r\n <span>• {{ time() }}</span>\r\n }\r\n </div>\r\n\r\n <div\r\n class=\"p-3.5 rounded-md rounded-tl-xs bg-neutral-tertiary text-body text-xs leading-relaxed shadow-xs max-w-md border border-default\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n\r\n<!-- 3. OTHER USER SENDER (Teacher / Peer) -->\r\n@if (sender() === 'other') {\r\n <div class=\"flex items-start gap-2.5 max-w-lg mr-auto\">\r\n <ol-avatar\r\n [src]=\"avatar()\"\r\n [text]=\"avatarText() || 'T'\"\r\n size=\"xs\"\r\n color=\"green\"\r\n class=\"shrink-0 mt-1\"\r\n ></ol-avatar>\r\n\r\n <div class=\"space-y-1 flex-1\">\r\n <div class=\"flex items-center gap-1.5 text-2xs text-body-subtle font-medium\">\r\n <span class=\"font-bold text-heading\">{{ senderName() || 'Giáo viên' }}</span>\r\n @if (time()) {\r\n <span>• {{ time() }}</span>\r\n }\r\n </div>\r\n\r\n <div\r\n class=\"p-3.5 rounded-md rounded-tl-xs bg-neutral-tertiary text-body text-xs leading-relaxed shadow-xs max-w-md\"\r\n >\r\n <ng-container [ngTemplateOutlet]=\"noiDungLong\" />\r\n </div>\r\n </div>\r\n </div>\r\n}\r\n"
|
|
34654
34948
|
}]
|
|
34655
34949
|
}],
|