@foxeltech/angular-ui 0.4.0 → 0.7.75
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/archivo/archivo-400-latin-ext.woff2 +0 -0
- package/archivo/archivo-400-latin.woff2 +0 -0
- package/archivo/archivo-400-vietnamese.woff2 +0 -0
- package/archivo/archivo-500-latin-ext.woff2 +0 -0
- package/archivo/archivo-500-latin.woff2 +0 -0
- package/archivo/archivo-500-vietnamese.woff2 +0 -0
- package/archivo/archivo-600-latin-ext.woff2 +0 -0
- package/archivo/archivo-600-latin.woff2 +0 -0
- package/archivo/archivo-600-vietnamese.woff2 +0 -0
- package/archivo/archivo-700-latin-ext.woff2 +0 -0
- package/archivo/archivo-700-latin.woff2 +0 -0
- package/archivo/archivo-700-vietnamese.woff2 +0 -0
- package/components.css +332 -30
- package/dialogs.css +2 -2
- package/familjen-grotesk/familjen-grotesk-400-latin-ext.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-400-latin.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-400-vietnamese.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-500-latin-ext.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-500-latin.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-500-vietnamese.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-600-latin-ext.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-600-latin.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-600-vietnamese.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-700-latin-ext.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-700-latin.woff2 +0 -0
- package/familjen-grotesk/familjen-grotesk-700-vietnamese.woff2 +0 -0
- package/fesm2022/foxeltech-angular-ui-chart.mjs +381 -40
- package/fesm2022/foxeltech-angular-ui-chart.mjs.map +1 -1
- package/fesm2022/foxeltech-angular-ui-flow.mjs +176 -3
- package/fesm2022/foxeltech-angular-ui-flow.mjs.map +1 -1
- package/fesm2022/foxeltech-angular-ui-rich-text.mjs +2 -2
- package/fesm2022/foxeltech-angular-ui-rich-text.mjs.map +1 -1
- package/fesm2022/foxeltech-angular-ui.mjs +1327 -151
- package/fesm2022/foxeltech-angular-ui.mjs.map +1 -1
- package/fonts.css +558 -133
- package/ibm-plex-mono/ibm-plex-mono-400-cyrillic-ext.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-400-cyrillic.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-400-latin-ext.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-400-latin.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-400-vietnamese.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-500-cyrillic-ext.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-500-cyrillic.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-500-latin-ext.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-500-latin.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-500-vietnamese.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-600-cyrillic-ext.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-600-cyrillic.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-600-latin-ext.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-600-latin.woff2 +0 -0
- package/ibm-plex-mono/ibm-plex-mono-600-vietnamese.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-400-cyrillic-ext.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-400-cyrillic.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-400-greek.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-400-latin-ext.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-400-latin.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-400-vietnamese.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-500-cyrillic-ext.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-500-cyrillic.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-500-greek.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-500-latin-ext.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-500-latin.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-500-vietnamese.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-600-cyrillic-ext.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-600-cyrillic.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-600-greek.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-600-latin-ext.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-600-latin.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-600-vietnamese.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-700-cyrillic-ext.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-700-cyrillic.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-700-greek.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-700-latin-ext.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-700-latin.woff2 +0 -0
- package/jetbrains-mono/jetbrains-mono-700-vietnamese.woff2 +0 -0
- package/newsreader/newsreader-400-latin-ext.woff2 +0 -0
- package/newsreader/newsreader-400-latin.woff2 +0 -0
- package/newsreader/newsreader-400-vietnamese.woff2 +0 -0
- package/newsreader/newsreader-500-latin-ext.woff2 +0 -0
- package/newsreader/newsreader-500-latin.woff2 +0 -0
- package/newsreader/newsreader-500-vietnamese.woff2 +0 -0
- package/newsreader/newsreader-600-latin-ext.woff2 +0 -0
- package/newsreader/newsreader-600-latin.woff2 +0 -0
- package/newsreader/newsreader-600-vietnamese.woff2 +0 -0
- package/package.json +5 -2
- package/source-serif-4/source-serif-4-400-cyrillic-ext.woff2 +0 -0
- package/source-serif-4/source-serif-4-400-cyrillic.woff2 +0 -0
- package/source-serif-4/source-serif-4-400-greek.woff2 +0 -0
- package/source-serif-4/source-serif-4-400-latin-ext.woff2 +0 -0
- package/source-serif-4/source-serif-4-400-latin.woff2 +0 -0
- package/source-serif-4/source-serif-4-400-vietnamese.woff2 +0 -0
- package/source-serif-4/source-serif-4-600-cyrillic-ext.woff2 +0 -0
- package/source-serif-4/source-serif-4-600-cyrillic.woff2 +0 -0
- package/source-serif-4/source-serif-4-600-greek.woff2 +0 -0
- package/source-serif-4/source-serif-4-600-latin-ext.woff2 +0 -0
- package/source-serif-4/source-serif-4-600-latin.woff2 +0 -0
- package/source-serif-4/source-serif-4-600-vietnamese.woff2 +0 -0
- package/src/lib/styles/components.css +332 -30
- package/src/lib/styles/dialogs.css +2 -2
- package/src/lib/styles/fonts.css +558 -133
- package/src/lib/styles/tailwind.css +13 -1
- package/src/lib/styles/theme.css +642 -69
- package/src/lib/styles/tokens.css +81 -19
- package/src/lib/styles/utilities.css +16 -1
- package/src/lib/ui/components/bar-list/bar-list.component.html +29 -0
- package/src/lib/ui/components/button/button.component.html +9 -5
- package/src/lib/ui/components/checkbox/checkbox.component.html +1 -1
- package/src/lib/ui/components/code-block/code-block.component.html +8 -0
- package/src/lib/ui/components/drawer/drawer.component.html +35 -12
- package/src/lib/ui/components/drawer/drawer.component.scss +106 -18
- package/src/lib/ui/components/facets-rail/facets-rail.component.html +37 -0
- package/src/lib/ui/components/filter-pills/filter-pills.component.html +16 -0
- package/src/lib/ui/components/kanban-board/kanban-board.component.scss +9 -9
- package/src/lib/ui/components/list-row/list-row.component.html +20 -0
- package/src/lib/ui/components/master-detail/master-detail.component.css +79 -5
- package/src/lib/ui/components/master-detail/master-detail.component.html +57 -7
- package/src/lib/ui/components/pending-badge/pending-badge.component.html +15 -0
- package/src/lib/ui/components/pipeline-stepper/pipeline-stepper.component.html +45 -0
- package/src/lib/ui/components/project-card/project-card.component.html +102 -0
- package/src/lib/ui/components/radio-button/radio-button.component.html +1 -1
- package/src/lib/ui/components/radio-button-toggle/radio-button-toggle.component.html +1 -1
- package/src/lib/ui/components/route-skeleton/route-skeleton.component.css +38 -0
- package/src/lib/ui/components/route-skeleton/route-skeleton.component.html +184 -0
- package/src/lib/ui/components/sample-frame/sample-frame.component.html +18 -0
- package/src/lib/ui/components/score-badge/score-badge.component.html +7 -0
- package/src/lib/ui/components/screenshot-frame/screenshot-frame.component.html +40 -0
- package/src/lib/ui/components/section-card/section-card.component.html +15 -6
- package/src/lib/ui/components/segmented/segmented.component.html +42 -0
- package/src/lib/ui/components/segmented/segmented.component.scss +12 -0
- package/src/lib/ui/components/skeleton-dashboard/skeleton-dashboard.component.html +24 -0
- package/src/lib/ui/components/skeleton-detail/skeleton-detail.component.html +8 -8
- package/src/lib/ui/components/skeleton-facets/skeleton-facets.component.html +15 -0
- package/src/lib/ui/components/skeleton-frame/skeleton-frame.component.html +59 -0
- package/src/lib/ui/components/skeleton-list/skeleton-list.component.html +6 -6
- package/src/lib/ui/components/slider/slider.component.html +2 -2
- package/src/lib/ui/components/sparkbar/sparkbar.component.html +14 -0
- package/src/lib/ui/components/stage-dots/stage-dots.component.html +13 -0
- package/src/lib/ui/components/stat-cards/stat-cards.component.html +44 -11
- package/src/lib/ui/components/status-dot/status-dot.component.html +13 -0
- package/src/lib/ui/components/switch/switch.component.html +1 -1
- package/src/lib/ui/components/tab-group/tab-group.component.html +37 -8
- package/src/lib/ui/components/tab-group/tab-group.component.scss +41 -1
- package/src/lib/ui/components/tag/tag.component.html +2 -2
- package/src/lib/ui/components/theme-picker/theme-picker.component.html +44 -0
- package/src/lib/ui/components/toast/toast.component.html +6 -6
- package/src/lib/ui/components/tree-diagram/tree-diagram.component.html +3 -3
- package/tailwind.css +13 -1
- package/theme.css +642 -69
- package/tokens.css +81 -19
- package/types/foxeltech-angular-ui-chart.d.ts +90 -3
- package/types/foxeltech-angular-ui-flow.d.ts +83 -1
- package/types/foxeltech-angular-ui.d.ts +909 -48
- package/utilities.css +16 -1
- package/BeVietnamPro/BeVietnamPro-Bold.woff2 +0 -0
- package/BeVietnamPro/BeVietnamPro-ExtraLight.woff2 +0 -0
- package/BeVietnamPro/BeVietnamPro-Light.woff2 +0 -0
- package/BeVietnamPro/BeVietnamPro-Medium.woff2 +0 -0
- package/BeVietnamPro/BeVietnamPro-Regular.woff2 +0 -0
- package/BeVietnamPro/BeVietnamPro-SemiBold.woff2 +0 -0
- package/BeVietnamPro/BeVietnamPro-Thin.woff2 +0 -0
- package/SpaceMono-Regular.woff2 +0 -0
- package/dm-sans/DMSans-Bold.woff2 +0 -0
- package/dm-sans/DMSans-Light.woff2 +0 -0
- package/dm-sans/DMSans-Medium.woff2 +0 -0
- package/dm-sans/DMSans-Regular.woff2 +0 -0
- package/dm-sans/DMSans-SemiBold.woff2 +0 -0
- package/dm-sans/DMSans-Thin.woff2 +0 -0
- package/orbitron/Orbitron-Bold.woff2 +0 -0
- package/orbitron/Orbitron-Medium.woff2 +0 -0
- package/orbitron/Orbitron-Regular.woff2 +0 -0
- package/orbitron/Orbitron-SemiBold.woff2 +0 -0
- package/roboto/Roboto-Black.woff2 +0 -0
- package/roboto/Roboto-BlackItalic.woff2 +0 -0
- package/roboto/Roboto-Bold.woff2 +0 -0
- package/roboto/Roboto-BoldCondensed.woff2 +0 -0
- package/roboto/Roboto-BoldCondensedItalic.woff2 +0 -0
- package/roboto/Roboto-BoldItalic.woff2 +0 -0
- package/roboto/Roboto-Condensed.woff2 +0 -0
- package/roboto/Roboto-CondensedItalic.woff2 +0 -0
- package/roboto/Roboto-Italic.woff2 +0 -0
- package/roboto/Roboto-Light.woff2 +0 -0
- package/roboto/Roboto-LightItalic.woff2 +0 -0
- package/roboto/Roboto-Medium.woff2 +0 -0
- package/roboto/Roboto-MediumItalic.woff2 +0 -0
- package/roboto/Roboto-Regular.woff2 +0 -0
- package/roboto/Roboto-Thin.woff2 +0 -0
- package/roboto/Roboto-ThinItalic.woff2 +0 -0
- package/src/lib/ui/components/skeleton-table-loading/skeleton-table-loading.component.html +0 -21
- package/src/lib/ui/components/skeleton-table-loading/skeleton-table-loading.component.scss +0 -0
- package/src/lib/ui/components/tab-component/tab.component.html +0 -1
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, DestroyRef, Injector, Directive, InjectionToken, input, isDevMode, ChangeDetectionStrategy, Component, Input, createComponent, Injectable, viewChild, ViewChild, Inject, EventEmitter, NgModule, HostListener, output, ChangeDetectorRef, TemplateRef, contentChildren, Output, ViewChildren,
|
|
2
|
+
import { inject, DestroyRef, Injector, Directive, InjectionToken, input, isDevMode, ChangeDetectionStrategy, Component, Input, createComponent, Injectable, viewChild, ViewChild, Inject, EventEmitter, signal, computed, NgModule, HostListener, output, ChangeDetectorRef, contentChild, TemplateRef, contentChildren, Output, ViewChildren, ElementRef, model } from '@angular/core';
|
|
3
3
|
import { MatPaginator } from '@angular/material/paginator';
|
|
4
4
|
import { MatTableDataSource, MatTable } from '@angular/material/table';
|
|
5
5
|
import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
|
|
6
6
|
import { Observable, firstValueFrom, Subject, debounceTime, distinctUntilChanged, BehaviorSubject, throwError, filter, takeUntil } from 'rxjs';
|
|
7
|
-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
7
|
+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
8
8
|
import * as i2 from '@angular/common/http';
|
|
9
9
|
import { HttpRequest, HttpClient } from '@angular/common/http';
|
|
10
10
|
import * as i1 from '@angular/material/dialog';
|
|
11
11
|
import { MAT_DIALOG_DATA, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialog } from '@angular/material/dialog';
|
|
12
|
+
import * as i2$1 from '@angular/router';
|
|
13
|
+
import { Router, RoutesRecognized, NavigationEnd, NavigationCancel, NavigationError, RouterLink, RouterModule } from '@angular/router';
|
|
12
14
|
import * as i1$1 from '@ngx-translate/core';
|
|
13
15
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
|
14
16
|
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
|
15
17
|
import * as i1$3 from '@angular/forms';
|
|
16
18
|
import { NgControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
17
|
-
import * as i2$1 from '@angular/router';
|
|
18
|
-
import { NavigationEnd, RouterLink, RouterModule } from '@angular/router';
|
|
19
19
|
import { catchError } from 'rxjs/operators';
|
|
20
20
|
import * as signalR from '@microsoft/signalr';
|
|
21
21
|
import { CdkScrollable } from '@angular/cdk/scrolling';
|
|
@@ -108,11 +108,11 @@ class ToastComponent {
|
|
|
108
108
|
message = input('', ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
|
|
109
109
|
type = input(...(ngDevMode ? [undefined, { debugName: "type" }] : /* istanbul ignore next */ []));
|
|
110
110
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ToastComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
111
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ToastComponent, isStandalone: true, selector: "fx-ui-toast", inputs: { toastId: { classPropertyName: "toastId", publicName: "toastId", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: false, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n animate.enter=\"horizontal-fade-in-animation\"\n animate.leave=\"horizontal-fade-out-animation\"\n [attr.role]=\"type() === 'error' ? 'alert' : 'status'\"\n class=\"flex border-l-[3px] items-center bg-bg-primary gap-semi min-w-[250px] max-w-[400px] px-large py-semi rounded shadow-lg transition-all duration-300 ease-in-out transform opacity-100 translate-y-0\"\n [ngClass]=\"{\n 'border-success': type() === 'success',\n 'border-
|
|
111
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ToastComponent, isStandalone: true, selector: "fx-ui-toast", inputs: { toastId: { classPropertyName: "toastId", publicName: "toastId", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: false, isRequired: false, transformFunction: null }, message: { classPropertyName: "message", publicName: "message", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div\n animate.enter=\"horizontal-fade-in-animation\"\n animate.leave=\"horizontal-fade-out-animation\"\n [attr.role]=\"type() === 'error' ? 'alert' : 'status'\"\n class=\"flex border-l-[3px] items-center bg-bg-primary gap-semi min-w-[250px] max-w-[400px] px-large py-semi rounded shadow-lg transition-all duration-300 ease-in-out transform opacity-100 translate-y-0\"\n [ngClass]=\"{\n 'tone-border-success': type() === 'success',\n 'tone-border-danger': type() === 'error',\n 'border-warning': type() === 'warning',\n 'border-information': type() === 'info'\n }\"\n >\n <div class=\"\" aria-hidden=\"true\">\n @switch (type()) { @case('success') {\n <fx-ui-hero-icon\n icon=\"check-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-success\"\n ></fx-ui-hero-icon>\n } @case('error') {\n <fx-ui-hero-icon\n icon=\"exclamation-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-danger\"\n ></fx-ui-hero-icon>\n } @case('warning') {\n <fx-ui-hero-icon\n icon=\"exclamation-triangle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-warning\"\n ></fx-ui-hero-icon>\n } @default {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-information\"\n ></fx-ui-hero-icon>\n } }\n </div>\n\n <div class=\"flex flex-col\">\n @if (title) {\n <strong class=\"txt-field-label\">{{ title }}</strong>\n }\n <p class=\"txt-default\">{{ message() }}</p>\n </div>\n </div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
112
112
|
}
|
|
113
113
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ToastComponent, decorators: [{
|
|
114
114
|
type: Component,
|
|
115
|
-
args: [{ selector: 'fx-ui-toast', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<div\n animate.enter=\"horizontal-fade-in-animation\"\n animate.leave=\"horizontal-fade-out-animation\"\n [attr.role]=\"type() === 'error' ? 'alert' : 'status'\"\n class=\"flex border-l-[3px] items-center bg-bg-primary gap-semi min-w-[250px] max-w-[400px] px-large py-semi rounded shadow-lg transition-all duration-300 ease-in-out transform opacity-100 translate-y-0\"\n [ngClass]=\"{\n 'border-success': type() === 'success',\n 'border-
|
|
115
|
+
args: [{ selector: 'fx-ui-toast', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<div\n animate.enter=\"horizontal-fade-in-animation\"\n animate.leave=\"horizontal-fade-out-animation\"\n [attr.role]=\"type() === 'error' ? 'alert' : 'status'\"\n class=\"flex border-l-[3px] items-center bg-bg-primary gap-semi min-w-[250px] max-w-[400px] px-large py-semi rounded shadow-lg transition-all duration-300 ease-in-out transform opacity-100 translate-y-0\"\n [ngClass]=\"{\n 'tone-border-success': type() === 'success',\n 'tone-border-danger': type() === 'error',\n 'border-warning': type() === 'warning',\n 'border-information': type() === 'info'\n }\"\n >\n <div class=\"\" aria-hidden=\"true\">\n @switch (type()) { @case('success') {\n <fx-ui-hero-icon\n icon=\"check-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-success\"\n ></fx-ui-hero-icon>\n } @case('error') {\n <fx-ui-hero-icon\n icon=\"exclamation-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-danger\"\n ></fx-ui-hero-icon>\n } @case('warning') {\n <fx-ui-hero-icon\n icon=\"exclamation-triangle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-warning\"\n ></fx-ui-hero-icon>\n } @default {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [solid]=\"true\"\n [size]=\"30\"\n class=\"tone-fg-information\"\n ></fx-ui-hero-icon>\n } }\n </div>\n\n <div class=\"flex flex-col\">\n @if (title) {\n <strong class=\"txt-field-label\">{{ title }}</strong>\n }\n <p class=\"txt-default\">{{ message() }}</p>\n </div>\n </div>\n" }]
|
|
116
116
|
}], propDecorators: { toastId: [{ type: i0.Input, args: [{ isSignal: true, alias: "toastId", required: true }] }], title: [{
|
|
117
117
|
type: Input
|
|
118
118
|
}], message: [{ type: i0.Input, args: [{ isSignal: true, alias: "message", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }] } });
|
|
@@ -412,8 +412,16 @@ class FxUtils {
|
|
|
412
412
|
}
|
|
413
413
|
return value ? `rgba(${value} / ${alpha})` : `rgba(255 255 255 / ${alpha})`;
|
|
414
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Status/severity string → tag tone.
|
|
417
|
+
*
|
|
418
|
+
* Coerces instead of trusting the argument: every caller feeds this raw API
|
|
419
|
+
* values, and a numeric or null field used to throw
|
|
420
|
+
* "key.toLowerCase is not a function" and take the whole screen down.
|
|
421
|
+
* Non-string input simply falls through to the default tone.
|
|
422
|
+
*/
|
|
415
423
|
static getTagClass(key, isCriticalExist = true) {
|
|
416
|
-
switch (key.toLowerCase()) {
|
|
424
|
+
switch (String(key ?? '').toLowerCase()) {
|
|
417
425
|
case 'error':
|
|
418
426
|
case 'failed':
|
|
419
427
|
case 'inactive':
|
|
@@ -796,6 +804,9 @@ class BaseTableComponent extends BaseComponent {
|
|
|
796
804
|
autoFocus: 'first-tabbable',
|
|
797
805
|
width: size,
|
|
798
806
|
disableClose: conf.disableClose ?? false,
|
|
807
|
+
// Dialogs are created by the root MatDialog: without this the dialog
|
|
808
|
+
// component cannot see route-scoped providers (APIs) — NG0201.
|
|
809
|
+
injector: this.injector,
|
|
799
810
|
data: {
|
|
800
811
|
method: 'create',
|
|
801
812
|
data: this.empty(),
|
|
@@ -831,6 +842,7 @@ class BaseTableComponent extends BaseComponent {
|
|
|
831
842
|
autoFocus: 'first-tabbable',
|
|
832
843
|
width: size,
|
|
833
844
|
disableClose: conf.disableClose ?? false,
|
|
845
|
+
injector: this.injector,
|
|
834
846
|
data: {
|
|
835
847
|
method: 'update',
|
|
836
848
|
data: model,
|
|
@@ -866,6 +878,7 @@ class BaseTableComponent extends BaseComponent {
|
|
|
866
878
|
autoFocus: 'first-tabbable',
|
|
867
879
|
width: size,
|
|
868
880
|
disableClose: conf.disableClose ?? false,
|
|
881
|
+
injector: this.injector,
|
|
869
882
|
data: {
|
|
870
883
|
data: model,
|
|
871
884
|
title: conf.title,
|
|
@@ -894,6 +907,7 @@ class BaseTableComponent extends BaseComponent {
|
|
|
894
907
|
panelClass: 'fx-dialog',
|
|
895
908
|
autoFocus: 'first-tabbable',
|
|
896
909
|
width: '400px',
|
|
910
|
+
injector: this.injector,
|
|
897
911
|
data: {
|
|
898
912
|
title: conf?.title,
|
|
899
913
|
message: conf?.message,
|
|
@@ -1169,15 +1183,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
1169
1183
|
args: [MAT_DIALOG_DATA]
|
|
1170
1184
|
}] }, { type: FxToastrService }, { type: HttpWrapper }, { type: i1.MatDialog }], propDecorators: { ngForm: [{ type: i0.ViewChild, args: ['f', { isSignal: true }] }] } });
|
|
1171
1185
|
|
|
1186
|
+
/**
|
|
1187
|
+
* Overlay chặn màn cho HÀNH ĐỘNG của người dùng (bấm nút, đổi trạng thái,
|
|
1188
|
+
* xuất báo cáo…). `app.html` của host bind `fx-ui-loading-panel` vào
|
|
1189
|
+
* `listenerAction`.
|
|
1190
|
+
*
|
|
1191
|
+
* ⚠️ **KHÔNG dùng cho điều hướng.** Channel `listener` + `start()`/`stop()` đã
|
|
1192
|
+
* bị XOÁ ở 0.7.70 (Pha 3). Điều hướng dùng skeleton: `FxRouteSkeletonService`
|
|
1193
|
+
* đọc `data.skeleton` của route đích và shell dựng `fx-ui-route-skeleton`.
|
|
1194
|
+
*
|
|
1195
|
+
* Vì sao xoá thay vì để lại: trước đó cả 46 call site đổ chung MỘT channel, lẫn
|
|
1196
|
+
* hai ngữ nghĩa (điều hướng vs hành động). Sau khi skeleton thay phần điều
|
|
1197
|
+
* hướng, `listener` không còn ai nghe — để lại là một cái bẫy: gọi `start()`
|
|
1198
|
+
* tưởng có overlay mà thực tế không có gì hiện.
|
|
1199
|
+
*/
|
|
1172
1200
|
class FxLoadingService {
|
|
1173
|
-
listener = new EventEmitter();
|
|
1174
1201
|
listenerAction = new EventEmitter();
|
|
1175
|
-
start() {
|
|
1176
|
-
this.listener.emit(true);
|
|
1177
|
-
}
|
|
1178
|
-
stop() {
|
|
1179
|
-
this.listener.emit(false);
|
|
1180
|
-
}
|
|
1181
1202
|
startAction() {
|
|
1182
1203
|
this.listenerAction.emit(true);
|
|
1183
1204
|
}
|
|
@@ -1194,16 +1215,97 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
1194
1215
|
}]
|
|
1195
1216
|
}] });
|
|
1196
1217
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1218
|
+
/**
|
|
1219
|
+
* Khai trên route: `data: { skeleton: 'table' }` (tên preset) hoặc
|
|
1220
|
+
* `data: { skeleton: ['heading', 'toolbar', 'table'] }` (mảng block, mô tả
|
|
1221
|
+
* ĐÚNG layout của màn — bảng ra bảng, chart ra chart).
|
|
1222
|
+
*/
|
|
1223
|
+
const FX_SKELETON_DATA_KEY = 'skeleton';
|
|
1224
|
+
/**
|
|
1225
|
+
* Đọc `data.skeleton` của route đích rồi bật/tắt skeleton cho vùng nội dung.
|
|
1226
|
+
*
|
|
1227
|
+
* ⚠️ Vì sao phải nghe router event chứ không để từng màn tự lo: Angular
|
|
1228
|
+
* **không tạo component cho tới khi resolver xong**, nên trong lúc resolver
|
|
1229
|
+
* của một màn chạy thì component đó chưa tồn tại — không có gì để vẽ
|
|
1230
|
+
* skeleton. Chỉ shell (tồn tại trước) làm được.
|
|
1231
|
+
*
|
|
1232
|
+
* Mốc dùng là `RoutesRecognized` — event ĐẦU TIÊN mang `RouterStateSnapshot`,
|
|
1233
|
+
* và nó xảy ra TRƯỚC `ResolveStart`. `NavigationStart` thì chưa biết route
|
|
1234
|
+
* đích nên không đọc được hint.
|
|
1235
|
+
*/
|
|
1236
|
+
class FxRouteSkeletonService {
|
|
1237
|
+
router = inject(Router);
|
|
1238
|
+
_hint = signal(null, ...(ngDevMode ? [{ debugName: "_hint" }] : /* istanbul ignore next */ []));
|
|
1239
|
+
/** Hint đang hiển thị, `null` = không vẽ skeleton. */
|
|
1240
|
+
hint = this._hint.asReadonly();
|
|
1241
|
+
active = computed(() => this._hint() !== null, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
1242
|
+
/**
|
|
1243
|
+
* Điều hướng nhanh hơn mốc này thì KHÔNG vẽ skeleton — tránh nháy một
|
|
1244
|
+
* nhịp khi resolver trả về gần như tức thì (dữ liệu đã cache).
|
|
1245
|
+
*/
|
|
1246
|
+
graceMs = 150;
|
|
1247
|
+
timer;
|
|
1248
|
+
started = false;
|
|
1249
|
+
constructor() {
|
|
1250
|
+
inject(DestroyRef).onDestroy(() => this.clearTimer());
|
|
1203
1251
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1252
|
+
/** Gọi một lần từ shell (host và mỗi remote standalone). */
|
|
1253
|
+
start() {
|
|
1254
|
+
if (this.started)
|
|
1255
|
+
return;
|
|
1256
|
+
this.started = true;
|
|
1257
|
+
this.router.events.subscribe((event) => {
|
|
1258
|
+
if (event instanceof RoutesRecognized) {
|
|
1259
|
+
const hint = readHint(event.state.root);
|
|
1260
|
+
this.clearTimer();
|
|
1261
|
+
if (!hint) {
|
|
1262
|
+
this._hint.set(null);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
this.timer = setTimeout(() => {
|
|
1266
|
+
this.timer = undefined;
|
|
1267
|
+
this._hint.set(hint);
|
|
1268
|
+
}, this.graceMs);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (event instanceof NavigationEnd ||
|
|
1272
|
+
event instanceof NavigationCancel ||
|
|
1273
|
+
event instanceof NavigationError) {
|
|
1274
|
+
this.clearTimer();
|
|
1275
|
+
this._hint.set(null);
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1206
1278
|
}
|
|
1279
|
+
clearTimer() {
|
|
1280
|
+
if (this.timer !== undefined) {
|
|
1281
|
+
clearTimeout(this.timer);
|
|
1282
|
+
this.timer = undefined;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxRouteSkeletonService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1286
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxRouteSkeletonService, providedIn: 'root' });
|
|
1287
|
+
}
|
|
1288
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FxRouteSkeletonService, decorators: [{
|
|
1289
|
+
type: Injectable,
|
|
1290
|
+
args: [{ providedIn: 'root' }]
|
|
1291
|
+
}], ctorParameters: () => [] });
|
|
1292
|
+
/**
|
|
1293
|
+
* Lấy hint ở nhánh SÂU NHẤT có khai — để route cha đặt mặc định và route con
|
|
1294
|
+
* ghi đè được.
|
|
1295
|
+
*/
|
|
1296
|
+
function readHint(root) {
|
|
1297
|
+
let node = root;
|
|
1298
|
+
let found = null;
|
|
1299
|
+
while (node) {
|
|
1300
|
+
const value = node.data?.[FX_SKELETON_DATA_KEY];
|
|
1301
|
+
// Chấp nhận cả tên preset và mảng block; mảng rỗng coi như không khai.
|
|
1302
|
+
if (typeof value === 'string' && value)
|
|
1303
|
+
found = value;
|
|
1304
|
+
else if (Array.isArray(value) && value.length)
|
|
1305
|
+
found = value;
|
|
1306
|
+
node = node.firstChild;
|
|
1307
|
+
}
|
|
1308
|
+
return found;
|
|
1207
1309
|
}
|
|
1208
1310
|
|
|
1209
1311
|
class FxStorageService {
|
|
@@ -1228,11 +1330,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
1228
1330
|
}] });
|
|
1229
1331
|
|
|
1230
1332
|
/**
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
*
|
|
1234
|
-
*
|
|
1235
|
-
*
|
|
1333
|
+
* Chạy MỘT request cho list resolver, lỗi thì hiện toast đã parse.
|
|
1334
|
+
*
|
|
1335
|
+
* ⚠️ KHÔNG còn bật overlay chặn màn: điều hướng dùng skeleton
|
|
1336
|
+
* (`FxRouteSkeletonService` + `data.skeleton` trên route). `BaseResolver` đã bị
|
|
1337
|
+
* xoá ở 0.7.70 — mọi resolver giờ là `@Injectable()` thuần hoặc dùng factory
|
|
1338
|
+
* này.
|
|
1236
1339
|
*
|
|
1237
1340
|
* Usage:
|
|
1238
1341
|
* resolve: { histories: fxListResolver(ScanManagementApi, (route, state, ctx) =>
|
|
@@ -1241,11 +1344,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
1241
1344
|
function fxListResolver(apiType, request) {
|
|
1242
1345
|
return async (route, state) => {
|
|
1243
1346
|
const api = inject(apiType);
|
|
1244
|
-
const loading = inject(FxLoadingService);
|
|
1245
1347
|
const toastr = inject(FxToastrService);
|
|
1246
1348
|
const storage = inject(FxStorageService);
|
|
1247
1349
|
try {
|
|
1248
|
-
loading.start();
|
|
1249
1350
|
const ctx = { pageSize: storage.resolve(state.url) || 10 };
|
|
1250
1351
|
const req = request(route, state, ctx);
|
|
1251
1352
|
if ((req.method ?? 'get') === 'post') {
|
|
@@ -1254,7 +1355,6 @@ function fxListResolver(apiType, request) {
|
|
|
1254
1355
|
return await api.get(req.url, req.params);
|
|
1255
1356
|
}
|
|
1256
1357
|
catch (err) {
|
|
1257
|
-
loading.stop();
|
|
1258
1358
|
toastr.error(FxUtils.parseError(err), 'Error');
|
|
1259
1359
|
return null;
|
|
1260
1360
|
}
|
|
@@ -1427,26 +1527,54 @@ class RealtimeTableSync {
|
|
|
1427
1527
|
|
|
1428
1528
|
const NEUTRAL_TEXT = 'text-text-secondary';
|
|
1429
1529
|
const NEUTRAL_BG = 'bg-bg-hover';
|
|
1530
|
+
/**
|
|
1531
|
+
* ⚠️ Trỏ vào `.tone-fg-*` của hệ MỚI, KHÔNG còn `text-critical` (bộ LEGACY
|
|
1532
|
+
* vivid). Bỏ sót chỗ này chính là lý do badge/chữ trong list ở mọi tab vẫn
|
|
1533
|
+
* ra màu cũ sau khi đã đổi `TONE_BADGE` (anh Tú báo 03/09).
|
|
1534
|
+
*/
|
|
1430
1535
|
const TONE_TEXT = {
|
|
1431
|
-
critical: '
|
|
1432
|
-
danger: '
|
|
1433
|
-
warning: '
|
|
1434
|
-
success: '
|
|
1435
|
-
information: '
|
|
1536
|
+
critical: 'tone-fg-critical',
|
|
1537
|
+
danger: 'tone-fg-danger',
|
|
1538
|
+
warning: 'tone-fg-warning',
|
|
1539
|
+
success: 'tone-fg-success',
|
|
1540
|
+
information: 'tone-fg-information',
|
|
1541
|
+
discovery: 'tone-fg-discovery',
|
|
1436
1542
|
};
|
|
1543
|
+
/** Nền ĐẶC — dùng cho trạng thái đang chọn. Xem `.tone-solid-*`. */
|
|
1437
1544
|
const TONE_BG = {
|
|
1438
|
-
critical: '
|
|
1439
|
-
danger: '
|
|
1440
|
-
warning: '
|
|
1441
|
-
success: '
|
|
1442
|
-
information: '
|
|
1545
|
+
critical: 'tone-solid-critical',
|
|
1546
|
+
danger: 'tone-solid-danger',
|
|
1547
|
+
warning: 'tone-solid-warning',
|
|
1548
|
+
success: 'tone-solid-success',
|
|
1549
|
+
information: 'tone-solid-information',
|
|
1550
|
+
discovery: 'tone-solid-discovery',
|
|
1443
1551
|
};
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1552
|
+
/**
|
|
1553
|
+
* ⚠️ ĐỪNG dùng `toneBadge()` cho badge CÓ CHẤM bên trong: Critical ra nền
|
|
1554
|
+
* đen đặc nên chấm (đỏ sẫm) nằm trên đó gần như vô hình. Demo chỉ dùng biến
|
|
1555
|
+
* thể `solid` cho badge severity ĐỨNG MỘT MÌNH; badge kèm `StatusDot` luôn
|
|
1556
|
+
* dùng nền MỀM. Chỗ đó hãy dùng `toneScale()`.
|
|
1557
|
+
*
|
|
1558
|
+
* Tông severity — theo ĐÚNG mô hình đo trên demo 03/09: token semantic
|
|
1559
|
+
* (`--og-negative`…) chứ không phải bộ vivid `--og-tone-*`, và Critical được
|
|
1560
|
+
* nhấn bằng nền ĐEN ĐẶC thay vì màu rực. Định nghĩa ở `.tone-tint-*` trong
|
|
1561
|
+
* components.css — xem comment ở đó để biết lý do không dùng utility Tailwind.
|
|
1562
|
+
*
|
|
1563
|
+
* `tone-tint-*` chỉ mang MÀU. Hình dạng pill nằm ở `.tone-badge` riêng, nên
|
|
1564
|
+
* ô icon vuông / chip filter dùng được màu mà không bị bo tròn 999px.
|
|
1565
|
+
*/
|
|
1566
|
+
/**
|
|
1567
|
+
* Cố tình KHÔNG khoá vào `Record<Tone, …>`: `FxUtils.getTagClass()` còn trả
|
|
1568
|
+
* `discovery` (và `default`), mà `discovery` có đúng một biến thể tương ứng
|
|
1569
|
+
* trong demo là `.og-badge--accent`. `default` không có key → rơi về neutral.
|
|
1570
|
+
*/
|
|
1571
|
+
const TONE_TINT = {
|
|
1572
|
+
critical: 'tone-tint-critical',
|
|
1573
|
+
danger: 'tone-tint-danger',
|
|
1574
|
+
warning: 'tone-tint-warning',
|
|
1575
|
+
success: 'tone-tint-success',
|
|
1576
|
+
information: 'tone-tint-information',
|
|
1577
|
+
discovery: 'tone-tint-discovery',
|
|
1450
1578
|
};
|
|
1451
1579
|
function hasVal(v) {
|
|
1452
1580
|
if (v == null)
|
|
@@ -1501,8 +1629,71 @@ function toneBg(tone) {
|
|
|
1501
1629
|
return tone ? TONE_BG[tone] : NEUTRAL_BG;
|
|
1502
1630
|
}
|
|
1503
1631
|
/** Tone → soft pill/badge classes (tinted bg + matching text), neutral fallback. */
|
|
1632
|
+
/**
|
|
1633
|
+
* Badge severity MẶC ĐỊNH — nền MỀM theo thang đo, Critical ra **đỏ sẫm**.
|
|
1634
|
+
*
|
|
1635
|
+
* ⚠️ Trước đây hàm này trả nền ĐẶC (`tone-tint-*`, Critical = đen `--og-text`)
|
|
1636
|
+
* vì demo có biến thể `.og-badge--solid`. Nhưng demo chỉ dùng biến thể đó ở
|
|
1637
|
+
* ĐÚNG MỘT chỗ — badge severity trong danh sách findings — còn app mình gọi
|
|
1638
|
+
* `toneBadge()` ở khắp nơi (`project-card`, `score-badge`, `list-row`,
|
|
1639
|
+
* `severity-badges`, `tab-group`, badge trong hàng…). Kết quả: pill đen tràn
|
|
1640
|
+
* ra mọi màn, anh Tú báo 3 lần (03/09). Nên MẶC ĐỊNH đảo về nền mềm; muốn
|
|
1641
|
+
* pill đen thì gọi `toneBadgeSolid()` một cách tường minh.
|
|
1642
|
+
*/
|
|
1504
1643
|
function toneBadge(tone) {
|
|
1505
|
-
return tone
|
|
1644
|
+
return `tone-badge ${toneScale(tone)}`;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Badge severity nền ĐẶC (Critical = `--og-text`, chữ `--og-surface`) — đúng
|
|
1648
|
+
* `.og-badge--solid` của demo. CHỈ dùng cho badge severity đứng một mình ở
|
|
1649
|
+
* nơi cần nhấn mạnh nhất; đừng dùng cho badge có chấm bên trong (chấm sẽ chìm).
|
|
1650
|
+
*/
|
|
1651
|
+
function toneBadgeSolid(tone) {
|
|
1652
|
+
return `tone-badge ${toneTint(tone)}`;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Nhận `string` rộng chứ không chỉ `Tone`: rất nhiều call site truyền kết quả
|
|
1656
|
+
* của `FxUtils.getTagClass()` / tone tự tính, vốn là `string`. Tone lạ →
|
|
1657
|
+
* neutral, thay vì sinh ra class rác như thời còn ghép chuỗi `'bg-' + tone`.
|
|
1658
|
+
*/
|
|
1659
|
+
const isTone = (t) => typeof t === 'string' && Object.prototype.hasOwnProperty.call(TONE_TINT, t);
|
|
1660
|
+
/** Chỉ MÀU của tông, không kèm hình dạng pill — cho ô icon, chip, ô nền. */
|
|
1661
|
+
function toneTint(tone) {
|
|
1662
|
+
return isTone(tone) ? TONE_TINT[tone] : 'tone-tint-neutral';
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* CHỮ theo tông — dùng thành viên dễ đọc nhất của mỗi họ severity, KHÔNG phải
|
|
1666
|
+
* ramp chart (ramp có `warning #f59e0b` chỉ 2.15:1 trên nền trắng). Xem
|
|
1667
|
+
* `.tone-fg-*` trong components.css.
|
|
1668
|
+
*/
|
|
1669
|
+
function toneFg(tone) {
|
|
1670
|
+
return isTone(tone) ? `tone-fg-${tone}` : 'tone-fg-neutral';
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Tông THANG ĐO — cho filter chip severity. Critical ra ĐỎ SẪM (nền mềm pha
|
|
1674
|
+
* từ ramp `--og-sev-*`) thay vì pill đen đặc của `toneTint()`: chip filter là
|
|
1675
|
+
* một thang đo, không phải badge severity của một finding. Xem `.tone-scale-*`.
|
|
1676
|
+
*/
|
|
1677
|
+
function toneScale(tone) {
|
|
1678
|
+
return isTone(tone) ? `tone-scale-${tone}` : 'tone-scale-neutral';
|
|
1679
|
+
}
|
|
1680
|
+
/** Viền theo tông — cho panel trạng thái (dropzone upload…). */
|
|
1681
|
+
function toneBorder(tone) {
|
|
1682
|
+
return isTone(tone) ? `tone-border-${tone}` : 'tone-border-neutral';
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* CHẤM trạng thái trong hàng (`● Confirmed`) — demo dùng màu SEMANTIC
|
|
1686
|
+
* (`.og-statusdot--*`), không phải ramp chart.
|
|
1687
|
+
*/
|
|
1688
|
+
function toneDot(tone) {
|
|
1689
|
+
return isTone(tone) ? `tone-dot-${tone}` : 'tone-dot-neutral';
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* CHẤM LEGEND của chart — phải TRÙNG màu cung donut nên dùng ramp
|
|
1693
|
+
* `--og-sev-*`. Đừng dùng cho chấm trạng thái trong hàng.
|
|
1694
|
+
*/
|
|
1695
|
+
function toneDotChart(tone) {
|
|
1696
|
+
return isTone(tone) ? `tone-dot-chart-${tone}` : 'tone-dot-chart-neutral';
|
|
1506
1697
|
}
|
|
1507
1698
|
/**
|
|
1508
1699
|
* Tone → resolved CSS colour string for ECharts / canvas where a Tailwind
|
|
@@ -1510,10 +1701,22 @@ function toneBadge(tone) {
|
|
|
1510
1701
|
* legacy comma syntax — see FxUtils.convertColorFromVariable.
|
|
1511
1702
|
*/
|
|
1512
1703
|
function toneColor(tone, alpha = 1) {
|
|
1513
|
-
|
|
1704
|
+
// Chart dùng ramp RIÊNG `--og-sev-*` (màu literal đo trên demo), không dùng
|
|
1705
|
+
// `--og-tone-*`. `information` không có trong ramp của demo (demo chỉ có 4
|
|
1706
|
+
// bậc Critical/High/Medium/Low) nên vẫn rơi về tone legacy.
|
|
1707
|
+
const SEV = new Set(['critical', 'danger', 'warning', 'success']);
|
|
1708
|
+
const name = tone ? (SEV.has(tone) ? `--og-sev-${tone}` : `--${tone}`) : '--text-secondary';
|
|
1514
1709
|
return FxUtils.convertColorFromVariable(name, alpha, { legacyCommaSyntax: true });
|
|
1515
1710
|
}
|
|
1516
1711
|
|
|
1712
|
+
function featureEnabled(flags, name) {
|
|
1713
|
+
return flags?.[name] === true;
|
|
1714
|
+
}
|
|
1715
|
+
/** Convenience for templates that gate on several flags at once. */
|
|
1716
|
+
function anyFeatureEnabled(flags, ...names) {
|
|
1717
|
+
return names.some((name) => featureEnabled(flags, name));
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1517
1720
|
class FxValidators {
|
|
1518
1721
|
/**
|
|
1519
1722
|
* Name fields: firstName, lastName, groupName, projectName, displayName
|
|
@@ -1905,37 +2108,67 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
1905
2108
|
}]
|
|
1906
2109
|
}] });
|
|
1907
2110
|
|
|
2111
|
+
const OG_THEMES = [
|
|
2112
|
+
'porcelain',
|
|
2113
|
+
'charcoal',
|
|
2114
|
+
'ivory',
|
|
2115
|
+
'studio',
|
|
2116
|
+
'blueprint',
|
|
2117
|
+
'terminal',
|
|
2118
|
+
];
|
|
2119
|
+
const OG_LIGHT_THEMES = ['porcelain', 'ivory', 'terminal'];
|
|
2120
|
+
const OG_THEME_META = [
|
|
2121
|
+
{ name: 'porcelain', label: 'Porcelain', accent: '#4a7dbd', pageBg: '#f2f2f1', dark: false },
|
|
2122
|
+
{ name: 'charcoal', label: 'Charcoal', accent: '#8fc866', pageBg: '#1a1811', dark: true },
|
|
2123
|
+
{ name: 'ivory', label: 'Ivory', accent: '#d97757', pageBg: '#e8e6e1', dark: false },
|
|
2124
|
+
{ name: 'studio', label: 'Studio', accent: '#5a8fd6', pageBg: '#0e0e10', dark: true },
|
|
2125
|
+
{ name: 'blueprint', label: 'Blueprint', accent: '#e07a3a', pageBg: '#0e0e10', dark: true },
|
|
2126
|
+
{ name: 'terminal', label: 'Terminal', accent: '#3a55d9', pageBg: '#e8e6e1', dark: false },
|
|
2127
|
+
];
|
|
2128
|
+
/**
|
|
2129
|
+
* Theme NGƯỜI DÙNG được chọn (anh Tú chốt 31/08): chỉ còn một cặp
|
|
2130
|
+
* light/dark. Bốn theme còn lại vẫn tồn tại đầy đủ trong theme.css và
|
|
2131
|
+
* setTheme() vẫn nhận — chúng dùng cho trang /themes của demo và cho
|
|
2132
|
+
* harness screenshot chạy ma trận 6 theme; chỉ **giấu khỏi picker**.
|
|
2133
|
+
* Muốn mở lại: thêm tên vào mảng này, không phải dựng lại token.
|
|
2134
|
+
*/
|
|
2135
|
+
const OG_PICKER_THEMES = ['porcelain', 'charcoal'];
|
|
2136
|
+
/** OG_THEME_META đã lọc theo OG_PICKER_THEMES — nguồn duy nhất cho mọi picker. */
|
|
2137
|
+
const OG_PICKER_THEME_META = OG_THEME_META.filter((t) => OG_PICKER_THEMES.includes(t.name));
|
|
2138
|
+
/**
|
|
2139
|
+
* V2 theme switching: one `data-og-theme` attribute on <html> selects the
|
|
2140
|
+
* --og-* token block (theme.css). During the migration window the service
|
|
2141
|
+
* also mirrors the legacy `light_theme` class on <body> so remotes built
|
|
2142
|
+
* against the pre-V2 stylesheet keep resolving the correct palette
|
|
2143
|
+
* (see the SKEW SHIELD note in theme.css).
|
|
2144
|
+
*/
|
|
1908
2145
|
class ThemeService {
|
|
1909
2146
|
renderer;
|
|
1910
2147
|
lightClass = 'light_theme';
|
|
1911
|
-
theme$ = new BehaviorSubject('
|
|
2148
|
+
theme$ = new BehaviorSubject('porcelain');
|
|
1912
2149
|
themeStorageKey = 'host-theme';
|
|
1913
2150
|
constructor(rendererFactory) {
|
|
1914
2151
|
this.renderer = rendererFactory.createRenderer(null, null);
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
if (enable) {
|
|
2152
|
+
this.setTheme(this.readSavedTheme());
|
|
2153
|
+
}
|
|
2154
|
+
/** Primary API: activate one of the six V2 themes. */
|
|
2155
|
+
setTheme(theme) {
|
|
2156
|
+
if (!OG_THEMES.includes(theme))
|
|
2157
|
+
theme = 'porcelain';
|
|
2158
|
+
this.renderer.setAttribute(document.documentElement, 'data-og-theme', theme);
|
|
2159
|
+
// Skew shim for not-yet-migrated remote stylesheets (.light_theme era).
|
|
2160
|
+
if (this.isLightTheme(theme)) {
|
|
1925
2161
|
this.renderer.addClass(document.body, this.lightClass);
|
|
1926
|
-
this.theme$.next('light');
|
|
1927
|
-
localStorage.setItem(this.themeStorageKey, 'light');
|
|
1928
2162
|
}
|
|
1929
2163
|
else {
|
|
1930
2164
|
this.renderer.removeClass(document.body, this.lightClass);
|
|
1931
|
-
this.theme$.next('dark');
|
|
1932
|
-
localStorage.setItem(this.themeStorageKey, 'dark');
|
|
1933
2165
|
}
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2166
|
+
localStorage.setItem(this.themeStorageKey, theme);
|
|
2167
|
+
this.theme$.next(theme);
|
|
2168
|
+
// detail.dark is the pre-V2 contract (chart rebuilds on it); detail.theme
|
|
2169
|
+
// is new. Listeners must re-read colors on EVERY event — two light themes
|
|
2170
|
+
// can differ even though `dark` did not change.
|
|
2171
|
+
window.dispatchEvent(new CustomEvent('theme-changed', { detail: { dark: !this.isLightTheme(theme), theme } }));
|
|
1939
2172
|
}
|
|
1940
2173
|
themeChanges() {
|
|
1941
2174
|
return this.theme$.asObservable();
|
|
@@ -1944,11 +2177,31 @@ class ThemeService {
|
|
|
1944
2177
|
return this.theme$.value;
|
|
1945
2178
|
}
|
|
1946
2179
|
isLight() {
|
|
1947
|
-
return this.theme$.value
|
|
2180
|
+
return this.isLightTheme(this.theme$.value);
|
|
2181
|
+
}
|
|
2182
|
+
/** @deprecated V2: use setTheme(). Maps light→porcelain, dark→charcoal. */
|
|
2183
|
+
setLightMode(enable) {
|
|
2184
|
+
this.setTheme(enable ? 'porcelain' : 'charcoal');
|
|
2185
|
+
}
|
|
2186
|
+
/** @deprecated V2: use setTheme(). Flips between the two default themes. */
|
|
2187
|
+
toggleTheme() {
|
|
2188
|
+
this.setTheme(this.isLight() ? 'charcoal' : 'porcelain');
|
|
1948
2189
|
}
|
|
1949
2190
|
getLogoPath() {
|
|
1950
2191
|
return 'assets/images/logo.png';
|
|
1951
2192
|
}
|
|
2193
|
+
isLightTheme(theme) {
|
|
2194
|
+
return OG_LIGHT_THEMES.includes(theme);
|
|
2195
|
+
}
|
|
2196
|
+
readSavedTheme() {
|
|
2197
|
+
const saved = localStorage.getItem(this.themeStorageKey);
|
|
2198
|
+
// Migrate pre-V2 stored values.
|
|
2199
|
+
if (saved === 'light')
|
|
2200
|
+
return 'porcelain';
|
|
2201
|
+
if (saved === 'dark')
|
|
2202
|
+
return 'charcoal';
|
|
2203
|
+
return OG_THEMES.includes(saved) ? saved : 'porcelain';
|
|
2204
|
+
}
|
|
1952
2205
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemeService, deps: [{ token: i0.RendererFactory2 }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1953
2206
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemeService, providedIn: 'root' });
|
|
1954
2207
|
}
|
|
@@ -2201,11 +2454,11 @@ class ButtonComponent {
|
|
|
2201
2454
|
this.clicked.emit();
|
|
2202
2455
|
}
|
|
2203
2456
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2204
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ButtonComponent, isStandalone: true, selector: "fx-ui-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, buttonVariant: { classPropertyName: "buttonVariant", publicName: "buttonVariant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"
|
|
2457
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ButtonComponent, isStandalone: true, selector: "fx-ui-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, buttonVariant: { classPropertyName: "buttonVariant", publicName: "buttonVariant", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2205
2458
|
}
|
|
2206
2459
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ButtonComponent, decorators: [{
|
|
2207
2460
|
type: Component,
|
|
2208
|
-
args: [{ selector: 'fx-ui-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"
|
|
2461
|
+
args: [{ selector: 'fx-ui-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<button\n [attr.type]=\"type()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-busy]=\"loading() || null\"\n [class]=\"class()\"\n [ngClass]=\"{\n 'btn-default': buttonVariant() === 'default',\n 'btn-primary': buttonVariant() === 'primary',\n 'btn-success': buttonVariant() === 'success',\n 'btn-cta': buttonVariant() === 'cta',\n 'btn-alternative': buttonVariant() === 'alternative',\n }\"\n [disabled]=\"disabled() || loading()\"\n (click)=\"onClick()\"\n>\n @if (loading()) {\n <div class=\"flex w-full items-center justify-center\" aria-hidden=\"true\">\n <svg\n [attr.fill]=\"\n buttonVariant() !== 'alternative' ? 'rgb(var(--text-primary))' : 'rgb(var(--primary))'\n \"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"0\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_lo66\"\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_z0Or\"\n begin=\"spinner_lo66.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"4\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_JsnR\"\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_Aguh\"\n begin=\"spinner_JsnR.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"3\">\n <animate\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_hSjk\"\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_UHR2\"\n begin=\"spinner_hSjk.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"3\">\n <animate\n id=\"spinner_4v5M\"\n begin=\"0;spinner_z0Or.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"3;0\"\n fill=\"freeze\"\n />\n <animate\n id=\"spinner_OLMs\"\n begin=\"spinner_4v5M.end\"\n attributeName=\"cx\"\n dur=\"0.001s\"\n values=\"20;4\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_OLMs.end\"\n attributeName=\"r\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"0;3\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_UHR2.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"4;12\"\n fill=\"freeze\"\n />\n <animate\n begin=\"spinner_Aguh.end\"\n attributeName=\"cx\"\n calcMode=\"spline\"\n dur=\"0.5s\"\n keySplines=\".36,.6,.31,1\"\n values=\"12;20\"\n fill=\"freeze\"\n />\n </circle>\n </svg>\n </div>\n } @else {\n <ng-container>\n <div class=\"flex items-center justify-center gap-small\">\n @if (icon) {\n <fx-ui-hero-icon\n [icon]=\"icon\"\n [size]=\"16\"\n class=\"flex\"\n [ngClass]=\"{\n 'text-text-inverse': buttonVariant() === 'default',\n 'text-text-primary': buttonVariant() === 'alternative',\n 'text-accent-contrast': buttonVariant() === 'primary',\n 'text-surface': buttonVariant() === 'success',\n 'text-white': buttonVariant() === 'cta',\n }\"\n ></fx-ui-hero-icon>\n }\n @if (label) {\n <!-- text-box: optical centering \u2014 Archivo's leading floats ink up otherwise -->\n <div class=\"[text-box:trim-both_cap_alphabetic]\">{{ label }}</div>\n }\n </div>\n </ng-container>\n }\n</button>\n" }]
|
|
2209
2462
|
}], propDecorators: { label: [{
|
|
2210
2463
|
type: Input
|
|
2211
2464
|
}], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], buttonVariant: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonVariant", required: false }] }], icon: [{
|
|
@@ -2417,11 +2670,11 @@ class CheckboxComponent extends FxComponent {
|
|
|
2417
2670
|
this.onTouched();
|
|
2418
2671
|
}
|
|
2419
2672
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CheckboxComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2420
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: CheckboxComponent, isStandalone: true, selector: "fx-ui-checkbox", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<label\n class=\"flex items-center gap-normal cursor-pointer select-none\"\n [class.opacity-50]=\"disabled\"\n [class.cursor-not-allowed]=\"disabled\"\n>\n <!-- sr-only (not `hidden`): keeps the input focusable and in the a11y tree -->\n <input\n type=\"checkbox\"\n class=\"peer sr-only\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onInputChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span\n class=\"relative w-4 h-4 border rounded-full transition-colors duration-150 shrink-0 peer-focus-visible:ring-2 peer-focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'rounded-full': rounded(),\n 'rounded-sm': !rounded(),\n 'border-border-strong bg-transparent': !value,\n 'border-transparent bg-gradient-primary': value,\n }\"\n >\n <!-- Keep SVG space reserved always -->\n <span class=\"absolute inset-0 flex items-center justify-center\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-
|
|
2673
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: CheckboxComponent, isStandalone: true, selector: "fx-ui-checkbox", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, usesInheritance: true, ngImport: i0, template: "<label\n class=\"flex items-center gap-normal cursor-pointer select-none\"\n [class.opacity-50]=\"disabled\"\n [class.cursor-not-allowed]=\"disabled\"\n>\n <!-- sr-only (not `hidden`): keeps the input focusable and in the a11y tree -->\n <input\n type=\"checkbox\"\n class=\"peer sr-only\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onInputChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span\n class=\"relative w-4 h-4 border rounded-full transition-colors duration-150 shrink-0 peer-focus-visible:ring-2 peer-focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'rounded-full': rounded(),\n 'rounded-sm': !rounded(),\n 'border-border-strong bg-transparent': !value,\n 'border-transparent bg-gradient-primary': value,\n }\"\n >\n <!-- Keep SVG space reserved always -->\n <span class=\"absolute inset-0 flex items-center justify-center\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-accent-contrast w-4 h-4 transition-opacity duration-150\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n [style.opacity]=\"value ? 1 : 0\"\n >\n <path d=\"M5 13l4 4L19 7\" />\n </svg>\n </span>\n </span>\n\n @if (label) {\n <span class=\"mb-0 txt-field-label\">{{ label }}</span>\n }\n</label>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2421
2674
|
}
|
|
2422
2675
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CheckboxComponent, decorators: [{
|
|
2423
2676
|
type: Component,
|
|
2424
|
-
args: [{ selector: 'fx-ui-checkbox', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<label\n class=\"flex items-center gap-normal cursor-pointer select-none\"\n [class.opacity-50]=\"disabled\"\n [class.cursor-not-allowed]=\"disabled\"\n>\n <!-- sr-only (not `hidden`): keeps the input focusable and in the a11y tree -->\n <input\n type=\"checkbox\"\n class=\"peer sr-only\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onInputChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span\n class=\"relative w-4 h-4 border rounded-full transition-colors duration-150 shrink-0 peer-focus-visible:ring-2 peer-focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'rounded-full': rounded(),\n 'rounded-sm': !rounded(),\n 'border-border-strong bg-transparent': !value,\n 'border-transparent bg-gradient-primary': value,\n }\"\n >\n <!-- Keep SVG space reserved always -->\n <span class=\"absolute inset-0 flex items-center justify-center\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-
|
|
2677
|
+
args: [{ selector: 'fx-ui-checkbox', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<label\n class=\"flex items-center gap-normal cursor-pointer select-none\"\n [class.opacity-50]=\"disabled\"\n [class.cursor-not-allowed]=\"disabled\"\n>\n <!-- sr-only (not `hidden`): keeps the input focusable and in the a11y tree -->\n <input\n type=\"checkbox\"\n class=\"peer sr-only\"\n [checked]=\"value\"\n [disabled]=\"disabled\"\n (change)=\"onInputChange($event)\"\n (blur)=\"onTouched()\"\n />\n <span\n class=\"relative w-4 h-4 border rounded-full transition-colors duration-150 shrink-0 peer-focus-visible:ring-2 peer-focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'rounded-full': rounded(),\n 'rounded-sm': !rounded(),\n 'border-border-strong bg-transparent': !value,\n 'border-transparent bg-gradient-primary': value,\n }\"\n >\n <!-- Keep SVG space reserved always -->\n <span class=\"absolute inset-0 flex items-center justify-center\">\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"text-accent-contrast w-4 h-4 transition-opacity duration-150\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"3\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n [style.opacity]=\"value ? 1 : 0\"\n >\n <path d=\"M5 13l4 4L19 7\" />\n </svg>\n </span>\n </span>\n\n @if (label) {\n <span class=\"mb-0 txt-field-label\">{{ label }}</span>\n }\n</label>\n" }]
|
|
2425
2678
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
|
|
2426
2679
|
type: Input
|
|
2427
2680
|
}], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
|
|
@@ -2559,7 +2812,7 @@ class DatetimePicker extends FxComponent {
|
|
|
2559
2812
|
provideNativeDateAdapter(),
|
|
2560
2813
|
// en-GB display ⇒ DD/MM/YYYY + 24h — matches FxUtils.formatDateTime.
|
|
2561
2814
|
{ provide: MAT_DATE_LOCALE, useValue: 'en-GB' },
|
|
2562
|
-
], usesInheritance: true, ngImport: i0, template: "<div class=\"flex flex-col mb-4 w-full text-left\">\n @if (label) {\n <label class=\"txt-field-label\" [attr.for]=\"fieldId\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <div class=\"flex gap-normal\">\n @if (showDate) {\n <div class=\"relative flex-1\">\n <input\n [id]=\"fieldId\"\n [matDatepicker]=\"datePicker\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [min]=\"min\"\n [max]=\"max\"\n [value]=\"dateValue\"\n (dateChange)=\"onDateChange($event.value)\"\n (click)=\"!disabled && datePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && datePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"calendar\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-datepicker #datePicker></mat-datepicker>\n </div>\n }\n @if (showTime) {\n <div class=\"relative\" [class.flex-1]=\"!showDate\" [class.w-32]=\"showDate\">\n <input\n [matTimepicker]=\"timePicker\"\n placeholder=\"HH:mm\"\n [disabled]=\"disabled\"\n [value]=\"dateValue\"\n (valueChange)=\"onTimeChange($event)\"\n (click)=\"!disabled && timePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && timePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-timepicker #timePicker></mat-timepicker>\n </div>\n }\n </div>\n @if (invalid) {\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;::ng-deep .mat-datepicker-content,::ng-deep mat-timepicker .mat-timepicker-panel,::ng-deep .mat-timepicker-panel{border-radius
|
|
2815
|
+
], usesInheritance: true, ngImport: i0, template: "<div class=\"flex flex-col mb-4 w-full text-left\">\n @if (label) {\n <label class=\"txt-field-label\" [attr.for]=\"fieldId\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <div class=\"flex gap-normal\">\n @if (showDate) {\n <div class=\"relative flex-1\">\n <input\n [id]=\"fieldId\"\n [matDatepicker]=\"datePicker\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [min]=\"min\"\n [max]=\"max\"\n [value]=\"dateValue\"\n (dateChange)=\"onDateChange($event.value)\"\n (click)=\"!disabled && datePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && datePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"calendar\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-datepicker #datePicker></mat-datepicker>\n </div>\n }\n @if (showTime) {\n <div class=\"relative\" [class.flex-1]=\"!showDate\" [class.w-32]=\"showDate\">\n <input\n [matTimepicker]=\"timePicker\"\n placeholder=\"HH:mm\"\n [disabled]=\"disabled\"\n [value]=\"dateValue\"\n (valueChange)=\"onTimeChange($event)\"\n (click)=\"!disabled && timePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && timePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-timepicker #timePicker></mat-timepicker>\n </div>\n }\n </div>\n @if (invalid) {\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;::ng-deep .mat-datepicker-content,::ng-deep mat-timepicker .mat-timepicker-panel,::ng-deep .mat-timepicker-panel{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 10px 10px -5px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);color:rgb(var(--text-primary));--mat-datepicker-calendar-container-background-color: rgb(var(--bg-primary));--mat-datepicker-calendar-container-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-selected-state-background-color: rgb(var(--primary));--mat-datepicker-calendar-date-selected-state-text-color: #fff;--mat-datepicker-calendar-date-today-outline-color: rgb(var(--primary));--mat-datepicker-calendar-date-hover-state-background-color: rgb(var(--bg-primary-hover));--mat-datepicker-calendar-header-text-color: rgb(var(--text-secondary));--mat-datepicker-calendar-period-button-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-navigation-button-icon-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-disabled-state-text-color: rgb(var(--text-placeholder));--mat-timepicker-container-background-color: rgb(var(--bg-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option{color:rgb(var(--text-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option:hover:not(.mdc-list-item--disabled){background-color:rgb(var(--bg-primary-hover))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: MatDatepickerModule }, { kind: "component", type: i1$2.MatDatepicker, selector: "mat-datepicker", exportAs: ["matDatepicker"] }, { kind: "directive", type: i1$2.MatDatepickerInput, selector: "input[matDatepicker]", inputs: ["matDatepicker", "min", "max", "matDatepickerFilter"], exportAs: ["matDatepickerInput"] }, { kind: "ngmodule", type: MatTimepickerModule }, { kind: "component", type: i2$2.MatTimepicker, selector: "mat-timepicker", inputs: ["interval", "options", "disableRipple", "aria-label", "aria-labelledby", "panelClass"], outputs: ["selected", "opened", "closed"], exportAs: ["matTimepicker"] }, { kind: "directive", type: i2$2.MatTimepickerInput, selector: "input[matTimepicker]", inputs: ["value", "matTimepicker", "matTimepickerMin", "matTimepickerMax", "matTimepickerOpenOnClick", "disabled"], outputs: ["valueChange"], exportAs: ["matTimepickerInput"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2563
2816
|
}
|
|
2564
2817
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DatetimePicker, decorators: [{
|
|
2565
2818
|
type: Component,
|
|
@@ -2567,7 +2820,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
2567
2820
|
provideNativeDateAdapter(),
|
|
2568
2821
|
// en-GB display ⇒ DD/MM/YYYY + 24h — matches FxUtils.formatDateTime.
|
|
2569
2822
|
{ provide: MAT_DATE_LOCALE, useValue: 'en-GB' },
|
|
2570
|
-
], template: "<div class=\"flex flex-col mb-4 w-full text-left\">\n @if (label) {\n <label class=\"txt-field-label\" [attr.for]=\"fieldId\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <div class=\"flex gap-normal\">\n @if (showDate) {\n <div class=\"relative flex-1\">\n <input\n [id]=\"fieldId\"\n [matDatepicker]=\"datePicker\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [min]=\"min\"\n [max]=\"max\"\n [value]=\"dateValue\"\n (dateChange)=\"onDateChange($event.value)\"\n (click)=\"!disabled && datePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && datePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"calendar\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-datepicker #datePicker></mat-datepicker>\n </div>\n }\n @if (showTime) {\n <div class=\"relative\" [class.flex-1]=\"!showDate\" [class.w-32]=\"showDate\">\n <input\n [matTimepicker]=\"timePicker\"\n placeholder=\"HH:mm\"\n [disabled]=\"disabled\"\n [value]=\"dateValue\"\n (valueChange)=\"onTimeChange($event)\"\n (click)=\"!disabled && timePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && timePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-timepicker #timePicker></mat-timepicker>\n </div>\n }\n </div>\n @if (invalid) {\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;::ng-deep .mat-datepicker-content,::ng-deep mat-timepicker .mat-timepicker-panel,::ng-deep .mat-timepicker-panel{border-radius
|
|
2823
|
+
], template: "<div class=\"flex flex-col mb-4 w-full text-left\">\n @if (label) {\n <label class=\"txt-field-label\" [attr.for]=\"fieldId\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n }\n\n <div class=\"flex gap-normal\">\n @if (showDate) {\n <div class=\"relative flex-1\">\n <input\n [id]=\"fieldId\"\n [matDatepicker]=\"datePicker\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"disabled\"\n [min]=\"min\"\n [max]=\"max\"\n [value]=\"dateValue\"\n (dateChange)=\"onDateChange($event.value)\"\n (click)=\"!disabled && datePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && datePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"calendar\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-datepicker #datePicker></mat-datepicker>\n </div>\n }\n @if (showTime) {\n <div class=\"relative\" [class.flex-1]=\"!showDate\" [class.w-32]=\"showDate\">\n <input\n [matTimepicker]=\"timePicker\"\n placeholder=\"HH:mm\"\n [disabled]=\"disabled\"\n [value]=\"dateValue\"\n (valueChange)=\"onTimeChange($event)\"\n (click)=\"!disabled && timePicker.open()\"\n (blur)=\"onBlur()\"\n readonly\n class=\"input-default pr-9\"\n />\n <span\n (click)=\"!disabled && timePicker.open()\"\n class=\"absolute inset-y-0 right-0 flex items-center pr-2.5 cursor-pointer text-text-placeholder hover:text-text-primary transition-colors\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"20\" class=\"text-text-primary\"></fx-ui-hero-icon>\n </span>\n <mat-timepicker #timePicker></mat-timepicker>\n </div>\n }\n </div>\n @if (invalid) {\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;::ng-deep .mat-datepicker-content,::ng-deep mat-timepicker .mat-timepicker-panel,::ng-deep .mat-timepicker-panel{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 10px 10px -5px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);color:rgb(var(--text-primary));--mat-datepicker-calendar-container-background-color: rgb(var(--bg-primary));--mat-datepicker-calendar-container-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-selected-state-background-color: rgb(var(--primary));--mat-datepicker-calendar-date-selected-state-text-color: #fff;--mat-datepicker-calendar-date-today-outline-color: rgb(var(--primary));--mat-datepicker-calendar-date-hover-state-background-color: rgb(var(--bg-primary-hover));--mat-datepicker-calendar-header-text-color: rgb(var(--text-secondary));--mat-datepicker-calendar-period-button-text-color: rgb(var(--text-primary));--mat-datepicker-calendar-navigation-button-icon-color: rgb(var(--text-primary));--mat-datepicker-calendar-date-disabled-state-text-color: rgb(var(--text-placeholder));--mat-timepicker-container-background-color: rgb(var(--bg-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option{color:rgb(var(--text-primary))}::ng-deep .mat-timepicker-panel .mat-mdc-option:hover:not(.mdc-list-item--disabled){background-color:rgb(var(--bg-primary-hover))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
|
|
2571
2824
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
|
|
2572
2825
|
type: Input
|
|
2573
2826
|
}], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], errorMessages: [{
|
|
@@ -2819,11 +3072,11 @@ class RadioButtonToggleComponent extends FxComponent {
|
|
|
2819
3072
|
this.onTouched();
|
|
2820
3073
|
}
|
|
2821
3074
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RadioButtonToggleComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2822
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RadioButtonToggleComponent, isStandalone: true, selector: "fx-ui-radio-toggle, [fx-ui-radio-toggle]", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "<div class=\"flex flex-col text-left mb-4 w-full\" [class]=\"class()\">\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if(label){<span class=\"txt-field-label\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if(required()) {<span class=\"txt-required\" aria-hidden=\"true\">*</span>} </span\n >}\n\n <div\n class=\"inline-flex w-full rounded-lg overflow-hidden\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n @for(opt of options(); track opt){<button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-
|
|
3075
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RadioButtonToggleComponent, isStandalone: true, selector: "fx-ui-radio-toggle, [fx-ui-radio-toggle]", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "<div class=\"flex flex-col text-left mb-4 w-full\" [class]=\"class()\">\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if(label){<span class=\"txt-field-label\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if(required()) {<span class=\"txt-required\" aria-hidden=\"true\">*</span>} </span\n >}\n\n <div\n class=\"inline-flex w-full rounded-lg overflow-hidden\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n @for(opt of options(); track opt){<button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-strong]=\"value !== opt.value\"\n class=\"flex-1 bg-bg-primary flex items-center pl-normal gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value\n }\"\n class=\"radio-container\"\n >\n @if(value === opt.value) {<span class=\"radio-dot\"></span>}\n </span>\n\n <span class=\"txt-field-label mb-0\">{{ opt.label }}</span></button\n >}\n </div>\n\n @if(invalid){\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2823
3076
|
}
|
|
2824
3077
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RadioButtonToggleComponent, decorators: [{
|
|
2825
3078
|
type: Component,
|
|
2826
|
-
args: [{ selector: 'fx-ui-radio-toggle, [fx-ui-radio-toggle]', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex flex-col text-left mb-4 w-full\" [class]=\"class()\">\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if(label){<span class=\"txt-field-label\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if(required()) {<span class=\"txt-required\" aria-hidden=\"true\">*</span>} </span\n >}\n\n <div\n class=\"inline-flex w-full rounded-lg overflow-hidden\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n @for(opt of options(); track opt){<button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-
|
|
3079
|
+
args: [{ selector: 'fx-ui-radio-toggle, [fx-ui-radio-toggle]', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex flex-col text-left mb-4 w-full\" [class]=\"class()\">\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if(label){<span class=\"txt-field-label\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if(required()) {<span class=\"txt-required\" aria-hidden=\"true\">*</span>} </span\n >}\n\n <div\n class=\"inline-flex w-full rounded-lg overflow-hidden\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n @for(opt of options(); track opt){<button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-strong]=\"value !== opt.value\"\n class=\"flex-1 bg-bg-primary flex items-center pl-normal gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value\n }\"\n class=\"radio-container\"\n >\n @if(value === opt.value) {<span class=\"radio-dot\"></span>}\n </span>\n\n <span class=\"txt-field-label mb-0\">{{ opt.label }}</span></button\n >}\n </div>\n\n @if(invalid){\n <div class=\"txt-invalid\">{{ getErrorMessage(label) }}</div>\n }\n</div>\n" }]
|
|
2827
3080
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
|
|
2828
3081
|
type: Input
|
|
2829
3082
|
}], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], errorMessages: [{
|
|
@@ -2855,7 +3108,7 @@ class RadioButtonComponent extends FxComponent {
|
|
|
2855
3108
|
this.onTouched();
|
|
2856
3109
|
}
|
|
2857
3110
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RadioButtonComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
2858
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RadioButtonComponent, isStandalone: true, selector: "fx-ui-radio", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "<div\n class=\"w-full rounded-lg overflow-hidden\"\n [class]=\"class()\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if (label) {\n <span class=\"txt-field-label block\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n @if (tooltip) {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [size]=\"16\"\n class=\"inline-block align-middle ml-0.5 text-text-secondary cursor-help relative -top-px\"\n [matTooltip]=\"tooltip\"\n matTooltipPosition=\"above\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n ></fx-ui-hero-icon>\n }\n </span>\n }\n\n @for (opt of options(); track opt) {\n <button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-
|
|
3111
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RadioButtonComponent, isStandalone: true, selector: "fx-ui-radio", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: false, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: false, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: false, isRequired: false, transformFunction: null }, class: { classPropertyName: "class", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, usesInheritance: true, ngImport: i0, template: "<div\n class=\"w-full rounded-lg overflow-hidden\"\n [class]=\"class()\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if (label) {\n <span class=\"txt-field-label block\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n @if (tooltip) {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [size]=\"16\"\n class=\"inline-block align-middle ml-0.5 text-text-secondary cursor-help relative -top-px\"\n [matTooltip]=\"tooltip\"\n matTooltipPosition=\"above\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n ></fx-ui-hero-icon>\n }\n </span>\n }\n\n @for (opt of options(); track opt) {\n <button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-strong]=\"value !== opt.value\"\n class=\"flex-1 px-normal w-full flex items-center gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n [matTooltip]=\"opt.tooltip || ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value,\n }\"\n class=\"radio-container\"\n >\n @if (value === opt.value) {\n <span class=\"radio-dot\"></span>\n }\n </span>\n <span class=\"txt-default mb-0\">{{ opt.label }}</span>\n </button>\n }\n @if (invalid) {\n <div class=\"txt-invalid\">\n {{ getErrorMessage(label) }}\n </div>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2859
3112
|
}
|
|
2860
3113
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RadioButtonComponent, decorators: [{
|
|
2861
3114
|
type: Component,
|
|
@@ -2863,7 +3116,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
2863
3116
|
HeroIconComponent,
|
|
2864
3117
|
MatTooltip,
|
|
2865
3118
|
NgClass,
|
|
2866
|
-
], template: "<div\n class=\"w-full rounded-lg overflow-hidden\"\n [class]=\"class()\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if (label) {\n <span class=\"txt-field-label block\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n @if (tooltip) {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [size]=\"16\"\n class=\"inline-block align-middle ml-0.5 text-text-secondary cursor-help relative -top-px\"\n [matTooltip]=\"tooltip\"\n matTooltipPosition=\"above\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n ></fx-ui-hero-icon>\n }\n </span>\n }\n\n @for (opt of options(); track opt) {\n <button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-
|
|
3119
|
+
], template: "<div\n class=\"w-full rounded-lg overflow-hidden\"\n [class]=\"class()\"\n role=\"radiogroup\"\n [attr.aria-labelledby]=\"label ? groupId + '-label' : null\"\n >\n <!-- A radiogroup has no single control a <label> could point at \u2014 name it via aria-labelledby. -->\n @if (label) {\n <span class=\"txt-field-label block\" [id]=\"groupId + '-label'\">\n {{ label }}\n @if (required()) {\n <span class=\"txt-required\" aria-hidden=\"true\">*</span>\n }\n @if (tooltip) {\n <fx-ui-hero-icon\n icon=\"information-circle\"\n [size]=\"16\"\n class=\"inline-block align-middle ml-0.5 text-text-secondary cursor-help relative -top-px\"\n [matTooltip]=\"tooltip\"\n matTooltipPosition=\"above\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n ></fx-ui-hero-icon>\n }\n </span>\n }\n\n @for (opt of options(); track opt) {\n <button\n type=\"button\"\n role=\"radio\"\n [attr.aria-checked]=\"value === opt.value\"\n (click)=\"onSelectValue(opt.value)\"\n [disabled]=\"disabled\"\n [class.text-strong]=\"value !== opt.value\"\n class=\"flex-1 px-normal w-full flex items-center gap-normal h-10 rounded-md hover:bg-bg-hover transition-all duration-150 disabled:opacity-60 disabled:cursor-not-allowed\"\n [matTooltip]=\"opt.tooltip || ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n >\n <span\n [ngClass]=\"{\n 'radio-checked': value === opt.value,\n 'radio-uncheck': value !== opt.value,\n }\"\n class=\"radio-container\"\n >\n @if (value === opt.value) {\n <span class=\"radio-dot\"></span>\n }\n </span>\n <span class=\"txt-default mb-0\">{{ opt.label }}</span>\n </button>\n }\n @if (invalid) {\n <div class=\"txt-invalid\">\n {{ getErrorMessage(label) }}\n </div>\n }\n</div>\n" }]
|
|
2867
3120
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { label: [{
|
|
2868
3121
|
type: Input
|
|
2869
3122
|
}], tooltip: [{
|
|
@@ -2874,6 +3127,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
2874
3127
|
|
|
2875
3128
|
class SearchBarComponent extends FxComponent {
|
|
2876
3129
|
ref;
|
|
3130
|
+
/** FxComponent leaves `value` undefined; without ngModel the input would
|
|
3131
|
+
render the literal string "undefined". */
|
|
3132
|
+
value = '';
|
|
2877
3133
|
placeholder = input('Search...', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
|
|
2878
3134
|
search = output();
|
|
2879
3135
|
valueChange = output();
|
|
@@ -3014,17 +3270,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
3014
3270
|
type: Input
|
|
3015
3271
|
}], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], class: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], enableSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableSearch", required: false }] }], searchFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchFn", required: false }] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
|
|
3016
3272
|
|
|
3017
|
-
class SkeletonTableLoadingComponent {
|
|
3018
|
-
columns = input(5, ...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
3019
|
-
rows = input(5, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
3020
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonTableLoadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3021
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonTableLoadingComponent, isStandalone: true, selector: "fx-ui-skeleton-table-loading", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"w-full animate-pulse space-y-normal\">\n <div\n class=\"grid gap-small\"\n [ngStyle]=\"{ 'grid-template-columns': 'repeat(' + columns() + ', minmax(0, 1fr))' }\"\n >\n @for(col of [].constructor(columns()); track col) {\n <div class=\"h-6 bg-bg-hover rounded\"></div>\n }\n </div>\n\n @for(row of [].constructor(rows()); track row) {\n <div\n class=\"grid gap-small\"\n [ngStyle]=\"{ 'grid-template-columns': 'repeat(' + columns() + ', minmax(0, 1fr))' }\"\n >\n @for(col of [].constructor(columns()); track col) {\n <div class=\"h-7 bg-bg-hover rounded\"></div>\n }\n </div>\n }\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3022
|
-
}
|
|
3023
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonTableLoadingComponent, decorators: [{
|
|
3024
|
-
type: Component,
|
|
3025
|
-
args: [{ selector: 'fx-ui-skeleton-table-loading', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgStyle], template: "<div class=\"w-full animate-pulse space-y-normal\">\n <div\n class=\"grid gap-small\"\n [ngStyle]=\"{ 'grid-template-columns': 'repeat(' + columns() + ', minmax(0, 1fr))' }\"\n >\n @for(col of [].constructor(columns()); track col) {\n <div class=\"h-6 bg-bg-hover rounded\"></div>\n }\n </div>\n\n @for(row of [].constructor(rows()); track row) {\n <div\n class=\"grid gap-small\"\n [ngStyle]=\"{ 'grid-template-columns': 'repeat(' + columns() + ', minmax(0, 1fr))' }\"\n >\n @for(col of [].constructor(columns()); track col) {\n <div class=\"h-7 bg-bg-hover rounded\"></div>\n }\n </div>\n }\n</div>\n" }]
|
|
3026
|
-
}], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
|
|
3027
|
-
|
|
3028
3273
|
class SliderComponent extends FxComponent {
|
|
3029
3274
|
ref;
|
|
3030
3275
|
trackRef = viewChild.required('track');
|
|
@@ -3155,7 +3400,7 @@ class SliderComponent extends FxComponent {
|
|
|
3155
3400
|
: { left: `${this.percent}%`, transform: 'translateX(-50%)' };
|
|
3156
3401
|
}
|
|
3157
3402
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SliderComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
3158
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SliderComponent, isStandalone: true, selector: "fx-ui-slider", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, vertical: { classPropertyName: "vertical", publicName: "vertical", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { listeners: { "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "trackRef", first: true, predicate: ["track"], descendants: true, isSignal: true }, { propertyName: "thumbRef", first: true, predicate: ["thumb"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n class=\"w-full select-none\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n role=\"group\"\n >\n <!-- Track click is a pointer shortcut; the thumb button below owns keyboard/focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n #track\n class=\"relative touch-none py-4\"\n (click)=\"onTrackClick($event)\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-orientation]=\"vertical() ? 'vertical' : 'horizontal'\"\n >\n <div\n class=\"relative flex items-center\"\n [ngClass]=\"{ 'flex-col h-48': vertical() }\"\n >\n <div\n class=\"relative flex-1 rounded-full bg-
|
|
3403
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SliderComponent, isStandalone: true, selector: "fx-ui-slider", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, vertical: { classPropertyName: "vertical", publicName: "vertical", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { listeners: { "keydown": "onKeydown($event)" } }, viewQueries: [{ propertyName: "trackRef", first: true, predicate: ["track"], descendants: true, isSignal: true }, { propertyName: "thumbRef", first: true, predicate: ["thumb"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n class=\"w-full select-none\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n role=\"group\"\n >\n <!-- Track click is a pointer shortcut; the thumb button below owns keyboard/focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n #track\n class=\"relative touch-none py-4\"\n (click)=\"onTrackClick($event)\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-orientation]=\"vertical() ? 'vertical' : 'horizontal'\"\n >\n <div\n class=\"relative flex items-center\"\n [ngClass]=\"{ 'flex-col h-48': vertical() }\"\n >\n <div\n class=\"relative flex-1 rounded-full bg-track h-2\"\n [ngClass]=\"{ 'w-1 rotate-0 h-full w-auto bg-track': vertical() }\"\n >\n @if (!vertical()) {\n <div\n class=\"absolute left-0 top-0 h-full rounded-full bg-primary\"\n [style.width.%]=\"percent\"\n ></div>\n }\n\n @if (vertical()) {\n <div\n class=\"absolute bottom-0 left-0 w-full rounded-full bg-primary\"\n [style.height.%]=\"percent\"\n style=\"transform-origin: bottom;\"\n ></div>\n }\n\n <button\n #thumb\n type=\"button\"\n class=\"absolute -mt-1 -ml-1.5 w-4 h-4 rounded-full shadow focus:ring-primary bg-primary border-2 border-text-primary\"\n [ngStyle]=\"thumbStyle()\"\n role=\"slider\"\n aria-label=\"Slider\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"value\"\n [attr.aria-disabled]=\"disabled\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n [disabled]=\"disabled\"\n ></button>\n </div>\n </div>\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3159
3404
|
}
|
|
3160
3405
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SliderComponent, decorators: [{
|
|
3161
3406
|
type: Component,
|
|
@@ -3163,7 +3408,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
3163
3408
|
NgClass,
|
|
3164
3409
|
NgStyle,
|
|
3165
3410
|
InputComponent,
|
|
3166
|
-
], template: "<div\n class=\"w-full select-none\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n role=\"group\"\n >\n <!-- Track click is a pointer shortcut; the thumb button below owns keyboard/focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n #track\n class=\"relative touch-none py-4\"\n (click)=\"onTrackClick($event)\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-orientation]=\"vertical() ? 'vertical' : 'horizontal'\"\n >\n <div\n class=\"relative flex items-center\"\n [ngClass]=\"{ 'flex-col h-48': vertical() }\"\n >\n <div\n class=\"relative flex-1 rounded-full bg-
|
|
3411
|
+
], template: "<div\n class=\"w-full select-none\"\n [class.cursor-not-allowed]=\"disabled\"\n [class.opacity-60]=\"disabled\"\n role=\"group\"\n >\n <!-- Track click is a pointer shortcut; the thumb button below owns keyboard/focus. -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n #track\n class=\"relative touch-none py-4\"\n (click)=\"onTrackClick($event)\"\n [attr.aria-disabled]=\"disabled\"\n [attr.aria-orientation]=\"vertical() ? 'vertical' : 'horizontal'\"\n >\n <div\n class=\"relative flex items-center\"\n [ngClass]=\"{ 'flex-col h-48': vertical() }\"\n >\n <div\n class=\"relative flex-1 rounded-full bg-track h-2\"\n [ngClass]=\"{ 'w-1 rotate-0 h-full w-auto bg-track': vertical() }\"\n >\n @if (!vertical()) {\n <div\n class=\"absolute left-0 top-0 h-full rounded-full bg-primary\"\n [style.width.%]=\"percent\"\n ></div>\n }\n\n @if (vertical()) {\n <div\n class=\"absolute bottom-0 left-0 w-full rounded-full bg-primary\"\n [style.height.%]=\"percent\"\n style=\"transform-origin: bottom;\"\n ></div>\n }\n\n <button\n #thumb\n type=\"button\"\n class=\"absolute -mt-1 -ml-1.5 w-4 h-4 rounded-full shadow focus:ring-primary bg-primary border-2 border-text-primary\"\n [ngStyle]=\"thumbStyle()\"\n role=\"slider\"\n aria-label=\"Slider\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"value\"\n [attr.aria-disabled]=\"disabled\"\n tabindex=\"0\"\n (pointerdown)=\"onPointerDown($event)\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\"\n [disabled]=\"disabled\"\n ></button>\n </div>\n </div>\n </div>\n</div>\n" }]
|
|
3167
3412
|
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { trackRef: [{ type: i0.ViewChild, args: ['track', { isSignal: true }] }], thumbRef: [{ type: i0.ViewChild, args: ['thumb', { isSignal: true }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], vertical: [{ type: i0.Input, args: [{ isSignal: true, alias: "vertical", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], onKeydown: [{
|
|
3168
3413
|
type: HostListener,
|
|
3169
3414
|
args: ['keydown', ['$event']]
|
|
@@ -3188,34 +3433,75 @@ class SwitchComponent extends FxComponent {
|
|
|
3188
3433
|
this.onTouched();
|
|
3189
3434
|
}
|
|
3190
3435
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SwitchComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
3191
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SwitchComponent, isStandalone: true, selector: "fx-ui-switch", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSwitch: "onSwitch" }, usesInheritance: true, ngImport: i0, template: "<button\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"!!value\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [disabled]=\"disabled\"\n class=\"relative flex items-center w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"{\n '
|
|
3436
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SwitchComponent, isStandalone: true, selector: "fx-ui-switch", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onSwitch: "onSwitch" }, usesInheritance: true, ngImport: i0, template: "<button\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"!!value\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [disabled]=\"disabled\"\n class=\"relative flex items-center w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'tone-solid-success': value && !disabled,\n 'bg-button-default': !value && !disabled,\n 'bg-bg-hover': disabled\n }\"\n (click)=\"toggle()\"\n>\n @if(value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute left-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">check</i>\n\n }\n @if(!value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute right-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">close</i>\n }\n <div\n class=\"absolute top-0.5 left-0.5 w-4 h-4 bg-text-inverse rounded-full shadow-md transform transition-transform duration-300\"\n [ngClass]=\"{ 'translate-x-5': value }\"\n ></div>\n</button>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3192
3437
|
}
|
|
3193
3438
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SwitchComponent, decorators: [{
|
|
3194
3439
|
type: Component,
|
|
3195
|
-
args: [{ selector: 'fx-ui-switch', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<button\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"!!value\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [disabled]=\"disabled\"\n class=\"relative flex items-center w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"{\n '
|
|
3440
|
+
args: [{ selector: 'fx-ui-switch', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<button\n type=\"button\"\n role=\"switch\"\n [attr.aria-checked]=\"!!value\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [disabled]=\"disabled\"\n class=\"relative flex items-center w-10 h-5 rounded-full transition-colors duration-300 cursor-pointer disabled:cursor-not-allowed focus-visible:ring-2 focus-visible:ring-primary/50\"\n [ngClass]=\"{\n 'tone-solid-success': value && !disabled,\n 'bg-button-default': !value && !disabled,\n 'bg-bg-hover': disabled\n }\"\n (click)=\"toggle()\"\n>\n @if(value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute left-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">check</i>\n\n }\n @if(!value) {\n <i style=\"font-size: 20px;\" class=\"material-symbols-outlined absolute right-0.5 top-0 text-text-inverse\" aria-hidden=\"true\">close</i>\n }\n <div\n class=\"absolute top-0.5 left-0.5 w-4 h-4 bg-text-inverse rounded-full shadow-md transform transition-transform duration-300\"\n [ngClass]=\"{ 'translate-x-5': value }\"\n ></div>\n</button>\n" }]
|
|
3196
3441
|
}], propDecorators: { onSwitch: [{ type: i0.Output, args: ["onSwitch"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
3197
3442
|
|
|
3443
|
+
/**
|
|
3444
|
+
* Một tab của `fx-ui-tab-group`.
|
|
3445
|
+
*
|
|
3446
|
+
* ⚠️ Nội dung tab PHẢI bọc trong `<ng-template>`:
|
|
3447
|
+
*
|
|
3448
|
+
* ```html
|
|
3449
|
+
* <fx-ui-tab [label]="'Components'">
|
|
3450
|
+
* <ng-template>
|
|
3451
|
+
* <app-info-components … />
|
|
3452
|
+
* </ng-template>
|
|
3453
|
+
* </fx-ui-tab>
|
|
3454
|
+
* ```
|
|
3455
|
+
*
|
|
3456
|
+
* Vì sao bắt buộc: trước đây tab dùng `<ng-content>` bọc trong `<ng-template>`
|
|
3457
|
+
* của CHÍNH nó. Cách đó **không hoãn được gì** — `<ng-content>` chỉ CHIẾU node,
|
|
3458
|
+
* còn node do view khai báo (component cha) tạo **eager**. Kết quả: mọi tab đều
|
|
3459
|
+
* khởi tạo và gọi API ngay khi vào màn, dù người dùng chưa mở (đo được trên
|
|
3460
|
+
* scan-info: cả 8 cụm cùng fetch). Nhận `TemplateRef` từ cha thì nội dung chỉ
|
|
3461
|
+
* được dựng khi tab-group stamp template đó.
|
|
3462
|
+
*
|
|
3463
|
+
* Hệ quả cần biết: đổi tab sẽ **huỷ** nội dung tab cũ và dựng lại khi quay lại
|
|
3464
|
+
* — đúng bản chất lazy, giống `matTabContent` của Material.
|
|
3465
|
+
*
|
|
3466
|
+
* `descendants: false` để chỉ bắt `<ng-template>` là con TRỰC TIẾP, tránh nuốt
|
|
3467
|
+
* nhầm template lồng sâu bên trong nội dung tab.
|
|
3468
|
+
*/
|
|
3198
3469
|
class TabComponent {
|
|
3199
3470
|
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
3200
3471
|
icon;
|
|
3201
|
-
|
|
3472
|
+
/** Trailing count badge (V2 'page' variant); null/undefined hides it. */
|
|
3473
|
+
count = input(null, ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
|
|
3474
|
+
/** Tints the count badge (e.g. vulnerabilities tab shows a danger count). */
|
|
3475
|
+
tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
|
|
3476
|
+
content = contentChild.required(TemplateRef, { descendants: false });
|
|
3202
3477
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3203
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.20", type: TabComponent, isStandalone: true, selector: "fx-ui-tab", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null } },
|
|
3478
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.20", type: TabComponent, isStandalone: true, selector: "fx-ui-tab", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "content", first: true, predicate: TemplateRef, isSignal: true }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3204
3479
|
}
|
|
3205
3480
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabComponent, decorators: [{
|
|
3206
3481
|
type: Component,
|
|
3207
|
-
args: [{
|
|
3482
|
+
args: [{
|
|
3483
|
+
selector: 'fx-ui-tab',
|
|
3484
|
+
template: '',
|
|
3485
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3486
|
+
}]
|
|
3208
3487
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], icon: [{
|
|
3209
3488
|
type: Input
|
|
3210
|
-
}], content: [{ type: i0.
|
|
3489
|
+
}], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], content: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { ...{ descendants: false }, isSignal: true }] }] } });
|
|
3211
3490
|
|
|
3212
3491
|
class TabGroupComponent {
|
|
3213
3492
|
renderer;
|
|
3214
3493
|
cdr = inject(ChangeDetectorRef);
|
|
3215
3494
|
tabs = contentChildren(TabComponent, ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
|
|
3216
3495
|
activeIndex = 0;
|
|
3496
|
+
/** Emitted on user selection — enables [(activeIndex)] two-way binding. */
|
|
3497
|
+
activeIndexChange = output();
|
|
3498
|
+
/** 'page' = the V2 workspace tab strip (larger hit area, count badges). */
|
|
3499
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
3217
3500
|
tabListRef = viewChild.required('tabList');
|
|
3218
|
-
underlineRef = viewChild
|
|
3501
|
+
underlineRef = viewChild('underline', ...(ngDevMode ? [{ debugName: "underlineRef" }] : /* istanbul ignore next */ []));
|
|
3502
|
+
pagePillRef = viewChild('pagePill', ...(ngDevMode ? [{ debugName: "pagePillRef" }] : /* istanbul ignore next */ []));
|
|
3503
|
+
/** Last applied pill geometry — skip DOM writes when nothing moved. */
|
|
3504
|
+
pillState = { left: -1, width: -1, painted: false };
|
|
3219
3505
|
canScrollLeft = false;
|
|
3220
3506
|
canScrollRight = false;
|
|
3221
3507
|
animateDelay = 50;
|
|
@@ -3234,8 +3520,12 @@ class TabGroupComponent {
|
|
|
3234
3520
|
}
|
|
3235
3521
|
selectTab(index) {
|
|
3236
3522
|
this.activeIndex = index;
|
|
3523
|
+
this.activeIndexChange.emit(index);
|
|
3237
3524
|
this.scrollToActiveTab(true);
|
|
3238
3525
|
}
|
|
3526
|
+
badgeClass(tab) {
|
|
3527
|
+
return toneBadge(tab.tone());
|
|
3528
|
+
}
|
|
3239
3529
|
/** Roving tablist keyboard nav: arrows move AND activate (selection follows focus). */
|
|
3240
3530
|
onTabKeydown(event, index) {
|
|
3241
3531
|
const count = this.tabs().length;
|
|
@@ -3263,9 +3553,52 @@ class TabGroupComponent {
|
|
|
3263
3553
|
const buttons = this.tabListRef().nativeElement.querySelectorAll('.tab-btn');
|
|
3264
3554
|
buttons[next]?.focus();
|
|
3265
3555
|
}
|
|
3556
|
+
/** Parent-driven activeIndex (deep-link ?tab=, quick-links) must also move the pill. */
|
|
3557
|
+
ngOnChanges(changes) {
|
|
3558
|
+
if (changes['activeIndex'] && !changes['activeIndex'].firstChange) {
|
|
3559
|
+
setTimeout(() => this.scrollToActiveTab(false), 0);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
/**
|
|
3563
|
+
* Re-measure every view check: catches label-width changes (language switch,
|
|
3564
|
+
* count arriving) with cached geometry so the DOM write only happens when
|
|
3565
|
+
* the pill actually moved.
|
|
3566
|
+
*/
|
|
3567
|
+
ngAfterViewChecked() {
|
|
3568
|
+
this.updatePagePill();
|
|
3569
|
+
}
|
|
3570
|
+
/** Slide the 'page' pill to the active tab (offset* is scroll-content relative). */
|
|
3571
|
+
updatePagePill() {
|
|
3572
|
+
const pillEl = this.pagePillRef()?.nativeElement;
|
|
3573
|
+
if (!pillEl)
|
|
3574
|
+
return;
|
|
3575
|
+
const tabButtons = this.tabListRef().nativeElement.querySelectorAll('.tab-btn');
|
|
3576
|
+
const activeTab = tabButtons[this.activeIndex];
|
|
3577
|
+
if (!activeTab)
|
|
3578
|
+
return;
|
|
3579
|
+
const left = activeTab.offsetLeft;
|
|
3580
|
+
const width = activeTab.offsetWidth;
|
|
3581
|
+
if (width === 0)
|
|
3582
|
+
return; // hidden (display:none ancestors) — keep last state
|
|
3583
|
+
if (left === this.pillState.left && width === this.pillState.width)
|
|
3584
|
+
return;
|
|
3585
|
+
// First paint positions without transition — no fly-in from x=0 on load.
|
|
3586
|
+
if (!this.pillState.painted) {
|
|
3587
|
+
this.renderer.setStyle(pillEl, 'transition', 'none');
|
|
3588
|
+
requestAnimationFrame(() => this.renderer.removeStyle(pillEl, 'transition'));
|
|
3589
|
+
this.pillState.painted = true;
|
|
3590
|
+
}
|
|
3591
|
+
this.renderer.setStyle(pillEl, 'width', `${width}px`);
|
|
3592
|
+
this.renderer.setStyle(pillEl, 'transform', `translateX(${left}px)`);
|
|
3593
|
+
this.pillState = { ...this.pillState, left, width };
|
|
3594
|
+
}
|
|
3266
3595
|
updateUnderlinePosition() {
|
|
3596
|
+
// The 'page' variant slides .fx-tabs-pill instead (see updatePagePill);
|
|
3597
|
+
// the underline element only exists on the default variant.
|
|
3598
|
+
const underlineEl = this.underlineRef()?.nativeElement;
|
|
3599
|
+
if (!underlineEl)
|
|
3600
|
+
return;
|
|
3267
3601
|
const tabListEl = this.tabListRef().nativeElement;
|
|
3268
|
-
const underlineEl = this.underlineRef().nativeElement;
|
|
3269
3602
|
const tabButtons = tabListEl.querySelectorAll('.tab-btn');
|
|
3270
3603
|
const activeTab = tabButtons[this.activeIndex];
|
|
3271
3604
|
if (!activeTab || !underlineEl)
|
|
@@ -3323,9 +3656,10 @@ class TabGroupComponent {
|
|
|
3323
3656
|
}
|
|
3324
3657
|
onResize() {
|
|
3325
3658
|
this.updateUnderlinePosition();
|
|
3659
|
+
this.updatePagePill();
|
|
3326
3660
|
}
|
|
3327
3661
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabGroupComponent, deps: [{ token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Component });
|
|
3328
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TabGroupComponent, isStandalone: true, selector: "fx-ui-tab-group", inputs: { activeIndex: "activeIndex" }, host: { listeners: { "window:resize": "onResize()" } }, queries: [{ propertyName: "tabs", predicate: TabComponent, isSignal: true }], viewQueries: [{ propertyName: "tabListRef", first: true, predicate: ["tabList"], descendants: true, isSignal: true }, { propertyName: "underlineRef", first: true, predicate: ["underline"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"relative\">\n <!-- Left scroll button -->\n <button\n class=\"left-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs left\"\n (click)=\"scroll('left')\"\n [style.opacity]=\"canScrollLeft ? 1 : 0\"\n [style.pointerEvents]=\"canScrollLeft ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollLeft ? null : -1\"\n [attr.aria-hidden]=\"!canScrollLeft\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n\n <div\n #tabList\n role=\"tablist\"\n class=\"relative flex overflow-x-auto no-scrollbar scroll-smooth\"\n (scroll)=\"onScroll()\"\n >\n @for (tab of tabs(); track tab; let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === activeIndex\"\n [tabindex]=\"i === activeIndex ? 0 : -1\"\n (click)=\"selectTab(i)\"\n (keydown)=\"onTabKeydown($event, i)\"\n class=\"tab-btn tab-bar-btn\"\n >\n <div class=\"flex flex-row items-center justify-center\">\n @if (tab.icon) {\n <fx-ui-hero-icon\n [icon]=\"tab.icon\"\n [size]=\"20\"\n class=\"flex mr-[2px]\"\n [ngClass]=\"{\n 'text-gradient-primary-start mb-small': i === activeIndex,\n 'text-text-placeholder': i !== activeIndex,\n }\"\n ></fx-ui-hero-icon>\n }\n <div\n [ngClass]=\"\n i === activeIndex ? 'txt-field-label text-gradient-primary-start' : 'txt-placeholder'\n \"\n >\n {{ tab.label() }}\n </div>\n </div>\n </button>\n }\n\n <span #underline class=\"underline\" style=\"width: 0; transform: translateX(0)\"></span>\n </div>\n\n <button\n class=\"right-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs right\"\n (click)=\"scroll('right')\"\n [style.opacity]=\"canScrollRight ? 1 : 0\"\n [style.pointerEvents]=\"canScrollRight ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollRight ? null : -1\"\n [attr.aria-hidden]=\"!canScrollRight\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n</div>\n\n<!-- Tab content -->\n<div class=\"transition-all duration-300 ease-in-out mt-4\">\n @for (tab of tabs(); track tab; let i = $index) {\n @if (i === activeIndex) {\n <div role=\"tabpanel\" class=\"animate-fadeIn\">\n <ng-container *ngTemplateOutlet=\"tab.content()\"></ng-container>\n </div>\n }\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-fadeIn{animation:fadeIn .28s ease-in-out}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.underline{position:absolute;bottom:0;height:2px;background-color:rgb(var(--gradient-primary-start));transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width;transform:translate(0);width:0;margin-left:32px;margin-right:32px}.left-scroll-button{position:absolute;top:0;left:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.right-scroll-button{position:absolute;top:0;right:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.tab-bar-btn{position:relative;padding-inline:1rem;padding-block:.5rem;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}@property --tw-duration{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-duration: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3662
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TabGroupComponent, isStandalone: true, selector: "fx-ui-tab-group", inputs: { activeIndex: { classPropertyName: "activeIndex", publicName: "activeIndex", isSignal: false, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeIndexChange: "activeIndexChange" }, host: { listeners: { "window:resize": "onResize()" } }, queries: [{ propertyName: "tabs", predicate: TabComponent, isSignal: true }], viewQueries: [{ propertyName: "tabListRef", first: true, predicate: ["tabList"], descendants: true, isSignal: true }, { propertyName: "underlineRef", first: true, predicate: ["underline"], descendants: true, isSignal: true }, { propertyName: "pagePillRef", first: true, predicate: ["pagePill"], descendants: true, isSignal: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"relative\">\n <!-- Left scroll button -->\n <button\n class=\"left-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs left\"\n (click)=\"scroll('left')\"\n [style.opacity]=\"canScrollLeft ? 1 : 0\"\n [style.pointerEvents]=\"canScrollLeft ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollLeft ? null : -1\"\n [attr.aria-hidden]=\"!canScrollLeft\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n\n <div\n #tabList\n role=\"tablist\"\n class=\"relative flex overflow-x-auto no-scrollbar scroll-smooth\"\n [class.fx-tabs-frame]=\"variant() === 'page'\"\n (scroll)=\"onScroll()\"\n >\n <!-- 'page' variant: one raised pill that SLIDES to the active tab\n (anh T\u00FA 29/08 \u2014 same motion as fx-ui-segmented) -->\n @if (variant() === 'page') {\n <span #pagePill aria-hidden=\"true\" class=\"fx-tabs-pill\"></span>\n }\n @for (tab of tabs(); track tab; let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === activeIndex\"\n [tabindex]=\"i === activeIndex ? 0 : -1\"\n (click)=\"selectTab(i)\"\n (keydown)=\"onTabKeydown($event, i)\"\n class=\"tab-btn tab-bar-btn\"\n [class.tab-bar-btn-page]=\"variant() === 'page'\"\n >\n <div class=\"flex flex-row items-center justify-center gap-xs\">\n @if (tab.icon) {\n <fx-ui-hero-icon\n [icon]=\"tab.icon\"\n [size]=\"variant() === 'page' ? 14 : 20\"\n class=\"flex mr-[2px]\"\n [ngClass]=\"\n variant() === 'page'\n ? i === activeIndex\n ? 'text-strong'\n : 'text-text-placeholder'\n : i === activeIndex\n ? 'text-accent-fg mb-small'\n : 'text-text-placeholder'\n \"\n ></fx-ui-hero-icon>\n }\n <div\n [ngClass]=\"\n variant() === 'page'\n ? i === activeIndex\n ? 'text-[13px] leading-none font-semibold text-strong [text-box:trim-both_cap_alphabetic]'\n : 'text-[13px] leading-none font-medium text-muted [text-box:trim-both_cap_alphabetic]'\n : i === activeIndex\n ? 'txt-field-label text-accent-fg'\n : 'txt-placeholder'\n \"\n >\n {{ tab.label() }}\n </div>\n @if (tab.count() !== null) {\n <span\n class=\"txt-tag-label px-normal py-[1px] rounded-pill\"\n [ngClass]=\"badgeClass(tab)\"\n style=\"font-family: var(--og-font-figure)\"\n >\n {{ tab.count() }}\n </span>\n }\n </div>\n </button>\n }\n\n @if (variant() !== 'page') {\n <span #underline class=\"underline\" style=\"width: 0; transform: translateX(0)\"></span>\n }\n </div>\n\n <button\n class=\"right-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs right\"\n (click)=\"scroll('right')\"\n [style.opacity]=\"canScrollRight ? 1 : 0\"\n [style.pointerEvents]=\"canScrollRight ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollRight ? null : -1\"\n [attr.aria-hidden]=\"!canScrollRight\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n</div>\n\n<!-- Tab content -->\n<div class=\"transition-all duration-300 ease-in-out mt-4\">\n @for (tab of tabs(); track tab; let i = $index) {\n @if (i === activeIndex) {\n <div role=\"tabpanel\" class=\"animate-fadeIn\">\n <ng-container *ngTemplateOutlet=\"tab.content()\"></ng-container>\n </div>\n }\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-fadeIn{animation:fadeIn .28s ease-in-out}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.underline{position:absolute;bottom:0;height:2px;background-color:rgb(var(--og-accent));transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width;transform:translate(0);width:0;margin-left:32px;margin-right:32px}.left-scroll-button{position:absolute;top:0;left:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.right-scroll-button{position:absolute;top:0;right:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.tab-bar-btn{position:relative;padding-inline:1rem;padding-block:.5rem;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.fx-tabs-frame{width:fit-content;max-width:100%;gap:.125rem;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-sunken));padding:3px}.tab-bar-btn-page{z-index:1;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:transparent;padding-inline:.75rem;padding-block:5px;color:rgb(var(--og-text-muted))}.tab-bar-btn-page[aria-selected=false]:hover{color:rgb(var(--og-text))}.fx-tabs-pill{pointer-events:none;position:absolute;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-raised));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);top:3px;bottom:3px;left:0;width:0;transform:translate(0);transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width}@media(prefers-reduced-motion:reduce){.fx-tabs-pill{transition:none}}@property --tw-duration{syntax: \"*\"; inherits: false;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-duration: initial;--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3329
3663
|
}
|
|
3330
3664
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TabGroupComponent, decorators: [{
|
|
3331
3665
|
type: Component,
|
|
@@ -3334,10 +3668,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
3334
3668
|
NgClass,
|
|
3335
3669
|
NgTemplateOutlet,
|
|
3336
3670
|
InputComponent,
|
|
3337
|
-
], template: "<div class=\"relative\">\n <!-- Left scroll button -->\n <button\n class=\"left-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs left\"\n (click)=\"scroll('left')\"\n [style.opacity]=\"canScrollLeft ? 1 : 0\"\n [style.pointerEvents]=\"canScrollLeft ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollLeft ? null : -1\"\n [attr.aria-hidden]=\"!canScrollLeft\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n\n <div\n #tabList\n role=\"tablist\"\n class=\"relative flex overflow-x-auto no-scrollbar scroll-smooth\"\n (scroll)=\"onScroll()\"\n >\n @for (tab of tabs(); track tab; let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === activeIndex\"\n [tabindex]=\"i === activeIndex ? 0 : -1\"\n (click)=\"selectTab(i)\"\n (keydown)=\"onTabKeydown($event, i)\"\n class=\"tab-btn tab-bar-btn\"\n >\n <div class=\"flex flex-row items-center justify-center\">\n @if (tab.icon) {\n <fx-ui-hero-icon\n [icon]=\"tab.icon\"\n [size]=\"20\"\n class=\"flex mr-[2px]\"\n
|
|
3671
|
+
], template: "<div class=\"relative\">\n <!-- Left scroll button -->\n <button\n class=\"left-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs left\"\n (click)=\"scroll('left')\"\n [style.opacity]=\"canScrollLeft ? 1 : 0\"\n [style.pointerEvents]=\"canScrollLeft ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollLeft ? null : -1\"\n [attr.aria-hidden]=\"!canScrollLeft\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\" />\n </svg>\n </button>\n\n <div\n #tabList\n role=\"tablist\"\n class=\"relative flex overflow-x-auto no-scrollbar scroll-smooth\"\n [class.fx-tabs-frame]=\"variant() === 'page'\"\n (scroll)=\"onScroll()\"\n >\n <!-- 'page' variant: one raised pill that SLIDES to the active tab\n (anh T\u00FA 29/08 \u2014 same motion as fx-ui-segmented) -->\n @if (variant() === 'page') {\n <span #pagePill aria-hidden=\"true\" class=\"fx-tabs-pill\"></span>\n }\n @for (tab of tabs(); track tab; let i = $index) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === activeIndex\"\n [tabindex]=\"i === activeIndex ? 0 : -1\"\n (click)=\"selectTab(i)\"\n (keydown)=\"onTabKeydown($event, i)\"\n class=\"tab-btn tab-bar-btn\"\n [class.tab-bar-btn-page]=\"variant() === 'page'\"\n >\n <div class=\"flex flex-row items-center justify-center gap-xs\">\n @if (tab.icon) {\n <fx-ui-hero-icon\n [icon]=\"tab.icon\"\n [size]=\"variant() === 'page' ? 14 : 20\"\n class=\"flex mr-[2px]\"\n [ngClass]=\"\n variant() === 'page'\n ? i === activeIndex\n ? 'text-strong'\n : 'text-text-placeholder'\n : i === activeIndex\n ? 'text-accent-fg mb-small'\n : 'text-text-placeholder'\n \"\n ></fx-ui-hero-icon>\n }\n <div\n [ngClass]=\"\n variant() === 'page'\n ? i === activeIndex\n ? 'text-[13px] leading-none font-semibold text-strong [text-box:trim-both_cap_alphabetic]'\n : 'text-[13px] leading-none font-medium text-muted [text-box:trim-both_cap_alphabetic]'\n : i === activeIndex\n ? 'txt-field-label text-accent-fg'\n : 'txt-placeholder'\n \"\n >\n {{ tab.label() }}\n </div>\n @if (tab.count() !== null) {\n <span\n class=\"txt-tag-label px-normal py-[1px] rounded-pill\"\n [ngClass]=\"badgeClass(tab)\"\n style=\"font-family: var(--og-font-figure)\"\n >\n {{ tab.count() }}\n </span>\n }\n </div>\n </button>\n }\n\n @if (variant() !== 'page') {\n <span #underline class=\"underline\" style=\"width: 0; transform: translateX(0)\"></span>\n }\n </div>\n\n <button\n class=\"right-scroll-button\"\n type=\"button\"\n aria-label=\"Scroll tabs right\"\n (click)=\"scroll('right')\"\n [style.opacity]=\"canScrollRight ? 1 : 0\"\n [style.pointerEvents]=\"canScrollRight ? 'auto' : 'none'\"\n [attr.tabindex]=\"canScrollRight ? null : -1\"\n [attr.aria-hidden]=\"!canScrollRight\"\n >\n <svg\n aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n class=\"h-5 w-5 text-text-primary\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\" />\n </svg>\n </button>\n</div>\n\n<!-- Tab content -->\n<div class=\"transition-all duration-300 ease-in-out mt-4\">\n @for (tab of tabs(); track tab; let i = $index) {\n @if (i === activeIndex) {\n <div role=\"tabpanel\" class=\"animate-fadeIn\">\n <ng-container *ngTemplateOutlet=\"tab.content()\"></ng-container>\n </div>\n }\n }\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;@keyframes fadeIn{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-fadeIn{animation:fadeIn .28s ease-in-out}.no-scrollbar::-webkit-scrollbar{display:none}.no-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.underline{position:absolute;bottom:0;height:2px;background-color:rgb(var(--og-accent));transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width;transform:translate(0);width:0;margin-left:32px;margin-right:32px}.left-scroll-button{position:absolute;top:0;left:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.right-scroll-button{position:absolute;top:0;right:0;z-index:10;display:flex;height:100%;align-items:center;background-color:rgb(var(--bg-primary));padding-inline:.5rem;transition-property:opacity;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.tab-bar-btn{position:relative;padding-inline:1rem;padding-block:.5rem;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s}.fx-tabs-frame{width:fit-content;max-width:100%;gap:.125rem;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-sunken));padding:3px}.tab-bar-btn-page{z-index:1;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:transparent;padding-inline:.75rem;padding-block:5px;color:rgb(var(--og-text-muted))}.tab-bar-btn-page[aria-selected=false]:hover{color:rgb(var(--og-text))}.fx-tabs-pill{pointer-events:none;position:absolute;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-raised));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);top:3px;bottom:3px;left:0;width:0;transform:translate(0);transition:transform .25s cubic-bezier(.2,.9,.2,1),width .25s cubic-bezier(.2,.9,.2,1);will-change:transform,width}@media(prefers-reduced-motion:reduce){.fx-tabs-pill{transition:none}}@property --tw-duration{syntax: \"*\"; inherits: false;}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-duration: initial;--tw-border-style: solid;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
|
|
3338
3672
|
}], ctorParameters: () => [{ type: i0.Renderer2 }], propDecorators: { tabs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TabComponent), { isSignal: true }] }], activeIndex: [{
|
|
3339
3673
|
type: Input
|
|
3340
|
-
}], tabListRef: [{ type: i0.ViewChild, args: ['tabList', { isSignal: true }] }], underlineRef: [{ type: i0.ViewChild, args: ['underline', { isSignal: true }] }], onResize: [{
|
|
3674
|
+
}], activeIndexChange: [{ type: i0.Output, args: ["activeIndexChange"] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], tabListRef: [{ type: i0.ViewChild, args: ['tabList', { isSignal: true }] }], underlineRef: [{ type: i0.ViewChild, args: ['underline', { isSignal: true }] }], pagePillRef: [{ type: i0.ViewChild, args: ['pagePill', { isSignal: true }] }], onResize: [{
|
|
3341
3675
|
type: HostListener,
|
|
3342
3676
|
args: ['window:resize']
|
|
3343
3677
|
}] } });
|
|
@@ -3367,11 +3701,11 @@ class TagComponent {
|
|
|
3367
3701
|
this.clicked.emit();
|
|
3368
3702
|
}
|
|
3369
3703
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TagComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3370
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TagComponent, isStandalone: true, selector: "fx-ui-tag", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, solid: { classPropertyName: "solid", publicName: "solid", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<!-- Interactive semantics only when a (clicked) listener is attached. -->\n<div\n (click)=\"onClick()\"\n (keydown.enter)=\"onClick()\"\n (keydown.space)=\"$event.preventDefault(); onClick()\"\n [attr.role]=\"clicked.observed ? 'button' : null\"\n [attr.tabindex]=\"clicked.observed ? 0 : null\"\n [class]=\"\n `flex flex-row gap-small ${rounded() ? 'tag-round' : 'tag-square'} tag-${type()}${solid() ? '-solid' : ''}${clicked.observed ? ' cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-
|
|
3704
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TagComponent, isStandalone: true, selector: "fx-ui-tag", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, solid: { classPropertyName: "solid", publicName: "solid", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clicked: "clicked" }, ngImport: i0, template: "<!-- Interactive semantics only when a (clicked) listener is attached. -->\n<div\n (click)=\"onClick()\"\n (keydown.enter)=\"onClick()\"\n (keydown.space)=\"$event.preventDefault(); onClick()\"\n [attr.role]=\"clicked.observed ? 'button' : null\"\n [attr.tabindex]=\"clicked.observed ? 0 : null\"\n [class]=\"\n `flex flex-row gap-small ${rounded() ? 'tag-round' : 'tag-square'} tag-${type()}${solid() ? '-solid' : ''}${clicked.observed ? ' cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-surface' : ''}`\"\n [icon]=\"icon\"\n [size]=\"18\"\n ></fx-ui-hero-icon>\n }\n\n <div [class]=\"`txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-surface font-medium' : ''}`\">\n {{ label() }}\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3371
3705
|
}
|
|
3372
3706
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TagComponent, decorators: [{
|
|
3373
3707
|
type: Component,
|
|
3374
|
-
args: [{ selector: 'fx-ui-tag', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<!-- Interactive semantics only when a (clicked) listener is attached. -->\n<div\n (click)=\"onClick()\"\n (keydown.enter)=\"onClick()\"\n (keydown.space)=\"$event.preventDefault(); onClick()\"\n [attr.role]=\"clicked.observed ? 'button' : null\"\n [attr.tabindex]=\"clicked.observed ? 0 : null\"\n [class]=\"\n `flex flex-row gap-small ${rounded() ? 'tag-round' : 'tag-square'} tag-${type()}${solid() ? '-solid' : ''}${clicked.observed ? ' cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-
|
|
3708
|
+
args: [{ selector: 'fx-ui-tag', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<!-- Interactive semantics only when a (clicked) listener is attached. -->\n<div\n (click)=\"onClick()\"\n (keydown.enter)=\"onClick()\"\n (keydown.space)=\"$event.preventDefault(); onClick()\"\n [attr.role]=\"clicked.observed ? 'button' : null\"\n [attr.tabindex]=\"clicked.observed ? 0 : null\"\n [class]=\"\n `flex flex-row gap-small ${rounded() ? 'tag-round' : 'tag-square'} tag-${type()}${solid() ? '-solid' : ''}${clicked.observed ? ' cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50' : ''}`\n \"\n>\n @if (icon) {\n <fx-ui-hero-icon\n [class]=\"`flex txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-surface' : ''}`\"\n [icon]=\"icon\"\n [size]=\"18\"\n ></fx-ui-hero-icon>\n }\n\n <div [class]=\"`txt-tag-${type()} ${solid() && type() !== 'default' ? 'text-surface font-medium' : ''}`\">\n {{ label() }}\n </div>\n</div>\n" }]
|
|
3375
3709
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], icon: [{
|
|
3376
3710
|
type: Input
|
|
3377
3711
|
}], solid: [{ type: i0.Input, args: [{ isSignal: true, alias: "solid", required: false }] }], clicked: [{
|
|
@@ -3464,11 +3798,11 @@ class TreeDiagram {
|
|
|
3464
3798
|
this.cdr.detectChanges();
|
|
3465
3799
|
}
|
|
3466
3800
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TreeDiagram, deps: [{ token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
3467
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TreeDiagram, isStandalone: true, selector: "fx-ui-tree-diagram", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "root", first: true, predicate: ["root"], descendants: true, isSignal: true }, { propertyName: "children", predicate: ["child"], descendants: true }], ngImport: i0, template: "<div class=\"relative p-8\" #container>\n\n <!-- ROOT -->\n <div\n #root\n class=\"mx-auto w-fit bg-
|
|
3801
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: TreeDiagram, isStandalone: true, selector: "fx-ui-tree-diagram", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null } }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "root", first: true, predicate: ["root"], descendants: true, isSignal: true }, { propertyName: "children", predicate: ["child"], descendants: true }], ngImport: i0, template: "<div class=\"relative p-8\" #container>\n\n <!-- ROOT -->\n <div\n #root\n class=\"mx-auto w-fit bg-accent text-accent-contrast px-7 py-3 rounded-md font-semibold text-center\"\n >\n {{ data().label }}\n </div>\n\n <!-- CHILDREN -->\n <div class=\"mt-16 flex justify-center gap-16\">\n @for (child of data().children; track child) {\n <div\n #child\n class=\"w-48 bg-surface-raised text-strong border border-border px-4 py-4 rounded-lg text-center\"\n >\n <div class=\"font-semibold text-lg\">{{ child.label }}</div>\n <div class=\"mt-1 text-sm opacity-90\">{{ child.status }}</div>\n </div>\n }\n </div>\n\n <!-- SVG CONNECTORS -->\n <svg\n class=\"absolute inset-0 pointer-events-none\"\n [attr.width]=\"svgWidth\"\n [attr.height]=\"svgHeight\"\n >\n @for (line of lines; track line) {\n <line\n [attr.x1]=\"line.x1\"\n [attr.y1]=\"line.y1\"\n [attr.x2]=\"line.x2\"\n [attr.y2]=\"line.y2\"\n stroke=\"rgb(var(--og-border-strong))\"\n stroke-width=\"2\"\n />\n }\n </svg>\n\n</div>\n", styles: [""], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3468
3802
|
}
|
|
3469
3803
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: TreeDiagram, decorators: [{
|
|
3470
3804
|
type: Component,
|
|
3471
|
-
args: [{ selector: 'fx-ui-tree-diagram', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"relative p-8\" #container>\n\n <!-- ROOT -->\n <div\n #root\n class=\"mx-auto w-fit bg-
|
|
3805
|
+
args: [{ selector: 'fx-ui-tree-diagram', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"relative p-8\" #container>\n\n <!-- ROOT -->\n <div\n #root\n class=\"mx-auto w-fit bg-accent text-accent-contrast px-7 py-3 rounded-md font-semibold text-center\"\n >\n {{ data().label }}\n </div>\n\n <!-- CHILDREN -->\n <div class=\"mt-16 flex justify-center gap-16\">\n @for (child of data().children; track child) {\n <div\n #child\n class=\"w-48 bg-surface-raised text-strong border border-border px-4 py-4 rounded-lg text-center\"\n >\n <div class=\"font-semibold text-lg\">{{ child.label }}</div>\n <div class=\"mt-1 text-sm opacity-90\">{{ child.status }}</div>\n </div>\n }\n </div>\n\n <!-- SVG CONNECTORS -->\n <svg\n class=\"absolute inset-0 pointer-events-none\"\n [attr.width]=\"svgWidth\"\n [attr.height]=\"svgHeight\"\n >\n @for (line of lines; track line) {\n <line\n [attr.x1]=\"line.x1\"\n [attr.y1]=\"line.y1\"\n [attr.x2]=\"line.x2\"\n [attr.y2]=\"line.y2\"\n stroke=\"rgb(var(--og-border-strong))\"\n stroke-width=\"2\"\n />\n }\n </svg>\n\n</div>\n" }]
|
|
3472
3806
|
}], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], container: [{ type: i0.ViewChild, args: ['container', { isSignal: true }] }], root: [{ type: i0.ViewChild, args: ['root', { isSignal: true }] }], children: [{
|
|
3473
3807
|
type: ViewChildren,
|
|
3474
3808
|
args: ['child']
|
|
@@ -3509,7 +3843,7 @@ class KanbanBoardComponent {
|
|
|
3509
3843
|
});
|
|
3510
3844
|
}
|
|
3511
3845
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: KanbanBoardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3512
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: KanbanBoardComponent, isStandalone: true, selector: "fx-ui-kanban-board", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { columnChanged: "columnChanged" }, ngImport: i0, template: "<div class=\"kanban-board\">\n <div class=\"kanban-columns\">\n @for (column of columns(); track column.id) {\n <div class=\"kanban-column\">\n <div class=\"kanban-column-header\">\n <h3 class=\"kanban-column-title\">{{ column.title }}</h3>\n <span class=\"kanban-column-count\">{{ column.items.length }}</span>\n </div>\n \n <div\n class=\"kanban-column-content\"\n cdkDropList\n [id]=\"column.id\"\n [cdkDropListData]=\"column.items\"\n [cdkDropListConnectedTo]=\"getColumnIds()\"\n (cdkDropListDropped)=\"onDrop($event, column.id)\"\n >\n @for (item of column.items; track $index) {\n <div class=\"kanban-item\" cdkDrag>\n <div class=\"kanban-item-placeholder\" *cdkDragPlaceholder></div>\n \n @if (itemTemplate) {\n <ng-container\n *ngTemplateOutlet=\"itemTemplate; context: { $implicit: item, index: $index }\"\n ></ng-container>\n } @else {\n <div class=\"kanban-item-default\">\n {{ item }}\n </div>\n }\n </div>\n }\n \n @if (column.items.length === 0) {\n <div class=\"kanban-column-empty\">\n Drop items here\n </div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;.kanban-board{height:100%;width:100%;overflow-x:auto}.kanban-columns{display:flex;min-height:100%;gap:calc(var(--spacing, .25rem) * 4);padding:calc(var(--spacing, .25rem) * 4)}.kanban-column{display:flex;min-width:300px;flex-direction:column;border-radius:.5rem;background-color:var(--color-gray-50, oklch(98.5% .002 247.839));flex-shrink:0}@media(prefers-color-scheme:dark){.kanban-column{background-color:var(--color-gray-800, oklch(27.8% .033 256.848))}}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200, oklch(92.8% .006 264.531));padding:calc(var(--spacing, .25rem) * 4)}@media(prefers-color-scheme:dark){.kanban-column-header{border-color:var(--color-gray-700, oklch(37.3% .034 259.733))}}.kanban-column-title{margin:0;font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:var(--color-gray-900, oklch(21% .034 264.665))}@media(prefers-color-scheme:dark){.kanban-column-title{color:var(--color-gray-100, oklch(96.7% .003 264.542))}}.kanban-column-count{border-radius:calc(infinity * 1px);background-color:var(--color-gray-200, oklch(92.8% .006 264.531));padding-inline:calc(var(--spacing, .25rem) * 2);padding-block:var(--spacing, .25rem);font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-gray-700, oklch(37.3% .034 259.733))}@media(prefers-color-scheme:dark){.kanban-column-count{background-color:var(--color-gray-700, oklch(37.3% .034 259.733))}}@media(prefers-color-scheme:dark){.kanban-column-count{color:var(--color-gray-300, oklch(87.2% .01 258.338))}}.kanban-column-content{flex:1;overflow-y:auto;padding:calc(var(--spacing, .25rem) * 4);min-height:200px}:where(.kanban-column-content>:not(:last-child)){--tw-space-y-reverse: 0;margin-block-start:calc(calc(var(--spacing, .25rem) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing, .25rem) * 3) * calc(1 - var(--tw-space-y-reverse)))}.kanban-item{border-radius:.5rem;border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200, oklch(92.8% .006 264.531));background-color:var(--color-white, #fff);--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:box-shadow;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s;cursor:move}@media(prefers-color-scheme:dark){.kanban-item{border-color:var(--color-gray-600, oklch(44.6% .03 256.802))}}@media(prefers-color-scheme:dark){.kanban-item{background-color:var(--color-gray-700, oklch(37.3% .034 259.733))}}.kanban-item:hover{--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.kanban-item-default{padding:calc(var(--spacing, .25rem) * 4);color:var(--color-gray-900, oklch(21% .034 264.665))}@media(prefers-color-scheme:dark){.kanban-item-default{color:var(--color-gray-100, oklch(96.7% .003 264.542))}}.kanban-item-placeholder{border-radius:.5rem;border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:var(--color-gray-400, oklch(70.7% .022 261.325));background-color:var(--color-gray-200, oklch(92.8% .006 264.531));opacity:50%;min-height:60px}@media(prefers-color-scheme:dark){.kanban-item-placeholder{border-color:var(--color-gray-500, oklch(55.1% .027 264.364))}}@media(prefers-color-scheme:dark){.kanban-item-placeholder{background-color:var(--color-gray-600, oklch(44.6% .03 256.802))}}.kanban-column-empty{display:flex;height:calc(var(--spacing, .25rem) * 32);align-items:center;justify-content:center;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:var(--color-gray-400, oklch(70.7% .022 261.325));border-radius:.5rem;border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:var(--color-gray-300, oklch(87.2% .01 258.338))}@media(prefers-color-scheme:dark){.kanban-column-empty{color:var(--color-gray-500, oklch(55.1% .027 264.364))}}@media(prefers-color-scheme:dark){.kanban-column-empty{border-color:var(--color-gray-600, oklch(44.6% .03 256.802))}}.cdk-drag-preview{border-radius:.5rem;opacity:80%;--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.kanban-column-content.cdk-drop-list-dragging .kanban-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-space-y-reverse{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-duration{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-font-weight: initial;--tw-space-y-reverse: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-duration: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3846
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: KanbanBoardComponent, isStandalone: true, selector: "fx-ui-kanban-board", inputs: { columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, itemTemplate: { classPropertyName: "itemTemplate", publicName: "itemTemplate", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { columnChanged: "columnChanged" }, ngImport: i0, template: "<div class=\"kanban-board\">\n <div class=\"kanban-columns\">\n @for (column of columns(); track column.id) {\n <div class=\"kanban-column\">\n <div class=\"kanban-column-header\">\n <h3 class=\"kanban-column-title\">{{ column.title }}</h3>\n <span class=\"kanban-column-count\">{{ column.items.length }}</span>\n </div>\n \n <div\n class=\"kanban-column-content\"\n cdkDropList\n [id]=\"column.id\"\n [cdkDropListData]=\"column.items\"\n [cdkDropListConnectedTo]=\"getColumnIds()\"\n (cdkDropListDropped)=\"onDrop($event, column.id)\"\n >\n @for (item of column.items; track $index) {\n <div class=\"kanban-item\" cdkDrag>\n <div class=\"kanban-item-placeholder\" *cdkDragPlaceholder></div>\n \n @if (itemTemplate) {\n <ng-container\n *ngTemplateOutlet=\"itemTemplate; context: { $implicit: item, index: $index }\"\n ></ng-container>\n } @else {\n <div class=\"kanban-item-default\">\n {{ item }}\n </div>\n }\n </div>\n }\n \n @if (column.items.length === 0) {\n <div class=\"kanban-column-empty\">\n Drop items here\n </div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;.kanban-board{height:100%;width:100%;overflow-x:auto}.kanban-columns{display:flex;min-height:100%;gap:calc(var(--spacing, .25rem) * 4);padding:calc(var(--spacing, .25rem) * 4)}.kanban-column{display:flex;min-width:300px;flex-direction:column;border-radius:var(--og-radius-card);background-color:rgb(var(--og-surface-sunken));flex-shrink:0}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:rgb(var(--og-border));padding:calc(var(--spacing, .25rem) * 4)}.kanban-column-title{margin:0;font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text))}.kanban-column-count{border-radius:calc(infinity * 1px);background-color:rgb(var(--og-neutral-soft-bg));padding-inline:calc(var(--spacing, .25rem) * 2);padding-block:var(--spacing, .25rem);font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--og-neutral-soft-fg))}.kanban-column-content{flex:1;overflow-y:auto;padding:calc(var(--spacing, .25rem) * 4);min-height:200px}:where(.kanban-column-content>:not(:last-child)){--tw-space-y-reverse: 0;margin-block-start:calc(calc(var(--spacing, .25rem) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing, .25rem) * 3) * calc(1 - var(--tw-space-y-reverse)))}.kanban-item{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-raised));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:box-shadow;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s;cursor:move}.kanban-item:hover{--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.kanban-item-default{padding:calc(var(--spacing, .25rem) * 4);color:rgb(var(--og-text))}.kanban-item-placeholder{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:rgb(var(--border-strong));background-color:rgb(var(--og-neutral-soft-bg));opacity:50%;min-height:60px}.kanban-column-empty{display:flex;height:calc(var(--spacing, .25rem) * 32);align-items:center;justify-content:center;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:rgb(var(--og-text-faint));border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:rgb(var(--og-border))}.cdk-drag-preview{border-radius:var(--og-radius-card);opacity:80%;--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.kanban-column-content.cdk-drop-list-dragging .kanban-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-space-y-reverse{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-duration{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-font-weight: initial;--tw-space-y-reverse: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-duration: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3513
3847
|
}
|
|
3514
3848
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: KanbanBoardComponent, decorators: [{
|
|
3515
3849
|
type: Component,
|
|
@@ -3518,7 +3852,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
3518
3852
|
CdkDrag,
|
|
3519
3853
|
CdkDragPlaceholder,
|
|
3520
3854
|
NgTemplateOutlet,
|
|
3521
|
-
], template: "<div class=\"kanban-board\">\n <div class=\"kanban-columns\">\n @for (column of columns(); track column.id) {\n <div class=\"kanban-column\">\n <div class=\"kanban-column-header\">\n <h3 class=\"kanban-column-title\">{{ column.title }}</h3>\n <span class=\"kanban-column-count\">{{ column.items.length }}</span>\n </div>\n \n <div\n class=\"kanban-column-content\"\n cdkDropList\n [id]=\"column.id\"\n [cdkDropListData]=\"column.items\"\n [cdkDropListConnectedTo]=\"getColumnIds()\"\n (cdkDropListDropped)=\"onDrop($event, column.id)\"\n >\n @for (item of column.items; track $index) {\n <div class=\"kanban-item\" cdkDrag>\n <div class=\"kanban-item-placeholder\" *cdkDragPlaceholder></div>\n \n @if (itemTemplate) {\n <ng-container\n *ngTemplateOutlet=\"itemTemplate; context: { $implicit: item, index: $index }\"\n ></ng-container>\n } @else {\n <div class=\"kanban-item-default\">\n {{ item }}\n </div>\n }\n </div>\n }\n \n @if (column.items.length === 0) {\n <div class=\"kanban-column-empty\">\n Drop items here\n </div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;.kanban-board{height:100%;width:100%;overflow-x:auto}.kanban-columns{display:flex;min-height:100%;gap:calc(var(--spacing, .25rem) * 4);padding:calc(var(--spacing, .25rem) * 4)}.kanban-column{display:flex;min-width:300px;flex-direction:column;border-radius
|
|
3855
|
+
], template: "<div class=\"kanban-board\">\n <div class=\"kanban-columns\">\n @for (column of columns(); track column.id) {\n <div class=\"kanban-column\">\n <div class=\"kanban-column-header\">\n <h3 class=\"kanban-column-title\">{{ column.title }}</h3>\n <span class=\"kanban-column-count\">{{ column.items.length }}</span>\n </div>\n \n <div\n class=\"kanban-column-content\"\n cdkDropList\n [id]=\"column.id\"\n [cdkDropListData]=\"column.items\"\n [cdkDropListConnectedTo]=\"getColumnIds()\"\n (cdkDropListDropped)=\"onDrop($event, column.id)\"\n >\n @for (item of column.items; track $index) {\n <div class=\"kanban-item\" cdkDrag>\n <div class=\"kanban-item-placeholder\" *cdkDragPlaceholder></div>\n \n @if (itemTemplate) {\n <ng-container\n *ngTemplateOutlet=\"itemTemplate; context: { $implicit: item, index: $index }\"\n ></ng-container>\n } @else {\n <div class=\"kanban-item-default\">\n {{ item }}\n </div>\n }\n </div>\n }\n \n @if (column.items.length === 0) {\n <div class=\"kanban-column-empty\">\n Drop items here\n </div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";@layer properties;.kanban-board{height:100%;width:100%;overflow-x:auto}.kanban-columns{display:flex;min-height:100%;gap:calc(var(--spacing, .25rem) * 4);padding:calc(var(--spacing, .25rem) * 4)}.kanban-column{display:flex;min-width:300px;flex-direction:column;border-radius:var(--og-radius-card);background-color:rgb(var(--og-surface-sunken));flex-shrink:0}.kanban-column-header{display:flex;align-items:center;justify-content:space-between;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:rgb(var(--og-border));padding:calc(var(--spacing, .25rem) * 4)}.kanban-column-title{margin:0;font-size:var(--text-lg, 1.125rem);line-height:var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text))}.kanban-column-count{border-radius:calc(infinity * 1px);background-color:rgb(var(--og-neutral-soft-bg));padding-inline:calc(var(--spacing, .25rem) * 2);padding-block:var(--spacing, .25rem);font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--og-neutral-soft-fg))}.kanban-column-content{flex:1;overflow-y:auto;padding:calc(var(--spacing, .25rem) * 4);min-height:200px}:where(.kanban-column-content>:not(:last-child)){--tw-space-y-reverse: 0;margin-block-start:calc(calc(var(--spacing, .25rem) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing, .25rem) * 3) * calc(1 - var(--tw-space-y-reverse)))}.kanban-item{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--og-border));background-color:rgb(var(--og-surface-raised));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:box-shadow;transition-timing-function:var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration, var(--default-transition-duration, .15s));--tw-duration: .2s;transition-duration:.2s;cursor:move}.kanban-item:hover{--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.kanban-item-default{padding:calc(var(--spacing, .25rem) * 4);color:rgb(var(--og-text))}.kanban-item-placeholder{border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:rgb(var(--border-strong));background-color:rgb(var(--og-neutral-soft-bg));opacity:50%;min-height:60px}.kanban-column-empty{display:flex;height:calc(var(--spacing, .25rem) * 32);align-items:center;justify-content:center;font-size:.875rem;line-height:var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));color:rgb(var(--og-text-faint));border-radius:var(--og-radius-card);border-style:var(--tw-border-style);border-width:2px;--tw-border-style: dashed;border-style:dashed;border-color:rgb(var(--og-border))}.cdk-drag-preview{border-radius:var(--og-radius-card);opacity:80%;--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.kanban-column-content.cdk-drop-list-dragging .kanban-item:not(.cdk-drag-placeholder){transition:transform .25s cubic-bezier(0,0,.2,1)}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-space-y-reverse{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-duration{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-font-weight: initial;--tw-space-y-reverse: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-duration: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
|
|
3522
3856
|
}], propDecorators: { columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], itemTemplate: [{
|
|
3523
3857
|
type: Input
|
|
3524
3858
|
}], columnChanged: [{ type: i0.Output, args: ["columnChanged"] }] } });
|
|
@@ -3527,8 +3861,15 @@ class DrawerComponent {
|
|
|
3527
3861
|
router;
|
|
3528
3862
|
cdr = inject(ChangeDetectorRef);
|
|
3529
3863
|
items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
3864
|
+
/** V2: bottom-pinned section (Administration group) — same item rendering,
|
|
3865
|
+
separated by a rule, above the [drawerFooter] slot and version. */
|
|
3866
|
+
footerItems = input([], ...(ngDevMode ? [{ debugName: "footerItems" }] : /* istanbul ignore next */ []));
|
|
3530
3867
|
isExpanded = true;
|
|
3531
3868
|
logo;
|
|
3869
|
+
/** Wordmark cạnh logo, chỉ hiện khi drawer mở. Bỏ trống = chỉ có mark. */
|
|
3870
|
+
logoText;
|
|
3871
|
+
/** Dòng phụ dưới logoText (tagline/workspace). Chỉ hiện khi drawer mở. */
|
|
3872
|
+
logoSubtext;
|
|
3532
3873
|
version;
|
|
3533
3874
|
toggleExpanded = output();
|
|
3534
3875
|
itemClicked = output();
|
|
@@ -3620,6 +3961,7 @@ class DrawerComponent {
|
|
|
3620
3961
|
return false;
|
|
3621
3962
|
};
|
|
3622
3963
|
expandParents(this.items());
|
|
3964
|
+
expandParents(this.footerItems());
|
|
3623
3965
|
}
|
|
3624
3966
|
getAccordionState(itemId) {
|
|
3625
3967
|
return this.isAccordionExpanded(itemId) ? 'expanded' : 'collapsed';
|
|
@@ -3628,7 +3970,7 @@ class DrawerComponent {
|
|
|
3628
3970
|
return this.isExpanded ? 'expanded' : 'collapsed';
|
|
3629
3971
|
}
|
|
3630
3972
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: DrawerComponent, deps: [{ token: i2$1.Router }], target: i0.ɵɵFactoryTarget.Component });
|
|
3631
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: DrawerComponent, isStandalone: true, selector: "fx-ui-drawer", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, isExpanded: { classPropertyName: "isExpanded", publicName: "isExpanded", isSignal: false, isRequired: false, transformFunction: null }, logo: { classPropertyName: "logo", publicName: "logo", isSignal: false, isRequired: false, transformFunction: null }, version: { classPropertyName: "version", publicName: "version", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { toggleExpanded: "toggleExpanded", itemClicked: "itemClicked" }, ngImport: i0, template: "<div class=\"drawer-host-wrapper\">\n <a\n href=\"/\"\n class=\"h-[60px]\"\n [ngClass]=\"{ 'flex items-center justify-center': !isExpanded, 'p-4': isExpanded }\"\n >\n @if (logo) {\n <img\n [src]=\"logo\"\n alt=\"Home\"\n class=\"h-[20px] w-auto transition-all duration-300 ease-in-out\"\n [ngClass]=\"{ 'scale-90': !isExpanded, 'ml-5': isExpanded }\"\n />\n } @else {\n <div class=\"h-[30px] w-auto transition-all duration-300 ease-in-out\"></div>\n }\n </a>\n <div class=\"drawer-container\" [@drawerExpand]=\"getDrawerState()\">\n <!-- Navigation Items -->\n <nav class=\"drawer-nav\">\n @for (item of items(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n\n <!-- Version -->\n @if (version) {\n <div class=\"drawer-version\" [matTooltip]=\"!isExpanded ? version : ''\" matTooltipPosition=\"right\">\n @if (isExpanded) {\n <span>{{ version }}</span>\n }\n @if (!isExpanded) {\n <span class=\"drawer-version-dot\"></span>\n }\n </div>\n }\n </div>\n\n <!-- Floating Toggle Button on right border -->\n <button\n type=\"button\"\n class=\"toggle-btn\"\n [attr.aria-label]=\"isExpanded ? 'Collapse navigation' : 'Expand navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n (click)=\"toggleDrawer()\"\n [matTooltip]=\"isExpanded ? 'Collapse' : 'Expand'\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n matTooltipPosition=\"right\"\n >\n <fx-ui-hero-icon\n [icon]=\"isExpanded ? 'chevron-left' : 'chevron-right'\"\n [size]=\"16\"\n class=\"text-text-primary\"\n ></fx-ui-hero-icon>\n </button>\n</div>\n\n<!-- Recursive Template for Drawer Items -->\n<ng-template #drawerItem let-item let-level=\"level\">\n <div class=\"drawer-item-wrapper\">\n <!-- Item Button/Link -->\n @if (item.route && !item.children) {\n <a\n [routerLink]=\"item.route\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n </div>\n </a>\n }\n\n @if (!item.route || item.children) {\n <button\n type=\"button\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-sm font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n @if (isExpanded && item.children && item.children.length > 0) {\n <fx-ui-hero-icon\n [icon]=\"'chevron-down'\"\n [size]=\"16\"\n class=\"chevron-icon\"\n [class.rotated]=\"isAccordionExpanded(item.id)\"\n ></fx-ui-hero-icon>\n }\n </div>\n </button>\n }\n\n <!-- Accordion Children -->\n @if (item.children && item.children.length > 0) {\n <div\n class=\"accordion-children\"\n [@accordionExpand]=\"getAccordionState(item.id)\"\n >\n @for (child of item.children; track child) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: child, level: level + 1 }\"\n ></ng-container>\n }\n </div>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;height:100%;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default))}.drawer-container{display:flex;height:100%;flex-direction:column;overflow:auto;background-color:rgb(var(--bg-primary));padding-inline:calc(var(--spacing, .25rem) * 2);transition:width .3s cubic-bezier(.4,0,.2,1)}.toggle-btn{position:absolute;top:50%;z-index:50;display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);right:-12px;transition:all .2s ease}.toggle-btn:hover{background-color:rgb(var(--bg-primary-hover));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.toggle-btn:active{transform:translateY(-50%) scale(.9)}.toggle-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-nav{flex:1;overflow-x:hidden;overflow-y:auto;padding-block:calc(var(--spacing, .25rem) * 2)}.drawer-nav::-webkit-scrollbar{width:calc(var(--spacing, .25rem) * 1.5)}.drawer-nav::-webkit-scrollbar-track{background-color:transparent}.drawer-nav::-webkit-scrollbar-thumb{border-radius:.125rem;background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.drawer-item-wrapper{position:relative}.drawer-item{position:relative;margin-top:var(--spacing, .25rem);display:flex;min-height:calc(var(--spacing, .25rem) * 8);width:100%;cursor:pointer;align-items:center;border-radius:.25rem;--tw-border-style: none;border-style:none;background-color:transparent;padding-inline:1rem;padding-block:calc(var(--spacing, .25rem) * 2);color:rgb(var(--text-primary));text-decoration-line:none;transition:all .2s ease}.drawer-item:hover{--tw-scale-x: 95%;--tw-scale-y: 95%;--tw-scale-z: 95%;scale:var(--tw-scale-x) var(--tw-scale-y);background-color:rgb(var(--bg-primary-hover));color:rgb(var(--text-primary))}.drawer-item.active{background-image:linear-gradient(135deg,rgb(var(--gradient-primary-start)),rgb(var(--gradient-primary-end)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-white, #fff)}.drawer-item.active .item-icon{color:var(--color-white, #fff)}.drawer-item:active{transform:scale(.98)}.drawer-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-item{-webkit-tap-highlight-color:transparent;--tw-outline-style: none;outline-style:none}.item-content{display:flex;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden}.item-icon{flex-shrink:0;color:inherit}.item-label{flex:1;overflow:hidden;text-align:left;font-size:13px;--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);text-overflow:ellipsis;white-space:nowrap}.chevron-icon{margin-left:auto;flex-shrink:0;color:rgb(var(--text-primary));transition:transform .3s cubic-bezier(.4,0,.2,1)}.chevron-icon.rotated{transform:rotate(180deg)}.accordion-children{overflow:hidden}.drawer-version{display:flex;align-items:center;justify-content:center;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:calc(var(--spacing, .25rem) * 4);padding-block:calc(var(--spacing, .25rem) * 3);font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;font-family:Roboto,monospace}.drawer-version-dot{display:block;height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);background-color:rgb(var(--text-placeholder))}.drawer-container[style*=\"width: 64px\"] .drawer-item{justify-content:center;padding-left:16px!important}.drawer-container[style*=\"width: 64px\"] .item-content{justify-content:center}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-outline-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-scale-x{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-leading{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-outline-style: solid;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1;--tw-font-weight: initial;--tw-leading: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], animations: [
|
|
3973
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: DrawerComponent, isStandalone: true, selector: "fx-ui-drawer", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, footerItems: { classPropertyName: "footerItems", publicName: "footerItems", isSignal: true, isRequired: false, transformFunction: null }, isExpanded: { classPropertyName: "isExpanded", publicName: "isExpanded", isSignal: false, isRequired: false, transformFunction: null }, logo: { classPropertyName: "logo", publicName: "logo", isSignal: false, isRequired: false, transformFunction: null }, logoText: { classPropertyName: "logoText", publicName: "logoText", isSignal: false, isRequired: false, transformFunction: null }, logoSubtext: { classPropertyName: "logoSubtext", publicName: "logoSubtext", isSignal: false, isRequired: false, transformFunction: null }, version: { classPropertyName: "version", publicName: "version", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { toggleExpanded: "toggleExpanded", itemClicked: "itemClicked" }, ngImport: i0, template: "<div class=\"drawer-host-wrapper\">\n <a href=\"/\" class=\"drawer-brand\" [class.is-collapsed]=\"!isExpanded\">\n @if (logo) {\n <img [src]=\"logo\" alt=\"Home\" class=\"drawer-brand-mark\" />\n } @else {\n <div class=\"drawer-brand-mark\"></div>\n }\n @if (isExpanded && (logoText || logoSubtext)) {\n <span class=\"drawer-brand-copy\">\n @if (logoText) {\n <span class=\"drawer-brand-text\">{{ logoText }}</span>\n }\n @if (logoSubtext) {\n <span class=\"drawer-brand-subtext\">{{ logoSubtext }}</span>\n }\n </span>\n }\n </a>\n <div class=\"drawer-container\" [@drawerExpand]=\"getDrawerState()\">\n <!-- Navigation Items -->\n <nav class=\"drawer-nav\">\n @for (item of items(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n\n <!-- V2: bottom section (Administration group, user block, \u2026) -->\n <div class=\"mt-auto\">\n @if (footerItems().length) {\n <nav class=\"drawer-footer-nav\">\n @for (item of footerItems(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n }\n <ng-content select=\"[drawerFooter]\"></ng-content>\n </div>\n\n <!-- Version -->\n @if (version) {\n <div class=\"drawer-version\" [matTooltip]=\"!isExpanded ? version : ''\" matTooltipPosition=\"right\">\n @if (isExpanded) {\n <span>{{ version }}</span>\n }\n @if (!isExpanded) {\n <span class=\"drawer-version-dot\"></span>\n }\n </div>\n }\n </div>\n\n <!-- Floating Toggle Button on right border -->\n <button\n type=\"button\"\n class=\"toggle-btn\"\n [attr.aria-label]=\"isExpanded ? 'Collapse navigation' : 'Expand navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n (click)=\"toggleDrawer()\"\n [matTooltip]=\"isExpanded ? 'Collapse' : 'Expand'\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n matTooltipPosition=\"right\"\n >\n <fx-ui-hero-icon\n [icon]=\"isExpanded ? 'chevron-left' : 'chevron-right'\"\n [size]=\"16\"\n class=\"text-text-primary\"\n ></fx-ui-hero-icon>\n </button>\n</div>\n\n<!-- Recursive Template for Drawer Items -->\n<ng-template #drawerItem let-item let-level=\"level\">\n @if (item.heading) {\n @if (isExpanded) {\n <div class=\"drawer-section-label\">{{ item.label }}</div>\n } @else {\n <div class=\"drawer-section-rule\"></div>\n }\n } @else {\n <div class=\"drawer-item-wrapper\">\n <!-- Item Button/Link -->\n @if (item.route && !item.children) {\n <a\n [routerLink]=\"item.route\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n </div>\n </a>\n }\n\n @if (!item.route || item.children) {\n <button\n type=\"button\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-sm font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n @if (isExpanded && item.children && item.children.length > 0) {\n <fx-ui-hero-icon\n [icon]=\"'chevron-down'\"\n [size]=\"16\"\n class=\"chevron-icon\"\n [class.rotated]=\"isAccordionExpanded(item.id)\"\n ></fx-ui-hero-icon>\n }\n </div>\n </button>\n }\n\n <!-- Accordion Children -->\n @if (item.children && item.children.length > 0) {\n <div\n class=\"accordion-children\"\n [@accordionExpand]=\"getAccordionState(item.id)\"\n >\n @for (child of item.children; track child) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: child, level: level + 1 }\"\n ></ng-container>\n }\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;display:flex;height:100%;flex-direction:column;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--og-page-bg))}.drawer-brand{display:flex;height:60px;flex-shrink:0;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden;text-decoration-line:none;padding-left:24px;padding-right:8px;transition:padding .3s cubic-bezier(.4,0,.2,1);max-width:260px}.drawer-brand.is-collapsed{justify-content:center;padding-left:0;padding-right:0;max-width:70px}.drawer-brand-mark{height:20px;width:auto;flex-shrink:0}.drawer-brand-copy{display:flex;min-width:0px;flex-direction:column;--tw-leading: var(--leading-tight, 1.25);line-height:var(--leading-tight, 1.25)}.drawer-brand-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--text-primary));-webkit-user-select:none;user-select:none;font-family:var(--og-font-display);letter-spacing:-.01em}.drawer-brand-subtext{margin-top:1px;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-container{display:flex;min-height:0px;flex:1;flex-direction:column;overflow:hidden;padding-inline:calc(var(--spacing, .25rem) * 2);transition:width .3s cubic-bezier(.4,0,.2,1)}.toggle-btn{position:absolute;top:50%;z-index:50;display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);right:-12px;transition:all .2s ease}.toggle-btn:hover{background-color:rgb(var(--bg-primary-hover));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.toggle-btn:active{transform:translateY(-50%) scale(.9)}.toggle-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-footer-nav{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:0px;padding-top:calc(var(--spacing, .25rem) * 2);padding-bottom:var(--spacing, .25rem)}.drawer-nav{flex:1;overflow-x:hidden;overflow-y:auto;padding-block:calc(var(--spacing, .25rem) * 2)}.drawer-nav::-webkit-scrollbar{width:calc(var(--spacing, .25rem) * 1.5)}.drawer-nav::-webkit-scrollbar-track{background-color:transparent}.drawer-nav::-webkit-scrollbar-thumb{border-radius:var(--og-radius-control);background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.drawer-section-label{padding-inline:1rem;padding-top:calc(var(--spacing, .25rem) * 4);padding-bottom:var(--spacing, .25rem);font-size:10.5px;line-height:1.4;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text-faint));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-section-rule{margin-inline:calc(var(--spacing, .25rem) * 3);margin-block:calc(var(--spacing, .25rem) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default))}.drawer-item-wrapper{position:relative}.drawer-item{position:relative;margin-top:var(--spacing, .25rem);display:flex;min-height:calc(var(--spacing, .25rem) * 8);width:100%;cursor:pointer;align-items:center;border-radius:var(--og-radius-control);--tw-border-style: none;border-style:none;background-color:transparent;padding-inline:1rem;padding-block:calc(var(--spacing, .25rem) * 2);color:rgb(var(--text-primary));text-decoration-line:none;transition:all .2s ease}.drawer-item:hover{--tw-scale-x: 95%;--tw-scale-y: 95%;--tw-scale-z: 95%;scale:var(--tw-scale-x) var(--tw-scale-y);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.5)}.drawer-item.active{--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.8);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 1px rgb(var(--og-border)),0 1px 2px rgb(var(--shadow-color)/.05)}.drawer-item.active:hover{background:rgb(var(--og-surface-raised)/.95)}.drawer-item:active{transform:scale(.98)}.drawer-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-item{-webkit-tap-highlight-color:transparent;--tw-outline-style: none;outline-style:none}.item-content{display:flex;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden}.item-icon{flex-shrink:0;color:inherit}.item-label{flex:1;overflow:hidden;text-align:left;font-size:13px;--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);text-overflow:ellipsis;white-space:nowrap}.chevron-icon{margin-left:auto;flex-shrink:0;color:rgb(var(--text-primary));transition:transform .3s cubic-bezier(.4,0,.2,1)}.chevron-icon.rotated{transform:rotate(180deg)}.accordion-children{overflow:hidden}.drawer-version{display:flex;align-items:center;justify-content:center;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:calc(var(--spacing, .25rem) * 4);padding-block:calc(var(--spacing, .25rem) * 3);font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;font-family:var(--og-font-mono)}.drawer-version-dot{display:block;height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);background-color:rgb(var(--text-placeholder))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-outline-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-scale-x{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: \"*\"; inherits: false; initial-value: 1;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-leading: initial;--tw-font-weight: initial;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-outline-style: solid;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], animations: [
|
|
3632
3974
|
trigger('drawerExpand', [
|
|
3633
3975
|
state('collapsed', style({ width: '70px' })),
|
|
3634
3976
|
state('expanded', style({ width: '260px' })),
|
|
@@ -3655,16 +3997,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
3655
3997
|
transition('collapsed <=> expanded', animate('300ms cubic-bezier(0.4, 0, 0.2, 1)')),
|
|
3656
3998
|
]),
|
|
3657
3999
|
], changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
3658
|
-
NgClass,
|
|
3659
4000
|
NgTemplateOutlet,
|
|
3660
4001
|
MatTooltip,
|
|
3661
4002
|
HeroIconComponent,
|
|
3662
4003
|
RouterLink,
|
|
3663
|
-
], template: "<div class=\"drawer-host-wrapper\">\n <a\n href=\"/\"\n class=\"h-[60px]\"\n [ngClass]=\"{ 'flex items-center justify-center': !isExpanded, 'p-4': isExpanded }\"\n >\n @if (logo) {\n <img\n [src]=\"logo\"\n alt=\"Home\"\n class=\"h-[20px] w-auto transition-all duration-300 ease-in-out\"\n [ngClass]=\"{ 'scale-90': !isExpanded, 'ml-5': isExpanded }\"\n />\n } @else {\n <div class=\"h-[30px] w-auto transition-all duration-300 ease-in-out\"></div>\n }\n </a>\n <div class=\"drawer-container\" [@drawerExpand]=\"getDrawerState()\">\n <!-- Navigation Items -->\n <nav class=\"drawer-nav\">\n @for (item of items(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n\n <!-- Version -->\n @if (version) {\n <div class=\"drawer-version\" [matTooltip]=\"!isExpanded ? version : ''\" matTooltipPosition=\"right\">\n @if (isExpanded) {\n <span>{{ version }}</span>\n }\n @if (!isExpanded) {\n <span class=\"drawer-version-dot\"></span>\n }\n </div>\n }\n </div>\n\n <!-- Floating Toggle Button on right border -->\n <button\n type=\"button\"\n class=\"toggle-btn\"\n [attr.aria-label]=\"isExpanded ? 'Collapse navigation' : 'Expand navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n (click)=\"toggleDrawer()\"\n [matTooltip]=\"isExpanded ? 'Collapse' : 'Expand'\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n matTooltipPosition=\"right\"\n >\n <fx-ui-hero-icon\n [icon]=\"isExpanded ? 'chevron-left' : 'chevron-right'\"\n [size]=\"16\"\n class=\"text-text-primary\"\n ></fx-ui-hero-icon>\n </button>\n</div>\n\n<!-- Recursive Template for Drawer Items -->\n<ng-template #drawerItem let-item let-level=\"level\">\n <div class=\"drawer-item-wrapper\">\n <!-- Item Button/Link -->\n @if (item.route && !item.children) {\n <a\n [routerLink]=\"item.route\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n </div>\n </a>\n }\n\n @if (!item.route || item.children) {\n <button\n type=\"button\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-sm font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n @if (isExpanded && item.children && item.children.length > 0) {\n <fx-ui-hero-icon\n [icon]=\"'chevron-down'\"\n [size]=\"16\"\n class=\"chevron-icon\"\n [class.rotated]=\"isAccordionExpanded(item.id)\"\n ></fx-ui-hero-icon>\n }\n </div>\n </button>\n }\n\n <!-- Accordion Children -->\n @if (item.children && item.children.length > 0) {\n <div\n class=\"accordion-children\"\n [@accordionExpand]=\"getAccordionState(item.id)\"\n >\n @for (child of item.children; track child) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: child, level: level + 1 }\"\n ></ng-container>\n }\n </div>\n }\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;height:100%;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default))}.drawer-container{display:flex;height:100%;flex-direction:column;overflow:auto;background-color:rgb(var(--bg-primary));padding-inline:calc(var(--spacing, .25rem) * 2);transition:width .3s cubic-bezier(.4,0,.2,1)}.toggle-btn{position:absolute;top:50%;z-index:50;display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);right:-12px;transition:all .2s ease}.toggle-btn:hover{background-color:rgb(var(--bg-primary-hover));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.toggle-btn:active{transform:translateY(-50%) scale(.9)}.toggle-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-nav{flex:1;overflow-x:hidden;overflow-y:auto;padding-block:calc(var(--spacing, .25rem) * 2)}.drawer-nav::-webkit-scrollbar{width:calc(var(--spacing, .25rem) * 1.5)}.drawer-nav::-webkit-scrollbar-track{background-color:transparent}.drawer-nav::-webkit-scrollbar-thumb{border-radius:.125rem;background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.drawer-item-wrapper{position:relative}.drawer-item{position:relative;margin-top:var(--spacing, .25rem);display:flex;min-height:calc(var(--spacing, .25rem) * 8);width:100%;cursor:pointer;align-items:center;border-radius:.25rem;--tw-border-style: none;border-style:none;background-color:transparent;padding-inline:1rem;padding-block:calc(var(--spacing, .25rem) * 2);color:rgb(var(--text-primary));text-decoration-line:none;transition:all .2s ease}.drawer-item:hover{--tw-scale-x: 95%;--tw-scale-y: 95%;--tw-scale-z: 95%;scale:var(--tw-scale-x) var(--tw-scale-y);background-color:rgb(var(--bg-primary-hover));color:rgb(var(--text-primary))}.drawer-item.active{background-image:linear-gradient(135deg,rgb(var(--gradient-primary-start)),rgb(var(--gradient-primary-end)));--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:var(--color-white, #fff)}.drawer-item.active .item-icon{color:var(--color-white, #fff)}.drawer-item:active{transform:scale(.98)}.drawer-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-item{-webkit-tap-highlight-color:transparent;--tw-outline-style: none;outline-style:none}.item-content{display:flex;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden}.item-icon{flex-shrink:0;color:inherit}.item-label{flex:1;overflow:hidden;text-align:left;font-size:13px;--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);text-overflow:ellipsis;white-space:nowrap}.chevron-icon{margin-left:auto;flex-shrink:0;color:rgb(var(--text-primary));transition:transform .3s cubic-bezier(.4,0,.2,1)}.chevron-icon.rotated{transform:rotate(180deg)}.accordion-children{overflow:hidden}.drawer-version{display:flex;align-items:center;justify-content:center;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:calc(var(--spacing, .25rem) * 4);padding-block:calc(var(--spacing, .25rem) * 3);font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;font-family:Roboto,monospace}.drawer-version-dot{display:block;height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);background-color:rgb(var(--text-placeholder))}.drawer-container[style*=\"width: 64px\"] .drawer-item{justify-content:center;padding-left:16px!important}.drawer-container[style*=\"width: 64px\"] .item-content{justify-content:center}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-outline-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-scale-x{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-leading{syntax: \"*\"; inherits: false;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-outline-style: solid;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1;--tw-font-weight: initial;--tw-leading: initial}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
|
|
3664
|
-
}], ctorParameters: () => [{ type: i2$1.Router }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], isExpanded: [{
|
|
4004
|
+
], template: "<div class=\"drawer-host-wrapper\">\n <a href=\"/\" class=\"drawer-brand\" [class.is-collapsed]=\"!isExpanded\">\n @if (logo) {\n <img [src]=\"logo\" alt=\"Home\" class=\"drawer-brand-mark\" />\n } @else {\n <div class=\"drawer-brand-mark\"></div>\n }\n @if (isExpanded && (logoText || logoSubtext)) {\n <span class=\"drawer-brand-copy\">\n @if (logoText) {\n <span class=\"drawer-brand-text\">{{ logoText }}</span>\n }\n @if (logoSubtext) {\n <span class=\"drawer-brand-subtext\">{{ logoSubtext }}</span>\n }\n </span>\n }\n </a>\n <div class=\"drawer-container\" [@drawerExpand]=\"getDrawerState()\">\n <!-- Navigation Items -->\n <nav class=\"drawer-nav\">\n @for (item of items(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n\n <!-- V2: bottom section (Administration group, user block, \u2026) -->\n <div class=\"mt-auto\">\n @if (footerItems().length) {\n <nav class=\"drawer-footer-nav\">\n @for (item of footerItems(); track item) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: item, level: 0 }\"\n ></ng-container>\n }\n </nav>\n }\n <ng-content select=\"[drawerFooter]\"></ng-content>\n </div>\n\n <!-- Version -->\n @if (version) {\n <div class=\"drawer-version\" [matTooltip]=\"!isExpanded ? version : ''\" matTooltipPosition=\"right\">\n @if (isExpanded) {\n <span>{{ version }}</span>\n }\n @if (!isExpanded) {\n <span class=\"drawer-version-dot\"></span>\n }\n </div>\n }\n </div>\n\n <!-- Floating Toggle Button on right border -->\n <button\n type=\"button\"\n class=\"toggle-btn\"\n [attr.aria-label]=\"isExpanded ? 'Collapse navigation' : 'Expand navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n (click)=\"toggleDrawer()\"\n [matTooltip]=\"isExpanded ? 'Collapse' : 'Expand'\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n matTooltipPosition=\"right\"\n >\n <fx-ui-hero-icon\n [icon]=\"isExpanded ? 'chevron-left' : 'chevron-right'\"\n [size]=\"16\"\n class=\"text-text-primary\"\n ></fx-ui-hero-icon>\n </button>\n</div>\n\n<!-- Recursive Template for Drawer Items -->\n<ng-template #drawerItem let-item let-level=\"level\">\n @if (item.heading) {\n @if (isExpanded) {\n <div class=\"drawer-section-label\">{{ item.label }}</div>\n } @else {\n <div class=\"drawer-section-rule\"></div>\n }\n } @else {\n <div class=\"drawer-item-wrapper\">\n <!-- Item Button/Link -->\n @if (item.route && !item.children) {\n <a\n [routerLink]=\"item.route\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-xs font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n </div>\n </a>\n }\n\n @if (!item.route || item.children) {\n <button\n type=\"button\"\n class=\"drawer-item\"\n [class.active]=\"isActiveRoute(item.route)\"\n [class.has-children]=\"item.children && item.children.length > 0\"\n [style.paddingLeft.px]=\"isExpanded ? 16 + level * 16 : 16\"\n [matTooltip]=\"!isExpanded ? item.label : ''\"\n matTooltipPosition=\"right\"\n matTooltipClass=\"text-text-primary text-sm font-semibold\"\n (click)=\"onItemClick(item, $event)\"\n >\n <div class=\"item-content\">\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"20\" class=\"item-icon\"></fx-ui-hero-icon>\n @if (isExpanded) {\n <span class=\"item-label\">{{ item.label }}</span>\n }\n @if (isExpanded && item.children && item.children.length > 0) {\n <fx-ui-hero-icon\n [icon]=\"'chevron-down'\"\n [size]=\"16\"\n class=\"chevron-icon\"\n [class.rotated]=\"isAccordionExpanded(item.id)\"\n ></fx-ui-hero-icon>\n }\n </div>\n </button>\n }\n\n <!-- Accordion Children -->\n @if (item.children && item.children.length > 0) {\n <div\n class=\"accordion-children\"\n [@accordionExpand]=\"getAccordionState(item.id)\"\n >\n @for (child of item.children; track child) {\n <ng-container\n *ngTemplateOutlet=\"drawerItem; context: { $implicit: child, level: level + 1 }\"\n ></ng-container>\n }\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";@layer properties;:host{display:block;height:100%}.drawer-host-wrapper{position:relative;display:flex;height:100%;flex-direction:column;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--og-page-bg))}.drawer-brand{display:flex;height:60px;flex-shrink:0;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden;text-decoration-line:none;padding-left:24px;padding-right:8px;transition:padding .3s cubic-bezier(.4,0,.2,1);max-width:260px}.drawer-brand.is-collapsed{justify-content:center;padding-left:0;padding-right:0;max-width:70px}.drawer-brand-mark{height:20px;width:auto;flex-shrink:0}.drawer-brand-copy{display:flex;min-width:0px;flex-direction:column;--tw-leading: var(--leading-tight, 1.25);line-height:var(--leading-tight, 1.25)}.drawer-brand-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--text-primary));-webkit-user-select:none;user-select:none;font-family:var(--og-font-display);letter-spacing:-.01em}.drawer-brand-subtext{margin-top:1px;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-container{display:flex;min-height:0px;flex:1;flex-direction:column;overflow:hidden;padding-inline:calc(var(--spacing, .25rem) * 2);transition:width .3s cubic-bezier(.4,0,.2,1)}.toggle-btn{position:absolute;top:50%;z-index:50;display:flex;height:calc(var(--spacing, .25rem) * 6);width:calc(var(--spacing, .25rem) * 6);--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y);cursor:pointer;align-items:center;justify-content:center;border-radius:calc(infinity * 1px);border-style:var(--tw-border-style);border-width:1px;border-color:rgb(var(--border-default));background-color:rgb(var(--bg-primary));--tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, rgb(var(--shadow-color) / .05));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);right:-12px;transition:all .2s ease}.toggle-btn:hover{background-color:rgb(var(--bg-primary-hover));--tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(var(--shadow-color) / .1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(var(--shadow-color) / .1));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.toggle-btn:active{transform:translateY(-50%) scale(.9)}.toggle-btn:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-footer-nav{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:0px;padding-top:calc(var(--spacing, .25rem) * 2);padding-bottom:var(--spacing, .25rem)}.drawer-nav{flex:1;overflow-x:hidden;overflow-y:auto;padding-block:calc(var(--spacing, .25rem) * 2)}.drawer-nav::-webkit-scrollbar{width:calc(var(--spacing, .25rem) * 1.5)}.drawer-nav::-webkit-scrollbar-track{background-color:transparent}.drawer-nav::-webkit-scrollbar-thumb{border-radius:var(--og-radius-control);background-color:rgb(var(--border-default))}.drawer-nav::-webkit-scrollbar-thumb:hover{background-color:rgb(var(--text-placeholder))}.drawer-section-label{padding-inline:1rem;padding-top:calc(var(--spacing, .25rem) * 4);padding-bottom:var(--spacing, .25rem);font-size:10.5px;line-height:1.4;--tw-font-weight: var(--font-weight-semibold, 600);font-weight:var(--font-weight-semibold, 600);color:rgb(var(--og-text-faint));-webkit-user-select:none;user-select:none;letter-spacing:var(--og-tracking-label)}.drawer-section-rule{margin-inline:calc(var(--spacing, .25rem) * 3);margin-block:calc(var(--spacing, .25rem) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default))}.drawer-item-wrapper{position:relative}.drawer-item{position:relative;margin-top:var(--spacing, .25rem);display:flex;min-height:calc(var(--spacing, .25rem) * 8);width:100%;cursor:pointer;align-items:center;border-radius:var(--og-radius-control);--tw-border-style: none;border-style:none;background-color:transparent;padding-inline:1rem;padding-block:calc(var(--spacing, .25rem) * 2);color:rgb(var(--text-primary));text-decoration-line:none;transition:all .2s ease}.drawer-item:hover{--tw-scale-x: 95%;--tw-scale-y: 95%;--tw-scale-z: 95%;scale:var(--tw-scale-x) var(--tw-scale-y);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.5)}.drawer-item.active{--tw-font-weight: var(--font-weight-medium, 500);font-weight:var(--font-weight-medium, 500);color:rgb(var(--text-primary));background:rgb(var(--og-surface-raised)/.8);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:inset 0 0 0 1px rgb(var(--og-border)),0 1px 2px rgb(var(--shadow-color)/.05)}.drawer-item.active:hover{background:rgb(var(--og-surface-raised)/.95)}.drawer-item:active{transform:scale(.98)}.drawer-item:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px;outline-offset:2px;outline-color:rgb(var(--primary))}.drawer-item{-webkit-tap-highlight-color:transparent;--tw-outline-style: none;outline-style:none}.item-content{display:flex;width:100%;align-items:center;gap:calc(var(--spacing, .25rem) * 3);overflow:hidden}.item-icon{flex-shrink:0;color:inherit}.item-label{flex:1;overflow:hidden;text-align:left;font-size:13px;--tw-leading: calc(var(--spacing, .25rem) * 5);line-height:calc(var(--spacing, .25rem) * 5);text-overflow:ellipsis;white-space:nowrap}.chevron-icon{margin-left:auto;flex-shrink:0;color:rgb(var(--text-primary));transition:transform .3s cubic-bezier(.4,0,.2,1)}.chevron-icon.rotated{transform:rotate(180deg)}.accordion-children{overflow:hidden}.drawer-version{display:flex;align-items:center;justify-content:center;border-top-style:var(--tw-border-style);border-top-width:1px;border-color:rgb(var(--border-default));padding-inline:calc(var(--spacing, .25rem) * 4);padding-block:calc(var(--spacing, .25rem) * 3);font-size:11px;color:rgb(var(--text-placeholder));-webkit-user-select:none;user-select:none;font-family:var(--og-font-mono)}.drawer-version-dot{display:block;height:calc(var(--spacing, .25rem) * 1.5);width:calc(var(--spacing, .25rem) * 1.5);border-radius:calc(infinity * 1px);background-color:rgb(var(--text-placeholder))}@property --tw-border-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-leading{syntax: \"*\"; inherits: false;}@property --tw-font-weight{syntax: \"*\"; inherits: false;}@property --tw-translate-x{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-y{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-translate-z{syntax: \"*\"; inherits: false; initial-value: 0;}@property --tw-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-inset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-shadow-color{syntax: \"*\"; inherits: false;}@property --tw-inset-shadow-alpha{syntax: \"<percentage>\"; inherits: false; initial-value: 100%;}@property --tw-ring-color{syntax: \"*\"; inherits: false;}@property --tw-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-inset-ring-color{syntax: \"*\"; inherits: false;}@property --tw-inset-ring-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-ring-inset{syntax: \"*\"; inherits: false;}@property --tw-ring-offset-width{syntax: \"<length>\"; inherits: false; initial-value: 0px;}@property --tw-ring-offset-color{syntax: \"*\"; inherits: false; initial-value: #fff;}@property --tw-ring-offset-shadow{syntax: \"*\"; inherits: false; initial-value: 0 0 #0000;}@property --tw-outline-style{syntax: \"*\"; inherits: false; initial-value: solid;}@property --tw-scale-x{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-y{syntax: \"*\"; inherits: false; initial-value: 1;}@property --tw-scale-z{syntax: \"*\"; inherits: false; initial-value: 1;}@layer properties{@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style: solid;--tw-leading: initial;--tw-font-weight: initial;--tw-translate-x: 0;--tw-translate-y: 0;--tw-translate-z: 0;--tw-shadow: 0 0 #0000;--tw-shadow-color: initial;--tw-shadow-alpha: 100%;--tw-inset-shadow: 0 0 #0000;--tw-inset-shadow-color: initial;--tw-inset-shadow-alpha: 100%;--tw-ring-color: initial;--tw-ring-shadow: 0 0 #0000;--tw-inset-ring-color: initial;--tw-inset-ring-shadow: 0 0 #0000;--tw-ring-inset: initial;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-offset-shadow: 0 0 #0000;--tw-outline-style: solid;--tw-scale-x: 1;--tw-scale-y: 1;--tw-scale-z: 1}}}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
|
|
4005
|
+
}], ctorParameters: () => [{ type: i2$1.Router }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], footerItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerItems", required: false }] }], isExpanded: [{
|
|
3665
4006
|
type: Input
|
|
3666
4007
|
}], logo: [{
|
|
3667
4008
|
type: Input
|
|
4009
|
+
}], logoText: [{
|
|
4010
|
+
type: Input
|
|
4011
|
+
}], logoSubtext: [{
|
|
4012
|
+
type: Input
|
|
3668
4013
|
}], version: [{
|
|
3669
4014
|
type: Input
|
|
3670
4015
|
}], toggleExpanded: [{ type: i0.Output, args: ["toggleExpanded"] }], itemClicked: [{ type: i0.Output, args: ["itemClicked"] }] } });
|
|
@@ -3672,40 +4017,80 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
3672
4017
|
/** Row-card skeleton placeholder for master-detail list panes. */
|
|
3673
4018
|
class SkeletonListComponent {
|
|
3674
4019
|
rows = input(5, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
}
|
|
4020
|
+
/** computed, không phải method: method trả mảng MỚI mỗi lần render. */
|
|
4021
|
+
rowList = computed(() => range$5(this.rows()), ...(ngDevMode ? [{ debugName: "rowList" }] : /* istanbul ignore next */ []));
|
|
3678
4022
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3679
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonListComponent, isStandalone: true, selector: "fx-ui-skeleton-list", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-normal
|
|
4023
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonListComponent, isStandalone: true, selector: "fx-ui-skeleton-list", inputs: { rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-normal\">\n @for (r of rowList(); track r) {\n <div class=\"rounded-lg border border-border-default p-semi\">\n <div class=\"flex items-center gap-normal mb-small\">\n <div class=\"w-8 h-8 rounded-lg skeleton-load shrink-0\"></div>\n <div class=\"h-4 flex-1 rounded skeleton-load\"></div>\n <div class=\"h-5 w-12 rounded-full skeleton-load shrink-0\"></div>\n </div>\n <div class=\"h-3 w-3/4 rounded skeleton-load\"></div>\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3680
4024
|
}
|
|
3681
4025
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonListComponent, decorators: [{
|
|
3682
4026
|
type: Component,
|
|
3683
|
-
args: [{ selector: 'fx-ui-skeleton-list', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-normal
|
|
4027
|
+
args: [{ selector: 'fx-ui-skeleton-list', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-normal\">\n @for (r of rowList(); track r) {\n <div class=\"rounded-lg border border-border-default p-semi\">\n <div class=\"flex items-center gap-normal mb-small\">\n <div class=\"w-8 h-8 rounded-lg skeleton-load shrink-0\"></div>\n <div class=\"h-4 flex-1 rounded skeleton-load\"></div>\n <div class=\"h-5 w-12 rounded-full skeleton-load shrink-0\"></div>\n </div>\n <div class=\"h-3 w-3/4 rounded skeleton-load\"></div>\n </div>\n }\n</div>\n" }]
|
|
3684
4028
|
}], propDecorators: { rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
|
|
4029
|
+
function range$5(n) {
|
|
4030
|
+
return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
|
|
4031
|
+
}
|
|
3685
4032
|
|
|
4033
|
+
const ROW_WIDTHS = [90, 70, 80, 60];
|
|
3686
4034
|
/** Card-shaped skeleton placeholder for detail panels. */
|
|
3687
4035
|
class SkeletonDetailComponent {
|
|
3688
4036
|
/** Number of content cards rendered below the header card. */
|
|
3689
4037
|
cards = input(3, ...(ngDevMode ? [{ debugName: "cards" }] : /* istanbul ignore next */ []));
|
|
3690
4038
|
/** Rows per content card. */
|
|
3691
4039
|
rows = input(4, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
}
|
|
4040
|
+
/** computed, không phải method: method trả mảng MỚI mỗi lần render. */
|
|
4041
|
+
cardList = computed(() => range$4(this.cards()), ...(ngDevMode ? [{ debugName: "cardList" }] : /* istanbul ignore next */ []));
|
|
4042
|
+
rowList = computed(() => range$4(this.rows()), ...(ngDevMode ? [{ debugName: "rowList" }] : /* istanbul ignore next */ []));
|
|
3695
4043
|
rowWidth(i) {
|
|
3696
|
-
return [
|
|
4044
|
+
return ROW_WIDTHS[i % ROW_WIDTHS.length];
|
|
3697
4045
|
}
|
|
3698
4046
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3699
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonDetailComponent, isStandalone: true, selector: "fx-ui-skeleton-detail", inputs: { cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-xl
|
|
4047
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonDetailComponent, isStandalone: true, selector: "fx-ui-skeleton-detail", inputs: { cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-xl\">\n <div class=\"card-common\">\n <div class=\"flex items-start gap-normal\">\n <div class=\"w-10 h-10 rounded-lg skeleton-load shrink-0\"></div>\n <div class=\"flex-1 min-w-0 flex flex-col gap-normal\">\n <div class=\"h-5 w-1/2 rounded skeleton-load\"></div>\n <div class=\"h-3 w-3/4 rounded skeleton-load\"></div>\n </div>\n </div>\n </div>\n\n @for (c of cardList(); track c) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded skeleton-load\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of rowList(); track r) {\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"rowWidth(r)\"></div>\n }\n </div>\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3700
4048
|
}
|
|
3701
4049
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDetailComponent, decorators: [{
|
|
3702
4050
|
type: Component,
|
|
3703
|
-
args: [{ selector: 'fx-ui-skeleton-detail', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-xl
|
|
4051
|
+
args: [{ selector: 'fx-ui-skeleton-detail', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-xl\">\n <div class=\"card-common\">\n <div class=\"flex items-start gap-normal\">\n <div class=\"w-10 h-10 rounded-lg skeleton-load shrink-0\"></div>\n <div class=\"flex-1 min-w-0 flex flex-col gap-normal\">\n <div class=\"h-5 w-1/2 rounded skeleton-load\"></div>\n <div class=\"h-3 w-3/4 rounded skeleton-load\"></div>\n </div>\n </div>\n </div>\n\n @for (c of cardList(); track c) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded skeleton-load\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of rowList(); track r) {\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"rowWidth(r)\"></div>\n }\n </div>\n </div>\n }\n</div>\n" }]
|
|
3704
4052
|
}], propDecorators: { cards: [{ type: i0.Input, args: [{ isSignal: true, alias: "cards", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
|
|
4053
|
+
function range$4(n) {
|
|
4054
|
+
return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
/**
|
|
4058
|
+
* V2 sparkbar — inline mini bar-trend (risk trend on project cards, deltas in
|
|
4059
|
+
* stat grids). Pure DOM, token-coloured; no chart engine.
|
|
4060
|
+
*
|
|
4061
|
+
* <fx-ui-sparkbar [values]="[3, 5, 2, 8, 6]" />
|
|
4062
|
+
*/
|
|
4063
|
+
class SparkbarComponent {
|
|
4064
|
+
values = input([], ...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
|
|
4065
|
+
/** Bar colour; null renders the accent. */
|
|
4066
|
+
tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
|
|
4067
|
+
height = input(28, ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
4068
|
+
bars = computed(() => {
|
|
4069
|
+
const values = this.values();
|
|
4070
|
+
const max = Math.max(...values, 1);
|
|
4071
|
+
// 8% floor so zero values still show a base mark.
|
|
4072
|
+
return values.map((v) => Math.max((v / max) * 100, 8));
|
|
4073
|
+
}, ...(ngDevMode ? [{ debugName: "bars" }] : /* istanbul ignore next */ []));
|
|
4074
|
+
barClass() {
|
|
4075
|
+
return this.tone() ? toneBg(this.tone()) : 'bg-accent';
|
|
4076
|
+
}
|
|
4077
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SparkbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4078
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SparkbarComponent, isStandalone: true, selector: "fx-ui-sparkbar", inputs: { values: { classPropertyName: "values", publicName: "values", 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 } }, ngImport: i0, template: "<div\n class=\"flex items-end gap-[2px]\"\n [style.height.px]=\"height()\"\n role=\"img\"\n [attr.aria-label]=\"'Trend: ' + values().join(', ')\"\n>\n @for (bar of bars(); track $index) {\n <div\n class=\"flex-1 min-w-[3px] rounded-[1px] opacity-80\"\n [ngClass]=\"barClass()\"\n [style.height.%]=\"bar\"\n ></div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4079
|
+
}
|
|
4080
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SparkbarComponent, decorators: [{
|
|
4081
|
+
type: Component,
|
|
4082
|
+
args: [{ selector: 'fx-ui-sparkbar', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div\n class=\"flex items-end gap-[2px]\"\n [style.height.px]=\"height()\"\n role=\"img\"\n [attr.aria-label]=\"'Trend: ' + values().join(', ')\"\n>\n @for (bar of bars(); track $index) {\n <div\n class=\"flex-1 min-w-[3px] rounded-[1px] opacity-80\"\n [ngClass]=\"barClass()\"\n [style.height.%]=\"bar\"\n ></div>\n }\n</div>\n" }]
|
|
4083
|
+
}], propDecorators: { values: [{ type: i0.Input, args: [{ isSignal: true, alias: "values", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
|
|
3705
4084
|
|
|
3706
4085
|
/** KPI tile row: big number + tinted icon box + caption, in a responsive grid. */
|
|
3707
4086
|
class StatCardsComponent {
|
|
3708
4087
|
cards = input([], ...(ngDevMode ? [{ debugName: "cards" }] : /* istanbul ignore next */ []));
|
|
4088
|
+
/**
|
|
4089
|
+
* 'tile' (default): tinted icon box + number + caption — the pre-V2 look.
|
|
4090
|
+
* 'figure': the V2 design's KPI column — caption on top, large figure-font
|
|
4091
|
+
* number, toned sub-caption underneath (no icon box).
|
|
4092
|
+
*/
|
|
4093
|
+
variant = input('tile', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
3709
4094
|
/** Grid columns; defaults to one column per card. */
|
|
3710
4095
|
cols;
|
|
3711
4096
|
get gridColumns() {
|
|
@@ -3719,10 +4104,17 @@ class StatCardsComponent {
|
|
|
3719
4104
|
return card.tone ? toneText(card.tone) : 'text-text-primary';
|
|
3720
4105
|
}
|
|
3721
4106
|
boxClass(card) {
|
|
3722
|
-
|
|
4107
|
+
// Ô icon vuông: chỉ lấy MÀU, không lấy hình pill của badge.
|
|
4108
|
+
return toneTint(card.tone ?? null);
|
|
4109
|
+
}
|
|
4110
|
+
deltaClass(card) {
|
|
4111
|
+
return card.delta?.tone ? toneText(card.delta.tone) : 'text-muted';
|
|
4112
|
+
}
|
|
4113
|
+
subClass(card) {
|
|
4114
|
+
return card.subTone ? toneText(card.subTone) : 'text-text-secondary';
|
|
3723
4115
|
}
|
|
3724
4116
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatCardsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3725
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: StatCardsComponent, isStandalone: true, selector: "fx-ui-stat-cards", inputs: { cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: false, transformFunction: null }, cols: { classPropertyName: "cols", publicName: "cols", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n <div class=\"card-common flex items-center gap-normal\">\n
|
|
4117
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: StatCardsComponent, isStandalone: true, selector: "fx-ui-stat-cards", inputs: { cards: { classPropertyName: "cards", publicName: "cards", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, cols: { classPropertyName: "cols", publicName: "cols", isSignal: false, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n @if (variant() === 'figure') {\n <div class=\"card-common flex flex-col gap-small min-w-0\">\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n <span\n class=\"txt-heading-03\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.sub) {\n <span class=\"txt-utility-caption\" [ngClass]=\"subClass(card)\">{{ card.sub }}</span>\n }\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n } @else {\n <div class=\"card-common flex items-center gap-normal\">\n @if (card.icon) {\n <div\n class=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n [ngClass]=\"boxClass(card)\"\n >\n <fx-ui-hero-icon [icon]=\"card.icon\" [size]=\"20\"></fx-ui-hero-icon>\n </div>\n }\n <div class=\"flex flex-col min-w-0 flex-1\">\n <div class=\"flex items-baseline gap-small\">\n <span\n class=\"txt-heading-04\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.delta) {\n <span class=\"text-xs font-semibold\" [ngClass]=\"deltaClass(card)\">\n {{ card.delta.direction === 'up' ? '\u25B2' : '\u25BC' }} {{ card.delta.value }}\n </span>\n }\n </div>\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n </div>\n }\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3726
4118
|
}
|
|
3727
4119
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatCardsComponent, decorators: [{
|
|
3728
4120
|
type: Component,
|
|
@@ -3730,8 +4122,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
3730
4122
|
NgStyle,
|
|
3731
4123
|
NgClass,
|
|
3732
4124
|
HeroIconComponent,
|
|
3733
|
-
|
|
3734
|
-
|
|
4125
|
+
SparkbarComponent,
|
|
4126
|
+
], template: "<div class=\"grid gap-normal\" [ngStyle]=\"{ 'grid-template-columns': gridColumns }\">\n @for (card of cards(); track card.label) {\n @if (variant() === 'figure') {\n <div class=\"card-common flex flex-col gap-small min-w-0\">\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n <span\n class=\"txt-heading-03\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.sub) {\n <span class=\"txt-utility-caption\" [ngClass]=\"subClass(card)\">{{ card.sub }}</span>\n }\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n } @else {\n <div class=\"card-common flex items-center gap-normal\">\n @if (card.icon) {\n <div\n class=\"w-10 h-10 rounded-lg flex items-center justify-center shrink-0\"\n [ngClass]=\"boxClass(card)\"\n >\n <fx-ui-hero-icon [icon]=\"card.icon\" [size]=\"20\"></fx-ui-hero-icon>\n </div>\n }\n <div class=\"flex flex-col min-w-0 flex-1\">\n <div class=\"flex items-baseline gap-small\">\n <span\n class=\"txt-heading-04\"\n [ngClass]=\"numClass(card)\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card.value }}</span\n >\n @if (card.delta) {\n <span class=\"text-xs font-semibold\" [ngClass]=\"deltaClass(card)\">\n {{ card.delta.direction === 'up' ? '\u25B2' : '\u25BC' }} {{ card.delta.value }}\n </span>\n }\n </div>\n <span class=\"txt-utility-caption text-text-secondary truncate\">{{ card.label }}</span>\n @if (card.spark?.length) {\n <fx-ui-sparkbar class=\"mt-xs\" [values]=\"card.spark!\" [height]=\"16\" [tone]=\"card.tone ?? null\" />\n }\n </div>\n </div>\n }\n }\n</div>\n" }]
|
|
4127
|
+
}], propDecorators: { cards: [{ type: i0.Input, args: [{ isSignal: true, alias: "cards", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], cols: [{
|
|
3735
4128
|
type: Input
|
|
3736
4129
|
}] } });
|
|
3737
4130
|
|
|
@@ -3771,18 +4164,24 @@ class FilterChipsComponent {
|
|
|
3771
4164
|
}
|
|
3772
4165
|
chipClass(option) {
|
|
3773
4166
|
const active = (this.value ?? '') === option.value;
|
|
4167
|
+
// Chip "All" (value rỗng) khi ĐANG CHỌN dùng **accent** (xanh), không phải
|
|
4168
|
+
// nền đen — khớp demo (ref 11: pill "All 40" là accent). Nền đen chỉ dành
|
|
4169
|
+
// cho badge severity Critical (anh Tú chốt 03/09).
|
|
3774
4170
|
if (!option.value) {
|
|
3775
4171
|
return active
|
|
3776
|
-
? 'bg-
|
|
3777
|
-
: 'bg-bg-hover text-text-secondary hover:bg-
|
|
4172
|
+
? 'bg-accent text-accent-contrast'
|
|
4173
|
+
: 'bg-bg-hover text-text-secondary hover:bg-accent-soft hover:text-accent-fg';
|
|
3778
4174
|
}
|
|
3779
4175
|
const tone = option.value in this.toneOverrides
|
|
3780
4176
|
? this.toneOverrides[option.value]
|
|
3781
4177
|
: severityTone(option.value);
|
|
3782
4178
|
if (!tone) {
|
|
3783
|
-
return active ? 'bg-
|
|
4179
|
+
return active ? 'bg-accent text-accent-contrast' : `${toneScale(null)} hover:opacity-50`;
|
|
3784
4180
|
}
|
|
3785
|
-
|
|
4181
|
+
// Chip tự có hình dạng riêng → chỉ lấy MÀU. Dùng thang đo (`tone-scale-*`)
|
|
4182
|
+
// chứ KHÔNG dùng `toneTint`: Critical ở filter phải là đỏ sẫm, không phải
|
|
4183
|
+
// pill đen đặc (anh Tú báo 03/09).
|
|
4184
|
+
return active ? `${toneBg(tone)} text-text-focus` : `${toneScale(tone)} hover:opacity-50`;
|
|
3786
4185
|
}
|
|
3787
4186
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterChipsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3788
4187
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FilterChipsComponent, isStandalone: true, selector: "fx-ui-filter-chips", inputs: { options: "options", value: "value", toneOverrides: "toneOverrides" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<div class=\"flex flex-wrap items-center gap-small\">\n @for (option of options; track option.value) {\n <button\n type=\"button\"\n class=\"px-3 py-1 rounded-full txt-utility-caption font-medium transition-colors cursor-pointer\"\n [ngClass]=\"chipClass(option)\"\n (click)=\"select(option)\"\n >\n {{ option.label }}\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
@@ -3798,6 +4197,129 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
3798
4197
|
type: Input
|
|
3799
4198
|
}] } });
|
|
3800
4199
|
|
|
4200
|
+
/**
|
|
4201
|
+
* V2 filter pills — the quick-filter row above ranked lists (design refs 02,
|
|
4202
|
+
* 03, 11): outlined capsules with a muted count, the active one filled with
|
|
4203
|
+
* the primary text colour.
|
|
4204
|
+
*
|
|
4205
|
+
* Distinct from `fx-ui-filter-chips`, which tints each chip by its severity
|
|
4206
|
+
* tone and carries no count. Presentational: the caller owns the filter state.
|
|
4207
|
+
*
|
|
4208
|
+
* <fx-ui-filter-pills [options]="pills" [value]="filters.status"
|
|
4209
|
+
* (valueChange)="filters.status = $event; refresh()" />
|
|
4210
|
+
*/
|
|
4211
|
+
class FilterPillsComponent {
|
|
4212
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
4213
|
+
/** Accepts undefined so screens can bind an optional filter-bag key. */
|
|
4214
|
+
value = input('', ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
4215
|
+
valueChange = output();
|
|
4216
|
+
isActive(option) {
|
|
4217
|
+
return (this.value() ?? '') === option.value;
|
|
4218
|
+
}
|
|
4219
|
+
/**
|
|
4220
|
+
* Pill ĐANG CHỌN dùng **accent** (xanh), không phải nền đen — khớp demo:
|
|
4221
|
+
* design ref 11 vẽ pill "All 40" là pill accent. Nền đen `--og-text` chỉ
|
|
4222
|
+
* dành cho badge severity Critical (`.og-badge--solid`), không dùng cho
|
|
4223
|
+
* trạng thái chọn của filter (anh Tú chốt 03/09).
|
|
4224
|
+
*/
|
|
4225
|
+
pillClass(option) {
|
|
4226
|
+
return this.isActive(option)
|
|
4227
|
+
? 'bg-accent text-accent-contrast border-accent'
|
|
4228
|
+
: 'border-border-default text-text-secondary hover:border-border-strong';
|
|
4229
|
+
}
|
|
4230
|
+
countClass(option) {
|
|
4231
|
+
return this.isActive(option) ? 'opacity-70' : 'text-text-placeholder';
|
|
4232
|
+
}
|
|
4233
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterPillsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4234
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FilterPillsComponent, isStandalone: true, selector: "fx-ui-filter-pills", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<div class=\"flex flex-wrap items-center gap-small\">\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n [attr.aria-pressed]=\"isActive(option)\"\n class=\"flex items-center gap-small px-semi py-xs rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors\"\n [ngClass]=\"pillClass(option)\"\n (click)=\"valueChange.emit(option.value)\"\n >\n {{ option.label }}\n @if (option.count !== null && option.count !== undefined) {\n <span [ngClass]=\"countClass(option)\" style=\"font-family: var(--og-font-figure)\">{{ option.count }}</span>\n }\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4235
|
+
}
|
|
4236
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FilterPillsComponent, decorators: [{
|
|
4237
|
+
type: Component,
|
|
4238
|
+
args: [{ selector: 'fx-ui-filter-pills', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex flex-wrap items-center gap-small\">\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n [attr.aria-pressed]=\"isActive(option)\"\n class=\"flex items-center gap-small px-semi py-xs rounded-full border txt-utility-caption font-bold cursor-pointer transition-colors\"\n [ngClass]=\"pillClass(option)\"\n (click)=\"valueChange.emit(option.value)\"\n >\n {{ option.label }}\n @if (option.count !== null && option.count !== undefined) {\n <span [ngClass]=\"countClass(option)\" style=\"font-family: var(--og-font-figure)\">{{ option.count }}</span>\n }\n </button>\n }\n</div>\n" }]
|
|
4239
|
+
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
|
|
4240
|
+
|
|
4241
|
+
/**
|
|
4242
|
+
* V2 stage progress dots — the walkthrough position indicator on the attack
|
|
4243
|
+
* chain toolbar (design refs 04, 07): one dot per stage, the current one drawn
|
|
4244
|
+
* as a wider accent bar, visited ones solid, the rest faint.
|
|
4245
|
+
*
|
|
4246
|
+
* <fx-ui-stage-dots [count]="8" [current]="stageIndex()"
|
|
4247
|
+
* (dotClick)="goToStage($event)" />
|
|
4248
|
+
*/
|
|
4249
|
+
class StageDotsComponent {
|
|
4250
|
+
count = input(0, ...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
|
|
4251
|
+
/** 0-based current stage; -1 (default) means none selected yet. */
|
|
4252
|
+
current = input(-1, ...(ngDevMode ? [{ debugName: "current" }] : /* istanbul ignore next */ []));
|
|
4253
|
+
/** Emit the clicked stage index; wire it to make the dots navigable. */
|
|
4254
|
+
dotClick = output();
|
|
4255
|
+
dots = computed(() => Array.from({ length: Math.max(0, this.count()) }, (_, i) => i), ...(ngDevMode ? [{ debugName: "dots" }] : /* istanbul ignore next */ []));
|
|
4256
|
+
dotClass(index) {
|
|
4257
|
+
const current = this.current();
|
|
4258
|
+
if (index === current)
|
|
4259
|
+
return 'w-5 bg-accent';
|
|
4260
|
+
if (current >= 0 && index < current)
|
|
4261
|
+
return 'w-1.5 bg-text-placeholder';
|
|
4262
|
+
return 'w-1.5 bg-border-strong';
|
|
4263
|
+
}
|
|
4264
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StageDotsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4265
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: StageDotsComponent, isStandalone: true, selector: "fx-ui-stage-dots", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dotClick: "dotClick" }, ngImport: i0, template: "<div class=\"flex items-center gap-xs\" role=\"tablist\">\n @for (i of dots(); track i) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === current()\"\n [attr.aria-label]=\"'Stage ' + (i + 1)\"\n class=\"h-1.5 rounded-full cursor-pointer transition-all duration-150 hover:opacity-70\"\n [ngClass]=\"dotClass(i)\"\n (click)=\"dotClick.emit(i)\"\n ></button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4266
|
+
}
|
|
4267
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StageDotsComponent, decorators: [{
|
|
4268
|
+
type: Component,
|
|
4269
|
+
args: [{ selector: 'fx-ui-stage-dots', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex items-center gap-xs\" role=\"tablist\">\n @for (i of dots(); track i) {\n <button\n type=\"button\"\n role=\"tab\"\n [attr.aria-selected]=\"i === current()\"\n [attr.aria-label]=\"'Stage ' + (i + 1)\"\n class=\"h-1.5 rounded-full cursor-pointer transition-all duration-150 hover:opacity-70\"\n [ngClass]=\"dotClass(i)\"\n (click)=\"dotClick.emit(i)\"\n ></button>\n }\n</div>\n" }]
|
|
4270
|
+
}], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], current: [{ type: i0.Input, args: [{ isSignal: true, alias: "current", required: false }] }], dotClick: [{ type: i0.Output, args: ["dotClick"] }] } });
|
|
4271
|
+
|
|
4272
|
+
/**
|
|
4273
|
+
* V2 evidence code block — the mono panel that carries a process tree, a
|
|
4274
|
+
* command line or any captured text (design ref 05, "Process tree").
|
|
4275
|
+
*
|
|
4276
|
+
* Whitespace is preserved and the box scrolls on its own rather than pushing
|
|
4277
|
+
* the page wide, so ASCII trees survive narrow viewports.
|
|
4278
|
+
*
|
|
4279
|
+
* <fx-ui-code-block [label]="'analyze.processTree' | translate" [code]="tree" />
|
|
4280
|
+
*/
|
|
4281
|
+
class CodeBlockComponent {
|
|
4282
|
+
code = input('', ...(ngDevMode ? [{ debugName: "code" }] : /* istanbul ignore next */ []));
|
|
4283
|
+
/** Caller-translated caption above the box; omit for a bare block. */
|
|
4284
|
+
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4285
|
+
/** Scroll cap; empty grows with the content. */
|
|
4286
|
+
maxHeight = input('', ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
|
|
4287
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CodeBlockComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4288
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: CodeBlockComponent, isStandalone: true, selector: "fx-ui-code-block", inputs: { code: { classPropertyName: "code", publicName: "code", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (label()) {\n <div class=\"txt-utility-caption font-bold text-text-secondary mb-normal\">{{ label() }}</div>\n}\n<pre\n class=\"m-0 p-large rounded-control border border-border bg-surface overflow-auto txt-body-compact-01 text-strong\"\n [style.max-height]=\"maxHeight() || null\"\n style=\"font-family: var(--og-font-mono); white-space: pre; tab-size: 2\"\n>{{ code() }}</pre>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4289
|
+
}
|
|
4290
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: CodeBlockComponent, decorators: [{
|
|
4291
|
+
type: Component,
|
|
4292
|
+
args: [{ selector: 'fx-ui-code-block', changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (label()) {\n <div class=\"txt-utility-caption font-bold text-text-secondary mb-normal\">{{ label() }}</div>\n}\n<pre\n class=\"m-0 p-large rounded-control border border-border bg-surface overflow-auto txt-body-compact-01 text-strong\"\n [style.max-height]=\"maxHeight() || null\"\n style=\"font-family: var(--og-font-mono); white-space: pre; tab-size: 2\"\n>{{ code() }}</pre>\n" }]
|
|
4293
|
+
}], propDecorators: { code: [{ type: i0.Input, args: [{ isSignal: true, alias: "code", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }] } });
|
|
4294
|
+
|
|
4295
|
+
/**
|
|
4296
|
+
* V2 captured-evidence frame — a faux window chrome (traffic lights + mono file
|
|
4297
|
+
* name + timestamp) around a screenshot, with a placeholder when the image is
|
|
4298
|
+
* missing (design ref 05, "Screenshot").
|
|
4299
|
+
*
|
|
4300
|
+
* <fx-ui-screenshot-frame [label]="'analyze.screenshot' | translate"
|
|
4301
|
+
* fileName="chain-05.png" timestamp="09:42:18"
|
|
4302
|
+
* [src]="url" [caption]="caption" />
|
|
4303
|
+
*/
|
|
4304
|
+
class ScreenshotFrameComponent {
|
|
4305
|
+
/** Caller-translated caption above the frame; omit for a bare frame. */
|
|
4306
|
+
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4307
|
+
fileName = input('', ...(ngDevMode ? [{ debugName: "fileName" }] : /* istanbul ignore next */ []));
|
|
4308
|
+
timestamp = input('', ...(ngDevMode ? [{ debugName: "timestamp" }] : /* istanbul ignore next */ []));
|
|
4309
|
+
/** Image URL; empty renders the placeholder body. */
|
|
4310
|
+
src = input('', ...(ngDevMode ? [{ debugName: "src" }] : /* istanbul ignore next */ []));
|
|
4311
|
+
/** Caller-translated line under (or inside) the frame. */
|
|
4312
|
+
caption = input('', ...(ngDevMode ? [{ debugName: "caption" }] : /* istanbul ignore next */ []));
|
|
4313
|
+
alt = input('', ...(ngDevMode ? [{ debugName: "alt" }] : /* istanbul ignore next */ []));
|
|
4314
|
+
height = input('180px', ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
4315
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScreenshotFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4316
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ScreenshotFrameComponent, isStandalone: true, selector: "fx-ui-screenshot-frame", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, fileName: { classPropertyName: "fileName", publicName: "fileName", isSignal: true, isRequired: false, transformFunction: null }, timestamp: { classPropertyName: "timestamp", publicName: "timestamp", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, alt: { classPropertyName: "alt", publicName: "alt", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (label()) {\n <div class=\"txt-utility-caption font-bold text-text-secondary mb-normal\">{{ label() }}</div>\n}\n<div class=\"rounded-control border border-border bg-surface overflow-hidden\">\n <!-- Window chrome: traffic lights + mono file name + capture time -->\n <div class=\"flex items-center gap-small px-normal py-small border-b border-border bg-surface-sunken\">\n <span class=\"flex items-center gap-xs shrink-0\">\n <span class=\"w-2 h-2 rounded-full bg-critical/70\"></span>\n <span class=\"w-2 h-2 rounded-full bg-warning/70\"></span>\n <span class=\"w-2 h-2 rounded-full bg-success/70\"></span>\n </span>\n @if (fileName()) {\n <span class=\"txt-utility-caption text-text-secondary truncate\" style=\"font-family: var(--og-font-mono)\">\n {{ fileName() }}\n </span>\n }\n @if (timestamp()) {\n <span class=\"txt-utility-caption text-text-placeholder shrink-0\" style=\"font-family: var(--og-font-mono)\">\n \u00B7 {{ timestamp() }}\n </span>\n }\n </div>\n\n @if (src()) {\n <img [src]=\"src()\" [alt]=\"alt() || fileName()\" class=\"block w-full object-cover\" [style.height]=\"height()\" />\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-normal px-large text-center\"\n [style.height]=\"height()\"\n >\n <fx-ui-hero-icon icon=\"camera\" [size]=\"22\" class=\"flex text-text-placeholder\"></fx-ui-hero-icon>\n @if (caption()) {\n <span class=\"txt-utility-caption text-text-placeholder max-w-[280px]\">{{ caption() }}</span>\n }\n </div>\n }\n</div>\n@if (src() && caption()) {\n <div class=\"txt-utility-caption text-text-placeholder mt-normal\">{{ caption() }}</div>\n}\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4317
|
+
}
|
|
4318
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScreenshotFrameComponent, decorators: [{
|
|
4319
|
+
type: Component,
|
|
4320
|
+
args: [{ selector: 'fx-ui-screenshot-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "@if (label()) {\n <div class=\"txt-utility-caption font-bold text-text-secondary mb-normal\">{{ label() }}</div>\n}\n<div class=\"rounded-control border border-border bg-surface overflow-hidden\">\n <!-- Window chrome: traffic lights + mono file name + capture time -->\n <div class=\"flex items-center gap-small px-normal py-small border-b border-border bg-surface-sunken\">\n <span class=\"flex items-center gap-xs shrink-0\">\n <span class=\"w-2 h-2 rounded-full bg-critical/70\"></span>\n <span class=\"w-2 h-2 rounded-full bg-warning/70\"></span>\n <span class=\"w-2 h-2 rounded-full bg-success/70\"></span>\n </span>\n @if (fileName()) {\n <span class=\"txt-utility-caption text-text-secondary truncate\" style=\"font-family: var(--og-font-mono)\">\n {{ fileName() }}\n </span>\n }\n @if (timestamp()) {\n <span class=\"txt-utility-caption text-text-placeholder shrink-0\" style=\"font-family: var(--og-font-mono)\">\n \u00B7 {{ timestamp() }}\n </span>\n }\n </div>\n\n @if (src()) {\n <img [src]=\"src()\" [alt]=\"alt() || fileName()\" class=\"block w-full object-cover\" [style.height]=\"height()\" />\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-normal px-large text-center\"\n [style.height]=\"height()\"\n >\n <fx-ui-hero-icon icon=\"camera\" [size]=\"22\" class=\"flex text-text-placeholder\"></fx-ui-hero-icon>\n @if (caption()) {\n <span class=\"txt-utility-caption text-text-placeholder max-w-[280px]\">{{ caption() }}</span>\n }\n </div>\n }\n</div>\n@if (src() && caption()) {\n <div class=\"txt-utility-caption text-text-placeholder mt-normal\">{{ caption() }}</div>\n}\n" }]
|
|
4321
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], fileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileName", required: false }] }], timestamp: [{ type: i0.Input, args: [{ isSignal: true, alias: "timestamp", required: false }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }], alt: [{ type: i0.Input, args: [{ isSignal: true, alias: "alt", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
|
|
4322
|
+
|
|
3801
4323
|
/** Thin tone-coloured progress/score bar with optional label and caption. */
|
|
3802
4324
|
class MeterComponent {
|
|
3803
4325
|
value = 0;
|
|
@@ -3830,26 +4352,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
3830
4352
|
type: Input
|
|
3831
4353
|
}] } });
|
|
3832
4354
|
|
|
3833
|
-
/**
|
|
4355
|
+
/**
|
|
4356
|
+
* Panel section: card with a header and projected body.
|
|
4357
|
+
* - titleStyle 'heading' (default): icon + heading, the pre-V2 look
|
|
4358
|
+
* - titleStyle 'eyebrow': the V2 design's panel header — small tracked
|
|
4359
|
+
* caption on the left, muted [aside] text on the right
|
|
4360
|
+
*/
|
|
3834
4361
|
class SectionCardComponent {
|
|
3835
4362
|
/** Caller-translated heading. */
|
|
3836
4363
|
title = input('', ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
3837
4364
|
/** hero-icon name; omitted → text-only header. */
|
|
3838
4365
|
icon;
|
|
3839
4366
|
iconTone = input(null, ...(ngDevMode ? [{ debugName: "iconTone" }] : /* istanbul ignore next */ []));
|
|
4367
|
+
titleStyle = input('heading', ...(ngDevMode ? [{ debugName: "titleStyle" }] : /* istanbul ignore next */ []));
|
|
4368
|
+
/** Caller-translated right-aligned hint (eyebrow style). */
|
|
4369
|
+
aside = input('', ...(ngDevMode ? [{ debugName: "aside" }] : /* istanbul ignore next */ []));
|
|
3840
4370
|
get iconClass() {
|
|
3841
4371
|
const iconTone = this.iconTone();
|
|
3842
4372
|
return iconTone ? toneText(iconTone) : 'text-text-secondary';
|
|
3843
4373
|
}
|
|
3844
4374
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3845
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SectionCardComponent, isStandalone: true, selector: "fx-ui-section-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, iconTone: { classPropertyName: "iconTone", publicName: "iconTone", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"card-common\">\n <div class=\"flex items-
|
|
4375
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SectionCardComponent, isStandalone: true, selector: "fx-ui-section-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: false, isRequired: false, transformFunction: null }, iconTone: { classPropertyName: "iconTone", publicName: "iconTone", isSignal: true, isRequired: false, transformFunction: null }, titleStyle: { classPropertyName: "titleStyle", publicName: "titleStyle", isSignal: true, isRequired: false, transformFunction: null }, aside: { classPropertyName: "aside", publicName: "aside", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"card-common\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3846
4376
|
}
|
|
3847
4377
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SectionCardComponent, decorators: [{
|
|
3848
4378
|
type: Component,
|
|
3849
|
-
args: [{ selector: 'fx-ui-section-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent, NgClass], template: "<div class=\"card-common\">\n <div class=\"flex items-
|
|
4379
|
+
args: [{ selector: 'fx-ui-section-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent, NgClass], template: "<div class=\"card-common\">\n @if (titleStyle() === 'eyebrow') {\n <div class=\"flex items-baseline justify-between gap-normal mb-large\">\n <span class=\"txt-eyebrow\">{{ title() }}</span>\n @if (aside()) {\n <span class=\"txt-utility-description text-right\">{{ aside() }}</span>\n }\n </div>\n } @else {\n <div class=\"flex items-center gap-normal mb-large\">\n @if (icon) {\n <fx-ui-hero-icon [ngClass]=\"iconClass\" [icon]=\"icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span class=\"txt-heading-06\">{{ title() }}</span>\n </div>\n }\n <ng-content></ng-content>\n</div>\n" }]
|
|
3850
4380
|
}], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], icon: [{
|
|
3851
4381
|
type: Input
|
|
3852
|
-
}], iconTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTone", required: false }] }] } });
|
|
4382
|
+
}], iconTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconTone", required: false }] }], titleStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleStyle", required: false }] }], aside: [{ type: i0.Input, args: [{ isSignal: true, alias: "aside", required: false }] }] } });
|
|
3853
4383
|
|
|
3854
4384
|
/**
|
|
3855
4385
|
* Label-over-value field for detail panels. The label uses txt-field-label
|
|
@@ -3938,11 +4468,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
3938
4468
|
|
|
3939
4469
|
let nextUid = 0;
|
|
3940
4470
|
/**
|
|
3941
|
-
*
|
|
3942
|
-
*
|
|
3943
|
-
* the
|
|
4471
|
+
* Master-detail shell driven by a BaseMasterDetailComponent. Two-column by
|
|
4472
|
+
* default; projecting a [mdFacets] block (V2 lookup screens) adds a third
|
|
4473
|
+
* facets-rail column before the list. The host page projects its own
|
|
4474
|
+
* header/filters and supplies templates for the parts that vary per entity:
|
|
3944
4475
|
*
|
|
3945
4476
|
* <fx-ui-master-detail [table]="this" [emptyMessage]="'noData' | translate">
|
|
4477
|
+
* <div mdFacets>…<fx-ui-facets-rail …/>…</div> <!-- optional, V2 -->
|
|
3946
4478
|
* <div mdHeader>…title / stat cards…</div>
|
|
3947
4479
|
* <div mdFilters>…search bar / filter chips…</div>
|
|
3948
4480
|
* <ng-template #row let-item let-selected="selected">…row body…</ng-template>
|
|
@@ -3953,19 +4485,38 @@ let nextUid = 0;
|
|
|
3953
4485
|
* tab stop, ArrowUp/Down/Home/End move the selection.
|
|
3954
4486
|
*/
|
|
3955
4487
|
class MasterDetailComponent {
|
|
4488
|
+
hostEl = inject((ElementRef));
|
|
4489
|
+
/** True when the host projected a [mdFacets] block — switches to 3 columns. */
|
|
4490
|
+
hasFacets = false;
|
|
3956
4491
|
table;
|
|
3957
4492
|
/** Fixed list-pane width on lg+ viewports (stacked full-width below). */
|
|
3958
4493
|
listWidth = input('420px', ...(ngDevMode ? [{ debugName: "listWidth" }] : /* istanbul ignore next */ []));
|
|
3959
|
-
|
|
4494
|
+
/**
|
|
4495
|
+
* Chiều cao CHUNG của cả ba cụm (facets / list / detail). Mặc định trừ phần
|
|
4496
|
+
* chrome phía trên (topbar + tab strip + context line) khỏi viewport để
|
|
4497
|
+
* bảng ba cột vừa đúng một màn, không kéo trang cuộn theo.
|
|
4498
|
+
* Chỉ có tác dụng từ 1080px — xem `.fx-md-row` trong CSS.
|
|
4499
|
+
*/
|
|
4500
|
+
paneHeight = input('calc(100vh - 300px)', ...(ngDevMode ? [{ debugName: "paneHeight" }] : /* istanbul ignore next */ []));
|
|
3960
4501
|
pageSizeOptions = input([5, 10, 20, 50], ...(ngDevMode ? [{ debugName: "pageSizeOptions" }] : /* istanbul ignore next */ []));
|
|
3961
4502
|
/** Labels come in via inputs — the lib carries no i18n. */
|
|
3962
4503
|
emptyMessage = input('No data', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
|
|
3963
4504
|
listLabel = input('', ...(ngDevMode ? [{ debugName: "listLabel" }] : /* istanbul ignore next */ []));
|
|
3964
4505
|
skeletonCards = input(3, ...(ngDevMode ? [{ debugName: "skeletonCards" }] : /* istanbul ignore next */ []));
|
|
4506
|
+
/** 'prevnext' renders the V2 "X of Y" + Prev/Next controls instead of the
|
|
4507
|
+
Material paginator (which stays in the DOM, hidden — it is the paging
|
|
4508
|
+
engine BaseTable subscribes to). */
|
|
4509
|
+
paginationStyle = input('paginator', ...(ngDevMode ? [{ debugName: "paginationStyle" }] : /* istanbul ignore next */ []));
|
|
4510
|
+
/** Caller-translated Prev/Next labels ("X of Y" is numeric). */
|
|
4511
|
+
prevLabel = input('Prev', ...(ngDevMode ? [{ debugName: "prevLabel" }] : /* istanbul ignore next */ []));
|
|
4512
|
+
nextLabel = input('Next', ...(ngDevMode ? [{ debugName: "nextLabel" }] : /* istanbul ignore next */ []));
|
|
3965
4513
|
rowTemplate = contentChild.required('row');
|
|
3966
4514
|
detailTemplate = contentChild.required('detail');
|
|
3967
4515
|
paginator = viewChild.required(MatPaginator);
|
|
3968
4516
|
uid = `fx-md-${nextUid++}`;
|
|
4517
|
+
ngAfterContentInit() {
|
|
4518
|
+
this.hasFacets = !!this.hostEl.nativeElement.querySelector('[mdFacets]');
|
|
4519
|
+
}
|
|
3969
4520
|
ngAfterViewInit() {
|
|
3970
4521
|
// The paginator lives in this view, not the host page's, so the host's
|
|
3971
4522
|
// @ViewChild(MatPaginator) never finds it. Hand it to the table so
|
|
@@ -3974,6 +4525,17 @@ class MasterDetailComponent {
|
|
|
3974
4525
|
this.table.paginator = this.paginator();
|
|
3975
4526
|
}
|
|
3976
4527
|
}
|
|
4528
|
+
/** "X of Y" range text for the prevnext pagination style. */
|
|
4529
|
+
rangeText() {
|
|
4530
|
+
const total = this.table.total ?? 0;
|
|
4531
|
+
if (!total)
|
|
4532
|
+
return '0 of 0';
|
|
4533
|
+
const page = this.paginator().pageIndex;
|
|
4534
|
+
const size = this.table.pageSize;
|
|
4535
|
+
const from = page * size + 1;
|
|
4536
|
+
const to = Math.min((page + 1) * size, total);
|
|
4537
|
+
return `${from}–${to} of ${total}`;
|
|
4538
|
+
}
|
|
3977
4539
|
isSelected(row) {
|
|
3978
4540
|
return this.table.idOf(row) === this.table.selectedId;
|
|
3979
4541
|
}
|
|
@@ -4012,7 +4574,7 @@ class MasterDetailComponent {
|
|
|
4012
4574
|
el?.scrollIntoView({ block: 'nearest' });
|
|
4013
4575
|
}
|
|
4014
4576
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4015
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: MasterDetailComponent, isStandalone: true, selector: "fx-ui-master-detail", inputs: { table: { classPropertyName: "table", publicName: "table", isSignal: false, isRequired: false, transformFunction: null }, listWidth: { classPropertyName: "listWidth", publicName: "listWidth", isSignal: true, isRequired: false, transformFunction: null },
|
|
4577
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: MasterDetailComponent, isStandalone: true, selector: "fx-ui-master-detail", inputs: { table: { classPropertyName: "table", publicName: "table", isSignal: false, isRequired: false, transformFunction: null }, listWidth: { classPropertyName: "listWidth", publicName: "listWidth", isSignal: true, isRequired: false, transformFunction: null }, paneHeight: { classPropertyName: "paneHeight", publicName: "paneHeight", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, listLabel: { classPropertyName: "listLabel", publicName: "listLabel", isSignal: true, isRequired: false, transformFunction: null }, skeletonCards: { classPropertyName: "skeletonCards", publicName: "skeletonCards", isSignal: true, isRequired: false, transformFunction: null }, paginationStyle: { classPropertyName: "paginationStyle", publicName: "paginationStyle", isSignal: true, isRequired: false, transformFunction: null }, prevLabel: { classPropertyName: "prevLabel", publicName: "prevLabel", isSignal: true, isRequired: false, transformFunction: null }, nextLabel: { classPropertyName: "nextLabel", publicName: "nextLabel", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "rowTemplate", first: true, predicate: ["row"], descendants: true, isSignal: true }, { propertyName: "detailTemplate", first: true, predicate: ["detail"], descendants: true, isSignal: true }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- 3 c\u1EE5m CHUNG chi\u1EC1u cao, m\u1ED7i c\u1EE5m SCROLL RI\u00CANG (anh T\u00FA ch\u1ED1t 03/09).\n `items-stretch` + chi\u1EC1u cao \u0111\u1EB7t tr\u00EAn h\u00E0ng (xem .fx-md-row trong CSS) \u0111\u1EC3\n c\u1EA3 ba pane b\u1EB1ng nhau; m\u1ED7i pane l\u00E0 flex-col + min-h-0 v\u00E0 ch\u1EC9 ph\u1EA7n th\u00E2n\n `overflow-auto flex-1` cu\u1ED9n \u2014 header/filter/paginator \u0111\u1EE9ng y\u00EAn.\n \u26A0\uFE0F H\u01B0\u1EDBng h\u00E0ng + chi\u1EC1u cao do CONTAINER QUERY trong CSS quy\u1EBFt \u0111\u1ECBnh (theo\n b\u1EC1 r\u1ED9ng c\u1EE7a ch\u00EDnh h\u00E0ng), KH\u00D4NG ph\u1EA3i `lg:flex-row`. \u0110\u1EEBng th\u00EAm l\u1EA1i class\n `lg:*` \u1EDF \u0111\u00E2y \u2014 `lg` trong lib l\u00E0 1080px v\u00E0 viewport l\u00E0 proxy sai; xem\n ghi ch\u00FA \u0111\u1EA7u master-detail.component.css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch gap-xl\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail (V2 3-column lookup) \u2014 wrapper always rendered so ng-content\n stays unconditional; hidden when the host projects nothing. -->\n <div\n class=\"fx-md-facets card-common bg-surface flex flex-col min-h-0\"\n [class.hidden]=\"!hasFacets\"\n >\n <div class=\"flex-1 min-h-0 overflow-auto pr-xs\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common flex flex-col min-h-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n <div\n class=\"flex flex-col gap-normal overflow-auto pr-xs flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left rounded-lg border p-semi transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-accent bg-accent-soft/60'\n : 'border-border-default hover:border-border-interactive bg-bg-primary'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"mt-normal\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between mt-normal\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Detail pane -->\n <div class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"card-common flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 968px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:320px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 400px;min-width:400px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: EmptyStateComponent, selector: "fx-ui-empty-state", inputs: ["message", "icon", "height"] }, { kind: "component", type: MatPaginator, selector: "mat-paginator", inputs: ["color", "pageIndex", "length", "pageSize", "pageSizeOptions", "hidePageSize", "showFirstLastButtons", "selectConfig", "disabled"], outputs: ["page"], exportAs: ["matPaginator"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }] });
|
|
4016
4578
|
}
|
|
4017
4579
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: MasterDetailComponent, decorators: [{
|
|
4018
4580
|
type: Component,
|
|
@@ -4023,10 +4585,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
4023
4585
|
EmptyStateComponent,
|
|
4024
4586
|
MatPaginator,
|
|
4025
4587
|
SkeletonDetailComponent,
|
|
4026
|
-
], template: "
|
|
4588
|
+
], template: "<!-- 3 c\u1EE5m CHUNG chi\u1EC1u cao, m\u1ED7i c\u1EE5m SCROLL RI\u00CANG (anh T\u00FA ch\u1ED1t 03/09).\n `items-stretch` + chi\u1EC1u cao \u0111\u1EB7t tr\u00EAn h\u00E0ng (xem .fx-md-row trong CSS) \u0111\u1EC3\n c\u1EA3 ba pane b\u1EB1ng nhau; m\u1ED7i pane l\u00E0 flex-col + min-h-0 v\u00E0 ch\u1EC9 ph\u1EA7n th\u00E2n\n `overflow-auto flex-1` cu\u1ED9n \u2014 header/filter/paginator \u0111\u1EE9ng y\u00EAn.\n \u26A0\uFE0F H\u01B0\u1EDBng h\u00E0ng + chi\u1EC1u cao do CONTAINER QUERY trong CSS quy\u1EBFt \u0111\u1ECBnh (theo\n b\u1EC1 r\u1ED9ng c\u1EE7a ch\u00EDnh h\u00E0ng), KH\u00D4NG ph\u1EA3i `lg:flex-row`. \u0110\u1EEBng th\u00EAm l\u1EA1i class\n `lg:*` \u1EDF \u0111\u00E2y \u2014 `lg` trong lib l\u00E0 1080px v\u00E0 viewport l\u00E0 proxy sai; xem\n ghi ch\u00FA \u0111\u1EA7u master-detail.component.css. -->\n<div\n class=\"fx-md-row flex flex-col items-stretch gap-xl\"\n [style.--fx-md-list-width]=\"listWidth()\"\n [style.--fx-md-pane-h]=\"paneHeight()\"\n>\n <!-- Facets rail (V2 3-column lookup) \u2014 wrapper always rendered so ng-content\n stays unconditional; hidden when the host projects nothing. -->\n <div\n class=\"fx-md-facets card-common bg-surface flex flex-col min-h-0\"\n [class.hidden]=\"!hasFacets\"\n >\n <div class=\"flex-1 min-h-0 overflow-auto pr-xs\">\n <ng-content select=\"[mdFacets]\"></ng-content>\n </div>\n </div>\n\n <!-- Master: list pane -->\n <div class=\"fx-md-list card-common flex flex-col min-h-0\">\n <ng-content select=\"[mdHeader]\"></ng-content>\n <ng-content select=\"[mdFilters]\"></ng-content>\n <div\n class=\"flex flex-col gap-normal overflow-auto pr-xs flex-1 min-h-0\"\n role=\"listbox\"\n [attr.aria-label]=\"listLabel() || null\"\n >\n @if (table.loading) {\n <fx-ui-skeleton-list [rows]=\"table.pageSize\"></fx-ui-skeleton-list>\n } @else {\n @for (row of table.dataSource.data; track table.idOf(row); let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"isSelected(row)\"\n [tabindex]=\"isSelected(row) ? 0 : -1\"\n (click)=\"table.selectItem(row)\"\n (keydown)=\"onKeydown($event, i)\"\n class=\"text-left rounded-lg border p-semi transition-colors\"\n [ngClass]=\"\n isSelected(row)\n ? 'border-accent bg-accent-soft/60'\n : 'border-border-default hover:border-border-interactive bg-bg-primary'\n \"\n >\n <ng-container\n [ngTemplateOutlet]=\"rowTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: row, selected: isSelected(row) }\"\n ></ng-container>\n </button>\n }\n @if (!table.dataSource.data.length) {\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n }\n }\n </div>\n <!-- No (page) binding: ngAfterViewInit hands this paginator to the table,\n whose own BaseTable subscription (running after this child's hook)\n wires page events \u2014 a binding here would double-fetch every change. -->\n <mat-paginator\n class=\"mt-normal\"\n [class.fx-md-paginator-hidden]=\"paginationStyle() === 'prevnext'\"\n [length]=\"table.total\"\n [pageSize]=\"table.pageSize\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n ></mat-paginator>\n @if (paginationStyle() === 'prevnext') {\n <div class=\"flex items-center justify-between mt-normal\">\n <span class=\"txt-utility-description\" style=\"font-family: var(--og-font-figure)\">\n {{ rangeText() }}\n </span>\n <div class=\"flex gap-small\">\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasPreviousPage()\"\n (click)=\"paginator().previousPage()\"\n >\n {{ prevLabel() }}\n </button>\n <button\n type=\"button\"\n class=\"btn-alternative !h-8 !px-semi text-sm\"\n [disabled]=\"!paginator().hasNextPage()\"\n (click)=\"paginator().nextPage()\"\n >\n {{ nextLabel() }}\n </button>\n </div>\n </div>\n }\n </div>\n\n <!-- Detail pane -->\n <div class=\"fx-md-detail w-full min-w-0 flex flex-col min-h-0\">\n <div class=\"flex-1 min-h-0 overflow-auto\">\n @if (table.selected) {\n @if (table.detailBusy) {\n <fx-ui-skeleton-detail [cards]=\"skeletonCards()\"></fx-ui-skeleton-detail>\n } @else {\n <ng-container\n [ngTemplateOutlet]=\"detailTemplate()\"\n [ngTemplateOutletContext]=\"{ $implicit: table.detail, selected: table.selected }\"\n ></ng-container>\n }\n } @else {\n <div class=\"card-common flex items-center justify-center h-full min-h-[300px]\">\n <fx-ui-empty-state [message]=\"emptyMessage()\"></fx-ui-empty-state>\n </div>\n }\n </div>\n </div>\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-md}.fx-md-list,.fx-md-facets{width:100%}@container fx-md (min-width: 968px){.fx-md-row{flex-direction:row;height:var(--fx-md-pane-h, calc(100vh - 300px) );min-height:420px}.fx-md-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-md-list{width:auto;flex:0 1 var(--fx-md-list-width, 420px);min-width:320px;max-width:var(--fx-md-list-width, 420px)}.fx-md-detail{flex:1 1 400px;min-width:400px}}.fx-md-paginator-hidden{position:absolute!important;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}\n"] }]
|
|
4027
4589
|
}], propDecorators: { table: [{
|
|
4028
4590
|
type: Input
|
|
4029
|
-
}], listWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "listWidth", required: false }] }],
|
|
4591
|
+
}], listWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "listWidth", required: false }] }], paneHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "paneHeight", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], listLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "listLabel", required: false }] }], skeletonCards: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonCards", required: false }] }], paginationStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationStyle", required: false }] }], prevLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], rowTemplate: [{ type: i0.ContentChild, args: ['row', { isSignal: true }] }], detailTemplate: [{ type: i0.ContentChild, args: ['detail', { isSignal: true }] }], paginator: [{ type: i0.ViewChild, args: [i0.forwardRef(() => MatPaginator), { isSignal: true }] }] } });
|
|
4030
4592
|
|
|
4031
4593
|
/**
|
|
4032
4594
|
* Row-action dropdown wrapper over mat-menu (behavior-layer rule, Playbook
|
|
@@ -4047,13 +4609,13 @@ class MenuComponent {
|
|
|
4047
4609
|
switch (item.tone) {
|
|
4048
4610
|
case 'critical':
|
|
4049
4611
|
case 'danger':
|
|
4050
|
-
return '
|
|
4612
|
+
return 'tone-fg-danger';
|
|
4051
4613
|
case 'warning':
|
|
4052
|
-
return '
|
|
4614
|
+
return 'tone-fg-warning';
|
|
4053
4615
|
case 'success':
|
|
4054
|
-
return '
|
|
4616
|
+
return 'tone-fg-success';
|
|
4055
4617
|
case 'information':
|
|
4056
|
-
return '
|
|
4618
|
+
return 'tone-fg-information';
|
|
4057
4619
|
default:
|
|
4058
4620
|
return 'text-text-primary';
|
|
4059
4621
|
}
|
|
@@ -4066,6 +4628,601 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
4066
4628
|
args: [{ selector: 'fx-ui-menu', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatMenuModule, HeroIconComponent], template: "<button\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-lg p-1 hover:bg-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n [matMenuTriggerFor]=\"menu\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <ng-content></ng-content>\n</button>\n\n<mat-menu #menu=\"matMenu\">\n @for (item of items(); track item.label) {\n <button\n mat-menu-item\n type=\"button\"\n [disabled]=\"item.disabled\"\n (click)=\"select.emit(item)\"\n >\n <span class=\"flex items-center gap-normal\" [class]=\"toneClass(item)\">\n @if (item.icon) {\n <fx-ui-hero-icon [icon]=\"item.icon\" [size]=\"18\"></fx-ui-hero-icon>\n }\n <span>{{ item.label }}</span>\n </span>\n </button>\n }\n</mat-menu>\n" }]
|
|
4067
4629
|
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
|
|
4068
4630
|
|
|
4631
|
+
/**
|
|
4632
|
+
* V2 theme picker — dropdown các theme OgeniumDashboards cho chọn
|
|
4633
|
+
* (OG_PICKER_THEME_META: porcelain/charcoal), mỗi dòng có swatch accent +
|
|
4634
|
+
* page. Drives ThemeService.setTheme(); replaces the pre-V2 sun/moon toggle
|
|
4635
|
+
* (behavior-layer rule: mat-menu stays inside the lib, apps never import
|
|
4636
|
+
* @angular/material directly).
|
|
4637
|
+
*
|
|
4638
|
+
* <fx-ui-theme-picker />
|
|
4639
|
+
*/
|
|
4640
|
+
class ThemePickerComponent {
|
|
4641
|
+
themeService = inject(ThemeService);
|
|
4642
|
+
themes = OG_PICKER_THEME_META;
|
|
4643
|
+
current = toSignal(this.themeService.themeChanges(), {
|
|
4644
|
+
initialValue: this.themeService.getCurrentTheme(),
|
|
4645
|
+
});
|
|
4646
|
+
/** Tra trên bộ ĐẦY ĐỦ: theme đang active có thể là một trong 4 theme đã ẩn
|
|
4647
|
+
khỏi picker (deep-link /themes, localStorage cũ) — nhãn vẫn phải đúng. */
|
|
4648
|
+
currentMeta() {
|
|
4649
|
+
return OG_THEME_META.find((t) => t.name === this.current()) ?? this.themes[0];
|
|
4650
|
+
}
|
|
4651
|
+
select(theme) {
|
|
4652
|
+
this.themeService.setTheme(theme);
|
|
4653
|
+
}
|
|
4654
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemePickerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4655
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ThemePickerComponent, isStandalone: true, selector: "fx-ui-theme-picker", ngImport: i0, template: "<button\n type=\"button\"\n [matMenuTriggerFor]=\"themeMenu\"\n class=\"flex items-center gap-small px-normal py-small rounded-control border border-border-default bg-transparent cursor-pointer hover:bg-bg-hover transition-colors\"\n aria-label=\"Change theme\"\n>\n <span\n class=\"w-4 h-4 rounded-full border border-border-strong/30 shrink-0\"\n [style.background]=\"\n 'linear-gradient(135deg, ' +\n currentMeta().accent +\n ' 50%, ' +\n currentMeta().pageBg +\n ' 50%)'\n \"\n ></span>\n <span class=\"txt-body-01 hidden md:inline\">{{ currentMeta().label }}</span>\n <fx-ui-hero-icon icon=\"chevron-down\" [size]=\"14\" class=\"flex text-text-secondary\" />\n</button>\n\n<mat-menu #themeMenu=\"matMenu\">\n @for (theme of themes; track theme.name) {\n <button\n mat-menu-item\n type=\"button\"\n (click)=\"select(theme.name)\"\n [attr.aria-current]=\"theme.name === current() ? 'true' : null\"\n >\n <div class=\"flex items-center gap-normal min-w-40\">\n <span\n class=\"w-5 h-5 rounded-full border border-border-strong/30 shrink-0\"\n [style.background]=\"\n 'linear-gradient(135deg, ' + theme.accent + ' 50%, ' + theme.pageBg + ' 50%)'\n \"\n ></span>\n <span class=\"txt-body-01 flex-1\">{{ theme.label }}</span>\n <span class=\"txt-utility-description\">{{ theme.dark ? 'Dark' : 'Light' }}</span>\n @if (theme.name === current()) {\n <fx-ui-hero-icon icon=\"check\" [size]=\"16\" class=\"flex text-text-selected\" />\n }\n </div>\n </button>\n }\n</mat-menu>\n", dependencies: [{ kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i1$4.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i1$4.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i1$4.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4656
|
+
}
|
|
4657
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ThemePickerComponent, decorators: [{
|
|
4658
|
+
type: Component,
|
|
4659
|
+
args: [{ selector: 'fx-ui-theme-picker', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MatMenuModule, HeroIconComponent], template: "<button\n type=\"button\"\n [matMenuTriggerFor]=\"themeMenu\"\n class=\"flex items-center gap-small px-normal py-small rounded-control border border-border-default bg-transparent cursor-pointer hover:bg-bg-hover transition-colors\"\n aria-label=\"Change theme\"\n>\n <span\n class=\"w-4 h-4 rounded-full border border-border-strong/30 shrink-0\"\n [style.background]=\"\n 'linear-gradient(135deg, ' +\n currentMeta().accent +\n ' 50%, ' +\n currentMeta().pageBg +\n ' 50%)'\n \"\n ></span>\n <span class=\"txt-body-01 hidden md:inline\">{{ currentMeta().label }}</span>\n <fx-ui-hero-icon icon=\"chevron-down\" [size]=\"14\" class=\"flex text-text-secondary\" />\n</button>\n\n<mat-menu #themeMenu=\"matMenu\">\n @for (theme of themes; track theme.name) {\n <button\n mat-menu-item\n type=\"button\"\n (click)=\"select(theme.name)\"\n [attr.aria-current]=\"theme.name === current() ? 'true' : null\"\n >\n <div class=\"flex items-center gap-normal min-w-40\">\n <span\n class=\"w-5 h-5 rounded-full border border-border-strong/30 shrink-0\"\n [style.background]=\"\n 'linear-gradient(135deg, ' + theme.accent + ' 50%, ' + theme.pageBg + ' 50%)'\n \"\n ></span>\n <span class=\"txt-body-01 flex-1\">{{ theme.label }}</span>\n <span class=\"txt-utility-description\">{{ theme.dark ? 'Dark' : 'Light' }}</span>\n @if (theme.name === current()) {\n <fx-ui-hero-icon icon=\"check\" [size]=\"16\" class=\"flex text-text-selected\" />\n }\n </div>\n </button>\n }\n</mat-menu>\n" }]
|
|
4660
|
+
}] });
|
|
4661
|
+
|
|
4662
|
+
/**
|
|
4663
|
+
* "Waiting on the backend" marker (anh Tú 29/08).
|
|
4664
|
+
*
|
|
4665
|
+
* Put this on any UI the V2 design calls for but no endpoint feeds yet, so the
|
|
4666
|
+
* gap is visible in the product instead of living only in a backlog file.
|
|
4667
|
+
* `grep -r "fx-ui-pending-badge"` then lists every outstanding field.
|
|
4668
|
+
*
|
|
4669
|
+
* <fx-ui-pending-badge [field]="'confirmed-chains weekly series'" />
|
|
4670
|
+
*
|
|
4671
|
+
* Keep the copy factual: it states that data is missing, never fakes a value.
|
|
4672
|
+
*/
|
|
4673
|
+
class PendingBadgeComponent {
|
|
4674
|
+
/** Caller-translated chip text. */
|
|
4675
|
+
label = input('Pending API', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4676
|
+
/**
|
|
4677
|
+
* What exactly is missing — shown as the title tooltip so a reviewer can read
|
|
4678
|
+
* the required field without opening the code.
|
|
4679
|
+
*/
|
|
4680
|
+
field = input('', ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
4681
|
+
/** 'chip' (default) sits inline; 'dot' is a bare marker for dense rows. */
|
|
4682
|
+
variant = input('chip', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
4683
|
+
get tooltip() {
|
|
4684
|
+
return this.field() ? `${this.label()} — ${this.field()}` : this.label();
|
|
4685
|
+
}
|
|
4686
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4687
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: PendingBadgeComponent, isStandalone: true, selector: "fx-ui-pending-badge", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-warning/70 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4688
|
+
}
|
|
4689
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PendingBadgeComponent, decorators: [{
|
|
4690
|
+
type: Component,
|
|
4691
|
+
args: [{ selector: 'fx-ui-pending-badge', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "@if (variant() === 'dot') {\n <span\n class=\"inline-block w-[7px] h-[7px] rounded-full bg-warning/70 align-middle\"\n [attr.title]=\"tooltip\"\n [attr.aria-label]=\"tooltip\"\n ></span>\n} @else {\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap align-middle\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ label() }}\n </span>\n}\n" }]
|
|
4692
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
|
|
4693
|
+
|
|
4694
|
+
/**
|
|
4695
|
+
* Wraps UI that is rendered from PLACEHOLDER data so the layout can be
|
|
4696
|
+
* reviewed before its endpoint exists (anh Tú 29/08: "muốn có full giao diện
|
|
4697
|
+
* trước để review").
|
|
4698
|
+
*
|
|
4699
|
+
* The frame makes the status unmistakable — dashed outline, a corner label and
|
|
4700
|
+
* the pending marker naming the missing field — so sample shapes can never be
|
|
4701
|
+
* mistaken for measured values. Drop the wrapper (and the sample input) the
|
|
4702
|
+
* moment the API lands.
|
|
4703
|
+
*
|
|
4704
|
+
* The pending chip is inlined rather than composed from fx-ui-pending-badge:
|
|
4705
|
+
* one lib component importing another inside the same barrel tripped NG0919
|
|
4706
|
+
* (metadata read before definition) in consuming apps.
|
|
4707
|
+
*
|
|
4708
|
+
* <fx-ui-sample-frame [field]="'weekly activity'" [label]="'Sample layout'">
|
|
4709
|
+
* …chart rendered from sample series…
|
|
4710
|
+
* </fx-ui-sample-frame>
|
|
4711
|
+
*/
|
|
4712
|
+
class SampleFrameComponent {
|
|
4713
|
+
/** Caller-translated corner label. */
|
|
4714
|
+
label = input('Sample layout', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
4715
|
+
/** Field the backend still owes — shown on the pending badge tooltip. */
|
|
4716
|
+
field = input('', ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
4717
|
+
/** Caller-translated pending-chip text. */
|
|
4718
|
+
pendingLabel = input('Pending API', ...(ngDevMode ? [{ debugName: "pendingLabel" }] : /* istanbul ignore next */ []));
|
|
4719
|
+
get tooltip() {
|
|
4720
|
+
return this.field() ? `${this.pendingLabel()} — ${this.field()}` : this.pendingLabel();
|
|
4721
|
+
}
|
|
4722
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4723
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.20", type: SampleFrameComponent, isStandalone: true, selector: "fx-ui-sample-frame", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: false, transformFunction: null }, pendingLabel: { classPropertyName: "pendingLabel", publicName: "pendingLabel", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal\">\n <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line\n instead of the label breaking mid-word (\"SAMPLE / LAYOUT\"). -->\n <div class=\"flex flex-wrap items-center gap-small mb-normal\">\n <span class=\"txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap\">{{ label() }}</span>\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90\">\n <ng-content></ng-content>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4724
|
+
}
|
|
4725
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SampleFrameComponent, decorators: [{
|
|
4726
|
+
type: Component,
|
|
4727
|
+
args: [{ selector: 'fx-ui-sample-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div class=\"relative rounded-lg border border-dashed border-warning/50 bg-warning/[0.03] p-normal\">\n <!-- flex-wrap + nowrap label: in narrow rails the chip drops to the next line\n instead of the label breaking mid-word (\"SAMPLE / LAYOUT\"). -->\n <div class=\"flex flex-wrap items-center gap-small mb-normal\">\n <span class=\"txt-tag-label text-warning uppercase tracking-wide whitespace-nowrap\">{{ label() }}</span>\n <span\n class=\"inline-flex items-center gap-xs px-small py-[1px] rounded-pill border border-dashed border-warning/60 bg-warning/10 text-warning txt-tag-label whitespace-nowrap\"\n [attr.title]=\"tooltip\"\n >\n <fx-ui-hero-icon icon=\"clock\" [size]=\"11\" class=\"flex\" />\n {{ pendingLabel() }}\n </span>\n </div>\n <!-- Sample content is non-interactive: nothing here reflects real state. -->\n <div class=\"pointer-events-none select-none opacity-90\">\n <ng-content></ng-content>\n </div>\n</div>\n" }]
|
|
4728
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], field: [{ type: i0.Input, args: [{ isSignal: true, alias: "field", required: false }] }], pendingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pendingLabel", required: false }] }] } });
|
|
4729
|
+
|
|
4730
|
+
/**
|
|
4731
|
+
* V2 score badge — the risk/score chip used on attack-path rows, dashboard
|
|
4732
|
+
* panels and project tiles. Colour comes from the 0–100 risk band unless an
|
|
4733
|
+
* explicit tone is given (bands: ≥90 critical, ≥70 danger, ≥40 warning, else
|
|
4734
|
+
* success — same thresholds as value-tone's scoreTone on a 0–100 scale).
|
|
4735
|
+
*/
|
|
4736
|
+
class ScoreBadgeComponent {
|
|
4737
|
+
score = input.required(...(ngDevMode ? [{ debugName: "score" }] : /* istanbul ignore next */ []));
|
|
4738
|
+
/** Denominator shown as "98 / 100"; omit for a bare score. */
|
|
4739
|
+
outOf = input(null, ...(ngDevMode ? [{ debugName: "outOf" }] : /* istanbul ignore next */ []));
|
|
4740
|
+
/** Overrides the band-derived tone. */
|
|
4741
|
+
tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
|
|
4742
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
4743
|
+
resolvedTone = computed(() => {
|
|
4744
|
+
const explicit = this.tone();
|
|
4745
|
+
if (explicit)
|
|
4746
|
+
return explicit;
|
|
4747
|
+
const value = Number(this.score());
|
|
4748
|
+
if (!isFinite(value))
|
|
4749
|
+
return null;
|
|
4750
|
+
if (value >= 90)
|
|
4751
|
+
return 'critical';
|
|
4752
|
+
if (value >= 70)
|
|
4753
|
+
return 'danger';
|
|
4754
|
+
if (value >= 40)
|
|
4755
|
+
return 'warning';
|
|
4756
|
+
return 'success';
|
|
4757
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedTone" }] : /* istanbul ignore next */ []));
|
|
4758
|
+
badgeClass() {
|
|
4759
|
+
return toneBadge(this.resolvedTone());
|
|
4760
|
+
}
|
|
4761
|
+
sizeClass() {
|
|
4762
|
+
switch (this.size()) {
|
|
4763
|
+
case 'sm':
|
|
4764
|
+
return 'text-xs px-normal py-[1px]';
|
|
4765
|
+
case 'lg':
|
|
4766
|
+
return 'text-2xl px-semi py-small font-semibold';
|
|
4767
|
+
default:
|
|
4768
|
+
return 'text-sm px-semi py-xs font-semibold';
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScoreBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4772
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ScoreBadgeComponent, isStandalone: true, selector: "fx-ui-score-badge", inputs: { score: { classPropertyName: "score", publicName: "score", isSignal: true, isRequired: true, transformFunction: null }, outOf: { classPropertyName: "outOf", publicName: "outOf", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<span\n class=\"inline-flex items-center justify-center rounded-control whitespace-nowrap\"\n [ngClass]=\"[badgeClass(), sizeClass()]\"\n style=\"font-family: var(--og-font-figure)\"\n>\n {{ score() }}@if (outOf() !== null) {<span class=\"opacity-60\"> / {{ outOf() }}</span>}\n</span>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4773
|
+
}
|
|
4774
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ScoreBadgeComponent, decorators: [{
|
|
4775
|
+
type: Component,
|
|
4776
|
+
args: [{ selector: 'fx-ui-score-badge', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<span\n class=\"inline-flex items-center justify-center rounded-control whitespace-nowrap\"\n [ngClass]=\"[badgeClass(), sizeClass()]\"\n style=\"font-family: var(--og-font-figure)\"\n>\n {{ score() }}@if (outOf() !== null) {<span class=\"opacity-60\"> / {{ outOf() }}</span>}\n</span>\n" }]
|
|
4777
|
+
}], propDecorators: { score: [{ type: i0.Input, args: [{ isSignal: true, alias: "score", required: true }] }], outOf: [{ type: i0.Input, args: [{ isSignal: true, alias: "outOf", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
|
|
4778
|
+
|
|
4779
|
+
/**
|
|
4780
|
+
* V2 StatusDot — tiny tone-coloured indicator used in list rows, project
|
|
4781
|
+
* cards and stat grids. Optional pulse for "live" states (running scans).
|
|
4782
|
+
*
|
|
4783
|
+
* <fx-ui-status-dot tone="success" />
|
|
4784
|
+
* <fx-ui-status-dot tone="warning" [pulse]="true" />
|
|
4785
|
+
*/
|
|
4786
|
+
class StatusDotComponent {
|
|
4787
|
+
tone = input(null, ...(ngDevMode ? [{ debugName: "tone" }] : /* istanbul ignore next */ []));
|
|
4788
|
+
/** Diameter in px. */
|
|
4789
|
+
size = input(8, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
4790
|
+
pulse = input(false, ...(ngDevMode ? [{ debugName: "pulse" }] : /* istanbul ignore next */ []));
|
|
4791
|
+
dotClass() {
|
|
4792
|
+
return toneBg(this.tone());
|
|
4793
|
+
}
|
|
4794
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatusDotComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4795
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: StatusDotComponent, isStandalone: true, selector: "fx-ui-status-dot", inputs: { tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, pulse: { classPropertyName: "pulse", publicName: "pulse", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<span\n class=\"relative inline-flex shrink-0\"\n [style.width.px]=\"size()\"\n [style.height.px]=\"size()\"\n>\n @if (pulse()) {\n <span\n class=\"absolute inline-flex h-full w-full rounded-full opacity-60 animate-ping motion-reduce:animate-none\"\n [ngClass]=\"dotClass()\"\n ></span>\n }\n <span class=\"relative inline-flex h-full w-full rounded-full\" [ngClass]=\"dotClass()\"></span>\n</span>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4796
|
+
}
|
|
4797
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: StatusDotComponent, decorators: [{
|
|
4798
|
+
type: Component,
|
|
4799
|
+
args: [{ selector: 'fx-ui-status-dot', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<span\n class=\"relative inline-flex shrink-0\"\n [style.width.px]=\"size()\"\n [style.height.px]=\"size()\"\n>\n @if (pulse()) {\n <span\n class=\"absolute inline-flex h-full w-full rounded-full opacity-60 animate-ping motion-reduce:animate-none\"\n [ngClass]=\"dotClass()\"\n ></span>\n }\n <span class=\"relative inline-flex h-full w-full rounded-full\" [ngClass]=\"dotClass()\"></span>\n</span>\n" }]
|
|
4800
|
+
}], propDecorators: { tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], pulse: [{ type: i0.Input, args: [{ isSignal: true, alias: "pulse", required: false }] }] } });
|
|
4801
|
+
|
|
4802
|
+
/**
|
|
4803
|
+
* V2 segmented control — mode toggles (Essentials/Advanced), Catalog/Table
|
|
4804
|
+
* view switches, sub-navigation inside a tab. Generic n-option control; the
|
|
4805
|
+
* semantics live with the caller.
|
|
4806
|
+
*
|
|
4807
|
+
* Rendered as a compact pill group (anh Tú 29/08): equal-width options inside
|
|
4808
|
+
* a rounded-full track, with a single "thumb" that SLIDES to the active
|
|
4809
|
+
* option (transform transition — no per-button background repaint).
|
|
4810
|
+
*
|
|
4811
|
+
* <fx-ui-segmented [options]="modes" [(value)]="mode" />
|
|
4812
|
+
*/
|
|
4813
|
+
class SegmentedComponent {
|
|
4814
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
4815
|
+
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
|
|
4816
|
+
/** Accessible name for the group. */
|
|
4817
|
+
ariaLabel = input('View mode', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
4818
|
+
/**
|
|
4819
|
+
* Both variants share the pill shape; they differ in the thumb fill:
|
|
4820
|
+
* 'default': raised surface thumb — view switches (Catalog/Table, ref 10).
|
|
4821
|
+
* 'pill': accent-filled thumb — the Essential/Advanced depth toggle
|
|
4822
|
+
* (design refs 04/05).
|
|
4823
|
+
*/
|
|
4824
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
4825
|
+
/** Index the sliding thumb sits on; -1 hides it (no/unknown selection). */
|
|
4826
|
+
activeIndex = computed(() => this.options().findIndex((o) => o.value === this.value()), ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
4827
|
+
/** Equal-width tracks → the thumb is 1/n of the inner width and slides by
|
|
4828
|
+
* multiples of its own width. Pure CSS transform, nothing to measure. */
|
|
4829
|
+
thumbWidth = computed(() => `calc((100% - 6px) / ${this.options().length || 1})`, ...(ngDevMode ? [{ debugName: "thumbWidth" }] : /* istanbul ignore next */ []));
|
|
4830
|
+
thumbTransform = computed(() => `translateX(${this.activeIndex() * 100}%)`, ...(ngDevMode ? [{ debugName: "thumbTransform" }] : /* istanbul ignore next */ []));
|
|
4831
|
+
select(option) {
|
|
4832
|
+
if (option.disabled || option.value === this.value())
|
|
4833
|
+
return;
|
|
4834
|
+
this.value.set(option.value);
|
|
4835
|
+
}
|
|
4836
|
+
thumbClass() {
|
|
4837
|
+
return this.variant() === 'pill'
|
|
4838
|
+
? 'bg-accent'
|
|
4839
|
+
: 'bg-surface-raised border border-border shadow-sm';
|
|
4840
|
+
}
|
|
4841
|
+
optionClass(option) {
|
|
4842
|
+
if (option.value !== this.value()) {
|
|
4843
|
+
return 'text-muted hover:text-strong';
|
|
4844
|
+
}
|
|
4845
|
+
return this.variant() === 'pill' ? 'text-accent-contrast' : 'text-strong';
|
|
4846
|
+
}
|
|
4847
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4848
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SegmentedComponent, isStandalone: true, selector: "fx-ui-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", 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 } }, outputs: { value: "valueChange" }, ngImport: i0, template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"], dependencies: [{ kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4849
|
+
}
|
|
4850
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SegmentedComponent, decorators: [{
|
|
4851
|
+
type: Component,
|
|
4852
|
+
args: [{ selector: 'fx-ui-segmented', changeDetection: ChangeDetectionStrategy.OnPush, imports: [HeroIconComponent], template: "<div\n class=\"relative inline-grid max-w-full grid-flow-col auto-cols-fr items-stretch p-[3px] bg-surface-sunken border border-border rounded-full\"\n role=\"group\"\n [attr.aria-label]=\"ariaLabel()\"\n>\n <!-- Sliding thumb: one element that translates to the active option -->\n @if (activeIndex() >= 0) {\n <span\n aria-hidden=\"true\"\n class=\"fx-segmented__thumb absolute top-[3px] bottom-[3px] left-[3px] rounded-full pointer-events-none\"\n [class]=\"thumbClass()\"\n [style.width]=\"thumbWidth()\"\n [style.transform]=\"thumbTransform()\"\n ></span>\n }\n @for (option of options(); track option.value) {\n <button\n type=\"button\"\n (click)=\"select(option)\"\n [disabled]=\"option.disabled\"\n [attr.aria-pressed]=\"option.value === value()\"\n class=\"relative z-[1] flex min-w-0 items-center justify-center gap-xs px-semi py-[5px] rounded-full text-[13px] leading-none font-medium whitespace-nowrap transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed\"\n [class]=\"optionClass(option)\"\n >\n @if (option.icon) {\n <fx-ui-hero-icon [icon]=\"option.icon\" [size]=\"14\" class=\"flex\" />\n }\n <!-- text-box trims Archivo's uneven leading \u2192 true optical centering\n (Chrome 128+/Safari 18.2+; older browsers ignore it harmlessly).\n truncate adds overflow:hidden, and the trimmed box ends at cap\n height / baseline \u2014 so the dot on \"i\" and the tail on \"y\" fall\n outside it and get clipped. py/-my pair gives both back without\n changing the control's height. -->\n <span class=\"min-w-0 truncate py-[3px] -my-[3px] [text-box:trim-both_cap_alphabetic]\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label px-small rounded-pill bg-neutral-soft text-neutral-soft-fg\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";.fx-segmented__thumb{transition:transform .2s cubic-bezier(.2,.9,.2,1),width .2s cubic-bezier(.2,.9,.2,1);will-change:transform}@media(prefers-reduced-motion:reduce){.fx-segmented__thumb{transition:none}}\n"] }]
|
|
4853
|
+
}], 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"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }] } });
|
|
4854
|
+
|
|
4855
|
+
/**
|
|
4856
|
+
* V2 ListRow — presentational body for master-detail #row templates and other
|
|
4857
|
+
* selectable lists: status dot + title, trailing badges, meta line. Extra
|
|
4858
|
+
* content projects below the meta line.
|
|
4859
|
+
*
|
|
4860
|
+
* <ng-template #row let-item>
|
|
4861
|
+
* <fx-ui-list-row
|
|
4862
|
+
* [title]="item.name"
|
|
4863
|
+
* [meta]="item.version + ' · ' + item.date"
|
|
4864
|
+
* [dotTone]="statusTone(item)"
|
|
4865
|
+
* [badges]="[{ label: item.severity, tone: severityTone(item.severity) }]"
|
|
4866
|
+
* />
|
|
4867
|
+
* </ng-template>
|
|
4868
|
+
*/
|
|
4869
|
+
class ListRowComponent {
|
|
4870
|
+
title = input.required(...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
|
|
4871
|
+
meta = input('', ...(ngDevMode ? [{ debugName: "meta" }] : /* istanbul ignore next */ []));
|
|
4872
|
+
badges = input([], ...(ngDevMode ? [{ debugName: "badges" }] : /* istanbul ignore next */ []));
|
|
4873
|
+
dotTone = input(undefined, ...(ngDevMode ? [{ debugName: "dotTone" }] : /* istanbul ignore next */ []));
|
|
4874
|
+
dotPulse = input(false, ...(ngDevMode ? [{ debugName: "dotPulse" }] : /* istanbul ignore next */ []));
|
|
4875
|
+
badgeClass(badge) {
|
|
4876
|
+
return toneBadge(badge.tone ?? null);
|
|
4877
|
+
}
|
|
4878
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ListRowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4879
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ListRowComponent, isStandalone: true, selector: "fx-ui-list-row", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, badges: { classPropertyName: "badges", publicName: "badges", isSignal: true, isRequired: false, transformFunction: null }, dotTone: { classPropertyName: "dotTone", publicName: "dotTone", isSignal: true, isRequired: false, transformFunction: null }, dotPulse: { classPropertyName: "dotPulse", publicName: "dotPulse", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex items-center gap-normal min-w-0\">\n @if (dotTone() !== undefined) {\n <fx-ui-status-dot [tone]=\"dotTone() ?? null\" [pulse]=\"dotPulse()\" />\n }\n <span class=\"txt-body-medium-01 truncate flex-1\">{{ title() }}</span>\n @for (badge of badges(); track badge.label) {\n <span\n class=\"txt-tag-label px-normal py-[1px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"badgeClass(badge)\"\n >\n {{ badge.label }}\n </span>\n }\n</div>\n@if (meta()) {\n <div class=\"txt-utility-description mt-xs truncate\" [class.ml-large]=\"dotTone() !== undefined\">\n {{ meta() }}\n </div>\n}\n<ng-content></ng-content>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: StatusDotComponent, selector: "fx-ui-status-dot", inputs: ["tone", "size", "pulse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4880
|
+
}
|
|
4881
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ListRowComponent, decorators: [{
|
|
4882
|
+
type: Component,
|
|
4883
|
+
args: [{ selector: 'fx-ui-list-row', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, StatusDotComponent], template: "<div class=\"flex items-center gap-normal min-w-0\">\n @if (dotTone() !== undefined) {\n <fx-ui-status-dot [tone]=\"dotTone() ?? null\" [pulse]=\"dotPulse()\" />\n }\n <span class=\"txt-body-medium-01 truncate flex-1\">{{ title() }}</span>\n @for (badge of badges(); track badge.label) {\n <span\n class=\"txt-tag-label px-normal py-[1px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"badgeClass(badge)\"\n >\n {{ badge.label }}\n </span>\n }\n</div>\n@if (meta()) {\n <div class=\"txt-utility-description mt-xs truncate\" [class.ml-large]=\"dotTone() !== undefined\">\n {{ meta() }}\n </div>\n}\n<ng-content></ng-content>\n" }]
|
|
4884
|
+
}], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], meta: [{ type: i0.Input, args: [{ isSignal: true, alias: "meta", required: false }] }], badges: [{ type: i0.Input, args: [{ isSignal: true, alias: "badges", required: false }] }], dotTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotTone", required: false }] }], dotPulse: [{ type: i0.Input, args: [{ isSignal: true, alias: "dotPulse", required: false }] }] } });
|
|
4885
|
+
|
|
4886
|
+
/**
|
|
4887
|
+
* V2 BarList — ranked horizontal bars ("Top projects by risk", "Top critical
|
|
4888
|
+
* attack paths"). Pure DOM (accessible, token-native, clickable) — charts via
|
|
4889
|
+
* ECharts are for time series, not rankings.
|
|
4890
|
+
*
|
|
4891
|
+
* <fx-ui-bar-list [items]="topRisk" (select)="open($event)" />
|
|
4892
|
+
*/
|
|
4893
|
+
class BarListComponent {
|
|
4894
|
+
items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
4895
|
+
/** Rows render as buttons only when true (a (select) listener exists). */
|
|
4896
|
+
interactive = input(false, ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
|
|
4897
|
+
select = output();
|
|
4898
|
+
max = computed(() => Math.max(...this.items().map((i) => i.value), 1), ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
|
|
4899
|
+
widthOf(item) {
|
|
4900
|
+
return Math.max((item.value / this.max()) * 100, 2);
|
|
4901
|
+
}
|
|
4902
|
+
barClass(item) {
|
|
4903
|
+
return item.tone ? toneBg(item.tone) : 'bg-accent';
|
|
4904
|
+
}
|
|
4905
|
+
onSelect(item) {
|
|
4906
|
+
if (this.interactive())
|
|
4907
|
+
this.select.emit(item);
|
|
4908
|
+
}
|
|
4909
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: BarListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4910
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: BarListComponent, isStandalone: true, selector: "fx-ui-bar-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, ngImport: i0, template: "<div class=\"flex flex-col gap-small\">\n @for (item of items(); track item.label) {\n <button\n type=\"button\"\n class=\"text-left rounded-control px-normal py-small -mx-normal transition-colors\"\n [class.hover:bg-surface-sunken]=\"interactive()\"\n [class.cursor-default]=\"!interactive()\"\n [tabindex]=\"interactive() ? 0 : -1\"\n (click)=\"onSelect(item)\"\n >\n <div class=\"flex items-baseline justify-between gap-normal\">\n <span class=\"txt-body-medium-01 truncate\">{{ item.label }}</span>\n <span class=\"text-sm font-semibold text-strong shrink-0\" style=\"font-family: var(--og-font-figure)\">\n {{ item.valueLabel ?? item.value }}\n </span>\n </div>\n @if (item.meta) {\n <div class=\"txt-utility-description truncate\">{{ item.meta }}</div>\n }\n <div class=\"mt-xs h-[var(--og-track-h)] rounded-pill bg-track overflow-hidden\">\n <div\n class=\"h-full rounded-pill transition-all duration-500\"\n [ngClass]=\"barClass(item)\"\n [style.width.%]=\"widthOf(item)\"\n ></div>\n </div>\n </button>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4911
|
+
}
|
|
4912
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: BarListComponent, decorators: [{
|
|
4913
|
+
type: Component,
|
|
4914
|
+
args: [{ selector: 'fx-ui-bar-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass], template: "<div class=\"flex flex-col gap-small\">\n @for (item of items(); track item.label) {\n <button\n type=\"button\"\n class=\"text-left rounded-control px-normal py-small -mx-normal transition-colors\"\n [class.hover:bg-surface-sunken]=\"interactive()\"\n [class.cursor-default]=\"!interactive()\"\n [tabindex]=\"interactive() ? 0 : -1\"\n (click)=\"onSelect(item)\"\n >\n <div class=\"flex items-baseline justify-between gap-normal\">\n <span class=\"txt-body-medium-01 truncate\">{{ item.label }}</span>\n <span class=\"text-sm font-semibold text-strong shrink-0\" style=\"font-family: var(--og-font-figure)\">\n {{ item.valueLabel ?? item.value }}\n </span>\n </div>\n @if (item.meta) {\n <div class=\"txt-utility-description truncate\">{{ item.meta }}</div>\n }\n <div class=\"mt-xs h-[var(--og-track-h)] rounded-pill bg-track overflow-hidden\">\n <div\n class=\"h-full rounded-pill transition-all duration-500\"\n [ngClass]=\"barClass(item)\"\n [style.width.%]=\"widthOf(item)\"\n ></div>\n </div>\n </button>\n }\n</div>\n" }]
|
|
4915
|
+
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
|
|
4916
|
+
|
|
4917
|
+
/**
|
|
4918
|
+
* V2 facets rail — the first column of 3-column lookup screens: filter groups
|
|
4919
|
+
* with counts + a Reset link. Presentational: filtering itself stays in the
|
|
4920
|
+
* page (feed the emitted selection into BaseMasterDetail filters).
|
|
4921
|
+
*
|
|
4922
|
+
* <div mdFacets>
|
|
4923
|
+
* <fx-ui-facets-rail [groups]="facetGroups" [selection]="facets"
|
|
4924
|
+
* (selectionChange)="onFacets($event)" />
|
|
4925
|
+
* </div>
|
|
4926
|
+
*/
|
|
4927
|
+
class FacetsRailComponent {
|
|
4928
|
+
groups = input([], ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
|
|
4929
|
+
selection = input({}, ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
|
|
4930
|
+
/** Caller-translated label for the reset affordance. */
|
|
4931
|
+
resetLabel = input('Reset', ...(ngDevMode ? [{ debugName: "resetLabel" }] : /* istanbul ignore next */ []));
|
|
4932
|
+
selectionChange = output();
|
|
4933
|
+
hasSelection = computed(() => Object.values(this.selection()).some((values) => values?.length), ...(ngDevMode ? [{ debugName: "hasSelection" }] : /* istanbul ignore next */ []));
|
|
4934
|
+
isSelected(group, option) {
|
|
4935
|
+
return (this.selection()[group.id] ?? []).includes(option.value);
|
|
4936
|
+
}
|
|
4937
|
+
toggle(group, option) {
|
|
4938
|
+
const current = this.selection()[group.id] ?? [];
|
|
4939
|
+
let next;
|
|
4940
|
+
if (group.multi) {
|
|
4941
|
+
next = current.includes(option.value)
|
|
4942
|
+
? current.filter((v) => v !== option.value)
|
|
4943
|
+
: [...current, option.value];
|
|
4944
|
+
}
|
|
4945
|
+
else {
|
|
4946
|
+
next = current.includes(option.value) ? [] : [option.value];
|
|
4947
|
+
}
|
|
4948
|
+
this.selectionChange.emit({ ...this.selection(), [group.id]: next });
|
|
4949
|
+
}
|
|
4950
|
+
reset() {
|
|
4951
|
+
this.selectionChange.emit({});
|
|
4952
|
+
}
|
|
4953
|
+
toneClass(option) {
|
|
4954
|
+
return option.tone ? toneText(option.tone) : '';
|
|
4955
|
+
}
|
|
4956
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FacetsRailComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4957
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: FacetsRailComponent, isStandalone: true, selector: "fx-ui-facets-rail", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, resetLabel: { classPropertyName: "resetLabel", publicName: "resetLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange" }, ngImport: i0, template: "<div class=\"flex flex-col gap-large\">\n @if (hasSelection()) {\n <button type=\"button\" class=\"txt-link self-end\" (click)=\"reset()\">\n {{ resetLabel() }}\n </button>\n }\n @for (group of groups(); track group.id) {\n <div role=\"group\" [attr.aria-label]=\"group.label\">\n <div class=\"txt-eyebrow mb-small\">{{ group.label }}</div>\n <div class=\"flex flex-col gap-xs\">\n @for (option of group.options; track option.value) {\n <button\n type=\"button\"\n (click)=\"toggle(group, option)\"\n [attr.aria-pressed]=\"isSelected(group, option)\"\n class=\"flex items-center gap-normal px-normal py-small rounded-control text-left transition-colors\"\n [ngClass]=\"\n isSelected(group, option)\n ? 'bg-accent-soft text-accent-soft-fg font-medium'\n : 'text-body hover:bg-surface-sunken'\n \"\n >\n @if (option.tone) {\n <fx-ui-status-dot [tone]=\"option.tone\" [size]=\"7\" />\n }\n <span class=\"text-sm truncate flex-1\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label text-faint\" style=\"font-family: var(--og-font-figure)\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n </div>\n </div>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: StatusDotComponent, selector: "fx-ui-status-dot", inputs: ["tone", "size", "pulse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4958
|
+
}
|
|
4959
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: FacetsRailComponent, decorators: [{
|
|
4960
|
+
type: Component,
|
|
4961
|
+
args: [{ selector: 'fx-ui-facets-rail', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, StatusDotComponent], template: "<div class=\"flex flex-col gap-large\">\n @if (hasSelection()) {\n <button type=\"button\" class=\"txt-link self-end\" (click)=\"reset()\">\n {{ resetLabel() }}\n </button>\n }\n @for (group of groups(); track group.id) {\n <div role=\"group\" [attr.aria-label]=\"group.label\">\n <div class=\"txt-eyebrow mb-small\">{{ group.label }}</div>\n <div class=\"flex flex-col gap-xs\">\n @for (option of group.options; track option.value) {\n <button\n type=\"button\"\n (click)=\"toggle(group, option)\"\n [attr.aria-pressed]=\"isSelected(group, option)\"\n class=\"flex items-center gap-normal px-normal py-small rounded-control text-left transition-colors\"\n [ngClass]=\"\n isSelected(group, option)\n ? 'bg-accent-soft text-accent-soft-fg font-medium'\n : 'text-body hover:bg-surface-sunken'\n \"\n >\n @if (option.tone) {\n <fx-ui-status-dot [tone]=\"option.tone\" [size]=\"7\" />\n }\n <span class=\"text-sm truncate flex-1\">{{ option.label }}</span>\n @if (option.count !== undefined) {\n <span class=\"txt-tag-label text-faint\" style=\"font-family: var(--og-font-figure)\">\n {{ option.count }}\n </span>\n }\n </button>\n }\n </div>\n </div>\n }\n</div>\n" }]
|
|
4962
|
+
}], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], resetLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "resetLabel", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
|
|
4963
|
+
|
|
4964
|
+
/** Trend blocks are softened solids — whole literals (Tailwind v4 has no safelist). */
|
|
4965
|
+
const TREND_BLOCK = {
|
|
4966
|
+
critical: 'tone-dot-chart-critical',
|
|
4967
|
+
danger: 'tone-dot-chart-danger',
|
|
4968
|
+
warning: 'tone-dot-chart-warning',
|
|
4969
|
+
success: 'tone-dot-chart-success',
|
|
4970
|
+
information: 'tone-dot-chart-information',
|
|
4971
|
+
};
|
|
4972
|
+
/**
|
|
4973
|
+
* V2 project card — the Project Management catalog tile (design 12): initials
|
|
4974
|
+
* avatar + name + schedule line, toned score chip, risk-trend blocks, metric
|
|
4975
|
+
* columns and a "Last analysis · … / Open ›" footer.
|
|
4976
|
+
*
|
|
4977
|
+
* <fx-ui-project-card [card]="project" (open)="goTo(project)" />
|
|
4978
|
+
*/
|
|
4979
|
+
class ProjectCardComponent {
|
|
4980
|
+
card = input.required(...(ngDevMode ? [{ debugName: "card" }] : /* istanbul ignore next */ []));
|
|
4981
|
+
open = output();
|
|
4982
|
+
scoreChipClass() {
|
|
4983
|
+
return toneBadge(this.card().scoreTone ?? null);
|
|
4984
|
+
}
|
|
4985
|
+
chipClass(chip) {
|
|
4986
|
+
return toneBadge(chip.tone ?? null);
|
|
4987
|
+
}
|
|
4988
|
+
blockClass(tone) {
|
|
4989
|
+
return tone ? TREND_BLOCK[tone] : 'bg-bg-hover';
|
|
4990
|
+
}
|
|
4991
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4992
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: ProjectCardComponent, isStandalone: true, selector: "fx-ui-project-card", inputs: { card: { classPropertyName: "card", publicName: "card", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { open: "open" }, ngImport: i0, template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <div class=\"min-w-0\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <div\n class=\"flex items-center justify-between w-full pt-normal border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: SparkbarComponent, selector: "fx-ui-sparkbar", inputs: ["values", "tone", "height"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4993
|
+
}
|
|
4994
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: ProjectCardComponent, decorators: [{
|
|
4995
|
+
type: Component,
|
|
4996
|
+
args: [{ selector: 'fx-ui-project-card', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, SparkbarComponent, HeroIconComponent], template: "<button\n type=\"button\"\n class=\"card-common bg-surface w-full flex flex-col gap-normal hover:border-accent hover:shadow-md transition-all\"\n (click)=\"open.emit(card())\"\n>\n <!-- Header: avatar + name/sub + score chip (design 12) -->\n <div class=\"flex items-start justify-between gap-normal w-full\">\n <div class=\"flex items-start gap-normal min-w-0\">\n @if (card().avatar) {\n <span\n class=\"w-9 h-9 rounded-control bg-accent-soft text-accent flex items-center justify-center shrink-0 txt-utility-caption font-bold uppercase\"\n style=\"font-family: var(--og-font-figure)\"\n >{{ card().avatar }}</span\n >\n }\n <div class=\"min-w-0\">\n <div class=\"txt-heading-06 truncate text-left\">{{ card().name }}</div>\n @if (card().sub; as sub) {\n <div class=\"flex items-center gap-xs txt-utility-description mt-xs\">\n @if (sub.icon) {\n <fx-ui-hero-icon [icon]=\"sub.icon\" [size]=\"13\" class=\"flex shrink-0\" />\n }\n <span class=\"truncate\">{{ sub.label }}</span>\n </div>\n } @else if (card().description) {\n <div class=\"txt-card-content truncate mt-xs text-left\">{{ card().description }}</div>\n }\n </div>\n </div>\n @if (card().score !== undefined) {\n <div class=\"text-right shrink-0\">\n <span\n class=\"txt-tag-label px-normal py-[2px] rounded-pill whitespace-nowrap\"\n [ngClass]=\"scoreChipClass()\"\n >{{ card().score }}</span\n >\n @if (card().scoreLabel) {\n <div class=\"txt-utility-description mt-xs\">{{ card().scoreLabel }}</div>\n }\n </div>\n }\n </div>\n\n @if (card().chips?.length) {\n <div class=\"flex flex-wrap gap-xs w-full\">\n @for (chip of card().chips; track chip.label) {\n <span class=\"txt-tag-label px-normal py-[1px] rounded-pill\" [ngClass]=\"chipClass(chip)\">\n {{ chip.label }}\n </span>\n }\n </div>\n }\n\n <!-- Design-12 risk trend: toned blocks + caption line -->\n @if (card().trend; as trend) {\n <div class=\"w-full\">\n <div class=\"flex gap-xs w-full\">\n @for (tone of trend.tones; track $index) {\n <span class=\"flex-1 h-4 rounded-[2px]\" [ngClass]=\"blockClass(tone)\"></span>\n }\n </div>\n @if (trend.caption || trend.aside) {\n <div class=\"flex items-center justify-between mt-xs txt-utility-description font-mono\">\n <span>{{ trend.caption }}</span>\n <span>{{ trend.aside }}</span>\n </div>\n }\n </div>\n }\n\n @if (card().spark?.length) {\n <fx-ui-sparkbar class=\"w-full\" [values]=\"card().spark!\" [height]=\"24\" />\n }\n\n <!-- Metric columns: figure + caption (design 12) -->\n @if (card().metrics?.length) {\n <div\n class=\"grid gap-normal w-full\"\n [style.grid-template-columns]=\"'repeat(' + card().metrics!.length + ', minmax(0, 1fr))'\"\n >\n @for (m of card().metrics; track m.label) {\n <div class=\"min-w-0 text-left\">\n <div class=\"txt-heading-06\" style=\"font-family: var(--og-font-figure)\">{{ m.value }}</div>\n <div class=\"txt-utility-description truncate\">{{ m.label }}</div>\n </div>\n }\n </div>\n }\n\n @if (card().meta || card().openLabel) {\n <div\n class=\"flex items-center justify-between w-full pt-normal border-t border-border-default\"\n >\n <span class=\"txt-utility-description text-left\">{{ card().meta }}</span>\n @if (card().openLabel) {\n <span class=\"txt-utility-caption font-bold text-accent whitespace-nowrap\"\n >{{ card().openLabel }} \u203A</span\n >\n }\n </div>\n }\n</button>\n" }]
|
|
4997
|
+
}], propDecorators: { card: [{ type: i0.Input, args: [{ isSignal: true, alias: "card", required: true }] }], open: [{ type: i0.Output, args: ["open"] }] } });
|
|
4998
|
+
|
|
4999
|
+
/**
|
|
5000
|
+
* V2 pipeline stepper — the New Analysis 7-stage pipeline header. Renders the
|
|
5001
|
+
* stage strip only; the page owns the stage bodies (project the current one
|
|
5002
|
+
* below). Stage states are data — the parent decides what is done/active.
|
|
5003
|
+
*
|
|
5004
|
+
* <fx-ui-pipeline-stepper [stages]="stages" (stageClick)="goTo($event)">
|
|
5005
|
+
* …current stage body…
|
|
5006
|
+
* </fx-ui-pipeline-stepper>
|
|
5007
|
+
*/
|
|
5008
|
+
class PipelineStepperComponent {
|
|
5009
|
+
stages = input([], ...(ngDevMode ? [{ debugName: "stages" }] : /* istanbul ignore next */ []));
|
|
5010
|
+
/** Only done stages are click-navigable (review a completed step). */
|
|
5011
|
+
clickable = input(true, ...(ngDevMode ? [{ debugName: "clickable" }] : /* istanbul ignore next */ []));
|
|
5012
|
+
stageClick = output();
|
|
5013
|
+
stateOf(stage) {
|
|
5014
|
+
return stage.state ?? 'pending';
|
|
5015
|
+
}
|
|
5016
|
+
circleClass(stage) {
|
|
5017
|
+
switch (this.stateOf(stage)) {
|
|
5018
|
+
case 'done':
|
|
5019
|
+
return 'bg-accent text-accent-contrast border-accent';
|
|
5020
|
+
case 'active':
|
|
5021
|
+
return 'bg-accent-soft text-accent-soft-fg border-accent';
|
|
5022
|
+
case 'error':
|
|
5023
|
+
return 'tone-scale-danger tone-border-danger';
|
|
5024
|
+
default:
|
|
5025
|
+
return 'bg-surface text-faint border-border';
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
labelClass(stage) {
|
|
5029
|
+
switch (this.stateOf(stage)) {
|
|
5030
|
+
case 'active':
|
|
5031
|
+
return 'text-strong font-semibold';
|
|
5032
|
+
case 'done':
|
|
5033
|
+
return 'text-body';
|
|
5034
|
+
case 'error':
|
|
5035
|
+
return 'tone-fg-danger';
|
|
5036
|
+
default:
|
|
5037
|
+
return 'text-faint';
|
|
5038
|
+
}
|
|
5039
|
+
}
|
|
5040
|
+
connectorClass(stage) {
|
|
5041
|
+
return this.stateOf(stage) === 'done' ? 'bg-accent' : 'bg-border';
|
|
5042
|
+
}
|
|
5043
|
+
canClick(stage) {
|
|
5044
|
+
return this.clickable() && this.stateOf(stage) === 'done';
|
|
5045
|
+
}
|
|
5046
|
+
onStageClick(stage, index) {
|
|
5047
|
+
if (this.canClick(stage))
|
|
5048
|
+
this.stageClick.emit(index);
|
|
5049
|
+
}
|
|
5050
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PipelineStepperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5051
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: PipelineStepperComponent, isStandalone: true, selector: "fx-ui-pipeline-stepper", inputs: { stages: { classPropertyName: "stages", publicName: "stages", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { stageClick: "stageClick" }, ngImport: i0, template: "<ol class=\"flex items-start overflow-x-auto pb-small\" role=\"list\">\n @for (stage of stages(); track stage.id; let i = $index; let last = $last) {\n <li class=\"flex items-start\" [class.flex-1]=\"!last\" [attr.aria-current]=\"stateOf(stage) === 'active' ? 'step' : null\">\n <button\n type=\"button\"\n class=\"flex flex-col items-center gap-xs min-w-20 px-xs\"\n [class.cursor-pointer]=\"canClick(stage)\"\n [class.cursor-default]=\"!canClick(stage)\"\n [tabindex]=\"canClick(stage) ? 0 : -1\"\n (click)=\"onStageClick(stage, i)\"\n >\n <span\n class=\"w-8 h-8 rounded-full border-2 flex items-center justify-center text-sm font-semibold transition-colors\"\n [ngClass]=\"circleClass(stage)\"\n >\n @switch (stateOf(stage)) {\n @case ('done') {\n <fx-ui-hero-icon icon=\"check\" [size]=\"16\" class=\"flex\" />\n }\n @case ('error') {\n <fx-ui-hero-icon icon=\"exclamation-triangle\" [size]=\"16\" class=\"flex\" />\n }\n @default {\n {{ i + 1 }}\n }\n }\n </span>\n <span class=\"text-xs text-center whitespace-nowrap\" [ngClass]=\"labelClass(stage)\">\n {{ stage.label }}\n </span>\n @if (stage.description) {\n <span class=\"txt-utility-description text-center whitespace-nowrap\">\n {{ stage.description }}\n </span>\n }\n </button>\n @if (!last) {\n <div class=\"flex-1 h-[2px] mt-4 mx-xs rounded-pill min-w-6\" [ngClass]=\"connectorClass(stage)\"></div>\n }\n </li>\n }\n</ol>\n<div class=\"mt-large\">\n <ng-content></ng-content>\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: HeroIconComponent, selector: "fx-ui-hero-icon", inputs: ["stroke", "icon", "solid", "outline", "size", "color", "class"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5052
|
+
}
|
|
5053
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: PipelineStepperComponent, decorators: [{
|
|
5054
|
+
type: Component,
|
|
5055
|
+
args: [{ selector: 'fx-ui-pipeline-stepper', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgClass, HeroIconComponent], template: "<ol class=\"flex items-start overflow-x-auto pb-small\" role=\"list\">\n @for (stage of stages(); track stage.id; let i = $index; let last = $last) {\n <li class=\"flex items-start\" [class.flex-1]=\"!last\" [attr.aria-current]=\"stateOf(stage) === 'active' ? 'step' : null\">\n <button\n type=\"button\"\n class=\"flex flex-col items-center gap-xs min-w-20 px-xs\"\n [class.cursor-pointer]=\"canClick(stage)\"\n [class.cursor-default]=\"!canClick(stage)\"\n [tabindex]=\"canClick(stage) ? 0 : -1\"\n (click)=\"onStageClick(stage, i)\"\n >\n <span\n class=\"w-8 h-8 rounded-full border-2 flex items-center justify-center text-sm font-semibold transition-colors\"\n [ngClass]=\"circleClass(stage)\"\n >\n @switch (stateOf(stage)) {\n @case ('done') {\n <fx-ui-hero-icon icon=\"check\" [size]=\"16\" class=\"flex\" />\n }\n @case ('error') {\n <fx-ui-hero-icon icon=\"exclamation-triangle\" [size]=\"16\" class=\"flex\" />\n }\n @default {\n {{ i + 1 }}\n }\n }\n </span>\n <span class=\"text-xs text-center whitespace-nowrap\" [ngClass]=\"labelClass(stage)\">\n {{ stage.label }}\n </span>\n @if (stage.description) {\n <span class=\"txt-utility-description text-center whitespace-nowrap\">\n {{ stage.description }}\n </span>\n }\n </button>\n @if (!last) {\n <div class=\"flex-1 h-[2px] mt-4 mx-xs rounded-pill min-w-6\" [ngClass]=\"connectorClass(stage)\"></div>\n }\n </li>\n }\n</ol>\n<div class=\"mt-large\">\n <ng-content></ng-content>\n</div>\n" }]
|
|
5056
|
+
}], propDecorators: { stages: [{ type: i0.Input, args: [{ isSignal: true, alias: "stages", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], stageClick: [{ type: i0.Output, args: ["stageClick"] }] } });
|
|
5057
|
+
|
|
5058
|
+
/** Facets-rail skeleton: N nhóm, mỗi nhóm một eyebrow + vài dòng lựa chọn. */
|
|
5059
|
+
class SkeletonFacetsComponent {
|
|
5060
|
+
groups = input(3, ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
|
|
5061
|
+
rows = input(4, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
|
|
5062
|
+
// computed, không phải method — xem [[skeleton-ng0956-array-holes]].
|
|
5063
|
+
groupList = computed(() => range$3(this.groups()), ...(ngDevMode ? [{ debugName: "groupList" }] : /* istanbul ignore next */ []));
|
|
5064
|
+
rowList = computed(() => range$3(this.rows()), ...(ngDevMode ? [{ debugName: "rowList" }] : /* istanbul ignore next */ []));
|
|
5065
|
+
rowWidth(i) {
|
|
5066
|
+
return [80, 65, 72, 58][i % 4];
|
|
5067
|
+
}
|
|
5068
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFacetsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5069
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonFacetsComponent, isStandalone: true, selector: "fx-ui-skeleton-facets", inputs: { groups: { classPropertyName: "groups", publicName: "groups", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-xl\">\n @for (g of groupList(); track g) {\n <div class=\"flex flex-col gap-normal\">\n <div class=\"h-3 w-20 rounded skeleton-load\"></div>\n <div class=\"flex flex-col gap-small\">\n @for (r of rowList(); track r) {\n <div class=\"flex items-center gap-normal\">\n <div class=\"w-3.5 h-3.5 rounded skeleton-load shrink-0\"></div>\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"rowWidth(r)\"></div>\n </div>\n }\n </div>\n </div>\n }\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5070
|
+
}
|
|
5071
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFacetsComponent, decorators: [{
|
|
5072
|
+
type: Component,
|
|
5073
|
+
args: [{ selector: 'fx-ui-skeleton-facets', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-xl\">\n @for (g of groupList(); track g) {\n <div class=\"flex flex-col gap-normal\">\n <div class=\"h-3 w-20 rounded skeleton-load\"></div>\n <div class=\"flex flex-col gap-small\">\n @for (r of rowList(); track r) {\n <div class=\"flex items-center gap-normal\">\n <div class=\"w-3.5 h-3.5 rounded skeleton-load shrink-0\"></div>\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"rowWidth(r)\"></div>\n </div>\n }\n </div>\n </div>\n }\n</div>\n" }]
|
|
5074
|
+
}], propDecorators: { groups: [{ type: i0.Input, args: [{ isSignal: true, alias: "groups", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }] } });
|
|
5075
|
+
function range$3(n) {
|
|
5076
|
+
return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
/** Dashboard skeleton: một hàng KPI + N panel có vùng chart. */
|
|
5080
|
+
class SkeletonDashboardComponent {
|
|
5081
|
+
kpis = input(4, ...(ngDevMode ? [{ debugName: "kpis" }] : /* istanbul ignore next */ []));
|
|
5082
|
+
panels = input(2, ...(ngDevMode ? [{ debugName: "panels" }] : /* istanbul ignore next */ []));
|
|
5083
|
+
kpiList = computed(() => range$2(this.kpis()), ...(ngDevMode ? [{ debugName: "kpiList" }] : /* istanbul ignore next */ []));
|
|
5084
|
+
panelList = computed(() => range$2(this.panels()), ...(ngDevMode ? [{ debugName: "panelList" }] : /* istanbul ignore next */ []));
|
|
5085
|
+
barList = computed(() => range$2(7), ...(ngDevMode ? [{ debugName: "barList" }] : /* istanbul ignore next */ []));
|
|
5086
|
+
/** Chiều cao cột giả — dựng hình chart mà không cần dữ liệu. */
|
|
5087
|
+
barHeight(i) {
|
|
5088
|
+
return [45, 70, 35, 85, 55, 75, 40][i % 7];
|
|
5089
|
+
}
|
|
5090
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDashboardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5091
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonDashboardComponent, isStandalone: true, selector: "fx-ui-skeleton-dashboard", inputs: { kpis: { classPropertyName: "kpis", publicName: "kpis", isSignal: true, isRequired: false, transformFunction: null }, panels: { classPropertyName: "panels", publicName: "panels", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex flex-col gap-xl\">\n <div class=\"grid gap-xl grid-cols-2 md:grid-cols-4\">\n @for (k of kpiList(); track k) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"h-3 w-16 rounded skeleton-load\"></div>\n <div class=\"h-7 w-24 rounded skeleton-load\"></div>\n <div class=\"h-3 w-20 rounded skeleton-load\"></div>\n </div>\n }\n </div>\n\n <div class=\"grid gap-xl grid-cols-1 md:grid-cols-2\">\n @for (p of panelList(); track p) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded skeleton-load\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (b of barList(); track b) {\n <div class=\"flex-1 rounded-t skeleton-load\" [style.height.%]=\"barHeight(b)\"></div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5092
|
+
}
|
|
5093
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonDashboardComponent, decorators: [{
|
|
5094
|
+
type: Component,
|
|
5095
|
+
args: [{ selector: 'fx-ui-skeleton-dashboard', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"flex flex-col gap-xl\">\n <div class=\"grid gap-xl grid-cols-2 md:grid-cols-4\">\n @for (k of kpiList(); track k) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"h-3 w-16 rounded skeleton-load\"></div>\n <div class=\"h-7 w-24 rounded skeleton-load\"></div>\n <div class=\"h-3 w-20 rounded skeleton-load\"></div>\n </div>\n }\n </div>\n\n <div class=\"grid gap-xl grid-cols-1 md:grid-cols-2\">\n @for (p of panelList(); track p) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"h-4 w-1/3 rounded skeleton-load\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (b of barList(); track b) {\n <div class=\"flex-1 rounded-t skeleton-load\" [style.height.%]=\"barHeight(b)\"></div>\n }\n </div>\n </div>\n }\n </div>\n</div>\n" }]
|
|
5096
|
+
}], propDecorators: { kpis: [{ type: i0.Input, args: [{ isSignal: true, alias: "kpis", required: false }] }], panels: [{ type: i0.Input, args: [{ isSignal: true, alias: "panels", required: false }] }] } });
|
|
5097
|
+
function range$2(n) {
|
|
5098
|
+
return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
|
|
5099
|
+
}
|
|
5100
|
+
|
|
5101
|
+
const PRESETS = {
|
|
5102
|
+
table: ['heading', 'toolbar', 'table'],
|
|
5103
|
+
panels: [
|
|
5104
|
+
'heading',
|
|
5105
|
+
'stat-cards',
|
|
5106
|
+
{ kind: 'panels', count: 3, cols: 3 },
|
|
5107
|
+
'toolbar',
|
|
5108
|
+
{ kind: 'cards', count: 6, cols: 3 },
|
|
5109
|
+
],
|
|
5110
|
+
dashboard: [
|
|
5111
|
+
'heading',
|
|
5112
|
+
'kpi-strip',
|
|
5113
|
+
{ kind: 'panels', count: 2 },
|
|
5114
|
+
{ kind: 'panels', count: 2 },
|
|
5115
|
+
'card',
|
|
5116
|
+
],
|
|
5117
|
+
'tabs-facets': ['tabs', 'facets'],
|
|
5118
|
+
facets: ['facets'],
|
|
5119
|
+
detail: ['detail'],
|
|
5120
|
+
form: ['heading', 'form'],
|
|
5121
|
+
};
|
|
5122
|
+
/**
|
|
5123
|
+
* Skeleton cho vùng NỘI DUNG trong lúc resolver của route đang chạy.
|
|
5124
|
+
*
|
|
5125
|
+
* ⚠️ Vì sao skeleton phải nằm ở shell chứ không ở từng màn: Angular **không
|
|
5126
|
+
* tạo component cho tới khi resolver xong**, nên một màn không thể tự vẽ
|
|
5127
|
+
* skeleton của chính nó trong lúc resolver của nó chạy. Thứ vẽ được phải tồn
|
|
5128
|
+
* tại TRƯỚC component đó → shell + router event. Xem `FxRouteSkeletonService`.
|
|
5129
|
+
*/
|
|
5130
|
+
class RouteSkeletonComponent {
|
|
5131
|
+
/** Tên preset, hoặc mảng block mô tả đúng layout của màn. */
|
|
5132
|
+
hint = input('table', ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
|
|
5133
|
+
blocks = computed(() => {
|
|
5134
|
+
const value = this.hint();
|
|
5135
|
+
// Mảng rỗng / tên preset lạ / null đều rơi về preset bảng: mục tiêu của cả
|
|
5136
|
+
// cơ chế này là KHÔNG BAO GIỜ để vùng nội dung trắng, nên vẽ hình generic
|
|
5137
|
+
// vẫn hơn không vẽ gì.
|
|
5138
|
+
const list = Array.isArray(value) && value.length
|
|
5139
|
+
? value
|
|
5140
|
+
: (PRESETS[value] ?? PRESETS.table);
|
|
5141
|
+
return list.map((b) => (typeof b === 'string' ? { kind: b } : b));
|
|
5142
|
+
}, ...(ngDevMode ? [{ debugName: "blocks" }] : /* istanbul ignore next */ []));
|
|
5143
|
+
cols(b) {
|
|
5144
|
+
return range$1(b.columns ?? 6);
|
|
5145
|
+
}
|
|
5146
|
+
rows(b) {
|
|
5147
|
+
return range$1(b.rows ?? 8);
|
|
5148
|
+
}
|
|
5149
|
+
items(b, fallback) {
|
|
5150
|
+
return range$1(b.count ?? fallback);
|
|
5151
|
+
}
|
|
5152
|
+
/** Lớp lưới cho `panels`/`cards` — Tailwind v4 không có safelist nên phải
|
|
5153
|
+
* là chuỗi tĩnh, không ghép động. */
|
|
5154
|
+
gridClass(b) {
|
|
5155
|
+
switch (b.cols ?? 2) {
|
|
5156
|
+
case 1:
|
|
5157
|
+
return 'grid-cols-1';
|
|
5158
|
+
case 3:
|
|
5159
|
+
return 'grid-cols-1 md:grid-cols-3';
|
|
5160
|
+
case 4:
|
|
5161
|
+
return 'grid-cols-2 md:grid-cols-4';
|
|
5162
|
+
default:
|
|
5163
|
+
return 'grid-cols-1 md:grid-cols-2';
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
/** Header ngắn hơn ô dữ liệu, cột đầu rộng nhất (thường là tên). */
|
|
5167
|
+
headWidth(c) {
|
|
5168
|
+
return c === 0 ? 55 : [40, 34, 46, 30, 38][c % 5];
|
|
5169
|
+
}
|
|
5170
|
+
/**
|
|
5171
|
+
* Bề rộng đổi theo cả hàng và cột — cùng bề rộng thì khối skeleton trông
|
|
5172
|
+
* như một hình chữ nhật đặc, mất cảm giác "từng field".
|
|
5173
|
+
*/
|
|
5174
|
+
cellWidth(r, c) {
|
|
5175
|
+
if (c === 0)
|
|
5176
|
+
return [92, 78, 85, 70][r % 4];
|
|
5177
|
+
return [62, 48, 74, 55, 66, 42][(r + c) % 6];
|
|
5178
|
+
}
|
|
5179
|
+
barHeight(i) {
|
|
5180
|
+
return [45, 70, 35, 85, 55, 75, 40][i % 7];
|
|
5181
|
+
}
|
|
5182
|
+
bars = range$1(7);
|
|
5183
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RouteSkeletonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5184
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: RouteSkeletonComponent, isStandalone: true, selector: "fx-ui-route-skeleton", inputs: { hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"w-full flex flex-col gap-xl\">\n @for (b of blocks(); track $index) {\n @switch (b.kind) {\n @case ('heading') {\n <div data-fx-skel=\"heading\" class=\"flex flex-col md:flex-row md:items-start justify-between gap-normal\">\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-7 w-64\"></div>\n <div class=\"skeleton-load h-3 w-32\"></div>\n </div>\n @if (items(b, 2).length) {\n <div class=\"flex items-center gap-normal\">\n @for (a of items(b, 2); track a) {\n <div class=\"skeleton-load h-9 rounded-lg\" [class]=\"a === 0 ? 'w-28' : 'w-32'\"></div>\n }\n </div>\n }\n </div>\n }\n\n @case ('toolbar') {\n <div data-fx-skel=\"toolbar\" class=\"flex flex-wrap items-center gap-normal\">\n <div class=\"skeleton-load h-9 w-72 rounded-lg\"></div>\n @for (f of items(b, 2); track f) {\n <div class=\"skeleton-load h-7 rounded-full\" [class]=\"f % 2 ? 'w-24' : 'w-20'\"></div>\n }\n </div>\n }\n\n @case ('table') {\n <!-- B\u1EA3ng: m\u1ED7i \u00D4 l\u00E0 m\u1ED9t `.skeleton-load` ri\u00EAng, \u0111\u00FAng c\u00E1ch\n `fx-ui-table-cell [loading]` l\u00E0m tr\u00EAn b\u1EA3ng th\u1EADt. -->\n <div data-fx-skel=\"table\" class=\"card-common overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-border-default\">\n @for (c of cols(b); track c) {\n <th class=\"px-normal py-semi text-left\">\n <div class=\"skeleton-load h-3\" [style.width.%]=\"headWidth(c)\"></div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (r of rows(b); track r) {\n <tr class=\"border-b border-border-subtle\">\n @for (c of cols(b); track c) {\n <td class=\"px-normal py-semi align-middle\">\n <div class=\"skeleton-load h-4\" [style.width.%]=\"cellWidth(r, c)\"></div>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n @case ('stat-cards') {\n <div data-fx-skel=\"stat-cards\" class=\"grid gap-normal grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"skeleton-load h-3 w-16\"></div>\n <div class=\"skeleton-load h-8 w-24\"></div>\n <div class=\"skeleton-load h-3 w-20\"></div>\n </div>\n }\n </div>\n }\n\n @case ('kpi-strip') {\n <!-- M\u1ED8T card chia c\u1ED9t (ki\u1EC3u app-dash-kpi-row), kh\u00F4ng ph\u1EA3i c\u00E1c tile r\u1EDDi -->\n <div data-fx-skel=\"kpi-strip\" class=\"card-common !p-0\">\n <div class=\"grid grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div\n class=\"flex flex-col gap-normal px-xl py-large border-border-subtle\"\n [class.border-l]=\"!$first\"\n >\n <div class=\"skeleton-load h-3 w-20\"></div>\n <div class=\"skeleton-load h-9 w-28\"></div>\n <div class=\"skeleton-load h-3 w-24\"></div>\n </div>\n }\n </div>\n </div>\n }\n\n @case ('panels') {\n <div data-fx-skel=\"panels\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 2); track i) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (x of bars; track x) {\n <div class=\"skeleton-load flex-1 rounded-t\" [style.height.%]=\"barHeight(x)\"></div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('cards') {\n <div data-fx-skel=\"cards\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 6); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"flex items-center gap-normal\">\n <div class=\"skeleton-load w-9 h-9 rounded-lg shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"skeleton-load h-4 w-3/4\"></div>\n <div class=\"skeleton-load h-3 w-1/2\"></div>\n </div>\n <div class=\"skeleton-load h-5 w-14 rounded-full shrink-0\"></div>\n </div>\n <div class=\"grid grid-cols-4 gap-normal pt-normal border-t border-border-subtle\">\n @for (m of [0, 1, 2, 3]; track m) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-2.5 w-full\"></div>\n <div class=\"skeleton-load h-4 w-2/3\"></div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('card') {\n <div data-fx-skel=\"card\" class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"skeleton-load h-3\" [style.width.%]=\"cellWidth(r, 1)\"></div>\n }\n </div>\n </div>\n }\n\n @case ('tabs') {\n <div data-fx-skel=\"tabs\" class=\"flex items-center gap-large border-b border-border-default pb-normal\">\n @for (t of items(b, 6); track t) {\n <div class=\"flex items-center gap-small\">\n <div class=\"skeleton-load w-4 h-4\"></div>\n <div class=\"skeleton-load h-3 w-16\"></div>\n </div>\n }\n </div>\n }\n\n @case ('facets') {\n <div data-fx-skel=\"facets\" class=\"fx-rs-row\">\n <div class=\"fx-rs-facets card-common overflow-hidden\">\n <fx-ui-skeleton-facets />\n </div>\n <div class=\"fx-rs-list card-common overflow-hidden\">\n <fx-ui-skeleton-list [rows]=\"6\" />\n </div>\n <div class=\"flex-1 min-w-0 overflow-hidden\">\n <fx-ui-skeleton-detail />\n </div>\n </div>\n }\n\n @case ('detail') {\n <fx-ui-skeleton-detail data-fx-skel=\"detail\" />\n }\n\n @case ('form') {\n <div data-fx-skel=\"form\" class=\"card-common flex flex-col gap-large\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-3 w-24\"></div>\n <div class=\"skeleton-load h-9 w-full rounded-lg\"></div>\n </div>\n }\n <div class=\"flex justify-end gap-normal\">\n <div class=\"skeleton-load h-9 w-24 rounded-lg\"></div>\n <div class=\"skeleton-load h-9 w-28 rounded-lg\"></div>\n </div>\n </div>\n }\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-rs}.fx-rs-row{display:flex;flex-direction:column;gap:var(--spacing-xl, 1.5rem)}@container fx-rs (min-width: 968px){.fx-rs-row{flex-direction:row;align-items:stretch;height:calc(100vh - 300px);min-height:420px}.fx-rs-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-rs-list{flex:0 1 420px;min-width:320px;max-width:420px}}\n"], dependencies: [{ kind: "component", type: SkeletonListComponent, selector: "fx-ui-skeleton-list", inputs: ["rows"] }, { kind: "component", type: SkeletonDetailComponent, selector: "fx-ui-skeleton-detail", inputs: ["cards", "rows"] }, { kind: "component", type: SkeletonFacetsComponent, selector: "fx-ui-skeleton-facets", inputs: ["groups", "rows"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5185
|
+
}
|
|
5186
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: RouteSkeletonComponent, decorators: [{
|
|
5187
|
+
type: Component,
|
|
5188
|
+
args: [{ selector: 'fx-ui-route-skeleton', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SkeletonListComponent, SkeletonDetailComponent, SkeletonFacetsComponent], template: "<div class=\"w-full flex flex-col gap-xl\">\n @for (b of blocks(); track $index) {\n @switch (b.kind) {\n @case ('heading') {\n <div data-fx-skel=\"heading\" class=\"flex flex-col md:flex-row md:items-start justify-between gap-normal\">\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-7 w-64\"></div>\n <div class=\"skeleton-load h-3 w-32\"></div>\n </div>\n @if (items(b, 2).length) {\n <div class=\"flex items-center gap-normal\">\n @for (a of items(b, 2); track a) {\n <div class=\"skeleton-load h-9 rounded-lg\" [class]=\"a === 0 ? 'w-28' : 'w-32'\"></div>\n }\n </div>\n }\n </div>\n }\n\n @case ('toolbar') {\n <div data-fx-skel=\"toolbar\" class=\"flex flex-wrap items-center gap-normal\">\n <div class=\"skeleton-load h-9 w-72 rounded-lg\"></div>\n @for (f of items(b, 2); track f) {\n <div class=\"skeleton-load h-7 rounded-full\" [class]=\"f % 2 ? 'w-24' : 'w-20'\"></div>\n }\n </div>\n }\n\n @case ('table') {\n <!-- B\u1EA3ng: m\u1ED7i \u00D4 l\u00E0 m\u1ED9t `.skeleton-load` ri\u00EAng, \u0111\u00FAng c\u00E1ch\n `fx-ui-table-cell [loading]` l\u00E0m tr\u00EAn b\u1EA3ng th\u1EADt. -->\n <div data-fx-skel=\"table\" class=\"card-common overflow-x-auto\">\n <table class=\"w-full\">\n <thead>\n <tr class=\"border-b border-border-default\">\n @for (c of cols(b); track c) {\n <th class=\"px-normal py-semi text-left\">\n <div class=\"skeleton-load h-3\" [style.width.%]=\"headWidth(c)\"></div>\n </th>\n }\n </tr>\n </thead>\n <tbody>\n @for (r of rows(b); track r) {\n <tr class=\"border-b border-border-subtle\">\n @for (c of cols(b); track c) {\n <td class=\"px-normal py-semi align-middle\">\n <div class=\"skeleton-load h-4\" [style.width.%]=\"cellWidth(r, c)\"></div>\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n @case ('stat-cards') {\n <div data-fx-skel=\"stat-cards\" class=\"grid gap-normal grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"skeleton-load h-3 w-16\"></div>\n <div class=\"skeleton-load h-8 w-24\"></div>\n <div class=\"skeleton-load h-3 w-20\"></div>\n </div>\n }\n </div>\n }\n\n @case ('kpi-strip') {\n <!-- M\u1ED8T card chia c\u1ED9t (ki\u1EC3u app-dash-kpi-row), kh\u00F4ng ph\u1EA3i c\u00E1c tile r\u1EDDi -->\n <div data-fx-skel=\"kpi-strip\" class=\"card-common !p-0\">\n <div class=\"grid grid-cols-2 md:grid-cols-4\">\n @for (i of items(b, 4); track i) {\n <div\n class=\"flex flex-col gap-normal px-xl py-large border-border-subtle\"\n [class.border-l]=\"!$first\"\n >\n <div class=\"skeleton-load h-3 w-20\"></div>\n <div class=\"skeleton-load h-9 w-28\"></div>\n <div class=\"skeleton-load h-3 w-24\"></div>\n </div>\n }\n </div>\n </div>\n }\n\n @case ('panels') {\n <div data-fx-skel=\"panels\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 2); track i) {\n <div class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex items-end gap-normal h-40\">\n @for (x of bars; track x) {\n <div class=\"skeleton-load flex-1 rounded-t\" [style.height.%]=\"barHeight(x)\"></div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('cards') {\n <div data-fx-skel=\"cards\" class=\"grid gap-xl\" [class]=\"gridClass(b)\">\n @for (i of items(b, 6); track i) {\n <div class=\"card-common flex flex-col gap-normal\">\n <div class=\"flex items-center gap-normal\">\n <div class=\"skeleton-load w-9 h-9 rounded-lg shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"skeleton-load h-4 w-3/4\"></div>\n <div class=\"skeleton-load h-3 w-1/2\"></div>\n </div>\n <div class=\"skeleton-load h-5 w-14 rounded-full shrink-0\"></div>\n </div>\n <div class=\"grid grid-cols-4 gap-normal pt-normal border-t border-border-subtle\">\n @for (m of [0, 1, 2, 3]; track m) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-2.5 w-full\"></div>\n <div class=\"skeleton-load h-4 w-2/3\"></div>\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n @case ('card') {\n <div data-fx-skel=\"card\" class=\"card-common flex flex-col gap-large\">\n <div class=\"skeleton-load h-4 w-1/3\"></div>\n <div class=\"flex flex-col gap-normal\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"skeleton-load h-3\" [style.width.%]=\"cellWidth(r, 1)\"></div>\n }\n </div>\n </div>\n }\n\n @case ('tabs') {\n <div data-fx-skel=\"tabs\" class=\"flex items-center gap-large border-b border-border-default pb-normal\">\n @for (t of items(b, 6); track t) {\n <div class=\"flex items-center gap-small\">\n <div class=\"skeleton-load w-4 h-4\"></div>\n <div class=\"skeleton-load h-3 w-16\"></div>\n </div>\n }\n </div>\n }\n\n @case ('facets') {\n <div data-fx-skel=\"facets\" class=\"fx-rs-row\">\n <div class=\"fx-rs-facets card-common overflow-hidden\">\n <fx-ui-skeleton-facets />\n </div>\n <div class=\"fx-rs-list card-common overflow-hidden\">\n <fx-ui-skeleton-list [rows]=\"6\" />\n </div>\n <div class=\"flex-1 min-w-0 overflow-hidden\">\n <fx-ui-skeleton-detail />\n </div>\n </div>\n }\n\n @case ('detail') {\n <fx-ui-skeleton-detail data-fx-skel=\"detail\" />\n }\n\n @case ('form') {\n <div data-fx-skel=\"form\" class=\"card-common flex flex-col gap-large\">\n @for (r of [0, 1, 2, 3]; track r) {\n <div class=\"flex flex-col gap-small\">\n <div class=\"skeleton-load h-3 w-24\"></div>\n <div class=\"skeleton-load h-9 w-full rounded-lg\"></div>\n </div>\n }\n <div class=\"flex justify-end gap-normal\">\n <div class=\"skeleton-load h-9 w-24 rounded-lg\"></div>\n <div class=\"skeleton-load h-9 w-28 rounded-lg\"></div>\n </div>\n </div>\n }\n }\n }\n</div>\n", styles: [":host{display:block;container-type:inline-size;container-name:fx-rs}.fx-rs-row{display:flex;flex-direction:column;gap:var(--spacing-xl, 1.5rem)}@container fx-rs (min-width: 968px){.fx-rs-row{flex-direction:row;align-items:stretch;height:calc(100vh - 300px);min-height:420px}.fx-rs-facets{flex:0 1 230px;min-width:200px;max-width:260px}.fx-rs-list{flex:0 1 420px;min-width:320px;max-width:420px}}\n"] }]
|
|
5189
|
+
}], propDecorators: { hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
|
|
5190
|
+
function range$1(n) {
|
|
5191
|
+
return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
/**
|
|
5195
|
+
* Skeleton của cả AppFrame (drawer + topbar + vùng nội dung).
|
|
5196
|
+
*
|
|
5197
|
+
* Chỉ dùng cho giai đoạn **bootstrap**, khi shell của host còn CHƯA tồn tại
|
|
5198
|
+
* (App.ngOnInit đang kiểm tenant + session, router chưa initialNavigation).
|
|
5199
|
+
* Mọi điều hướng SAU đó đã có shell thật → dùng `fx-ui-route-skeleton` bên
|
|
5200
|
+
* trong vùng nội dung, đừng vẽ lại cả frame.
|
|
5201
|
+
*
|
|
5202
|
+
* ⚠️ `contentHint` là thứ làm F5 không còn vẽ hình generic. Lần điều hướng ĐẦU
|
|
5203
|
+
* tạo ra chính `HomeComponent` — nơi chứa `fx-ui-route-skeleton` — nên vùng nội
|
|
5204
|
+
* dung không thể tự vẽ skeleton của route ở lần đó. Truyền hint vào đây để
|
|
5205
|
+
* frame dựng đúng composition của màn đích ngay từ F5.
|
|
5206
|
+
*/
|
|
5207
|
+
class SkeletonFrameComponent {
|
|
5208
|
+
navItems = input(7, ...(ngDevMode ? [{ debugName: "navItems" }] : /* istanbul ignore next */ []));
|
|
5209
|
+
/** Composition của route đích; null thì vẽ khối generic. */
|
|
5210
|
+
contentHint = input(null, ...(ngDevMode ? [{ debugName: "contentHint" }] : /* istanbul ignore next */ []));
|
|
5211
|
+
navList = computed(() => range(this.navItems()), ...(ngDevMode ? [{ debugName: "navList" }] : /* istanbul ignore next */ []));
|
|
5212
|
+
navWidth(i) {
|
|
5213
|
+
return [70, 55, 62, 48, 66, 52, 58][i % 7];
|
|
5214
|
+
}
|
|
5215
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFrameComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5216
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.20", type: SkeletonFrameComponent, isStandalone: true, selector: "fx-ui-skeleton-frame", inputs: { navItems: { classPropertyName: "navItems", publicName: "navItems", isSignal: true, isRequired: false, transformFunction: null }, contentHint: { classPropertyName: "contentHint", publicName: "contentHint", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"flex h-screen w-full overflow-hidden bg-bg-primary\">\n <!-- drawer -->\n <div\n class=\"hidden md:flex w-[260px] shrink-0 flex-col gap-xl border-r border-border-subtle p-semi\"\n >\n <div class=\"flex items-center gap-normal\">\n <div class=\"w-6 h-6 rounded skeleton-load shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"h-3.5 w-20 rounded skeleton-load\"></div>\n <div class=\"h-2.5 w-full rounded skeleton-load\"></div>\n </div>\n </div>\n\n <div class=\"flex flex-col gap-small\">\n @for (n of navList(); track n) {\n <div class=\"flex items-center gap-normal py-1\">\n <div class=\"w-4 h-4 rounded skeleton-load shrink-0\"></div>\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"navWidth(n)\"></div>\n </div>\n }\n </div>\n\n <div class=\"mt-auto flex items-center gap-normal border-t border-border-subtle pt-semi\">\n <div class=\"w-8 h-8 rounded-full skeleton-load shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"h-3 w-24 rounded skeleton-load\"></div>\n <div class=\"h-2.5 w-32 rounded skeleton-load\"></div>\n </div>\n </div>\n </div>\n\n <div class=\"flex flex-1 min-w-0 flex-col\">\n <!-- topbar -->\n <div\n class=\"flex h-14 shrink-0 items-center gap-normal border-b border-border-subtle px-semi\"\n >\n <div class=\"h-3.5 w-40 rounded skeleton-load\"></div>\n <div class=\"ml-auto h-8 w-32 rounded-lg skeleton-load\"></div>\n </div>\n\n <!-- content: d\u1EF1ng \u0111\u00FAng composition c\u1EE7a route \u0111\u00EDch n\u1EBFu \u0111\u00E3 bi\u1EBFt -->\n <div class=\"flex-1 min-h-0 overflow-hidden p-semi\">\n @if (contentHint(); as hint) {\n <fx-ui-route-skeleton [hint]=\"hint\" />\n } @else {\n <div class=\"flex flex-col gap-xl\">\n <div class=\"h-6 w-1/4 rounded skeleton-load\"></div>\n <div class=\"grid gap-xl grid-cols-2 md:grid-cols-4\">\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n </div>\n <div class=\"h-64 rounded-lg skeleton-load\"></div>\n </div>\n }\n </div>\n </div>\n</div>\n", dependencies: [{ kind: "component", type: RouteSkeletonComponent, selector: "fx-ui-route-skeleton", inputs: ["hint"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5217
|
+
}
|
|
5218
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: SkeletonFrameComponent, decorators: [{
|
|
5219
|
+
type: Component,
|
|
5220
|
+
args: [{ selector: 'fx-ui-skeleton-frame', changeDetection: ChangeDetectionStrategy.OnPush, imports: [RouteSkeletonComponent], template: "<div class=\"flex h-screen w-full overflow-hidden bg-bg-primary\">\n <!-- drawer -->\n <div\n class=\"hidden md:flex w-[260px] shrink-0 flex-col gap-xl border-r border-border-subtle p-semi\"\n >\n <div class=\"flex items-center gap-normal\">\n <div class=\"w-6 h-6 rounded skeleton-load shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"h-3.5 w-20 rounded skeleton-load\"></div>\n <div class=\"h-2.5 w-full rounded skeleton-load\"></div>\n </div>\n </div>\n\n <div class=\"flex flex-col gap-small\">\n @for (n of navList(); track n) {\n <div class=\"flex items-center gap-normal py-1\">\n <div class=\"w-4 h-4 rounded skeleton-load shrink-0\"></div>\n <div class=\"h-3 rounded skeleton-load\" [style.width.%]=\"navWidth(n)\"></div>\n </div>\n }\n </div>\n\n <div class=\"mt-auto flex items-center gap-normal border-t border-border-subtle pt-semi\">\n <div class=\"w-8 h-8 rounded-full skeleton-load shrink-0\"></div>\n <div class=\"flex flex-col gap-small flex-1 min-w-0\">\n <div class=\"h-3 w-24 rounded skeleton-load\"></div>\n <div class=\"h-2.5 w-32 rounded skeleton-load\"></div>\n </div>\n </div>\n </div>\n\n <div class=\"flex flex-1 min-w-0 flex-col\">\n <!-- topbar -->\n <div\n class=\"flex h-14 shrink-0 items-center gap-normal border-b border-border-subtle px-semi\"\n >\n <div class=\"h-3.5 w-40 rounded skeleton-load\"></div>\n <div class=\"ml-auto h-8 w-32 rounded-lg skeleton-load\"></div>\n </div>\n\n <!-- content: d\u1EF1ng \u0111\u00FAng composition c\u1EE7a route \u0111\u00EDch n\u1EBFu \u0111\u00E3 bi\u1EBFt -->\n <div class=\"flex-1 min-h-0 overflow-hidden p-semi\">\n @if (contentHint(); as hint) {\n <fx-ui-route-skeleton [hint]=\"hint\" />\n } @else {\n <div class=\"flex flex-col gap-xl\">\n <div class=\"h-6 w-1/4 rounded skeleton-load\"></div>\n <div class=\"grid gap-xl grid-cols-2 md:grid-cols-4\">\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n <div class=\"h-24 rounded-lg skeleton-load\"></div>\n </div>\n <div class=\"h-64 rounded-lg skeleton-load\"></div>\n </div>\n }\n </div>\n </div>\n</div>\n" }]
|
|
5221
|
+
}], propDecorators: { navItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "navItems", required: false }] }], contentHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentHint", required: false }] }] } });
|
|
5222
|
+
function range(n) {
|
|
5223
|
+
return Array.from({ length: Math.max(0, Number(n) || 0) }, (_, i) => i);
|
|
5224
|
+
}
|
|
5225
|
+
|
|
4069
5226
|
const FX_COMPONENTS = [
|
|
4070
5227
|
InputComponent,
|
|
4071
5228
|
SelectComponent,
|
|
@@ -4085,7 +5242,6 @@ const FX_COMPONENTS = [
|
|
|
4085
5242
|
TagComponent,
|
|
4086
5243
|
SliderComponent,
|
|
4087
5244
|
SwitchComponent,
|
|
4088
|
-
SkeletonTableLoadingComponent,
|
|
4089
5245
|
CircleProgressBar,
|
|
4090
5246
|
TreeDiagram,
|
|
4091
5247
|
TableCell,
|
|
@@ -4096,12 +5252,32 @@ const FX_COMPONENTS = [
|
|
|
4096
5252
|
StatCardsComponent,
|
|
4097
5253
|
EmptyStateComponent,
|
|
4098
5254
|
FilterChipsComponent,
|
|
5255
|
+
FilterPillsComponent,
|
|
5256
|
+
StageDotsComponent,
|
|
5257
|
+
CodeBlockComponent,
|
|
5258
|
+
ScreenshotFrameComponent,
|
|
4099
5259
|
MeterComponent,
|
|
4100
5260
|
SectionCardComponent,
|
|
4101
5261
|
InfoFieldComponent,
|
|
4102
5262
|
SeverityBadgesComponent,
|
|
4103
5263
|
MasterDetailComponent,
|
|
4104
5264
|
MenuComponent,
|
|
5265
|
+
ThemePickerComponent,
|
|
5266
|
+
PendingBadgeComponent,
|
|
5267
|
+
SampleFrameComponent,
|
|
5268
|
+
ScoreBadgeComponent,
|
|
5269
|
+
StatusDotComponent,
|
|
5270
|
+
SparkbarComponent,
|
|
5271
|
+
SegmentedComponent,
|
|
5272
|
+
ListRowComponent,
|
|
5273
|
+
BarListComponent,
|
|
5274
|
+
FacetsRailComponent,
|
|
5275
|
+
ProjectCardComponent,
|
|
5276
|
+
PipelineStepperComponent,
|
|
5277
|
+
SkeletonFacetsComponent,
|
|
5278
|
+
SkeletonDashboardComponent,
|
|
5279
|
+
SkeletonFrameComponent,
|
|
5280
|
+
RouteSkeletonComponent,
|
|
4105
5281
|
];
|
|
4106
5282
|
|
|
4107
5283
|
const DIALOGS_COMPONENT = [
|
|
@@ -4125,14 +5301,14 @@ class UiModule {
|
|
|
4125
5301
|
ReactiveFormsModule,
|
|
4126
5302
|
RouterModule,
|
|
4127
5303
|
HasPermissionDirective,
|
|
4128
|
-
TrimOnBlurDirective, InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent,
|
|
5304
|
+
TrimOnBlurDirective, InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, SkeletonFacetsComponent, SkeletonDashboardComponent, SkeletonFrameComponent, RouteSkeletonComponent, ConfirmationDialogComponent], exports: [InputComponent, SelectComponent, RadioButtonComponent, CheckboxComponent, DndUploadComponent, ButtonComponent, RadioButtonToggleComponent, DatetimePicker, LoadingPanel, SearchBarComponent, TabGroupComponent, TabComponent, HeroIconComponent, ToastComponent, ToastContainerComponent, TagComponent, SliderComponent, SwitchComponent, CircleProgressBar, TreeDiagram, TableCell, KanbanBoardComponent, DrawerComponent, SkeletonListComponent, SkeletonDetailComponent, StatCardsComponent, EmptyStateComponent, FilterChipsComponent, FilterPillsComponent, StageDotsComponent, CodeBlockComponent, ScreenshotFrameComponent, MeterComponent, SectionCardComponent, InfoFieldComponent, SeverityBadgesComponent, MasterDetailComponent, MenuComponent, ThemePickerComponent, PendingBadgeComponent, SampleFrameComponent, ScoreBadgeComponent, StatusDotComponent, SparkbarComponent, SegmentedComponent, ListRowComponent, BarListComponent, FacetsRailComponent, ProjectCardComponent, PipelineStepperComponent, SkeletonFacetsComponent, SkeletonDashboardComponent, SkeletonFrameComponent, RouteSkeletonComponent, ConfirmationDialogComponent, FormsModule,
|
|
4129
5305
|
ReactiveFormsModule,
|
|
4130
5306
|
RouterModule,
|
|
4131
5307
|
HasPermissionDirective,
|
|
4132
5308
|
TrimOnBlurDirective] });
|
|
4133
5309
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.20", ngImport: i0, type: UiModule, imports: [FormsModule,
|
|
4134
5310
|
ReactiveFormsModule,
|
|
4135
|
-
RouterModule, SelectComponent, DatetimePicker, MasterDetailComponent, MenuComponent, FormsModule,
|
|
5311
|
+
RouterModule, SelectComponent, DatetimePicker, MasterDetailComponent, MenuComponent, ThemePickerComponent, FormsModule,
|
|
4136
5312
|
ReactiveFormsModule,
|
|
4137
5313
|
RouterModule] });
|
|
4138
5314
|
}
|
|
@@ -4168,5 +5344,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.20", ngImpo
|
|
|
4168
5344
|
* Generated bundle index. Do not edit.
|
|
4169
5345
|
*/
|
|
4170
5346
|
|
|
4171
|
-
export { AuthInterceptor, AuthStateService, BaseComponent, BaseDialogComponent, BaseMasterDetailComponent,
|
|
5347
|
+
export { AuthInterceptor, AuthStateService, BarListComponent, BaseComponent, BaseDialogComponent, BaseMasterDetailComponent, BaseTableComponent, BreadcrumbService, ButtonComponent, CheckboxComponent, CircleProgressBar, CodeBlockComponent, ConfirmationDialogComponent, DatetimePicker, DndUploadComponent, DrawerComponent, EmptyStateComponent, FX_ICON_SPRITE_URL, FX_SKELETON_DATA_KEY, FacetsRailComponent, FilterChipsComponent, FilterPillsComponent, FxComponent, FxDialogService, FxLoadingService, FxRouteSkeletonService, FxStorageService, FxToastrService, FxUtils, FxValidators, HasPermissionDirective, HeroIconComponent, HttpLoaderFactory, HttpWrapper, InfoFieldComponent, InputComponent, KanbanBoardComponent, ListRowComponent, LoadingPanel, MasterDetailComponent, MenuComponent, MeterComponent, NotificationService, OG_LIGHT_THEMES, OG_PICKER_THEMES, OG_PICKER_THEME_META, OG_THEMES, OG_THEME_META, PendingBadgeComponent, PermissionGuard, PermissionService, PipelineStepperComponent, ProjectCardComponent, RadioButtonComponent, RadioButtonToggleComponent, RealtimeTableSync, RouteSkeletonComponent, SampleFrameComponent, ScoreBadgeComponent, ScreenshotFrameComponent, SearchBarComponent, SectionCardComponent, SegmentedComponent, SelectComponent, SeverityBadgesComponent, SkeletonDashboardComponent, SkeletonDetailComponent, SkeletonFacetsComponent, SkeletonFrameComponent, SkeletonListComponent, SliderComponent, SparkbarComponent, StageDotsComponent, StatCardsComponent, StatusDotComponent, SwitchComponent, TabComponent, TabGroupComponent, TableCell, TagComponent, ThemePickerComponent, ThemeService, ToastComponent, ToastContainerComponent, TranslationModule, TranslationService, TreeDiagram, TrimOnBlurDirective, UiModule, anyFeatureEnabled, countTone, createApi, featureEnabled, fxListResolver, scoreTone, severityTone, toneBadge, toneBadgeSolid, toneBg, toneBorder, toneColor, toneDot, toneDotChart, toneFg, toneScale, toneText, toneTint };
|
|
4172
5348
|
//# sourceMappingURL=foxeltech-angular-ui.mjs.map
|