@edmicro/olkit 2.1.2 → 2.1.8

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.
@@ -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=\"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
+ 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
- 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
+ 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
- doc = inject(DOCUMENT);
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 = inject(OlFocusTrapService);
1173
- overlayContainer = inject(OlOverlayContainer);
1174
- destroyRef = inject(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 = inject(ElementRef);
1594
- destroyRef = inject(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 = inject(ElementRef);
2094
- renderer = inject(Renderer2);
2095
- appRef = inject(ApplicationRef);
2096
- injector = inject(EnvironmentInjector);
2097
- positioningService = inject(OlPositioningService);
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="close" size="sm" />
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="close" size="sm" />
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 = inject(OlFocusTrapService);
3083
- destroyRef = inject(DestroyRef);
3084
- overlayContainer = inject(OlOverlayContainer);
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 = inject(OlOverlayContainer);
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=\"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",
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
- 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"
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
- el = inject(ElementRef);
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=\"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",
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
- 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"
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="question-circle" size="xs" />
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="question-circle" size="xs" />
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=\"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",
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
- 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"
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=\"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
+ 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=\"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"
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 = inject(ElementRef);
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 = inject(OlOverlayContainer);
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: [OlDatepickerStateService, {
8620
- provide: NG_VALUE_ACCESSOR,
8621
- useExisting: forwardRef(() => OlDatepicker),
8622
- multi: true
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: [OlDatepickerStateService, {
8729
- provide: NG_VALUE_ACCESSOR,
8730
- useExisting: forwardRef(() => OlDatepicker),
8731
- multi: true
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 = inject(ElementRef);
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 = inject(OlOverlayContainer);
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: [OlDatepickerStateService, {
9444
- provide: NG_VALUE_ACCESSOR,
9445
- useExisting: forwardRef(() => OlDateRangePicker),
9446
- multi: true
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: [OlDatepickerStateService, {
9553
- provide: NG_VALUE_ACCESSOR,
9554
- useExisting: forwardRef(() => OlDateRangePicker),
9555
- multi: true
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
- el = inject(ElementRef);
9817
- vcr = inject(ViewContainerRef);
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="close" size="xs" />
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="close" size="xs" />
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 = inject(OlFocusTrapService);
10244
- overlayContainer = inject(OlOverlayContainer);
10245
- destroyRef = inject(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 = inject(ElementRef);
10604
- destroyRef = inject(DestroyRef);
10605
- positioning = inject(OlPositioningService);
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 = inject(OlOverlayContainer);
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=\"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",
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
- 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"
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=\"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",
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
- 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"
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
- ngImport: i0,
14083
- 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",
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
- 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"
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 = inject(DestroyRef);
14736
- elementRef = inject(ElementRef);
14737
- overlayContainer = inject(OlOverlayContainer);
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
- ngImport: i0,
18316
- 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",
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
- 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"
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 = inject(OlFocusTrapService);
18873
- overlayContainer = inject(OlOverlayContainer);
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: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",
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
- 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"
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: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",
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
- 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"
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=\"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",
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
- 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"
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 = inject(OlOverlayContainer);
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 = inject(OlOverlayContainer);
22587
+ overlayContainer;
22366
22588
  detachPanel = null;
22367
22589
  releaseEscape = null;
22368
22590
  stopTracking = null;
22369
- destroyRef = inject(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
- sanitizer = inject(DomSanitizer);
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 = inject(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 = inject(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 = inject(ElementRef);
24449
- overlayContainer = inject(OlOverlayContainer);
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=\"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",
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=\"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"
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 = inject(ElementRef);
24617
- destroyRef = inject(DestroyRef);
24618
- overlayContainer = inject(OlOverlayContainer);
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
- ngImport: i0,
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 = inject(OlPositioningService);
25255
- destroyRef = inject(DestroyRef);
25256
- host = inject(ElementRef);
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 = inject(OlOverlayContainer);
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 = inject(ElementRef);
26370
- destroyRef = inject(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 = inject(OlOverlayContainer);
26851
- router = inject(Router);
26852
- currentUrl = toSignal(this.router.events.pipe(filter((e) => e instanceof NavigationEnd), map((e) => e.urlAfterRedirects)), { initialValue: this.router.url });
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
- router = inject(Router, { optional: true });
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: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:magnifer-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:magnifer-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",
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
- 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:magnifer-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:magnifer-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"
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=\"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",
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=\"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"
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 = inject(ElementRef);
29390
- overlayContainer = inject(OlOverlayContainer);
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=\"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",
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=\"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",
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
  }],