@edmicro/olkit 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmignore +2 -2
- package/fesm2022/edmicro-olkit.mjs +421 -42
- package/fesm2022/edmicro-olkit.mjs.map +1 -1
- package/olkit.css +1 -1
- package/package.json +1 -1
- package/styles/patterns/big-comb.svg +1 -0
- package/styles/patterns/comb-linear.svg +1 -0
- package/styles/patterns/comb.svg +1 -0
- package/styles/patterns/creative.svg +1 -0
- package/styles/patterns/diamond-left.svg +1 -0
- package/styles/patterns/diamond-right.svg +1 -0
- package/styles/patterns/dots.svg +1 -0
- package/styles/patterns/lines-modern.svg +1 -0
- package/styles/patterns/lines-oblique.svg +1 -0
- package/styles/patterns/lines.svg +1 -0
- package/styles/patterns/modern.svg +1 -0
- package/styles/patterns/oblique-lines.svg +1 -0
- package/styles/patterns/rounded-lines.svg +1 -0
- package/styles/patterns/squares.svg +1 -0
- package/styles/patterns/tech-square.svg +1 -0
- package/styles/patterns/tech-squares.svg +1 -0
- package/styles/patterns/tech.svg +1 -0
- package/styles/patterns/technology-top.svg +1 -0
- package/styles/patterns/technology.svg +1 -0
- package/styles/patterns/vertical-lines.svg +1 -0
- package/styles/patterns.css +163 -0
- package/types/edmicro-olkit.d.ts +238 -6
- package/types/edmicro-olkit.d.ts.map +1 -1
- package/version.txt +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, Service, input, signal, computed, effect, ChangeDetectionStrategy, Component, forwardRef, model, output, contentChildren, HostListener, DOCUMENT, Injectable, DestroyRef, viewChild, ElementRef, Renderer2, ApplicationRef, EnvironmentInjector, createComponent, Directive, contentChild, afterRenderEffect, ViewEncapsulation, ViewContainerRef, ContentChild, untracked, ViewChildren, ViewChild, afterNextRender, TemplateRef } from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, Service, input, signal, computed, effect, ChangeDetectionStrategy, Component, forwardRef, model, output, contentChildren, HostListener, DOCUMENT, Injectable, DestroyRef, viewChild, ElementRef, Renderer2, ApplicationRef, EnvironmentInjector, createComponent, Directive, contentChild, afterRenderEffect, ViewEncapsulation, ViewContainerRef, ContentChild, untracked, ViewChildren, ViewChild, afterNextRender, TemplateRef, isDevMode } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/common';
|
|
4
4
|
import { CommonModule, NgTemplateOutlet, NgStyle } from '@angular/common';
|
|
5
5
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
@@ -141,7 +141,7 @@ const ICON_ALIASES = {
|
|
|
141
141
|
* icon lõi thì không tải bộ nào (bộ Solar 10 MB). Khoá là tên ĐÃ CHUẨN HOÁ
|
|
142
142
|
* (chữ thường) mà component truyền vào, giá trị là SVG đã tra theo bí danh.
|
|
143
143
|
*
|
|
144
|
-
*
|
|
144
|
+
* 64 icon · olkit 24 · lucide 20 · mynaui 5 · solar 15
|
|
145
145
|
*/
|
|
146
146
|
const CORE_ICONS = {
|
|
147
147
|
'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>',
|
|
@@ -192,13 +192,15 @@ const CORE_ICONS = {
|
|
|
192
192
|
'question-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="M10.125 8.875C10.125 7.83947 10.9645 7 12 7C13.0355 7 13.875 7.83947 13.875 8.875C13.875 9.56245 13.505 10.1635 12.9534 10.4899C12.478 10.7711 12 11.1977 12 11.75V13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <circle cx="12" cy="16" r="1" fill="currentColor"/> </svg>',
|
|
193
193
|
refresh: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M3.67981 11.3333H2.92981V11.3333H3.67981ZM3.67981 13L3.15157 13.5324C3.44398 13.8225 3.91565 13.8225 4.20805 13.5324L3.67981 13ZM5.88787 11.8657C6.18191 11.574 6.18377 11.0991 5.89203 10.8051C5.60029 10.511 5.12542 10.5092 4.83138 10.8009L5.35962 11.3333L5.88787 11.8657ZM2.52824 10.8009C2.2342 10.5092 1.75933 10.511 1.46759 10.8051C1.17585 11.0991 1.17772 11.574 1.47176 11.8657L2 11.3333L2.52824 10.8009ZM18.6156 7.39279C18.8325 7.74565 19.2944 7.85585 19.6473 7.63892C20.0001 7.42199 20.1103 6.96007 19.8934 6.60721L19.2545 7L18.6156 7.39279ZM12.0789 3V2.25C7.03155 2.25 2.92981 6.3112 2.92981 11.3333H3.67981H4.42981C4.42981 7.15072 7.84884 3.75 12.0789 3.75V3ZM3.67981 11.3333H2.92981L2.92981 13H3.67981H4.42981L4.42981 11.3333H3.67981ZM3.67981 13L4.20805 13.5324L5.88787 11.8657L5.35962 11.3333L4.83138 10.8009L3.15157 12.4676L3.67981 13ZM3.67981 13L4.20805 12.4676L2.52824 10.8009L2 11.3333L1.47176 11.8657L3.15157 13.5324L3.67981 13ZM19.2545 7L19.8934 6.60721C18.287 3.99427 15.3873 2.25 12.0789 2.25V3V3.75C14.8484 3.75 17.2727 5.20845 18.6156 7.39279L19.2545 7Z" fill="currentColor"/> <path d="M20.3139 11L20.8411 10.4666C20.549 10.1778 20.0788 10.1778 19.7867 10.4666L20.3139 11ZM18.1004 12.1333C17.8058 12.4244 17.8031 12.8993 18.0942 13.1939C18.3854 13.4885 18.8603 13.4913 19.1549 13.2001L18.6277 12.6667L18.1004 12.1333ZM21.4729 13.2001C21.7675 13.4913 22.2424 13.4885 22.5335 13.1939C22.8247 12.8993 22.822 12.4244 22.5274 12.1332L22.0001 12.6667L21.4729 13.2001ZM5.31794 16.6061C5.1004 16.2536 4.6383 16.1442 4.28581 16.3618C3.93331 16.5793 3.82391 17.0414 4.04144 17.3939L4.67969 17L5.31794 16.6061ZM11.8827 21V21.75C16.9451 21.75 21.0639 17.6915 21.0639 12.6667H20.3139H19.5639C19.5639 16.8466 16.1332 20.25 11.8827 20.25V21ZM20.3139 12.6667H21.0639V11H20.3139H19.5639V12.6667H20.3139ZM20.3139 11L19.7867 10.4666L18.1004 12.1333L18.6277 12.6667L19.1549 13.2001L20.8411 11.5334L20.3139 11ZM20.3139 11L19.7867 11.5334L21.4729 13.2001L22.0001 12.6667L22.5274 12.1332L20.8411 10.4666L20.3139 11ZM4.67969 17L4.04144 17.3939C5.65405 20.007 8.56403 21.75 11.8827 21.75V21V20.25C9.10023 20.25 6.66584 18.7903 5.31794 16.6061L4.67969 17Z" fill="currentColor"/> </svg>',
|
|
194
194
|
search: '<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="M19 11.5a7.5 7.5 0 1 1-15 0a7.5 7.5 0 0 1 15 0m-2.107 5.42l3.08 3.08"/></svg>',
|
|
195
|
+
'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="M20 12H4M10 18L4 12L10 6"/></svg>',
|
|
195
196
|
'solar:check-circle-bold': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM16.0303 8.96967C16.3232 9.26256 16.3232 9.73744 16.0303 10.0303L11.0303 15.0303C10.7374 15.3232 10.2626 15.3232 9.96967 15.0303L7.96967 13.0303C7.67678 12.7374 7.67678 12.2626 7.96967 11.9697C8.26256 11.6768 8.73744 11.6768 9.03033 11.9697L10.5 13.4393L12.7348 11.2045L14.9697 8.96967C15.2626 8.67678 15.7374 8.67678 16.0303 8.96967Z" clip-rule="evenodd"/></svg>',
|
|
196
197
|
'solar:danger-circle-bold': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V13C12.75 13.4142 12.4142 13.75 12 13.75C11.5858 13.75 11.25 13.4142 11.25 13V7C11.25 6.58579 11.5858 6.25 12 6.25ZM12 17C12.5523 17 13 16.5523 13 16C13 15.4477 12.5523 15 12 15C11.4477 15 11 15.4477 11 16C11 16.5523 11.4477 17 12 17Z" clip-rule="evenodd"/></svg>',
|
|
197
198
|
'solar:eye-bold': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor"><path d="M9.75 12C9.75 10.7574 10.7574 9.75 12 9.75C13.2426 9.75 14.25 10.7574 14.25 12C14.25 13.2426 13.2426 14.25 12 14.25C10.7574 14.25 9.75 13.2426 9.75 12Z"/><path fill-rule="evenodd" d="M2 12C2 13.6394 2.42496 14.1915 3.27489 15.2957C4.97196 17.5004 7.81811 20 12 20C16.1819 20 19.028 17.5004 20.7251 15.2957C21.575 14.1915 22 13.6394 22 12C22 10.3606 21.575 9.80853 20.7251 8.70433C19.028 6.49956 16.1819 4 12 4C7.81811 4 4.97196 6.49956 3.27489 8.70433C2.42496 9.80853 2 10.3606 2 12ZM12 8.25C9.92893 8.25 8.25 9.92893 8.25 12C8.25 14.0711 9.92893 15.75 12 15.75C14.0711 15.75 15.75 14.0711 15.75 12C15.75 9.92893 14.0711 8.25 12 8.25Z" clip-rule="evenodd"/></g></svg>',
|
|
198
199
|
'solar:gallery-add-linear': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path d="M22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2"/><path d="M2 12.5001L3.75159 10.9675C4.66286 10.1702 6.03628 10.2159 6.89249 11.0721L11.1822 15.3618C11.8694 16.0491 12.9512 16.1428 13.7464 15.5839L14.0446 15.3744C15.1888 14.5702 16.7369 14.6634 17.7765 15.599L21 18.5001"/><path d="M15 5.5H18.5M18.5 5.5H22M18.5 5.5V9M18.5 5.5V2"/></g></svg>',
|
|
199
200
|
'solar:hamburger-menu-linear': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path d="M20 7L4 7"/><path d="M20 12L4 12"/><path d="M20 17L4 17"/></g></svg>',
|
|
200
201
|
'solar:lock-password-linear': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 16C2 13.1716 2 11.7574 2.87868 10.8787C3.75736 10 5.17157 10 8 10H16C18.8284 10 20.2426 10 21.1213 10.8787C22 11.7574 22 13.1716 22 16C22 18.8284 22 20.2426 21.1213 21.1213C20.2426 22 18.8284 22 16 22H8C5.17157 22 3.75736 22 2.87868 21.1213C2 20.2426 2 18.8284 2 16Z"/><path stroke-linecap="round" d="M6 10V8C6 4.68629 8.68629 2 12 2C15.3137 2 18 4.68629 18 8V10"/><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H8.009M11.991 16H12M15.991 16H16"/><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H8.009M11.991 16H12M15.991 16H16"/></g></svg>',
|
|
201
|
-
'solar:logout-
|
|
202
|
+
'solar:logout-linear': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path d="M12 20C7.58172 20 4 16.4183 4 12C4 7.58172 7.58172 4 12 4"/><path stroke-linejoin="round" d="M10 12H20M17 15L20 12L17 9"/></g></svg>',
|
|
203
|
+
'solar:magnifer-linear': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11.5" cy="11.5" r="9.5"/><path stroke-linecap="round" d="M18.5 18.5L22 22"/></g></svg>',
|
|
202
204
|
'solar:refresh-bold': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor"><path d="M12.0789 2.25C7.2854 2.25 3.34478 5.913 2.96055 10.5833H2.00002C1.69614 10.5833 1.42229 10.7667 1.30655 11.0477C1.19081 11.3287 1.25606 11.6517 1.47178 11.8657L3.15159 13.5324C3.444 13.8225 3.91567 13.8225 4.20808 13.5324L5.88789 11.8657C6.10361 11.6517 6.16886 11.3287 6.05312 11.0477C5.93738 10.7667 5.66353 10.5833 5.35965 10.5833H4.4668C4.84652 6.75167 8.10479 3.75 12.0789 3.75C14.8484 3.75 17.2727 5.20845 18.6156 7.39279C18.8325 7.74565 19.2944 7.85585 19.6473 7.63892C20.0002 7.42199 20.1104 6.96007 19.8934 6.60721C18.2871 3.99427 15.3873 2.25 12.0789 2.25Z"/><path d="M20.8411 10.4666C20.549 10.1778 20.0789 10.1778 19.7867 10.4666L18.1005 12.1333C17.8841 12.3471 17.8184 12.6703 17.9339 12.9517C18.0495 13.233 18.3235 13.4167 18.6277 13.4167H19.5268C19.1455 17.2462 15.8759 20.25 11.8828 20.25C9.10026 20.25 6.66586 18.7903 5.31796 16.6061C5.10042 16.2536 4.63833 16.1442 4.28583 16.3618C3.93334 16.5793 3.82393 17.0414 4.04146 17.3939C5.65407 20.007 8.56406 21.75 11.8828 21.75C16.6906 21.75 20.6475 18.0892 21.0331 13.4167H22.0002C22.3043 13.4167 22.5783 13.233 22.6939 12.9517C22.8095 12.6703 22.7437 12.3471 22.5274 12.1333L20.8411 10.4666Z"/></g></svg>',
|
|
203
205
|
'solar:trash-bin-trash-bold': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor"><path d="M3 6.38597C3 5.90152 3.34538 5.50879 3.77143 5.50879L6.43567 5.50832C6.96502 5.49306 7.43202 5.11033 7.61214 4.54412C7.61688 4.52923 7.62232 4.51087 7.64185 4.44424L7.75665 4.05256C7.8269 3.81241 7.8881 3.60318 7.97375 3.41617C8.31209 2.67736 8.93808 2.16432 9.66147 2.03297C9.84457 1.99972 10.0385 1.99986 10.2611 2.00002H13.7391C13.9617 1.99986 14.1556 1.99972 14.3387 2.03297C15.0621 2.16432 15.6881 2.67736 16.0264 3.41617C16.1121 3.60318 16.1733 3.81241 16.2435 4.05256L16.3583 4.44424C16.3778 4.51087 16.3833 4.52923 16.388 4.54412C16.5682 5.11033 17.1278 5.49353 17.6571 5.50879H20.2286C20.6546 5.50879 21 5.90152 21 6.38597C21 6.87043 20.6546 7.26316 20.2286 7.26316H3.77143C3.34538 7.26316 3 6.87043 3 6.38597Z"/><path fill-rule="evenodd" d="M11.5956 22.0001H12.4044C15.1871 22.0001 16.5785 22.0001 17.4831 21.1142C18.3878 20.2283 18.4803 18.7751 18.6654 15.8686L18.9321 11.6807C19.0326 10.1037 19.0828 9.31524 18.6289 8.81558C18.1751 8.31592 17.4087 8.31592 15.876 8.31592H8.12404C6.59127 8.31592 5.82488 8.31592 5.37105 8.81558C4.91722 9.31524 4.96744 10.1037 5.06788 11.6807L5.33459 15.8686C5.5197 18.7751 5.61225 20.2283 6.51689 21.1142C7.42153 22.0001 8.81289 22.0001 11.5956 22.0001ZM10.2463 12.1886C10.2051 11.7548 9.83753 11.4382 9.42537 11.4816C9.01321 11.525 8.71251 11.9119 8.75372 12.3457L9.25372 17.6089C9.29494 18.0427 9.66247 18.3593 10.0746 18.3159C10.4868 18.2725 10.7875 17.8856 10.7463 17.4518L10.2463 12.1886ZM14.5746 11.4816C14.9868 11.525 15.2875 11.9119 15.2463 12.3457L14.7463 17.6089C14.7051 18.0427 14.3375 18.3593 13.9254 18.3159C13.5132 18.2725 13.2125 17.8856 13.2537 17.4518L13.7537 12.1886C13.7949 11.7548 14.1625 11.4382 14.5746 11.4816Z" clip-rule="evenodd"/></g></svg>',
|
|
204
206
|
'solar:upload-minimalistic-linear': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="M3 15C3 17.8284 3 19.2426 3.87868 20.1213C4.75736 21 6.17157 21 9 21H15C17.8284 21 19.2426 21 20.1213 20.1213C21 19.2426 21 17.8284 21 15"/><path d="M12 16V3M8 7.375L12 3L16 7.375"/></g></svg>',
|
|
@@ -2656,6 +2658,10 @@ class OlTooltipDirective {
|
|
|
2656
2658
|
showTimeout;
|
|
2657
2659
|
hideTimeout;
|
|
2658
2660
|
touchTimeout;
|
|
2661
|
+
/** Hẹn hiện tooltip khi NHẤN GIỮ — huỷ nếu nhấc tay sớm (chạm nhanh). */
|
|
2662
|
+
longPressTimeout;
|
|
2663
|
+
/** Mốc chạm gần nhất — bỏ qua mouseenter/focus trình duyệt tự sinh sau cú chạm. */
|
|
2664
|
+
lastTouchAt = 0;
|
|
2659
2665
|
tooltipComponent;
|
|
2660
2666
|
tooltipElement;
|
|
2661
2667
|
cleanupAutoUpdate;
|
|
@@ -2666,6 +2672,7 @@ class OlTooltipDirective {
|
|
|
2666
2672
|
blurListener;
|
|
2667
2673
|
touchStartListener;
|
|
2668
2674
|
touchEndListener;
|
|
2675
|
+
touchCancelListeners = [];
|
|
2669
2676
|
constructor() {
|
|
2670
2677
|
// Setup event listeners
|
|
2671
2678
|
this.setupListeners();
|
|
@@ -2687,7 +2694,7 @@ class OlTooltipDirective {
|
|
|
2687
2694
|
const element = this.elementRef.nativeElement;
|
|
2688
2695
|
// Mouse events
|
|
2689
2696
|
this.mouseEnterListener = this.renderer.listen(element, 'mouseenter', () => {
|
|
2690
|
-
if (!this.tooltipDisabled()) {
|
|
2697
|
+
if (!this.tooltipDisabled() && !this.fromRecentTouch()) {
|
|
2691
2698
|
this.scheduleShow();
|
|
2692
2699
|
}
|
|
2693
2700
|
});
|
|
@@ -2696,31 +2703,49 @@ class OlTooltipDirective {
|
|
|
2696
2703
|
});
|
|
2697
2704
|
// Focus events (keyboard accessibility)
|
|
2698
2705
|
this.focusListener = this.renderer.listen(element, 'focus', () => {
|
|
2699
|
-
if (!this.tooltipDisabled()) {
|
|
2706
|
+
if (!this.tooltipDisabled() && !this.fromRecentTouch()) {
|
|
2700
2707
|
this.show(); // Show immediately on focus
|
|
2701
2708
|
}
|
|
2702
2709
|
});
|
|
2703
2710
|
this.blurListener = this.renderer.listen(element, 'blur', () => {
|
|
2704
2711
|
this.hide(true); // Hide immediately on blur
|
|
2705
2712
|
});
|
|
2706
|
-
// Touch
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2713
|
+
// Touch: chạm NHANH là bấm — không hiện tooltip, KHÔNG preventDefault.
|
|
2714
|
+
// Bản trước gọi `preventDefault()` ở touchstart: trình duyệt huỷ luôn cú
|
|
2715
|
+
// `click` sinh từ cú chạm, nên MỌI `ol-button` chỉ-icon (tự gắn tooltip
|
|
2716
|
+
// từ `ariaLabel`) không bấm được trên điện thoại (đo 2026-10-06: bộ chọn
|
|
2717
|
+
// Giao diện trong khay menu onluyenv4). Tooltip chỉ hiện khi NHẤN GIỮ
|
|
2718
|
+
// 500ms, tự ẩn sau 2s.
|
|
2719
|
+
this.touchStartListener = this.renderer.listen(element, 'touchstart', () => {
|
|
2720
|
+
this.lastTouchAt = Date.now();
|
|
2721
|
+
if (this.tooltipDisabled())
|
|
2722
|
+
return;
|
|
2723
|
+
this.clearLongPress();
|
|
2724
|
+
this.longPressTimeout = setTimeout(() => {
|
|
2725
|
+
this.longPressTimeout = undefined;
|
|
2710
2726
|
this.show();
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2727
|
+
this.touchTimeout = setTimeout(() => this.hide(), 2000);
|
|
2728
|
+
}, 500);
|
|
2729
|
+
}, { passive: true });
|
|
2730
|
+
const endTouch = () => {
|
|
2731
|
+
this.lastTouchAt = Date.now();
|
|
2732
|
+
this.clearLongPress();
|
|
2733
|
+
};
|
|
2734
|
+
this.touchEndListener = this.renderer.listen(element, 'touchend', endTouch);
|
|
2735
|
+
this.touchCancelListeners = [
|
|
2736
|
+
this.renderer.listen(element, 'touchcancel', endTouch),
|
|
2737
|
+
this.renderer.listen(element, 'touchmove', endTouch, { passive: true }),
|
|
2738
|
+
];
|
|
2739
|
+
}
|
|
2740
|
+
/** Sự kiện chuột/focus này có phải do cú chạm vừa rồi sinh ra không (≤ 800ms). */
|
|
2741
|
+
fromRecentTouch() {
|
|
2742
|
+
return Date.now() - this.lastTouchAt < 800;
|
|
2743
|
+
}
|
|
2744
|
+
clearLongPress() {
|
|
2745
|
+
if (this.longPressTimeout) {
|
|
2746
|
+
clearTimeout(this.longPressTimeout);
|
|
2747
|
+
this.longPressTimeout = undefined;
|
|
2748
|
+
}
|
|
2724
2749
|
}
|
|
2725
2750
|
/**
|
|
2726
2751
|
* Remove event listeners
|
|
@@ -2732,6 +2757,7 @@ class OlTooltipDirective {
|
|
|
2732
2757
|
this.blurListener?.();
|
|
2733
2758
|
this.touchStartListener?.();
|
|
2734
2759
|
this.touchEndListener?.();
|
|
2760
|
+
this.touchCancelListeners.forEach((off) => off());
|
|
2735
2761
|
}
|
|
2736
2762
|
/**
|
|
2737
2763
|
* Schedule tooltip show with delay
|
|
@@ -2905,6 +2931,7 @@ class OlTooltipDirective {
|
|
|
2905
2931
|
clearTimeout(this.touchTimeout);
|
|
2906
2932
|
this.touchTimeout = undefined;
|
|
2907
2933
|
}
|
|
2934
|
+
this.clearLongPress();
|
|
2908
2935
|
// Destroy tooltip
|
|
2909
2936
|
this.destroyTooltip();
|
|
2910
2937
|
}
|
|
@@ -2919,6 +2946,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
2919
2946
|
}]
|
|
2920
2947
|
}], ctorParameters: () => [], propDecorators: { olTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "olTooltip", required: true }] }], tooltipPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipPosition", required: false }] }], tooltipTheme: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipTheme", required: false }] }], tooltipDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipDelay", required: false }] }], tooltipDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipDisabled", required: false }] }], tooltipArrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipArrow", required: false }] }] } });
|
|
2921
2948
|
|
|
2949
|
+
/**
|
|
2950
|
+
* Ghép class, bỏ `shadow-xs` của lớp nền khi biến thể đã chọn `shadow-none`
|
|
2951
|
+
* (ghost, disabled). Để cả hai thì `shadow-xs` thắng theo thứ tự CSS — nút
|
|
2952
|
+
* ghost từng mang bóng mờ dù khai `shadow-none` (đo 2026-10-06, nút Đăng xuất
|
|
2953
|
+
* chỉ-icon trong khay menu mobile).
|
|
2954
|
+
*/
|
|
2955
|
+
function joinButtonClasses(classes) {
|
|
2956
|
+
const out = classes.includes('shadow-none') ? classes.filter((c) => c !== 'shadow-xs') : classes;
|
|
2957
|
+
return out.join(' ');
|
|
2958
|
+
}
|
|
2922
2959
|
class OlButton {
|
|
2923
2960
|
color = input('brand', /* @ts-ignore */
|
|
2924
2961
|
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
@@ -3102,7 +3139,7 @@ class OlButton {
|
|
|
3102
3139
|
// Figma State=Disabled: fills=bg-disabled, strokes=border-default-medium,
|
|
3103
3140
|
// chữ text-fg-disabled. Token tự đổi theo chế độ nên bỏ 2 class dark:.
|
|
3104
3141
|
classes.push('bg-disabled', 'text-fg-disabled', 'border', 'border-default-medium', 'shadow-none', 'cursor-not-allowed', 'pointer-events-none');
|
|
3105
|
-
return classes
|
|
3142
|
+
return joinButtonClasses(classes);
|
|
3106
3143
|
}
|
|
3107
3144
|
// Colors & Outline Modes (Matching Figma Ôn Luyện Design System)
|
|
3108
3145
|
if (isOutline) {
|
|
@@ -3224,7 +3261,7 @@ class OlButton {
|
|
|
3224
3261
|
break;
|
|
3225
3262
|
}
|
|
3226
3263
|
}
|
|
3227
|
-
return classes
|
|
3264
|
+
return joinButtonClasses(classes);
|
|
3228
3265
|
}, /* @ts-ignore */
|
|
3229
3266
|
...(ngDevMode ? [{ debugName: "buttonClasses" }] : /* istanbul ignore next */ []));
|
|
3230
3267
|
handleClick(event) {
|
|
@@ -17161,7 +17198,7 @@ const APP_NAV_DEFAULT_USER_ACTIONS = [
|
|
|
17161
17198
|
{
|
|
17162
17199
|
id: 'logout',
|
|
17163
17200
|
label: 'Đăng xuất',
|
|
17164
|
-
icon: 'solar:logout-
|
|
17201
|
+
icon: 'solar:logout-linear',
|
|
17165
17202
|
danger: true,
|
|
17166
17203
|
dividerBefore: true,
|
|
17167
17204
|
},
|
|
@@ -17328,6 +17365,7 @@ class OlAppNav {
|
|
|
17328
17365
|
...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
|
|
17329
17366
|
/**
|
|
17330
17367
|
* Số mục lên thanh đáy mobile (đầu nhóm `main`); ô cuối luôn là nút Menu.
|
|
17368
|
+
* Bỏ qua khi `config.mobileBar` có khai — khi đó danh sách quyết định.
|
|
17331
17369
|
*
|
|
17332
17370
|
* Bốn: màn 375px chia 5 ô mỗi ô 75px — vừa icon 24px và một dòng nhãn 10px.
|
|
17333
17371
|
* Sáu ô thì mỗi ô 62px, nhãn bắt đầu bị cắt.
|
|
@@ -17337,6 +17375,16 @@ class OlAppNav {
|
|
|
17337
17375
|
/** Khay menu mobile đang mở. Hai chiều: `[(mobileMenuOpen)]` — khung đóng khi chuyển trang. */
|
|
17338
17376
|
mobileMenuOpen = model(false, /* @ts-ignore */
|
|
17339
17377
|
...(ngDevMode ? [{ debugName: "mobileMenuOpen" }] : /* istanbul ignore next */ []));
|
|
17378
|
+
/**
|
|
17379
|
+
* Chỗ đặt menu tài khoản trong khay mobile:
|
|
17380
|
+
* - `bottom` (mặc định, như các bản trước): trọn menu ở cuối khay.
|
|
17381
|
+
* - `split`: việc thường (Trang cá nhân, Đổi mật khẩu… — mục không `danger`)
|
|
17382
|
+
* lên ĐẦU khay ngay dưới thẻ tài khoản; mục `danger` (Đăng xuất) thành nút
|
|
17383
|
+
* CHỈ ICON đỏ bên phải thẻ tài khoản. Template `olAppNavUserMenu` nhận
|
|
17384
|
+
* `part: 'top' | 'bottom'` để tự chia — và KHÔNG vẽ mục danger ở `bottom`.
|
|
17385
|
+
*/
|
|
17386
|
+
mobileUserMenu = input('bottom', /* @ts-ignore */
|
|
17387
|
+
...(ngDevMode ? [{ debugName: "mobileUserMenu" }] : /* istanbul ignore next */ []));
|
|
17340
17388
|
/** Chữ trên nút Menu của thanh đáy. */
|
|
17341
17389
|
mobileMenuLabel = input('Menu', /* @ts-ignore */
|
|
17342
17390
|
...(ngDevMode ? [{ debugName: "mobileMenuLabel" }] : /* istanbul ignore next */ []));
|
|
@@ -17381,13 +17429,35 @@ class OlAppNav {
|
|
|
17381
17429
|
.flatMap((g) => g.items), /* @ts-ignore */
|
|
17382
17430
|
...(ngDevMode ? [{ debugName: "footerItems" }] : /* istanbul ignore next */ []));
|
|
17383
17431
|
/**
|
|
17384
|
-
* Mục lên thanh đáy mobile
|
|
17385
|
-
*
|
|
17386
|
-
*
|
|
17387
|
-
|
|
17388
|
-
|
|
17389
|
-
|
|
17390
|
-
|
|
17432
|
+
* Mục lên thanh đáy mobile. Có `config.mobileBar` → đúng các id đó, đúng
|
|
17433
|
+
* thứ tự đó (mọi nhóm, mọi cấp). Không có → `mobilePrimaryCount` mục gốc ĐẦU
|
|
17434
|
+
* TIÊN của phần trên. Cả hai chỉ nhận mục có `route` và KHÔNG có con — ô
|
|
17435
|
+
* thanh đáy là một cú chạm điều hướng, không chứa được menu con.
|
|
17436
|
+
*/
|
|
17437
|
+
primaryItems = computed(() => {
|
|
17438
|
+
const ids = this.config().mobileBar;
|
|
17439
|
+
if (!ids) {
|
|
17440
|
+
return this.mainItems()
|
|
17441
|
+
.filter((i) => !!i.route && !i.children?.length)
|
|
17442
|
+
.slice(0, this.mobilePrimaryCount());
|
|
17443
|
+
}
|
|
17444
|
+
// `mobileBar`: theo đúng thứ tự khai, tìm ở mọi nhóm, mọi cấp.
|
|
17445
|
+
const byId = new Map();
|
|
17446
|
+
const collect = (items) => items.forEach((i) => {
|
|
17447
|
+
byId.set(i.id, i);
|
|
17448
|
+
if (i.children)
|
|
17449
|
+
collect(i.children);
|
|
17450
|
+
});
|
|
17451
|
+
collect(this.allGroups().flatMap((g) => g.items));
|
|
17452
|
+
return ids.flatMap((id) => {
|
|
17453
|
+
const item = byId.get(id);
|
|
17454
|
+
const ok = !!item?.route && !item.children?.length;
|
|
17455
|
+
if (!ok && isDevMode()) {
|
|
17456
|
+
console.warn(`[ol-app-nav] mobileBar: "${id}" ${item ? 'không có route hoặc là mục cha' : 'không có trong groups'} — bỏ qua.`);
|
|
17457
|
+
}
|
|
17458
|
+
return ok ? [item] : [];
|
|
17459
|
+
});
|
|
17460
|
+
}, /* @ts-ignore */
|
|
17391
17461
|
...(ngDevMode ? [{ debugName: "primaryItems" }] : /* istanbul ignore next */ []));
|
|
17392
17462
|
/**
|
|
17393
17463
|
* Nhóm vào khay mobile: mọi nhóm (kể cả `footer` — dưới `md` không còn menu
|
|
@@ -17562,11 +17632,28 @@ class OlAppNav {
|
|
|
17562
17632
|
return out;
|
|
17563
17633
|
}
|
|
17564
17634
|
/** Ngữ cảnh template menu tài khoản — popup desktop và khay mobile. */
|
|
17565
|
-
userMenuPopupContext = computed(() => this.buildUserMenuContext('popup'), /* @ts-ignore */
|
|
17635
|
+
userMenuPopupContext = computed(() => this.buildUserMenuContext('popup', 'all'), /* @ts-ignore */
|
|
17566
17636
|
...(ngDevMode ? [{ debugName: "userMenuPopupContext" }] : /* istanbul ignore next */ []));
|
|
17567
|
-
userMenuSheetContext = computed(() => this.buildUserMenuContext('sheet'), /* @ts-ignore */
|
|
17637
|
+
userMenuSheetContext = computed(() => this.buildUserMenuContext('sheet', this.mobileUserMenu() === 'split' ? 'bottom' : 'all'), /* @ts-ignore */
|
|
17568
17638
|
...(ngDevMode ? [{ debugName: "userMenuSheetContext" }] : /* istanbul ignore next */ []));
|
|
17569
|
-
buildUserMenuContext(
|
|
17639
|
+
userMenuSheetTopContext = computed(() => this.buildUserMenuContext('sheet', 'top'), /* @ts-ignore */
|
|
17640
|
+
...(ngDevMode ? [{ debugName: "userMenuSheetTopContext" }] : /* istanbul ignore next */ []));
|
|
17641
|
+
/** Mục `danger` (Đăng xuất) — `split`: nút icon đỏ trên thẻ tài khoản đầu khay. */
|
|
17642
|
+
cardActions = computed(() => this.userActions().filter((a) => a.danger), /* @ts-ignore */
|
|
17643
|
+
...(ngDevMode ? [{ debugName: "cardActions" }] : /* istanbul ignore next */ []));
|
|
17644
|
+
/**
|
|
17645
|
+
* Mục tài khoản mặc định (không template) cho phần khay đang vẽ. `split`:
|
|
17646
|
+
* việc thường ở `top`, mục danger đã lên thẻ tài khoản nên `bottom` rỗng.
|
|
17647
|
+
*/
|
|
17648
|
+
sheetActionsFor(part) {
|
|
17649
|
+
const all = this.userActions();
|
|
17650
|
+
if (part === 'top')
|
|
17651
|
+
return all.filter((a) => !a.danger);
|
|
17652
|
+
if (part === 'bottom')
|
|
17653
|
+
return [];
|
|
17654
|
+
return all;
|
|
17655
|
+
}
|
|
17656
|
+
buildUserMenuContext(variant, part) {
|
|
17570
17657
|
const user = this.config().user;
|
|
17571
17658
|
if (!user)
|
|
17572
17659
|
return null;
|
|
@@ -17577,6 +17664,7 @@ class OlAppNav {
|
|
|
17577
17664
|
select: (action) => this.onUserAction(action),
|
|
17578
17665
|
close: () => variant === 'popup' ? this.userMenuOpen.set(false) : this.mobileMenuOpen.set(false),
|
|
17579
17666
|
variant,
|
|
17667
|
+
part,
|
|
17580
17668
|
};
|
|
17581
17669
|
}
|
|
17582
17670
|
/** Đường kẻ chỉ vẽ khi phần dưới có mục — không thì thừa một vạch. */
|
|
@@ -17702,8 +17790,12 @@ class OlAppNav {
|
|
|
17702
17790
|
}
|
|
17703
17791
|
/** Một dòng trong khay mobile. */
|
|
17704
17792
|
sheetItemClasses(item, level = 1) {
|
|
17705
|
-
|
|
17706
|
-
|
|
17793
|
+
// 40px · chữ 14px · icon 16px — cùng nhịp với ol-dropdown-item của menu tài
|
|
17794
|
+
// khoản trong khay. Thẻ nổi (2026-10-06): mỗi dòng bo `rounded-md` riêng,
|
|
17795
|
+
// nền rê chuột / đang chọn là một viên bo tròn chứ không kéo hết bề ngang.
|
|
17796
|
+
// Cấp 2 thụt ps-8 = 8 + icon 16 + khe 8: chữ thẳng hàng chữ cấp 1.
|
|
17797
|
+
const indent = level === 1 ? 'ps-2' : level === 2 ? 'ps-8' : 'ps-12';
|
|
17798
|
+
const base = `flex h-10 w-full cursor-pointer items-center gap-2 rounded-md pe-2 text-start text-sm font-medium transition-colors ${indent}`;
|
|
17707
17799
|
if (item.disabled)
|
|
17708
17800
|
return `${base} opacity-40 pointer-events-none text-body`;
|
|
17709
17801
|
if (item.children?.length) {
|
|
@@ -17724,7 +17816,7 @@ class OlAppNav {
|
|
|
17724
17816
|
* nên chữ kéo xuống `fg-danger-strong` cho khớp nền.
|
|
17725
17817
|
*/
|
|
17726
17818
|
sheetActionClasses(action) {
|
|
17727
|
-
const base = 'flex w-full cursor-pointer items-center gap-
|
|
17819
|
+
const base = 'flex h-10 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-start text-sm font-medium transition-colors';
|
|
17728
17820
|
return action.danger
|
|
17729
17821
|
? `${base} text-fg-danger hover:bg-danger-soft hover:text-fg-danger-strong`
|
|
17730
17822
|
: `${base} text-body hover:bg-neutral-secondary`;
|
|
@@ -17752,7 +17844,7 @@ class OlAppNav {
|
|
|
17752
17844
|
this.collapsed.update((v) => !v);
|
|
17753
17845
|
}
|
|
17754
17846
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17755
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAppNav, isStandalone: true, selector: "ol-app-nav", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, logoSrc: { classPropertyName: "logoSrc", publicName: "logoSrc", isSignal: true, isRequired: false, transformFunction: null }, logoMarkSrc: { classPropertyName: "logoMarkSrc", publicName: "logoMarkSrc", isSignal: true, isRequired: false, transformFunction: null }, logoTone: { classPropertyName: "logoTone", publicName: "logoTone", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, userMenuOpen: { classPropertyName: "userMenuOpen", publicName: "userMenuOpen", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, mobilePrimaryCount: { classPropertyName: "mobilePrimaryCount", publicName: "mobilePrimaryCount", isSignal: true, isRequired: false, transformFunction: null }, mobileMenuOpen: { classPropertyName: "mobileMenuOpen", publicName: "mobileMenuOpen", isSignal: true, isRequired: false, transformFunction: null }, mobileMenuLabel: { classPropertyName: "mobileMenuLabel", publicName: "mobileMenuLabel", isSignal: true, isRequired: false, transformFunction: null }, showRail: { classPropertyName: "showRail", publicName: "showRail", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", itemClick: "itemClick", userClick: "userClick", userAction: "userAction", userMenuOpen: "userMenuOpenChange", mobileMenuOpen: "mobileMenuOpenChange" }, host: { classAttribute: "block" }, queries: [{ propertyName: "itemDef", first: true, predicate: OlAppNavItemDef, descendants: true, isSignal: true }, { propertyName: "headerDef", first: true, predicate: OlAppNavHeaderDef, descendants: true, isSignal: true }, { propertyName: "footerDef", first: true, predicate: OlAppNavFooterDef, descendants: true, isSignal: true }, { propertyName: "logoDef", first: true, predicate: OlAppNavLogoDef, descendants: true, isSignal: true }, { propertyName: "userDef", first: true, predicate: OlAppNavUserDef, descendants: true, isSignal: true }, { propertyName: "userMenuDef", first: true, predicate: OlAppNavUserMenuDef, descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n [class]=\"\n 'relative h-full flex-col justify-between rounded-2xl pt-5 transition-[width] duration-200 ' +\n railDisplay()\n \"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 15px khi thu g\u1ECDn: `<ul>` l\u00E0 flex-col n\u00EAn `<li>` k\u00E9o gi\u00E3n m\u1EE5c\n con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-10` m\u1ED9t m\u00ECnh kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 15\u00D72 = 40, \u0111\u00FAng\n \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c (40px \u2014 b\u1EA3n g\u1ECDn 2026-10-06, Figma 44px / 13px).\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex min-h-0 flex-1 flex-col gap-5 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoDef(); as logo) {\n <ng-container\n [ngTemplateOutlet]=\"logo.template\"\n [ngTemplateOutletContext]=\"{ $implicit: collapsed(), collapsed: collapsed() }\"\n />\n } @else if (logoSrc()) {\n <img\n [src]=\"collapsed() && logoMarkSrc() ? logoMarkSrc() : logoSrc()\"\n alt=\"\u00D4n Luy\u1EC7n\"\n [class]=\"collapsed() ? 'size-10 object-contain' : 'h-10 w-auto max-w-full object-contain'\"\n />\n } @else {\n <!--\n Logo c\u1EE7a kit (Figma `Logo ol` #6603:1269). Thu g\u1ECDn: b\u1EA3n VU\u00D4NG 40px \u2014\n b\u1EA3n d\u00E0i r\u1ED9ng ~164px, \u1EDF khung 70px tr\u00E0n h\u1EB3n ra ngo\u00E0i. M\u1EDF: b\u1EA3n d\u00E0i cao\n 32px (~164px, Figma app-nav \u0111\u1EB7t logo r\u1ED9ng 155px).\n -->\n @if (collapsed()) {\n <ol-logo variant=\"mark\" [tone]=\"logoTone()\" [height]=\"40\" />\n } @else {\n <ol-logo variant=\"full\" [tone]=\"logoTone()\" [height]=\"32\" />\n }\n }\n </div>\n\n @if (headerDef(); as header) {\n <ng-container\n [ngTemplateOutlet]=\"header.template\"\n [ngTemplateOutletContext]=\"slotContext('rail')\"\n />\n }\n\n <!--\n V\u00F9ng m\u1EE5c CU\u1ED8N \u0110\u01AF\u1EE2C: menu nhi\u1EC1u nh\u00F3m (qu\u1EA3n tr\u1ECB tr\u01B0\u1EDDng: 5 nh\u00F3m, menu con 3\n c\u1EA5p) cao h\u01A1n m\u00E0n h\u00ECnh. Logo v\u00E0 th\u1EBB t\u00E0i kho\u1EA3n \u0111\u1EE9ng y\u00EAn, ch\u1EC9 danh s\u00E1ch cu\u1ED9n.\n\n Thanh cu\u1ED9n M\u1EA2NH, tr\u1EAFng m\u1EDD \u2014 KH\u00D4NG \u1EA9n h\u1EB3n: \u0111o menu gi\u00E1o vi\u00EAn (10 + 5 m\u1EE5c)\n \u1EDF m\u00E0n cao 900px, 3 m\u1EE5c cu\u1ED1i n\u1EB1m d\u01B0\u1EDBi m\u00E9p m\u00E0 kh\u00F4ng c\u00F3 d\u1EA5u hi\u1EC7u g\u00EC cho\n bi\u1EBFt c\u00F2n n\u1EEFa. Thanh m\u1EB7c \u0111\u1ECBnh x\u00E1m \u0111\u1EB7c th\u00EC ch\u01B0\u1EDBng tr\u00EAn n\u1EC1n navy.\n -->\n <div\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]\"\n >\n @for (group of mainGroups(); track $index) {\n <div class=\"flex flex-col gap-1\">\n @if (group.label && !collapsed()) {\n <p class=\"px-2.5 pt-1 pb-0.5 text-xs font-bold uppercase tracking-wide text-white/70\">\n {{ group.label }}\n </p>\n }\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\n />\n </div>\n }\n\n @if (footerDef(); as footer) {\n <ng-container\n [ngTemplateOutlet]=\"footer.template\"\n [ngTemplateOutletContext]=\"slotContext('rail')\"\n />\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex shrink-0 flex-col gap-1 overflow-hidden rounded-b-2xl pt-2 pb-4\"\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: footerItems(), level: 1 }\"\n />\n\n <!--\n TH\u1EBA T\u00C0I KHO\u1EA2N \u2014 b\u1EA5m m\u1EDF menu t\u00E0i kho\u1EA3n (popup n\u1ED5i `ol-dropdown`).\n\n Popup ch\u1EE9 kh\u00F4ng \u0111\u1ED5i danh s\u00E1ch t\u1EA1i ch\u1ED7: \u1EDF b\u1EC1 r\u1ED9ng 70px (thu g\u1ECDn) kh\u00F4ng c\u00F3\n ch\u1ED7 cho ba d\u00F2ng ch\u1EEF; popup n\u1ED5i ra ngo\u00E0i menu th\u00EC \u0111\u1ECDc \u0111\u01B0\u1EE3c \u1EDF c\u1EA3 hai tr\u1EA1ng\n th\u00E1i. Lu\u00F4n m\u1EDF SANG PH\u1EA2I, c\u1EA1nh sidebar (`right-end`: m\u00E9p d\u01B0\u1EDBi popup th\u1EB3ng\n m\u00E9p d\u01B0\u1EDBi th\u1EBB, popup m\u1ECDc l\u00EAn tr\u00EAn v\u00EC th\u1EBB n\u1EB1m s\u00E1t \u0111\u00E1y) \u2014 ng\u01B0\u1EDDi d\u00F9ng y\u00EAu\n c\u1EA7u 2026-10-06, b\u1EA3n tr\u01B0\u1EDBc m\u1EDF l\u00EAn tr\u00EAn \u0111\u00E8 l\u00EAn ch\u00EDnh menu khi m\u1EDF r\u1ED9ng.\n\n `user.actions = []` th\u00EC kh\u00F4ng c\u00F3 popup \u2014 b\u1EA5m th\u1EBB ch\u1EC9 ph\u00E1t `userClick`\n nh\u01B0 c\u00E1c b\u1EA3n tr\u01B0\u1EDBc.\n\n `autoClose=\"outside\"`: b\u1EA5m TRONG popup kh\u00F4ng t\u1EF1 \u0111\u00F3ng \u2014 ru\u1ED9t tu\u1EF3 bi\u1EBFn c\u00F3\n th\u1EC3 ch\u1EE9a \u0111i\u1EC1u khi\u1EC3n (b\u1ED9 ch\u1ECDn S\u00E1ng/T\u1ED1i) m\u00E0 ng\u01B0\u1EDDi d\u00F9ng c\u1EA7n th\u1EA5y k\u1EBFt qu\u1EA3.\n M\u1EE5c h\u00E0nh \u0111\u1ED9ng t\u1EF1 \u0111\u00F3ng qua `select()` / `close()` c\u1EE7a ng\u1EEF c\u1EA3nh.\n -->\n @if (config().user; as user) {\n <div class=\"mt-2 flex\" [class]=\"collapsed() ? 'justify-center' : ''\">\n @if (userActions().length > 0 || userMenuDef()) {\n <ol-dropdown\n class=\"min-w-0\"\n [class]=\"collapsed() ? '' : 'w-full'\"\n placement=\"right-end\"\n [offset]=\"userMenuOffset()\"\n menuClass=\"w-72\"\n autoClose=\"outside\"\n [(isOpen)]=\"userMenuOpen\"\n >\n <button\n trigger\n type=\"button\"\n [class]=\"userCardClasses()\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"userMenuOpen()\"\n [attr.aria-label]=\"collapsed() ? 'T\u00E0i kho\u1EA3n ' + user.name : null\"\n (click)=\"userClick.emit()\"\n >\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: collapsed(),\n initials: userInitials(),\n variant: 'rail',\n }\"\n />\n </button>\n\n <!--\n Ru\u1ED9t popup ch\u1EC9 d\u1EF1ng khi m\u1EDF: `ol-dropdown` \u1EA9n panel b\u1EB1ng class\n `hidden`, n\u00EAn d\u1EF1ng s\u1EB5n th\u00EC t\u00EAn + email n\u1EB1m trong DOM c\u1EA3 khi menu\n thu g\u1ECDn \u2014 tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u00E0 t\u00ECm ki\u1EBFm trang v\u1EABn th\u1EA5y ch\u00FAng.\n -->\n @if (userMenuOpen()) {\n @if (userMenuDef(); as menu) {\n <ng-container\n [ngTemplateOutlet]=\"menu.template\"\n [ngTemplateOutletContext]=\"userMenuPopupContext()\"\n />\n } @else {\n <ng-container [ngTemplateOutlet]=\"userMenuBody\" />\n }\n }\n </ol-dropdown>\n } @else {\n <button type=\"button\" [class]=\"userCardClasses()\" (click)=\"userClick.emit()\">\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: collapsed(),\n initials: userInitials(),\n variant: 'rail',\n }\"\n />\n </button>\n }\n </div>\n }\n </div>\n</nav>\n\n<!--\n \u2500\u2500 MOBILE: THANH \u0110\u00C1Y + KHAY MENU (`layout=\"auto\"` d\u01B0\u1EDBi `md`, ho\u1EB7c `\"mobile\"`) \u2500\u2500\n\n Chuy\u1EC3n t\u1EEB ph\u1EA7n t\u1EF1 d\u1EF1ng c\u1EE7a onluyenv4 v\u00E0o th\u01B0 vi\u1EC7n (2026-10-06):\n\n \u00B7 thanh \u0111\u00E1y `mobilePrimaryCount` m\u1EE5c \u0111\u1EA7u nh\u00F3m ch\u00EDnh + n\u00FAt Menu \u1EDF \u00F4 cu\u1ED1i\n \u00B7 khay tr\u01B0\u1EE3t l\u00EAn t\u1EEB \u0111\u00E1y (n\u00FAt m\u1EDF n\u1EB1m \u1EDF \u0111\u00E1y ph\u1EA3i \u2014 \u0111i theo ng\u00F3n tay), ch\u1EE9a\n th\u1EBB t\u00E0i kho\u1EA3n, c\u00E1c m\u1EE5c KH\u00D4NG l\u00EAn thanh \u0111\u00E1y, v\u00E0 menu t\u00E0i kho\u1EA3n\n \u00B7 `fixed` theo m\u00E0n h\u00ECnh; `pb-[env(safe-area-inset-bottom)]` ch\u1EEBa thanh vu\u1ED1t iPhone\n\n Khay d\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class: \u1EA9n b\u1EB1ng `hidden` th\u00EC n\u00FAt b\u00EAn trong\n v\u1EABn tab t\u1EDBi \u0111\u01B0\u1EE3c d\u00F9 kh\u00F4ng nh\u00ECn th\u1EA5y. L\u1EDBp ph\u1EE7 l\u00E0 `<button>` \u0111\u1EC3 \u0111\u00F3ng \u0111\u01B0\u1EE3c\n b\u1EB1ng b\u00E0n ph\u00EDm.\n-->\n@if (hasMobile()) {\n <nav\n class=\"fixed inset-x-0 bottom-0 z-40 flex items-stretch border-t border-default bg-neutral-primary pb-[env(safe-area-inset-bottom)]\"\n [class]=\"mobileDisplay()\"\n aria-label=\"\u0110i\u1EC1u h\u01B0\u1EDBng ch\u00EDnh\"\n >\n @for (item of primaryItems(); track item.id) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"barItemClasses(item)\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n [attr.aria-label]=\"item.label\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? barBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'bar')\"\n />\n </a>\n }\n\n <button\n type=\"button\"\n class=\"flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 py-2 transition-colors\"\n [class]=\"mobileMenuOpen() ? 'text-fg-brand' : 'text-body-subtle hover:text-heading'\"\n [attr.aria-expanded]=\"mobileMenuOpen()\"\n [attr.aria-controls]=\"sheetId\"\n (click)=\"toggleMobileMenu()\"\n >\n <ol-icon name=\"solar:hamburger-menu-linear\" size=\"base\" />\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\n {{ mobileMenuLabel() }}\n </span>\n </button>\n </nav>\n\n @if (mobileMenuOpen()) {\n <button\n type=\"button\"\n class=\"fixed inset-0 z-40 cursor-default bg-black/40\"\n [class]=\"mobileDisplay()\"\n aria-label=\"\u0110\u00F3ng menu\"\n (click)=\"mobileMenuOpen.set(false)\"\n ></button>\n\n <div\n [id]=\"sheetId\"\n class=\"fixed inset-x-0 bottom-0 z-50 max-h-[80vh] overflow-y-auto rounded-t-lg bg-neutral-primary pb-[env(safe-area-inset-bottom)] shadow-2xl\"\n [class]=\"mobileDisplay()\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"mobileMenuLabel()\"\n >\n <div class=\"flex justify-center pt-2.5\">\n <span class=\"h-1 w-10 rounded-full bg-neutral-tertiary\" aria-hidden=\"true\"></span>\n </div>\n\n @if (config().user; as user) {\n <div class=\"flex items-center gap-3 p-4\">\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? sheetUserBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: false,\n initials: userInitials(),\n variant: 'sheet',\n }\"\n />\n </div>\n }\n\n @if (headerDef(); as header) {\n <div class=\"border-t border-default px-4 py-3\">\n <ng-container\n [ngTemplateOutlet]=\"header.template\"\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\n />\n </div>\n }\n\n @for (group of sheetGroups(); track $index) {\n <div class=\"border-t border-default py-1\">\n @if (group.label) {\n <p class=\"px-4 pt-2 pb-1 text-xs font-bold uppercase tracking-wide text-body-subtle\">\n {{ group.label }}\n </p>\n }\n <ng-container\n [ngTemplateOutlet]=\"sheetTree\"\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\n />\n </div>\n }\n\n @if (footerDef(); as footer) {\n <div class=\"border-t border-default px-4 py-3\">\n <ng-container\n [ngTemplateOutlet]=\"footer.template\"\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\n />\n </div>\n }\n\n <!-- Menu t\u00E0i kho\u1EA3n: template tu\u1EF3 bi\u1EBFn n\u1EBFu c\u00F3, kh\u00F4ng th\u00EC danh s\u00E1ch m\u1EE5c -->\n @if (config().user) {\n @if (userMenuDef(); as menu) {\n <!-- `px-2`: ru\u1ED9t tu\u1EF3 bi\u1EBFn (ol-dropdown-item\u2026) kh\u00F4ng t\u1EF1 c\u00F3 l\u1EC1 ngang nh\u01B0 d\u00F2ng khay -->\n <div class=\"border-t border-default px-2 py-1\">\n <ng-container\n [ngTemplateOutlet]=\"menu.template\"\n [ngTemplateOutletContext]=\"userMenuSheetContext()\"\n />\n </div>\n } @else if (userActions().length > 0) {\n <ul class=\"border-t border-default py-1\">\n @for (action of userActions(); track action.id) {\n @if (action.dividerBefore && !$first) {\n <li class=\"my-1 h-px bg-neutral-tertiary\" aria-hidden=\"true\"></li>\n }\n <li>\n @if (action.route) {\n <a\n [routerLink]=\"action.route\"\n [class]=\"sheetActionClasses(action)\"\n (click)=\"onUserAction(action)\"\n >\n <ol-icon [name]=\"action.icon\" size=\"base\" class=\"shrink-0\" />\n <span class=\"min-w-0 flex-1 truncate text-sm\">{{ action.label }}</span>\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"sheetActionClasses(action)\"\n (click)=\"onUserAction(action)\"\n >\n <ol-icon [name]=\"action.icon\" size=\"base\" class=\"shrink-0\" />\n <span class=\"min-w-0 flex-1 truncate text-sm\">{{ action.label }}</span>\n </button>\n }\n </li>\n }\n </ul>\n }\n }\n </div>\n }\n}\n\n<!--\n C\u00E2y m\u1EE5c (\u0111\u1EC7 quy) \u2014 c\u1EA5p 1 \u00B7 2 \u00B7 3. Nh\u00E1nh con ch\u1EC9 v\u1EBD khi m\u1EDF v\u00E0 menu kh\u00F4ng\n thu g\u1ECDn; thu g\u1ECDn th\u00EC menu con hi\u1EC7n N\u1ED4I c\u1EA1nh m\u1EE5c cha (xem `#navItem`).\n-->\n<ng-template #navTree let-items let-level=\"level\">\n <ul class=\"flex flex-col gap-1\" [class]=\"level === 1 ? '' : 'mt-1'\">\n @for (item of items; track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item, level: level }\"\n />\n @if (item.children?.length && isExpanded(item) && !collapsed() && level < 3) {\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\n />\n }\n </li>\n }\n </ul>\n</ng-template>\n\n<!--\n M\u1ED9t m\u1EE5c menu. Ba lo\u1EA1i h\u00E0nh vi, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 con \u2192 `<button aria-expanded>` m\u1EDF/\u0111\u00F3ng nh\u00E1nh, kh\u00F4ng \u0111i\u1EC1u h\u01B0\u1EDBng\n c\u00F3 `route` \u2192 `<a routerLink>`; m\u1EE5c \u0111ang ch\u1ECDn t\u00EDnh b\u1EB1ng `isItemActive`\n (route \u00B7 activeRoutes \u00B7 activeId), kh\u00F4ng `routerLinkActive`\n c\u00F2n l\u1EA1i \u2192 `<button>` ch\u1EC9 ph\u00E1t `itemClick` (disabled lo trong class)\n-->\n<ng-template #navItem let-item let-level=\"level\">\n @if (item.children?.length && collapsed() && level === 1) {\n <!--\n THU G\u1ECCN + m\u1EE5c cha: r\u00EA chu\u1ED9t / b\u1EA5m / Enter m\u1EDF menu con N\u1ED4I b\u00EAn ph\u1EA3i\n (`ol-dropdown`). Kh\u00F4ng tooltip \u2014 \u0111\u1EA7u panel \u0111\u00E3 ghi t\u00EAn m\u1EE5c.\n -->\n <div\n class=\"flex justify-center\"\n (mouseenter)=\"openFlyout(item.id)\"\n (mouseleave)=\"scheduleFlyoutClose()\"\n >\n <ol-dropdown\n placement=\"right\"\n menuClass=\"w-64\"\n [isOpen]=\"flyoutId() === item.id\"\n (isOpenChange)=\"onFlyoutOpenChange(item.id, $event)\"\n >\n <button\n trigger\n type=\"button\"\n [class]=\"itemClasses(item, level)\"\n [attr.aria-label]=\"item.label\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"flyoutId() === item.id\"\n [disabled]=\"item.disabled ?? false\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n\n @if (flyoutId() === item.id) {\n <div (mouseenter)=\"cancelFlyoutClose()\" (mouseleave)=\"scheduleFlyoutClose()\">\n <p class=\"px-2 pt-1 pb-1.5 text-xs font-bold uppercase tracking-wide text-body-subtle\">\n {{ item.label }}\n </p>\n <ng-container\n [ngTemplateOutlet]=\"flyoutTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: 2 }\"\n />\n </div>\n }\n </ol-dropdown>\n </div>\n } @else if (item.children?.length) {\n <button\n type=\"button\"\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [attr.aria-expanded]=\"isExpanded(item)\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n } @else if (item.route) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"itemClasses(item, level)\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n }\n</ng-template>\n\n<!--\n Ru\u1ED9t menu n\u1ED5i: con l\u00E0 li\u00EAn k\u1EBFt (`ol-dropdown-item`); con c\u00F3 ch\u00E1u th\u00EC th\u00E0nh\n nh\u00E3n nh\u00F3m, ch\u00E1u th\u1EE5t v\u00E0o d\u01B0\u1EDBi n\u00F3. M\u1EE5c \u0111ang ch\u1ECDn mang `selected`.\n-->\n<ng-template #flyoutTree let-items let-level=\"level\">\n @for (child of items; track child.id) {\n @if (child.children?.length) {\n <p class=\"px-2 pt-2 pb-1 text-sm font-semibold text-heading\" [class.ps-5]=\"level > 2\">\n {{ child.label }}\n </p>\n <div class=\"ps-3\">\n <ng-container\n [ngTemplateOutlet]=\"flyoutTree\"\n [ngTemplateOutletContext]=\"{ $implicit: child.children, level: level + 1 }\"\n />\n </div>\n } @else {\n <ol-dropdown-item\n [label]=\"child.label\"\n [routerLink]=\"child.route ?? null\"\n [selected]=\"isItemActive(child)\"\n [disabled]=\"child.disabled ?? false\"\n (itemClick)=\"onFlyoutSelect(child)\"\n />\n }\n }\n</ng-template>\n\n<!--\n Ru\u1ED9t m\u1ED9t m\u1EE5c \u1EDF menu d\u1ECDc: icon 24px (ch\u1EC9 c\u1EA5p 1) + nh\u00E3n + nh\u00E3n ph\u1EE5 + huy hi\u1EC7u\n + m\u0169i t\u00EAn (m\u1EE5c cha). Nh\u00E3n \u1EA9n khi thu g\u1ECDn.\n-->\n<ng-template #itemBody let-item let-level=\"level\" let-expanded=\"expanded\">\n @if (level === 1) {\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n }\n\n @if (!collapsed() || level > 1) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.extraText) {\n <span\n class=\"shrink-0 rounded-sm border border-white px-1 text-2xs leading-tight text-white\"\n >{{ item.extraText }}</span\n >\n }\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n @if (item.children?.length) {\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 text-white transition-transform duration-200\"\n [class.rotate-90]=\"expanded\"\n />\n }\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c \u1EDF thanh \u0111\u00E1y mobile: icon + nh\u00E3n ng\u1EAFn (\u0111\u1ECDc nh\u00E3n \u0111\u1EA7y \u0111\u1EE7 qua aria-label) -->\n<ng-template #barBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" />\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\n {{ item.shortLabel ?? item.label }}\n </span>\n</ng-template>\n\n<!-- C\u00E2y m\u1EE5c trong khay mobile \u2014 c\u00F9ng lu\u1EADt m\u1EDF/\u0111\u00F3ng v\u1EDBi menu d\u1ECDc -->\n<ng-template #sheetTree let-items let-level=\"level\">\n <ul>\n @for (item of items; track item.id) {\n <li>\n @if (item.route && !item.children?.length) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"sheetItemClasses(item, level)\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"sheetItemClasses(item, level)\"\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item) : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\n />\n </button>\n }\n @if (item.children?.length && isExpanded(item) && level < 3) {\n <ng-container\n [ngTemplateOutlet]=\"sheetTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\n />\n }\n </li>\n }\n </ul>\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t d\u00F2ng trong khay mobile: n\u1EC1n s\u00E1ng n\u00EAn ch\u1EEF theo token, kh\u00F4ng tr\u1EAFng -->\n<ng-template #sheetBody let-item let-level=\"level\" let-expanded=\"expanded\">\n @if (level === 1) {\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0\" />\n }\n <span class=\"min-w-0 flex-1 truncate text-sm\">{{ item.label }}</span>\n @if (item.extraText) {\n <span class=\"shrink-0 rounded-sm border border-default px-1 text-2xs leading-tight\">{{\n item.extraText\n }}</span>\n }\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n @if (item.children?.length) {\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 transition-transform duration-200\"\n [class.rotate-90]=\"expanded\"\n />\n }\n</ng-template>\n\n<!-- Ru\u1ED9t th\u1EBB t\u00E0i kho\u1EA3n \u1EDF \u0111\u00E1y menu d\u1ECDc: avatar + t\u00EAn/email (\u1EA9n khi thu g\u1ECDn) + m\u0169i t\u00EAn -->\n<ng-template #userCardBody let-user>\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs text-white/80\">{{ user.subtitle }}</span>\n }\n </span>\n <!--\n M\u0169i t\u00EAn CH\u1EC8 SANG PH\u1EA2I \u2014 menu t\u00E0i kho\u1EA3n m\u1EDF sang ph\u1EA3i c\u1EA1nh sidebar\n (`right-end`); m\u0169i t\u00EAn xu\u1ED1ng c\u1EE7a b\u1EA3n tr\u01B0\u1EDBc ch\u1EC9 sai h\u01B0\u1EDBng. R\u00EA chu\u1ED9t / \u0111ang\n m\u1EDF th\u00EC nh\u00EDch 2px sang ph\u1EA3i b\u00E1o h\u01B0\u1EDBng m\u1EDF.\n -->\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 text-white/80 transition-transform duration-200 group-hover/user:translate-x-0.5\"\n [class.translate-x-0.5]=\"userMenuOpen()\"\n />\n }\n</ng-template>\n\n<!-- Kh\u1ED1i t\u00E0i kho\u1EA3n \u0111\u1EA7u khay mobile -->\n<ng-template #sheetUserBody let-user>\n <ol-avatar\n size=\"lg\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\n }\n </span>\n</ng-template>\n\n<!--\n Ru\u1ED9t popup t\u00E0i kho\u1EA3n m\u1EB7c \u0111\u1ECBnh: \u0111\u1EA7u l\u00E0 avatar + t\u00EAn + email \u0111\u1EC3 ng\u01B0\u1EDDi d\u00F9ng\n bi\u1EBFt \u0111ang \u1EDF t\u00E0i kho\u1EA3n n\u00E0o, d\u01B0\u1EDBi l\u00E0 c\u00E1c m\u1EE5c `userActions()`.\n-->\n<ng-template #userMenuBody>\n @if (config().user; as user) {\n <div class=\"flex items-center gap-3 px-2 py-1.5\">\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\n }\n </span>\n </div>\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\n @for (action of userActions(); track action.id) {\n @if (action.dividerBefore && !$first) {\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\n }\n <ol-dropdown-item\n [icon]=\"action.icon\"\n [label]=\"action.label\"\n [routerLink]=\"action.route ?? null\"\n [danger]=\"action.danger ?? false\"\n (itemClick)=\"onUserAction(action)\"\n />\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlLogo, selector: "ol-logo", inputs: ["variant", "tone", "height", "alt"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "component", type: OlDropdown, selector: "ol-dropdown", inputs: ["label", "isOpen", "appendTo", "placement", "offset", "trigger", "autoClose", "disabled", "triggerClass", "menuClass"], outputs: ["isOpenChange", "opened", "closed"] }, { kind: "component", type: OlDropdownItem, selector: "ol-dropdown-item", inputs: ["routerLink", "href", "icon", "label", "secondaryText", "kbd", "suffixIcon", "selected", "active", "danger", "disabled"], outputs: ["itemClick"] }, { kind: "directive", type: OlTooltipDirective, selector: "[olTooltip]", inputs: ["olTooltip", "tooltipPosition", "tooltipTheme", "tooltipDelay", "tooltipDisabled", "tooltipArrow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17847
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAppNav, isStandalone: true, selector: "ol-app-nav", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, logoSrc: { classPropertyName: "logoSrc", publicName: "logoSrc", isSignal: true, isRequired: false, transformFunction: null }, logoMarkSrc: { classPropertyName: "logoMarkSrc", publicName: "logoMarkSrc", isSignal: true, isRequired: false, transformFunction: null }, logoTone: { classPropertyName: "logoTone", publicName: "logoTone", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, userMenuOpen: { classPropertyName: "userMenuOpen", publicName: "userMenuOpen", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, mobilePrimaryCount: { classPropertyName: "mobilePrimaryCount", publicName: "mobilePrimaryCount", isSignal: true, isRequired: false, transformFunction: null }, mobileMenuOpen: { classPropertyName: "mobileMenuOpen", publicName: "mobileMenuOpen", isSignal: true, isRequired: false, transformFunction: null }, mobileUserMenu: { classPropertyName: "mobileUserMenu", publicName: "mobileUserMenu", isSignal: true, isRequired: false, transformFunction: null }, mobileMenuLabel: { classPropertyName: "mobileMenuLabel", publicName: "mobileMenuLabel", isSignal: true, isRequired: false, transformFunction: null }, showRail: { classPropertyName: "showRail", publicName: "showRail", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", itemClick: "itemClick", userClick: "userClick", userAction: "userAction", userMenuOpen: "userMenuOpenChange", mobileMenuOpen: "mobileMenuOpenChange" }, host: { classAttribute: "block" }, queries: [{ propertyName: "itemDef", first: true, predicate: OlAppNavItemDef, descendants: true, isSignal: true }, { propertyName: "headerDef", first: true, predicate: OlAppNavHeaderDef, descendants: true, isSignal: true }, { propertyName: "footerDef", first: true, predicate: OlAppNavFooterDef, descendants: true, isSignal: true }, { propertyName: "logoDef", first: true, predicate: OlAppNavLogoDef, descendants: true, isSignal: true }, { propertyName: "userDef", first: true, predicate: OlAppNavUserDef, descendants: true, isSignal: true }, { propertyName: "userMenuDef", first: true, predicate: OlAppNavUserMenuDef, descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n [class]=\"\n 'relative h-full flex-col justify-between rounded-2xl pt-5 transition-[width] duration-200 ' +\n railDisplay()\n \"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 15px khi thu g\u1ECDn: `<ul>` l\u00E0 flex-col n\u00EAn `<li>` k\u00E9o gi\u00E3n m\u1EE5c\n con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-10` m\u1ED9t m\u00ECnh kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 15\u00D72 = 40, \u0111\u00FAng\n \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c (40px \u2014 b\u1EA3n g\u1ECDn 2026-10-06, Figma 44px / 13px).\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex min-h-0 flex-1 flex-col gap-5 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoDef(); as logo) {\n <ng-container\n [ngTemplateOutlet]=\"logo.template\"\n [ngTemplateOutletContext]=\"{ $implicit: collapsed(), collapsed: collapsed() }\"\n />\n } @else if (logoSrc()) {\n <img\n [src]=\"collapsed() && logoMarkSrc() ? logoMarkSrc() : logoSrc()\"\n alt=\"\u00D4n Luy\u1EC7n\"\n [class]=\"collapsed() ? 'size-10 object-contain' : 'h-10 w-auto max-w-full object-contain'\"\n />\n } @else {\n <!--\n Logo c\u1EE7a kit (Figma `Logo ol` #6603:1269). Thu g\u1ECDn: b\u1EA3n VU\u00D4NG 40px \u2014\n b\u1EA3n d\u00E0i r\u1ED9ng ~164px, \u1EDF khung 70px tr\u00E0n h\u1EB3n ra ngo\u00E0i. M\u1EDF: b\u1EA3n d\u00E0i cao\n 32px (~164px, Figma app-nav \u0111\u1EB7t logo r\u1ED9ng 155px).\n -->\n @if (collapsed()) {\n <ol-logo variant=\"mark\" [tone]=\"logoTone()\" [height]=\"40\" />\n } @else {\n <ol-logo variant=\"full\" [tone]=\"logoTone()\" [height]=\"32\" />\n }\n }\n </div>\n\n @if (headerDef(); as header) {\n <ng-container\n [ngTemplateOutlet]=\"header.template\"\n [ngTemplateOutletContext]=\"slotContext('rail')\"\n />\n }\n\n <!--\n V\u00F9ng m\u1EE5c CU\u1ED8N \u0110\u01AF\u1EE2C: menu nhi\u1EC1u nh\u00F3m (qu\u1EA3n tr\u1ECB tr\u01B0\u1EDDng: 5 nh\u00F3m, menu con 3\n c\u1EA5p) cao h\u01A1n m\u00E0n h\u00ECnh. Logo v\u00E0 th\u1EBB t\u00E0i kho\u1EA3n \u0111\u1EE9ng y\u00EAn, ch\u1EC9 danh s\u00E1ch cu\u1ED9n.\n\n Thanh cu\u1ED9n M\u1EA2NH, tr\u1EAFng m\u1EDD \u2014 KH\u00D4NG \u1EA9n h\u1EB3n: \u0111o menu gi\u00E1o vi\u00EAn (10 + 5 m\u1EE5c)\n \u1EDF m\u00E0n cao 900px, 3 m\u1EE5c cu\u1ED1i n\u1EB1m d\u01B0\u1EDBi m\u00E9p m\u00E0 kh\u00F4ng c\u00F3 d\u1EA5u hi\u1EC7u g\u00EC cho\n bi\u1EBFt c\u00F2n n\u1EEFa. Thanh m\u1EB7c \u0111\u1ECBnh x\u00E1m \u0111\u1EB7c th\u00EC ch\u01B0\u1EDBng tr\u00EAn n\u1EC1n navy.\n -->\n <div\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]\"\n >\n @for (group of mainGroups(); track $index) {\n <div class=\"flex flex-col gap-1\">\n @if (group.label && !collapsed()) {\n <p class=\"px-2.5 pt-1 pb-0.5 text-xs font-bold uppercase tracking-wide text-white/70\">\n {{ group.label }}\n </p>\n }\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\n />\n </div>\n }\n\n @if (footerDef(); as footer) {\n <ng-container\n [ngTemplateOutlet]=\"footer.template\"\n [ngTemplateOutletContext]=\"slotContext('rail')\"\n />\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex shrink-0 flex-col gap-1 overflow-hidden rounded-b-2xl pt-2 pb-4\"\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: footerItems(), level: 1 }\"\n />\n\n <!--\n TH\u1EBA T\u00C0I KHO\u1EA2N \u2014 b\u1EA5m m\u1EDF menu t\u00E0i kho\u1EA3n (popup n\u1ED5i `ol-dropdown`).\n\n Popup ch\u1EE9 kh\u00F4ng \u0111\u1ED5i danh s\u00E1ch t\u1EA1i ch\u1ED7: \u1EDF b\u1EC1 r\u1ED9ng 70px (thu g\u1ECDn) kh\u00F4ng c\u00F3\n ch\u1ED7 cho ba d\u00F2ng ch\u1EEF; popup n\u1ED5i ra ngo\u00E0i menu th\u00EC \u0111\u1ECDc \u0111\u01B0\u1EE3c \u1EDF c\u1EA3 hai tr\u1EA1ng\n th\u00E1i. Lu\u00F4n m\u1EDF SANG PH\u1EA2I, c\u1EA1nh sidebar (`right-end`: m\u00E9p d\u01B0\u1EDBi popup th\u1EB3ng\n m\u00E9p d\u01B0\u1EDBi th\u1EBB, popup m\u1ECDc l\u00EAn tr\u00EAn v\u00EC th\u1EBB n\u1EB1m s\u00E1t \u0111\u00E1y) \u2014 ng\u01B0\u1EDDi d\u00F9ng y\u00EAu\n c\u1EA7u 2026-10-06, b\u1EA3n tr\u01B0\u1EDBc m\u1EDF l\u00EAn tr\u00EAn \u0111\u00E8 l\u00EAn ch\u00EDnh menu khi m\u1EDF r\u1ED9ng.\n\n `user.actions = []` th\u00EC kh\u00F4ng c\u00F3 popup \u2014 b\u1EA5m th\u1EBB ch\u1EC9 ph\u00E1t `userClick`\n nh\u01B0 c\u00E1c b\u1EA3n tr\u01B0\u1EDBc.\n\n `autoClose=\"outside\"`: b\u1EA5m TRONG popup kh\u00F4ng t\u1EF1 \u0111\u00F3ng \u2014 ru\u1ED9t tu\u1EF3 bi\u1EBFn c\u00F3\n th\u1EC3 ch\u1EE9a \u0111i\u1EC1u khi\u1EC3n (b\u1ED9 ch\u1ECDn S\u00E1ng/T\u1ED1i) m\u00E0 ng\u01B0\u1EDDi d\u00F9ng c\u1EA7n th\u1EA5y k\u1EBFt qu\u1EA3.\n M\u1EE5c h\u00E0nh \u0111\u1ED9ng t\u1EF1 \u0111\u00F3ng qua `select()` / `close()` c\u1EE7a ng\u1EEF c\u1EA3nh.\n -->\n @if (config().user; as user) {\n <div class=\"mt-2 flex\" [class]=\"collapsed() ? 'justify-center' : ''\">\n @if (userActions().length > 0 || userMenuDef()) {\n <ol-dropdown\n class=\"min-w-0\"\n [class]=\"collapsed() ? '' : 'w-full'\"\n placement=\"right-end\"\n [offset]=\"userMenuOffset()\"\n menuClass=\"w-72\"\n autoClose=\"outside\"\n [(isOpen)]=\"userMenuOpen\"\n >\n <button\n trigger\n type=\"button\"\n [class]=\"userCardClasses()\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"userMenuOpen()\"\n [attr.aria-label]=\"collapsed() ? 'T\u00E0i kho\u1EA3n ' + user.name : null\"\n (click)=\"userClick.emit()\"\n >\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: collapsed(),\n initials: userInitials(),\n variant: 'rail',\n }\"\n />\n </button>\n\n <!--\n Ru\u1ED9t popup ch\u1EC9 d\u1EF1ng khi m\u1EDF: `ol-dropdown` \u1EA9n panel b\u1EB1ng class\n `hidden`, n\u00EAn d\u1EF1ng s\u1EB5n th\u00EC t\u00EAn + email n\u1EB1m trong DOM c\u1EA3 khi menu\n thu g\u1ECDn \u2014 tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u00E0 t\u00ECm ki\u1EBFm trang v\u1EABn th\u1EA5y ch\u00FAng.\n -->\n @if (userMenuOpen()) {\n @if (userMenuDef(); as menu) {\n <ng-container\n [ngTemplateOutlet]=\"menu.template\"\n [ngTemplateOutletContext]=\"userMenuPopupContext()\"\n />\n } @else {\n <ng-container [ngTemplateOutlet]=\"userMenuBody\" />\n }\n }\n </ol-dropdown>\n } @else {\n <button type=\"button\" [class]=\"userCardClasses()\" (click)=\"userClick.emit()\">\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: collapsed(),\n initials: userInitials(),\n variant: 'rail',\n }\"\n />\n </button>\n }\n </div>\n }\n </div>\n</nav>\n\n<!--\n \u2500\u2500 MOBILE: THANH \u0110\u00C1Y + KHAY MENU (`layout=\"auto\"` d\u01B0\u1EDBi `md`, ho\u1EB7c `\"mobile\"`) \u2500\u2500\n\n Chuy\u1EC3n t\u1EEB ph\u1EA7n t\u1EF1 d\u1EF1ng c\u1EE7a onluyenv4 v\u00E0o th\u01B0 vi\u1EC7n (2026-10-06):\n\n \u00B7 thanh \u0111\u00E1y `mobilePrimaryCount` m\u1EE5c \u0111\u1EA7u nh\u00F3m ch\u00EDnh + n\u00FAt Menu \u1EDF \u00F4 cu\u1ED1i\n \u00B7 khay tr\u01B0\u1EE3t l\u00EAn t\u1EEB \u0111\u00E1y (n\u00FAt m\u1EDF n\u1EB1m \u1EDF \u0111\u00E1y ph\u1EA3i \u2014 \u0111i theo ng\u00F3n tay), ch\u1EE9a\n th\u1EBB t\u00E0i kho\u1EA3n, c\u00E1c m\u1EE5c KH\u00D4NG l\u00EAn thanh \u0111\u00E1y, v\u00E0 menu t\u00E0i kho\u1EA3n\n \u00B7 `fixed` theo m\u00E0n h\u00ECnh; `pb-[env(safe-area-inset-bottom)]` ch\u1EEBa thanh vu\u1ED1t iPhone\n\n Khay d\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class: \u1EA9n b\u1EB1ng `hidden` th\u00EC n\u00FAt b\u00EAn trong\n v\u1EABn tab t\u1EDBi \u0111\u01B0\u1EE3c d\u00F9 kh\u00F4ng nh\u00ECn th\u1EA5y. L\u1EDBp ph\u1EE7 l\u00E0 `<button>` \u0111\u1EC3 \u0111\u00F3ng \u0111\u01B0\u1EE3c\n b\u1EB1ng b\u00E0n ph\u00EDm.\n-->\n@if (hasMobile()) {\n <nav\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)]\"\n [class]=\"mobileDisplay()\"\n aria-label=\"\u0110i\u1EC1u h\u01B0\u1EDBng ch\u00EDnh\"\n >\n @for (item of primaryItems(); track item.id) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"barItemClasses(item)\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n [attr.aria-label]=\"item.label\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? barBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'bar')\"\n />\n </a>\n }\n\n <button\n type=\"button\"\n class=\"flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 py-2 transition-colors\"\n [class]=\"mobileMenuOpen() ? 'text-fg-brand' : 'text-body-subtle hover:text-heading'\"\n [attr.aria-expanded]=\"mobileMenuOpen()\"\n [attr.aria-controls]=\"sheetId\"\n (click)=\"toggleMobileMenu()\"\n >\n <ol-icon name=\"solar:hamburger-menu-linear\" size=\"base\" />\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\n {{ mobileMenuLabel() }}\n </span>\n </button>\n </nav>\n\n <!--\n Khay v\u00E0 l\u1EDBp ph\u1EE7 D\u1EEANG tr\u00EAn thanh \u0111\u00E1y (cao 56px + v\u00F9ng vu\u1ED1t iPhone): thanh\n \u0111\u00E1y lu\u00F4n th\u1EA5y \u0111\u01B0\u1EE3c v\u00E0 b\u1EA5m \u0111\u01B0\u1EE3c khi khay m\u1EDF \u2014 n\u00FAt \"Menu\" b\u1EA5m l\u1EA7n n\u1EEFa th\u00EC\n \u0111\u00F3ng, c\u00E1c m\u1EE5c kh\u00E1c chuy\u1EC3n trang lu\u00F4n (ng\u01B0\u1EDDi d\u00F9ng y\u00EAu c\u1EA7u 2026-10-06). Thanh\n \u0111\u00E1y z-50, tr\u00EAn l\u1EDBp ph\u1EE7 z-40.\n -->\n @if (mobileMenuOpen()) {\n <button\n type=\"button\"\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\"\n animate.leave=\"opacity-0\"\n [class]=\"mobileDisplay()\"\n aria-label=\"\u0110\u00F3ng menu\"\n (click)=\"mobileMenuOpen.set(false)\"\n ></button>\n\n <!--\n Khung nh\u01B0 b\u1EA3n c\u0169: k\u00E9o h\u1EBFt b\u1EC1 ngang, d\u00EDnh m\u00E9p tr\u00EAn thanh \u0111\u00E1y, bo 2 g\u00F3c\n tr\u00EAn (ng\u01B0\u1EDDi d\u00F9ng 2026-10-06 \u2014 kh\u00F4ng th\u1EBB n\u1ED5i c\u00E1ch m\u00E9p). B\u00EAn trong `px-2`\n \u0111\u1EC3 m\u1ED7i d\u00F2ng bo tr\u00F2n ri\u00EAng. M\u1EDF: tr\u01B0\u1EE3t L\u00CAN t\u1EEB \u0111\u00E1y (`starting:` =\n `@starting-style`). \u0110\u00F3ng: tr\u01B0\u1EE3t XU\u1ED0NG \u2014 `animate.leave` g\u1EAFn\n `translate-y-full opacity-0`, Angular ch\u1EDD transition 200ms xong m\u1EDBi g\u1EE1\n kh\u1ECFi DOM (`@if` g\u1EE1 ngay th\u00EC kh\u00F4ng k\u1ECBp ch\u1EA1y).\n\n Hai ph\u1EA7n: \u0110\u1EA6U \u0111\u1EE9ng y\u00EAn (v\u1EA1ch k\u00E9o, th\u1EBB t\u00E0i kho\u1EA3n + \u0110\u0103ng xu\u1EA5t, v\u00E0 ph\u1EA7n `top`\n c\u1EE7a menu t\u00E0i kho\u1EA3n \u2014 Trang c\u00E1 nh\u00E2n, \u0110\u1ED5i m\u1EADt kh\u1EA9u, Giao di\u1EC7n\u2026), TH\u00C2N cu\u1ED9n\n (khe, nh\u00F3m m\u1EE5c, ph\u1EA7n `bottom`). Menu d\u00E0i th\u00EC ch\u1EC9 th\u00E2n cu\u1ED9n (ng\u01B0\u1EDDi d\u00F9ng\n 2026-10-06).\n -->\n <div\n [id]=\"sheetId\"\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\"\n animate.leave=\"translate-y-full opacity-0\"\n [class]=\"mobileDisplay()\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"mobileMenuLabel()\"\n >\n <div class=\"shrink-0\">\n <div class=\"flex justify-center pt-2.5\">\n <span class=\"h-1 w-10 rounded-full bg-neutral-tertiary\" aria-hidden=\"true\"></span>\n </div>\n\n @if (config().user; as user) {\n <div class=\"flex items-center gap-3 px-2 pt-2 pb-2\">\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? sheetUserBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: false,\n initials: userInitials(),\n variant: 'sheet',\n }\"\n />\n <!--\n `split`: m\u1EE5c danger (\u0110\u0103ng xu\u1EA5t) th\u00E0nh n\u00FAt CH\u1EC8 ICON vi\u1EC1n \u0111\u1ECF b\u00EAn ph\u1EA3i th\u1EBB\n t\u00E0i kho\u1EA3n \u2014 m\u1ED9t ch\u1EA1m, kh\u00F4ng ph\u1EA3i cu\u1ED9n xu\u1ED1ng cu\u1ED1i khay (ng\u01B0\u1EDDi d\u00F9ng\n 2026-10-06). T\u00EAn \u0111\u1ECDc + tooltip (nh\u1EA5n gi\u1EEF) t\u1EEB `label`.\n -->\n @if (mobileUserMenu() === 'split') {\n @for (action of cardActions(); track action.id) {\n <ol-button\n class=\"ms-auto shrink-0\"\n color=\"danger\"\n [outline]=\"true\"\n size=\"sm\"\n [iconOnly]=\"true\"\n [ariaLabel]=\"action.label\"\n (btnClick)=\"onUserAction(action)\"\n >\n <ol-icon leftIcon [name]=\"action.icon\" size=\"base\" />\n </ol-button>\n }\n }\n </div>\n }\n\n <!--\n `mobileUserMenu=\"split\"`: vi\u1EC7c th\u01B0\u1EDDng c\u1EE7a t\u00E0i kho\u1EA3n (Trang c\u00E1 nh\u00E2n, \u0110\u1ED5i\n m\u1EADt kh\u1EA9u, c\u00E0i \u0111\u1EB7t\u2026) l\u00EAn \u0110\u1EA6U khay, ngay d\u01B0\u1EDBi th\u1EBB t\u00E0i kho\u1EA3n.\n -->\n @if (mobileUserMenu() === 'split') {\n <ng-container\n [ngTemplateOutlet]=\"sheetAccount\"\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetTopContext() }\"\n />\n }\n </div>\n\n <div class=\"-mx-2 min-h-0 flex-1 overflow-y-auto overscroll-contain px-2\">\n <!--\n `empty:hidden`: template khe c\u00F3 th\u1EC3 kh\u00F4ng v\u1EBD g\u00EC (b\u1ED9 ch\u1ECDn vai ch\u1EC9 hi\u1EC7n\n v\u1EDBi t\u00E0i kho\u1EA3n hai vai). Kh\u00F4ng c\u00F3 n\u00F3, khay c\u00F2n m\u1ED9t d\u1EA3i tr\u1ED1ng 24px k\u1EB9p\n gi\u1EEFa hai \u0111\u01B0\u1EDDng k\u1EBB. `:empty` b\u1ECF qua n\u00FAt ch\u00FA th\u00EDch Angular ch\u00E8n v\u00E0o.\n -->\n @if (headerDef(); as header) {\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\n <ng-container\n [ngTemplateOutlet]=\"header.template\"\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\n />\n </div>\n }\n\n @for (group of sheetGroups(); track $index) {\n <div class=\"mt-1 border-t border-default pt-1\">\n @if (group.label) {\n <p class=\"px-2 pt-2 pb-1 text-xs font-bold uppercase tracking-wide text-body-subtle\">\n {{ group.label }}\n </p>\n }\n <ng-container\n [ngTemplateOutlet]=\"sheetTree\"\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\n />\n </div>\n }\n\n @if (footerDef(); as footer) {\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\n <ng-container\n [ngTemplateOutlet]=\"footer.template\"\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\n />\n </div>\n }\n\n <!-- Menu t\u00E0i kho\u1EA3n (cu\u1ED1i khay): tr\u1ECDn menu, ho\u1EB7c ch\u1EC9 ph\u1EA7n `bottom` khi `mobileUserMenu=\"split\"` -->\n <ng-container\n [ngTemplateOutlet]=\"sheetAccount\"\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetContext() }\"\n />\n </div>\n </div>\n }\n}\n\n<!--\n Kh\u1ED1i menu t\u00E0i kho\u1EA3n trong khay \u2014 v\u1EBD \u1EDF \u0111\u1EA7u (`part: 'top'`) v\u00E0/ho\u1EB7c cu\u1ED1i khay.\n Template tu\u1EF3 bi\u1EBFn n\u1EBFu c\u00F3, kh\u00F4ng th\u00EC danh s\u00E1ch m\u1EE5c m\u1EB7c \u0111\u1ECBnh c\u1EE7a ph\u1EA7n \u0111\u00F3.\n `empty:hidden`: ph\u1EA7n kh\u00F4ng c\u00F3 m\u1EE5c n\u00E0o th\u00EC kh\u00F4ng \u0111\u1EC3 l\u1EA1i d\u1EA3i k\u1EBB.\n-->\n<ng-template #sheetAccount let-ctx>\n @if (ctx) {\n @if (userMenuDef(); as menu) {\n <!--\n `px-2`: ru\u1ED9t tu\u1EF3 bi\u1EBFn (ol-dropdown-item\u2026) kh\u00F4ng t\u1EF1 c\u00F3 l\u1EC1 ngang nh\u01B0 d\u00F2ng khay.\n `min-h-10` cho m\u1EE5c menu: ol-dropdown-item cao 36px, d\u00F2ng khay 40px \u2014 \u00E9p\n b\u1EB1ng nhau \u0111\u1EC3 c\u1EA3 khay m\u1ED9t nh\u1ECBp.\n -->\n <div\n class=\"mt-1 border-t border-default pt-1 empty:hidden [&_[role=menuitem]]:min-h-10 [&_[role=menuitem]]:rounded-md\"\n >\n <ng-container [ngTemplateOutlet]=\"menu.template\" [ngTemplateOutletContext]=\"ctx\" />\n </div>\n } @else if (sheetActionsFor(ctx.part).length > 0) {\n <ul class=\"mt-1 border-t border-default pt-1\">\n @for (action of sheetActionsFor(ctx.part); track action.id) {\n @if (action.dividerBefore && !$first) {\n <li class=\"my-1 h-px bg-neutral-tertiary\" aria-hidden=\"true\"></li>\n }\n <li>\n @if (action.route) {\n <a\n [routerLink]=\"action.route\"\n [class]=\"sheetActionClasses(action)\"\n (click)=\"onUserAction(action)\"\n >\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"sheetActionClasses(action)\"\n (click)=\"onUserAction(action)\"\n >\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\n </button>\n }\n </li>\n }\n </ul>\n }\n }\n</ng-template>\n\n<!--\n C\u00E2y m\u1EE5c (\u0111\u1EC7 quy) \u2014 c\u1EA5p 1 \u00B7 2 \u00B7 3. Nh\u00E1nh con ch\u1EC9 v\u1EBD khi m\u1EDF v\u00E0 menu kh\u00F4ng\n thu g\u1ECDn; thu g\u1ECDn th\u00EC menu con hi\u1EC7n N\u1ED4I c\u1EA1nh m\u1EE5c cha (xem `#navItem`).\n-->\n<ng-template #navTree let-items let-level=\"level\">\n <ul class=\"flex flex-col gap-1\" [class]=\"level === 1 ? '' : 'mt-1'\">\n @for (item of items; track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item, level: level }\"\n />\n @if (item.children?.length && isExpanded(item) && !collapsed() && level < 3) {\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\n />\n }\n </li>\n }\n </ul>\n</ng-template>\n\n<!--\n M\u1ED9t m\u1EE5c menu. Ba lo\u1EA1i h\u00E0nh vi, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 con \u2192 `<button aria-expanded>` m\u1EDF/\u0111\u00F3ng nh\u00E1nh, kh\u00F4ng \u0111i\u1EC1u h\u01B0\u1EDBng\n c\u00F3 `route` \u2192 `<a routerLink>`; m\u1EE5c \u0111ang ch\u1ECDn t\u00EDnh b\u1EB1ng `isItemActive`\n (route \u00B7 activeRoutes \u00B7 activeId), kh\u00F4ng `routerLinkActive`\n c\u00F2n l\u1EA1i \u2192 `<button>` ch\u1EC9 ph\u00E1t `itemClick` (disabled lo trong class)\n-->\n<ng-template #navItem let-item let-level=\"level\">\n @if (item.children?.length && collapsed() && level === 1) {\n <!--\n THU G\u1ECCN + m\u1EE5c cha: r\u00EA chu\u1ED9t / b\u1EA5m / Enter m\u1EDF menu con N\u1ED4I b\u00EAn ph\u1EA3i\n (`ol-dropdown`). Kh\u00F4ng tooltip \u2014 \u0111\u1EA7u panel \u0111\u00E3 ghi t\u00EAn m\u1EE5c.\n -->\n <div\n class=\"flex justify-center\"\n (mouseenter)=\"openFlyout(item.id)\"\n (mouseleave)=\"scheduleFlyoutClose()\"\n >\n <ol-dropdown\n placement=\"right\"\n menuClass=\"w-64\"\n [isOpen]=\"flyoutId() === item.id\"\n (isOpenChange)=\"onFlyoutOpenChange(item.id, $event)\"\n >\n <button\n trigger\n type=\"button\"\n [class]=\"itemClasses(item, level)\"\n [attr.aria-label]=\"item.label\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"flyoutId() === item.id\"\n [disabled]=\"item.disabled ?? false\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n\n @if (flyoutId() === item.id) {\n <div (mouseenter)=\"cancelFlyoutClose()\" (mouseleave)=\"scheduleFlyoutClose()\">\n <p class=\"px-2 pt-1 pb-1.5 text-xs font-bold uppercase tracking-wide text-body-subtle\">\n {{ item.label }}\n </p>\n <ng-container\n [ngTemplateOutlet]=\"flyoutTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: 2 }\"\n />\n </div>\n }\n </ol-dropdown>\n </div>\n } @else if (item.children?.length) {\n <button\n type=\"button\"\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [attr.aria-expanded]=\"isExpanded(item)\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n } @else if (item.route) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"itemClasses(item, level)\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n }\n</ng-template>\n\n<!--\n Ru\u1ED9t menu n\u1ED5i: con l\u00E0 li\u00EAn k\u1EBFt (`ol-dropdown-item`); con c\u00F3 ch\u00E1u th\u00EC th\u00E0nh\n nh\u00E3n nh\u00F3m, ch\u00E1u th\u1EE5t v\u00E0o d\u01B0\u1EDBi n\u00F3. M\u1EE5c \u0111ang ch\u1ECDn mang `selected`.\n-->\n<ng-template #flyoutTree let-items let-level=\"level\">\n @for (child of items; track child.id) {\n @if (child.children?.length) {\n <p class=\"px-2 pt-2 pb-1 text-sm font-semibold text-heading\" [class.ps-5]=\"level > 2\">\n {{ child.label }}\n </p>\n <div class=\"ps-3\">\n <ng-container\n [ngTemplateOutlet]=\"flyoutTree\"\n [ngTemplateOutletContext]=\"{ $implicit: child.children, level: level + 1 }\"\n />\n </div>\n } @else {\n <ol-dropdown-item\n [label]=\"child.label\"\n [routerLink]=\"child.route ?? null\"\n [selected]=\"isItemActive(child)\"\n [disabled]=\"child.disabled ?? false\"\n (itemClick)=\"onFlyoutSelect(child)\"\n />\n }\n }\n</ng-template>\n\n<!--\n Ru\u1ED9t m\u1ED9t m\u1EE5c \u1EDF menu d\u1ECDc: icon 24px (ch\u1EC9 c\u1EA5p 1) + nh\u00E3n + nh\u00E3n ph\u1EE5 + huy hi\u1EC7u\n + m\u0169i t\u00EAn (m\u1EE5c cha). Nh\u00E3n \u1EA9n khi thu g\u1ECDn.\n-->\n<ng-template #itemBody let-item let-level=\"level\" let-expanded=\"expanded\">\n @if (level === 1) {\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n }\n\n @if (!collapsed() || level > 1) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.extraText) {\n <span\n class=\"shrink-0 rounded-sm border border-white px-1 text-2xs leading-tight text-white\"\n >{{ item.extraText }}</span\n >\n }\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n @if (item.children?.length) {\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 text-white transition-transform duration-200\"\n [class.rotate-90]=\"expanded\"\n />\n }\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c \u1EDF thanh \u0111\u00E1y mobile: icon + nh\u00E3n ng\u1EAFn (\u0111\u1ECDc nh\u00E3n \u0111\u1EA7y \u0111\u1EE7 qua aria-label) -->\n<ng-template #barBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" />\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\n {{ item.shortLabel ?? item.label }}\n </span>\n</ng-template>\n\n<!-- C\u00E2y m\u1EE5c trong khay mobile \u2014 c\u00F9ng lu\u1EADt m\u1EDF/\u0111\u00F3ng v\u1EDBi menu d\u1ECDc -->\n<ng-template #sheetTree let-items let-level=\"level\">\n <ul>\n @for (item of items; track item.id) {\n <li>\n @if (item.route && !item.children?.length) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"sheetItemClasses(item, level)\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"sheetItemClasses(item, level)\"\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item) : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\n />\n </button>\n }\n @if (item.children?.length && isExpanded(item) && level < 3) {\n <ng-container\n [ngTemplateOutlet]=\"sheetTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\n />\n }\n </li>\n }\n </ul>\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t d\u00F2ng trong khay mobile: n\u1EC1n s\u00E1ng n\u00EAn ch\u1EEF theo token, kh\u00F4ng tr\u1EAFng -->\n<ng-template #sheetBody let-item let-level=\"level\" let-expanded=\"expanded\">\n @if (level === 1) {\n <ol-icon [name]=\"item.icon\" size=\"sm\" class=\"shrink-0\" />\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ item.label }}</span>\n @if (item.extraText) {\n <span class=\"shrink-0 rounded-sm border border-default px-1 text-2xs leading-tight\">{{\n item.extraText\n }}</span>\n }\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n @if (item.children?.length) {\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 transition-transform duration-200\"\n [class.rotate-90]=\"expanded\"\n />\n }\n</ng-template>\n\n<!-- Ru\u1ED9t th\u1EBB t\u00E0i kho\u1EA3n \u1EDF \u0111\u00E1y menu d\u1ECDc: avatar + t\u00EAn/email (\u1EA9n khi thu g\u1ECDn) + m\u0169i t\u00EAn -->\n<ng-template #userCardBody let-user>\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs text-white/80\">{{ user.subtitle }}</span>\n }\n </span>\n <!--\n M\u0169i t\u00EAn CH\u1EC8 SANG PH\u1EA2I \u2014 menu t\u00E0i kho\u1EA3n m\u1EDF sang ph\u1EA3i c\u1EA1nh sidebar\n (`right-end`); m\u0169i t\u00EAn xu\u1ED1ng c\u1EE7a b\u1EA3n tr\u01B0\u1EDBc ch\u1EC9 sai h\u01B0\u1EDBng. R\u00EA chu\u1ED9t / \u0111ang\n m\u1EDF th\u00EC nh\u00EDch 2px sang ph\u1EA3i b\u00E1o h\u01B0\u1EDBng m\u1EDF.\n -->\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 text-white/80 transition-transform duration-200 group-hover/user:translate-x-0.5\"\n [class.translate-x-0.5]=\"userMenuOpen()\"\n />\n }\n</ng-template>\n\n<!-- Kh\u1ED1i t\u00E0i kho\u1EA3n \u0111\u1EA7u khay mobile -->\n<ng-template #sheetUserBody let-user>\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\n }\n </span>\n</ng-template>\n\n<!--\n Ru\u1ED9t popup t\u00E0i kho\u1EA3n m\u1EB7c \u0111\u1ECBnh: \u0111\u1EA7u l\u00E0 avatar + t\u00EAn + email \u0111\u1EC3 ng\u01B0\u1EDDi d\u00F9ng\n bi\u1EBFt \u0111ang \u1EDF t\u00E0i kho\u1EA3n n\u00E0o, d\u01B0\u1EDBi l\u00E0 c\u00E1c m\u1EE5c `userActions()`.\n-->\n<ng-template #userMenuBody>\n @if (config().user; as user) {\n <div class=\"flex items-center gap-3 px-2 py-1.5\">\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\n }\n </span>\n </div>\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\n @for (action of userActions(); track action.id) {\n @if (action.dividerBefore && !$first) {\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\n }\n <ol-dropdown-item\n [icon]=\"action.icon\"\n [label]=\"action.label\"\n [routerLink]=\"action.route ?? null\"\n [danger]=\"action.danger ?? false\"\n (itemClick)=\"onUserAction(action)\"\n />\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlButton, selector: "ol-button", inputs: ["color", "size", "pill", "outline", "iconOnly", "loading", "disabled", "type", "ariaLabel", "ariaExpanded", "ariaControls", "ariaPressed"], outputs: ["btnClick"] }, { kind: "component", type: OlLogo, selector: "ol-logo", inputs: ["variant", "tone", "height", "alt"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "component", type: OlDropdown, selector: "ol-dropdown", inputs: ["label", "isOpen", "appendTo", "placement", "offset", "trigger", "autoClose", "disabled", "triggerClass", "menuClass"], outputs: ["isOpenChange", "opened", "closed"] }, { kind: "component", type: OlDropdownItem, selector: "ol-dropdown-item", inputs: ["routerLink", "href", "icon", "label", "secondaryText", "kbd", "suffixIcon", "selected", "active", "danger", "disabled"], outputs: ["itemClick"] }, { kind: "directive", type: OlTooltipDirective, selector: "[olTooltip]", inputs: ["olTooltip", "tooltipPosition", "tooltipTheme", "tooltipDelay", "tooltipDisabled", "tooltipArrow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
17756
17848
|
}
|
|
17757
17849
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, decorators: [{
|
|
17758
17850
|
type: Component,
|
|
@@ -17760,6 +17852,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
17760
17852
|
NgTemplateOutlet,
|
|
17761
17853
|
RouterLink,
|
|
17762
17854
|
OlIcon,
|
|
17855
|
+
OlButton,
|
|
17763
17856
|
OlLogo,
|
|
17764
17857
|
OlAvatar,
|
|
17765
17858
|
OlBadge,
|
|
@@ -17768,8 +17861,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
17768
17861
|
OlTooltipDirective,
|
|
17769
17862
|
], host: {
|
|
17770
17863
|
class: 'block',
|
|
17771
|
-
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n [class]=\"\n 'relative h-full flex-col justify-between rounded-2xl pt-5 transition-[width] duration-200 ' +\n railDisplay()\n \"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 15px khi thu g\u1ECDn: `<ul>` l\u00E0 flex-col n\u00EAn `<li>` k\u00E9o gi\u00E3n m\u1EE5c\n con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-10` m\u1ED9t m\u00ECnh kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 15\u00D72 = 40, \u0111\u00FAng\n \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c (40px \u2014 b\u1EA3n g\u1ECDn 2026-10-06, Figma 44px / 13px).\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex min-h-0 flex-1 flex-col gap-5 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoDef(); as logo) {\n <ng-container\n [ngTemplateOutlet]=\"logo.template\"\n [ngTemplateOutletContext]=\"{ $implicit: collapsed(), collapsed: collapsed() }\"\n />\n } @else if (logoSrc()) {\n <img\n [src]=\"collapsed() && logoMarkSrc() ? logoMarkSrc() : logoSrc()\"\n alt=\"\u00D4n Luy\u1EC7n\"\n [class]=\"collapsed() ? 'size-10 object-contain' : 'h-10 w-auto max-w-full object-contain'\"\n />\n } @else {\n <!--\n Logo c\u1EE7a kit (Figma `Logo ol` #6603:1269). Thu g\u1ECDn: b\u1EA3n VU\u00D4NG 40px \u2014\n b\u1EA3n d\u00E0i r\u1ED9ng ~164px, \u1EDF khung 70px tr\u00E0n h\u1EB3n ra ngo\u00E0i. M\u1EDF: b\u1EA3n d\u00E0i cao\n 32px (~164px, Figma app-nav \u0111\u1EB7t logo r\u1ED9ng 155px).\n -->\n @if (collapsed()) {\n <ol-logo variant=\"mark\" [tone]=\"logoTone()\" [height]=\"40\" />\n } @else {\n <ol-logo variant=\"full\" [tone]=\"logoTone()\" [height]=\"32\" />\n }\n }\n </div>\n\n @if (headerDef(); as header) {\n <ng-container\n [ngTemplateOutlet]=\"header.template\"\n [ngTemplateOutletContext]=\"slotContext('rail')\"\n />\n }\n\n <!--\n V\u00F9ng m\u1EE5c CU\u1ED8N \u0110\u01AF\u1EE2C: menu nhi\u1EC1u nh\u00F3m (qu\u1EA3n tr\u1ECB tr\u01B0\u1EDDng: 5 nh\u00F3m, menu con 3\n c\u1EA5p) cao h\u01A1n m\u00E0n h\u00ECnh. Logo v\u00E0 th\u1EBB t\u00E0i kho\u1EA3n \u0111\u1EE9ng y\u00EAn, ch\u1EC9 danh s\u00E1ch cu\u1ED9n.\n\n Thanh cu\u1ED9n M\u1EA2NH, tr\u1EAFng m\u1EDD \u2014 KH\u00D4NG \u1EA9n h\u1EB3n: \u0111o menu gi\u00E1o vi\u00EAn (10 + 5 m\u1EE5c)\n \u1EDF m\u00E0n cao 900px, 3 m\u1EE5c cu\u1ED1i n\u1EB1m d\u01B0\u1EDBi m\u00E9p m\u00E0 kh\u00F4ng c\u00F3 d\u1EA5u hi\u1EC7u g\u00EC cho\n bi\u1EBFt c\u00F2n n\u1EEFa. Thanh m\u1EB7c \u0111\u1ECBnh x\u00E1m \u0111\u1EB7c th\u00EC ch\u01B0\u1EDBng tr\u00EAn n\u1EC1n navy.\n -->\n <div\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]\"\n >\n @for (group of mainGroups(); track $index) {\n <div class=\"flex flex-col gap-1\">\n @if (group.label && !collapsed()) {\n <p class=\"px-2.5 pt-1 pb-0.5 text-xs font-bold uppercase tracking-wide text-white/70\">\n {{ group.label }}\n </p>\n }\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\n />\n </div>\n }\n\n @if (footerDef(); as footer) {\n <ng-container\n [ngTemplateOutlet]=\"footer.template\"\n [ngTemplateOutletContext]=\"slotContext('rail')\"\n />\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex shrink-0 flex-col gap-1 overflow-hidden rounded-b-2xl pt-2 pb-4\"\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: footerItems(), level: 1 }\"\n />\n\n <!--\n TH\u1EBA T\u00C0I KHO\u1EA2N \u2014 b\u1EA5m m\u1EDF menu t\u00E0i kho\u1EA3n (popup n\u1ED5i `ol-dropdown`).\n\n Popup ch\u1EE9 kh\u00F4ng \u0111\u1ED5i danh s\u00E1ch t\u1EA1i ch\u1ED7: \u1EDF b\u1EC1 r\u1ED9ng 70px (thu g\u1ECDn) kh\u00F4ng c\u00F3\n ch\u1ED7 cho ba d\u00F2ng ch\u1EEF; popup n\u1ED5i ra ngo\u00E0i menu th\u00EC \u0111\u1ECDc \u0111\u01B0\u1EE3c \u1EDF c\u1EA3 hai tr\u1EA1ng\n th\u00E1i. Lu\u00F4n m\u1EDF SANG PH\u1EA2I, c\u1EA1nh sidebar (`right-end`: m\u00E9p d\u01B0\u1EDBi popup th\u1EB3ng\n m\u00E9p d\u01B0\u1EDBi th\u1EBB, popup m\u1ECDc l\u00EAn tr\u00EAn v\u00EC th\u1EBB n\u1EB1m s\u00E1t \u0111\u00E1y) \u2014 ng\u01B0\u1EDDi d\u00F9ng y\u00EAu\n c\u1EA7u 2026-10-06, b\u1EA3n tr\u01B0\u1EDBc m\u1EDF l\u00EAn tr\u00EAn \u0111\u00E8 l\u00EAn ch\u00EDnh menu khi m\u1EDF r\u1ED9ng.\n\n `user.actions = []` th\u00EC kh\u00F4ng c\u00F3 popup \u2014 b\u1EA5m th\u1EBB ch\u1EC9 ph\u00E1t `userClick`\n nh\u01B0 c\u00E1c b\u1EA3n tr\u01B0\u1EDBc.\n\n `autoClose=\"outside\"`: b\u1EA5m TRONG popup kh\u00F4ng t\u1EF1 \u0111\u00F3ng \u2014 ru\u1ED9t tu\u1EF3 bi\u1EBFn c\u00F3\n th\u1EC3 ch\u1EE9a \u0111i\u1EC1u khi\u1EC3n (b\u1ED9 ch\u1ECDn S\u00E1ng/T\u1ED1i) m\u00E0 ng\u01B0\u1EDDi d\u00F9ng c\u1EA7n th\u1EA5y k\u1EBFt qu\u1EA3.\n M\u1EE5c h\u00E0nh \u0111\u1ED9ng t\u1EF1 \u0111\u00F3ng qua `select()` / `close()` c\u1EE7a ng\u1EEF c\u1EA3nh.\n -->\n @if (config().user; as user) {\n <div class=\"mt-2 flex\" [class]=\"collapsed() ? 'justify-center' : ''\">\n @if (userActions().length > 0 || userMenuDef()) {\n <ol-dropdown\n class=\"min-w-0\"\n [class]=\"collapsed() ? '' : 'w-full'\"\n placement=\"right-end\"\n [offset]=\"userMenuOffset()\"\n menuClass=\"w-72\"\n autoClose=\"outside\"\n [(isOpen)]=\"userMenuOpen\"\n >\n <button\n trigger\n type=\"button\"\n [class]=\"userCardClasses()\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"userMenuOpen()\"\n [attr.aria-label]=\"collapsed() ? 'T\u00E0i kho\u1EA3n ' + user.name : null\"\n (click)=\"userClick.emit()\"\n >\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: collapsed(),\n initials: userInitials(),\n variant: 'rail',\n }\"\n />\n </button>\n\n <!--\n Ru\u1ED9t popup ch\u1EC9 d\u1EF1ng khi m\u1EDF: `ol-dropdown` \u1EA9n panel b\u1EB1ng class\n `hidden`, n\u00EAn d\u1EF1ng s\u1EB5n th\u00EC t\u00EAn + email n\u1EB1m trong DOM c\u1EA3 khi menu\n thu g\u1ECDn \u2014 tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u00E0 t\u00ECm ki\u1EBFm trang v\u1EABn th\u1EA5y ch\u00FAng.\n -->\n @if (userMenuOpen()) {\n @if (userMenuDef(); as menu) {\n <ng-container\n [ngTemplateOutlet]=\"menu.template\"\n [ngTemplateOutletContext]=\"userMenuPopupContext()\"\n />\n } @else {\n <ng-container [ngTemplateOutlet]=\"userMenuBody\" />\n }\n }\n </ol-dropdown>\n } @else {\n <button type=\"button\" [class]=\"userCardClasses()\" (click)=\"userClick.emit()\">\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: collapsed(),\n initials: userInitials(),\n variant: 'rail',\n }\"\n />\n </button>\n }\n </div>\n }\n </div>\n</nav>\n\n<!--\n \u2500\u2500 MOBILE: THANH \u0110\u00C1Y + KHAY MENU (`layout=\"auto\"` d\u01B0\u1EDBi `md`, ho\u1EB7c `\"mobile\"`) \u2500\u2500\n\n Chuy\u1EC3n t\u1EEB ph\u1EA7n t\u1EF1 d\u1EF1ng c\u1EE7a onluyenv4 v\u00E0o th\u01B0 vi\u1EC7n (2026-10-06):\n\n \u00B7 thanh \u0111\u00E1y `mobilePrimaryCount` m\u1EE5c \u0111\u1EA7u nh\u00F3m ch\u00EDnh + n\u00FAt Menu \u1EDF \u00F4 cu\u1ED1i\n \u00B7 khay tr\u01B0\u1EE3t l\u00EAn t\u1EEB \u0111\u00E1y (n\u00FAt m\u1EDF n\u1EB1m \u1EDF \u0111\u00E1y ph\u1EA3i \u2014 \u0111i theo ng\u00F3n tay), ch\u1EE9a\n th\u1EBB t\u00E0i kho\u1EA3n, c\u00E1c m\u1EE5c KH\u00D4NG l\u00EAn thanh \u0111\u00E1y, v\u00E0 menu t\u00E0i kho\u1EA3n\n \u00B7 `fixed` theo m\u00E0n h\u00ECnh; `pb-[env(safe-area-inset-bottom)]` ch\u1EEBa thanh vu\u1ED1t iPhone\n\n Khay d\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class: \u1EA9n b\u1EB1ng `hidden` th\u00EC n\u00FAt b\u00EAn trong\n v\u1EABn tab t\u1EDBi \u0111\u01B0\u1EE3c d\u00F9 kh\u00F4ng nh\u00ECn th\u1EA5y. L\u1EDBp ph\u1EE7 l\u00E0 `<button>` \u0111\u1EC3 \u0111\u00F3ng \u0111\u01B0\u1EE3c\n b\u1EB1ng b\u00E0n ph\u00EDm.\n-->\n@if (hasMobile()) {\n <nav\n class=\"fixed inset-x-0 bottom-0 z-40 flex items-stretch border-t border-default bg-neutral-primary pb-[env(safe-area-inset-bottom)]\"\n [class]=\"mobileDisplay()\"\n aria-label=\"\u0110i\u1EC1u h\u01B0\u1EDBng ch\u00EDnh\"\n >\n @for (item of primaryItems(); track item.id) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"barItemClasses(item)\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n [attr.aria-label]=\"item.label\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? barBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'bar')\"\n />\n </a>\n }\n\n <button\n type=\"button\"\n class=\"flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 py-2 transition-colors\"\n [class]=\"mobileMenuOpen() ? 'text-fg-brand' : 'text-body-subtle hover:text-heading'\"\n [attr.aria-expanded]=\"mobileMenuOpen()\"\n [attr.aria-controls]=\"sheetId\"\n (click)=\"toggleMobileMenu()\"\n >\n <ol-icon name=\"solar:hamburger-menu-linear\" size=\"base\" />\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\n {{ mobileMenuLabel() }}\n </span>\n </button>\n </nav>\n\n @if (mobileMenuOpen()) {\n <button\n type=\"button\"\n class=\"fixed inset-0 z-40 cursor-default bg-black/40\"\n [class]=\"mobileDisplay()\"\n aria-label=\"\u0110\u00F3ng menu\"\n (click)=\"mobileMenuOpen.set(false)\"\n ></button>\n\n <div\n [id]=\"sheetId\"\n class=\"fixed inset-x-0 bottom-0 z-50 max-h-[80vh] overflow-y-auto rounded-t-lg bg-neutral-primary pb-[env(safe-area-inset-bottom)] shadow-2xl\"\n [class]=\"mobileDisplay()\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"mobileMenuLabel()\"\n >\n <div class=\"flex justify-center pt-2.5\">\n <span class=\"h-1 w-10 rounded-full bg-neutral-tertiary\" aria-hidden=\"true\"></span>\n </div>\n\n @if (config().user; as user) {\n <div class=\"flex items-center gap-3 p-4\">\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? sheetUserBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: false,\n initials: userInitials(),\n variant: 'sheet',\n }\"\n />\n </div>\n }\n\n @if (headerDef(); as header) {\n <div class=\"border-t border-default px-4 py-3\">\n <ng-container\n [ngTemplateOutlet]=\"header.template\"\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\n />\n </div>\n }\n\n @for (group of sheetGroups(); track $index) {\n <div class=\"border-t border-default py-1\">\n @if (group.label) {\n <p class=\"px-4 pt-2 pb-1 text-xs font-bold uppercase tracking-wide text-body-subtle\">\n {{ group.label }}\n </p>\n }\n <ng-container\n [ngTemplateOutlet]=\"sheetTree\"\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\n />\n </div>\n }\n\n @if (footerDef(); as footer) {\n <div class=\"border-t border-default px-4 py-3\">\n <ng-container\n [ngTemplateOutlet]=\"footer.template\"\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\n />\n </div>\n }\n\n <!-- Menu t\u00E0i kho\u1EA3n: template tu\u1EF3 bi\u1EBFn n\u1EBFu c\u00F3, kh\u00F4ng th\u00EC danh s\u00E1ch m\u1EE5c -->\n @if (config().user) {\n @if (userMenuDef(); as menu) {\n <!-- `px-2`: ru\u1ED9t tu\u1EF3 bi\u1EBFn (ol-dropdown-item\u2026) kh\u00F4ng t\u1EF1 c\u00F3 l\u1EC1 ngang nh\u01B0 d\u00F2ng khay -->\n <div class=\"border-t border-default px-2 py-1\">\n <ng-container\n [ngTemplateOutlet]=\"menu.template\"\n [ngTemplateOutletContext]=\"userMenuSheetContext()\"\n />\n </div>\n } @else if (userActions().length > 0) {\n <ul class=\"border-t border-default py-1\">\n @for (action of userActions(); track action.id) {\n @if (action.dividerBefore && !$first) {\n <li class=\"my-1 h-px bg-neutral-tertiary\" aria-hidden=\"true\"></li>\n }\n <li>\n @if (action.route) {\n <a\n [routerLink]=\"action.route\"\n [class]=\"sheetActionClasses(action)\"\n (click)=\"onUserAction(action)\"\n >\n <ol-icon [name]=\"action.icon\" size=\"base\" class=\"shrink-0\" />\n <span class=\"min-w-0 flex-1 truncate text-sm\">{{ action.label }}</span>\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"sheetActionClasses(action)\"\n (click)=\"onUserAction(action)\"\n >\n <ol-icon [name]=\"action.icon\" size=\"base\" class=\"shrink-0\" />\n <span class=\"min-w-0 flex-1 truncate text-sm\">{{ action.label }}</span>\n </button>\n }\n </li>\n }\n </ul>\n }\n }\n </div>\n }\n}\n\n<!--\n C\u00E2y m\u1EE5c (\u0111\u1EC7 quy) \u2014 c\u1EA5p 1 \u00B7 2 \u00B7 3. Nh\u00E1nh con ch\u1EC9 v\u1EBD khi m\u1EDF v\u00E0 menu kh\u00F4ng\n thu g\u1ECDn; thu g\u1ECDn th\u00EC menu con hi\u1EC7n N\u1ED4I c\u1EA1nh m\u1EE5c cha (xem `#navItem`).\n-->\n<ng-template #navTree let-items let-level=\"level\">\n <ul class=\"flex flex-col gap-1\" [class]=\"level === 1 ? '' : 'mt-1'\">\n @for (item of items; track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item, level: level }\"\n />\n @if (item.children?.length && isExpanded(item) && !collapsed() && level < 3) {\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\n />\n }\n </li>\n }\n </ul>\n</ng-template>\n\n<!--\n M\u1ED9t m\u1EE5c menu. Ba lo\u1EA1i h\u00E0nh vi, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 con \u2192 `<button aria-expanded>` m\u1EDF/\u0111\u00F3ng nh\u00E1nh, kh\u00F4ng \u0111i\u1EC1u h\u01B0\u1EDBng\n c\u00F3 `route` \u2192 `<a routerLink>`; m\u1EE5c \u0111ang ch\u1ECDn t\u00EDnh b\u1EB1ng `isItemActive`\n (route \u00B7 activeRoutes \u00B7 activeId), kh\u00F4ng `routerLinkActive`\n c\u00F2n l\u1EA1i \u2192 `<button>` ch\u1EC9 ph\u00E1t `itemClick` (disabled lo trong class)\n-->\n<ng-template #navItem let-item let-level=\"level\">\n @if (item.children?.length && collapsed() && level === 1) {\n <!--\n THU G\u1ECCN + m\u1EE5c cha: r\u00EA chu\u1ED9t / b\u1EA5m / Enter m\u1EDF menu con N\u1ED4I b\u00EAn ph\u1EA3i\n (`ol-dropdown`). Kh\u00F4ng tooltip \u2014 \u0111\u1EA7u panel \u0111\u00E3 ghi t\u00EAn m\u1EE5c.\n -->\n <div\n class=\"flex justify-center\"\n (mouseenter)=\"openFlyout(item.id)\"\n (mouseleave)=\"scheduleFlyoutClose()\"\n >\n <ol-dropdown\n placement=\"right\"\n menuClass=\"w-64\"\n [isOpen]=\"flyoutId() === item.id\"\n (isOpenChange)=\"onFlyoutOpenChange(item.id, $event)\"\n >\n <button\n trigger\n type=\"button\"\n [class]=\"itemClasses(item, level)\"\n [attr.aria-label]=\"item.label\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"flyoutId() === item.id\"\n [disabled]=\"item.disabled ?? false\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n\n @if (flyoutId() === item.id) {\n <div (mouseenter)=\"cancelFlyoutClose()\" (mouseleave)=\"scheduleFlyoutClose()\">\n <p class=\"px-2 pt-1 pb-1.5 text-xs font-bold uppercase tracking-wide text-body-subtle\">\n {{ item.label }}\n </p>\n <ng-container\n [ngTemplateOutlet]=\"flyoutTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: 2 }\"\n />\n </div>\n }\n </ol-dropdown>\n </div>\n } @else if (item.children?.length) {\n <button\n type=\"button\"\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [attr.aria-expanded]=\"isExpanded(item)\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n } @else if (item.route) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"itemClasses(item, level)\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n }\n</ng-template>\n\n<!--\n Ru\u1ED9t menu n\u1ED5i: con l\u00E0 li\u00EAn k\u1EBFt (`ol-dropdown-item`); con c\u00F3 ch\u00E1u th\u00EC th\u00E0nh\n nh\u00E3n nh\u00F3m, ch\u00E1u th\u1EE5t v\u00E0o d\u01B0\u1EDBi n\u00F3. M\u1EE5c \u0111ang ch\u1ECDn mang `selected`.\n-->\n<ng-template #flyoutTree let-items let-level=\"level\">\n @for (child of items; track child.id) {\n @if (child.children?.length) {\n <p class=\"px-2 pt-2 pb-1 text-sm font-semibold text-heading\" [class.ps-5]=\"level > 2\">\n {{ child.label }}\n </p>\n <div class=\"ps-3\">\n <ng-container\n [ngTemplateOutlet]=\"flyoutTree\"\n [ngTemplateOutletContext]=\"{ $implicit: child.children, level: level + 1 }\"\n />\n </div>\n } @else {\n <ol-dropdown-item\n [label]=\"child.label\"\n [routerLink]=\"child.route ?? null\"\n [selected]=\"isItemActive(child)\"\n [disabled]=\"child.disabled ?? false\"\n (itemClick)=\"onFlyoutSelect(child)\"\n />\n }\n }\n</ng-template>\n\n<!--\n Ru\u1ED9t m\u1ED9t m\u1EE5c \u1EDF menu d\u1ECDc: icon 24px (ch\u1EC9 c\u1EA5p 1) + nh\u00E3n + nh\u00E3n ph\u1EE5 + huy hi\u1EC7u\n + m\u0169i t\u00EAn (m\u1EE5c cha). Nh\u00E3n \u1EA9n khi thu g\u1ECDn.\n-->\n<ng-template #itemBody let-item let-level=\"level\" let-expanded=\"expanded\">\n @if (level === 1) {\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n }\n\n @if (!collapsed() || level > 1) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.extraText) {\n <span\n class=\"shrink-0 rounded-sm border border-white px-1 text-2xs leading-tight text-white\"\n >{{ item.extraText }}</span\n >\n }\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n @if (item.children?.length) {\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 text-white transition-transform duration-200\"\n [class.rotate-90]=\"expanded\"\n />\n }\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c \u1EDF thanh \u0111\u00E1y mobile: icon + nh\u00E3n ng\u1EAFn (\u0111\u1ECDc nh\u00E3n \u0111\u1EA7y \u0111\u1EE7 qua aria-label) -->\n<ng-template #barBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" />\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\n {{ item.shortLabel ?? item.label }}\n </span>\n</ng-template>\n\n<!-- C\u00E2y m\u1EE5c trong khay mobile \u2014 c\u00F9ng lu\u1EADt m\u1EDF/\u0111\u00F3ng v\u1EDBi menu d\u1ECDc -->\n<ng-template #sheetTree let-items let-level=\"level\">\n <ul>\n @for (item of items; track item.id) {\n <li>\n @if (item.route && !item.children?.length) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"sheetItemClasses(item, level)\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"sheetItemClasses(item, level)\"\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item) : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\n />\n </button>\n }\n @if (item.children?.length && isExpanded(item) && level < 3) {\n <ng-container\n [ngTemplateOutlet]=\"sheetTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\n />\n }\n </li>\n }\n </ul>\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t d\u00F2ng trong khay mobile: n\u1EC1n s\u00E1ng n\u00EAn ch\u1EEF theo token, kh\u00F4ng tr\u1EAFng -->\n<ng-template #sheetBody let-item let-level=\"level\" let-expanded=\"expanded\">\n @if (level === 1) {\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0\" />\n }\n <span class=\"min-w-0 flex-1 truncate text-sm\">{{ item.label }}</span>\n @if (item.extraText) {\n <span class=\"shrink-0 rounded-sm border border-default px-1 text-2xs leading-tight\">{{\n item.extraText\n }}</span>\n }\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n @if (item.children?.length) {\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 transition-transform duration-200\"\n [class.rotate-90]=\"expanded\"\n />\n }\n</ng-template>\n\n<!-- Ru\u1ED9t th\u1EBB t\u00E0i kho\u1EA3n \u1EDF \u0111\u00E1y menu d\u1ECDc: avatar + t\u00EAn/email (\u1EA9n khi thu g\u1ECDn) + m\u0169i t\u00EAn -->\n<ng-template #userCardBody let-user>\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs text-white/80\">{{ user.subtitle }}</span>\n }\n </span>\n <!--\n M\u0169i t\u00EAn CH\u1EC8 SANG PH\u1EA2I \u2014 menu t\u00E0i kho\u1EA3n m\u1EDF sang ph\u1EA3i c\u1EA1nh sidebar\n (`right-end`); m\u0169i t\u00EAn xu\u1ED1ng c\u1EE7a b\u1EA3n tr\u01B0\u1EDBc ch\u1EC9 sai h\u01B0\u1EDBng. R\u00EA chu\u1ED9t / \u0111ang\n m\u1EDF th\u00EC nh\u00EDch 2px sang ph\u1EA3i b\u00E1o h\u01B0\u1EDBng m\u1EDF.\n -->\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 text-white/80 transition-transform duration-200 group-hover/user:translate-x-0.5\"\n [class.translate-x-0.5]=\"userMenuOpen()\"\n />\n }\n</ng-template>\n\n<!-- Kh\u1ED1i t\u00E0i kho\u1EA3n \u0111\u1EA7u khay mobile -->\n<ng-template #sheetUserBody let-user>\n <ol-avatar\n size=\"lg\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\n }\n </span>\n</ng-template>\n\n<!--\n Ru\u1ED9t popup t\u00E0i kho\u1EA3n m\u1EB7c \u0111\u1ECBnh: \u0111\u1EA7u l\u00E0 avatar + t\u00EAn + email \u0111\u1EC3 ng\u01B0\u1EDDi d\u00F9ng\n bi\u1EBFt \u0111ang \u1EDF t\u00E0i kho\u1EA3n n\u00E0o, d\u01B0\u1EDBi l\u00E0 c\u00E1c m\u1EE5c `userActions()`.\n-->\n<ng-template #userMenuBody>\n @if (config().user; as user) {\n <div class=\"flex items-center gap-3 px-2 py-1.5\">\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\n }\n </span>\n </div>\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\n @for (action of userActions(); track action.id) {\n @if (action.dividerBefore && !$first) {\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\n }\n <ol-dropdown-item\n [icon]=\"action.icon\"\n [label]=\"action.label\"\n [routerLink]=\"action.route ?? null\"\n [danger]=\"action.danger ?? false\"\n (itemClick)=\"onUserAction(action)\"\n />\n }\n }\n</ng-template>\n" }]
|
|
17772
|
-
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], activeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeId", required: false }] }], logoSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "logoSrc", required: false }] }], logoMarkSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "logoMarkSrc", required: false }] }], logoTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "logoTone", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], collapsedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedWidth", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], userClick: [{ type: i0.Output, args: ["userClick"] }], userAction: [{ type: i0.Output, args: ["userAction"] }], userMenuOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "userMenuOpen", required: false }] }, { type: i0.Output, args: ["userMenuOpenChange"] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], mobilePrimaryCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobilePrimaryCount", required: false }] }], mobileMenuOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileMenuOpen", required: false }] }, { type: i0.Output, args: ["mobileMenuOpenChange"] }], mobileMenuLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileMenuLabel", required: false }] }], itemDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavItemDef), { isSignal: true }] }], headerDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavHeaderDef), { isSignal: true }] }], footerDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavFooterDef), { isSignal: true }] }], logoDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavLogoDef), { isSignal: true }] }], userDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavUserDef), { isSignal: true }] }], userMenuDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavUserMenuDef), { isSignal: true }] }], showRail: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRail", required: false }] }] } });
|
|
17864
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n [class]=\"\n 'relative h-full flex-col justify-between rounded-2xl pt-5 transition-[width] duration-200 ' +\n railDisplay()\n \"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 15px khi thu g\u1ECDn: `<ul>` l\u00E0 flex-col n\u00EAn `<li>` k\u00E9o gi\u00E3n m\u1EE5c\n con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-10` m\u1ED9t m\u00ECnh kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 15\u00D72 = 40, \u0111\u00FAng\n \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c (40px \u2014 b\u1EA3n g\u1ECDn 2026-10-06, Figma 44px / 13px).\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex min-h-0 flex-1 flex-col gap-5 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoDef(); as logo) {\n <ng-container\n [ngTemplateOutlet]=\"logo.template\"\n [ngTemplateOutletContext]=\"{ $implicit: collapsed(), collapsed: collapsed() }\"\n />\n } @else if (logoSrc()) {\n <img\n [src]=\"collapsed() && logoMarkSrc() ? logoMarkSrc() : logoSrc()\"\n alt=\"\u00D4n Luy\u1EC7n\"\n [class]=\"collapsed() ? 'size-10 object-contain' : 'h-10 w-auto max-w-full object-contain'\"\n />\n } @else {\n <!--\n Logo c\u1EE7a kit (Figma `Logo ol` #6603:1269). Thu g\u1ECDn: b\u1EA3n VU\u00D4NG 40px \u2014\n b\u1EA3n d\u00E0i r\u1ED9ng ~164px, \u1EDF khung 70px tr\u00E0n h\u1EB3n ra ngo\u00E0i. M\u1EDF: b\u1EA3n d\u00E0i cao\n 32px (~164px, Figma app-nav \u0111\u1EB7t logo r\u1ED9ng 155px).\n -->\n @if (collapsed()) {\n <ol-logo variant=\"mark\" [tone]=\"logoTone()\" [height]=\"40\" />\n } @else {\n <ol-logo variant=\"full\" [tone]=\"logoTone()\" [height]=\"32\" />\n }\n }\n </div>\n\n @if (headerDef(); as header) {\n <ng-container\n [ngTemplateOutlet]=\"header.template\"\n [ngTemplateOutletContext]=\"slotContext('rail')\"\n />\n }\n\n <!--\n V\u00F9ng m\u1EE5c CU\u1ED8N \u0110\u01AF\u1EE2C: menu nhi\u1EC1u nh\u00F3m (qu\u1EA3n tr\u1ECB tr\u01B0\u1EDDng: 5 nh\u00F3m, menu con 3\n c\u1EA5p) cao h\u01A1n m\u00E0n h\u00ECnh. Logo v\u00E0 th\u1EBB t\u00E0i kho\u1EA3n \u0111\u1EE9ng y\u00EAn, ch\u1EC9 danh s\u00E1ch cu\u1ED9n.\n\n Thanh cu\u1ED9n M\u1EA2NH, tr\u1EAFng m\u1EDD \u2014 KH\u00D4NG \u1EA9n h\u1EB3n: \u0111o menu gi\u00E1o vi\u00EAn (10 + 5 m\u1EE5c)\n \u1EDF m\u00E0n cao 900px, 3 m\u1EE5c cu\u1ED1i n\u1EB1m d\u01B0\u1EDBi m\u00E9p m\u00E0 kh\u00F4ng c\u00F3 d\u1EA5u hi\u1EC7u g\u00EC cho\n bi\u1EBFt c\u00F2n n\u1EEFa. Thanh m\u1EB7c \u0111\u1ECBnh x\u00E1m \u0111\u1EB7c th\u00EC ch\u01B0\u1EDBng tr\u00EAn n\u1EC1n navy.\n -->\n <div\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]\"\n >\n @for (group of mainGroups(); track $index) {\n <div class=\"flex flex-col gap-1\">\n @if (group.label && !collapsed()) {\n <p class=\"px-2.5 pt-1 pb-0.5 text-xs font-bold uppercase tracking-wide text-white/70\">\n {{ group.label }}\n </p>\n }\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\n />\n </div>\n }\n\n @if (footerDef(); as footer) {\n <ng-container\n [ngTemplateOutlet]=\"footer.template\"\n [ngTemplateOutletContext]=\"slotContext('rail')\"\n />\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex shrink-0 flex-col gap-1 overflow-hidden rounded-b-2xl pt-2 pb-4\"\n [class]=\"collapsed() ? 'px-[15px]' : 'px-4'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: footerItems(), level: 1 }\"\n />\n\n <!--\n TH\u1EBA T\u00C0I KHO\u1EA2N \u2014 b\u1EA5m m\u1EDF menu t\u00E0i kho\u1EA3n (popup n\u1ED5i `ol-dropdown`).\n\n Popup ch\u1EE9 kh\u00F4ng \u0111\u1ED5i danh s\u00E1ch t\u1EA1i ch\u1ED7: \u1EDF b\u1EC1 r\u1ED9ng 70px (thu g\u1ECDn) kh\u00F4ng c\u00F3\n ch\u1ED7 cho ba d\u00F2ng ch\u1EEF; popup n\u1ED5i ra ngo\u00E0i menu th\u00EC \u0111\u1ECDc \u0111\u01B0\u1EE3c \u1EDF c\u1EA3 hai tr\u1EA1ng\n th\u00E1i. Lu\u00F4n m\u1EDF SANG PH\u1EA2I, c\u1EA1nh sidebar (`right-end`: m\u00E9p d\u01B0\u1EDBi popup th\u1EB3ng\n m\u00E9p d\u01B0\u1EDBi th\u1EBB, popup m\u1ECDc l\u00EAn tr\u00EAn v\u00EC th\u1EBB n\u1EB1m s\u00E1t \u0111\u00E1y) \u2014 ng\u01B0\u1EDDi d\u00F9ng y\u00EAu\n c\u1EA7u 2026-10-06, b\u1EA3n tr\u01B0\u1EDBc m\u1EDF l\u00EAn tr\u00EAn \u0111\u00E8 l\u00EAn ch\u00EDnh menu khi m\u1EDF r\u1ED9ng.\n\n `user.actions = []` th\u00EC kh\u00F4ng c\u00F3 popup \u2014 b\u1EA5m th\u1EBB ch\u1EC9 ph\u00E1t `userClick`\n nh\u01B0 c\u00E1c b\u1EA3n tr\u01B0\u1EDBc.\n\n `autoClose=\"outside\"`: b\u1EA5m TRONG popup kh\u00F4ng t\u1EF1 \u0111\u00F3ng \u2014 ru\u1ED9t tu\u1EF3 bi\u1EBFn c\u00F3\n th\u1EC3 ch\u1EE9a \u0111i\u1EC1u khi\u1EC3n (b\u1ED9 ch\u1ECDn S\u00E1ng/T\u1ED1i) m\u00E0 ng\u01B0\u1EDDi d\u00F9ng c\u1EA7n th\u1EA5y k\u1EBFt qu\u1EA3.\n M\u1EE5c h\u00E0nh \u0111\u1ED9ng t\u1EF1 \u0111\u00F3ng qua `select()` / `close()` c\u1EE7a ng\u1EEF c\u1EA3nh.\n -->\n @if (config().user; as user) {\n <div class=\"mt-2 flex\" [class]=\"collapsed() ? 'justify-center' : ''\">\n @if (userActions().length > 0 || userMenuDef()) {\n <ol-dropdown\n class=\"min-w-0\"\n [class]=\"collapsed() ? '' : 'w-full'\"\n placement=\"right-end\"\n [offset]=\"userMenuOffset()\"\n menuClass=\"w-72\"\n autoClose=\"outside\"\n [(isOpen)]=\"userMenuOpen\"\n >\n <button\n trigger\n type=\"button\"\n [class]=\"userCardClasses()\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"userMenuOpen()\"\n [attr.aria-label]=\"collapsed() ? 'T\u00E0i kho\u1EA3n ' + user.name : null\"\n (click)=\"userClick.emit()\"\n >\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: collapsed(),\n initials: userInitials(),\n variant: 'rail',\n }\"\n />\n </button>\n\n <!--\n Ru\u1ED9t popup ch\u1EC9 d\u1EF1ng khi m\u1EDF: `ol-dropdown` \u1EA9n panel b\u1EB1ng class\n `hidden`, n\u00EAn d\u1EF1ng s\u1EB5n th\u00EC t\u00EAn + email n\u1EB1m trong DOM c\u1EA3 khi menu\n thu g\u1ECDn \u2014 tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u00E0 t\u00ECm ki\u1EBFm trang v\u1EABn th\u1EA5y ch\u00FAng.\n -->\n @if (userMenuOpen()) {\n @if (userMenuDef(); as menu) {\n <ng-container\n [ngTemplateOutlet]=\"menu.template\"\n [ngTemplateOutletContext]=\"userMenuPopupContext()\"\n />\n } @else {\n <ng-container [ngTemplateOutlet]=\"userMenuBody\" />\n }\n }\n </ol-dropdown>\n } @else {\n <button type=\"button\" [class]=\"userCardClasses()\" (click)=\"userClick.emit()\">\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? userCardBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: collapsed(),\n initials: userInitials(),\n variant: 'rail',\n }\"\n />\n </button>\n }\n </div>\n }\n </div>\n</nav>\n\n<!--\n \u2500\u2500 MOBILE: THANH \u0110\u00C1Y + KHAY MENU (`layout=\"auto\"` d\u01B0\u1EDBi `md`, ho\u1EB7c `\"mobile\"`) \u2500\u2500\n\n Chuy\u1EC3n t\u1EEB ph\u1EA7n t\u1EF1 d\u1EF1ng c\u1EE7a onluyenv4 v\u00E0o th\u01B0 vi\u1EC7n (2026-10-06):\n\n \u00B7 thanh \u0111\u00E1y `mobilePrimaryCount` m\u1EE5c \u0111\u1EA7u nh\u00F3m ch\u00EDnh + n\u00FAt Menu \u1EDF \u00F4 cu\u1ED1i\n \u00B7 khay tr\u01B0\u1EE3t l\u00EAn t\u1EEB \u0111\u00E1y (n\u00FAt m\u1EDF n\u1EB1m \u1EDF \u0111\u00E1y ph\u1EA3i \u2014 \u0111i theo ng\u00F3n tay), ch\u1EE9a\n th\u1EBB t\u00E0i kho\u1EA3n, c\u00E1c m\u1EE5c KH\u00D4NG l\u00EAn thanh \u0111\u00E1y, v\u00E0 menu t\u00E0i kho\u1EA3n\n \u00B7 `fixed` theo m\u00E0n h\u00ECnh; `pb-[env(safe-area-inset-bottom)]` ch\u1EEBa thanh vu\u1ED1t iPhone\n\n Khay d\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class: \u1EA9n b\u1EB1ng `hidden` th\u00EC n\u00FAt b\u00EAn trong\n v\u1EABn tab t\u1EDBi \u0111\u01B0\u1EE3c d\u00F9 kh\u00F4ng nh\u00ECn th\u1EA5y. L\u1EDBp ph\u1EE7 l\u00E0 `<button>` \u0111\u1EC3 \u0111\u00F3ng \u0111\u01B0\u1EE3c\n b\u1EB1ng b\u00E0n ph\u00EDm.\n-->\n@if (hasMobile()) {\n <nav\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)]\"\n [class]=\"mobileDisplay()\"\n aria-label=\"\u0110i\u1EC1u h\u01B0\u1EDBng ch\u00EDnh\"\n >\n @for (item of primaryItems(); track item.id) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"barItemClasses(item)\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n [attr.aria-label]=\"item.label\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? barBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'bar')\"\n />\n </a>\n }\n\n <button\n type=\"button\"\n class=\"flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 py-2 transition-colors\"\n [class]=\"mobileMenuOpen() ? 'text-fg-brand' : 'text-body-subtle hover:text-heading'\"\n [attr.aria-expanded]=\"mobileMenuOpen()\"\n [attr.aria-controls]=\"sheetId\"\n (click)=\"toggleMobileMenu()\"\n >\n <ol-icon name=\"solar:hamburger-menu-linear\" size=\"base\" />\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\n {{ mobileMenuLabel() }}\n </span>\n </button>\n </nav>\n\n <!--\n Khay v\u00E0 l\u1EDBp ph\u1EE7 D\u1EEANG tr\u00EAn thanh \u0111\u00E1y (cao 56px + v\u00F9ng vu\u1ED1t iPhone): thanh\n \u0111\u00E1y lu\u00F4n th\u1EA5y \u0111\u01B0\u1EE3c v\u00E0 b\u1EA5m \u0111\u01B0\u1EE3c khi khay m\u1EDF \u2014 n\u00FAt \"Menu\" b\u1EA5m l\u1EA7n n\u1EEFa th\u00EC\n \u0111\u00F3ng, c\u00E1c m\u1EE5c kh\u00E1c chuy\u1EC3n trang lu\u00F4n (ng\u01B0\u1EDDi d\u00F9ng y\u00EAu c\u1EA7u 2026-10-06). Thanh\n \u0111\u00E1y z-50, tr\u00EAn l\u1EDBp ph\u1EE7 z-40.\n -->\n @if (mobileMenuOpen()) {\n <button\n type=\"button\"\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\"\n animate.leave=\"opacity-0\"\n [class]=\"mobileDisplay()\"\n aria-label=\"\u0110\u00F3ng menu\"\n (click)=\"mobileMenuOpen.set(false)\"\n ></button>\n\n <!--\n Khung nh\u01B0 b\u1EA3n c\u0169: k\u00E9o h\u1EBFt b\u1EC1 ngang, d\u00EDnh m\u00E9p tr\u00EAn thanh \u0111\u00E1y, bo 2 g\u00F3c\n tr\u00EAn (ng\u01B0\u1EDDi d\u00F9ng 2026-10-06 \u2014 kh\u00F4ng th\u1EBB n\u1ED5i c\u00E1ch m\u00E9p). B\u00EAn trong `px-2`\n \u0111\u1EC3 m\u1ED7i d\u00F2ng bo tr\u00F2n ri\u00EAng. M\u1EDF: tr\u01B0\u1EE3t L\u00CAN t\u1EEB \u0111\u00E1y (`starting:` =\n `@starting-style`). \u0110\u00F3ng: tr\u01B0\u1EE3t XU\u1ED0NG \u2014 `animate.leave` g\u1EAFn\n `translate-y-full opacity-0`, Angular ch\u1EDD transition 200ms xong m\u1EDBi g\u1EE1\n kh\u1ECFi DOM (`@if` g\u1EE1 ngay th\u00EC kh\u00F4ng k\u1ECBp ch\u1EA1y).\n\n Hai ph\u1EA7n: \u0110\u1EA6U \u0111\u1EE9ng y\u00EAn (v\u1EA1ch k\u00E9o, th\u1EBB t\u00E0i kho\u1EA3n + \u0110\u0103ng xu\u1EA5t, v\u00E0 ph\u1EA7n `top`\n c\u1EE7a menu t\u00E0i kho\u1EA3n \u2014 Trang c\u00E1 nh\u00E2n, \u0110\u1ED5i m\u1EADt kh\u1EA9u, Giao di\u1EC7n\u2026), TH\u00C2N cu\u1ED9n\n (khe, nh\u00F3m m\u1EE5c, ph\u1EA7n `bottom`). Menu d\u00E0i th\u00EC ch\u1EC9 th\u00E2n cu\u1ED9n (ng\u01B0\u1EDDi d\u00F9ng\n 2026-10-06).\n -->\n <div\n [id]=\"sheetId\"\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\"\n animate.leave=\"translate-y-full opacity-0\"\n [class]=\"mobileDisplay()\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"mobileMenuLabel()\"\n >\n <div class=\"shrink-0\">\n <div class=\"flex justify-center pt-2.5\">\n <span class=\"h-1 w-10 rounded-full bg-neutral-tertiary\" aria-hidden=\"true\"></span>\n </div>\n\n @if (config().user; as user) {\n <div class=\"flex items-center gap-3 px-2 pt-2 pb-2\">\n <ng-container\n [ngTemplateOutlet]=\"userDef()?.template ?? sheetUserBody\"\n [ngTemplateOutletContext]=\"{\n $implicit: user,\n collapsed: false,\n initials: userInitials(),\n variant: 'sheet',\n }\"\n />\n <!--\n `split`: m\u1EE5c danger (\u0110\u0103ng xu\u1EA5t) th\u00E0nh n\u00FAt CH\u1EC8 ICON vi\u1EC1n \u0111\u1ECF b\u00EAn ph\u1EA3i th\u1EBB\n t\u00E0i kho\u1EA3n \u2014 m\u1ED9t ch\u1EA1m, kh\u00F4ng ph\u1EA3i cu\u1ED9n xu\u1ED1ng cu\u1ED1i khay (ng\u01B0\u1EDDi d\u00F9ng\n 2026-10-06). T\u00EAn \u0111\u1ECDc + tooltip (nh\u1EA5n gi\u1EEF) t\u1EEB `label`.\n -->\n @if (mobileUserMenu() === 'split') {\n @for (action of cardActions(); track action.id) {\n <ol-button\n class=\"ms-auto shrink-0\"\n color=\"danger\"\n [outline]=\"true\"\n size=\"sm\"\n [iconOnly]=\"true\"\n [ariaLabel]=\"action.label\"\n (btnClick)=\"onUserAction(action)\"\n >\n <ol-icon leftIcon [name]=\"action.icon\" size=\"base\" />\n </ol-button>\n }\n }\n </div>\n }\n\n <!--\n `mobileUserMenu=\"split\"`: vi\u1EC7c th\u01B0\u1EDDng c\u1EE7a t\u00E0i kho\u1EA3n (Trang c\u00E1 nh\u00E2n, \u0110\u1ED5i\n m\u1EADt kh\u1EA9u, c\u00E0i \u0111\u1EB7t\u2026) l\u00EAn \u0110\u1EA6U khay, ngay d\u01B0\u1EDBi th\u1EBB t\u00E0i kho\u1EA3n.\n -->\n @if (mobileUserMenu() === 'split') {\n <ng-container\n [ngTemplateOutlet]=\"sheetAccount\"\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetTopContext() }\"\n />\n }\n </div>\n\n <div class=\"-mx-2 min-h-0 flex-1 overflow-y-auto overscroll-contain px-2\">\n <!--\n `empty:hidden`: template khe c\u00F3 th\u1EC3 kh\u00F4ng v\u1EBD g\u00EC (b\u1ED9 ch\u1ECDn vai ch\u1EC9 hi\u1EC7n\n v\u1EDBi t\u00E0i kho\u1EA3n hai vai). Kh\u00F4ng c\u00F3 n\u00F3, khay c\u00F2n m\u1ED9t d\u1EA3i tr\u1ED1ng 24px k\u1EB9p\n gi\u1EEFa hai \u0111\u01B0\u1EDDng k\u1EBB. `:empty` b\u1ECF qua n\u00FAt ch\u00FA th\u00EDch Angular ch\u00E8n v\u00E0o.\n -->\n @if (headerDef(); as header) {\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\n <ng-container\n [ngTemplateOutlet]=\"header.template\"\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\n />\n </div>\n }\n\n @for (group of sheetGroups(); track $index) {\n <div class=\"mt-1 border-t border-default pt-1\">\n @if (group.label) {\n <p class=\"px-2 pt-2 pb-1 text-xs font-bold uppercase tracking-wide text-body-subtle\">\n {{ group.label }}\n </p>\n }\n <ng-container\n [ngTemplateOutlet]=\"sheetTree\"\n [ngTemplateOutletContext]=\"{ $implicit: group.items, level: 1 }\"\n />\n </div>\n }\n\n @if (footerDef(); as footer) {\n <div class=\"mt-1 border-t border-default px-2 pt-3 pb-2 empty:hidden\">\n <ng-container\n [ngTemplateOutlet]=\"footer.template\"\n [ngTemplateOutletContext]=\"slotContext('sheet')\"\n />\n </div>\n }\n\n <!-- Menu t\u00E0i kho\u1EA3n (cu\u1ED1i khay): tr\u1ECDn menu, ho\u1EB7c ch\u1EC9 ph\u1EA7n `bottom` khi `mobileUserMenu=\"split\"` -->\n <ng-container\n [ngTemplateOutlet]=\"sheetAccount\"\n [ngTemplateOutletContext]=\"{ $implicit: userMenuSheetContext() }\"\n />\n </div>\n </div>\n }\n}\n\n<!--\n Kh\u1ED1i menu t\u00E0i kho\u1EA3n trong khay \u2014 v\u1EBD \u1EDF \u0111\u1EA7u (`part: 'top'`) v\u00E0/ho\u1EB7c cu\u1ED1i khay.\n Template tu\u1EF3 bi\u1EBFn n\u1EBFu c\u00F3, kh\u00F4ng th\u00EC danh s\u00E1ch m\u1EE5c m\u1EB7c \u0111\u1ECBnh c\u1EE7a ph\u1EA7n \u0111\u00F3.\n `empty:hidden`: ph\u1EA7n kh\u00F4ng c\u00F3 m\u1EE5c n\u00E0o th\u00EC kh\u00F4ng \u0111\u1EC3 l\u1EA1i d\u1EA3i k\u1EBB.\n-->\n<ng-template #sheetAccount let-ctx>\n @if (ctx) {\n @if (userMenuDef(); as menu) {\n <!--\n `px-2`: ru\u1ED9t tu\u1EF3 bi\u1EBFn (ol-dropdown-item\u2026) kh\u00F4ng t\u1EF1 c\u00F3 l\u1EC1 ngang nh\u01B0 d\u00F2ng khay.\n `min-h-10` cho m\u1EE5c menu: ol-dropdown-item cao 36px, d\u00F2ng khay 40px \u2014 \u00E9p\n b\u1EB1ng nhau \u0111\u1EC3 c\u1EA3 khay m\u1ED9t nh\u1ECBp.\n -->\n <div\n class=\"mt-1 border-t border-default pt-1 empty:hidden [&_[role=menuitem]]:min-h-10 [&_[role=menuitem]]:rounded-md\"\n >\n <ng-container [ngTemplateOutlet]=\"menu.template\" [ngTemplateOutletContext]=\"ctx\" />\n </div>\n } @else if (sheetActionsFor(ctx.part).length > 0) {\n <ul class=\"mt-1 border-t border-default pt-1\">\n @for (action of sheetActionsFor(ctx.part); track action.id) {\n @if (action.dividerBefore && !$first) {\n <li class=\"my-1 h-px bg-neutral-tertiary\" aria-hidden=\"true\"></li>\n }\n <li>\n @if (action.route) {\n <a\n [routerLink]=\"action.route\"\n [class]=\"sheetActionClasses(action)\"\n (click)=\"onUserAction(action)\"\n >\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"sheetActionClasses(action)\"\n (click)=\"onUserAction(action)\"\n >\n <ol-icon [name]=\"action.icon\" size=\"sm\" class=\"shrink-0\" />\n <span class=\"min-w-0 flex-1 truncate\">{{ action.label }}</span>\n </button>\n }\n </li>\n }\n </ul>\n }\n }\n</ng-template>\n\n<!--\n C\u00E2y m\u1EE5c (\u0111\u1EC7 quy) \u2014 c\u1EA5p 1 \u00B7 2 \u00B7 3. Nh\u00E1nh con ch\u1EC9 v\u1EBD khi m\u1EDF v\u00E0 menu kh\u00F4ng\n thu g\u1ECDn; thu g\u1ECDn th\u00EC menu con hi\u1EC7n N\u1ED4I c\u1EA1nh m\u1EE5c cha (xem `#navItem`).\n-->\n<ng-template #navTree let-items let-level=\"level\">\n <ul class=\"flex flex-col gap-1\" [class]=\"level === 1 ? '' : 'mt-1'\">\n @for (item of items; track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item, level: level }\"\n />\n @if (item.children?.length && isExpanded(item) && !collapsed() && level < 3) {\n <ng-container\n [ngTemplateOutlet]=\"navTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\n />\n }\n </li>\n }\n </ul>\n</ng-template>\n\n<!--\n M\u1ED9t m\u1EE5c menu. Ba lo\u1EA1i h\u00E0nh vi, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 con \u2192 `<button aria-expanded>` m\u1EDF/\u0111\u00F3ng nh\u00E1nh, kh\u00F4ng \u0111i\u1EC1u h\u01B0\u1EDBng\n c\u00F3 `route` \u2192 `<a routerLink>`; m\u1EE5c \u0111ang ch\u1ECDn t\u00EDnh b\u1EB1ng `isItemActive`\n (route \u00B7 activeRoutes \u00B7 activeId), kh\u00F4ng `routerLinkActive`\n c\u00F2n l\u1EA1i \u2192 `<button>` ch\u1EC9 ph\u00E1t `itemClick` (disabled lo trong class)\n-->\n<ng-template #navItem let-item let-level=\"level\">\n @if (item.children?.length && collapsed() && level === 1) {\n <!--\n THU G\u1ECCN + m\u1EE5c cha: r\u00EA chu\u1ED9t / b\u1EA5m / Enter m\u1EDF menu con N\u1ED4I b\u00EAn ph\u1EA3i\n (`ol-dropdown`). Kh\u00F4ng tooltip \u2014 \u0111\u1EA7u panel \u0111\u00E3 ghi t\u00EAn m\u1EE5c.\n -->\n <div\n class=\"flex justify-center\"\n (mouseenter)=\"openFlyout(item.id)\"\n (mouseleave)=\"scheduleFlyoutClose()\"\n >\n <ol-dropdown\n placement=\"right\"\n menuClass=\"w-64\"\n [isOpen]=\"flyoutId() === item.id\"\n (isOpenChange)=\"onFlyoutOpenChange(item.id, $event)\"\n >\n <button\n trigger\n type=\"button\"\n [class]=\"itemClasses(item, level)\"\n [attr.aria-label]=\"item.label\"\n aria-haspopup=\"menu\"\n [attr.aria-expanded]=\"flyoutId() === item.id\"\n [disabled]=\"item.disabled ?? false\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n\n @if (flyoutId() === item.id) {\n <div (mouseenter)=\"cancelFlyoutClose()\" (mouseleave)=\"scheduleFlyoutClose()\">\n <p class=\"px-2 pt-1 pb-1.5 text-xs font-bold uppercase tracking-wide text-body-subtle\">\n {{ item.label }}\n </p>\n <ng-container\n [ngTemplateOutlet]=\"flyoutTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: 2 }\"\n />\n </div>\n }\n </ol-dropdown>\n </div>\n } @else if (item.children?.length) {\n <button\n type=\"button\"\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [attr.aria-expanded]=\"isExpanded(item)\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n } @else if (item.route) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"itemClasses(item, level)\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item, level) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() && level === 1 ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!(collapsed() && level === 1)\"\n [attr.aria-label]=\"collapsed() && level === 1 ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? itemBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'rail', level)\"\n />\n </button>\n }\n</ng-template>\n\n<!--\n Ru\u1ED9t menu n\u1ED5i: con l\u00E0 li\u00EAn k\u1EBFt (`ol-dropdown-item`); con c\u00F3 ch\u00E1u th\u00EC th\u00E0nh\n nh\u00E3n nh\u00F3m, ch\u00E1u th\u1EE5t v\u00E0o d\u01B0\u1EDBi n\u00F3. M\u1EE5c \u0111ang ch\u1ECDn mang `selected`.\n-->\n<ng-template #flyoutTree let-items let-level=\"level\">\n @for (child of items; track child.id) {\n @if (child.children?.length) {\n <p class=\"px-2 pt-2 pb-1 text-sm font-semibold text-heading\" [class.ps-5]=\"level > 2\">\n {{ child.label }}\n </p>\n <div class=\"ps-3\">\n <ng-container\n [ngTemplateOutlet]=\"flyoutTree\"\n [ngTemplateOutletContext]=\"{ $implicit: child.children, level: level + 1 }\"\n />\n </div>\n } @else {\n <ol-dropdown-item\n [label]=\"child.label\"\n [routerLink]=\"child.route ?? null\"\n [selected]=\"isItemActive(child)\"\n [disabled]=\"child.disabled ?? false\"\n (itemClick)=\"onFlyoutSelect(child)\"\n />\n }\n }\n</ng-template>\n\n<!--\n Ru\u1ED9t m\u1ED9t m\u1EE5c \u1EDF menu d\u1ECDc: icon 24px (ch\u1EC9 c\u1EA5p 1) + nh\u00E3n + nh\u00E3n ph\u1EE5 + huy hi\u1EC7u\n + m\u0169i t\u00EAn (m\u1EE5c cha). Nh\u00E3n \u1EA9n khi thu g\u1ECDn.\n-->\n<ng-template #itemBody let-item let-level=\"level\" let-expanded=\"expanded\">\n @if (level === 1) {\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n }\n\n @if (!collapsed() || level > 1) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.extraText) {\n <span\n class=\"shrink-0 rounded-sm border border-white px-1 text-2xs leading-tight text-white\"\n >{{ item.extraText }}</span\n >\n }\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n @if (item.children?.length) {\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 text-white transition-transform duration-200\"\n [class.rotate-90]=\"expanded\"\n />\n }\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c \u1EDF thanh \u0111\u00E1y mobile: icon + nh\u00E3n ng\u1EAFn (\u0111\u1ECDc nh\u00E3n \u0111\u1EA7y \u0111\u1EE7 qua aria-label) -->\n<ng-template #barBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" />\n <span class=\"w-full truncate px-0.5 text-center text-2xs leading-tight\">\n {{ item.shortLabel ?? item.label }}\n </span>\n</ng-template>\n\n<!-- C\u00E2y m\u1EE5c trong khay mobile \u2014 c\u00F9ng lu\u1EADt m\u1EDF/\u0111\u00F3ng v\u1EDBi menu d\u1ECDc -->\n<ng-template #sheetTree let-items let-level=\"level\">\n <ul>\n @for (item of items; track item.id) {\n <li>\n @if (item.route && !item.children?.length) {\n <a\n [routerLink]=\"item.route\"\n [class]=\"sheetItemClasses(item, level)\"\n [attr.aria-current]=\"isItemActive(item) ? 'page' : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\n />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"sheetItemClasses(item, level)\"\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item) : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container\n [ngTemplateOutlet]=\"itemDef()?.template ?? sheetBody\"\n [ngTemplateOutletContext]=\"itemContext(item, 'sheet', level)\"\n />\n </button>\n }\n @if (item.children?.length && isExpanded(item) && level < 3) {\n <ng-container\n [ngTemplateOutlet]=\"sheetTree\"\n [ngTemplateOutletContext]=\"{ $implicit: item.children, level: level + 1 }\"\n />\n }\n </li>\n }\n </ul>\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t d\u00F2ng trong khay mobile: n\u1EC1n s\u00E1ng n\u00EAn ch\u1EEF theo token, kh\u00F4ng tr\u1EAFng -->\n<ng-template #sheetBody let-item let-level=\"level\" let-expanded=\"expanded\">\n @if (level === 1) {\n <ol-icon [name]=\"item.icon\" size=\"sm\" class=\"shrink-0\" />\n }\n <span class=\"min-w-0 flex-1 truncate\">{{ item.label }}</span>\n @if (item.extraText) {\n <span class=\"shrink-0 rounded-sm border border-default px-1 text-2xs leading-tight\">{{\n item.extraText\n }}</span>\n }\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n @if (item.children?.length) {\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 transition-transform duration-200\"\n [class.rotate-90]=\"expanded\"\n />\n }\n</ng-template>\n\n<!-- Ru\u1ED9t th\u1EBB t\u00E0i kho\u1EA3n \u1EDF \u0111\u00E1y menu d\u1ECDc: avatar + t\u00EAn/email (\u1EA9n khi thu g\u1ECDn) + m\u0169i t\u00EAn -->\n<ng-template #userCardBody let-user>\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs text-white/80\">{{ user.subtitle }}</span>\n }\n </span>\n <!--\n M\u0169i t\u00EAn CH\u1EC8 SANG PH\u1EA2I \u2014 menu t\u00E0i kho\u1EA3n m\u1EDF sang ph\u1EA3i c\u1EA1nh sidebar\n (`right-end`); m\u0169i t\u00EAn xu\u1ED1ng c\u1EE7a b\u1EA3n tr\u01B0\u1EDBc ch\u1EC9 sai h\u01B0\u1EDBng. R\u00EA chu\u1ED9t / \u0111ang\n m\u1EDF th\u00EC nh\u00EDch 2px sang ph\u1EA3i b\u00E1o h\u01B0\u1EDBng m\u1EDF.\n -->\n <ol-icon\n name=\"alt-arrow-right\"\n size=\"xs\"\n class=\"shrink-0 text-white/80 transition-transform duration-200 group-hover/user:translate-x-0.5\"\n [class.translate-x-0.5]=\"userMenuOpen()\"\n />\n }\n</ng-template>\n\n<!-- Kh\u1ED1i t\u00E0i kho\u1EA3n \u0111\u1EA7u khay mobile -->\n<ng-template #sheetUserBody let-user>\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\n }\n </span>\n</ng-template>\n\n<!--\n Ru\u1ED9t popup t\u00E0i kho\u1EA3n m\u1EB7c \u0111\u1ECBnh: \u0111\u1EA7u l\u00E0 avatar + t\u00EAn + email \u0111\u1EC3 ng\u01B0\u1EDDi d\u00F9ng\n bi\u1EBFt \u0111ang \u1EDF t\u00E0i kho\u1EA3n n\u00E0o, d\u01B0\u1EDBi l\u00E0 c\u00E1c m\u1EE5c `userActions()`.\n-->\n<ng-template #userMenuBody>\n @if (config().user; as user) {\n <div class=\"flex items-center gap-3 px-2 py-1.5\">\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-sm font-semibold text-heading\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"block truncate text-xs text-body-subtle\">{{ user.subtitle }}</span>\n }\n </span>\n </div>\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\n @for (action of userActions(); track action.id) {\n @if (action.dividerBefore && !$first) {\n <div class=\"h-px bg-neutral-tertiary\" role=\"separator\"></div>\n }\n <ol-dropdown-item\n [icon]=\"action.icon\"\n [label]=\"action.label\"\n [routerLink]=\"action.route ?? null\"\n [danger]=\"action.danger ?? false\"\n (itemClick)=\"onUserAction(action)\"\n />\n }\n }\n</ng-template>\n" }]
|
|
17865
|
+
}], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: true }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }, { type: i0.Output, args: ["collapsedChange"] }], activeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeId", required: false }] }], logoSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "logoSrc", required: false }] }], logoMarkSrc: [{ type: i0.Input, args: [{ isSignal: true, alias: "logoMarkSrc", required: false }] }], logoTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "logoTone", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], collapsedWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedWidth", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], userClick: [{ type: i0.Output, args: ["userClick"] }], userAction: [{ type: i0.Output, args: ["userAction"] }], userMenuOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "userMenuOpen", required: false }] }, { type: i0.Output, args: ["userMenuOpenChange"] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], mobilePrimaryCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobilePrimaryCount", required: false }] }], mobileMenuOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileMenuOpen", required: false }] }, { type: i0.Output, args: ["mobileMenuOpenChange"] }], mobileUserMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileUserMenu", required: false }] }], mobileMenuLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileMenuLabel", required: false }] }], itemDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavItemDef), { isSignal: true }] }], headerDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavHeaderDef), { isSignal: true }] }], footerDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavFooterDef), { isSignal: true }] }], logoDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavLogoDef), { isSignal: true }] }], userDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavUserDef), { isSignal: true }] }], userMenuDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppNavUserMenuDef), { isSignal: true }] }], showRail: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRail", required: false }] }] } });
|
|
17773
17866
|
/** Tra cứu trong cây mục — tách khỏi component để đọc gọn. */
|
|
17774
17867
|
const AppNavTreeIds = {
|
|
17775
17868
|
/** Id của mục và mọi con cháu. */
|
|
@@ -17789,6 +17882,292 @@ const AppNavTreeIds = {
|
|
|
17789
17882
|
},
|
|
17790
17883
|
};
|
|
17791
17884
|
|
|
17885
|
+
/** `<ng-template olAppHeaderTitle>` */
|
|
17886
|
+
class OlAppHeaderTitleDef {
|
|
17887
|
+
template = inject(TemplateRef);
|
|
17888
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppHeaderTitleDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
17889
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: OlAppHeaderTitleDef, isStandalone: true, selector: "ng-template[olAppHeaderTitle]", ngImport: i0 });
|
|
17890
|
+
}
|
|
17891
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppHeaderTitleDef, decorators: [{
|
|
17892
|
+
type: Directive,
|
|
17893
|
+
args: [{ selector: 'ng-template[olAppHeaderTitle]', standalone: true }]
|
|
17894
|
+
}] });
|
|
17895
|
+
/** `<ng-template olAppHeaderActions>` */
|
|
17896
|
+
class OlAppHeaderActionsDef {
|
|
17897
|
+
template = inject(TemplateRef);
|
|
17898
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppHeaderActionsDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
17899
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: OlAppHeaderActionsDef, isStandalone: true, selector: "ng-template[olAppHeaderActions]", ngImport: i0 });
|
|
17900
|
+
}
|
|
17901
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppHeaderActionsDef, decorators: [{
|
|
17902
|
+
type: Directive,
|
|
17903
|
+
args: [{ selector: 'ng-template[olAppHeaderActions]', standalone: true }]
|
|
17904
|
+
}] });
|
|
17905
|
+
/** `<ng-template olAppHeaderContent>` */
|
|
17906
|
+
class OlAppHeaderContentDef {
|
|
17907
|
+
template = inject(TemplateRef);
|
|
17908
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppHeaderContentDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
17909
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: OlAppHeaderContentDef, isStandalone: true, selector: "ng-template[olAppHeaderContent]", ngImport: i0 });
|
|
17910
|
+
}
|
|
17911
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppHeaderContentDef, decorators: [{
|
|
17912
|
+
type: Directive,
|
|
17913
|
+
args: [{ selector: 'ng-template[olAppHeaderContent]', standalone: true }]
|
|
17914
|
+
}] });
|
|
17915
|
+
/** Gom sẵn để import một lần. */
|
|
17916
|
+
const OL_APP_HEADER_TEMPLATES = [
|
|
17917
|
+
OlAppHeaderTitleDef,
|
|
17918
|
+
OlAppHeaderActionsDef,
|
|
17919
|
+
OlAppHeaderContentDef,
|
|
17920
|
+
];
|
|
17921
|
+
|
|
17922
|
+
/**
|
|
17923
|
+
* Thanh trên của trang trong app — đi cặp `ol-app-nav`.
|
|
17924
|
+
*
|
|
17925
|
+
* Bố cục một hàng: [←] [tiêu đề] [ô tìm — co theo chỗ] [hành động].
|
|
17926
|
+
*
|
|
17927
|
+
* ## Hai hành vi theo bề ngang
|
|
17928
|
+
*
|
|
17929
|
+
* Desktop theo `variant`. Dưới `md` (`mobileSolid`, mặc định bật) header luôn
|
|
17930
|
+
* DÍNH ĐỈNH và NỀN ĐẶC: ở màn hẹp không còn menu trái, header là mốc duy nhất
|
|
17931
|
+
* cho biết đang ở màn nào; dính mà trong suốt thì nội dung cuộn qua hiện
|
|
17932
|
+
* xuyên lên, chữ chồng chữ.
|
|
17933
|
+
*
|
|
17934
|
+
* ## Ô tìm co giãn
|
|
17935
|
+
*
|
|
17936
|
+
* Từ `searchCollapseAt` trở lên ô nằm giữa hàng, `flex-1`, co lại khi chật;
|
|
17937
|
+
* dưới mốc đó thành nút kính lúp mở ô trong bảng thả xuống — không bóp ô tới
|
|
17938
|
+
* mức không gõ nổi.
|
|
17939
|
+
*
|
|
17940
|
+
* ## Khe
|
|
17941
|
+
*
|
|
17942
|
+
* `olAppHeaderTitle` · `olAppHeaderActions` · `olAppHeaderContent` — xem
|
|
17943
|
+
* `app-header-templates.ts`. `olAppHeaderContent` thay trọn ruột (thanh làm
|
|
17944
|
+
* bài), khung vẫn của component.
|
|
17945
|
+
*/
|
|
17946
|
+
class OlAppHeader {
|
|
17947
|
+
router = inject(Router, { optional: true });
|
|
17948
|
+
variant = input('plain', /* @ts-ignore */
|
|
17949
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
17950
|
+
size = input('base', /* @ts-ignore */
|
|
17951
|
+
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
17952
|
+
/** Dưới `md`: dính đỉnh + nền đặc + viền dưới. */
|
|
17953
|
+
mobileSolid = input(true, /* @ts-ignore */
|
|
17954
|
+
...(ngDevMode ? [{ debugName: "mobileSolid" }] : /* istanbul ignore next */ []));
|
|
17955
|
+
/** Dính đỉnh cả trên desktop — `plain` tự có nền đặc khi dính. */
|
|
17956
|
+
sticky = input(false, /* @ts-ignore */
|
|
17957
|
+
...(ngDevMode ? [{ debugName: "sticky" }] : /* istanbul ignore next */ []));
|
|
17958
|
+
/**
|
|
17959
|
+
* Class thêm vào thẻ `<header>` — host là `contents` nên class đặt trên
|
|
17960
|
+
* `<ol-app-header>` không có hộp nào để áp. Ví dụ kéo tràn mép ở mobile:
|
|
17961
|
+
* `headerClass="-mx-4 -mt-4 md:mx-0 md:mt-0"`.
|
|
17962
|
+
*/
|
|
17963
|
+
headerClass = input('', /* @ts-ignore */
|
|
17964
|
+
...(ngDevMode ? [{ debugName: "headerClass" }] : /* istanbul ignore next */ []));
|
|
17965
|
+
title = input('', /* @ts-ignore */
|
|
17966
|
+
...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
17967
|
+
/** Dòng nhỏ phía trên tiêu đề, ví dụ "HỌC SINH · ÔN LUYỆN". Ẩn dưới `sm`. */
|
|
17968
|
+
subtitle = input('', /* @ts-ignore */
|
|
17969
|
+
...(ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []));
|
|
17970
|
+
/**
|
|
17971
|
+
* Đường dẫn phía trên tiêu đề, thay `subtitle` khi có ≥ 2 mục. Mục cuối là
|
|
17972
|
+
* trang hiện tại (không bấm được). Ẩn dưới `sm` như `subtitle`.
|
|
17973
|
+
*/
|
|
17974
|
+
breadcrumbs = input([], /* @ts-ignore */
|
|
17975
|
+
...(ngDevMode ? [{ debugName: "breadcrumbs" }] : /* istanbul ignore next */ []));
|
|
17976
|
+
showBreadcrumbs = computed(() => (this.breadcrumbs() ?? []).length >= 2, /* @ts-ignore */
|
|
17977
|
+
...(ngDevMode ? [{ debugName: "showBreadcrumbs" }] : /* istanbul ignore next */ []));
|
|
17978
|
+
// ── Quay lại ──────────────────────────────────────────────────────
|
|
17979
|
+
/** Đường dẫn quay lại — bấm thì `router.navigateByUrl`. */
|
|
17980
|
+
backLink = input('', /* @ts-ignore */
|
|
17981
|
+
...(ngDevMode ? [{ debugName: "backLink" }] : /* istanbul ignore next */ []));
|
|
17982
|
+
/** Hiện nút quay lại và chỉ phát `(back)` — nơi dùng tự điều hướng. */
|
|
17983
|
+
showBack = input(false, /* @ts-ignore */
|
|
17984
|
+
...(ngDevMode ? [{ debugName: "showBack" }] : /* istanbul ignore next */ []));
|
|
17985
|
+
/** Tên đọc + tooltip của nút quay lại: "Quay lại <backLabel>". */
|
|
17986
|
+
backLabel = input('', /* @ts-ignore */
|
|
17987
|
+
...(ngDevMode ? [{ debugName: "backLabel" }] : /* istanbul ignore next */ []));
|
|
17988
|
+
back = output();
|
|
17989
|
+
// ── Ô tìm ─────────────────────────────────────────────────────────
|
|
17990
|
+
showSearch = input(false, /* @ts-ignore */
|
|
17991
|
+
...(ngDevMode ? [{ debugName: "showSearch" }] : /* istanbul ignore next */ []));
|
|
17992
|
+
searchPlaceholder = input('Tìm kiếm…', /* @ts-ignore */
|
|
17993
|
+
...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
|
|
17994
|
+
searchAriaLabel = input('Tìm kiếm', /* @ts-ignore */
|
|
17995
|
+
...(ngDevMode ? [{ debugName: "searchAriaLabel" }] : /* istanbul ignore next */ []));
|
|
17996
|
+
searchCollapseAt = input('xl', /* @ts-ignore */
|
|
17997
|
+
...(ngDevMode ? [{ debugName: "searchCollapseAt" }] : /* istanbul ignore next */ []));
|
|
17998
|
+
searchValue = model('', /* @ts-ignore */
|
|
17999
|
+
...(ngDevMode ? [{ debugName: "searchValue" }] : /* istanbul ignore next */ []));
|
|
18000
|
+
/** Bấm Enter trong ô tìm — giá trị đã cắt khoảng trắng, bỏ qua khi rỗng. */
|
|
18001
|
+
search = output();
|
|
18002
|
+
// ── Khe ───────────────────────────────────────────────────────────
|
|
18003
|
+
titleDef = contentChild(OlAppHeaderTitleDef, /* @ts-ignore */
|
|
18004
|
+
...(ngDevMode ? [{ debugName: "titleDef" }] : /* istanbul ignore next */ []));
|
|
18005
|
+
actionsDef = contentChild(OlAppHeaderActionsDef, /* @ts-ignore */
|
|
18006
|
+
...(ngDevMode ? [{ debugName: "actionsDef" }] : /* istanbul ignore next */ []));
|
|
18007
|
+
contentDef = contentChild(OlAppHeaderContentDef, /* @ts-ignore */
|
|
18008
|
+
...(ngDevMode ? [{ debugName: "contentDef" }] : /* istanbul ignore next */ []));
|
|
18009
|
+
hasBack = computed(() => this.showBack() || !!this.backLink(), /* @ts-ignore */
|
|
18010
|
+
...(ngDevMode ? [{ debugName: "hasBack" }] : /* istanbul ignore next */ []));
|
|
18011
|
+
backAriaLabel = computed(() => this.backLabel() ? `Quay lại ${this.backLabel()}` : 'Quay lại', /* @ts-ignore */
|
|
18012
|
+
...(ngDevMode ? [{ debugName: "backAriaLabel" }] : /* istanbul ignore next */ []));
|
|
18013
|
+
/** Khung `<header>` — kiểu nhìn + hành vi mobile + dính đỉnh. */
|
|
18014
|
+
headerClasses = computed(() => {
|
|
18015
|
+
const height = this.size() === 'sm' ? 'h-14' : 'h-16';
|
|
18016
|
+
const base = `flex w-full items-center gap-3 px-4 ${height}`;
|
|
18017
|
+
const look = {
|
|
18018
|
+
plain: 'bg-transparent',
|
|
18019
|
+
solid: 'border-b border-default bg-neutral-primary',
|
|
18020
|
+
floating: 'rounded-lg border border-default bg-neutral-primary shadow-xs',
|
|
18021
|
+
};
|
|
18022
|
+
const sticky = this.sticky()
|
|
18023
|
+
? this.variant() === 'plain'
|
|
18024
|
+
? 'sticky top-0 z-30 bg-neutral-secondary'
|
|
18025
|
+
: 'sticky top-0 z-30'
|
|
18026
|
+
: '';
|
|
18027
|
+
// Dưới md: luôn nền đặc + dính — ghi đè bo góc / bóng của `floating`.
|
|
18028
|
+
const mobile = this.mobileSolid()
|
|
18029
|
+
? 'max-md:sticky max-md:top-0 max-md:z-30 max-md:rounded-none max-md:border-0 max-md:border-b max-md:border-default max-md:bg-neutral-primary max-md:shadow-none'
|
|
18030
|
+
: '';
|
|
18031
|
+
return [base, look[this.variant()], sticky, mobile, this.headerClass()]
|
|
18032
|
+
.filter(Boolean)
|
|
18033
|
+
.join(' ');
|
|
18034
|
+
}, /* @ts-ignore */
|
|
18035
|
+
...(ngDevMode ? [{ debugName: "headerClasses" }] : /* istanbul ignore next */ []));
|
|
18036
|
+
/** Ô tìm đầy đủ: chỉ hiện từ mốc. Viết đủ hai chuỗi để Tailwind quét thấy. */
|
|
18037
|
+
searchInlineClasses = computed(() => this.searchCollapseAt() === 'lg'
|
|
18038
|
+
? 'hidden min-w-0 max-w-md flex-1 lg:block'
|
|
18039
|
+
: 'hidden min-w-0 max-w-md flex-1 xl:block', /* @ts-ignore */
|
|
18040
|
+
...(ngDevMode ? [{ debugName: "searchInlineClasses" }] : /* istanbul ignore next */ []));
|
|
18041
|
+
/** Nút kính lúp: chỉ dưới mốc. */
|
|
18042
|
+
searchToggleClasses = computed(() => this.searchCollapseAt() === 'lg' ? 'shrink-0 lg:hidden' : 'shrink-0 xl:hidden', /* @ts-ignore */
|
|
18043
|
+
...(ngDevMode ? [{ debugName: "searchToggleClasses" }] : /* istanbul ignore next */ []));
|
|
18044
|
+
/**
|
|
18045
|
+
* Khối tiêu đề: chiếm chỗ trống khi ô tìm không nằm giữa hàng; khi ô tìm
|
|
18046
|
+
* hiện thì nhường `flex-1` cho ô tìm, tiêu đề chỉ rộng theo chữ.
|
|
18047
|
+
*/
|
|
18048
|
+
titleClasses = computed(() => {
|
|
18049
|
+
if (!this.showSearch())
|
|
18050
|
+
return 'min-w-0 flex-1';
|
|
18051
|
+
return this.searchCollapseAt() === 'lg'
|
|
18052
|
+
? 'min-w-0 flex-1 lg:max-w-[40%] lg:flex-none'
|
|
18053
|
+
: 'min-w-0 flex-1 xl:max-w-[40%] xl:flex-none';
|
|
18054
|
+
}, /* @ts-ignore */
|
|
18055
|
+
...(ngDevMode ? [{ debugName: "titleClasses" }] : /* istanbul ignore next */ []));
|
|
18056
|
+
onBack() {
|
|
18057
|
+
this.back.emit();
|
|
18058
|
+
const link = this.backLink();
|
|
18059
|
+
if (link && this.router)
|
|
18060
|
+
void this.router.navigateByUrl(link);
|
|
18061
|
+
}
|
|
18062
|
+
submitSearch(event) {
|
|
18063
|
+
event.preventDefault();
|
|
18064
|
+
const value = (this.searchValue() ?? '').trim();
|
|
18065
|
+
if (value)
|
|
18066
|
+
this.search.emit(value);
|
|
18067
|
+
}
|
|
18068
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppHeader, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18069
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAppHeader, isStandalone: true, selector: "ol-app-header", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, mobileSolid: { classPropertyName: "mobileSolid", publicName: "mobileSolid", isSignal: true, isRequired: false, transformFunction: null }, sticky: { classPropertyName: "sticky", publicName: "sticky", isSignal: true, isRequired: false, transformFunction: null }, headerClass: { classPropertyName: "headerClass", publicName: "headerClass", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, breadcrumbs: { classPropertyName: "breadcrumbs", publicName: "breadcrumbs", isSignal: true, isRequired: false, transformFunction: null }, backLink: { classPropertyName: "backLink", publicName: "backLink", isSignal: true, isRequired: false, transformFunction: null }, showBack: { classPropertyName: "showBack", publicName: "showBack", isSignal: true, isRequired: false, transformFunction: null }, backLabel: { classPropertyName: "backLabel", publicName: "backLabel", isSignal: true, isRequired: false, transformFunction: null }, showSearch: { classPropertyName: "showSearch", publicName: "showSearch", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, searchAriaLabel: { classPropertyName: "searchAriaLabel", publicName: "searchAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, searchCollapseAt: { classPropertyName: "searchCollapseAt", publicName: "searchCollapseAt", isSignal: true, isRequired: false, transformFunction: null }, searchValue: { classPropertyName: "searchValue", publicName: "searchValue", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { back: "back", searchValue: "searchValueChange", search: "search" }, host: { classAttribute: "contents" }, queries: [{ propertyName: "titleDef", first: true, predicate: OlAppHeaderTitleDef, descendants: true, isSignal: true }, { propertyName: "actionsDef", first: true, predicate: OlAppHeaderActionsDef, descendants: true, isSignal: true }, { propertyName: "contentDef", first: true, predicate: OlAppHeaderContentDef, descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n Thanh tr\u00EAn c\u1EE7a trang trong app. Khung (`headerClasses`) lu\u00F4n c\u1EE7a component;\n ru\u1ED9t m\u1EB7c \u0111\u1ECBnh l\u00E0 [\u2190] [ti\u00EAu \u0111\u1EC1] [\u00F4 t\u00ECm] [h\u00E0nh \u0111\u1ED9ng], ho\u1EB7c tr\u1ECDn khe\n `olAppHeaderContent`.\n-->\n<header [class]=\"headerClasses()\">\n @if (contentDef(); as content) {\n <div class=\"flex min-w-0 flex-1 items-center gap-3\">\n <ng-container [ngTemplateOutlet]=\"content.template\" />\n </div>\n } @else {\n @if (hasBack()) {\n <ol-button\n class=\"shrink-0\"\n color=\"ghost\"\n [iconOnly]=\"true\"\n [ariaLabel]=\"backAriaLabel()\"\n (btnClick)=\"onBack()\"\n >\n <ol-icon leftIcon name=\"solar:arrow-left-linear\" size=\"base\" />\n </ol-button>\n }\n\n <div [class]=\"titleClasses()\">\n @if (titleDef(); as titleTpl) {\n <ng-container [ngTemplateOutlet]=\"titleTpl.template\" />\n } @else {\n @if (showBreadcrumbs()) {\n <!-- \u0110\u01B0\u1EDDng d\u1EABn: m\u1EE5c cu\u1ED1i l\u00E0 trang hi\u1EC7n t\u1EA1i; ch\u1EEF xs \u0111\u1EC3 ti\u00EAu \u0111\u1EC1 n\u1ED5i h\u01A1n; \u1EA9n d\u01B0\u1EDBi sm -->\n <ol-breadcrumb class=\"hidden sm:block [&_a]:text-xs [&_span]:text-xs\" separator=\"chevron\">\n @for (crumb of breadcrumbs(); track $index) {\n <ol-breadcrumb-item\n [label]=\"crumb.label\"\n [routerLink]=\"$last ? null : (crumb.link ?? null)\"\n [current]=\"$last\"\n />\n }\n </ol-breadcrumb>\n } @else if (subtitle()) {\n <p\n class=\"hidden truncate text-2xs font-medium uppercase tracking-wide text-body-subtle sm:block\"\n >\n {{ subtitle() }}\n </p>\n }\n @if (title()) {\n <h1 class=\"truncate text-base font-semibold leading-tight text-heading\">\n {{ title() }}\n </h1>\n }\n }\n </div>\n\n @if (showSearch()) {\n <!-- \u00D4 t\u00ECm \u0111\u1EA7y \u0111\u1EE7 \u2014 t\u1EEB m\u1ED1c `searchCollapseAt`, gi\u1EEFa h\u00E0ng, co theo ch\u1ED7 -->\n <form [class]=\"searchInlineClasses()\" role=\"search\" (submit)=\"submitSearch($event)\">\n <ol-input\n class=\"block\"\n [ariaLabel]=\"searchAriaLabel()\"\n [placeholder]=\"searchPlaceholder()\"\n [rightIcon]=\"true\"\n [(value)]=\"searchValue\"\n >\n <ol-icon rightIcon name=\"solar:magnifer-linear\" size=\"base\" class=\"text-body-subtle\" />\n </ol-input>\n </form>\n }\n\n <div class=\"ms-auto flex shrink-0 items-center gap-2\">\n @if (showSearch()) {\n <!-- D\u01B0\u1EDBi m\u1ED1c: n\u00FAt k\u00EDnh l\u00FAp m\u1EDF \u00F4 t\u00ECm trong b\u1EA3ng th\u1EA3 xu\u1ED1ng -->\n <ol-dropdown [class]=\"searchToggleClasses()\" placement=\"bottom-end\" menuClass=\"w-80\">\n <ol-button trigger color=\"ghost\" [iconOnly]=\"true\" [ariaLabel]=\"searchAriaLabel()\">\n <ol-icon leftIcon name=\"solar:magnifer-linear\" size=\"base\" />\n </ol-button>\n <form class=\"p-1\" role=\"search\" (submit)=\"submitSearch($event)\">\n <ol-input\n [ariaLabel]=\"searchAriaLabel()\"\n [placeholder]=\"searchPlaceholder()\"\n [(value)]=\"searchValue\"\n />\n </form>\n </ol-dropdown>\n }\n @if (actionsDef(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions.template\" />\n }\n </div>\n }\n</header>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: OlBreadcrumb, selector: "ol-breadcrumb", inputs: ["variant", "separator", "contained"] }, { kind: "component", type: OlBreadcrumbItem, selector: "ol-breadcrumb-item", inputs: ["label", "routerLink", "href", "icon", "current", "active", "status", "showSeparator", "separator", "disabled", "clickable"], outputs: ["itemClick"] }, { kind: "component", type: OlButton, selector: "ol-button", inputs: ["color", "size", "pill", "outline", "iconOnly", "loading", "disabled", "type", "ariaLabel", "ariaExpanded", "ariaControls", "ariaPressed"], outputs: ["btnClick"] }, { kind: "component", type: OlDropdown, selector: "ol-dropdown", inputs: ["label", "isOpen", "appendTo", "placement", "offset", "trigger", "autoClose", "disabled", "triggerClass", "menuClass"], outputs: ["isOpenChange", "opened", "closed"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlInput, selector: "ol-input", inputs: ["value", "type", "label", "placeholder", "helperText", "disabled", "readonly", "size", "state", "required", "showAsterisk", "labelIcon", "labelTooltip", "ariaLabel", "prefixText", "suffixText", "addOnIcon", "leftIcon", "rightIcon", "clearable", "clearAriaLabel", "rightHelperText", "id", "floatingLabel", "labelPosition", "labelWidth", "floatingVariant"], outputs: ["valueChange", "disabledChange", "cleared"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
18070
|
+
}
|
|
18071
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppHeader, decorators: [{
|
|
18072
|
+
type: Component,
|
|
18073
|
+
args: [{ selector: 'ol-app-header', standalone: true, imports: [
|
|
18074
|
+
NgTemplateOutlet,
|
|
18075
|
+
OlBreadcrumb,
|
|
18076
|
+
OlBreadcrumbItem,
|
|
18077
|
+
OlButton,
|
|
18078
|
+
OlDropdown,
|
|
18079
|
+
OlIcon,
|
|
18080
|
+
OlInput,
|
|
18081
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'contents' }, template: "<!--\n Thanh tr\u00EAn c\u1EE7a trang trong app. Khung (`headerClasses`) lu\u00F4n c\u1EE7a component;\n ru\u1ED9t m\u1EB7c \u0111\u1ECBnh l\u00E0 [\u2190] [ti\u00EAu \u0111\u1EC1] [\u00F4 t\u00ECm] [h\u00E0nh \u0111\u1ED9ng], ho\u1EB7c tr\u1ECDn khe\n `olAppHeaderContent`.\n-->\n<header [class]=\"headerClasses()\">\n @if (contentDef(); as content) {\n <div class=\"flex min-w-0 flex-1 items-center gap-3\">\n <ng-container [ngTemplateOutlet]=\"content.template\" />\n </div>\n } @else {\n @if (hasBack()) {\n <ol-button\n class=\"shrink-0\"\n color=\"ghost\"\n [iconOnly]=\"true\"\n [ariaLabel]=\"backAriaLabel()\"\n (btnClick)=\"onBack()\"\n >\n <ol-icon leftIcon name=\"solar:arrow-left-linear\" size=\"base\" />\n </ol-button>\n }\n\n <div [class]=\"titleClasses()\">\n @if (titleDef(); as titleTpl) {\n <ng-container [ngTemplateOutlet]=\"titleTpl.template\" />\n } @else {\n @if (showBreadcrumbs()) {\n <!-- \u0110\u01B0\u1EDDng d\u1EABn: m\u1EE5c cu\u1ED1i l\u00E0 trang hi\u1EC7n t\u1EA1i; ch\u1EEF xs \u0111\u1EC3 ti\u00EAu \u0111\u1EC1 n\u1ED5i h\u01A1n; \u1EA9n d\u01B0\u1EDBi sm -->\n <ol-breadcrumb class=\"hidden sm:block [&_a]:text-xs [&_span]:text-xs\" separator=\"chevron\">\n @for (crumb of breadcrumbs(); track $index) {\n <ol-breadcrumb-item\n [label]=\"crumb.label\"\n [routerLink]=\"$last ? null : (crumb.link ?? null)\"\n [current]=\"$last\"\n />\n }\n </ol-breadcrumb>\n } @else if (subtitle()) {\n <p\n class=\"hidden truncate text-2xs font-medium uppercase tracking-wide text-body-subtle sm:block\"\n >\n {{ subtitle() }}\n </p>\n }\n @if (title()) {\n <h1 class=\"truncate text-base font-semibold leading-tight text-heading\">\n {{ title() }}\n </h1>\n }\n }\n </div>\n\n @if (showSearch()) {\n <!-- \u00D4 t\u00ECm \u0111\u1EA7y \u0111\u1EE7 \u2014 t\u1EEB m\u1ED1c `searchCollapseAt`, gi\u1EEFa h\u00E0ng, co theo ch\u1ED7 -->\n <form [class]=\"searchInlineClasses()\" role=\"search\" (submit)=\"submitSearch($event)\">\n <ol-input\n class=\"block\"\n [ariaLabel]=\"searchAriaLabel()\"\n [placeholder]=\"searchPlaceholder()\"\n [rightIcon]=\"true\"\n [(value)]=\"searchValue\"\n >\n <ol-icon rightIcon name=\"solar:magnifer-linear\" size=\"base\" class=\"text-body-subtle\" />\n </ol-input>\n </form>\n }\n\n <div class=\"ms-auto flex shrink-0 items-center gap-2\">\n @if (showSearch()) {\n <!-- D\u01B0\u1EDBi m\u1ED1c: n\u00FAt k\u00EDnh l\u00FAp m\u1EDF \u00F4 t\u00ECm trong b\u1EA3ng th\u1EA3 xu\u1ED1ng -->\n <ol-dropdown [class]=\"searchToggleClasses()\" placement=\"bottom-end\" menuClass=\"w-80\">\n <ol-button trigger color=\"ghost\" [iconOnly]=\"true\" [ariaLabel]=\"searchAriaLabel()\">\n <ol-icon leftIcon name=\"solar:magnifer-linear\" size=\"base\" />\n </ol-button>\n <form class=\"p-1\" role=\"search\" (submit)=\"submitSearch($event)\">\n <ol-input\n [ariaLabel]=\"searchAriaLabel()\"\n [placeholder]=\"searchPlaceholder()\"\n [(value)]=\"searchValue\"\n />\n </form>\n </ol-dropdown>\n }\n @if (actionsDef(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions.template\" />\n }\n </div>\n }\n</header>\n" }]
|
|
18082
|
+
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], mobileSolid: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileSolid", required: false }] }], sticky: [{ type: i0.Input, args: [{ isSignal: true, alias: "sticky", required: false }] }], headerClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerClass", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], breadcrumbs: [{ type: i0.Input, args: [{ isSignal: true, alias: "breadcrumbs", required: false }] }], backLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "backLink", required: false }] }], showBack: [{ type: i0.Input, args: [{ isSignal: true, alias: "showBack", required: false }] }], backLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "backLabel", required: false }] }], back: [{ type: i0.Output, args: ["back"] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], searchAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchAriaLabel", required: false }] }], searchCollapseAt: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchCollapseAt", required: false }] }], searchValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchValue", required: false }] }, { type: i0.Output, args: ["searchValueChange"] }], search: [{ type: i0.Output, args: ["search"] }], titleDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppHeaderTitleDef), { isSignal: true }] }], actionsDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppHeaderActionsDef), { isSignal: true }] }], contentDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlAppHeaderContentDef), { isSignal: true }] }] } });
|
|
18083
|
+
|
|
18084
|
+
/**
|
|
18085
|
+
* Kiểu dữ liệu cho `ol-app-header` — thanh trên của trang trong app (đi cặp
|
|
18086
|
+
* `ol-app-nav`). Khác `ol-navbar`: đó là navbar trang WEB (logo + liên kết).
|
|
18087
|
+
*/
|
|
18088
|
+
|
|
18089
|
+
/**
|
|
18090
|
+
* 20 hoạ tiết của Figma component "Pattern" (#1240:55097). Thứ tự theo Figma.
|
|
18091
|
+
* Dùng mảng này cho bộ chọn / trang trưng bày.
|
|
18092
|
+
*/
|
|
18093
|
+
const OL_PATTERN_TYPES = [
|
|
18094
|
+
'lines',
|
|
18095
|
+
'rounded-lines',
|
|
18096
|
+
'creative',
|
|
18097
|
+
'diamond-right',
|
|
18098
|
+
'diamond-left',
|
|
18099
|
+
'vertical-lines',
|
|
18100
|
+
'oblique-lines',
|
|
18101
|
+
'dots',
|
|
18102
|
+
'lines-modern',
|
|
18103
|
+
'squares',
|
|
18104
|
+
'modern',
|
|
18105
|
+
'comb',
|
|
18106
|
+
'tech',
|
|
18107
|
+
'tech-square',
|
|
18108
|
+
'tech-squares',
|
|
18109
|
+
'technology',
|
|
18110
|
+
'comb-linear',
|
|
18111
|
+
'big-comb',
|
|
18112
|
+
'lines-oblique',
|
|
18113
|
+
'technology-top',
|
|
18114
|
+
];
|
|
18115
|
+
/** Class của từng cách phủ — `cover` là mặc định của `.ol-pattern`. */
|
|
18116
|
+
const FIT_CLASS = {
|
|
18117
|
+
cover: '',
|
|
18118
|
+
contain: 'ol-pattern--fit-contain',
|
|
18119
|
+
repeat: 'ol-pattern--fit-repeat',
|
|
18120
|
+
};
|
|
18121
|
+
/**
|
|
18122
|
+
* Hoạ tiết nền — lớp trang trí phủ khối cha (banner, thẻ, nền trang).
|
|
18123
|
+
*
|
|
18124
|
+
* Màu = `currentColor`: đặt `class="text-white"` trên nền tối,
|
|
18125
|
+
* `class="text-fg-brand"` trên nền sáng. Độ đậm qua `opacity`.
|
|
18126
|
+
*
|
|
18127
|
+
* CẦN import `@edmicro/olkit/styles/patterns.css` (file riêng — 20 SVG chỉ tải
|
|
18128
|
+
* kiểu đang dùng). Thiếu file đó thì hoạ tiết không hiện.
|
|
18129
|
+
*
|
|
18130
|
+
* ```html
|
|
18131
|
+
* <div class="relative overflow-hidden rounded-lg bg-brand">
|
|
18132
|
+
* <ol-pattern type="tech" class="text-white" [opacity]="0.2" />
|
|
18133
|
+
* …nội dung…
|
|
18134
|
+
* </div>
|
|
18135
|
+
* ```
|
|
18136
|
+
*/
|
|
18137
|
+
class OlPattern {
|
|
18138
|
+
type = input('lines', /* @ts-ignore */
|
|
18139
|
+
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
18140
|
+
fit = input('cover', /* @ts-ignore */
|
|
18141
|
+
...(ngDevMode ? [{ debugName: "fit" }] : /* istanbul ignore next */ []));
|
|
18142
|
+
position = input('absolute', /* @ts-ignore */
|
|
18143
|
+
...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
18144
|
+
/** Độ đậm 0–1. Trên nền sáng thường 0,06–0,15; trên nền tối 0,15–0,4. */
|
|
18145
|
+
opacity = input(1, /* @ts-ignore */
|
|
18146
|
+
...(ngDevMode ? [{ debugName: "opacity" }] : /* istanbul ignore next */ []));
|
|
18147
|
+
hostClasses = computed(() => {
|
|
18148
|
+
const position = this.position() === 'fixed' ? 'fixed' : 'absolute';
|
|
18149
|
+
const fit = FIT_CLASS[this.fit()] ?? '';
|
|
18150
|
+
return `ol-pattern ol-pattern--${this.type()} ${fit} pointer-events-none inset-0 block ${position}`;
|
|
18151
|
+
}, /* @ts-ignore */
|
|
18152
|
+
...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
|
|
18153
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlPattern, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
18154
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.6", type: OlPattern, isStandalone: true, selector: "ol-pattern", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, opacity: { classPropertyName: "opacity", publicName: "opacity", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "aria-hidden": "true" }, properties: { "class": "hostClasses()", "style.opacity": "opacity()" } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
18155
|
+
}
|
|
18156
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlPattern, decorators: [{
|
|
18157
|
+
type: Component,
|
|
18158
|
+
args: [{
|
|
18159
|
+
selector: 'ol-pattern',
|
|
18160
|
+
standalone: true,
|
|
18161
|
+
template: '',
|
|
18162
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
18163
|
+
host: {
|
|
18164
|
+
'aria-hidden': 'true',
|
|
18165
|
+
'[class]': 'hostClasses()',
|
|
18166
|
+
'[style.opacity]': 'opacity()',
|
|
18167
|
+
},
|
|
18168
|
+
}]
|
|
18169
|
+
}], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], opacity: [{ type: i0.Input, args: [{ isSignal: true, alias: "opacity", required: false }] }] } });
|
|
18170
|
+
|
|
17792
18171
|
class OlSidebar {
|
|
17793
18172
|
collapsed = model(false, /* @ts-ignore */
|
|
17794
18173
|
...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
|
|
@@ -21247,5 +21626,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
21247
21626
|
* Generated bundle index. Do not edit.
|
|
21248
21627
|
*/
|
|
21249
21628
|
|
|
21250
|
-
export { APP_NAV_DEFAULT_USER_ACTIONS, CHAT_BUBBLE_STATUS, FIELD_ASTERISK, FIELD_CLEAR_RESERVE_END, FIELD_DISABLED, FIELD_HEIGHT, FIELD_HELPER, FIELD_HELPER_GAP, FIELD_ICON_GAP, FIELD_ICON_RESERVE_END, FIELD_ICON_RESERVE_START, FIELD_LABEL, FIELD_LABEL_BESIDE, FIELD_LABEL_GAP, FIELD_MIN_HEIGHT, FIELD_PADDING_END, FIELD_PADDING_START, FIELD_PADDING_X, FIELD_RADIUS, FIELD_STATE, FIELD_TEXT, FILE_KIND_META, FLOAT_LABEL_START, MONTHS_FULL_EN, MONTHS_FULL_VI, MONTHS_SHORT_VI, OL_APP_NAV_TEMPLATES, OL_ICONS, OL_LOGO_FULL_COLOR, OL_LOGO_FULL_WHITE, OL_LOGO_MARK_COLOR, OL_LOGO_MARK_WHITE, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppNav, OlAppNavFooterDef, OlAppNavHeaderDef, OlAppNavItemDef, OlAppNavLogoDef, OlAppNavUserDef, OlAppNavUserMenuDef, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, OlDropdownSearch, OlDropdownState, OlEmpty, OlFileDropzone, OlFileInput, OlFileItem, OlFileUpload, OlFocusTrapService, OlFooter, OlFrame, OlGallery, OlGalleryItem, OlHelper, OlIcon, OlIconService, OlIconShape, OlIconStack, OlImagePreview, OlInput, OlInputCounter, OlItem, OlJumbotron, OlKbd, OlLabel, OlListGroup, OlListGroupItem, OlLoadingOverlay, OlLogo, OlMenubar, OlModal, OlModalFooter, OlModalHeader, OlMonthView, OlNavDropdown, OlNavbar, OlNavbarItem, OlOtpInput, OlOverlayContainer, OlPagination, OlPopover, OlPopoverContent, OlPositioningService, OlProgress, OlProgressBarService, OlProgressBarTop, OlRadio, OlRange, OlRating, OlResizable, OlRichTextEditor, OlScrollArea, OlScrollspy, OlSegmented, OlSelect, OlSeparator, OlSidebar, OlSidebarGroup, OlSidebarItem, OlSidebarSubmenu, OlSkeleton, OlSpeedDial, OlSpeedDialItem, OlSpinner, OlStepItem, OlStepper, OlTabItem, OlTable, OlTableLoading, OlTableRow, OlTabs, OlTagInput, OlTextarea, OlTimeline, OlTimelineItem, OlTimepicker, OlToast, OlToastContainer, OlToastService, OlToggle, OlToggleGroup, OlToggleInput, OlTooltip, OlTooltipDirective, OlTree, OlTreeNodeItem, OlYearView, PICKER_ICON_RESERVE_START, WEEKDAYS_SHORT_EN, WEEKDAYS_SHORT_VI, boDau, cloneDate, detectFileKind, endOfDay, fieldLabelLayout, fieldSizeClasses, fileExtension, filesFromClipboard, formatDate, formatFileSize, generateMonthMatrix, generateMonthsMatrix, generateYearsMatrix, isBetween, isDateDisabled, isEditableTarget, isSameDay, isSameMonth, isSameYear, isToday, matchesAccept, multiValuePadY, parseDate, pasteShortcutLabel, provideOlIcons, startOfDay, stripDiacritics, toUploadFile, validateFiles };
|
|
21629
|
+
export { APP_NAV_DEFAULT_USER_ACTIONS, CHAT_BUBBLE_STATUS, FIELD_ASTERISK, FIELD_CLEAR_RESERVE_END, FIELD_DISABLED, FIELD_HEIGHT, FIELD_HELPER, FIELD_HELPER_GAP, FIELD_ICON_GAP, FIELD_ICON_RESERVE_END, FIELD_ICON_RESERVE_START, FIELD_LABEL, FIELD_LABEL_BESIDE, FIELD_LABEL_GAP, FIELD_MIN_HEIGHT, FIELD_PADDING_END, FIELD_PADDING_START, FIELD_PADDING_X, FIELD_RADIUS, FIELD_STATE, FIELD_TEXT, FILE_KIND_META, FLOAT_LABEL_START, MONTHS_FULL_EN, MONTHS_FULL_VI, MONTHS_SHORT_VI, OL_APP_HEADER_TEMPLATES, OL_APP_NAV_TEMPLATES, OL_ICONS, OL_LOGO_FULL_COLOR, OL_LOGO_FULL_WHITE, OL_LOGO_MARK_COLOR, OL_LOGO_MARK_WHITE, OL_PATTERN_TYPES, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppHeader, OlAppHeaderActionsDef, OlAppHeaderContentDef, OlAppHeaderTitleDef, OlAppNav, OlAppNavFooterDef, OlAppNavHeaderDef, OlAppNavItemDef, OlAppNavLogoDef, OlAppNavUserDef, OlAppNavUserMenuDef, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, OlDropdownSearch, OlDropdownState, OlEmpty, OlFileDropzone, OlFileInput, OlFileItem, OlFileUpload, OlFocusTrapService, OlFooter, OlFrame, OlGallery, OlGalleryItem, OlHelper, OlIcon, OlIconService, OlIconShape, OlIconStack, OlImagePreview, OlInput, OlInputCounter, OlItem, OlJumbotron, OlKbd, OlLabel, OlListGroup, OlListGroupItem, OlLoadingOverlay, OlLogo, OlMenubar, OlModal, OlModalFooter, OlModalHeader, OlMonthView, OlNavDropdown, OlNavbar, OlNavbarItem, OlOtpInput, OlOverlayContainer, OlPagination, OlPattern, OlPopover, OlPopoverContent, OlPositioningService, OlProgress, OlProgressBarService, OlProgressBarTop, OlRadio, OlRange, OlRating, OlResizable, OlRichTextEditor, OlScrollArea, OlScrollspy, OlSegmented, OlSelect, OlSeparator, OlSidebar, OlSidebarGroup, OlSidebarItem, OlSidebarSubmenu, OlSkeleton, OlSpeedDial, OlSpeedDialItem, OlSpinner, OlStepItem, OlStepper, OlTabItem, OlTable, OlTableLoading, OlTableRow, OlTabs, OlTagInput, OlTextarea, OlTimeline, OlTimelineItem, OlTimepicker, OlToast, OlToastContainer, OlToastService, OlToggle, OlToggleGroup, OlToggleInput, OlTooltip, OlTooltipDirective, OlTree, OlTreeNodeItem, OlYearView, PICKER_ICON_RESERVE_START, WEEKDAYS_SHORT_EN, WEEKDAYS_SHORT_VI, boDau, cloneDate, detectFileKind, endOfDay, fieldLabelLayout, fieldSizeClasses, fileExtension, filesFromClipboard, formatDate, formatFileSize, generateMonthMatrix, generateMonthsMatrix, generateYearsMatrix, isBetween, isDateDisabled, isEditableTarget, isSameDay, isSameMonth, isSameYear, isToday, matchesAccept, multiValuePadY, parseDate, pasteShortcutLabel, provideOlIcons, startOfDay, stripDiacritics, toUploadFile, validateFiles };
|
|
21251
21630
|
//# sourceMappingURL=edmicro-olkit.mjs.map
|