@edmicro/olkit 1.1.8 → 1.2.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/fesm2022/edmicro-olkit.mjs +790 -45
- package/fesm2022/edmicro-olkit.mjs.map +1 -1
- package/olkit.css +1 -1
- package/package.json +1 -1
- package/types/edmicro-olkit.d.ts +486 -10
- package/types/edmicro-olkit.d.ts.map +1 -1
- package/version.txt +2 -2
|
@@ -1,15 +1,16 @@
|
|
|
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 } 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';
|
|
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';
|
|
6
|
-
import { of, from } from 'rxjs';
|
|
6
|
+
import { of, from, filter, map as map$1 } from 'rxjs';
|
|
7
7
|
import { map, catchError } from 'rxjs/operators';
|
|
8
8
|
import { offset, flip, shift, arrow, size, computePosition, autoUpdate } from '@floating-ui/dom';
|
|
9
9
|
import * as i2 from '@angular/router';
|
|
10
|
-
import { RouterModule,
|
|
10
|
+
import { RouterModule, Router, NavigationEnd, RouterLink } from '@angular/router';
|
|
11
11
|
import * as i1$1 from '@angular/forms';
|
|
12
12
|
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
13
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Bí danh tên icon → tên chuẩn trong registry.
|
|
@@ -140,7 +141,7 @@ const ICON_ALIASES = {
|
|
|
140
141
|
* icon lõi thì không tải bộ nào (bộ Solar 10 MB). Khoá là tên ĐÃ CHUẨN HOÁ
|
|
141
142
|
* (chữ thường) mà component truyền vào, giá trị là SVG đã tra theo bí danh.
|
|
142
143
|
*
|
|
143
|
-
*
|
|
144
|
+
* 62 icon · olkit 24 · lucide 20 · mynaui 5 · solar 13
|
|
144
145
|
*/
|
|
145
146
|
const CORE_ICONS = {
|
|
146
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>',
|
|
@@ -195,9 +196,13 @@ const CORE_ICONS = {
|
|
|
195
196
|
'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>',
|
|
196
197
|
'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>',
|
|
197
198
|
'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
|
+
'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
|
+
'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-2-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="M9.00195 7C9.01406 4.82497 9.11051 3.64706 9.87889 2.87868C10.7576 2 12.1718 2 15.0002 2L16.0002 2C18.8286 2 20.2429 2 21.1215 2.87868C22.0002 3.75736 22.0002 5.17157 22.0002 8L22.0002 16C22.0002 18.8284 22.0002 20.2426 21.1215 21.1213C20.2429 22 18.8286 22 16.0002 22H15.0002C12.1718 22 10.7576 22 9.87889 21.1213C9.11051 20.3529 9.01406 19.175 9.00195 17"/><path stroke-linejoin="round" d="M15 12L2 12M5.5 15L2 12L5.5 9"/></g></svg>',
|
|
198
202
|
'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>',
|
|
199
203
|
'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>',
|
|
200
204
|
'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>',
|
|
205
|
+
'solar:user-circle-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="12" cy="9" r="3"/><circle cx="12" cy="12" r="10"/><path stroke-linecap="round" d="M17.9691 20C17.81 17.1085 16.9247 15 11.9999 15C7.07521 15 6.18991 17.1085 6.03076 20"/></g></svg>',
|
|
201
206
|
sparkles: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M10.5766 8.70419C11.2099 7.56806 11.5266 7 12 7C12.4734 7 12.7901 7.56806 13.4234 8.70419L13.5873 8.99812C13.7672 9.32097 13.8572 9.48239 13.9975 9.5889C14.1378 9.69541 14.3126 9.73495 14.6621 9.81402L14.9802 9.88601C16.2101 10.1643 16.825 10.3034 16.9713 10.7739C17.1176 11.2443 16.6984 11.7345 15.86 12.715L15.643 12.9686C15.4048 13.2472 15.2857 13.3865 15.2321 13.5589C15.1785 13.7312 15.1965 13.9171 15.2325 14.2888L15.2653 14.6272C15.3921 15.9353 15.4554 16.5894 15.0724 16.8801C14.6894 17.1709 14.1137 16.9058 12.9622 16.3756L12.6643 16.2384C12.337 16.0878 12.1734 16.0124 12 16.0124C11.8266 16.0124 11.663 16.0878 11.3357 16.2384L11.0378 16.3756C9.88634 16.9058 9.31059 17.1709 8.92757 16.8801C8.54456 16.5894 8.60794 15.9353 8.7347 14.6272L8.76749 14.2888C8.80351 13.9171 8.82152 13.7312 8.76793 13.5589C8.71434 13.3865 8.59521 13.2472 8.35696 12.9686L8.14005 12.715C7.30162 11.7345 6.88241 11.2443 7.02871 10.7739C7.17501 10.3034 7.78993 10.1643 9.01977 9.88601L9.33794 9.81402C9.68743 9.73495 9.86217 9.69541 10.0025 9.5889C10.1428 9.48239 10.2328 9.32097 10.4127 8.99812L10.5766 8.70419Z" stroke="currentColor" stroke-width="1.5"/> <path d="M12 2V4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M12 20V22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M2 12L4 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M20 12L22 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M6 18L6.34305 17.657" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M17.6567 6.34326L18 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M18 18L17.657 17.657" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> <path d="M6.34326 6.34326L6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> </svg>',
|
|
202
207
|
star: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M9.15316 5.40838C10.4198 3.13613 11.0531 2 12 2C12.9469 2 13.5802 3.13612 14.8468 5.40837L15.1745 5.99623C15.5345 6.64193 15.7144 6.96479 15.9951 7.17781C16.2757 7.39083 16.6251 7.4699 17.3241 7.62805L17.9605 7.77203C20.4201 8.32856 21.65 8.60682 21.9426 9.54773C22.2352 10.4886 21.3968 11.4691 19.7199 13.4299L19.2861 13.9372C18.8096 14.4944 18.5713 14.773 18.4641 15.1177C18.357 15.4624 18.393 15.8341 18.465 16.5776L18.5306 17.2544C18.7841 19.8706 18.9109 21.1787 18.1449 21.7602C17.3788 22.3417 16.2273 21.8115 13.9243 20.7512L13.3285 20.4768C12.6741 20.1755 12.3469 20.0248 12 20.0248C11.6531 20.0248 11.3259 20.1755 10.6715 20.4768L10.0757 20.7512C7.77268 21.8115 6.62118 22.3417 5.85515 21.7602C5.08912 21.1787 5.21588 19.8706 5.4694 17.2544L5.53498 16.5776C5.60703 15.8341 5.64305 15.4624 5.53586 15.1177C5.42868 14.773 5.19043 14.4944 4.71392 13.9372L4.2801 13.4299C2.60325 11.4691 1.76482 10.4886 2.05742 9.54773C2.35002 8.60682 3.57986 8.32856 6.03954 7.77203L6.67589 7.62805C7.37485 7.4699 7.72433 7.39083 8.00494 7.17781C8.28555 6.96479 8.46553 6.64194 8.82547 5.99623L9.15316 5.40838Z" stroke="currentColor" stroke-width="1.5"/> </svg>',
|
|
203
208
|
user: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle cx="12" cy="6" r="4" stroke="currentColor" stroke-width="1.5"/> <path d="M20 17.5C20 19.9853 20 22 12 22C4 22 4 19.9853 4 17.5C4 15.0147 7.58172 13 12 13C16.4183 13 20 15.0147 20 17.5Z" stroke="currentColor" stroke-width="1.5"/> </svg>',
|
|
@@ -8231,6 +8236,13 @@ class OlDropdown {
|
|
|
8231
8236
|
...(ngDevMode ? [{ debugName: "appendTo" }] : /* istanbul ignore next */ []));
|
|
8232
8237
|
placement = input('bottom-start', /* @ts-ignore */
|
|
8233
8238
|
...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
8239
|
+
/**
|
|
8240
|
+
* Khoảng cách (px) từ nút tới panel. Mặc định 8. Tăng khi nút nằm thụt
|
|
8241
|
+
* trong một khung mà panel phải mọc RA NGOÀI khung — vd. thẻ tài khoản trong
|
|
8242
|
+
* `ol-app-nav` (đệm 20px) mở sang phải phải vượt qua mép sidebar.
|
|
8243
|
+
*/
|
|
8244
|
+
offset = input(8, /* @ts-ignore */
|
|
8245
|
+
...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
|
|
8234
8246
|
trigger = input('click', /* @ts-ignore */
|
|
8235
8247
|
...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
8236
8248
|
autoClose = input(true, /* @ts-ignore */
|
|
@@ -8286,7 +8298,7 @@ class OlDropdown {
|
|
|
8286
8298
|
}
|
|
8287
8299
|
const options = {
|
|
8288
8300
|
placement: this.placement(),
|
|
8289
|
-
offset:
|
|
8301
|
+
offset: this.offset(),
|
|
8290
8302
|
flip: true,
|
|
8291
8303
|
shift: true,
|
|
8292
8304
|
};
|
|
@@ -8562,14 +8574,14 @@ class OlDropdown {
|
|
|
8562
8574
|
}
|
|
8563
8575
|
}
|
|
8564
8576
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDropdown, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8565
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDropdown, isStandalone: true, selector: "ol-dropdown", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: false, transformFunction: null }, autoClose: { classPropertyName: "autoClose", publicName: "autoClose", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, triggerClass: { classPropertyName: "triggerClass", publicName: "triggerClass", isSignal: true, isRequired: false, transformFunction: null }, menuClass: { classPropertyName: "menuClass", publicName: "menuClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", opened: "opened", closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, classAttribute: "relative inline-block text-left" }, viewQueries: [{ propertyName: "triggerButtonRef", first: true, predicate: ["triggerButton"], descendants: true, isSignal: true }, { propertyName: "menuRef", first: true, predicate: ["menu"], descendants: true, isSignal: true }, { propertyName: "cursorAnchorRef", first: true, predicate: ["cursorAnchor"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n (mouseenter)=\"onMouseEnter()\"\n (mouseleave)=\"onMouseLeave()\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n <!-- Custom Slot Trigger or Default Button -->\n <div (click)=\"onTriggerClick()\" (keydown)=\"onTriggerKeydown($event)\">\n <ng-content select=\"[slot=trigger], [trigger]\" />\n @if (label()) {\n <button\n #triggerButton\n type=\"button\"\n [id]=\"buttonId\"\n [disabled]=\"disabled()\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"menuId\"\n [class]=\"\n triggerClass()\n ? 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium cursor-pointer ' +\n triggerClass()\n : 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-body bg-neutral-primary border border-default rounded-md hover:bg-neutral-tertiary focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed'\n \"\n >\n <ng-content select=\"[triggerIcon]\" />\n <span>{{ label() }}</span>\n <ol-icon\n name=\"chevron-down\"\n size=\"xs\"\n class=\"transition-transform duration-200\"\n [class.rotate-180]=\"isOpen()\"\n ></ol-icon>\n </button>\n } @else if (!label() && triggerClass()) {\n <button\n #triggerButton\n type=\"button\"\n [id]=\"buttonId\"\n [disabled]=\"disabled()\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"menuId\"\n [class]=\"'inline-flex items-center justify-center p-2.5 cursor-pointer ' + triggerClass()\"\n >\n <ng-content select=\"[triggerIcon]\" />\n </button>\n }\n </div>\n\n @if (trigger() === 'context') {\n <span\n #cursorAnchor\n aria-hidden=\"true\"\n class=\"pointer-events-none fixed h-0 w-0\"\n [style.left.px]=\"cursorX()\"\n [style.top.px]=\"cursorY()\"\n ></span>\n }\n\n <div\n #menu\n [id]=\"menuId\"\n role=\"menu\"\n [attr.aria-labelledby]=\"buttonId\"\n [class]=\"\n 'z-dropdown absolute top-0 left-0 min-w-48 bg-neutral-primary rounded-md shadow-lg border border-default p-2 space-y-1.5 focus:outline-hidden transition-opacity duration-150 ' +\n (menuClass() ? ' ' + menuClass() : '')\n \"\n [class.hidden]=\"!isOpen()\"\n >\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8577
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlDropdown, isStandalone: true, selector: "ol-dropdown", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isOpen: { classPropertyName: "isOpen", publicName: "isOpen", isSignal: true, isRequired: false, transformFunction: null }, appendTo: { classPropertyName: "appendTo", publicName: "appendTo", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "offset", isSignal: true, isRequired: false, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: false, transformFunction: null }, autoClose: { classPropertyName: "autoClose", publicName: "autoClose", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, triggerClass: { classPropertyName: "triggerClass", publicName: "triggerClass", isSignal: true, isRequired: false, transformFunction: null }, menuClass: { classPropertyName: "menuClass", publicName: "menuClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isOpen: "isOpenChange", opened: "opened", closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, classAttribute: "relative inline-block text-left" }, viewQueries: [{ propertyName: "triggerButtonRef", first: true, predicate: ["triggerButton"], descendants: true, isSignal: true }, { propertyName: "menuRef", first: true, predicate: ["menu"], descendants: true, isSignal: true }, { propertyName: "cursorAnchorRef", first: true, predicate: ["cursorAnchor"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n (mouseenter)=\"onMouseEnter()\"\n (mouseleave)=\"onMouseLeave()\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n <!-- Custom Slot Trigger or Default Button -->\n <div (click)=\"onTriggerClick()\" (keydown)=\"onTriggerKeydown($event)\">\n <ng-content select=\"[slot=trigger], [trigger]\" />\n @if (label()) {\n <button\n #triggerButton\n type=\"button\"\n [id]=\"buttonId\"\n [disabled]=\"disabled()\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"menuId\"\n [class]=\"\n triggerClass()\n ? 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium cursor-pointer ' +\n triggerClass()\n : 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-body bg-neutral-primary border border-default rounded-md hover:bg-neutral-tertiary focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed'\n \"\n >\n <ng-content select=\"[triggerIcon]\" />\n <span>{{ label() }}</span>\n <ol-icon\n name=\"chevron-down\"\n size=\"xs\"\n class=\"transition-transform duration-200\"\n [class.rotate-180]=\"isOpen()\"\n ></ol-icon>\n </button>\n } @else if (!label() && triggerClass()) {\n <button\n #triggerButton\n type=\"button\"\n [id]=\"buttonId\"\n [disabled]=\"disabled()\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"menuId\"\n [class]=\"'inline-flex items-center justify-center p-2.5 cursor-pointer ' + triggerClass()\"\n >\n <ng-content select=\"[triggerIcon]\" />\n </button>\n }\n </div>\n\n @if (trigger() === 'context') {\n <span\n #cursorAnchor\n aria-hidden=\"true\"\n class=\"pointer-events-none fixed h-0 w-0\"\n [style.left.px]=\"cursorX()\"\n [style.top.px]=\"cursorY()\"\n ></span>\n }\n\n <div\n #menu\n [id]=\"menuId\"\n role=\"menu\"\n [attr.aria-labelledby]=\"buttonId\"\n [class]=\"\n 'z-dropdown absolute top-0 left-0 min-w-48 bg-neutral-primary rounded-md shadow-lg border border-default p-2 space-y-1.5 focus:outline-hidden transition-opacity duration-150 ' +\n (menuClass() ? ' ' + menuClass() : '')\n \"\n [class.hidden]=\"!isOpen()\"\n >\n <ng-content />\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8566
8578
|
}
|
|
8567
8579
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlDropdown, decorators: [{
|
|
8568
8580
|
type: Component,
|
|
8569
8581
|
args: [{ selector: 'ol-dropdown', standalone: true, imports: [CommonModule, OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
8570
8582
|
class: 'relative inline-block text-left',
|
|
8571
8583
|
}, template: "<div\n (mouseenter)=\"onMouseEnter()\"\n (mouseleave)=\"onMouseLeave()\"\n (contextmenu)=\"onContextMenu($event)\"\n>\n <!-- Custom Slot Trigger or Default Button -->\n <div (click)=\"onTriggerClick()\" (keydown)=\"onTriggerKeydown($event)\">\n <ng-content select=\"[slot=trigger], [trigger]\" />\n @if (label()) {\n <button\n #triggerButton\n type=\"button\"\n [id]=\"buttonId\"\n [disabled]=\"disabled()\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"menuId\"\n [class]=\"\n triggerClass()\n ? 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium cursor-pointer ' +\n triggerClass()\n : 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-body bg-neutral-primary border border-default rounded-md hover:bg-neutral-tertiary focus:outline-hidden focus:ring-2 focus:ring-brand-subtle cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed'\n \"\n >\n <ng-content select=\"[triggerIcon]\" />\n <span>{{ label() }}</span>\n <ol-icon\n name=\"chevron-down\"\n size=\"xs\"\n class=\"transition-transform duration-200\"\n [class.rotate-180]=\"isOpen()\"\n ></ol-icon>\n </button>\n } @else if (!label() && triggerClass()) {\n <button\n #triggerButton\n type=\"button\"\n [id]=\"buttonId\"\n [disabled]=\"disabled()\"\n aria-haspopup=\"true\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-controls]=\"menuId\"\n [class]=\"'inline-flex items-center justify-center p-2.5 cursor-pointer ' + triggerClass()\"\n >\n <ng-content select=\"[triggerIcon]\" />\n </button>\n }\n </div>\n\n @if (trigger() === 'context') {\n <span\n #cursorAnchor\n aria-hidden=\"true\"\n class=\"pointer-events-none fixed h-0 w-0\"\n [style.left.px]=\"cursorX()\"\n [style.top.px]=\"cursorY()\"\n ></span>\n }\n\n <div\n #menu\n [id]=\"menuId\"\n role=\"menu\"\n [attr.aria-labelledby]=\"buttonId\"\n [class]=\"\n 'z-dropdown absolute top-0 left-0 min-w-48 bg-neutral-primary rounded-md shadow-lg border border-default p-2 space-y-1.5 focus:outline-hidden transition-opacity duration-150 ' +\n (menuClass() ? ' ' + menuClass() : '')\n \"\n [class.hidden]=\"!isOpen()\"\n >\n <ng-content />\n </div>\n</div>\n" }]
|
|
8572
|
-
}], ctorParameters: () => [], propDecorators: { triggerButtonRef: [{ type: i0.ViewChild, args: ['triggerButton', { isSignal: true }] }], menuRef: [{ type: i0.ViewChild, args: ['menu', { isSignal: true }] }], cursorAnchorRef: [{ type: i0.ViewChild, args: ['cursorAnchor', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }, { type: i0.Output, args: ["isOpenChange"] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: false }] }], autoClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoClose", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], triggerClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerClass", required: false }] }], menuClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuClass", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], onDocumentClick: [{
|
|
8584
|
+
}], ctorParameters: () => [], propDecorators: { triggerButtonRef: [{ type: i0.ViewChild, args: ['triggerButton', { isSignal: true }] }], menuRef: [{ type: i0.ViewChild, args: ['menu', { isSignal: true }] }], cursorAnchorRef: [{ type: i0.ViewChild, args: ['cursorAnchor', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], isOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "isOpen", required: false }] }, { type: i0.Output, args: ["isOpenChange"] }], appendTo: [{ type: i0.Input, args: [{ isSignal: true, alias: "appendTo", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], offset: [{ type: i0.Input, args: [{ isSignal: true, alias: "offset", required: false }] }], trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: false }] }], autoClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoClose", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], triggerClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerClass", required: false }] }], menuClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuClass", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], onDocumentClick: [{
|
|
8573
8585
|
type: HostListener,
|
|
8574
8586
|
args: ['document:click', ['$event']]
|
|
8575
8587
|
}] } });
|
|
@@ -11712,6 +11724,15 @@ class OlSegmented {
|
|
|
11712
11724
|
/** Tên nhóm cho bộ đọc màn hình — nên đặt, vì nhóm không có nhãn nhìn thấy. */
|
|
11713
11725
|
ariaLabel = input('', /* @ts-ignore */
|
|
11714
11726
|
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
11727
|
+
/** Tông khung theo nền — xem `SegmentedVariant`. */
|
|
11728
|
+
variant = input('default', /* @ts-ignore */
|
|
11729
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
11730
|
+
/**
|
|
11731
|
+
* Class màu nền tuỳ ý cho khung (rãnh), thay nền của `variant` — ví dụ
|
|
11732
|
+
* `bg-brand` hay `bg-[var(--bg-app-nav-hover)]`. Mục đang chọn giữ nguyên.
|
|
11733
|
+
*/
|
|
11734
|
+
trackClass = input('', /* @ts-ignore */
|
|
11735
|
+
...(ngDevMode ? [{ debugName: "trackClass" }] : /* istanbul ignore next */ []));
|
|
11715
11736
|
host = inject(ElementRef);
|
|
11716
11737
|
onChange = () => { };
|
|
11717
11738
|
onTouched = () => { };
|
|
@@ -11720,7 +11741,8 @@ class OlSegmented {
|
|
|
11720
11741
|
trackClasses = computed(() => {
|
|
11721
11742
|
const width = this.fullWidth() ? 'flex w-full' : 'inline-flex max-w-full';
|
|
11722
11743
|
const state = this.disabled() ? 'opacity-50' : '';
|
|
11723
|
-
|
|
11744
|
+
const bg = this.trackClass() || (this.variant() === 'inverse' ? 'bg-white/15' : 'bg-neutral-tertiary');
|
|
11745
|
+
return `${width} items-center gap-1 p-1 rounded-md ${bg} overflow-x-auto ${state}`;
|
|
11724
11746
|
}, /* @ts-ignore */
|
|
11725
11747
|
...(ngDevMode ? [{ debugName: "trackClasses" }] : /* istanbul ignore next */ []));
|
|
11726
11748
|
/** Mục đang Tab vào được: mục đang chọn, không có thì mục bật đầu tiên. */
|
|
@@ -11734,11 +11756,17 @@ class OlSegmented {
|
|
|
11734
11756
|
const size = this.size() === 'sm' ? 'h-7 px-3 text-xs' : 'h-8 px-4 text-sm';
|
|
11735
11757
|
const grow = this.fullWidth() ? 'flex-1 min-w-0' : 'shrink-0';
|
|
11736
11758
|
const base = `${size} ${grow} inline-flex items-center justify-center gap-1.5 rounded whitespace-nowrap transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-subtle`;
|
|
11759
|
+
const inverse = this.variant() === 'inverse';
|
|
11737
11760
|
if (option.value === this.value()) {
|
|
11738
|
-
|
|
11761
|
+
const selected = inverse && !option.color
|
|
11762
|
+
? 'bg-white text-[var(--color-brand-900,#152147)] shadow-xs'
|
|
11763
|
+
: SELECTED_COLOR[option.color ?? 'neutral'];
|
|
11764
|
+
return `${base} font-semibold ${selected}`;
|
|
11739
11765
|
}
|
|
11740
11766
|
const blocked = option.disabled || this.disabled();
|
|
11741
|
-
|
|
11767
|
+
const idle = inverse ? 'text-white/85' : 'text-body';
|
|
11768
|
+
const hover = inverse ? 'hover:text-white' : 'hover:text-heading';
|
|
11769
|
+
return `${base} ${idle} font-medium ${blocked ? 'cursor-not-allowed opacity-50' : `cursor-pointer ${hover}`}`;
|
|
11742
11770
|
}
|
|
11743
11771
|
select(option) {
|
|
11744
11772
|
if (option.disabled || this.disabled() || option.value === this.value())
|
|
@@ -11787,7 +11815,7 @@ class OlSegmented {
|
|
|
11787
11815
|
this.disabled.set(isDisabled);
|
|
11788
11816
|
}
|
|
11789
11817
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11790
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSegmented, isStandalone: true, selector: "ol-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange" }, host: { properties: { "class": "hostClasses()" } }, providers: [
|
|
11818
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlSegmented, isStandalone: true, selector: "ol-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, trackClass: { classPropertyName: "trackClass", publicName: "trackClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange" }, host: { properties: { "class": "hostClasses()" } }, providers: [
|
|
11791
11819
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => OlSegmented), multi: true },
|
|
11792
11820
|
], ngImport: i0, template: "<div\n role=\"radiogroup\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-disabled]=\"disabled() || null\"\n [class]=\"trackClasses()\"\n>\n @for (option of options(); track option.value; let i = $index) {\n <button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"option.value === value()\"\n [disabled]=\"option.disabled || disabled()\"\n [tabindex]=\"i === focusIndex() ? 0 : -1\"\n [class]=\"itemClasses(option)\"\n (click)=\"select(option)\"\n (keydown)=\"onKeydown($event, i)\"\n >\n @if (option.icon) {\n <ol-icon [name]=\"option.icon\" size=\"sm\" class=\"shrink-0\" />\n }\n <span class=\"truncate\">{{ option.label }}</span>\n </button>\n }\n</div>\n", dependencies: [{ kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11793
11821
|
}
|
|
@@ -11796,7 +11824,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
11796
11824
|
args: [{ selector: 'ol-segmented', standalone: true, imports: [OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': 'hostClasses()' }, providers: [
|
|
11797
11825
|
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => OlSegmented), multi: true },
|
|
11798
11826
|
], template: "<div\n role=\"radiogroup\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-disabled]=\"disabled() || null\"\n [class]=\"trackClasses()\"\n>\n @for (option of options(); track option.value; let i = $index) {\n <button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"option.value === value()\"\n [disabled]=\"option.disabled || disabled()\"\n [tabindex]=\"i === focusIndex() ? 0 : -1\"\n [class]=\"itemClasses(option)\"\n (click)=\"select(option)\"\n (keydown)=\"onKeydown($event, i)\"\n >\n @if (option.icon) {\n <ol-icon [name]=\"option.icon\" size=\"sm\" class=\"shrink-0\" />\n }\n <span class=\"truncate\">{{ option.label }}</span>\n </button>\n }\n</div>\n" }]
|
|
11799
|
-
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
11827
|
+
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], trackClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackClass", required: false }] }] } });
|
|
11800
11828
|
|
|
11801
11829
|
const FILE_KIND_META = {
|
|
11802
11830
|
image: { label: 'Hình ảnh', icon: 'lucide:file-image', color: 'green' },
|
|
@@ -12712,7 +12740,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
12712
12740
|
args: [{ selector: 'ol-image-preview', standalone: true, imports: [OlIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (open() && image(); as img) {\n <div\n #panel\n role=\"dialog\"\n aria-modal=\"true\"\n tabindex=\"-1\"\n [attr.aria-label]=\"'Xem \u1EA3nh ' + (current() + 1) + ' / ' + count()\"\n class=\"fixed inset-0 z-modal flex flex-col bg-overlay-backdrop backdrop-blur-xs outline-hidden\"\n (keydown)=\"onKeydown($event)\"\n >\n <!-- Thanh tr\u00EAn: s\u1ED1 th\u1EE9 t\u1EF1 \u00B7 ph\u00F3ng/thu \u00B7 \u0111\u00F3ng. -->\n <div class=\"flex items-center justify-between gap-3 px-4 py-3 text-fg-white\">\n <span class=\"text-sm font-medium tabular-nums\" aria-live=\"polite\">\n @if (count() > 1) {\n {{ current() + 1 }} / {{ count() }}\n }\n </span>\n <div\n class=\"ms-auto flex items-center gap-1 rounded-full bg-neutral-primary p-1 text-heading shadow-lg\"\n >\n <button\n type=\"button\"\n aria-label=\"Thu nh\u1ECF\"\n class=\"inline-flex size-8 items-center justify-center rounded-full transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle disabled:cursor-not-allowed disabled:opacity-40\"\n [disabled]=\"!canZoomOut()\"\n (click)=\"zoomOut()\"\n >\n <ol-icon name=\"lucide:zoom-out\" size=\"sm\" />\n </button>\n <button\n type=\"button\"\n aria-label=\"V\u1EC1 c\u1EE1 g\u1ED1c\"\n class=\"min-w-12 rounded-full px-1 text-center text-xs font-semibold tabular-nums transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n (click)=\"resetZoom()\"\n >\n {{ zoomPercent() }}%\n </button>\n <button\n type=\"button\"\n aria-label=\"Ph\u00F3ng to\"\n class=\"inline-flex size-8 items-center justify-center rounded-full transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle disabled:cursor-not-allowed disabled:opacity-40\"\n [disabled]=\"!canZoomIn()\"\n (click)=\"zoomIn()\"\n >\n <ol-icon name=\"lucide:zoom-in\" size=\"sm\" />\n </button>\n </div>\n <button\n type=\"button\"\n data-ol-dismiss\n aria-label=\"\u0110\u00F3ng xem \u1EA3nh\"\n class=\"inline-flex size-10 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-lg transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n (click)=\"close()\"\n >\n <ol-icon name=\"close\" size=\"base\" />\n </button>\n </div>\n\n <!--\n \u1EA2nh: v\u1EEBa khung, kh\u00F4ng c\u1EAFt (object-contain). B\u1EA5m n\u1EC1n t\u1ED1i quanh \u1EA3nh \u0111\u1EC3 \u0111\u00F3ng.\n `overflow-hidden`: \u1EA3nh ph\u00F3ng to kh\u00F4ng \u0111\u01B0\u1EE3c tr\u00E0n \u0111\u00E8 l\u00EAn thanh tr\u00EAn v\u00E0 d\u1EA3i\n \u1EA3nh nh\u1ECF \u2014 \u0111o 2026-09-28, \u1EA3nh 364% che n\u00FAt \"Ph\u00F3ng to\", kh\u00F4ng b\u1EA5m \u0111\u01B0\u1EE3c.\n `tabindex=\"-1\"` \u1EDF khung ngo\u00E0i: b\u1EA5m v\u00E0o \u1EA3nh (kh\u00F4ng focus \u0111\u01B0\u1EE3c) th\u00EC ti\u00EAu\n \u0111i\u1EC3m \u1EDF l\u1EA1i tr\u00ECnh xem, ph\u00EDm + \u2212 0 \u2190 \u2192 v\u1EABn ch\u1EA1y.\n -->\n <div\n class=\"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden px-4 pb-4\"\n (click)=\"onBackdropClick($event)\"\n >\n <img\n #mainImage\n [src]=\"img.src\"\n [alt]=\"img.alt || ''\"\n [class]=\"imageClasses()\"\n [style]=\"imageStyle()\"\n draggable=\"false\"\n tabindex=\"0\"\n (click)=\"onImageClick($event)\"\n (keydown.enter)=\"toggleZoom(); $event.preventDefault()\"\n (keydown.space)=\"toggleZoom(); $event.preventDefault()\"\n (wheel)=\"onWheel($event)\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n (pointercancel)=\"onPointerUp($event)\"\n />\n\n @if (count() > 1) {\n <button\n type=\"button\"\n aria-label=\"\u1EA2nh tr\u01B0\u1EDBc\"\n class=\"absolute start-3 top-1/2 inline-flex size-11 -translate-y-1/2 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-lg transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n (click)=\"prev()\"\n >\n <ol-icon name=\"chevron-left\" size=\"base\" />\n </button>\n <button\n type=\"button\"\n aria-label=\"\u1EA2nh sau\"\n class=\"absolute end-3 top-1/2 inline-flex size-11 -translate-y-1/2 items-center justify-center rounded-full bg-neutral-primary text-heading shadow-lg transition-colors hover:bg-neutral-secondary focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle\"\n (click)=\"next()\"\n >\n <ol-icon name=\"chevron-right\" size=\"base\" />\n </button>\n }\n </div>\n\n @if (img.alt) {\n <p class=\"truncate px-4 pb-3 text-center text-sm text-fg-white\">{{ img.alt }}</p>\n }\n\n <!--\n D\u1EA3i \u1EA3nh nh\u1ECF: th\u1EA5y c\u00E1c \u1EA3nh c\u00F2n l\u1EA1i, b\u1EA5m \u0111\u1EC3 nh\u1EA3y t\u1EDBi; \u1EA3nh \u0111ang xem c\u00F3 vi\u1EC1n\n v\u00E0 t\u1EF1 cu\u1ED9n v\u00E0o t\u1EA7m nh\u00ECn. Cu\u1ED9n ngang khi d\u00E0i (\u0111i\u1EC7n tho\u1EA1i), c\u0103n gi\u1EEFa khi\n ng\u1EAFn \u2014 b\u1EB1ng ms-auto/me-auto \u1EDF \u00F4 \u0111\u1EA7u/cu\u1ED1i, KH\u00D4NG justify-center: n\u00F3 c\u1EAFt\n m\u1EA5t c\u00E1c \u00F4 \u0111\u1EA7u khi d\u1EA3i tr\u00E0n v\u00E0 kh\u00F4ng cu\u1ED9n t\u1EDBi \u0111\u01B0\u1EE3c. L\u1EC1 d\u01B0\u1EDBi max() v\u1EDBi\n safe-area: ch\u1EEBa v\u1EA1ch Home c\u1EE7a iPhone.\n -->\n @if (count() > 1) {\n <div\n #thumbStrip\n class=\"flex shrink-0 gap-2 overflow-x-auto px-4 pt-1 pb-[max(1rem,env(safe-area-inset-bottom))] [scrollbar-width:none] [&>:first-child]:ms-auto [&>:last-child]:me-auto\"\n role=\"group\"\n aria-label=\"Danh s\u00E1ch \u1EA3nh\"\n >\n @for (thumb of images(); track $index; let i = $index) {\n <button\n type=\"button\"\n [attr.aria-label]=\"'Xem \u1EA3nh ' + (i + 1) + (thumb.alt ? ': ' + thumb.alt : '')\"\n [attr.aria-current]=\"i === current() ? 'true' : null\"\n [class]=\"thumbClasses(i)\"\n (click)=\"index.set(i)\"\n >\n <img [src]=\"thumb.src\" alt=\"\" class=\"size-full object-cover\" loading=\"lazy\" />\n </button>\n }\n </div>\n }\n </div>\n}\n" }]
|
|
12713
12741
|
}], ctorParameters: () => [], propDecorators: { images: [{ type: i0.Input, args: [{ isSignal: true, alias: "images", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: false }] }, { type: i0.Output, args: ["indexChange"] }], closed: [{ type: i0.Output, args: ["closed"] }], panel: [{ type: i0.ViewChild, args: ['panel', { isSignal: true }] }], thumbStrip: [{ type: i0.ViewChild, args: ['thumbStrip', { isSignal: true }] }], imageEl: [{ type: i0.ViewChild, args: ['mainImage', { isSignal: true }] }] } });
|
|
12714
12742
|
|
|
12715
|
-
let nextId = 0;
|
|
12743
|
+
let nextId$2 = 0;
|
|
12716
12744
|
/**
|
|
12717
12745
|
* Khung tải tệp có tiêu đề — Figma `ol-file-input-form` "Button header + …"
|
|
12718
12746
|
* (3587:6834 · 3592:6990 · 3603:2606 · 3611:3494).
|
|
@@ -12771,7 +12799,7 @@ class OlFileUpload {
|
|
|
12771
12799
|
fileInput = viewChild.required('fileInput', /* @ts-ignore */
|
|
12772
12800
|
...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
|
|
12773
12801
|
host = inject(ElementRef);
|
|
12774
|
-
uid = `ol-file-upload-${nextId++}`;
|
|
12802
|
+
uid = `ol-file-upload-${nextId$2++}`;
|
|
12775
12803
|
errors = signal([], /* @ts-ignore */
|
|
12776
12804
|
...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
|
|
12777
12805
|
isMultiple = computed(() => this.mode() === 'multiple' || this.mode() === 'list' || this.mode() === 'images', /* @ts-ignore */
|
|
@@ -14979,6 +15007,84 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
14979
15007
|
args: [{ selector: 'ol-list-group-item', standalone: true, imports: [NgTemplateOutlet], host: { class: 'block w-full' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<li\n (click)=\"handleClick($event)\"\n class=\"w-full transition-colors\"\n [class]=\"textClasses()\"\n [class.border-b]=\"hasDivider()\"\n [class.border-default-strong]=\"hasDivider()\"\n>\n <!--\n `href` bi\u1EBFn m\u1EE5c th\u00E0nh li\u00EAn k\u1EBFt. Th\u1EBB `<a>` n\u1EB1m B\u00CAN TRONG `<li>`, kh\u00F4ng thay\n th\u1EBF n\u00F3 \u2014 `<a>` tr\u1EF1c ti\u1EBFp trong `<ul>` l\u00E0 HTML h\u1ECFng. \u0110\u1EC7m chuy\u1EC3n xu\u1ED1ng th\u1EBB\n trong c\u00F9ng \u0111\u1EC3 v\u00F9ng b\u1EA5m ph\u1EE7 tr\u1ECDn m\u1EE5c.\n -->\n @if (href() && !disabled()) {\n <a [href]=\"href()\" class=\"block w-full px-4 py-2\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </a>\n } @else {\n <span class=\"block w-full px-4 py-2\" [attr.aria-disabled]=\"disabled() ? 'true' : null\">\n <ng-container [ngTemplateOutlet]=\"ruot\" />\n </span>\n }\n</li>\n\n<ng-template #ruot><ng-content /></ng-template>\n" }]
|
|
14980
15008
|
}], propDecorators: { active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
|
|
14981
15009
|
|
|
15010
|
+
const OL_LOGO_FULL_COLOR = {
|
|
15011
|
+
viewBox: '0 0 1000 195',
|
|
15012
|
+
body: '<path d="M180.147 56.4135V59.7061C184.94 53.3863 190.182 48.7454 195.872 45.7877C201.562 42.8301 208.106 41.3513 215.502 41.3469C222.69 41.3469 229.112 42.9084 234.776 46.0357C240.296 49.015 244.732 53.6603 247.458 59.3059C249.26 62.6594 250.444 66.3086 250.94 70.0839C251.537 74.9901 251.802 79.9311 251.742 84.8721V135.535C251.742 140.989 250.492 145.112 247.997 147.905C245.503 150.697 242.255 152.093 238.258 152.098C234.197 152.098 230.901 150.667 228.371 147.805C225.842 144.943 224.58 140.85 224.58 135.53V90.135C224.58 81.1446 223.33 74.2768 220.831 69.5315C218.332 64.7862 213.365 62.4071 205.924 62.3984C201.144 62.3766 196.482 63.9033 192.642 66.7479C188.649 69.649 185.719 73.62 183.852 78.6698C182.52 82.7278 181.853 90.3133 181.858 101.418V135.53C181.858 141.041 180.574 145.182 178.009 147.953C175.445 150.723 172.123 152.106 168.048 152.093C164.339 152.189 160.782 150.614 158.362 147.805C155.823 144.956 154.561 140.867 154.578 135.53V56.805C154.578 51.616 155.71 47.7406 157.974 45.1832C160.238 42.6257 163.333 41.3426 167.26 41.3382C169.542 41.3121 171.784 41.908 173.751 43.0606C175.758 44.2741 177.374 46.04 178.397 48.1495C179.563 50.4025 180.147 53.1557 180.147 56.4135Z" fill="#6CA4C9"/><path d="M286 135.699V17.5633C286 12.1091 287.215 7.98575 289.644 5.18904C292.073 2.39233 295.352 0.996149 299.483 1.0005C303.61 1.0005 306.941 2.37928 309.47 5.1412C312 7.90311 313.266 12.0438 313.262 17.5633V135.695C313.262 141.206 311.982 145.342 309.422 148.113C306.862 150.883 303.549 152.262 299.483 152.253C295.486 152.253 292.239 150.822 289.744 147.965C287.249 145.107 286 141.019 286 135.699Z" fill="#FDC214"/><path d="M400.469 136.731V133.251C397.473 137.113 394.017 140.597 390.181 143.629C386.65 146.386 382.662 148.5 378.396 149.866C374.138 151.223 369.28 151.906 363.82 151.91C357.229 151.91 351.321 150.545 346.092 147.817C341.064 145.277 336.858 141.367 333.959 136.539C330.563 130.759 328.865 122.447 328.865 111.599V57.6093C328.865 52.1551 330.097 48.0796 332.557 45.3873C335.017 42.695 338.282 41.3466 342.348 41.3466C346.415 41.3466 349.802 42.708 352.331 45.4351C354.86 48.1623 356.123 52.2203 356.127 57.6137V101.213C356.127 107.533 356.658 112.839 357.725 117.123C358.792 121.407 360.703 124.77 363.467 127.205C366.232 129.632 369.976 130.846 374.704 130.846C379.349 130.828 383.877 129.402 387.686 126.749C391.64 124.148 394.735 120.433 396.576 116.075C398.105 112.221 398.866 103.775 398.871 90.7349V57.6093C398.871 52.2203 400.138 48.1623 402.667 45.4308C405.197 42.6993 408.492 41.3379 412.55 41.3423C416.612 41.3423 419.877 42.6906 422.341 45.383C424.805 48.0753 426.037 52.1507 426.033 57.605V136.526C426.033 141.719 424.853 145.612 422.489 148.205C420.125 150.797 417.077 152.093 413.346 152.093C409.82 152.18 406.433 150.701 404.104 148.052C401.679 145.364 400.469 141.589 400.469 136.731Z" fill="#FDC214"/><path d="M473.635 155.49L476.129 149.401L442.572 64.9905C440.508 60.1365 439.477 56.6134 439.481 54.4169C439.468 52.1291 440.09 49.8847 441.279 47.9318C442.481 45.9354 444.183 44.2826 446.22 43.1474C448.201 41.9817 450.452 41.3598 452.751 41.3511C456.682 41.3511 459.642 42.5994 461.636 45.0916C463.63 47.5839 465.398 51.1591 466.935 55.8174L490.009 122.869L511.881 60.5062C513.209 56.4612 514.776 52.4944 516.574 48.6364C517.972 45.7745 519.447 43.852 521.019 42.8473C522.591 41.8426 524.798 41.3511 527.658 41.3511C529.752 41.3598 531.803 41.9252 533.601 42.9952C535.447 44.0434 536.966 45.5788 537.994 47.436C539.008 49.2323 539.544 51.2635 539.544 53.3252C539.274 54.5865 538.838 56.3829 538.238 58.7142C537.637 61.0455 536.91 63.4073 536.061 65.7951L500.505 158.787C497.444 166.968 494.449 173.384 491.519 178.038C488.689 182.605 484.662 186.315 479.878 188.768C475.05 191.264 468.541 192.513 460.352 192.513C452.163 192.513 446.368 191.643 442.376 189.903C438.384 188.163 436.39 185.006 436.385 180.43C436.385 177.298 437.335 174.884 439.233 173.192C441.131 171.5 443.943 170.652 447.67 170.652C449.12 170.652 450.565 170.848 451.963 171.244C453.434 171.613 454.94 171.814 456.455 171.848C459.59 171.848 462.045 171.379 463.857 170.448C465.668 169.517 467.27 167.903 468.698 165.611C470.126 163.319 471.771 159.944 473.635 155.49Z" fill="#FDC214"/><path d="M688.527 56.4134V59.706C693.321 53.3862 698.562 48.7453 704.257 45.7876C709.951 42.83 716.491 41.3512 723.879 41.3468C731.075 41.3468 737.501 42.9083 743.156 46.0356C748.677 49.0193 753.113 53.6602 755.838 59.3058C757.641 62.6593 758.82 66.3128 759.321 70.0838C759.913 74.99 760.179 79.931 760.118 84.872V135.535C760.118 140.989 758.873 145.112 756.378 147.905C753.883 150.697 750.64 152.093 746.639 152.098C742.577 152.098 739.282 150.667 736.752 147.805C734.223 144.943 732.96 140.854 732.956 135.535V90.1392C732.956 81.1489 731.706 74.2811 729.212 69.5314C726.717 64.7818 721.741 62.4026 714.283 62.3983C709.499 62.3765 704.84 63.9032 700.996 66.7478C697.004 69.6489 694.074 73.6199 692.211 78.6697C690.878 82.7277 690.212 90.3132 690.217 101.417V135.543C690.217 141.054 688.937 145.191 686.372 147.961C683.808 150.732 680.495 152.111 676.433 152.102C672.724 152.202 669.167 150.627 666.747 147.813C664.213 144.951 662.946 140.858 662.95 135.539V56.8136C662.95 51.6246 664.082 47.7493 666.346 45.1918C668.61 42.6343 671.705 41.3512 675.632 41.3468C677.909 41.3164 680.151 41.9036 682.123 43.0431C684.13 44.2566 685.741 46.0225 686.764 48.132C687.94 50.3981 688.527 53.1557 688.527 56.4134Z" fill="#FDC214"/><path d="M597.285 194.996C593.345 195.066 589.527 193.639 586.597 191.004C583.602 188.342 582.104 184.619 582.104 179.83C582.052 175.898 583.602 172.118 586.401 169.352C589.157 166.468 592.997 164.876 596.985 164.963C601.007 164.894 604.882 166.464 607.721 169.313C610.616 172.044 612.227 175.863 612.166 179.839C612.166 184.562 610.685 188.272 607.721 190.965C604.891 193.6 601.155 195.044 597.285 194.996Z" fill="#FDC214"/><path d="M624.687 102.418H571.355C571.421 108.607 572.666 114.061 575.099 118.781C577.533 123.5 580.759 127.058 584.786 129.454C588.817 131.855 593.428 133.099 598.121 133.051C601.138 133.082 604.146 132.695 607.059 131.903C609.837 131.103 612.479 129.894 614.895 128.31C617.351 126.745 619.689 125 621.892 123.091C624.02 121.234 626.776 118.707 630.163 115.51C631.561 114.314 633.559 113.718 636.158 113.713C638.953 113.713 641.217 114.479 642.946 116.006C644.687 117.541 645.558 119.698 645.558 122.495C645.558 124.957 644.591 127.832 642.663 131.124C640.734 134.417 637.821 137.575 633.925 140.602C630.028 143.629 625.135 146.143 619.245 148.14C613.354 150.136 606.58 151.132 598.922 151.128C581.412 151.128 567.798 146.139 558.077 136.165C548.355 126.192 543.497 112.652 543.492 95.5544C543.423 87.9385 544.637 80.3661 547.088 73.1546C549.3 66.5826 552.874 60.5499 557.576 55.4479C562.304 50.4417 568.112 46.575 574.555 44.1393C581.277 41.5122 588.735 40.1987 596.924 40.1987C607.573 40.1987 616.715 42.443 624.351 46.9316C631.988 51.4203 637.695 57.2182 641.478 64.3295C645.27 71.4496 647.169 78.7002 647.169 86.0769C647.169 92.9273 645.205 97.3682 641.278 99.395C637.351 101.422 631.822 102.431 624.687 102.418ZM601.312 21.7003L591.329 10.0307L581.342 21.7003C578.547 25.0277 576.649 27.0545 575.652 27.7896C574.655 28.5247 572.788 28.89 570.058 28.8857H562.069C559.339 28.8857 557.972 28.4246 557.972 27.4895C557.972 26.7588 559.004 25.0973 561.068 22.5006L572.152 9.2304C573.088 8.09953 574.15 6.78599 575.348 5.28977C576.249 4.08932 577.311 3.015 578.499 2.10161C579.701 1.3448 581.042 0.83591 582.444 0.601039C584.416 0.213935 586.423 0.0312573 588.43 0.0530047H594.029C598.421 0.0530047 601.43 0.583641 603.062 1.64926C604.695 2.71488 607.107 5.24193 610.307 9.2304L621.787 22.7138C623.716 25.2452 624.687 26.737 624.687 27.2068C624.687 28.3376 623.189 28.9031 620.189 28.8987H612.601C610.603 28.8987 609.022 28.6204 607.856 28.0288C606.576 27.3808 605.439 26.4804 604.512 25.3843C603.446 24.1969 602.379 22.966 601.312 21.7003ZM571.355 86.9425H620.79C620.124 77.6302 617.612 70.6624 613.25 66.0389C608.887 61.4154 603.141 59.1058 596.005 59.1189C589.214 59.1189 583.637 61.4632 579.279 66.152C574.921 70.8407 572.278 77.7738 571.355 86.9555V86.9425Z" fill="#FDC214"/><path d="M70 154.603C108.66 154.603 140 124.526 140 87.4256C140 50.3247 108.66 20.2485 70 20.2485C31.3401 20.2485 0 50.3247 0 87.4256C0 124.526 31.3401 154.603 70 154.603Z" fill="url(#__ID__-g)"/><path d="M7.28825 120.038C7.28825 120.038 24.1446 113.06 27.0185 97.8379C27.0185 97.8379 26.442 116.503 12.0547 126.858L4.57275 129.443L7.28825 120.038Z" fill="white"/><path d="M133.447 120.038C133.447 120.038 116.595 113.06 113.721 97.8379C113.721 97.8379 114.293 116.503 128.663 126.858L136.145 129.443L133.447 120.038Z" fill="white"/><path d="M102.894 28.6732C102.894 28.6732 112.519 28.2335 115.583 18.3403C115.583 18.3403 121.216 32.5778 114.702 38.6852C114.715 38.694 103.809 34.1695 102.894 28.6732Z" fill="#8DC1E6"/><path d="M41.0098 28.3743C41.0098 28.3743 29.0475 28.8756 28.1673 18.2041C28.1673 18.2041 19.9108 29.9133 26.7369 38.5445C26.7546 38.5445 37.1632 41.4641 41.0098 28.3743Z" fill="#8DC1E6"/><path d="M37.9421 9.38804L65.9421 0.734784C65.9421 0.734784 70.1804 -0.918481 74.5992 0.734784L104.087 10.307C104.087 10.307 88.9689 18.7756 83.441 16.9333C83.441 16.9333 70 11.2172 57.4744 17.1927C57.4656 17.1927 43.8353 16.0187 37.9421 9.38804Z" fill="#3A3B3A"/><path d="M102.3 10.7999V14.511L101.147 15.0298V21.4318H103.906V15.0694L102.749 14.511L102.775 10.6064L102.3 10.7999Z" fill="#C92027"/><path d="M105.319 89.2241C117.387 89.2241 127.171 79.2158 127.171 66.8699C127.171 54.524 117.387 44.5156 105.319 44.5156C93.2508 44.5156 83.4675 54.524 83.4675 66.8699C83.4675 79.2158 93.2508 89.2241 105.319 89.2241Z" fill="white"/><path d="M36.6966 89.5718C48.7649 89.5718 58.5482 79.5634 58.5482 67.2175C58.5482 54.8716 48.7649 44.8633 36.6966 44.8633C24.6283 44.8633 14.845 54.8716 14.845 67.2175C14.845 79.5634 24.6283 89.5718 36.6966 89.5718Z" fill="white"/><path d="M36.6966 82.3165C45.0824 82.3165 51.8805 75.4009 51.8805 66.8699C51.8805 58.339 45.0824 51.4233 36.6966 51.4233C28.3108 51.4233 21.5127 58.339 21.5127 66.8699C21.5127 75.4009 28.3108 82.3165 36.6966 82.3165Z" fill="#3A3B3A"/><path d="M105.425 81.9689C113.811 81.9689 120.609 75.0532 120.609 66.5223C120.609 57.9914 113.811 51.0757 105.425 51.0757C97.039 51.0757 90.241 57.9914 90.241 66.5223C90.241 75.0532 97.039 81.9689 105.425 81.9689Z" fill="#3A3B3A"/><path d="M42.7834 61.255C45.5471 61.255 47.7875 59.0541 47.7875 56.3392C47.7875 53.6242 45.5471 51.4233 42.7834 51.4233C40.0197 51.4233 37.7793 53.6242 37.7793 56.3392C37.7793 59.0541 40.0197 61.255 42.7834 61.255Z" fill="white"/><path d="M110.429 60.9029C113.193 60.9029 115.433 58.702 115.433 55.9871C115.433 53.2722 113.193 51.0713 110.429 51.0713C107.665 51.0713 105.425 53.2722 105.425 55.9871C105.425 58.702 107.665 60.9029 110.429 60.9029Z" fill="white"/><path opacity="0.35" d="M70.1539 60.4946C70.1539 60.4946 82.213 60.4946 79.6296 74.1604C79.6296 74.1604 75.9678 96.3344 71.8748 94.9933C71.8748 94.9933 67.9974 96.7829 62.8304 75.5015C62.8304 75.5015 59.3844 62.9569 70.1539 60.4946Z" fill="#010101"/><path d="M69.9473 62.3149C69.9473 62.3149 82.2704 62.3149 79.6297 71.6058C79.6297 71.6058 75.8888 86.6918 71.7077 85.7772C71.7077 85.7772 67.7467 86.9952 62.4653 72.5203C62.4565 72.5203 58.9356 63.9902 69.9473 62.3149Z" fill="#F39120"/><path d="M69.9473 59.5576C69.9473 59.5576 82.2704 59.5576 79.6297 68.2505C79.6297 68.2505 75.8888 82.3604 71.7077 81.5073C71.7077 81.5073 67.7467 82.6462 62.4653 69.1079C62.4565 69.1079 58.9356 61.1185 69.9473 59.5576Z" fill="#FDC20F"/><path d="M71.1354 65.8541C73.0653 65.8541 74.6299 65.2163 74.6299 64.4295C74.6299 63.6427 73.0653 63.0049 71.1354 63.0049C69.2054 63.0049 67.6409 63.6427 67.6409 64.4295C67.6409 65.2163 69.2054 65.8541 71.1354 65.8541Z" fill="white"/><path d="M928.414 56.2478V59.5273C933.207 53.2162 938.445 48.571 944.135 45.5916C950.212 42.547 956.943 41.0203 963.743 41.1508C970.47 41.0464 977.109 42.6731 983.025 45.8787C988.541 48.8581 992.973 53.4989 995.694 59.1402C997.497 62.4893 998.677 66.1342 999.173 69.9008C999.778 74.794 1000.05 79.7263 999.991 84.6542V135.304C999.991 140.767 998.746 144.89 996.26 147.665C991.149 153.045 982.638 153.272 977.248 148.161C977.044 147.965 976.843 147.765 976.652 147.556C974.127 144.694 972.86 140.593 972.851 135.26V89.9388C972.851 80.9615 971.606 74.0981 969.111 69.3484C966.617 64.5988 961.645 62.224 954.196 62.224C949.416 62.2022 944.762 63.7246 940.922 66.5691C936.856 69.5616 933.795 73.7153 932.145 78.478C930.817 82.5317 930.151 90.1085 930.151 101.208V135.3C930.151 140.797 928.871 144.929 926.311 147.7C923.756 150.449 920.129 151.945 916.376 151.797C912.667 151.897 909.11 150.323 906.694 147.508C904.169 144.647 902.902 140.545 902.902 135.213V56.6045C902.902 51.4112 904.034 47.5402 906.298 44.9914C908.562 42.4382 911.657 41.1595 915.575 41.1464C917.848 41.116 920.09 41.7032 922.058 42.8427C924.065 44.0519 925.676 45.8178 926.703 47.9229C927.935 50.5152 928.523 53.3685 928.418 56.2348L928.414 56.2478Z" fill="#F9C01B"/><path d="M785.708 152.645C781.777 152.706 777.967 151.28 775.042 148.652C772.047 145.991 770.549 142.272 770.553 137.487C770.497 133.555 772.047 129.776 774.846 127.014C777.602 124.139 781.437 122.547 785.421 122.63C789.439 122.56 793.318 124.126 796.152 126.975C799.043 129.706 800.658 133.525 800.597 137.5C800.597 142.215 799.117 145.921 796.157 148.613C793.322 151.249 789.578 152.693 785.704 152.645H785.708Z" fill="#F9C01B"/><path d="M793.575 57.0919C792.939 55.6044 792.63 53.9951 792.682 52.3771C792.709 49.3237 793.81 46.3748 795.8 44.0565C797.898 41.499 800.85 40.2203 804.65 40.2203C806.932 40.2072 809.174 40.8292 811.115 42.0253C813.153 43.3041 814.725 45.2048 815.604 47.4448L842.566 110.182C846.88 100.347 851.43 89.8913 856.21 78.8132C860.99 67.7351 865.466 57.2789 869.632 47.4448C871.761 42.6342 875.453 40.229 880.707 40.2246C883.986 40.0898 887.133 41.5077 889.197 44.0522C891.191 46.3704 892.292 49.3194 892.314 52.3727C892.305 53.1643 892.236 53.9516 892.101 54.7345C891.962 55.5391 891.739 56.3264 891.443 57.0875L853.524 145.225C851.408 150.145 847.755 152.602 842.561 152.602C837.011 152.602 833.245 150.084 831.273 145.051L831.495 145.264L793.575 57.0919Z" fill="#F9C01B"/><defs><linearGradient id="__ID__-g" x1="70" y1="20.2485" x2="70" y2="154.603" gradientUnits="userSpaceOnUse"><stop stop-color="#8EC1E5"/><stop offset="0.23" stop-color="#87BADE"/><stop offset="0.57" stop-color="#72A6CB"/><stop offset="0.97" stop-color="#5187AC"/><stop offset="0.99" stop-color="#4F85AB"/></linearGradient></defs>',
|
|
15013
|
+
};
|
|
15014
|
+
const OL_LOGO_FULL_WHITE = {
|
|
15015
|
+
viewBox: '0 0 1000 197',
|
|
15016
|
+
body: '<path d="M180.147 57.4955V60.7881C184.94 54.4683 190.182 49.8274 195.872 46.8698C201.562 43.9121 208.106 42.4333 215.502 42.429C222.69 42.429 229.112 43.9904 234.776 47.1177C240.296 50.0971 244.732 54.7423 247.458 60.3879C249.26 63.7414 250.444 67.3906 250.94 71.1659C251.537 76.0721 251.802 81.0131 251.742 85.9542V136.617C251.742 142.071 250.492 146.194 247.997 148.987C245.503 151.779 242.255 153.175 238.258 153.18C234.197 153.18 230.901 151.749 228.371 148.887C225.842 146.025 224.58 141.932 224.58 136.612V91.217C224.58 82.2267 223.33 75.3588 220.831 70.6136C218.332 65.8683 213.365 63.4891 205.924 63.4804C201.144 63.4587 196.482 64.9853 192.642 67.8299C188.649 70.731 185.719 74.7021 183.852 79.7518C182.52 83.8099 181.853 91.3953 181.858 102.5V136.612C181.858 142.123 180.574 146.264 178.009 149.035C175.445 151.805 172.123 153.188 168.048 153.175C164.339 153.271 160.782 151.696 158.362 148.887C155.823 146.038 154.561 141.949 154.578 136.612V57.887C154.578 52.6981 155.71 48.8227 157.974 46.2652C160.238 43.7077 163.333 42.4246 167.26 42.4203C169.542 42.3942 171.784 42.99 173.751 44.1426C175.758 45.3562 177.374 47.122 178.397 49.2315C179.563 51.4846 180.147 54.2378 180.147 57.4955Z" fill="white"/><path d="M286 136.781V18.6453C286 13.1911 287.215 9.06779 289.644 6.27107C292.073 3.47436 295.352 2.07818 299.483 2.08253C303.61 2.08253 306.941 3.46131 309.47 6.22323C312 8.98515 313.266 13.1258 313.262 18.6453V136.777C313.262 142.288 311.982 146.424 309.422 149.195C306.862 151.965 303.549 153.344 299.483 153.336C295.486 153.336 292.239 151.905 289.744 149.047C287.249 146.189 286 142.101 286 136.781Z" fill="white"/><path d="M400.469 137.813V134.333C397.473 138.195 394.017 141.679 390.181 144.711C386.65 147.468 382.662 149.582 378.396 150.948C374.138 152.305 369.28 152.988 363.82 152.992C357.229 152.992 351.321 151.627 346.092 148.899C341.064 146.359 336.858 142.449 333.959 137.621C330.563 131.841 328.865 123.529 328.865 112.681V58.6914C328.865 53.2371 330.097 49.1617 332.557 46.4693C335.017 43.777 338.282 42.4287 342.348 42.4287C346.415 42.4287 349.802 43.7901 352.331 46.5172C354.86 49.2443 356.123 53.3024 356.127 58.6957V102.295C356.127 108.615 356.658 113.921 357.725 118.205C358.792 122.489 360.703 125.852 363.467 128.287C366.232 130.714 369.976 131.928 374.704 131.928C379.349 131.91 383.877 130.484 387.686 127.831C391.64 125.23 394.735 121.515 396.576 117.157C398.105 113.303 398.866 104.857 398.871 91.817V58.6914C398.871 53.3024 400.138 49.2443 402.667 46.5128C405.197 43.7814 408.492 42.42 412.55 42.4243C416.612 42.4243 419.877 43.7727 422.341 46.465C424.805 49.1573 426.037 53.2328 426.033 58.687V137.608C426.033 142.802 424.853 146.694 422.489 149.287C420.125 151.879 417.077 153.175 413.346 153.175C409.82 153.262 406.433 151.783 404.104 149.134C401.679 146.446 400.469 142.671 400.469 137.813Z" fill="white"/><path d="M473.635 156.572L476.129 150.483L442.572 66.0725C440.508 61.2185 439.477 57.6954 439.481 55.4989C439.468 53.2111 440.09 50.9668 441.279 49.0139C442.481 47.0175 444.183 45.3647 446.22 44.2294C448.201 43.0638 450.452 42.4418 452.751 42.4331C456.682 42.4331 459.642 43.6814 461.636 46.1737C463.63 48.6659 465.398 52.2412 466.935 56.8995L490.009 123.951L511.881 61.5882C513.209 57.5432 514.776 53.5765 516.574 49.7185C517.972 46.8565 519.447 44.9341 521.019 43.9293C522.591 42.9246 524.798 42.4331 527.658 42.4331C529.752 42.4418 531.803 43.0072 533.601 44.0772C535.447 45.1254 536.966 46.6608 537.994 48.518C539.008 50.3144 539.544 52.3456 539.544 54.4072C539.274 55.6686 538.838 57.4649 538.238 59.7962C537.637 62.1275 536.91 64.4893 536.061 66.8772L500.505 159.869C497.444 168.05 494.449 174.466 491.519 179.12C488.689 183.687 484.662 187.397 479.878 189.85C475.05 192.346 468.541 193.595 460.352 193.595C452.163 193.595 446.368 192.725 442.376 190.985C438.384 189.245 436.39 186.088 436.385 181.512C436.385 178.38 437.335 175.966 439.233 174.274C441.131 172.582 443.943 171.734 447.67 171.734C449.12 171.734 450.565 171.93 451.963 172.326C453.434 172.696 454.94 172.896 456.455 172.93C459.59 172.93 462.045 172.461 463.857 171.53C465.668 170.599 467.27 168.985 468.698 166.693C470.126 164.401 471.771 161.026 473.635 156.572Z" fill="white"/><path d="M688.527 57.4954V60.788C693.321 54.4682 698.562 49.8273 704.257 46.8697C709.951 43.912 716.491 42.4332 723.879 42.4289C731.075 42.4289 737.501 43.9903 743.156 47.1176C748.677 50.1013 753.113 54.7422 755.838 60.3878C757.641 63.7413 758.82 67.3949 759.321 71.1658C759.913 76.0721 760.179 81.0131 760.118 85.9541V136.617C760.118 142.071 758.873 146.194 756.378 148.987C753.883 151.779 750.64 153.175 746.639 153.18C742.577 153.18 739.282 151.749 736.752 148.887C734.223 146.025 732.96 141.936 732.956 136.617V91.2213C732.956 82.2309 731.706 75.3631 729.212 70.6135C726.717 65.8638 721.741 63.4847 714.283 63.4803C709.499 63.4586 704.84 64.9852 700.996 67.8298C697.004 70.7309 694.074 74.702 692.211 79.7517C690.878 83.8098 690.212 91.3953 690.217 102.499V136.625C690.217 142.136 688.937 146.273 686.372 149.043C683.808 151.814 680.495 153.193 676.433 153.184C672.724 153.284 669.167 151.709 666.747 148.895C664.213 146.033 662.946 141.94 662.95 136.621V57.8956C662.95 52.7067 664.082 48.8313 666.346 46.2738C668.61 43.7163 671.705 42.4332 675.632 42.4289C677.909 42.3984 680.151 42.9856 682.123 44.1252C684.13 45.3387 685.741 47.1045 686.764 49.214C687.94 51.4801 688.527 54.2377 688.527 57.4954Z" fill="white"/><path d="M597.285 196.079C593.345 196.148 589.527 194.721 586.597 192.086C583.602 189.424 582.104 185.701 582.104 180.912C582.052 176.98 583.602 173.2 586.401 170.434C589.157 167.55 592.997 165.958 596.985 166.045C601.007 165.976 604.882 167.546 607.721 170.395C610.616 173.126 612.227 176.945 612.166 180.921C612.166 185.644 610.685 189.354 607.721 192.047C604.891 194.682 601.155 196.126 597.285 196.079Z" fill="white"/><path d="M624.687 103.5H571.355C571.421 109.689 572.666 115.143 575.099 119.863C577.533 124.582 580.759 128.14 584.786 130.536C588.817 132.937 593.428 134.181 598.121 134.133C601.138 134.164 604.146 133.777 607.059 132.985C609.837 132.185 612.479 130.976 614.895 129.392C617.351 127.827 619.689 126.082 621.892 124.173C624.021 122.316 626.776 119.789 630.163 116.592C631.561 115.396 633.559 114.8 636.158 114.796C638.953 114.796 641.217 115.561 642.946 117.088C644.687 118.623 645.558 120.78 645.558 123.577C645.558 126.039 644.591 128.914 642.663 132.206C640.734 135.499 637.821 138.657 633.925 141.684C630.028 144.711 625.135 147.225 619.245 149.222C613.354 151.218 606.58 152.214 598.922 152.21C581.412 152.21 567.798 147.221 558.077 137.248C548.355 127.274 543.497 113.734 543.492 96.6365C543.423 89.0205 544.637 81.4481 547.088 74.2367C549.3 67.6646 552.874 61.6319 557.576 56.5299C562.304 51.5237 568.112 47.657 574.555 45.2213C581.277 42.5942 588.735 41.2807 596.924 41.2807C607.573 41.2807 616.715 43.525 624.351 48.0137C631.988 52.5023 637.695 58.3002 641.478 65.4116C645.27 72.5317 647.169 79.7822 647.169 87.159C647.169 94.0094 645.205 98.4502 641.278 100.477C637.351 102.504 631.822 103.513 624.687 103.5ZM601.312 22.7824L591.329 11.1127L581.342 22.7824C578.547 26.1097 576.649 28.1366 575.652 28.8716C574.655 29.6067 572.788 29.9721 570.058 29.9677H562.069C559.339 29.9677 557.972 29.5067 557.972 28.5715C557.972 27.8408 559.004 26.1793 561.068 23.5827L572.152 10.3124C573.088 9.18156 574.15 7.86802 575.348 6.3718C576.249 5.17135 577.311 4.09703 578.499 3.18364C579.701 2.42683 581.042 1.91794 582.444 1.68307C584.416 1.29597 586.423 1.11329 588.43 1.13504H594.029C598.421 1.13504 601.43 1.66567 603.062 2.73129C604.695 3.79691 607.107 6.32396 610.307 10.3124L621.787 23.7958C623.716 26.3272 624.687 27.8191 624.687 28.2888C624.687 29.4197 623.189 29.9851 620.189 29.9808H612.601C610.603 29.9808 609.022 29.7024 607.856 29.1109C606.576 28.4628 605.439 27.5624 604.512 26.4664C603.446 25.279 602.379 24.0481 601.312 22.7824ZM571.355 88.0245H620.79C620.124 78.7123 617.612 71.7444 613.25 67.1209C608.887 62.4974 603.141 60.1879 596.005 60.2009C589.214 60.2009 583.637 62.5453 579.279 67.234C574.921 71.9227 572.278 78.8558 571.355 88.0375V88.0245Z" fill="white"/><path d="M131.269 119.195C136.705 109.491 139.557 98.5462 139.553 87.4176C139.553 67.2358 130.323 49.1469 115.708 36.8343C119.75 30.0444 115.102 18.3391 115.102 18.3391C113.506 23.7089 108.976 27.685 103.452 28.5632C82.7688 17.6531 58.1004 17.439 37.225 27.9821C33.663 27.2393 28.7145 24.9848 28.2741 18.2036C28.2741 18.2036 21.6863 27.5889 25.1917 35.7507C9.79239 48.0634 0 66.6416 0 87.4176C0 98.6641 2.90807 109.718 8.44954 119.501C12.3081 117.561 24.5202 110.531 26.9225 97.8514C26.9225 97.8514 26.3819 115.822 12.8356 126.252C25.4794 143.406 46.2675 154.6 69.7763 154.6C93.285 154.6 114.378 143.24 126.996 125.867C113.916 115.377 113.38 97.8471 113.38 97.8471C115.664 109.941 126.861 116.893 131.269 119.195ZM36.6016 89.576C24.5638 89.576 14.7976 79.5747 14.7976 67.2271C14.7976 54.8795 24.5638 44.8913 36.6016 44.8913C48.6393 44.8913 58.3969 54.8926 58.3969 67.2271C58.3969 79.5616 48.6481 89.576 36.6016 89.576ZM79.3855 69.2719C79.3855 69.2719 75.8714 85.0713 71.5071 84.0445C71.5071 84.0445 67.5483 85.3465 62.2859 70.2157C62.2859 70.2157 58.75 61.3242 69.737 59.5502C69.7501 59.5502 82.032 59.5502 79.3855 69.2675V69.2719ZM105.004 89.2221C92.9667 89.2221 83.2005 79.2164 83.2005 66.8819C83.2005 54.5474 92.9667 44.5417 105.004 44.5417C117.042 44.5417 126.8 54.5343 126.8 66.8775C126.8 79.2208 117.055 89.2221 105.004 89.2221Z" fill="white"/><path d="M37.8354 9.38196L65.7477 0.730764C68.5424 -0.243588 71.5813 -0.243588 74.3803 0.730764L103.788 10.2908C103.788 10.2908 88.7376 18.7497 83.2266 16.9103C74.8817 13.6551 65.6038 13.7556 57.333 17.1943C57.333 17.1943 43.7083 16.0014 37.8398 9.37759L37.8354 9.38196Z" fill="white"/><path d="M101.996 10.7978V14.5161L100.832 15.0447V21.4195H103.588V15.0841L102.454 14.5161V10.6187L101.996 10.7978Z" fill="white"/><path d="M36.6016 48.3296C26.5651 48.3296 18.4294 56.6356 18.4294 66.8816C18.4294 77.1276 26.5651 85.4336 36.6016 85.4336C46.6382 85.4336 54.7694 77.1276 54.7694 66.8816C54.7694 56.6356 46.6382 48.3296 36.6016 48.3296ZM36.6016 79.9065C29.5516 79.9065 23.8445 74.0735 23.8445 66.8816C23.8445 59.6898 29.5516 53.8567 36.6016 53.8567C43.6516 53.8567 49.3588 59.6854 49.3588 66.8816C49.3588 74.0778 43.6429 79.9065 36.6016 79.9065Z" fill="white"/><path d="M105.1 47.9761C95.0726 47.9761 86.9282 56.2821 86.9282 66.5281C86.9282 76.7741 95.0726 85.0801 105.1 85.0801C115.128 85.0801 123.268 76.7741 123.268 66.5281C123.268 56.2821 115.137 47.9761 105.1 47.9761ZM105.1 79.5486C98.0678 79.5486 92.3432 73.7156 92.3432 66.5237C92.3432 59.3319 98.0547 53.5076 105.1 53.5076C112.146 53.5076 117.858 59.3406 117.858 66.5325C117.858 73.7243 112.15 79.5486 105.1 79.5486Z" fill="white"/><path d="M70.9056 65.8508C72.8368 65.8508 74.4023 65.217 74.4023 64.4352C74.4023 63.6533 72.8368 63.0195 70.9056 63.0195C68.9744 63.0195 67.4089 63.6533 67.4089 64.4352C67.4089 65.217 68.9744 65.8508 70.9056 65.8508Z" fill="white"/><path d="M42.6402 62.8011C45.39 62.8011 47.6192 60.5984 47.6192 57.8812C47.6192 55.1641 45.39 52.9614 42.6402 52.9614C39.8903 52.9614 37.6611 55.1641 37.6611 57.8812C37.6611 60.5984 39.8903 62.8011 42.6402 62.8011Z" fill="white"/><path d="M110.084 62.4773C112.834 62.4773 115.063 60.2747 115.063 57.5575C115.063 54.8404 112.834 52.6377 110.084 52.6377C107.334 52.6377 105.105 54.8404 105.105 57.5575C105.105 60.2747 107.334 62.4773 110.084 62.4773Z" fill="white"/><path d="M928.414 57.3298V60.6093C933.207 54.2983 938.445 49.653 944.135 46.6736C950.212 43.629 956.943 42.1023 963.743 42.2328C970.47 42.1284 977.109 43.7551 983.025 46.9607C988.541 49.9401 992.973 54.581 995.694 60.2222C997.497 63.5713 998.677 67.2162 999.173 70.9828C999.778 75.876 1000.05 80.8083 999.991 85.7363V136.386C999.991 141.849 998.746 145.972 996.26 148.747C991.149 154.127 982.638 154.354 977.248 149.243C977.044 149.047 976.843 148.847 976.652 148.638C974.127 145.776 972.86 141.675 972.851 136.342V91.0209C972.851 82.0436 971.606 75.1801 969.111 70.4305C966.617 65.6808 961.645 63.306 954.196 63.306C949.416 63.2843 944.762 64.8066 940.922 67.6511C936.856 70.6436 933.795 74.7973 932.145 79.56C930.817 83.6137 930.151 91.1905 930.151 102.29V136.382C930.151 141.879 928.871 146.011 926.311 148.782C923.756 151.531 920.129 153.027 916.376 152.879C912.667 152.979 909.11 151.405 906.694 148.591C904.169 145.729 902.902 141.627 902.902 136.295V57.6865C902.902 52.4932 904.034 48.6222 906.298 46.0734C908.562 43.5203 911.657 42.2415 915.575 42.2285C917.848 42.198 920.09 42.7852 922.058 43.9248C924.065 45.1339 925.676 46.8998 926.703 49.0049C927.935 51.5972 928.523 54.4505 928.418 57.3168L928.414 57.3298Z" fill="white"/><path d="M785.708 153.727C781.777 153.788 777.967 152.362 775.042 149.734C772.047 147.073 770.549 143.354 770.553 138.569C770.497 134.637 772.047 130.858 774.846 128.096C777.602 125.221 781.437 123.629 785.421 123.712C789.439 123.642 793.318 125.208 796.152 128.057C799.043 130.788 800.658 134.607 800.597 138.582C800.597 143.297 799.117 147.003 796.157 149.695C793.322 152.331 789.578 153.775 785.704 153.727H785.708Z" fill="white"/><path d="M793.575 58.1739C792.939 56.6864 792.63 55.0771 792.682 53.4591C792.709 50.4058 793.81 47.4568 795.8 45.1386C797.898 42.5811 800.85 41.3023 804.65 41.3023C806.932 41.2893 809.174 41.9112 811.115 43.1074C813.153 44.3861 814.725 46.2868 815.604 48.5268L842.566 111.264C846.88 101.429 851.43 90.9733 856.21 79.8952C860.99 68.8171 865.466 58.361 869.632 48.5268C871.761 43.7163 875.453 41.311 880.707 41.3067C883.986 41.1718 887.133 42.5898 889.197 45.1342C891.191 47.4525 892.292 50.4014 892.314 53.4548C892.305 54.2464 892.236 55.0336 892.101 55.8165C891.962 56.6212 891.739 57.4084 891.443 58.1696L853.524 146.307C851.408 151.227 847.755 153.684 842.561 153.684C837.011 153.684 833.245 151.166 831.273 146.133L831.495 146.346L793.575 58.1739Z" fill="white"/>',
|
|
15017
|
+
};
|
|
15018
|
+
const OL_LOGO_MARK_COLOR = {
|
|
15019
|
+
viewBox: '0 0 200 200',
|
|
15020
|
+
body: '<path d="M100 190.124C145.073 190.124 181.612 155.059 181.612 111.804C181.612 68.5482 145.073 33.4829 100 33.4829C54.927 33.4829 18.3882 68.5482 18.3882 111.804C18.3882 155.059 54.927 190.124 100 190.124Z" fill="url(#__ID__-g)"/><path d="M26.8854 149.826C26.8854 149.826 46.538 141.69 49.8886 123.943C49.8886 123.943 49.2164 145.704 32.4425 157.777L23.7195 160.791L26.8854 149.826Z" fill="white"/><path d="M173.971 149.826C173.971 149.826 154.324 141.69 150.973 123.943C150.973 123.943 151.64 145.704 168.394 157.777L177.117 160.791L173.971 149.826Z" fill="white"/><path d="M138.351 43.3053C138.351 43.3053 149.573 42.7926 153.144 31.2583C153.144 31.2583 159.712 47.8575 152.118 54.978C152.133 54.9883 139.418 49.7132 138.351 43.3053Z" fill="#8DC1E6"/><path d="M66.2008 42.9564C66.2008 42.9564 52.2542 43.5408 51.2279 31.0991C51.2279 31.0991 41.6018 44.7506 49.5603 54.8137C49.5808 54.8137 61.7161 58.2176 66.2008 42.9564Z" fill="#8DC1E6"/><path d="M62.6243 20.8209L95.269 10.7322C95.269 10.7322 100.21 8.80465 105.362 10.7322L139.741 21.8923C139.741 21.8923 122.115 31.7656 115.671 29.6177C115.671 29.6177 100 22.9534 85.3965 29.9201C85.3863 29.9201 69.495 28.5514 62.6243 20.8209Z" fill="#3A3B3A"/><path d="M137.658 22.4668V26.7934L136.313 27.3983V34.8623H139.531V27.4445L138.181 26.7934L138.212 22.2412L137.658 22.4668Z" fill="#C92027"/><path d="M141.178 113.9C155.248 113.9 166.655 102.232 166.655 87.8378C166.655 73.4439 155.248 61.7754 141.178 61.7754C127.108 61.7754 115.702 73.4439 115.702 87.8378C115.702 102.232 127.108 113.9 141.178 113.9Z" fill="white"/><path d="M61.1722 114.306C75.2425 114.306 86.6487 102.637 86.6487 88.2431C86.6487 73.8492 75.2425 62.1807 61.1722 62.1807C47.102 62.1807 35.6958 73.8492 35.6958 88.2431C35.6958 102.637 47.102 114.306 61.1722 114.306Z" fill="white"/><path d="M61.1721 105.847C70.949 105.847 78.8748 97.7841 78.8748 87.838C78.8748 77.892 70.949 69.8291 61.1721 69.8291C51.3952 69.8291 43.4695 77.892 43.4695 87.838C43.4695 97.7841 51.3952 105.847 61.1721 105.847Z" fill="#3A3B3A"/><path d="M141.301 105.442C151.078 105.442 159.004 97.3788 159.004 87.4327C159.004 77.4867 151.078 69.4238 141.301 69.4238C131.524 69.4238 123.599 77.4867 123.599 87.4327C123.599 97.3788 131.524 105.442 141.301 105.442Z" fill="#3A3B3A"/><path d="M68.2685 81.2917C71.4906 81.2917 74.1027 78.7257 74.1027 75.5604C74.1027 72.3951 71.4906 69.8291 68.2685 69.8291C65.0464 69.8291 62.4343 72.3951 62.4343 75.5604C62.4343 78.7257 65.0464 81.2917 68.2685 81.2917Z" fill="white"/><path d="M147.135 80.881C150.358 80.881 152.97 78.315 152.97 75.1497C152.97 71.9844 150.358 69.4185 147.135 69.4185C143.913 69.4185 141.301 71.9844 141.301 75.1497C141.301 78.315 143.913 80.881 147.135 80.881Z" fill="white"/><path opacity="0.35" d="M100.179 80.4053C100.179 80.4053 114.239 80.4053 111.227 96.338C111.227 96.338 106.958 122.19 102.186 120.627C102.186 120.627 97.6652 122.713 91.6411 97.9016C91.6411 97.9016 87.6234 83.276 100.179 80.4053Z" fill="#010101"/><path d="M99.9385 82.5273C99.9385 82.5273 114.306 82.5273 111.227 93.3594C111.227 93.3594 106.866 110.948 101.991 109.882C101.991 109.882 97.3729 111.302 91.2155 94.4256C91.2052 94.4256 87.1003 84.4805 99.9385 82.5273Z" fill="#F39120"/><path d="M99.9385 79.3125C99.9385 79.3125 114.306 79.3125 111.227 89.4473C111.227 89.4473 106.866 105.898 101.991 104.903C101.991 104.903 97.3729 106.231 91.2155 90.447C91.2052 90.447 87.1003 81.1324 99.9385 79.3125Z" fill="#FDC20F"/><path d="M101.324 86.6539C103.574 86.6539 105.398 85.9103 105.398 84.993C105.398 84.0757 103.574 83.332 101.324 83.332C99.0736 83.332 97.2495 84.0757 97.2495 84.993C97.2495 85.9103 99.0736 86.6539 101.324 86.6539Z" fill="white"/><defs><linearGradient id="__ID__-g" x1="100" y1="33.4829" x2="100" y2="190.124" gradientUnits="userSpaceOnUse"><stop stop-color="#8EC1E5"/><stop offset="0.23" stop-color="#87BADE"/><stop offset="0.57" stop-color="#72A6CB"/><stop offset="0.97" stop-color="#5187AC"/><stop offset="0.99" stop-color="#4F85AB"/></linearGradient></defs>',
|
|
15021
|
+
};
|
|
15022
|
+
const OL_LOGO_MARK_WHITE = {
|
|
15023
|
+
viewBox: '0 0 200 200',
|
|
15024
|
+
body: '<path d="M171.693 148.845C178.032 137.531 181.356 124.771 181.351 111.796C181.351 88.2664 170.59 67.1769 153.551 52.8218C158.263 44.9056 152.845 31.2585 152.845 31.2585C150.984 37.5191 145.703 42.1547 139.262 43.1787C115.148 30.4587 86.3874 30.2091 62.0492 42.5011C57.8963 41.6352 52.1269 39.0066 51.6135 31.1006C51.6135 31.1006 43.9328 42.0427 48.0197 51.5584C30.066 65.9136 18.6492 87.5736 18.6492 111.796C18.6492 124.908 22.0396 137.796 28.5003 149.202C32.9989 146.94 47.2369 138.744 50.0377 123.961C50.0377 123.961 49.4074 144.913 33.614 157.072C48.3552 177.072 72.5917 190.123 100 190.123C127.409 190.123 152.001 176.878 166.712 156.624C151.462 144.393 150.837 123.956 150.837 123.956C153.5 138.056 166.554 146.161 171.693 148.845ZM61.3223 114.312C47.2877 114.312 35.9014 102.652 35.9014 88.2562C35.9014 73.8603 47.2877 62.2153 61.3223 62.2153C75.3569 62.2153 86.7331 73.8756 86.7331 88.2562C86.7331 102.637 75.3671 114.312 61.3223 114.312ZM111.203 90.6402C111.203 90.6402 107.106 109.06 102.018 107.863C102.018 107.863 97.4026 109.381 91.2672 91.7406C91.2672 91.7406 87.1448 81.3741 99.9544 79.3059C99.9696 79.3059 114.289 79.3059 111.203 90.6351V90.6402ZM141.072 113.9C127.037 113.9 115.651 102.234 115.651 87.8538C115.651 73.4732 127.037 61.8077 141.072 61.8077C155.107 61.8077 166.483 73.4579 166.483 87.8487C166.483 102.239 155.122 113.9 141.072 113.9Z" fill="white"/><path d="M62.761 20.8157L95.3034 10.7294C98.5617 9.59345 102.105 9.59345 105.368 10.7294L139.654 21.8753C139.654 21.8753 122.107 31.7374 115.682 29.5928C105.953 25.7977 95.1357 25.9149 85.4929 29.9239C85.4929 29.9239 69.608 28.5333 62.7661 20.8106L62.761 20.8157Z" fill="white"/><path d="M137.565 22.4662V26.8012L136.208 27.4176V34.8499H139.42V27.4635L138.098 26.8012V22.2573L137.565 22.4662Z" fill="white"/><path d="M61.3224 66.2241C49.6209 66.2241 40.1357 75.908 40.1357 87.8536C40.1357 99.7992 49.6209 109.483 61.3224 109.483C73.0238 109.483 82.5039 99.7992 82.5039 87.8536C82.5039 75.908 73.0238 66.2241 61.3224 66.2241ZM61.3224 103.039C53.1029 103.039 46.449 96.2385 46.449 87.8536C46.449 79.4687 53.1029 72.6681 61.3224 72.6681C69.5418 72.6681 76.1957 79.4636 76.1957 87.8536C76.1957 96.2436 69.5317 103.039 61.3224 103.039Z" fill="white"/><path d="M141.184 65.812C129.493 65.812 119.997 75.4959 119.997 87.4415C119.997 99.3871 129.493 109.071 141.184 109.071C152.875 109.071 162.365 99.3871 162.365 87.4415C162.365 75.4959 152.885 65.812 141.184 65.812ZM141.184 102.622C132.985 102.622 126.311 95.8213 126.311 87.4364C126.311 79.0515 132.97 72.2611 141.184 72.2611C149.398 72.2611 156.057 79.0617 156.057 87.4466C156.057 95.8314 149.403 102.622 141.184 102.622Z" fill="white"/><path d="M101.317 86.652C103.568 86.652 105.393 85.9131 105.393 85.0016C105.393 84.09 103.568 83.3511 101.317 83.3511C99.0652 83.3511 97.24 84.09 97.24 85.0016C97.24 85.9131 99.0652 86.652 101.317 86.652Z" fill="white"/><path d="M68.3626 83.0964C71.5686 83.0964 74.1676 80.5283 74.1676 77.3605C74.1676 74.1926 71.5686 71.6245 68.3626 71.6245C65.1566 71.6245 62.5576 74.1926 62.5576 77.3605C62.5576 80.5283 65.1566 83.0964 68.3626 83.0964Z" fill="white"/><path d="M146.994 82.719C150.2 82.719 152.799 80.1509 152.799 76.983C152.799 73.8151 150.2 71.2471 146.994 71.2471C143.788 71.2471 141.189 73.8151 141.189 76.983C141.189 80.1509 143.788 82.719 146.994 82.719Z" fill="white"/>',
|
|
15025
|
+
};
|
|
15026
|
+
|
|
15027
|
+
const ART = {
|
|
15028
|
+
full: { color: OL_LOGO_FULL_COLOR, white: OL_LOGO_FULL_WHITE },
|
|
15029
|
+
mark: { color: OL_LOGO_MARK_COLOR, white: OL_LOGO_MARK_WHITE },
|
|
15030
|
+
};
|
|
15031
|
+
/** Đếm lần vẽ — id gradient riêng cho mỗi logo trên trang. */
|
|
15032
|
+
let nextId$1 = 0;
|
|
15033
|
+
/**
|
|
15034
|
+
* Logo Ôn Luyện — Figma `Logo ol` (#6603:1269), 4 bản: dài/vuông × màu/trắng.
|
|
15035
|
+
*
|
|
15036
|
+
* SVG nhúng sẵn trong gói: nơi dùng không phải chép file ảnh vào `assets/`.
|
|
15037
|
+
* Cao theo `height` (px), rộng tự theo tỉ lệ.
|
|
15038
|
+
*
|
|
15039
|
+
* ```html
|
|
15040
|
+
* <ol-logo /> <!-- dài, tự đổi theo chế độ, cao 32px -->
|
|
15041
|
+
* <ol-logo variant="mark" tone="white" [height]="40" />
|
|
15042
|
+
* ```
|
|
15043
|
+
*/
|
|
15044
|
+
class OlLogo {
|
|
15045
|
+
sanitizer = inject(DomSanitizer);
|
|
15046
|
+
uid = `ol-logo-${nextId$1++}`;
|
|
15047
|
+
variant = input('full', /* @ts-ignore */
|
|
15048
|
+
...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
15049
|
+
tone = input('auto', /* @ts-ignore */
|
|
15050
|
+
...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
|
|
15051
|
+
/** Chiều cao (px). Bản dài 32px rộng ~164px; bản vuông bằng cạnh. */
|
|
15052
|
+
height = input(32, /* @ts-ignore */
|
|
15053
|
+
...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
15054
|
+
/** Tên đọc cho trình đọc màn hình. */
|
|
15055
|
+
alt = input('Ôn Luyện', /* @ts-ignore */
|
|
15056
|
+
...(ngDevMode ? [{ debugName: "alt" }] : /* istanbul ignore next */ []));
|
|
15057
|
+
/** Bản màu — `null` khi `tone="white"`. */
|
|
15058
|
+
colorSvg = computed(() => this.tone() === 'white' ? null : this.render(ART[this.variant()].color, 'c'), /* @ts-ignore */
|
|
15059
|
+
...(ngDevMode ? [{ debugName: "colorSvg" }] : /* istanbul ignore next */ []));
|
|
15060
|
+
/** Bản trắng — `null` khi `tone="color"`. */
|
|
15061
|
+
whiteSvg = computed(() => this.tone() === 'color' ? null : this.render(ART[this.variant()].white, 'w'), /* @ts-ignore */
|
|
15062
|
+
...(ngDevMode ? [{ debugName: "whiteSvg" }] : /* istanbul ignore next */ []));
|
|
15063
|
+
colorClass = computed(() => (this.tone() === 'auto' ? 'h-full dark:hidden' : 'h-full'), /* @ts-ignore */
|
|
15064
|
+
...(ngDevMode ? [{ debugName: "colorClass" }] : /* istanbul ignore next */ []));
|
|
15065
|
+
whiteClass = computed(() => this.tone() === 'auto' ? 'hidden h-full dark:block' : 'h-full', /* @ts-ignore */
|
|
15066
|
+
...(ngDevMode ? [{ debugName: "whiteClass" }] : /* istanbul ignore next */ []));
|
|
15067
|
+
/**
|
|
15068
|
+
* SVG tĩnh từ `logo.art.ts` (chỉ `<path>`/`<linearGradient>`, không lấy từ
|
|
15069
|
+
* người dùng) nên đánh dấu tin cậy được — cùng cách `OlIconService` làm.
|
|
15070
|
+
*/
|
|
15071
|
+
render(art, suffix) {
|
|
15072
|
+
const body = art.body.replaceAll('__ID__', `${this.uid}-${suffix}`);
|
|
15073
|
+
return this.sanitizer.bypassSecurityTrustHtml(`<svg viewBox="${art.viewBox}" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" style="display:block;height:100%;width:auto">${body}</svg>`);
|
|
15074
|
+
}
|
|
15075
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlLogo, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
15076
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlLogo, isStandalone: true, selector: "ol-logo", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "img" }, properties: { "attr.aria-label": "alt()", "style.height.px": "height()" }, classAttribute: "inline-flex shrink-0 align-middle" }, ngImport: i0, template: "<!--\n `auto` v\u1EBD c\u1EA3 hai b\u1EA3n: b\u1EA3n m\u00E0u \u1EA9n trong `.dark`, b\u1EA3n tr\u1EAFng ch\u1EC9 hi\u1EC7n trong\n `.dark`. Hai b\u1EA3n c\u00F3 id gradient kh\u00E1c nhau n\u00EAn b\u1EA3n b\u1ECB \u1EA9n kh\u00F4ng k\u00E9o b\u1EA3n \u0111ang\n hi\u1EC7n m\u1EA5t m\u00E0u.\n-->\n@if (colorSvg(); as svg) {\n <span [class]=\"colorClass()\" data-tone=\"color\" [innerHTML]=\"svg\"></span>\n}\n@if (whiteSvg(); as svg) {\n <span [class]=\"whiteClass()\" data-tone=\"white\" [innerHTML]=\"svg\"></span>\n}\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
15077
|
+
}
|
|
15078
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlLogo, decorators: [{
|
|
15079
|
+
type: Component,
|
|
15080
|
+
args: [{ selector: 'ol-logo', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
15081
|
+
class: 'inline-flex shrink-0 align-middle',
|
|
15082
|
+
role: 'img',
|
|
15083
|
+
'[attr.aria-label]': 'alt()',
|
|
15084
|
+
'[style.height.px]': 'height()',
|
|
15085
|
+
}, template: "<!--\n `auto` v\u1EBD c\u1EA3 hai b\u1EA3n: b\u1EA3n m\u00E0u \u1EA9n trong `.dark`, b\u1EA3n tr\u1EAFng ch\u1EC9 hi\u1EC7n trong\n `.dark`. Hai b\u1EA3n c\u00F3 id gradient kh\u00E1c nhau n\u00EAn b\u1EA3n b\u1ECB \u1EA9n kh\u00F4ng k\u00E9o b\u1EA3n \u0111ang\n hi\u1EC7n m\u1EA5t m\u00E0u.\n-->\n@if (colorSvg(); as svg) {\n <span [class]=\"colorClass()\" data-tone=\"color\" [innerHTML]=\"svg\"></span>\n}\n@if (whiteSvg(); as svg) {\n <span [class]=\"whiteClass()\" data-tone=\"white\" [innerHTML]=\"svg\"></span>\n}\n" }]
|
|
15086
|
+
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], alt: [{ type: i0.Input, args: [{ isSignal: true, alias: "alt", required: false }] }] } });
|
|
15087
|
+
|
|
14982
15088
|
/**
|
|
14983
15089
|
* Dải nút hành động cuối Modal — khớp Figma `ol-modal-footer`.
|
|
14984
15090
|
* Figma có 4 biến thể: Buttons, 3 buttons, Buttons & links, Pagination.
|
|
@@ -16937,6 +17043,131 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
16937
17043
|
args: [{ selector: 'ol-resizable', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'block' }, template: "<div [class]=\"rootClasses()\">\n <!--\n `flex-basis` ph\u1EA7n tr\u0103m + `min-w-0` \u0111\u1EC3 n\u1ED9i dung d\u00E0i kh\u00F4ng \u0111\u1EA9y khung r\u1ED9ng ra.\n Thi\u1EBFu `min-w-0` th\u00EC m\u1ED9t b\u1EA3ng hay m\u1ED9t d\u00F2ng ch\u1EEF kh\u00F4ng ng\u1EAFt s\u1EBD \u00E9p khung gi\u1EEF\n nguy\u00EAn b\u1EC1 r\u1ED9ng v\u00E0 v\u1EA1ch k\u00E9o \u0111\u1EE9ng im \u2014 l\u1ED7i tr\u00F4ng nh\u01B0 k\u00E9o h\u1ECFng.\n -->\n <div class=\"min-h-0 min-w-0 overflow-auto\" [style.flex-basis]=\"firstStyle()\">\n <ng-content select=\"[first]\" />\n </div>\n\n <!--\n `role=\"separator\"` v\u1EDBi `aria-valuenow` l\u00E0 c\u00E1ch chu\u1EA9n b\u00E1o cho tr\u00ECnh \u0111\u1ECDc m\u00E0n\n h\u00ECnh bi\u1EBFt \u0111\u00E2y l\u00E0 v\u1EA1ch chia v\u00E0 \u0111ang \u1EDF \u0111\u00E2u. `aria-orientation` NG\u01AF\u1EE2C v\u1EDBi\n h\u01B0\u1EDBng chia: khung chia ngang th\u00EC v\u1EA1ch \u0111\u1EE9ng d\u1ECDc.\n\n Th\u1EBB ngo\u00E0i r\u1ED9ng 24px cho \u0111\u1EE7 ng\u01B0\u1EE1ng v\u00F9ng b\u1EA5m WCAG 2.5.8; `span` con m\u1EDBi l\u00E0\n ph\u1EA7n m\u00E0u m\u1EA3nh 6px. Xem handleClasses() trong .ts.\n -->\n <div\n [class]=\"handleClasses()\"\n role=\"separator\"\n [attr.tabindex]=\"disabled() ? null : 0\"\n [attr.aria-orientation]=\"ariaOrientation()\"\n [attr.aria-valuenow]=\"ratio()\"\n [attr.aria-valuemin]=\"minRatio()\"\n [attr.aria-valuemax]=\"maxRatio()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n (pointerdown)=\"onPointerDown($event)\"\n (keydown)=\"onKeydown($event)\"\n >\n <span [class]=\"handleBarClasses()\"></span>\n </div>\n\n <div class=\"min-h-0 min-w-0 flex-1 overflow-auto\">\n <ng-content select=\"[second]\" />\n </div>\n</div>\n" }]
|
|
16938
17044
|
}], ctorParameters: () => [], propDecorators: { direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], ratio: [{ type: i0.Input, args: [{ isSignal: true, alias: "ratio", required: false }] }, { type: i0.Output, args: ["ratioChange"] }], minRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "minRatio", required: false }] }], maxRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRatio", required: false }] }], keyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStep", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], resizeEnd: [{ type: i0.Output, args: ["resizeEnd"] }] } });
|
|
16939
17045
|
|
|
17046
|
+
/** `<ng-template olAppNavItem let-item let-active="active" let-variant="variant">` */
|
|
17047
|
+
class OlAppNavItemDef {
|
|
17048
|
+
template = inject(TemplateRef);
|
|
17049
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
17050
|
+
return true;
|
|
17051
|
+
}
|
|
17052
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavItemDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
17053
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: OlAppNavItemDef, isStandalone: true, selector: "ng-template[olAppNavItem]", ngImport: i0 });
|
|
17054
|
+
}
|
|
17055
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavItemDef, decorators: [{
|
|
17056
|
+
type: Directive,
|
|
17057
|
+
args: [{ selector: 'ng-template[olAppNavItem]', standalone: true }]
|
|
17058
|
+
}] });
|
|
17059
|
+
/**
|
|
17060
|
+
* `<ng-template olAppNavHeader let-collapsed>` — vùng ngay dưới logo, trên
|
|
17061
|
+
* các mục (ví dụ bộ chọn vai Quản trị / Giảng dạy). Cũng hiện ở đầu khay mobile.
|
|
17062
|
+
*/
|
|
17063
|
+
class OlAppNavHeaderDef {
|
|
17064
|
+
template = inject(TemplateRef);
|
|
17065
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
17066
|
+
return true;
|
|
17067
|
+
}
|
|
17068
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavHeaderDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
17069
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: OlAppNavHeaderDef, isStandalone: true, selector: "ng-template[olAppNavHeader]", ngImport: i0 });
|
|
17070
|
+
}
|
|
17071
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavHeaderDef, decorators: [{
|
|
17072
|
+
type: Directive,
|
|
17073
|
+
args: [{ selector: 'ng-template[olAppNavHeader]', standalone: true }]
|
|
17074
|
+
}] });
|
|
17075
|
+
/**
|
|
17076
|
+
* `<ng-template olAppNavFooter let-collapsed>` — vùng cuối danh sách mục chính,
|
|
17077
|
+
* trên đường kẻ (ví dụ ô "Niên khoá 2026 - 2027"). Cũng hiện cuối khay mobile.
|
|
17078
|
+
*/
|
|
17079
|
+
class OlAppNavFooterDef {
|
|
17080
|
+
template = inject(TemplateRef);
|
|
17081
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
17082
|
+
return true;
|
|
17083
|
+
}
|
|
17084
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavFooterDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
17085
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: OlAppNavFooterDef, isStandalone: true, selector: "ng-template[olAppNavFooter]", ngImport: i0 });
|
|
17086
|
+
}
|
|
17087
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavFooterDef, decorators: [{
|
|
17088
|
+
type: Directive,
|
|
17089
|
+
args: [{ selector: 'ng-template[olAppNavFooter]', standalone: true }]
|
|
17090
|
+
}] });
|
|
17091
|
+
/** `<ng-template olAppNavLogo let-collapsed>` */
|
|
17092
|
+
class OlAppNavLogoDef {
|
|
17093
|
+
template = inject(TemplateRef);
|
|
17094
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
17095
|
+
return true;
|
|
17096
|
+
}
|
|
17097
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavLogoDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
17098
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: OlAppNavLogoDef, isStandalone: true, selector: "ng-template[olAppNavLogo]", ngImport: i0 });
|
|
17099
|
+
}
|
|
17100
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavLogoDef, decorators: [{
|
|
17101
|
+
type: Directive,
|
|
17102
|
+
args: [{ selector: 'ng-template[olAppNavLogo]', standalone: true }]
|
|
17103
|
+
}] });
|
|
17104
|
+
/** `<ng-template olAppNavUser let-user let-collapsed="collapsed">` */
|
|
17105
|
+
class OlAppNavUserDef {
|
|
17106
|
+
template = inject(TemplateRef);
|
|
17107
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
17108
|
+
return true;
|
|
17109
|
+
}
|
|
17110
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavUserDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
17111
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: OlAppNavUserDef, isStandalone: true, selector: "ng-template[olAppNavUser]", ngImport: i0 });
|
|
17112
|
+
}
|
|
17113
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavUserDef, decorators: [{
|
|
17114
|
+
type: Directive,
|
|
17115
|
+
args: [{ selector: 'ng-template[olAppNavUser]', standalone: true }]
|
|
17116
|
+
}] });
|
|
17117
|
+
/** `<ng-template olAppNavUserMenu let-user let-actions="actions" let-select="select">` */
|
|
17118
|
+
class OlAppNavUserMenuDef {
|
|
17119
|
+
template = inject(TemplateRef);
|
|
17120
|
+
static ngTemplateContextGuard(_dir, ctx) {
|
|
17121
|
+
return true;
|
|
17122
|
+
}
|
|
17123
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavUserMenuDef, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
17124
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: OlAppNavUserMenuDef, isStandalone: true, selector: "ng-template[olAppNavUserMenu]", ngImport: i0 });
|
|
17125
|
+
}
|
|
17126
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNavUserMenuDef, decorators: [{
|
|
17127
|
+
type: Directive,
|
|
17128
|
+
args: [{ selector: 'ng-template[olAppNavUserMenu]', standalone: true }]
|
|
17129
|
+
}] });
|
|
17130
|
+
/** Gom sáu directive để nơi dùng nhập một lần: `imports: [OlAppNav, OL_APP_NAV_TEMPLATES]`. */
|
|
17131
|
+
const OL_APP_NAV_TEMPLATES = [
|
|
17132
|
+
OlAppNavItemDef,
|
|
17133
|
+
OlAppNavHeaderDef,
|
|
17134
|
+
OlAppNavFooterDef,
|
|
17135
|
+
OlAppNavLogoDef,
|
|
17136
|
+
OlAppNavUserDef,
|
|
17137
|
+
OlAppNavUserMenuDef,
|
|
17138
|
+
];
|
|
17139
|
+
|
|
17140
|
+
/**
|
|
17141
|
+
* Kiểu dữ liệu cho `ol-app-nav` — menu điều hướng Ôn Luyện.
|
|
17142
|
+
*
|
|
17143
|
+
* Tách riêng khỏi component để nơi dùng khai được danh sách menu ở bất kỳ đâu:
|
|
17144
|
+
* hằng số trong mã, tệp JSON, hoặc trả về từ API theo quyền tài khoản. Component
|
|
17145
|
+
* KHÔNG nhúng sẵn danh sách nào — ba quyền Học sinh · Giáo viên · Phụ huynh đều
|
|
17146
|
+
* là dữ liệu truyền vào.
|
|
17147
|
+
*
|
|
17148
|
+
* Figma `ol-app-nav` (#5303:30109) mô tả ba quyền, nhưng đó là NỘI DUNG mẫu,
|
|
17149
|
+
* không phải ràng buộc của component. Thêm quyền thứ tư chỉ là thêm một mảng.
|
|
17150
|
+
*/
|
|
17151
|
+
/**
|
|
17152
|
+
* Menu tài khoản mặc định — ba việc mọi tài khoản đều cần.
|
|
17153
|
+
*
|
|
17154
|
+
* Không có `route`: đường dẫn trang cá nhân, cách đổi mật khẩu (trang riêng
|
|
17155
|
+
* hay hộp thoại) khác nhau giữa các sản phẩm. Nơi dùng bắt `userAction` theo
|
|
17156
|
+
* `id`, hoặc tự khai `user.actions` có `route`.
|
|
17157
|
+
*/
|
|
17158
|
+
const APP_NAV_DEFAULT_USER_ACTIONS = [
|
|
17159
|
+
{ id: 'profile', label: 'Trang cá nhân', icon: 'solar:user-circle-linear' },
|
|
17160
|
+
{ id: 'change-password', label: 'Đổi mật khẩu', icon: 'solar:lock-password-linear' },
|
|
17161
|
+
{
|
|
17162
|
+
id: 'logout',
|
|
17163
|
+
label: 'Đăng xuất',
|
|
17164
|
+
icon: 'solar:logout-2-linear',
|
|
17165
|
+
danger: true,
|
|
17166
|
+
dividerBefore: true,
|
|
17167
|
+
},
|
|
17168
|
+
];
|
|
17169
|
+
|
|
17170
|
+
let nextId = 0;
|
|
16940
17171
|
/**
|
|
16941
17172
|
* `ol-app-nav` — menu điều hướng dọc của Ôn Luyện.
|
|
16942
17173
|
*
|
|
@@ -16953,6 +17184,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
16953
17184
|
* Không gộp hai cái làm một: gộp thì `ol-sidebar` phải mang theo nền gradient
|
|
16954
17185
|
* và bố cục ba vùng mà 90% nơi dùng không cần.
|
|
16955
17186
|
*
|
|
17187
|
+
* ## Dữ liệu + template từng vùng
|
|
17188
|
+
*
|
|
17189
|
+
* Danh sách mục vẫn đi qua `config` (nạp từ JSON/API theo vai). Phần RUỘT của
|
|
17190
|
+
* bốn vùng thay được bằng `<ng-template>` truyền vào — xem `app-nav-templates.ts`:
|
|
17191
|
+
* `olAppNavItem` · `olAppNavLogo` · `olAppNavUser` · `olAppNavUserMenu`. Vỏ
|
|
17192
|
+
* (liên kết, mục đang chọn, tooltip, mở/đóng popup) vẫn do component giữ.
|
|
17193
|
+
*
|
|
17194
|
+
* ## Mobile
|
|
17195
|
+
*
|
|
17196
|
+
* `layout="auto"`: dưới `md` menu dọc ẩn, thay bằng thanh đáy (`mobilePrimaryCount`
|
|
17197
|
+
* mục đầu nhóm chính + nút Menu) và khay menu trượt lên chứa phần còn lại, thẻ
|
|
17198
|
+
* tài khoản và menu tài khoản. Hai phần này `position: fixed` theo màn hình;
|
|
17199
|
+
* muốn xem trước trong khung (trang docs) thì bọc khung bằng `transform` —
|
|
17200
|
+
* phần tử `fixed` khi đó neo theo khung thay vì màn hình.
|
|
17201
|
+
*
|
|
16956
17202
|
* ## Dữ liệu từ ngoài, không nhúng cứng
|
|
16957
17203
|
*
|
|
16958
17204
|
* Ba quyền trong Figma là NỘI DUNG mẫu. Component nhận `config` — nơi dùng nạp
|
|
@@ -16969,6 +17215,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
16969
17215
|
* như Figma mô tả ("dùng trên nền tối, chữ và biểu tượng đều `text-white`").
|
|
16970
17216
|
*/
|
|
16971
17217
|
class OlAppNav {
|
|
17218
|
+
overlayContainer = inject(OlOverlayContainer);
|
|
17219
|
+
router = inject(Router);
|
|
17220
|
+
/**
|
|
17221
|
+
* URL hiện tại (bỏ query/hash) — nguồn của mục đang chọn và nhánh tự mở.
|
|
17222
|
+
*
|
|
17223
|
+
* Không dùng `routerLinkActive` nữa: nó không biết `activeRoutes`, không cho
|
|
17224
|
+
* biết mục CHA có con đang mở, và không chạy với mục cha (là nút, không có
|
|
17225
|
+
* `routerLink`). Tự so URL thì một luật áp cho cả cây.
|
|
17226
|
+
*/
|
|
17227
|
+
currentUrl = toSignal(this.router.events.pipe(filter((e) => e instanceof NavigationEnd), map$1((e) => e.urlAfterRedirects)), { initialValue: this.router.url });
|
|
17228
|
+
/** Id các mục cha đang mở. */
|
|
17229
|
+
expandedIds = signal(new Set(), /* @ts-ignore */
|
|
17230
|
+
...(ngDevMode ? [{ debugName: "expandedIds" }] : /* istanbul ignore next */ []));
|
|
17231
|
+
/** Hàm gỡ khay mobile khỏi ngăn xếp Escape. `null` khi khay đóng. */
|
|
17232
|
+
releaseEscape = null;
|
|
17233
|
+
constructor() {
|
|
17234
|
+
/**
|
|
17235
|
+
* Vào trang nào thì mở sẵn nhánh chứa trang đó — người dùng mở link trực
|
|
17236
|
+
* tiếp vẫn thấy mình đang ở đâu trong cây.
|
|
17237
|
+
*
|
|
17238
|
+
* Mỗi lần ĐỔI TRANG, các nhánh mở = đúng đường tới trang đó (THAY, không
|
|
17239
|
+
* cộng dồn) — giữ luật "chỉ một nhánh mở mỗi cấp" kể cả khi điều hướng
|
|
17240
|
+
* từ menu nổi, liên kết trong trang hay nút Back.
|
|
17241
|
+
*/
|
|
17242
|
+
effect(() => {
|
|
17243
|
+
this.currentUrl();
|
|
17244
|
+
const ids = this.activeAncestorIds(this.allGroups().flatMap((g) => g.items));
|
|
17245
|
+
this.expandedIds.update((set) => set.size === ids.length && ids.every((id) => set.has(id)) ? set : new Set(ids));
|
|
17246
|
+
});
|
|
17247
|
+
/**
|
|
17248
|
+
* Khay mobile vào ngăn xếp Escape chung của thư viện khi mở, ra khi đóng.
|
|
17249
|
+
*
|
|
17250
|
+
* Không tự nghe `document:keydown.escape`: trình nghe chung chạy ở pha
|
|
17251
|
+
* capture và chặn lan truyền, nên trình nghe riêng không bao giờ nhận
|
|
17252
|
+
* được phím; và nếu có nhận thì một phím đóng MỌI lớp đang mở.
|
|
17253
|
+
*/
|
|
17254
|
+
effect(() => {
|
|
17255
|
+
if (this.mobileMenuOpen()) {
|
|
17256
|
+
this.releaseEscape ??= this.overlayContainer.register(() => this.mobileMenuOpen.set(false));
|
|
17257
|
+
}
|
|
17258
|
+
else {
|
|
17259
|
+
this.releaseEscape?.();
|
|
17260
|
+
this.releaseEscape = null;
|
|
17261
|
+
}
|
|
17262
|
+
});
|
|
17263
|
+
}
|
|
16972
17264
|
/** Danh sách menu và thẻ tài khoản. Xem `AppNavConfig`. */
|
|
16973
17265
|
config = input.required(/* @ts-ignore */
|
|
16974
17266
|
...(ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []));
|
|
@@ -16988,9 +17280,28 @@ class OlAppNav {
|
|
|
16988
17280
|
*/
|
|
16989
17281
|
activeId = input(null, /* @ts-ignore */
|
|
16990
17282
|
...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
|
|
16991
|
-
/**
|
|
17283
|
+
/**
|
|
17284
|
+
* Logo — ba cách, theo thứ tự ưu tiên:
|
|
17285
|
+
*
|
|
17286
|
+
* 1. `<ng-template olAppNavLogo let-collapsed>` — tự vẽ hoàn toàn.
|
|
17287
|
+
* 2. `logoSrc` (+ `logoMarkSrc` khi thu gọn) — ảnh của nơi dùng.
|
|
17288
|
+
* 3. Mặc định: `ol-logo` của kit — mở thì bản DÀI, thu gọn thì bản VUÔNG.
|
|
17289
|
+
*/
|
|
16992
17290
|
logoSrc = input('', /* @ts-ignore */
|
|
16993
17291
|
...(ngDevMode ? [{ debugName: "logoSrc" }] : /* istanbul ignore next */ []));
|
|
17292
|
+
/**
|
|
17293
|
+
* Ảnh logo vuông khi thu gọn (khung 70px không đủ chỗ logo dài). Bỏ trống
|
|
17294
|
+
* thì thu gọn vẫn dùng `logoSrc`, co vừa ô 40px.
|
|
17295
|
+
*/
|
|
17296
|
+
logoMarkSrc = input('', /* @ts-ignore */
|
|
17297
|
+
...(ngDevMode ? [{ debugName: "logoMarkSrc" }] : /* istanbul ignore next */ []));
|
|
17298
|
+
/**
|
|
17299
|
+
* Tông logo của kit. Mặc định `auto`: chế độ sáng dùng logo MÀU, `.dark`
|
|
17300
|
+
* dùng logo trắng (người dùng chốt 2026-10-06 — bản trước mặc định `white`
|
|
17301
|
+
* ở cả hai chế độ). Muốn luôn trắng thì truyền `white`.
|
|
17302
|
+
*/
|
|
17303
|
+
logoTone = input('auto', /* @ts-ignore */
|
|
17304
|
+
...(ngDevMode ? [{ debugName: "logoTone" }] : /* istanbul ignore next */ []));
|
|
16994
17305
|
/** Chiều rộng khi mở. Figma: 270px. */
|
|
16995
17306
|
width = input('270px', /* @ts-ignore */
|
|
16996
17307
|
...(ngDevMode ? [{ debugName: "width" }] : /* istanbul ignore next */ []));
|
|
@@ -16999,8 +17310,51 @@ class OlAppNav {
|
|
|
16999
17310
|
...(ngDevMode ? [{ debugName: "collapsedWidth" }] : /* istanbul ignore next */ []));
|
|
17000
17311
|
/** Phát khi bấm một mục. Mục có `route` thì router đã lo điều hướng. */
|
|
17001
17312
|
itemClick = output();
|
|
17002
|
-
/**
|
|
17313
|
+
/**
|
|
17314
|
+
* Phát khi bấm thẻ tài khoản (cả khi menu tài khoản mở ra sau đó). Giữ để
|
|
17315
|
+
* tương thích các bản trước; chọn một mục trong menu thì nghe `userAction`.
|
|
17316
|
+
*/
|
|
17003
17317
|
userClick = output();
|
|
17318
|
+
/** Phát khi chọn một mục trong menu tài khoản — `profile`, `change-password`, `logout`… */
|
|
17319
|
+
userAction = output();
|
|
17320
|
+
/** Popup tài khoản (desktop) đang mở. Hai chiều: `[(userMenuOpen)]`. */
|
|
17321
|
+
userMenuOpen = model(false, /* @ts-ignore */
|
|
17322
|
+
...(ngDevMode ? [{ debugName: "userMenuOpen" }] : /* istanbul ignore next */ []));
|
|
17323
|
+
/**
|
|
17324
|
+
* Bố cục theo bề ngang. Mặc định `desktop` — giữ nguyên hành vi các bản
|
|
17325
|
+
* trước. `auto`: dưới `md` chuyển sang thanh đáy + khay menu. Xem `AppNavLayout`.
|
|
17326
|
+
*/
|
|
17327
|
+
layout = input('desktop', /* @ts-ignore */
|
|
17328
|
+
...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
|
|
17329
|
+
/**
|
|
17330
|
+
* Số mục lên thanh đáy mobile (đầu nhóm `main`); ô cuối luôn là nút Menu.
|
|
17331
|
+
*
|
|
17332
|
+
* Bốn: màn 375px chia 5 ô mỗi ô 75px — vừa icon 24px và một dòng nhãn 10px.
|
|
17333
|
+
* Sáu ô thì mỗi ô 62px, nhãn bắt đầu bị cắt.
|
|
17334
|
+
*/
|
|
17335
|
+
mobilePrimaryCount = input(4, /* @ts-ignore */
|
|
17336
|
+
...(ngDevMode ? [{ debugName: "mobilePrimaryCount" }] : /* istanbul ignore next */ []));
|
|
17337
|
+
/** Khay menu mobile đang mở. Hai chiều: `[(mobileMenuOpen)]` — khung đóng khi chuyển trang. */
|
|
17338
|
+
mobileMenuOpen = model(false, /* @ts-ignore */
|
|
17339
|
+
...(ngDevMode ? [{ debugName: "mobileMenuOpen" }] : /* istanbul ignore next */ []));
|
|
17340
|
+
/** Chữ trên nút Menu của thanh đáy. */
|
|
17341
|
+
mobileMenuLabel = input('Menu', /* @ts-ignore */
|
|
17342
|
+
...(ngDevMode ? [{ debugName: "mobileMenuLabel" }] : /* istanbul ignore next */ []));
|
|
17343
|
+
// ── Template truyền vào ─────────────────────────────────────────────
|
|
17344
|
+
itemDef = contentChild(OlAppNavItemDef, /* @ts-ignore */
|
|
17345
|
+
...(ngDevMode ? [{ debugName: "itemDef" }] : /* istanbul ignore next */ []));
|
|
17346
|
+
headerDef = contentChild(OlAppNavHeaderDef, /* @ts-ignore */
|
|
17347
|
+
...(ngDevMode ? [{ debugName: "headerDef" }] : /* istanbul ignore next */ []));
|
|
17348
|
+
footerDef = contentChild(OlAppNavFooterDef, /* @ts-ignore */
|
|
17349
|
+
...(ngDevMode ? [{ debugName: "footerDef" }] : /* istanbul ignore next */ []));
|
|
17350
|
+
logoDef = contentChild(OlAppNavLogoDef, /* @ts-ignore */
|
|
17351
|
+
...(ngDevMode ? [{ debugName: "logoDef" }] : /* istanbul ignore next */ []));
|
|
17352
|
+
userDef = contentChild(OlAppNavUserDef, /* @ts-ignore */
|
|
17353
|
+
...(ngDevMode ? [{ debugName: "userDef" }] : /* istanbul ignore next */ []));
|
|
17354
|
+
userMenuDef = contentChild(OlAppNavUserMenuDef, /* @ts-ignore */
|
|
17355
|
+
...(ngDevMode ? [{ debugName: "userMenuDef" }] : /* istanbul ignore next */ []));
|
|
17356
|
+
/** Id khay mobile — `aria-controls` của nút Menu trỏ vào. */
|
|
17357
|
+
sheetId = `ol-app-nav-sheet-${nextId++}`;
|
|
17004
17358
|
/**
|
|
17005
17359
|
* Hiện thanh kéo (rail) ở mép phải để thu gọn / mở lại.
|
|
17006
17360
|
*
|
|
@@ -17012,16 +17366,219 @@ class OlAppNav {
|
|
|
17012
17366
|
/** Chữ trên tooltip của thanh kéo, đổi theo trạng thái. */
|
|
17013
17367
|
railLabel = computed(() => (this.collapsed() ? 'Mở rộng' : 'Thu gọn'), /* @ts-ignore */
|
|
17014
17368
|
...(ngDevMode ? [{ debugName: "railLabel" }] : /* istanbul ignore next */ []));
|
|
17015
|
-
/**
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17369
|
+
/** Mọi nhóm, giữ thứ tự khai. */
|
|
17370
|
+
allGroups = computed(() => this.config().groups, /* @ts-ignore */
|
|
17371
|
+
...(ngDevMode ? [{ debugName: "allGroups" }] : /* istanbul ignore next */ []));
|
|
17372
|
+
/** Nhóm phần trên — vẽ theo nhóm để hiện `label`. */
|
|
17373
|
+
mainGroups = computed(() => this.allGroups().filter((g) => g.section === 'main' && g.items.length > 0), /* @ts-ignore */
|
|
17374
|
+
...(ngDevMode ? [{ debugName: "mainGroups" }] : /* istanbul ignore next */ []));
|
|
17375
|
+
/** Mục gốc của phần trên, phẳng (cho thanh đáy mobile). */
|
|
17376
|
+
mainItems = computed(() => this.mainGroups().flatMap((g) => g.items), /* @ts-ignore */
|
|
17019
17377
|
...(ngDevMode ? [{ debugName: "mainItems" }] : /* istanbul ignore next */ []));
|
|
17020
17378
|
/** Mục của phần dưới, nằm sát thẻ tài khoản. */
|
|
17021
17379
|
footerItems = computed(() => this.config()
|
|
17022
17380
|
.groups.filter((g) => g.section === 'footer')
|
|
17023
17381
|
.flatMap((g) => g.items), /* @ts-ignore */
|
|
17024
17382
|
...(ngDevMode ? [{ debugName: "footerItems" }] : /* istanbul ignore next */ []));
|
|
17383
|
+
/**
|
|
17384
|
+
* Mục lên thanh đáy mobile: `mobilePrimaryCount` mục gốc ĐẦU TIÊN của phần
|
|
17385
|
+
* trên mà có `route` và KHÔNG có con — ô thanh đáy là một cú chạm điều
|
|
17386
|
+
* hướng, không chứa được menu con. Mục cha vào khay.
|
|
17387
|
+
*/
|
|
17388
|
+
primaryItems = computed(() => this.mainItems()
|
|
17389
|
+
.filter((i) => !!i.route && !i.children?.length)
|
|
17390
|
+
.slice(0, this.mobilePrimaryCount()), /* @ts-ignore */
|
|
17391
|
+
...(ngDevMode ? [{ debugName: "primaryItems" }] : /* istanbul ignore next */ []));
|
|
17392
|
+
/**
|
|
17393
|
+
* Nhóm vào khay mobile: mọi nhóm (kể cả `footer` — dưới `md` không còn menu
|
|
17394
|
+
* dọc nào chứa chúng), bỏ các mục đã lên thanh đáy, giữ tiêu đề nhóm.
|
|
17395
|
+
*/
|
|
17396
|
+
sheetGroups = computed(() => {
|
|
17397
|
+
const onBar = new Set(this.primaryItems().map((i) => i.id));
|
|
17398
|
+
return [...this.mainGroups(), ...this.allGroups().filter((g) => g.section === 'footer')]
|
|
17399
|
+
.map((g) => ({ ...g, items: g.items.filter((i) => !onBar.has(i.id)) }))
|
|
17400
|
+
.filter((g) => g.items.length > 0);
|
|
17401
|
+
}, /* @ts-ignore */
|
|
17402
|
+
...(ngDevMode ? [{ debugName: "sheetGroups" }] : /* istanbul ignore next */ []));
|
|
17403
|
+
/** Phần còn lại, phẳng — giữ cho nơi dùng đã đọc trước đây. */
|
|
17404
|
+
overflowItems = computed(() => this.sheetGroups().flatMap((g) => g.items), /* @ts-ignore */
|
|
17405
|
+
...(ngDevMode ? [{ debugName: "overflowItems" }] : /* istanbul ignore next */ []));
|
|
17406
|
+
/**
|
|
17407
|
+
* Khoảng cách popup tài khoản → thẻ, để popup nằm HẲN ngoài sidebar, cách
|
|
17408
|
+
* mép 8px. Thẻ thụt trong sidebar: mở rộng đệm 16px (`px-4`, bản gọn);
|
|
17409
|
+
* thu gọn nút giữa khung 70px còn 15px. Đo 2026-10-06 sau bản gọn: khe 8px
|
|
17410
|
+
* ở cả hai trạng thái (offset 8 cố định từng làm popup chồng mép sidebar).
|
|
17411
|
+
*/
|
|
17412
|
+
userMenuOffset = computed(() => (this.collapsed() ? 23 : 24), /* @ts-ignore */
|
|
17413
|
+
...(ngDevMode ? [{ debugName: "userMenuOffset" }] : /* istanbul ignore next */ []));
|
|
17414
|
+
/** Menu tài khoản đã chốt: `user.actions`, hoặc mặc định khi không khai. */
|
|
17415
|
+
userActions = computed(() => this.config().user?.actions ?? APP_NAV_DEFAULT_USER_ACTIONS, /* @ts-ignore */
|
|
17416
|
+
...(ngDevMode ? [{ debugName: "userActions" }] : /* istanbul ignore next */ []));
|
|
17417
|
+
/** Class menu dọc theo `layout`. */
|
|
17418
|
+
railDisplay = computed(() => {
|
|
17419
|
+
switch (this.layout()) {
|
|
17420
|
+
case 'mobile':
|
|
17421
|
+
return 'hidden';
|
|
17422
|
+
case 'auto':
|
|
17423
|
+
return 'hidden md:flex';
|
|
17424
|
+
default:
|
|
17425
|
+
return 'flex';
|
|
17426
|
+
}
|
|
17427
|
+
}, /* @ts-ignore */
|
|
17428
|
+
...(ngDevMode ? [{ debugName: "railDisplay" }] : /* istanbul ignore next */ []));
|
|
17429
|
+
/** Có vẽ thanh đáy + khay không, và ẩn từ `md` khi `auto`. */
|
|
17430
|
+
hasMobile = computed(() => this.layout() !== 'desktop', /* @ts-ignore */
|
|
17431
|
+
...(ngDevMode ? [{ debugName: "hasMobile" }] : /* istanbul ignore next */ []));
|
|
17432
|
+
mobileDisplay = computed(() => (this.layout() === 'auto' ? 'md:hidden' : ''), /* @ts-ignore */
|
|
17433
|
+
...(ngDevMode ? [{ debugName: "mobileDisplay" }] : /* istanbul ignore next */ []));
|
|
17434
|
+
/** Ngữ cảnh template một mục. */
|
|
17435
|
+
itemContext(item, variant, level = 1) {
|
|
17436
|
+
return {
|
|
17437
|
+
$implicit: item,
|
|
17438
|
+
collapsed: variant === 'rail' && this.collapsed(),
|
|
17439
|
+
active: this.isItemActive(item),
|
|
17440
|
+
variant,
|
|
17441
|
+
level,
|
|
17442
|
+
expanded: this.isExpanded(item),
|
|
17443
|
+
};
|
|
17444
|
+
}
|
|
17445
|
+
/** Ngữ cảnh khe đầu / chân menu. */
|
|
17446
|
+
slotContext(variant) {
|
|
17447
|
+
const collapsed = variant === 'rail' && this.collapsed();
|
|
17448
|
+
return { $implicit: collapsed, collapsed, variant };
|
|
17449
|
+
}
|
|
17450
|
+
// ── Cây: đang chọn · nhánh mở ────────────────────────────────────────
|
|
17451
|
+
/** Bỏ query/hash và `/` cuối — `/a/b/?x=1#y` → `/a/b`. */
|
|
17452
|
+
static normalize(url) {
|
|
17453
|
+
const path = url.split(/[?#]/)[0].replace(/\/+$/, '');
|
|
17454
|
+
return path || '/';
|
|
17455
|
+
}
|
|
17456
|
+
/** `route` khớp URL hiện tại: đúng hệt, hoặc URL nằm dưới nó (`route/…`). `/` chỉ khớp đúng. */
|
|
17457
|
+
isRouteActive(route) {
|
|
17458
|
+
const current = OlAppNav.normalize(this.currentUrl());
|
|
17459
|
+
const target = OlAppNav.normalize(route);
|
|
17460
|
+
if (target === '/')
|
|
17461
|
+
return current === '/';
|
|
17462
|
+
return current === target || current.startsWith(target + '/');
|
|
17463
|
+
}
|
|
17464
|
+
/**
|
|
17465
|
+
* Mục đang chọn: `activeId` trùng, hoặc `route` / `activeRoutes` khớp URL.
|
|
17466
|
+
* Mục CHA không bao giờ "đang chọn" — trang nằm ở con; cha chỉ được đánh
|
|
17467
|
+
* dấu "chứa mục đang chọn" (`hasActiveChild`).
|
|
17468
|
+
*/
|
|
17469
|
+
isItemActive(item) {
|
|
17470
|
+
if (item.children?.length)
|
|
17471
|
+
return false;
|
|
17472
|
+
if (this.activeId() === item.id)
|
|
17473
|
+
return true;
|
|
17474
|
+
return [item.route, ...(item.activeRoutes ?? [])].some((r) => !!r && this.isRouteActive(r));
|
|
17475
|
+
}
|
|
17476
|
+
/** Mục có con (cháu…) đang chọn. */
|
|
17477
|
+
hasActiveChild(item) {
|
|
17478
|
+
return (item.children ?? []).some((c) => this.isItemActive(c) || this.hasActiveChild(c));
|
|
17479
|
+
}
|
|
17480
|
+
isExpanded(item) {
|
|
17481
|
+
return !!item.children?.length && this.expandedIds().has(item.id);
|
|
17482
|
+
}
|
|
17483
|
+
/**
|
|
17484
|
+
* Mở / đóng một nhánh (menu dọc đang mở, hoặc khay mobile).
|
|
17485
|
+
*
|
|
17486
|
+
* Kiểu ACCORDION — mỗi cấp chỉ một nhánh mở: mở một mục thì đóng các mục
|
|
17487
|
+
* CÙNG CẤP (kèm mọi nhánh con của chúng). Cấp 1 tính chung mọi nhóm, cả
|
|
17488
|
+
* nhóm `footer`; cấp 2 · 3 tính trong cùng mục cha. Menu dài (Quản trị
|
|
17489
|
+
* trường 5 nhóm) mà mở nhiều nhánh một lúc thì cuộn mãi không hết.
|
|
17490
|
+
*/
|
|
17491
|
+
toggleExpand(item) {
|
|
17492
|
+
this.expandedIds.update((set) => {
|
|
17493
|
+
const next = new Set(set);
|
|
17494
|
+
if (next.has(item.id)) {
|
|
17495
|
+
for (const id of AppNavTreeIds.subtree(item))
|
|
17496
|
+
next.delete(id);
|
|
17497
|
+
return next;
|
|
17498
|
+
}
|
|
17499
|
+
const roots = this.allGroups().flatMap((g) => g.items);
|
|
17500
|
+
for (const sibling of AppNavTreeIds.siblings(roots, item.id)) {
|
|
17501
|
+
if (sibling.id === item.id)
|
|
17502
|
+
continue;
|
|
17503
|
+
for (const id of AppNavTreeIds.subtree(sibling))
|
|
17504
|
+
next.delete(id);
|
|
17505
|
+
}
|
|
17506
|
+
next.add(item.id);
|
|
17507
|
+
return next;
|
|
17508
|
+
});
|
|
17509
|
+
}
|
|
17510
|
+
// ── Menu nổi khi thu gọn ─────────────────────────────────────────────
|
|
17511
|
+
//
|
|
17512
|
+
// Thu gọn 70px không có chỗ cho menu con. Rê chuột (hoặc bấm / Enter) vào
|
|
17513
|
+
// mục cha thì menu con hiện NỔI bên phải. Không mở rộng cả menu dọc.
|
|
17514
|
+
//
|
|
17515
|
+
// Tự giữ trạng thái mở thay vì `trigger="hover"` của `ol-dropdown`: panel
|
|
17516
|
+
// của nó treo ra `body`, nên chuột đi từ nút sang panel là rời vùng nghe
|
|
17517
|
+
// của dropdown và panel đóng giữa đường. Ở đây cả nút lẫn ruột panel (ruột
|
|
17518
|
+
// là template của chính component này) cùng huỷ hẹn giờ đóng.
|
|
17519
|
+
/** Id mục cha đang mở menu nổi. */
|
|
17520
|
+
flyoutId = signal(null, /* @ts-ignore */
|
|
17521
|
+
...(ngDevMode ? [{ debugName: "flyoutId" }] : /* istanbul ignore next */ []));
|
|
17522
|
+
flyoutCloseTimer = null;
|
|
17523
|
+
/** Rê vào nút hoặc panel: mở (và huỷ hẹn đóng). */
|
|
17524
|
+
openFlyout(id) {
|
|
17525
|
+
this.cancelFlyoutClose();
|
|
17526
|
+
this.flyoutId.set(id);
|
|
17527
|
+
}
|
|
17528
|
+
cancelFlyoutClose() {
|
|
17529
|
+
if (this.flyoutCloseTimer)
|
|
17530
|
+
clearTimeout(this.flyoutCloseTimer);
|
|
17531
|
+
this.flyoutCloseTimer = null;
|
|
17532
|
+
}
|
|
17533
|
+
/** Rời nút hoặc panel: đóng sau 150ms — đủ để chuột băng qua khe giữa hai cái. */
|
|
17534
|
+
scheduleFlyoutClose() {
|
|
17535
|
+
this.cancelFlyoutClose();
|
|
17536
|
+
this.flyoutCloseTimer = setTimeout(() => {
|
|
17537
|
+
this.flyoutCloseTimer = null;
|
|
17538
|
+
this.flyoutId.set(null);
|
|
17539
|
+
}, 150);
|
|
17540
|
+
}
|
|
17541
|
+
/** `ol-dropdown` báo mở/đóng (bấm nút, bấm ra ngoài, Esc). */
|
|
17542
|
+
onFlyoutOpenChange(id, open) {
|
|
17543
|
+
this.cancelFlyoutClose();
|
|
17544
|
+
this.flyoutId.set(open ? id : this.flyoutId() === id ? null : this.flyoutId());
|
|
17545
|
+
}
|
|
17546
|
+
/** Chọn một mục trong menu nổi: đóng menu, phát `itemClick` (router lo điều hướng). */
|
|
17547
|
+
onFlyoutSelect(item) {
|
|
17548
|
+
this.cancelFlyoutClose();
|
|
17549
|
+
this.flyoutId.set(null);
|
|
17550
|
+
this.onItemClick(item);
|
|
17551
|
+
}
|
|
17552
|
+
/** Id mọi mục cha nằm trên đường tới mục đang chọn. */
|
|
17553
|
+
activeAncestorIds(items) {
|
|
17554
|
+
const out = [];
|
|
17555
|
+
for (const item of items) {
|
|
17556
|
+
if (!item.children?.length)
|
|
17557
|
+
continue;
|
|
17558
|
+
if (this.hasActiveChild(item))
|
|
17559
|
+
out.push(item.id);
|
|
17560
|
+
out.push(...this.activeAncestorIds(item.children));
|
|
17561
|
+
}
|
|
17562
|
+
return out;
|
|
17563
|
+
}
|
|
17564
|
+
/** Ngữ cảnh template menu tài khoản — popup desktop và khay mobile. */
|
|
17565
|
+
userMenuPopupContext = computed(() => this.buildUserMenuContext('popup'), /* @ts-ignore */
|
|
17566
|
+
...(ngDevMode ? [{ debugName: "userMenuPopupContext" }] : /* istanbul ignore next */ []));
|
|
17567
|
+
userMenuSheetContext = computed(() => this.buildUserMenuContext('sheet'), /* @ts-ignore */
|
|
17568
|
+
...(ngDevMode ? [{ debugName: "userMenuSheetContext" }] : /* istanbul ignore next */ []));
|
|
17569
|
+
buildUserMenuContext(variant) {
|
|
17570
|
+
const user = this.config().user;
|
|
17571
|
+
if (!user)
|
|
17572
|
+
return null;
|
|
17573
|
+
return {
|
|
17574
|
+
$implicit: user,
|
|
17575
|
+
actions: this.userActions(),
|
|
17576
|
+
initials: this.userInitials(),
|
|
17577
|
+
select: (action) => this.onUserAction(action),
|
|
17578
|
+
close: () => variant === 'popup' ? this.userMenuOpen.set(false) : this.mobileMenuOpen.set(false),
|
|
17579
|
+
variant,
|
|
17580
|
+
};
|
|
17581
|
+
}
|
|
17025
17582
|
/** Đường kẻ chỉ vẽ khi phần dưới có mục — không thì thừa một vạch. */
|
|
17026
17583
|
showDivider = computed(() => this.footerItems().length > 0, /* @ts-ignore */
|
|
17027
17584
|
...(ngDevMode ? [{ debugName: "showDivider" }] : /* istanbul ignore next */ []));
|
|
@@ -17040,7 +17597,7 @@ class OlAppNav {
|
|
|
17040
17597
|
* Gộp vào MỘT chuỗi thay vì nhiều `[class.x]`: hai binding cùng tên thuộc
|
|
17041
17598
|
* tính thì cái sau thắng, cái trước mất im lặng.
|
|
17042
17599
|
*/
|
|
17043
|
-
itemClasses(item) {
|
|
17600
|
+
itemClasses(item, level = 1) {
|
|
17044
17601
|
/**
|
|
17045
17602
|
* `rounded-md` = 12px theo Figma `ol-app-sidebar-item` (#5301:338).
|
|
17046
17603
|
*
|
|
@@ -17058,13 +17615,46 @@ class OlAppNav {
|
|
|
17058
17615
|
* Nên khi thu gọn: đệm ngang về 0, `justify-center`, và mục rộng đúng ô
|
|
17059
17616
|
* vuông 44px cho đều với chiều cao.
|
|
17060
17617
|
*/
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17618
|
+
/**
|
|
17619
|
+
* GỌN (người dùng chọn 2026-10-06, lệch Figma 44px có chủ ý; bản 36px/14px
|
|
17620
|
+
* "bé quá"): MỌI cấp cao 40px (`h-10`), chữ 16px — cha và con bằng nhau.
|
|
17621
|
+
* Cấp 1 đệm ngang 10px; cấp 2 · 3 không icon, thụt 40 · 56px để chữ thẳng
|
|
17622
|
+
* hàng chữ cấp 1 (đệm 10 + icon 20 + khe 10 = 40). Thu gọn: ô vuông 40px.
|
|
17623
|
+
*/
|
|
17624
|
+
const base = level > 1
|
|
17625
|
+
? `flex h-10 items-center gap-2.5 rounded-md pe-2.5 text-base transition-colors ol-app-nav-item ${level === 2 ? 'ps-10' : 'ps-14'}`
|
|
17626
|
+
: this.collapsed()
|
|
17627
|
+
? 'mx-auto flex size-10 shrink-0 items-center justify-center rounded-md text-base transition-colors ol-app-nav-item'
|
|
17628
|
+
: 'flex h-10 items-center gap-2.5 rounded-md px-2.5 text-base transition-colors ol-app-nav-item';
|
|
17064
17629
|
if (item.disabled)
|
|
17065
17630
|
return `${base} opacity-40 pointer-events-none`;
|
|
17066
|
-
|
|
17067
|
-
|
|
17631
|
+
/**
|
|
17632
|
+
* Mục CHA — ba trạng thái, nền vàng luôn để dành cho trang đang xem:
|
|
17633
|
+
*
|
|
17634
|
+
* thu gọn + chứa trang đang xem tô như mục chọn (con bị giấu, chỉ còn
|
|
17635
|
+
* cha để chỉ chỗ)
|
|
17636
|
+
* đang MỞ giữ nền nổi + chữ đậm — người dùng
|
|
17637
|
+
* yêu cầu 2026-10-06: trước chỉ có nền
|
|
17638
|
+
* khi rê chuột, rời chuột là không thấy
|
|
17639
|
+
* nhánh nào đang mở. Cấp 1 nền
|
|
17640
|
+
* brand-600 (như hover); cấp 2 nền trắng
|
|
17641
|
+
* 10% để phân cấp
|
|
17642
|
+
* đóng + chứa trang đang xem chữ đậm
|
|
17643
|
+
*/
|
|
17644
|
+
if (item.children?.length) {
|
|
17645
|
+
if (this.collapsed() && level === 1 && this.hasActiveChild(item)) {
|
|
17646
|
+
return `${base} ol-app-nav-item-active font-medium`;
|
|
17647
|
+
}
|
|
17648
|
+
if (this.isExpanded(item) && !this.collapsed()) {
|
|
17649
|
+
return level === 1
|
|
17650
|
+
? `${base} ol-app-nav-item-open cursor-pointer font-semibold bg-[var(--bg-app-nav-hover,#5168ac)]`
|
|
17651
|
+
: `${base} ol-app-nav-item-open cursor-pointer font-semibold bg-white/10 hover:bg-[var(--bg-app-nav-hover,#5168ac)]`;
|
|
17652
|
+
}
|
|
17653
|
+
return this.hasActiveChild(item)
|
|
17654
|
+
? `${base} cursor-pointer font-semibold hover:bg-[var(--bg-app-nav-hover,#5168ac)]`
|
|
17655
|
+
: `${base} cursor-pointer hover:bg-[var(--bg-app-nav-hover,#5168ac)]`;
|
|
17656
|
+
}
|
|
17657
|
+
return this.isItemActive(item)
|
|
17068
17658
|
? `${base} ol-app-nav-item-active font-medium`
|
|
17069
17659
|
: `${base} cursor-pointer hover:bg-[var(--bg-app-nav-hover,#5168ac)]`;
|
|
17070
17660
|
}
|
|
@@ -17085,43 +17675,119 @@ class OlAppNav {
|
|
|
17085
17675
|
return lastTwo.map((w) => w[0]?.toUpperCase() ?? '').join('');
|
|
17086
17676
|
}, /* @ts-ignore */
|
|
17087
17677
|
...(ngDevMode ? [{ debugName: "userInitials" }] : /* istanbul ignore next */ []));
|
|
17678
|
+
/** Nút thẻ tài khoản ở đáy menu dọc. */
|
|
17679
|
+
/**
|
|
17680
|
+
* Nút thẻ tài khoản ở đáy menu dọc.
|
|
17681
|
+
*
|
|
17682
|
+
* Mở rộng: đệm phải 10px (`pe-2.5`, bằng đệm mục menu phía trên) — bản trước
|
|
17683
|
+
* `p-1` 4px làm mũi tên dính sát viền bo (người dùng chỉ ra 2026-10-06).
|
|
17684
|
+
* Đang mở menu tài khoản thì GIỮ nền nổi như mục cha đang mở.
|
|
17685
|
+
*/
|
|
17686
|
+
userCardClasses() {
|
|
17687
|
+
const open = this.userMenuOpen() ? ' bg-[var(--bg-app-nav-hover,#5168ac)]' : '';
|
|
17688
|
+
const base = 'group/user flex cursor-pointer items-center rounded-md transition-colors hover:bg-[var(--bg-app-nav-hover,#5168ac)]' +
|
|
17689
|
+
open;
|
|
17690
|
+
return this.collapsed()
|
|
17691
|
+
? `${base} justify-center p-1`
|
|
17692
|
+
: `${base} w-full gap-2.5 py-1.5 ps-1.5 pe-2.5 text-start`;
|
|
17693
|
+
}
|
|
17694
|
+
/** Một ô thanh đáy mobile. Mục có route thì `routerLinkActive` tô thêm `text-fg-brand`. */
|
|
17695
|
+
barItemClasses(item) {
|
|
17696
|
+
const base = 'flex min-w-0 flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 py-2 transition-colors';
|
|
17697
|
+
if (item.disabled)
|
|
17698
|
+
return `${base} opacity-40 pointer-events-none text-body-subtle`;
|
|
17699
|
+
return this.isItemActive(item)
|
|
17700
|
+
? `${base} text-fg-brand`
|
|
17701
|
+
: `${base} text-body-subtle hover:text-heading`;
|
|
17702
|
+
}
|
|
17703
|
+
/** Một dòng trong khay mobile. */
|
|
17704
|
+
sheetItemClasses(item, level = 1) {
|
|
17705
|
+
const indent = level === 1 ? 'ps-4' : level === 2 ? 'ps-12' : 'ps-16';
|
|
17706
|
+
const base = `flex w-full cursor-pointer items-center gap-3 py-3 pe-4 text-start transition-colors ${indent}`;
|
|
17707
|
+
if (item.disabled)
|
|
17708
|
+
return `${base} opacity-40 pointer-events-none text-body`;
|
|
17709
|
+
if (item.children?.length) {
|
|
17710
|
+
// Đang mở: nền xám nhạt + chữ đậm (cùng luật với menu dọc).
|
|
17711
|
+
if (this.isExpanded(item))
|
|
17712
|
+
return `${base} bg-neutral-secondary font-semibold text-heading`;
|
|
17713
|
+
return this.hasActiveChild(item)
|
|
17714
|
+
? `${base} font-semibold text-heading hover:bg-neutral-secondary`
|
|
17715
|
+
: `${base} text-body hover:bg-neutral-secondary`;
|
|
17716
|
+
}
|
|
17717
|
+
return this.isItemActive(item)
|
|
17718
|
+
? `${base} bg-brand-softer text-fg-brand`
|
|
17719
|
+
: `${base} text-body hover:bg-neutral-secondary`;
|
|
17720
|
+
}
|
|
17721
|
+
/**
|
|
17722
|
+
* Một mục menu tài khoản trong khay. Mục `danger` đổi CẢ nền lẫn chữ khi
|
|
17723
|
+
* hover: `text-fg-danger` trên `bg-danger-soft` chỉ đạt 4,09 (dưới AA 4,5),
|
|
17724
|
+
* nên chữ kéo xuống `fg-danger-strong` cho khớp nền.
|
|
17725
|
+
*/
|
|
17726
|
+
sheetActionClasses(action) {
|
|
17727
|
+
const base = 'flex w-full cursor-pointer items-center gap-3 px-4 py-3 text-start transition-colors';
|
|
17728
|
+
return action.danger
|
|
17729
|
+
? `${base} text-fg-danger hover:bg-danger-soft hover:text-fg-danger-strong`
|
|
17730
|
+
: `${base} text-body hover:bg-neutral-secondary`;
|
|
17731
|
+
}
|
|
17088
17732
|
onItemClick(item) {
|
|
17089
17733
|
if (item.disabled)
|
|
17090
17734
|
return;
|
|
17735
|
+
if (item.children?.length) {
|
|
17736
|
+
this.toggleExpand(item);
|
|
17737
|
+
return;
|
|
17738
|
+
}
|
|
17739
|
+
this.mobileMenuOpen.set(false);
|
|
17091
17740
|
this.itemClick.emit(item);
|
|
17092
17741
|
}
|
|
17742
|
+
/** Chọn một mục menu tài khoản — đóng popup/khay rồi phát cho nơi dùng. */
|
|
17743
|
+
onUserAction(action) {
|
|
17744
|
+
this.userMenuOpen.set(false);
|
|
17745
|
+
this.mobileMenuOpen.set(false);
|
|
17746
|
+
this.userAction.emit(action);
|
|
17747
|
+
}
|
|
17748
|
+
toggleMobileMenu() {
|
|
17749
|
+
this.mobileMenuOpen.update((v) => !v);
|
|
17750
|
+
}
|
|
17093
17751
|
toggle() {
|
|
17094
17752
|
this.collapsed.update((v) => !v);
|
|
17095
17753
|
}
|
|
17096
17754
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
17097
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlAppNav, isStandalone: true, selector: "ol-app-nav", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: true, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, activeId: { classPropertyName: "activeId", publicName: "activeId", isSignal: true, isRequired: false, transformFunction: null }, logoSrc: { classPropertyName: "logoSrc", publicName: "logoSrc", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, collapsedWidth: { classPropertyName: "collapsedWidth", publicName: "collapsedWidth", isSignal: true, isRequired: false, transformFunction: null }, showRail: { classPropertyName: "showRail", publicName: "showRail", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", itemClick: "itemClick", userClick: "userClick" }, host: { classAttribute: "block" }, ngImport: i0, template: "<!--\n Menu \u0111i\u1EC1u h\u01B0\u1EDBng \u00D4n Luy\u1EC7n \u2014 Figma `ol-app-nav` (#5303:30109).\n\n M\u00E0u l\u1EA5y t\u1EEB TOKEN, kh\u00F4ng ghim hex:\n\n --bg-app-nav-from \u0111i\u1EC3m \u0111\u1EA7u gradient (--color-navy-from)\n --bg-app-nav-to \u0111i\u1EC3m cu\u1ED1i (--color-navy-to)\n --bg-app-nav-hover n\u1EC1n m\u1EE5c khi r\u00EA chu\u1ED9t (--color-brand-600)\n\n B\u1EA3n \u0111\u1EA7u t\u00F4i ghim th\u1EB3ng `#445a9a` v\u00E0 `#0f2977` v\u00E0o \u0111\u00E2y v\u1EDBi l\u00FD do \"b\u1EA3n s\u1EAFc c\u1EE7a\n m\u1ED9t s\u1EA3n ph\u1EA9m, kh\u00F4ng ph\u1EA3i b\u1EADc m\u00E0u d\u00F9ng chung\". L\u00FD do \u0111\u00F3 sai ch\u1ED7: primitives l\u00E0\n n\u01A1i M\u1ECCI m\u00E0u g\u1ED1c n\u1EB1m, k\u1EC3 c\u1EA3 m\u00E0u ch\u1EC9 m\u1ED9t component d\u00F9ng. Ghim trong template\n th\u00EC `check:tokens` kh\u00F4ng th\u1EA5y (guard ch\u1EC9 qu\u00E9t class Tailwind b\u1EADc s\u1ED1, kh\u00F4ng\n qu\u00E9t `style=`), v\u00E0 \u0111\u1ED5i m\u00E0u ph\u1EA3i s\u1EEDa t\u1EEBng n\u01A1i d\u00F9ng.\n\n Gi\u00E1 tr\u1ECB d\u1EF1 ph\u00F2ng gi\u1EEF l\u1EA1i \u0111\u1EC3 component v\u1EABn v\u1EBD \u0111\u00FAng khi n\u01A1i d\u00F9ng qu\u00EAn nh\u1EADp\n `tokens.css` \u2014 \u0111\u00E3 x\u1EA3y ra t\u1EDBi b\u1EA3n 1.0.14, g\u00F3i publish kh\u00F4ng c\u00F3 file CSS n\u00E0o.\n\n B\u1ED1 c\u1EE5c `justify-between` chia ba v\u00F9ng nh\u01B0 Figma: ph\u1EA7n tr\u00EAn co theo n\u1ED9i dung,\n ph\u1EA7n d\u01B0\u1EDBi d\u00EDnh \u0111\u00E1y, kho\u1EA3ng tr\u1ED1ng \u1EDF gi\u1EEFa.\n-->\n<nav\n class=\"relative flex h-full flex-col justify-between rounded-2xl pt-6 transition-[width] duration-200\"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 13px khi thu g\u1ECDn, KH\u00D4NG ph\u1EA3i 12px: `<ul>` l\u00E0 flex-col n\u00EAn `<li>`\n k\u00E9o gi\u00E3n m\u1EE5c con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-11` kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 13\u00D72 = 44,\n \u0111\u00FAng \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c. \u0110\u1EC7m 12px cho ra 46\u00D744, l\u1EC7ch m\u1ED9t chi\u1EC1u.\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex flex-col gap-7 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoSrc()) {\n <img [src]=\"logoSrc()\" alt=\"\u00D4n Luy\u1EC7n\" class=\"h-10 w-auto max-w-full object-contain\" />\n } @else if (collapsed()) {\n <!--\n Thu g\u1ECDn: ch\u1EC9 ch\u1EEF t\u1EAFt. Logo \u0111\u1EA7y \u0111\u1EE7 r\u1ED9ng 155px (Figma) n\u00EAn \u1EDF khung 70px\n n\u00F3 tr\u00E0n h\u1EB3n ra ngo\u00E0i \u2014 \u0111o \u0111\u01B0\u1EE3c tr\u00EAn tr\u00ECnh duy\u1EC7t, ch\u1EEF \"\u00D4nluy\u1EC7n\" b\u1ECB c\u1EAFt\n gi\u1EEFa ch\u1EEBng \u1EDF m\u00E9p tr\u00E1i.\n -->\n <span class=\"text-2xl font-bold text-warning\" aria-label=\"\u00D4n Luy\u1EC7n\">\u00D4L</span>\n } @else {\n <!-- D\u1EF1 ph\u00F2ng khi ch\u01B0a truy\u1EC1n \u1EA3nh: ch\u1EEF gi\u1EEF \u0111\u00FAng hai m\u00E0u c\u1EE7a logo -->\n <span class=\"whitespace-nowrap text-2xl font-bold text-white\">\n \u00D4n<span class=\"text-warning\">luy\u1EC7n</span>\n </span>\n }\n </div>\n\n <ul class=\"flex flex-col gap-2\">\n @for (item of mainItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex flex-col gap-[7px] overflow-hidden rounded-b-2xl pb-5\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ul class=\"flex flex-col gap-[7px]\">\n @for (item of footerItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n\n @if (config().user; as user) {\n <button\n type=\"button\"\n class=\"mt-[30px] flex cursor-pointer items-center rounded-md transition-colors hover:bg-[var(--bg-app-nav-hover,#5168ac)]\"\n [class]=\"collapsed() ? 'mx-auto justify-center p-1' : 'w-full gap-2 p-1 text-start'\"\n (click)=\"userClick.emit()\"\n >\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n\n <!--\n Kh\u1ED1i t\u00EAn v\u00E0 email ch\u1EC9 hi\u1EC7n khi m\u1EDF. D\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class:\n \u1EA9n b\u1EB1ng `hidden` th\u00EC tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc, v\u00E0 ch\u1EEF v\u1EABn chi\u1EBFm ch\u1ED7\n trong t\u00EDnh to\u00E1n b\u1ED1 c\u1EE5c \u1EDF b\u1EC1 r\u1ED9ng 70px.\n -->\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs\">{{ user.subtitle }}</span>\n }\n </span>\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" class=\"shrink-0 text-white\" />\n }\n </button>\n }\n </div>\n</nav>\n\n<!--\n M\u1ED9t m\u1EE5c menu \u2014 d\u00F9ng chung cho c\u1EA3 ph\u1EA7n tr\u00EAn l\u1EABn ph\u1EA7n d\u01B0\u1EDBi.\n\n Ba nh\u00E1nh v\u00EC ba lo\u1EA1i h\u00E0nh vi kh\u00E1c nhau, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 `route` \u2192 `<a routerLink>` \u0111\u1EC3 router lo \u0111i\u1EC1u h\u01B0\u1EDBng v\u00E0 tr\u1EA1ng th\u00E1i active\n kh\u00F4ng route \u2192 `<button>` ch\u1EC9 ph\u00E1t s\u1EF1 ki\u1EC7n\n disabled \u2192 v\u1EABn `<button>` nh\u01B0ng `pointer-events-none` (lo trong class)\n-->\n<ng-template #navItem let-item>\n @if (item.route) {\n <a\n [routerLink]=\"item.route\"\n routerLinkActive=\"ol-app-nav-item-active font-medium\"\n [class]=\"itemClasses(item)\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </button>\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c: icon 24px + nh\u00E3n + huy hi\u1EC7u. Nh\u00E3n \u1EA9n khi thu g\u1ECDn. -->\n<ng-template #itemBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n\n @if (!collapsed()) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: OlIcon, selector: "ol-icon", inputs: ["name", "size", "color", "variant", "strokeWidth"] }, { kind: "component", type: OlAvatar, selector: "ol-avatar", inputs: ["src", "alt", "text", "initials", "size", "shape", "bordered", "status", "statusPosition", "removable", "name", "helper", "showHelper"], outputs: ["remove"] }, { kind: "component", type: OlBadge, selector: "ol-badge", inputs: ["color", "bordered", "theme", "size", "shape", "rounded", "type", "dot", "leftIcon", "rightIcon", "avatar", "avatarText", "secondaryText", "loading", "removable", "href"], outputs: ["dismiss"] }, { kind: "directive", type: OlTooltipDirective, selector: "[olTooltip]", inputs: ["olTooltip", "tooltipPosition", "tooltipTheme", "tooltipDelay", "tooltipDisabled", "tooltipArrow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
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 });
|
|
17098
17756
|
}
|
|
17099
17757
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlAppNav, decorators: [{
|
|
17100
17758
|
type: Component,
|
|
17101
17759
|
args: [{ selector: 'ol-app-nav', standalone: true, imports: [
|
|
17102
17760
|
NgTemplateOutlet,
|
|
17103
17761
|
RouterLink,
|
|
17104
|
-
RouterLinkActive,
|
|
17105
17762
|
OlIcon,
|
|
17763
|
+
OlLogo,
|
|
17106
17764
|
OlAvatar,
|
|
17107
17765
|
OlBadge,
|
|
17766
|
+
OlDropdown,
|
|
17767
|
+
OlDropdownItem,
|
|
17108
17768
|
OlTooltipDirective,
|
|
17109
17769
|
], host: {
|
|
17110
17770
|
class: 'block',
|
|
17111
|
-
}, 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=\"relative flex h-full flex-col justify-between rounded-2xl pt-6 transition-[width] duration-200\"\n style=\"\n background-image: linear-gradient(\n 164.52deg,\n var(--bg-app-nav-from, #445a9a) 0%,\n var(--bg-app-nav-to, #0f2977) 100%\n );\n \"\n [style.width]=\"navWidth()\"\n [attr.aria-label]=\"'Menu \u0111i\u1EC1u h\u01B0\u1EDBng'\"\n>\n <!--\n THANH K\u00C9O (rail) \u2014 thu g\u1ECDn / m\u1EDF l\u1EA1i b\u1EB1ng m\u1ED9t c\u00FA b\u1EA5m \u1EDF m\u00E9p ph\u1EA3i.\n\n D\u1EF1ng theo \u0111\u00FAng h\u00E0nh vi m\u1EABu ng\u01B0\u1EDDi d\u00F9ng \u0111\u01B0a:\n\n \u00B7 hai v\u1EA1ch d\u1ECDc 8\u00D72px, bo hai \u0111\u1EA7u ngo\u00E0i\n \u00B7 hover th\u00EC xoay \u00B140\u00B0 th\u00E0nh h\u00ECnh m\u0169i t\u00EAn, v\u00E0 \u0111\u1EADm l\u00EAn\n \u00B7 tooltip tr\u01B0\u1EE3t t\u1EEB tr\u00E1i sang, ch\u1EC9 hi\u1EC7n khi hover\n \u00B7 v\u00F9ng b\u1EA5m r\u1ED9ng 28px d\u00F9 v\u1EA1ch ch\u1EC9 2px \u2014 ng\u00F3n tay v\u00E0 chu\u1ED9t c\u1EA7n ch\u1ED7\n\n \u0110\u1EB7t TRONG `<nav>` v\u1EDBi `absolute` ch\u1EE9 kh\u00F4ng `fixed` nh\u01B0 m\u1EABu: `fixed` neo\n theo m\u00E0n h\u00ECnh n\u00EAn menu \u0111\u1EB7t \u1EDF \u0111\u00E2u n\u00F3 c\u0169ng nh\u1EA3y v\u1EC1 m\u00E9p tr\u00E1i. `absolute` b\u00E1m\n \u0111\u00FAng m\u00E9p ph\u1EA3i c\u1EE7a ch\u00EDnh menu, v\u00E0 `group/rail` cho hover \u0111i xuy\u00EAn xu\u1ED1ng hai\n v\u1EA1ch con.\n\n `hidden md:flex`: d\u01B0\u1EDBi 768px menu th\u01B0\u1EDDng chuy\u1EC3n sang d\u1EA1ng ng\u0103n k\u00E9o, thanh\n k\u00E9o kh\u00F4ng c\u00F2n ngh\u0129a.\n -->\n @if (showRail()) {\n <button\n type=\"button\"\n class=\"group/rail absolute end-0 top-1/2 z-20 hidden h-12 w-7 -translate-y-1/2 cursor-pointer items-center justify-center outline-none focus-visible:rounded-sm focus-visible:ring-2 focus-visible:ring-white/60 md:flex\"\n [attr.aria-label]=\"railLabel() + ' menu'\"\n [attr.aria-expanded]=\"!collapsed()\"\n (click)=\"toggle()\"\n >\n <span class=\"flex flex-col items-center\">\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-bottom rounded-t-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n <span\n aria-hidden=\"true\"\n class=\"block h-2 w-0.5 origin-top rounded-b-full bg-white/40 transition-all duration-100 ease-linear group-hover/rail:-rotate-[40deg] group-hover/rail:bg-white/70\"\n ></span>\n </span>\n\n <!--\n Tooltip t\u1EF1 d\u1EF1ng, kh\u00F4ng d\u00F9ng `olTooltip`: directive \u0111\u00F3 treo panel ra\n `.ol-overlay-container` n\u00EAn kh\u00F4ng ch\u1EA1y \u0111\u01B0\u1EE3c hi\u1EC7u \u1EE9ng tr\u01B0\u1EE3t g\u1EAFn v\u1EDBi\n `group-hover` c\u1EE7a ch\u00EDnh n\u00FAt.\n -->\n <span\n class=\"pointer-events-none absolute start-full -ms-1 -translate-x-0.5 whitespace-nowrap rounded-md border border-default bg-heading px-2 py-0.5 text-xs font-medium text-white opacity-0 shadow-xs transition-all duration-200 ease-out group-hover/rail:translate-x-0 group-hover/rail:opacity-100\"\n >\n {{ railLabel() }}\n </span>\n </button>\n }\n <!-- \u2500\u2500 Ph\u1EA7n tr\u00EAn: logo + menu ch\u00EDnh \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!--\n \u0110\u1EC7m ngang 13px khi thu g\u1ECDn, KH\u00D4NG ph\u1EA3i 12px: `<ul>` l\u00E0 flex-col n\u00EAn `<li>`\n k\u00E9o gi\u00E3n m\u1EE5c con h\u1EBFt b\u1EC1 r\u1ED9ng, `size-11` kh\u00F4ng th\u1EAFng \u0111\u01B0\u1EE3c. 70 \u2212 13\u00D72 = 44,\n \u0111\u00FAng \u00F4 vu\u00F4ng b\u1EB1ng chi\u1EC1u cao m\u1EE5c. \u0110\u1EC7m 12px cho ra 46\u00D744, l\u1EC7ch m\u1ED9t chi\u1EC1u.\n\n `overflow-hidden` \u0111\u1EB7t \u1EDF V\u00D9NG CON, kh\u00F4ng \u1EDF `<nav>`: `<nav>` ph\u1EA3i cho tooltip\n c\u1EE7a thanh k\u00E9o tr\u00E0n ra ngo\u00E0i m\u00E9p, nh\u01B0ng n\u1ED9i dung v\u1EABn c\u1EA7n b\u1ECB c\u1EAFt theo bo g\u00F3c\n 24px \u2014 nh\u00E3n d\u00E0i \u1EDF b\u1EC1 r\u1ED9ng 70px s\u1EBD th\u00F2 ra kh\u1ECFi khung bo.\n -->\n <div\n class=\"flex flex-col gap-7 overflow-hidden rounded-t-2xl\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n <div class=\"flex items-center justify-center\">\n @if (logoSrc()) {\n <img [src]=\"logoSrc()\" alt=\"\u00D4n Luy\u1EC7n\" class=\"h-10 w-auto max-w-full object-contain\" />\n } @else if (collapsed()) {\n <!--\n Thu g\u1ECDn: ch\u1EC9 ch\u1EEF t\u1EAFt. Logo \u0111\u1EA7y \u0111\u1EE7 r\u1ED9ng 155px (Figma) n\u00EAn \u1EDF khung 70px\n n\u00F3 tr\u00E0n h\u1EB3n ra ngo\u00E0i \u2014 \u0111o \u0111\u01B0\u1EE3c tr\u00EAn tr\u00ECnh duy\u1EC7t, ch\u1EEF \"\u00D4nluy\u1EC7n\" b\u1ECB c\u1EAFt\n gi\u1EEFa ch\u1EEBng \u1EDF m\u00E9p tr\u00E1i.\n -->\n <span class=\"text-2xl font-bold text-warning\" aria-label=\"\u00D4n Luy\u1EC7n\">\u00D4L</span>\n } @else {\n <!-- D\u1EF1 ph\u00F2ng khi ch\u01B0a truy\u1EC1n \u1EA3nh: ch\u1EEF gi\u1EEF \u0111\u00FAng hai m\u00E0u c\u1EE7a logo -->\n <span class=\"whitespace-nowrap text-2xl font-bold text-white\">\n \u00D4n<span class=\"text-warning\">luy\u1EC7n</span>\n </span>\n }\n </div>\n\n <ul class=\"flex flex-col gap-2\">\n @for (item of mainItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n </div>\n\n <!-- \u2500\u2500 Ph\u1EA7n d\u01B0\u1EDBi: menu ph\u1EE5 + th\u1EBB t\u00E0i kho\u1EA3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n class=\"flex flex-col gap-[7px] overflow-hidden rounded-b-2xl pb-5\"\n [class]=\"collapsed() ? 'px-[13px]' : 'px-5'\"\n >\n @if (showDivider()) {\n <!-- `aria-hidden`: \u0111\u01B0\u1EDDng k\u1EBB l\u00E0 d\u1EA5u hi\u1EC7u nh\u00ECn, tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh kh\u00F4ng c\u1EA7n -->\n <div class=\"h-px w-full bg-white/60\" aria-hidden=\"true\"></div>\n }\n\n <ul class=\"flex flex-col gap-[7px]\">\n @for (item of footerItems(); track item.id) {\n <li>\n <ng-container\n [ngTemplateOutlet]=\"navItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n />\n </li>\n }\n </ul>\n\n @if (config().user; as user) {\n <button\n type=\"button\"\n class=\"mt-[30px] flex cursor-pointer items-center rounded-md transition-colors hover:bg-[var(--bg-app-nav-hover,#5168ac)]\"\n [class]=\"collapsed() ? 'mx-auto justify-center p-1' : 'w-full gap-2 p-1 text-start'\"\n (click)=\"userClick.emit()\"\n >\n <ol-avatar\n size=\"base\"\n [src]=\"user.avatarSrc ?? ''\"\n [initials]=\"userInitials()\"\n [alt]=\"user.name\"\n class=\"shrink-0\"\n />\n\n <!--\n Kh\u1ED1i t\u00EAn v\u00E0 email ch\u1EC9 hi\u1EC7n khi m\u1EDF. D\u00F9ng `@if` ch\u1EE9 kh\u00F4ng \u1EA9n b\u1EB1ng class:\n \u1EA9n b\u1EB1ng `hidden` th\u00EC tr\u00ECnh \u0111\u1ECDc m\u00E0n h\u00ECnh v\u1EABn \u0111\u1ECDc, v\u00E0 ch\u1EEF v\u1EABn chi\u1EBFm ch\u1ED7\n trong t\u00EDnh to\u00E1n b\u1ED1 c\u1EE5c \u1EDF b\u1EC1 r\u1ED9ng 70px.\n -->\n @if (!collapsed()) {\n <span class=\"flex min-w-0 flex-1 flex-col gap-0.5 text-white\">\n <span class=\"truncate text-sm font-semibold\">{{ user.name }}</span>\n @if (user.subtitle) {\n <span class=\"truncate text-xs\">{{ user.subtitle }}</span>\n }\n </span>\n <ol-icon name=\"alt-arrow-down\" size=\"xs\" class=\"shrink-0 text-white\" />\n }\n </button>\n }\n </div>\n</nav>\n\n<!--\n M\u1ED9t m\u1EE5c menu \u2014 d\u00F9ng chung cho c\u1EA3 ph\u1EA7n tr\u00EAn l\u1EABn ph\u1EA7n d\u01B0\u1EDBi.\n\n Ba nh\u00E1nh v\u00EC ba lo\u1EA1i h\u00E0nh vi kh\u00E1c nhau, kh\u00F4ng g\u1ED9p \u0111\u01B0\u1EE3c:\n c\u00F3 `route` \u2192 `<a routerLink>` \u0111\u1EC3 router lo \u0111i\u1EC1u h\u01B0\u1EDBng v\u00E0 tr\u1EA1ng th\u00E1i active\n kh\u00F4ng route \u2192 `<button>` ch\u1EC9 ph\u00E1t s\u1EF1 ki\u1EC7n\n disabled \u2192 v\u1EABn `<button>` nh\u01B0ng `pointer-events-none` (lo trong class)\n-->\n<ng-template #navItem let-item>\n @if (item.route) {\n <a\n [routerLink]=\"item.route\"\n routerLinkActive=\"ol-app-nav-item-active font-medium\"\n [class]=\"itemClasses(item)\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </a>\n } @else {\n <button\n type=\"button\"\n [class]=\"itemClasses(item) + ' w-full text-start'\"\n [olTooltip]=\"collapsed() ? item.label : ''\"\n tooltipPosition=\"right\"\n [tooltipDisabled]=\"!collapsed()\"\n [attr.aria-label]=\"collapsed() ? item.label : null\"\n [disabled]=\"item.disabled ?? false\"\n (click)=\"onItemClick(item)\"\n >\n <ng-container [ngTemplateOutlet]=\"itemBody\" [ngTemplateOutletContext]=\"{ $implicit: item }\" />\n </button>\n }\n</ng-template>\n\n<!-- Ru\u1ED9t m\u1ED9t m\u1EE5c: icon 24px + nh\u00E3n + huy hi\u1EC7u. Nh\u00E3n \u1EA9n khi thu g\u1ECDn. -->\n<ng-template #itemBody let-item>\n <ol-icon [name]=\"item.icon\" size=\"base\" class=\"shrink-0 text-white\" />\n\n @if (!collapsed()) {\n <span class=\"min-w-0 flex-1 truncate text-white\">{{ item.label }}</span>\n\n @if (item.badge) {\n <ol-badge color=\"danger\" size=\"xs\" class=\"shrink-0\">{{ item.badge }}</ol-badge>\n }\n }\n</ng-template>\n" }]
|
|
17112
|
-
}], 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 }] }], 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"] }], showRail: [{ type: i0.Input, args: [{ isSignal: true, alias: "showRail", required: false }] }] } });
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
17123
|
-
|
|
17124
|
-
|
|
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 }] }] } });
|
|
17773
|
+
/** Tra cứu trong cây mục — tách khỏi component để đọc gọn. */
|
|
17774
|
+
const AppNavTreeIds = {
|
|
17775
|
+
/** Id của mục và mọi con cháu. */
|
|
17776
|
+
subtree(item) {
|
|
17777
|
+
return [item.id, ...(item.children ?? []).flatMap((c) => AppNavTreeIds.subtree(c))];
|
|
17778
|
+
},
|
|
17779
|
+
/** Danh sách anh em (cùng cấp, cùng cha) chứa mục `id`; rỗng nếu không thấy. */
|
|
17780
|
+
siblings(items, id) {
|
|
17781
|
+
if (items.some((i) => i.id === id))
|
|
17782
|
+
return items;
|
|
17783
|
+
for (const item of items) {
|
|
17784
|
+
const found = AppNavTreeIds.siblings(item.children ?? [], id);
|
|
17785
|
+
if (found.length)
|
|
17786
|
+
return found;
|
|
17787
|
+
}
|
|
17788
|
+
return [];
|
|
17789
|
+
},
|
|
17790
|
+
};
|
|
17125
17791
|
|
|
17126
17792
|
class OlSidebar {
|
|
17127
17793
|
collapsed = model(false, /* @ts-ignore */
|
|
@@ -19651,6 +20317,85 @@ class OlChart {
|
|
|
19651
20317
|
isItemHidden(name) {
|
|
19652
20318
|
return this.hiddenItems().has(name);
|
|
19653
20319
|
}
|
|
20320
|
+
// Zoom & Pan state for mobile/touch devices (amCharts 4 style)
|
|
20321
|
+
zoomScale = signal(1, /* @ts-ignore */
|
|
20322
|
+
...(ngDevMode ? [{ debugName: "zoomScale" }] : /* istanbul ignore next */ []));
|
|
20323
|
+
panX = signal(0, /* @ts-ignore */
|
|
20324
|
+
...(ngDevMode ? [{ debugName: "panX" }] : /* istanbul ignore next */ []));
|
|
20325
|
+
panY = signal(0, /* @ts-ignore */
|
|
20326
|
+
...(ngDevMode ? [{ debugName: "panY" }] : /* istanbul ignore next */ []));
|
|
20327
|
+
isPanning = false;
|
|
20328
|
+
lastPanX = 0;
|
|
20329
|
+
lastPanY = 0;
|
|
20330
|
+
showZoomControls = computed(() => {
|
|
20331
|
+
const chartType = this.type();
|
|
20332
|
+
// Hiển thị zoom controls cho tất cả biểu đồ có SVG plot area
|
|
20333
|
+
return (chartType === 'line' ||
|
|
20334
|
+
chartType === 'smooth-line' ||
|
|
20335
|
+
chartType === 'area' ||
|
|
20336
|
+
chartType === 'smooth-area' ||
|
|
20337
|
+
chartType === 'step-line' ||
|
|
20338
|
+
chartType === 'bar' ||
|
|
20339
|
+
chartType === 'stacked-bar' ||
|
|
20340
|
+
chartType === 'scatter' ||
|
|
20341
|
+
chartType === 'boxplot' ||
|
|
20342
|
+
chartType === 'radar' ||
|
|
20343
|
+
chartType === 'pie' ||
|
|
20344
|
+
chartType === 'donut' ||
|
|
20345
|
+
chartType === 'rose');
|
|
20346
|
+
}, /* @ts-ignore */
|
|
20347
|
+
...(ngDevMode ? [{ debugName: "showZoomControls" }] : /* istanbul ignore next */ []));
|
|
20348
|
+
svgTransform = computed(() => {
|
|
20349
|
+
const scale = this.zoomScale();
|
|
20350
|
+
const x = this.panX();
|
|
20351
|
+
const y = this.panY();
|
|
20352
|
+
return `translate(${x}, ${y}) scale(${scale})`;
|
|
20353
|
+
}, /* @ts-ignore */
|
|
20354
|
+
...(ngDevMode ? [{ debugName: "svgTransform" }] : /* istanbul ignore next */ []));
|
|
20355
|
+
zoomIn() {
|
|
20356
|
+
const current = this.zoomScale();
|
|
20357
|
+
const newScale = Math.min(current + 0.3, 3);
|
|
20358
|
+
console.log('Zoom In:', current, '->', newScale);
|
|
20359
|
+
this.zoomScale.set(newScale);
|
|
20360
|
+
}
|
|
20361
|
+
zoomOut() {
|
|
20362
|
+
const current = this.zoomScale();
|
|
20363
|
+
const newScale = Math.max(current - 0.3, 1);
|
|
20364
|
+
console.log('Zoom Out:', current, '->', newScale);
|
|
20365
|
+
this.zoomScale.set(newScale);
|
|
20366
|
+
// Reset pan nếu zoom về 1
|
|
20367
|
+
if (newScale === 1) {
|
|
20368
|
+
this.panX.set(0);
|
|
20369
|
+
this.panY.set(0);
|
|
20370
|
+
}
|
|
20371
|
+
}
|
|
20372
|
+
resetZoom() {
|
|
20373
|
+
console.log('Reset Zoom');
|
|
20374
|
+
this.zoomScale.set(1);
|
|
20375
|
+
this.panX.set(0);
|
|
20376
|
+
this.panY.set(0);
|
|
20377
|
+
}
|
|
20378
|
+
onPanStart(event) {
|
|
20379
|
+
if (this.zoomScale() <= 1)
|
|
20380
|
+
return;
|
|
20381
|
+
this.isPanning = true;
|
|
20382
|
+
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
|
20383
|
+
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
20384
|
+
this.lastPanX = clientX - this.panX();
|
|
20385
|
+
this.lastPanY = clientY - this.panY();
|
|
20386
|
+
}
|
|
20387
|
+
onPanMove(event) {
|
|
20388
|
+
if (!this.isPanning || this.zoomScale() <= 1)
|
|
20389
|
+
return;
|
|
20390
|
+
event.preventDefault();
|
|
20391
|
+
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
|
20392
|
+
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
20393
|
+
this.panX.set(clientX - this.lastPanX);
|
|
20394
|
+
this.panY.set(clientY - this.lastPanY);
|
|
20395
|
+
}
|
|
20396
|
+
onPanEnd() {
|
|
20397
|
+
this.isPanning = false;
|
|
20398
|
+
}
|
|
19654
20399
|
seriesLegendList = computed(() => {
|
|
19655
20400
|
return this.series().map((s, i) => ({
|
|
19656
20401
|
name: s.name,
|
|
@@ -20247,11 +20992,11 @@ class OlChart {
|
|
|
20247
20992
|
}, /* @ts-ignore */
|
|
20248
20993
|
...(ngDevMode ? [{ debugName: "horizontalBarsComputed" }] : /* istanbul ignore next */ []));
|
|
20249
20994
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
20250
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlChart, isStandalone: true, selector: "ol-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, categories: { classPropertyName: "categories", publicName: "categories", isSignal: true, isRequired: false, transformFunction: null }, series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: false, transformFunction: null }, dataPoints: { classPropertyName: "dataPoints", publicName: "dataPoints", isSignal: true, isRequired: false, transformFunction: null }, indicators: { classPropertyName: "indicators", publicName: "indicators", isSignal: true, isRequired: false, transformFunction: null }, scatterSeries: { classPropertyName: "scatterSeries", publicName: "scatterSeries", isSignal: true, isRequired: false, transformFunction: null }, heatmapData: { classPropertyName: "heatmapData", publicName: "heatmapData", isSignal: true, isRequired: false, transformFunction: null }, boxplotData: { classPropertyName: "boxplotData", publicName: "boxplotData", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, xAxisTitle: { classPropertyName: "xAxisTitle", publicName: "xAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, yAxisTitle: { classPropertyName: "yAxisTitle", publicName: "yAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 16/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n text-gray-700 dark:text-gray-300 -> text-body (3 l\u01B0\u1EE3t, 2026-09-24)\n\n Ba l\u01B0\u1EE3t cu\u1ED1i: ch\u00FA th\u00EDch c\u0169 x\u1EBFp ch\u00FAng v\u00E0o nh\u00F3m \"kh\u00F4ng token n\u00E0o kh\u1EDBp\", \u0111o l\u1EA1i\n th\u00EC `--text-body` \u0111\u1ED5i \u0110\u00DANG chi\u1EC1u #4a5565 -> #d1d5db, v\u00E0 gi\u00E1 tr\u1ECB t\u1ED1i TR\u00D9NG\n KH\u00CDT gray-300 c\u0169. S\u00E1ng nh\u1EA1t h\u01A1n m\u1ED9t b\u1EADc (#364153 -> #4a5565), t\u01B0\u01A1ng ph\u1EA3n\n tr\u00EAn n\u1EC1n tr\u1EAFng v\u1EABn 7,56 \u2014 d\u01B0 chu\u1EA9n AA 4,5.\n\n 35 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
20995
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: OlChart, isStandalone: true, selector: "ol-chart", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, headingLevel: { classPropertyName: "headingLevel", publicName: "headingLevel", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, categories: { classPropertyName: "categories", publicName: "categories", isSignal: true, isRequired: false, transformFunction: null }, series: { classPropertyName: "series", publicName: "series", isSignal: true, isRequired: false, transformFunction: null }, dataPoints: { classPropertyName: "dataPoints", publicName: "dataPoints", isSignal: true, isRequired: false, transformFunction: null }, indicators: { classPropertyName: "indicators", publicName: "indicators", isSignal: true, isRequired: false, transformFunction: null }, scatterSeries: { classPropertyName: "scatterSeries", publicName: "scatterSeries", isSignal: true, isRequired: false, transformFunction: null }, heatmapData: { classPropertyName: "heatmapData", publicName: "heatmapData", isSignal: true, isRequired: false, transformFunction: null }, boxplotData: { classPropertyName: "boxplotData", publicName: "boxplotData", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showLegend: { classPropertyName: "showLegend", publicName: "showLegend", isSignal: true, isRequired: false, transformFunction: null }, showGrid: { classPropertyName: "showGrid", publicName: "showGrid", isSignal: true, isRequired: false, transformFunction: null }, xAxisTitle: { classPropertyName: "xAxisTitle", publicName: "xAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, yAxisTitle: { classPropertyName: "yAxisTitle", publicName: "yAxisTitle", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 16/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n text-gray-700 dark:text-gray-300 -> text-body (3 l\u01B0\u1EE3t, 2026-09-24)\n\n Ba l\u01B0\u1EE3t cu\u1ED1i: ch\u00FA th\u00EDch c\u0169 x\u1EBFp ch\u00FAng v\u00E0o nh\u00F3m \"kh\u00F4ng token n\u00E0o kh\u1EDBp\", \u0111o l\u1EA1i\n th\u00EC `--text-body` \u0111\u1ED5i \u0110\u00DANG chi\u1EC1u #4a5565 -> #d1d5db, v\u00E0 gi\u00E1 tr\u1ECB t\u1ED1i TR\u00D9NG\n KH\u00CDT gray-300 c\u0169. S\u00E1ng nh\u1EA1t h\u01A1n m\u1ED9t b\u1EADc (#364153 -> #4a5565), t\u01B0\u01A1ng ph\u1EA3n\n tr\u00EAn n\u1EC1n tr\u1EAFng v\u1EABn 7,56 \u2014 d\u01B0 chu\u1EA9n AA 4,5.\n\n 35 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none relative\"\n>\n <!-- Zoom Controls (Mobile-friendly, amCharts 4 style) -->\n @if (showZoomControls()) {\n <div\n class=\"absolute top-2 right-2 z-50 flex flex-col gap-0.5 bg-white/90 dark:bg-gray-800/90 rounded shadow-md border border-default/50 p-0.5 backdrop-blur-sm touch-none\"\n style=\"pointer-events: auto;\"\n >\n <button\n type=\"button\"\n (click)=\"zoomIn(); $event.stopPropagation()\"\n [disabled]=\"zoomScale() >= 3\"\n class=\"w-6 h-6 flex items-center justify-center rounded hover:bg-neutral-secondary active:bg-neutral-tertiary disabled:opacity-30 disabled:cursor-not-allowed transition-colors text-body font-semibold text-sm touch-none\"\n title=\"Ph\u00F3ng to\"\n aria-label=\"Ph\u00F3ng to bi\u1EC3u \u0111\u1ED3\"\n >\n +\n </button>\n <button\n type=\"button\"\n (click)=\"zoomOut(); $event.stopPropagation()\"\n [disabled]=\"zoomScale() <= 1\"\n class=\"w-6 h-6 flex items-center justify-center rounded hover:bg-neutral-secondary active:bg-neutral-tertiary disabled:opacity-30 disabled:cursor-not-allowed transition-colors text-body font-semibold text-sm touch-none\"\n title=\"Thu nh\u1ECF\"\n aria-label=\"Thu nh\u1ECF bi\u1EC3u \u0111\u1ED3\"\n >\n \u2212\n </button>\n @if (zoomScale() > 1) {\n <button\n type=\"button\"\n (click)=\"resetZoom(); $event.stopPropagation()\"\n class=\"w-6 h-6 flex items-center justify-center rounded hover:bg-neutral-secondary active:bg-neutral-tertiary transition-colors text-body-subtle text-xs touch-none\"\n title=\"\u0110\u1EB7t l\u1EA1i\"\n aria-label=\"\u0110\u1EB7t l\u1EA1i zoom\"\n >\n \u27F2\n </button>\n }\n </div>\n }\n\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden touch-none\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <g [attr.transform]=\"svgTransform()\">\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden touch-none\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0 touch-none\">\n <svg \n class=\"w-full h-full transform -rotate-90\" \n viewBox=\"0 0 100 100\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </g>\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0 touch-none\">\n <svg \n class=\"w-full h-full\" \n viewBox=\"0 0 200 200\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </g>\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72 touch-none\">\n <svg \n viewBox=\"0 0 300 300\" \n class=\"w-full h-full overflow-visible\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden touch-none\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden touch-none\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
20251
20996
|
}
|
|
20252
20997
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: OlChart, decorators: [{
|
|
20253
20998
|
type: Component,
|
|
20254
|
-
args: [{ selector: 'ol-chart', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 16/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n text-gray-700 dark:text-gray-300 -> text-body (3 l\u01B0\u1EE3t, 2026-09-24)\n\n Ba l\u01B0\u1EE3t cu\u1ED1i: ch\u00FA th\u00EDch c\u0169 x\u1EBFp ch\u00FAng v\u00E0o nh\u00F3m \"kh\u00F4ng token n\u00E0o kh\u1EDBp\", \u0111o l\u1EA1i\n th\u00EC `--text-body` \u0111\u1ED5i \u0110\u00DANG chi\u1EC1u #4a5565 -> #d1d5db, v\u00E0 gi\u00E1 tr\u1ECB t\u1ED1i TR\u00D9NG\n KH\u00CDT gray-300 c\u0169. S\u00E1ng nh\u1EA1t h\u01A1n m\u1ED9t b\u1EADc (#364153 -> #4a5565), t\u01B0\u01A1ng ph\u1EA3n\n tr\u00EAn n\u1EC1n tr\u1EAFng v\u1EABn 7,56 \u2014 d\u01B0 chu\u1EA9n AA 4,5.\n\n 35 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none\"\n>\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0\">\n <svg class=\"w-full h-full\" viewBox=\"0 0 200 200\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72\">\n <svg viewBox=\"0 0 300 300\" class=\"w-full h-full overflow-visible\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n >\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"] }]
|
|
20999
|
+
args: [{ selector: 'ol-chart', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n M\u00E0u SVG c\u1EE7a chart. 16/51 l\u01B0\u1EE3t b\u1EADc s\u1ED1 \u0111\u00E3 \u0111\u1ED5i sang token (\u0111o getComputedStyle\n kh\u1EDBp C\u1EA2 HAI ch\u1EBF \u0111\u1ED9):\n\n text-gray-100 dark:text-gray-800 -> text-neutral-tertiary\n text-gray-200 dark:text-gray-800 -> text-default\n fill-gray-500 dark:fill-gray-400 -> fill-body-subtle\n fill-gray-600 dark:fill-gray-400 -> fill-body\n text-gray-700 dark:text-gray-300 -> text-body (3 l\u01B0\u1EE3t, 2026-09-24)\n\n Ba l\u01B0\u1EE3t cu\u1ED1i: ch\u00FA th\u00EDch c\u0169 x\u1EBFp ch\u00FAng v\u00E0o nh\u00F3m \"kh\u00F4ng token n\u00E0o kh\u1EDBp\", \u0111o l\u1EA1i\n th\u00EC `--text-body` \u0111\u1ED5i \u0110\u00DANG chi\u1EC1u #4a5565 -> #d1d5db, v\u00E0 gi\u00E1 tr\u1ECB t\u1ED1i TR\u00D9NG\n KH\u00CDT gray-300 c\u0169. S\u00E1ng nh\u1EA1t h\u01A1n m\u1ED9t b\u1EADc (#364153 -> #4a5565), t\u01B0\u01A1ng ph\u1EA3n\n tr\u00EAn n\u1EC1n tr\u1EAFng v\u1EABn 7,56 \u2014 d\u01B0 chu\u1EA9n AA 4,5.\n\n 35 l\u01B0\u1EE3t c\u00F2n l\u1EA1i GI\u1EEE b\u1EADc s\u1ED1 v\u00EC kh\u00F4ng token n\u00E0o kh\u1EDBp \u2014 ch\u00FAng \u0111\u1EA3o chi\u1EC1u ho\u1EB7c\n kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9, kh\u00E1c m\u1ECDi token hi\u1EC7n c\u00F3 (\u0111\u00E3 tra 20 token):\n\n fill-gray-400 / dark:fill-gray-500 t\u1ED1i \u0110\u1EACM h\u01A1n s\u00E1ng\n stroke-gray-300 / dark:stroke-gray-700 t\u1ED1i \u0111\u1EADm h\u01A1n s\u00E1ng\n fill-gray-600 / dark:fill-gray-300\n fill-gray-900, stroke-gray-900 kh\u00F4ng \u0111\u1ED5i theo ch\u1EBF \u0111\u1ED9\n\n L\u01B0u \u00FD: token @theme SINH \u0110\u01AF\u1EE2C `fill-*`/`stroke-*` b\u00ECnh th\u01B0\u1EDDng\n (`fill-body`, `stroke-default` \u0111\u1EC1u emit). L\u1EA7n \u0111o \u0111\u1EA7u ra #000000 ch\u1EC9 v\u00EC ch\u01B0a\n ai vi\u1EBFt ch\u00FAng trong m\u00E3 \u2014 Tailwind ch\u1EC9 emit class xu\u1EA5t hi\u1EC7n d\u1EA1ng ch\u1EEF.\n-->\n<div\n class=\"w-full bg-neutral-primary rounded-md border border-default p-5 shadow-sm space-y-4 font-sans select-none relative\"\n>\n <!-- Zoom Controls (Mobile-friendly, amCharts 4 style) -->\n @if (showZoomControls()) {\n <div\n class=\"absolute top-2 right-2 z-50 flex flex-col gap-0.5 bg-white/90 dark:bg-gray-800/90 rounded shadow-md border border-default/50 p-0.5 backdrop-blur-sm touch-none\"\n style=\"pointer-events: auto;\"\n >\n <button\n type=\"button\"\n (click)=\"zoomIn(); $event.stopPropagation()\"\n [disabled]=\"zoomScale() >= 3\"\n class=\"w-6 h-6 flex items-center justify-center rounded hover:bg-neutral-secondary active:bg-neutral-tertiary disabled:opacity-30 disabled:cursor-not-allowed transition-colors text-body font-semibold text-sm touch-none\"\n title=\"Ph\u00F3ng to\"\n aria-label=\"Ph\u00F3ng to bi\u1EC3u \u0111\u1ED3\"\n >\n +\n </button>\n <button\n type=\"button\"\n (click)=\"zoomOut(); $event.stopPropagation()\"\n [disabled]=\"zoomScale() <= 1\"\n class=\"w-6 h-6 flex items-center justify-center rounded hover:bg-neutral-secondary active:bg-neutral-tertiary disabled:opacity-30 disabled:cursor-not-allowed transition-colors text-body font-semibold text-sm touch-none\"\n title=\"Thu nh\u1ECF\"\n aria-label=\"Thu nh\u1ECF bi\u1EC3u \u0111\u1ED3\"\n >\n \u2212\n </button>\n @if (zoomScale() > 1) {\n <button\n type=\"button\"\n (click)=\"resetZoom(); $event.stopPropagation()\"\n class=\"w-6 h-6 flex items-center justify-center rounded hover:bg-neutral-secondary active:bg-neutral-tertiary transition-colors text-body-subtle text-xs touch-none\"\n title=\"\u0110\u1EB7t l\u1EA1i\"\n aria-label=\"\u0110\u1EB7t l\u1EA1i zoom\"\n >\n \u27F2\n </button>\n }\n </div>\n }\n\n <!-- Header Title -->\n @if (title()) {\n <div class=\"flex items-center justify-between\">\n <div>\n <!-- Ru\u1ED9t ti\u00EAu \u0111\u1EC1 d\u00F9ng chung cho s\u00E1u b\u1EADc: nh\u00E2n b\u1EA3n s\u00E1u l\u1EA7n th\u00EC m\u1ED7i l\u1EA7n\n s\u1EEDa ph\u1EA3i s\u1EEDa c\u1EA3 s\u00E1u, v\u00E0 d\u1EC5 l\u1EC7ch nhau. -->\n <ng-template #ruotTieuDe>\n <span>{{ title() }}</span>\n @if (unit()) {\n <span class=\"text-xs font-normal text-body-subtle\">({{ unit() }})</span>\n }\n </ng-template>\n @switch (headingLevel()) {\n @case (1) {\n <h1 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h1>\n }\n @case (2) {\n <h2 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h2>\n }\n @case (3) {\n <h3 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h3>\n }\n @case (5) {\n <h5 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h5>\n }\n @case (6) {\n <h6 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h6>\n }\n @default {\n <h4 class=\"font-bold text-sm text-heading flex items-center gap-2\">\n <ng-container [ngTemplateOutlet]=\"ruotTieuDe\" />\n </h4>\n }\n }\n @if (subtitle()) {\n <p class=\"text-xs text-body-subtle mt-0.5\">{{ subtitle() }}</p>\n }\n </div>\n </div>\n }\n\n <!-- 1. LINE / SMOOTH LINE / AREA / SMOOTH AREA / STEP LINE -->\n @if (\n type() === 'line' ||\n type() === 'smooth-line' ||\n type() === 'area' ||\n type() === 'smooth-area' ||\n type() === 'step-line'\n ) {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden touch-none\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <defs>\n @for (s of linePaths(); track s; let i = $index) {\n <linearGradient [id]=\"'area-grad-' + i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.35\" />\n <stop offset=\"100%\" [attr.stop-color]=\"s.color\" stop-opacity=\"0.02\" />\n </linearGradient>\n }\n </defs>\n\n <g [attr.transform]=\"svgTransform()\">\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <!-- Calculate Y pos -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Area Fills -->\n @for (s of linePaths(); track s; let i = $index) {\n @if (s.areaPathD) {\n <path\n [attr.d]=\"s.areaPathD\"\n [attr.fill]=\"'url(#area-grad-' + i + ')'\"\n class=\"transition-all duration-300 pointer-events-none\"\n />\n }\n }\n\n <!-- Line Strokes -->\n @for (s of linePaths(); track s) {\n <path\n [attr.d]=\"s.linePathD\"\n fill=\"none\"\n [attr.stroke]=\"s.color\"\n [attr.stroke-width]=\"s.strokeWidth\"\n [attr.stroke-dasharray]=\"s.strokeDasharray\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"transition-all duration-300\"\n />\n }\n\n <!-- Points and interactive dots -->\n @for (s of linePaths(); track s) {\n @for (p of s.points; track p) {\n <g class=\"group/point cursor-pointer\">\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"4\"\n [attr.fill]=\"s.color\"\n class=\"transition-all duration-200 stroke-white dark:stroke-gray-900 stroke-2 group-hover/point:r-6\"\n />\n <!-- SVG Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/point:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.x - 30\"\n [attr.y]=\"p.y - 32\"\n width=\"60\"\n height=\"22\"\n rx=\"5\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.x\"\n [attr.y]=\"p.y - 18\"\n text-anchor=\"middle\"\n class=\"text-2xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (cat of categories(); track cat; let i = $index) {\n <text\n [attr.x]=\"\n padding.left +\n (categories().length > 1\n ? (i / (categories().length - 1)) * innerWidth()\n : innerWidth() / 2)\n \"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ cat }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 2. VERTICAL COLUMN (BAR) & STACKED BAR -->\n @if (type() === 'bar' || type() === 'stacked-bar') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden touch-none\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Y-Axis Grid Lines & Labels -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"4 4\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- 2.1 Grouped Bar Columns -->\n @if (type() === 'bar') {\n @for (b of columnsComputed().bars; track b) {\n <g class=\"group/cbar cursor-pointer\">\n <rect\n [attr.x]=\"b.x\"\n [attr.y]=\"b.y\"\n [attr.width]=\"b.width\"\n [attr.height]=\"b.height\"\n [attr.fill]=\"b.color\"\n rx=\"3\"\n class=\"transition-all duration-300 hover:opacity-80\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/cbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.x + b.width / 2 - 45\"\n [attr.y]=\"b.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"b.x + b.width / 2\"\n [attr.y]=\"b.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ b.name }}: {{ b.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- 2.2 Stacked Bar Columns -->\n @if (type() === 'stacked-bar') {\n @for (seg of stackedColumnsComputed().segments; track seg) {\n <g class=\"group/sbar cursor-pointer\">\n <rect\n [attr.x]=\"seg.x\"\n [attr.y]=\"seg.y\"\n [attr.width]=\"seg.width\"\n [attr.height]=\"seg.height\"\n [attr.fill]=\"seg.color\"\n rx=\"2\"\n class=\"transition-all duration-300 hover:brightness-110\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sbar:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"seg.x + seg.width / 2 - 45\"\n [attr.y]=\"seg.y - 32\"\n width=\"90\"\n height=\"22\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"seg.x + seg.width / 2\"\n [attr.y]=\"seg.y - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </text>\n </g>\n </g>\n }\n }\n\n <!-- X-Axis Labels & X-Axis Title -->\n <g>\n @for (\n cat of type() === 'bar'\n ? columnsComputed().categories\n : stackedColumnsComputed().categories;\n track cat\n ) {\n <text\n [attr.x]=\"cat.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle font-medium\"\n >\n {{ cat.displayLabel }}\n </text>\n }\n\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 3. HORIZONTAL BAR / STACKED HORIZONTAL / DIVERGING BAR -->\n @if (\n type() === 'horizontal-bar' || type() === 'stacked-horizontal-bar' || type() === 'diverging-bar'\n ) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"space-y-3\">\n @for (item of horizontalBarsComputed(); track item) {\n <div class=\"space-y-1\">\n <!-- Category & Value Header -->\n <div class=\"flex items-center justify-between text-xs\">\n <span class=\"font-medium text-body\">{{ item.category }}</span>\n @if (item.isStacked) {\n <span class=\"text-xs font-bold text-heading\"> {{ item.total }}{{ unit() }} </span>\n }\n </div>\n\n <!-- 3.1 Stacked Horizontal -->\n @if (item.isStacked) {\n <div\n class=\"h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden flex relative\"\n >\n @for (seg of item.segments; track seg) {\n <div\n class=\"h-full transition-all duration-300 group/hseg relative\"\n [style.width.%]=\"seg.widthPct\"\n [style.background-color]=\"seg.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/hseg:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ seg.name }}: {{ seg.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.2 Diverging (Positive/Negative) -->\n @if (item.isDiverging) {\n <div class=\"relative h-4 w-full bg-neutral-tertiary rounded-full overflow-hidden\">\n <!-- Center Zero Line -->\n <div\n class=\"absolute top-0 bottom-0 w-0.5 bg-gray-400 dark:bg-gray-500 z-10\"\n [style.left.%]=\"item.zeroPct\"\n ></div>\n @for (itm of item.items; track itm) {\n <div\n class=\"absolute top-0 bottom-0 rounded-full transition-all duration-300 group/div\"\n [style.left.%]=\"itm.leftPct\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n >\n <div\n class=\"absolute -top-7 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-1.5 rounded opacity-0 group-hover/div:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ itm.name }}: {{ itm.value }}{{ unit() }}\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 3.3 Grouped Horizontal Bar -->\n @if (item.isGrouped) {\n <div class=\"space-y-1.5\">\n @for (itm of item.items; track itm) {\n <div class=\"flex items-center gap-3\">\n <div class=\"flex-1 h-3 bg-neutral-tertiary rounded-full overflow-hidden\">\n <div\n class=\"h-full rounded-full transition-all duration-300\"\n [style.width.%]=\"itm.widthPct\"\n [style.background-color]=\"itm.color\"\n ></div>\n </div>\n <span class=\"text-2xs font-semibold text-body w-12 text-right\">\n {{ itm.value }}{{ unit() }}\n </span>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n </div>\n }\n\n <!-- 4. PIE / DONUT / ROSE (Nightingale) -->\n @if (type() === 'pie' || type() === 'donut' || type() === 'rose') {\n <div class=\"flex flex-col sm:flex-row items-center justify-around gap-6 py-2\">\n <!-- Donut Standard -->\n @if (type() === 'donut') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0 touch-none\">\n <svg \n class=\"w-full h-full transform -rotate-90\" \n viewBox=\"0 0 100 100\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n @for (seg of donutSegments(); track seg) {\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n [attr.stroke]=\"seg.color\"\n stroke-width=\"10\"\n [attr.stroke-dasharray]=\"seg.strokeDasharray\"\n [attr.stroke-dashoffset]=\"seg.strokeDashoffset\"\n fill=\"transparent\"\n class=\"transition-all duration-500 cursor-pointer hover:stroke-12\"\n />\n }\n </g>\n </svg>\n <div class=\"absolute flex flex-col items-center justify-center text-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ totalDonutValue() }}</span>\n <span class=\"text-2xs text-body-subtle\">T\u1ED5ng c\u1ED9ng</span>\n </div>\n </div>\n }\n\n <!-- Pie / Rose SVG -->\n @if (type() === 'pie' || type() === 'rose') {\n <div class=\"relative w-44 h-44 flex items-center justify-center shrink-0 touch-none\">\n <svg \n class=\"w-full h-full\" \n viewBox=\"0 0 200 200\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n @for (slice of pieSlices(); track slice) {\n <g class=\"group/slice cursor-pointer\">\n <path\n [attr.d]=\"slice.pathD\"\n [attr.fill]=\"slice.color\"\n class=\"transition-all duration-300 opacity-90 group-hover/slice:opacity-100 stroke-white dark:stroke-gray-900 stroke-1\"\n />\n </g>\n }\n </g>\n </svg>\n </div>\n }\n\n <!-- Legend Breakdown Table (Click to filter / toggle) -->\n <div class=\"space-y-1.5 w-full max-w-xs\">\n @for (item of pieBreakdownItems(); track item) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(item.label)\"\n [attr.aria-pressed]=\"!item.hidden\"\n class=\"flex items-center justify-between text-xs cursor-pointer select-none py-1.5 px-2 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/pieleg\"\n [class.opacity-35]=\"item.hidden\"\n [class.line-through]=\"item.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n ph\u00E2n \u0111o\u1EA1n n\u00E0y\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n class=\"w-2.5 h-2.5 rounded-full shrink-0 transition-transform group-hover/pieleg:scale-125\"\n [style.background-color]=\"item.hidden ? 'var(--color-gray-400)' : item.color\"\n ></span>\n <span class=\"text-body truncate max-w-36 font-medium\">{{ item.label }}</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <span class=\"font-bold text-heading\">{{ item.value }}{{ unit() }}</span>\n <span class=\"text-2xs text-body-subtle w-9 text-right\">{{ item.percentage }}%</span>\n </div>\n </button>\n }\n </div>\n </div>\n }\n\n <!-- 5. RADAR CHART -->\n @if (type() === 'radar') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <div class=\"relative w-72 h-72 touch-none\">\n <svg \n viewBox=\"0 0 300 300\" \n class=\"w-full h-full overflow-visible\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n <!-- Grid Rings -->\n @for (ring of radarGridRings(); track ring) {\n <polygon\n [attr.points]=\"ring\"\n fill=\"none\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n stroke-dasharray=\"3 3\"\n />\n }\n\n <!-- Radar Radial Axes -->\n @for (axis of radarAxes(); track axis) {\n <g>\n <line\n [attr.x1]=\"axis.x1\"\n [attr.y1]=\"axis.y1\"\n [attr.x2]=\"axis.x2\"\n [attr.y2]=\"axis.y2\"\n stroke=\"currentColor\"\n class=\"text-default\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"axis.labelX\"\n [attr.y]=\"axis.labelY\"\n [attr.text-anchor]=\"axis.anchor\"\n class=\"text-2xs font-semibold fill-gray-600 dark:fill-gray-300\"\n >\n {{ axis.name }}\n </text>\n </g>\n }\n\n <!-- Series Polygons -->\n @for (s of radarSeriesPolygons(); track s) {\n <g>\n <polygon\n [attr.points]=\"s.polygonStr\"\n [attr.fill]=\"s.color\"\n fill-opacity=\"0.25\"\n [attr.stroke]=\"s.color\"\n stroke-width=\"2\"\n class=\"ol-radar-vung\"\n />\n <!-- Vertex Points -->\n @for (p of s.points; track p) {\n <circle\n [attr.cx]=\"p.x\"\n [attr.cy]=\"p.y\"\n r=\"3.5\"\n [attr.fill]=\"s.color\"\n class=\"stroke-white dark:stroke-gray-900 stroke-1.5 cursor-pointer\"\n />\n }\n </g>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 6. GAUGE / RADIAL CHART -->\n @if (type() === 'gauge' || type() === 'radial') {\n <div class=\"flex flex-col items-center justify-center py-2\">\n <!-- Semi-circle Gauge -->\n @if (type() === 'gauge') {\n <div class=\"relative w-48 h-32 overflow-hidden flex items-end justify-center\">\n <svg class=\"w-48 h-48 absolute top-0 transform rotate-180\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"141.37 282.74\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"141.37 282.74\"\n [attr.stroke-dashoffset]=\"gaugeOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"relative z-10 text-center mb-1\">\n <span class=\"text-3xl font-black text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-2xs text-body-subtle block\"\n >{{ value() }} / {{ max() }} {{ unit() || '\u0111i\u1EC3m' }}</span\n >\n </div>\n </div>\n }\n\n <!-- Full Radial Circle -->\n @if (type() === 'radial') {\n <div class=\"relative w-40 h-40 flex items-center justify-center\">\n <svg class=\"w-full h-full transform -rotate-90\" viewBox=\"0 0 100 100\">\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"currentColor\"\n stroke-width=\"10\"\n class=\"text-neutral-tertiary\"\n fill=\"transparent\"\n />\n <circle\n cx=\"50\"\n cy=\"50\"\n r=\"45\"\n stroke=\"var(--color-brand-700)\"\n stroke-width=\"10\"\n stroke-dasharray=\"282.74 282.74\"\n [attr.stroke-dashoffset]=\"radialOffset()\"\n stroke-linecap=\"round\"\n fill=\"transparent\"\n class=\"transition-all duration-700\"\n />\n </svg>\n <div class=\"absolute text-center flex flex-col items-center\">\n <span class=\"text-2xl font-bold text-heading\">{{ gaugePercentage() }}%</span>\n <span class=\"text-3xs text-body-subtle\">{{ value() }}/{{ max() }} {{ unit() }}</span>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- 7. SCATTER PLOT -->\n @if (type() === 'scatter') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden touch-none\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines & Y-Axis Labels -->\n @if (showGrid()) {\n <g>\n @for (tick of scatterDataComputed().yTicks; track tick) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"tick.y\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"tick.y\"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"tick.y + 4\"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400 dark:fill-gray-500 font-medium\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n }\n\n <!-- Scatter Points -->\n @for (s of scatterDataComputed().seriesList; track s) {\n @for (p of s.points; track p) {\n <g class=\"group/sp cursor-pointer\">\n <circle\n [attr.cx]=\"p.cx\"\n [attr.cy]=\"p.cy\"\n [attr.r]=\"p.r\"\n [attr.fill]=\"p.color\"\n class=\"opacity-80 group-hover/sp:opacity-100 transition-all stroke-white dark:stroke-gray-900 stroke-1 group-hover/sp:r-8\"\n />\n <!-- Tooltip -->\n <g\n class=\"opacity-0 group-hover/sp:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"p.cx - 45\"\n [attr.y]=\"p.cy - 34\"\n width=\"90\"\n height=\"24\"\n rx=\"4\"\n class=\"fill-gray-900/90 dark:fill-white/95\"\n />\n <text\n [attr.x]=\"p.cx\"\n [attr.y]=\"p.cy - 18\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white dark:fill-gray-900\"\n >\n {{ p.label || s.name }}: ({{ p.x }}, {{ p.y }})\n </text>\n </g>\n </g>\n }\n }\n\n <!-- Axes lines -->\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"padding.top\"\n [attr.x2]=\"padding.left\"\n [attr.y2]=\"padding.top + innerHeight()\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n\n <!-- X-Axis Ticks & Labels -->\n <g>\n @for (tick of scatterDataComputed().xTicks; track tick) {\n <line\n [attr.x1]=\"tick.x\"\n [attr.y1]=\"padding.top + innerHeight()\"\n [attr.x2]=\"tick.x\"\n [attr.y2]=\"padding.top + innerHeight() + 4\"\n class=\"stroke-gray-300 dark:stroke-gray-700\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"tick.x\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body-subtle\"\n >\n {{ tick.val }}\n </text>\n }\n </g>\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- 8. HEATMAP MATRIX -->\n @if (type() === 'heatmap' && heatmapComputed()) {\n <div class=\"w-full space-y-3 py-1\">\n <div class=\"overflow-x-auto\">\n <div class=\"min-w-fit space-y-2\">\n <!-- Top X-Labels -->\n <div class=\"flex items-center gap-1.5 pl-20\">\n @for (xLab of heatmapComputed()!.xLabels; track xLab) {\n <div class=\"w-9 text-center text-3xs font-semibold text-body-subtle\">\n {{ xLab }}\n </div>\n }\n </div>\n\n <!-- Matrix Rows -->\n @for (yLab of heatmapComputed()!.yLabels; track yLab; let r = $index) {\n <div class=\"flex items-center gap-1.5\">\n <span class=\"w-20 text-right text-2xs font-medium text-body pr-2 truncate\">\n {{ yLab }}\n </span>\n <div class=\"flex items-center gap-1.5\">\n @for (cell of heatmapComputed()!.cells; track cell) {\n @if (cell.row === r) {\n <div\n class=\"w-9 h-8 rounded-lg bg-brand transition-transform duration-200 hover:scale-110 cursor-pointer flex items-center justify-center group/cell relative\"\n [style.opacity]=\"cell.opacity\"\n >\n <!-- Tooltip -->\n <div\n class=\"absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 text-white text-2xs py-0.5 px-2 rounded opacity-0 group-hover/cell:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20 shadow\"\n >\n {{ cell.yLabel }} - {{ cell.xLabel }}: <b>{{ cell.value }}</b>\n {{ heatmapComputed()!.unit }}\n </div>\n </div>\n }\n }\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Heatmap legend bar -->\n <div class=\"flex items-center justify-end gap-2 pt-2 text-2xs text-body-subtle\">\n <span>\u00CDt</span>\n <div\n class=\"w-24 h-2 rounded bg-linear-to-r from-brand-100 via-brand-400 to-brand-700\"\n ></div>\n <span>Nhi\u1EC1u</span>\n </div>\n </div>\n }\n\n <!-- 9. BOXPLOT -->\n @if (type() === 'boxplot') {\n <div class=\"w-full\">\n <div class=\"relative w-full overflow-hidden touch-none\" data-plot-area [style.height.px]=\"height()\">\n <svg\n [attr.viewBox]=\"'0 0 ' + svgWidth + ' ' + svgHeight\"\n class=\"w-full h-full overflow-visible\"\n (mousedown)=\"onPanStart($event)\"\n (mousemove)=\"onPanMove($event)\"\n (mouseup)=\"onPanEnd()\"\n (mouseleave)=\"onPanEnd()\"\n (touchstart)=\"onPanStart($event)\"\n (touchmove)=\"onPanMove($event)\"\n (touchend)=\"onPanEnd()\"\n [style.cursor]=\"zoomScale() > 1 ? 'grab' : 'default'\"\n >\n <g [attr.transform]=\"svgTransform()\">\n <!-- Y-Axis Title -->\n @if (yAxisTitle()) {\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"14\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ yAxisTitle() }}\n </text>\n }\n\n <!-- Grid lines -->\n @if (showGrid()) {\n <g>\n @for (val of yGridSteps(); track val; let i = $index) {\n <line\n [attr.x1]=\"padding.left\"\n [attr.y1]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n [attr.x2]=\"padding.left + innerWidth()\"\n [attr.y2]=\"\n padding.top + innerHeight() - (i / (yGridSteps().length - 1)) * innerHeight()\n \"\n stroke=\"currentColor\"\n class=\"text-neutral-tertiary\"\n stroke-dasharray=\"3 3\"\n stroke-width=\"1\"\n />\n <text\n [attr.x]=\"padding.left - 8\"\n [attr.y]=\"\n padding.top +\n innerHeight() -\n (i / (yGridSteps().length - 1)) * innerHeight() +\n 4\n \"\n text-anchor=\"end\"\n class=\"text-2xs fill-gray-400\"\n >\n {{ val }}\n </text>\n }\n </g>\n }\n\n <!-- Boxplot Items -->\n @for (b of boxplotComputed(); track b) {\n <g class=\"group/box cursor-pointer\">\n <!-- Whisker Vertical Line (Min to Max) -->\n <line\n [attr.x1]=\"b.centerX\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Min Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMin\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMin\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Max Cap -->\n <line\n [attr.x1]=\"b.centerX - 8\"\n [attr.y1]=\"b.yMax\"\n [attr.x2]=\"b.centerX + 8\"\n [attr.y2]=\"b.yMax\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n />\n <!-- Box (Q1 to Q3) -->\n <rect\n [attr.x]=\"b.boxX\"\n [attr.y]=\"b.yQ3\"\n [attr.width]=\"b.boxWidth\"\n [attr.height]=\"b.yQ1 - b.yQ3\"\n [attr.fill]=\"b.color\"\n fill-opacity=\"0.2\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"1.5\"\n rx=\"3\"\n class=\"transition-all group-hover/box:fill-opacity-40\"\n />\n <!-- Median Divider Line -->\n <line\n [attr.x1]=\"b.boxX\"\n [attr.y1]=\"b.yMedian\"\n [attr.x2]=\"b.boxX + b.boxWidth\"\n [attr.y2]=\"b.yMedian\"\n [attr.stroke]=\"b.color\"\n stroke-width=\"2.5\"\n />\n\n <!-- Category label -->\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"padding.top + innerHeight() + 16\"\n text-anchor=\"middle\"\n class=\"text-2xs fill-body font-medium\"\n >\n {{ b.label }}\n </text>\n\n <!-- Hover Tooltip -->\n <g\n class=\"opacity-0 group-hover/box:opacity-100 transition-opacity pointer-events-none z-20\"\n >\n <rect\n [attr.x]=\"b.centerX - 55\"\n [attr.y]=\"b.yMax - 38\"\n width=\"110\"\n height=\"34\"\n rx=\"5\"\n class=\"fill-gray-900/95 text-white\"\n />\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 24\"\n text-anchor=\"middle\"\n class=\"text-3xs font-bold fill-white\"\n >\n {{ b.label }}\n </text>\n <text\n [attr.x]=\"b.centerX\"\n [attr.y]=\"b.yMax - 10\"\n text-anchor=\"middle\"\n class=\"text-3xs fill-gray-300\"\n >\n Min:{{ b.raw.min }} | Med:{{ b.raw.median }} | Max:{{ b.raw.max }}\n </text>\n </g>\n </g>\n }\n\n <!-- X-Axis Title -->\n @if (xAxisTitle()) {\n <text\n [attr.x]=\"svgWidth - 8\"\n [attr.y]=\"svgHeight - 6\"\n text-anchor=\"end\"\n [attr.font-size]=\"8\"\n class=\"text-3xs font-normal fill-gray-400 dark:fill-gray-500\"\n >\n {{ xAxisTitle() }}\n </text>\n }\n </g>\n </svg>\n </div>\n </div>\n }\n\n <!-- Series Legend Footer (Click to filter / toggle series like amCharts 4) -->\n @if (\n showLegend() &&\n series().length > 1 &&\n type() !== 'radar' &&\n type() !== 'pie' &&\n type() !== 'donut' &&\n type() !== 'rose'\n ) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of seriesLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/leg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/leg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'radar' && series().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of radarLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/rleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/rleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n\n @if (showLegend() && type() === 'scatter' && scatterSeries().length > 0) {\n <div\n class=\"flex flex-wrap items-center justify-center gap-2 pt-3 border-t border-default-subtle\"\n >\n @for (s of scatterLegendList(); track s) {\n <button\n type=\"button\"\n (click)=\"toggleLegendItem(s.name)\"\n [attr.aria-pressed]=\"!s.hidden\"\n class=\"flex items-center gap-2 text-xs cursor-pointer select-none py-1 px-2.5 rounded-lg transition-all hover:bg-neutral-tertiary dark:hover:bg-dark active:scale-95 group/sleg\"\n [class.opacity-35]=\"s.hidden\"\n [class.line-through]=\"s.hidden\"\n [class.text-body-subtle]=\"s.hidden\"\n [class.dark:text-body-subtle]=\"s.hidden\"\n [class.text-body]=\"!s.hidden\"\n title=\"Nh\u1EA5p \u0111\u1EC3 \u1EA9n/hi\u1EC7n chu\u1ED7i n\u00E0y tr\u00EAn bi\u1EC3u \u0111\u1ED3\"\n >\n <span\n class=\"w-2.5 h-2.5 rounded-full transition-transform group-hover/sleg:scale-125\"\n [style.background-color]=\"s.hidden ? 'var(--color-gray-400)' : s.color\"\n ></span>\n <span class=\"font-medium\">{{ s.name }}</span>\n </button>\n }\n </div>\n }\n</div>\n", styles: [".ol-radar-vung{transition:fill-opacity .3s}.ol-radar-vung:hover{fill-opacity:.4}\n"] }]
|
|
20255
21000
|
}], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], headingLevel: [{ type: i0.Input, args: [{ isSignal: true, alias: "headingLevel", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], categories: [{ type: i0.Input, args: [{ isSignal: true, alias: "categories", required: false }] }], series: [{ type: i0.Input, args: [{ isSignal: true, alias: "series", required: false }] }], dataPoints: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataPoints", required: false }] }], indicators: [{ type: i0.Input, args: [{ isSignal: true, alias: "indicators", required: false }] }], scatterSeries: [{ type: i0.Input, args: [{ isSignal: true, alias: "scatterSeries", required: false }] }], heatmapData: [{ type: i0.Input, args: [{ isSignal: true, alias: "heatmapData", required: false }] }], boxplotData: [{ type: i0.Input, args: [{ isSignal: true, alias: "boxplotData", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], showLegend: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLegend", required: false }] }], showGrid: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGrid", required: false }] }], xAxisTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "xAxisTitle", required: false }] }], yAxisTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "yAxisTitle", required: false }] }], unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: false }] }] } });
|
|
20256
21001
|
|
|
20257
21002
|
/** Nhãn hiển thị + icon cho từng trạng thái. */
|
|
@@ -20502,5 +21247,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
20502
21247
|
* Generated bundle index. Do not edit.
|
|
20503
21248
|
*/
|
|
20504
21249
|
|
|
20505
|
-
export { 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_ICONS, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppNav, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, 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, 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 };
|
|
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 };
|
|
20506
21251
|
//# sourceMappingURL=edmicro-olkit.mjs.map
|