@elasticias/ui 1.0.42 → 1.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/elasticias-ui.mjs +303 -46
- package/fesm2022/elasticias-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/styles/_patterns.scss +182 -45
- package/types/elasticias-ui.d.ts +98 -12
|
@@ -22,7 +22,7 @@ import * as i1$6 from 'primeng/dialog';
|
|
|
22
22
|
import { DialogModule } from 'primeng/dialog';
|
|
23
23
|
import { SCREEN_REF_DATA_SERVICE, ScreenStateEnum } from '@elasticias/screens';
|
|
24
24
|
import { Permissions } from '@elasticias/types';
|
|
25
|
-
import {
|
|
25
|
+
import { EfViewportService, EfShortcutService, EF_PREFERENCE_STORE, formatShortcut, efTablePrefsKey, EfThemeConfigService, EF_BUILD_INFO, EfPermissionService, EfActiveModuleService, EF_SESSION, EF_SIDE_COLLAPSED_KEY, EfToastService } from '@elasticias/core';
|
|
26
26
|
import * as i1$7 from 'primeng/fieldset';
|
|
27
27
|
import { FieldsetModule } from 'primeng/fieldset';
|
|
28
28
|
import * as i2$2 from 'primeng/accordion';
|
|
@@ -3895,6 +3895,13 @@ const EF_DATA_CARD_MOBILE_LAYOUT = new InjectionToken('EF_DATA_CARD_MOBILE_LAYOU
|
|
|
3895
3895
|
*
|
|
3896
3896
|
* Renders a windowed page list (first / current ±1 / last with
|
|
3897
3897
|
* ellipsis) for total > 7 pages. Below 7 pages all buttons show.
|
|
3898
|
+
*
|
|
3899
|
+
* On a phone (`EfViewportService.isMobile`) the window becomes
|
|
3900
|
+
* first · previous · "5 of 20" · next · last. The window is up to nine
|
|
3901
|
+
* 48px touch targets (about 464px) and a 390px phone has about 316px
|
|
3902
|
+
* inside a card, so it ran off the edge. The compact set still reaches
|
|
3903
|
+
* every page the window offered: its numbered neighbours are one step
|
|
3904
|
+
* away, and the first and the last page are one tap.
|
|
3898
3905
|
*/
|
|
3899
3906
|
class EfPagerComponent {
|
|
3900
3907
|
pageNumber = input(1, ...(ngDevMode ? [{ debugName: "pageNumber" }] : /* istanbul ignore next */ []));
|
|
@@ -3906,10 +3913,14 @@ class EfPagerComponent {
|
|
|
3906
3913
|
ofLabelKey = input('common_of', ...(ngDevMode ? [{ debugName: "ofLabelKey" }] : /* istanbul ignore next */ []));
|
|
3907
3914
|
prevLabelKey = input('common_page_previous', ...(ngDevMode ? [{ debugName: "prevLabelKey" }] : /* istanbul ignore next */ []));
|
|
3908
3915
|
nextLabelKey = input('common_page_next', ...(ngDevMode ? [{ debugName: "nextLabelKey" }] : /* istanbul ignore next */ []));
|
|
3916
|
+
firstLabelKey = input('common_page_first', ...(ngDevMode ? [{ debugName: "firstLabelKey" }] : /* istanbul ignore next */ []));
|
|
3917
|
+
lastLabelKey = input('common_page_last', ...(ngDevMode ? [{ debugName: "lastLabelKey" }] : /* istanbul ignore next */ []));
|
|
3909
3918
|
/** Hide the lines-per-page selector + range when only paging is wanted. */
|
|
3910
3919
|
hideSize = input(false, ...(ngDevMode ? [{ debugName: "hideSize" }] : /* istanbul ignore next */ []));
|
|
3911
3920
|
pageChange = output();
|
|
3912
3921
|
pageSizeChange = output();
|
|
3922
|
+
/** Phone layout: first · prev · "n of N" · next · last. */
|
|
3923
|
+
compact = inject(EfViewportService).isMobile;
|
|
3913
3924
|
/** Falls back to the first configured option if the value ever goes empty. */
|
|
3914
3925
|
get defaultSize() {
|
|
3915
3926
|
return this.pageSizeOptions()[0] ?? this.pageSize();
|
|
@@ -3975,12 +3986,12 @@ class EfPagerComponent {
|
|
|
3975
3986
|
return btn.kind === 'page' ? `p${btn.value}` : `e${index}`;
|
|
3976
3987
|
}
|
|
3977
3988
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfPagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3978
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfPagerComponent, isStandalone: true, selector: "ef-pager", inputs: { pageNumber: { classPropertyName: "pageNumber", publicName: "pageNumber", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, linesPerPageLabelKey: { classPropertyName: "linesPerPageLabelKey", publicName: "linesPerPageLabelKey", isSignal: true, isRequired: false, transformFunction: null }, ofLabelKey: { classPropertyName: "ofLabelKey", publicName: "ofLabelKey", isSignal: true, isRequired: false, transformFunction: null }, prevLabelKey: { classPropertyName: "prevLabelKey", publicName: "prevLabelKey", isSignal: true, isRequired: false, transformFunction: null }, nextLabelKey: { classPropertyName: "nextLabelKey", publicName: "nextLabelKey", isSignal: true, isRequired: false, transformFunction: null }, hideSize: { classPropertyName: "hideSize", publicName: "hideSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<div class=\"pager\">\n @if (!hideSize()) {\n <div class=\"size\">\n <span>{{ linesPerPageLabelKey() | translate }}</span>\n <ef-select\n size=\"small\"\n appendTo=\"body\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [options]=\"sizeOptions()\"\n [value]=\"pageSize()\"\n [ariaLabelKey]=\"linesPerPageLabelKey()\"\n [showClear]=\"false\"\n (selectionChangeEvent)=\"onSizeChange($event)\"\n />\n @if (totalCount() > 0) {\n <span>\u00B7 {{ range().start }}\u2013{{ range().end }} {{ ofLabelKey() | translate }} {{ totalCount() }}</span>\n }\n </div>\n } @else {\n <span></span>\n }\n\n <div class=\"pages\">\n <button\n class=\"pg\"\n type=\"button\"\n [disabled]=\"pageNumber() <= 1\"\n [attr.aria-label]=\"prevLabelKey() | translate\"\n (click)=\"goToPage(pageNumber() - 1)\"\n >\u2039</button>\n\n @for (btn of pageButtons(); track trackBtn($index, btn)) {\n
|
|
3989
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfPagerComponent, isStandalone: true, selector: "ef-pager", inputs: { pageNumber: { classPropertyName: "pageNumber", publicName: "pageNumber", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, linesPerPageLabelKey: { classPropertyName: "linesPerPageLabelKey", publicName: "linesPerPageLabelKey", isSignal: true, isRequired: false, transformFunction: null }, ofLabelKey: { classPropertyName: "ofLabelKey", publicName: "ofLabelKey", isSignal: true, isRequired: false, transformFunction: null }, prevLabelKey: { classPropertyName: "prevLabelKey", publicName: "prevLabelKey", isSignal: true, isRequired: false, transformFunction: null }, nextLabelKey: { classPropertyName: "nextLabelKey", publicName: "nextLabelKey", isSignal: true, isRequired: false, transformFunction: null }, firstLabelKey: { classPropertyName: "firstLabelKey", publicName: "firstLabelKey", isSignal: true, isRequired: false, transformFunction: null }, lastLabelKey: { classPropertyName: "lastLabelKey", publicName: "lastLabelKey", isSignal: true, isRequired: false, transformFunction: null }, hideSize: { classPropertyName: "hideSize", publicName: "hideSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<div class=\"pager\">\n @if (!hideSize()) {\n <div class=\"size\">\n <span>{{ linesPerPageLabelKey() | translate }}</span>\n <ef-select\n size=\"small\"\n appendTo=\"body\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [options]=\"sizeOptions()\"\n [value]=\"pageSize()\"\n [ariaLabelKey]=\"linesPerPageLabelKey()\"\n [showClear]=\"false\"\n (selectionChangeEvent)=\"onSizeChange($event)\"\n />\n @if (totalCount() > 0) {\n <span>\u00B7 {{ range().start }}\u2013{{ range().end }} {{ ofLabelKey() | translate }} {{ totalCount() }}</span>\n }\n </div>\n } @else {\n <span></span>\n }\n\n <div class=\"pages\">\n @if (compact()) {\n <button\n class=\"pg\"\n type=\"button\"\n [disabled]=\"pageNumber() <= 1\"\n [attr.aria-label]=\"firstLabelKey() | translate\"\n (click)=\"goToPage(1)\"\n >\u00AB</button>\n }\n\n <button\n class=\"pg\"\n type=\"button\"\n [disabled]=\"pageNumber() <= 1\"\n [attr.aria-label]=\"prevLabelKey() | translate\"\n (click)=\"goToPage(pageNumber() - 1)\"\n >\u2039</button>\n\n @if (compact()) {\n <span class=\"pg-status\">{{ pageNumber() }} {{ ofLabelKey() | translate }} {{ totalPages() }}</span>\n } @else {\n @for (btn of pageButtons(); track trackBtn($index, btn)) {\n @if (btn.kind === 'page') {\n <button\n class=\"pg\"\n type=\"button\"\n [class.active]=\"btn.value === pageNumber()\"\n (click)=\"goToPage(btn.value)\"\n >{{ btn.value }}</button>\n } @else {\n <span class=\"pg\" aria-hidden=\"true\" style=\"cursor: default;\">\u2026</span>\n }\n }\n }\n\n <button\n class=\"pg\"\n type=\"button\"\n [disabled]=\"pageNumber() >= totalPages()\"\n [attr.aria-label]=\"nextLabelKey() | translate\"\n (click)=\"goToPage(pageNumber() + 1)\"\n >\u203A</button>\n\n @if (compact()) {\n <button\n class=\"pg\"\n type=\"button\"\n [disabled]=\"pageNumber() >= totalPages()\"\n [attr.aria-label]=\"lastLabelKey() | translate\"\n (click)=\"goToPage(totalPages())\"\n >\u00BB</button>\n }\n </div>\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfSelectComponent, selector: "ef-select", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "dataKey", "group", "optionGroupLabel", "optionGroupChildren", "size", "fluid", "inline", "appendTo", "multiple", "filter", "filterBy", "filterPlaceholder", "filterPlaceholderKey", "autofocusFilter", "autofocus", "editable", "loading", "virtualScroll", "virtualScrollItemSize", "scrollHeight", "multiDisplay", "showToggleAll", "emptyMessage", "emptyMessageKey", "emptyFilterMessage", "emptyFilterMessageKey", "styleClass", "panelStyleClass", "filterByKeys", "variant", "value"], outputs: ["selectionChangeEvent", "filterChangeEvent", "panelShowEvent", "panelHideEvent"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3979
3990
|
}
|
|
3980
3991
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfPagerComponent, decorators: [{
|
|
3981
3992
|
type: Component,
|
|
3982
|
-
args: [{ selector: 'ef-pager', standalone: true, imports: [CommonModule, TranslateModule, EfSelectComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"pager\">\n @if (!hideSize()) {\n <div class=\"size\">\n <span>{{ linesPerPageLabelKey() | translate }}</span>\n <ef-select\n size=\"small\"\n appendTo=\"body\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [options]=\"sizeOptions()\"\n [value]=\"pageSize()\"\n [ariaLabelKey]=\"linesPerPageLabelKey()\"\n [showClear]=\"false\"\n (selectionChangeEvent)=\"onSizeChange($event)\"\n />\n @if (totalCount() > 0) {\n <span>\u00B7 {{ range().start }}\u2013{{ range().end }} {{ ofLabelKey() | translate }} {{ totalCount() }}</span>\n }\n </div>\n } @else {\n <span></span>\n }\n\n <div class=\"pages\">\n <button\n class=\"pg\"\n type=\"button\"\n [disabled]=\"pageNumber() <= 1\"\n [attr.aria-label]=\"prevLabelKey() | translate\"\n (click)=\"goToPage(pageNumber() - 1)\"\n >\u2039</button>\n\n @for (btn of pageButtons(); track trackBtn($index, btn)) {\n
|
|
3983
|
-
}], propDecorators: { pageNumber: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageNumber", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], totalCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalCount", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], linesPerPageLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "linesPerPageLabelKey", required: false }] }], ofLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "ofLabelKey", required: false }] }], prevLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabelKey", required: false }] }], nextLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabelKey", required: false }] }], hideSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideSize", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], pageSizeChange: [{ type: i0.Output, args: ["pageSizeChange"] }] } });
|
|
3993
|
+
args: [{ selector: 'ef-pager', standalone: true, imports: [CommonModule, TranslateModule, EfSelectComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"pager\">\n @if (!hideSize()) {\n <div class=\"size\">\n <span>{{ linesPerPageLabelKey() | translate }}</span>\n <ef-select\n size=\"small\"\n appendTo=\"body\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [options]=\"sizeOptions()\"\n [value]=\"pageSize()\"\n [ariaLabelKey]=\"linesPerPageLabelKey()\"\n [showClear]=\"false\"\n (selectionChangeEvent)=\"onSizeChange($event)\"\n />\n @if (totalCount() > 0) {\n <span>\u00B7 {{ range().start }}\u2013{{ range().end }} {{ ofLabelKey() | translate }} {{ totalCount() }}</span>\n }\n </div>\n } @else {\n <span></span>\n }\n\n <div class=\"pages\">\n @if (compact()) {\n <button\n class=\"pg\"\n type=\"button\"\n [disabled]=\"pageNumber() <= 1\"\n [attr.aria-label]=\"firstLabelKey() | translate\"\n (click)=\"goToPage(1)\"\n >\u00AB</button>\n }\n\n <button\n class=\"pg\"\n type=\"button\"\n [disabled]=\"pageNumber() <= 1\"\n [attr.aria-label]=\"prevLabelKey() | translate\"\n (click)=\"goToPage(pageNumber() - 1)\"\n >\u2039</button>\n\n @if (compact()) {\n <span class=\"pg-status\">{{ pageNumber() }} {{ ofLabelKey() | translate }} {{ totalPages() }}</span>\n } @else {\n @for (btn of pageButtons(); track trackBtn($index, btn)) {\n @if (btn.kind === 'page') {\n <button\n class=\"pg\"\n type=\"button\"\n [class.active]=\"btn.value === pageNumber()\"\n (click)=\"goToPage(btn.value)\"\n >{{ btn.value }}</button>\n } @else {\n <span class=\"pg\" aria-hidden=\"true\" style=\"cursor: default;\">\u2026</span>\n }\n }\n }\n\n <button\n class=\"pg\"\n type=\"button\"\n [disabled]=\"pageNumber() >= totalPages()\"\n [attr.aria-label]=\"nextLabelKey() | translate\"\n (click)=\"goToPage(pageNumber() + 1)\"\n >\u203A</button>\n\n @if (compact()) {\n <button\n class=\"pg\"\n type=\"button\"\n [disabled]=\"pageNumber() >= totalPages()\"\n [attr.aria-label]=\"lastLabelKey() | translate\"\n (click)=\"goToPage(totalPages())\"\n >\u00BB</button>\n }\n </div>\n</div>\n" }]
|
|
3994
|
+
}], propDecorators: { pageNumber: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageNumber", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], totalCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalCount", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], linesPerPageLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "linesPerPageLabelKey", required: false }] }], ofLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "ofLabelKey", required: false }] }], prevLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "prevLabelKey", required: false }] }], nextLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabelKey", required: false }] }], firstLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstLabelKey", required: false }] }], lastLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastLabelKey", required: false }] }], hideSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideSize", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], pageSizeChange: [{ type: i0.Output, args: ["pageSizeChange"] }] } });
|
|
3984
3995
|
|
|
3985
3996
|
/** Canonical tone list — used for membership checks by the resolver. */
|
|
3986
3997
|
const EF_STATUS_COLORS = [
|
|
@@ -4151,6 +4162,9 @@ const ROW_SHORTCUTS = [
|
|
|
4151
4162
|
* - Click the trigger to toggle. Trigger gets `.open` (ink-active) while the menu is open.
|
|
4152
4163
|
* - Only one row menu is open at a time — opening one closes the last.
|
|
4153
4164
|
* - The menu flips above the trigger when there isn't room below it.
|
|
4165
|
+
* - The menu is fixed to the viewport, placed from the trigger's rect, so a
|
|
4166
|
+
* table's horizontal scroller cannot clip it. Scrolling any ancestor or
|
|
4167
|
+
* resizing the window closes it, rather than leaving it behind its row.
|
|
4154
4168
|
* - Click outside or press `Escape` to close.
|
|
4155
4169
|
* - Clicking an item runs its `command` then closes the menu.
|
|
4156
4170
|
* - dblclick on the trigger or menu does NOT propagate, so the data-card's
|
|
@@ -4177,6 +4191,17 @@ class EfRowActionsComponent {
|
|
|
4177
4191
|
static nextInstanceId = 0;
|
|
4178
4192
|
instanceId = EfRowActionsComponent.nextInstanceId++;
|
|
4179
4193
|
constructor() {
|
|
4194
|
+
// A row can go while its menu is open (a refresh, paging, a filter).
|
|
4195
|
+
// Close it properly so the listeners go and anything counting open
|
|
4196
|
+
// menus through the toggle event hears about it.
|
|
4197
|
+
// (Not close(): an output must not emit from a destroyed component.)
|
|
4198
|
+
inject(DestroyRef).onDestroy(() => {
|
|
4199
|
+
this.stopListeningForDisplacement();
|
|
4200
|
+
if (EfRowActionsComponent.openInstance === this)
|
|
4201
|
+
EfRowActionsComponent.openInstance = null;
|
|
4202
|
+
if (this.open())
|
|
4203
|
+
this.dispatchToggle(false);
|
|
4204
|
+
});
|
|
4180
4205
|
for (const { actionId, keys, labelKey } of ROW_SHORTCUTS) {
|
|
4181
4206
|
this.shortcuts.register({
|
|
4182
4207
|
id: `row-actions.${this.instanceId}.${actionId}`,
|
|
@@ -4254,8 +4279,13 @@ class EfRowActionsComponent {
|
|
|
4254
4279
|
// wrong direction, then correct against the real height once
|
|
4255
4280
|
// the menu is in the DOM.
|
|
4256
4281
|
this.dropUp.set(this.shouldDropUp(this.estimateMenuHeight()));
|
|
4282
|
+
this.place();
|
|
4283
|
+
this.listenForDisplacement();
|
|
4257
4284
|
this.correctPlacementAfterRender();
|
|
4258
4285
|
}
|
|
4286
|
+
else {
|
|
4287
|
+
this.stopListeningForDisplacement();
|
|
4288
|
+
}
|
|
4259
4289
|
this.dispatchToggle(isOpen);
|
|
4260
4290
|
if (isOpen)
|
|
4261
4291
|
this.opened.emit();
|
|
@@ -4272,6 +4302,7 @@ class EfRowActionsComponent {
|
|
|
4272
4302
|
close() {
|
|
4273
4303
|
if (!this.open())
|
|
4274
4304
|
return;
|
|
4305
|
+
this.stopListeningForDisplacement();
|
|
4275
4306
|
this.open.set(false);
|
|
4276
4307
|
if (EfRowActionsComponent.openInstance === this) {
|
|
4277
4308
|
EfRowActionsComponent.openInstance = null;
|
|
@@ -4289,11 +4320,142 @@ class EfRowActionsComponent {
|
|
|
4289
4320
|
* below wins, which is where it has always been.
|
|
4290
4321
|
*/
|
|
4291
4322
|
shouldDropUp(menuHeight) {
|
|
4292
|
-
const rect = this.
|
|
4293
|
-
const below =
|
|
4323
|
+
const rect = this.triggerRect();
|
|
4324
|
+
const below = EfRowActionsComponent.viewportHeight() - rect.bottom;
|
|
4294
4325
|
const above = rect.top;
|
|
4295
4326
|
return below < menuHeight + EfRowActionsComponent.EDGE_GAP && above > below;
|
|
4296
4327
|
}
|
|
4328
|
+
/** Gap between the trigger and the menu. Matches the CSS `calc(100% + 8px)`. */
|
|
4329
|
+
static MENU_GAP = 8;
|
|
4330
|
+
/**
|
|
4331
|
+
* Where the fixed menu sits, as physical offsets from the viewport
|
|
4332
|
+
* edges. The four values are always set, so they override the CSS
|
|
4333
|
+
* `top` / `bottom` / `inset-inline-*` an absolute menu uses.
|
|
4334
|
+
*/
|
|
4335
|
+
menuPosition = signal(null, ...(ngDevMode ? [{ debugName: "menuPosition" }] : /* istanbul ignore next */ []));
|
|
4336
|
+
/**
|
|
4337
|
+
* Fixed rather than absolute: inside ef-data-card the table sits in a
|
|
4338
|
+
* horizontal scroller, and `overflow-x: auto` makes the block axis clip
|
|
4339
|
+
* too, so an absolute menu was cut off at the scroller's edge. Lifting
|
|
4340
|
+
* the scroller's overflow while a menu is open would reset its
|
|
4341
|
+
* scrollLeft and spill the table across the page, exactly when the
|
|
4342
|
+
* reader has scrolled across to reach the actions column.
|
|
4343
|
+
*/
|
|
4344
|
+
place() {
|
|
4345
|
+
const rect = this.triggerRect();
|
|
4346
|
+
const vw = EfRowActionsComponent.viewportWidth();
|
|
4347
|
+
const vh = EfRowActionsComponent.viewportHeight();
|
|
4348
|
+
const gap = EfRowActionsComponent.MENU_GAP;
|
|
4349
|
+
const px = (n) => `${Math.round(n)}px`;
|
|
4350
|
+
// The menu's inline end meets the trigger's inline end (or its
|
|
4351
|
+
// inline start with menuLeft); in a right-to-left page those are
|
|
4352
|
+
// the physical left and right edges respectively.
|
|
4353
|
+
const rtl = this.isRtl();
|
|
4354
|
+
const alignRight = rtl === this.menuLeft();
|
|
4355
|
+
// A fixed menu cannot be scrolled into view (a scroll closes it), so
|
|
4356
|
+
// one taller than the room on its side scrolls inside itself instead.
|
|
4357
|
+
const room = this.dropUp() ? rect.top - gap - gap : vh - (rect.bottom + gap) - gap;
|
|
4358
|
+
this.menuPosition.set({
|
|
4359
|
+
top: this.dropUp() ? 'auto' : px(rect.bottom + gap),
|
|
4360
|
+
bottom: this.dropUp() ? px(vh - rect.top + gap) : 'auto',
|
|
4361
|
+
right: alignRight ? px(vw - rect.right) : 'auto',
|
|
4362
|
+
left: alignRight ? 'auto' : px(rect.left),
|
|
4363
|
+
maxHeight: px(Math.max(room, 120)),
|
|
4364
|
+
});
|
|
4365
|
+
}
|
|
4366
|
+
/**
|
|
4367
|
+
* An ancestor with a transform, filter or containment becomes the fixed
|
|
4368
|
+
* menu's containing block (a dialog mid-animation, for one), and the
|
|
4369
|
+
* viewport offsets land in the wrong place. Measure where the menu
|
|
4370
|
+
* actually is against where the trigger says it should be, and shift
|
|
4371
|
+
* by the difference. Also nudges it back inside the viewport.
|
|
4372
|
+
*/
|
|
4373
|
+
correctOffset(menu) {
|
|
4374
|
+
const pos = this.menuPosition();
|
|
4375
|
+
if (!pos)
|
|
4376
|
+
return;
|
|
4377
|
+
const rect = this.triggerRect();
|
|
4378
|
+
const m = menu.getBoundingClientRect();
|
|
4379
|
+
if (!m.width && !m.height)
|
|
4380
|
+
return;
|
|
4381
|
+
const gap = EfRowActionsComponent.MENU_GAP;
|
|
4382
|
+
const vw = EfRowActionsComponent.viewportWidth();
|
|
4383
|
+
const wantTop = this.dropUp() ? rect.top - gap - m.height : rect.bottom + gap;
|
|
4384
|
+
// Only meaningful once the entry animation (a translate and scale)
|
|
4385
|
+
// is over; see correctPlacementAfterRender.
|
|
4386
|
+
const alignRight = pos.right !== 'auto';
|
|
4387
|
+
let wantLeft = alignRight ? rect.right - m.width : rect.left;
|
|
4388
|
+
// Keep the whole menu on screen when the trigger hugs an edge.
|
|
4389
|
+
wantLeft = Math.min(Math.max(wantLeft, gap), Math.max(gap, vw - gap - m.width));
|
|
4390
|
+
const dx = wantLeft - m.left;
|
|
4391
|
+
const dy = wantTop - m.top;
|
|
4392
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1)
|
|
4393
|
+
return;
|
|
4394
|
+
const shift = (value, by) => value === 'auto' ? value : `${Math.round(parseFloat(value) + by)}px`;
|
|
4395
|
+
this.menuPosition.set({
|
|
4396
|
+
...pos,
|
|
4397
|
+
top: shift(pos.top, dy),
|
|
4398
|
+
bottom: shift(pos.bottom, -dy),
|
|
4399
|
+
left: shift(pos.left, dx),
|
|
4400
|
+
right: shift(pos.right, -dx),
|
|
4401
|
+
});
|
|
4402
|
+
}
|
|
4403
|
+
/** The nearest `dir` wins (the app sets it on <html>); computed style covers a page without one. */
|
|
4404
|
+
isRtl() {
|
|
4405
|
+
const el = this.host.nativeElement;
|
|
4406
|
+
const dir = el.closest('[dir]')?.getAttribute('dir');
|
|
4407
|
+
if (dir)
|
|
4408
|
+
return dir.toLowerCase() === 'rtl';
|
|
4409
|
+
return getComputedStyle(el).direction === 'rtl';
|
|
4410
|
+
}
|
|
4411
|
+
triggerRect() {
|
|
4412
|
+
const trigger = this.host.nativeElement.querySelector('.row-actions__trigger');
|
|
4413
|
+
return (trigger ?? this.host.nativeElement).getBoundingClientRect();
|
|
4414
|
+
}
|
|
4415
|
+
/** The fixed containing block: the viewport without its scrollbars. */
|
|
4416
|
+
static viewportWidth() {
|
|
4417
|
+
return document.documentElement.clientWidth || window.innerWidth;
|
|
4418
|
+
}
|
|
4419
|
+
static viewportHeight() {
|
|
4420
|
+
return document.documentElement.clientHeight || window.innerHeight;
|
|
4421
|
+
}
|
|
4422
|
+
/* ── Displacement ───────────────────────────────────────────────
|
|
4423
|
+
A fixed menu does not move with its row. Any scroll (the table's own
|
|
4424
|
+
horizontal scroller, the page, a dialog body) or a resize would leave
|
|
4425
|
+
it floating over the wrong row, so either one closes it. Scrolls
|
|
4426
|
+
inside the menu itself are not displacement. */
|
|
4427
|
+
displacementListener = null;
|
|
4428
|
+
listenForDisplacement() {
|
|
4429
|
+
if (this.displacementListener)
|
|
4430
|
+
return;
|
|
4431
|
+
// Width only: an on-screen keyboard closing (a focused cell input
|
|
4432
|
+
// blurs when the trigger is tapped) or a browser toolbar sliding
|
|
4433
|
+
// changes the height alone, and would close the menu as it opens.
|
|
4434
|
+
const openWidth = EfRowActionsComponent.viewportWidth();
|
|
4435
|
+
const listener = (event) => {
|
|
4436
|
+
if (event.type === 'resize') {
|
|
4437
|
+
if (EfRowActionsComponent.viewportWidth() !== openWidth)
|
|
4438
|
+
this.close();
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
const target = event.target;
|
|
4442
|
+
if (target instanceof Node && this.host.nativeElement.querySelector('.row-actions__menu')?.contains(target)) {
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4445
|
+
this.close();
|
|
4446
|
+
};
|
|
4447
|
+
this.displacementListener = listener;
|
|
4448
|
+
document.addEventListener('scroll', listener, { capture: true, passive: true });
|
|
4449
|
+
window.addEventListener('resize', listener, { passive: true });
|
|
4450
|
+
}
|
|
4451
|
+
stopListeningForDisplacement() {
|
|
4452
|
+
const listener = this.displacementListener;
|
|
4453
|
+
if (!listener)
|
|
4454
|
+
return;
|
|
4455
|
+
this.displacementListener = null;
|
|
4456
|
+
document.removeEventListener('scroll', listener, { capture: true });
|
|
4457
|
+
window.removeEventListener('resize', listener);
|
|
4458
|
+
}
|
|
4297
4459
|
/** Rough height before the menu exists: item padding + line box, with
|
|
4298
4460
|
* dividers and the menu's own padding and border. */
|
|
4299
4461
|
estimateMenuHeight() {
|
|
@@ -4310,11 +4472,34 @@ class EfRowActionsComponent {
|
|
|
4310
4472
|
const menu = this.host.nativeElement.querySelector('.row-actions__menu');
|
|
4311
4473
|
if (!menu)
|
|
4312
4474
|
return;
|
|
4313
|
-
|
|
4475
|
+
// scrollHeight, not offsetHeight: the first placement already
|
|
4476
|
+
// capped the menu to the room below, so its offsetHeight can
|
|
4477
|
+
// never say it does not fit there.
|
|
4478
|
+
this.dropUp.set(this.shouldDropUp(menu.scrollHeight));
|
|
4479
|
+
this.place();
|
|
4480
|
+
// The new offsets reach the DOM on the next change detection.
|
|
4481
|
+
// Measure only after the entry animation: its translate and
|
|
4482
|
+
// scale are in getBoundingClientRect, and correcting mid-flight
|
|
4483
|
+
// would lock a few pixels of the animation into the offsets.
|
|
4484
|
+
requestAnimationFrame(() => {
|
|
4485
|
+
if (!this.open())
|
|
4486
|
+
return;
|
|
4487
|
+
const running = menu.getAnimations?.() ?? [];
|
|
4488
|
+
if (!running.length) {
|
|
4489
|
+
this.correctOffset(menu);
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
Promise.all(running.map(a => a.finished))
|
|
4493
|
+
.then(() => {
|
|
4494
|
+
if (this.open())
|
|
4495
|
+
this.correctOffset(menu);
|
|
4496
|
+
})
|
|
4497
|
+
.catch(() => undefined);
|
|
4498
|
+
});
|
|
4314
4499
|
});
|
|
4315
4500
|
}
|
|
4316
|
-
/** Bubbling DOM event so ancestors
|
|
4317
|
-
*
|
|
4501
|
+
/** Bubbling DOM event so ancestors can react to a row menu opening or
|
|
4502
|
+
* closing. ef-data-card no longer needs it (the menu is fixed). */
|
|
4318
4503
|
dispatchToggle(open) {
|
|
4319
4504
|
this.host.nativeElement.dispatchEvent(new CustomEvent('ef-row-actions-toggle', {
|
|
4320
4505
|
bubbles: true,
|
|
@@ -4355,11 +4540,11 @@ class EfRowActionsComponent {
|
|
|
4355
4540
|
/** Stable trackBy across renders. */
|
|
4356
4541
|
trackByItem = (i, action) => action.id ?? (action.separator ? `__sep_${i}` : action.label ?? action.labelKey ?? i);
|
|
4357
4542
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfRowActionsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4358
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfRowActionsComponent, isStandalone: true, selector: "ef-row-actions", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, triggerIcon: { classPropertyName: "triggerIcon", publicName: "triggerIcon", isSignal: true, isRequired: false, transformFunction: null }, triggerAriaKey: { classPropertyName: "triggerAriaKey", publicName: "triggerAriaKey", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, menuLeft: { classPropertyName: "menuLeft", publicName: "menuLeft", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "opened", closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<div class=\"row-actions\">\n <button\n type=\"button\"\n class=\"row-actions__trigger\"\n [class.open]=\"open()\"\n [disabled]=\"disabled() || visibleItems().length === 0\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-label]=\"triggerAriaKey() | translate\"\n (click)=\"toggle($event)\"\n (dblclick)=\"onDblClick($event)\"\n >\n <i [class]=\"triggerIcon()\" aria-hidden=\"true\"></i>\n </button>\n\n @if (open()) {\n <div\n class=\"row-actions__menu open\"\n [class.left]=\"menuLeft()\"\n [class.up]=\"dropUp()\"\n role=\"menu\"\n [attr.aria-label]=\"triggerAriaKey() | translate\"\n (dblclick)=\"onDblClick($event)\"\n >\n @for (action of visibleItems(); track trackByItem($index, action)) {\n @if (action.separator) {\n <div class=\"row-actions__divider\" role=\"separator\"></div>\n } @else {\n <button\n type=\"button\"\n class=\"row-actions__item\"\n [class.danger]=\"action.severity === 'danger'\"\n [disabled]=\"action.disabled\"\n role=\"menuitem\"\n (click)=\"runCommand(action, $event)\"\n >\n @if (action.icon) {\n <i class=\"ico\" [class]=\"'ico ' + action.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"lbl\">\n {{ action.labelKey ? (action.labelKey | translate) : action.label }}\n </span>\n @if (action.kbd) {\n <span class=\"kbd\" aria-hidden=\"true\">{{ action.kbd }}</span>\n }\n </button>\n }\n }\n </div>\n }\n</div>\n", styles: [":host{display:inline-flex;align-items:center;justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4543
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfRowActionsComponent, isStandalone: true, selector: "ef-row-actions", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: false, transformFunction: null }, triggerIcon: { classPropertyName: "triggerIcon", publicName: "triggerIcon", isSignal: true, isRequired: false, transformFunction: null }, triggerAriaKey: { classPropertyName: "triggerAriaKey", publicName: "triggerAriaKey", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, menuLeft: { classPropertyName: "menuLeft", publicName: "menuLeft", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "opened", closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" } }, ngImport: i0, template: "<div class=\"row-actions\">\n <button\n type=\"button\"\n class=\"row-actions__trigger\"\n [class.open]=\"open()\"\n [disabled]=\"disabled() || visibleItems().length === 0\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-label]=\"triggerAriaKey() | translate\"\n (click)=\"toggle($event)\"\n (dblclick)=\"onDblClick($event)\"\n >\n <i [class]=\"triggerIcon()\" aria-hidden=\"true\"></i>\n </button>\n\n @if (open()) {\n <div\n class=\"row-actions__menu open\"\n [class.left]=\"menuLeft()\"\n [class.up]=\"dropUp()\"\n [class.is-floating]=\"menuPosition() !== null\"\n [style.top]=\"menuPosition()?.top\"\n [style.right]=\"menuPosition()?.right\"\n [style.bottom]=\"menuPosition()?.bottom\"\n [style.left]=\"menuPosition()?.left\"\n [style.max-height]=\"menuPosition()?.maxHeight\"\n role=\"menu\"\n [attr.aria-label]=\"triggerAriaKey() | translate\"\n (dblclick)=\"onDblClick($event)\"\n >\n @for (action of visibleItems(); track trackByItem($index, action)) {\n @if (action.separator) {\n <div class=\"row-actions__divider\" role=\"separator\"></div>\n } @else {\n <button\n type=\"button\"\n class=\"row-actions__item\"\n [class.danger]=\"action.severity === 'danger'\"\n [disabled]=\"action.disabled\"\n role=\"menuitem\"\n (click)=\"runCommand(action, $event)\"\n >\n @if (action.icon) {\n <i class=\"ico\" [class]=\"'ico ' + action.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"lbl\">\n {{ action.labelKey ? (action.labelKey | translate) : action.label }}\n </span>\n @if (action.kbd) {\n <span class=\"kbd\" aria-hidden=\"true\">{{ action.kbd }}</span>\n }\n </button>\n }\n }\n </div>\n }\n</div>\n", styles: [":host{display:inline-flex;align-items:center;justify-content:flex-end}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4359
4544
|
}
|
|
4360
4545
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfRowActionsComponent, decorators: [{
|
|
4361
4546
|
type: Component,
|
|
4362
|
-
args: [{ selector: 'ef-row-actions', standalone: true, imports: [CommonModule, TranslateModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"row-actions\">\n <button\n type=\"button\"\n class=\"row-actions__trigger\"\n [class.open]=\"open()\"\n [disabled]=\"disabled() || visibleItems().length === 0\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-label]=\"triggerAriaKey() | translate\"\n (click)=\"toggle($event)\"\n (dblclick)=\"onDblClick($event)\"\n >\n <i [class]=\"triggerIcon()\" aria-hidden=\"true\"></i>\n </button>\n\n @if (open()) {\n <div\n class=\"row-actions__menu open\"\n [class.left]=\"menuLeft()\"\n [class.up]=\"dropUp()\"\n role=\"menu\"\n [attr.aria-label]=\"triggerAriaKey() | translate\"\n (dblclick)=\"onDblClick($event)\"\n >\n @for (action of visibleItems(); track trackByItem($index, action)) {\n @if (action.separator) {\n <div class=\"row-actions__divider\" role=\"separator\"></div>\n } @else {\n <button\n type=\"button\"\n class=\"row-actions__item\"\n [class.danger]=\"action.severity === 'danger'\"\n [disabled]=\"action.disabled\"\n role=\"menuitem\"\n (click)=\"runCommand(action, $event)\"\n >\n @if (action.icon) {\n <i class=\"ico\" [class]=\"'ico ' + action.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"lbl\">\n {{ action.labelKey ? (action.labelKey | translate) : action.label }}\n </span>\n @if (action.kbd) {\n <span class=\"kbd\" aria-hidden=\"true\">{{ action.kbd }}</span>\n }\n </button>\n }\n }\n </div>\n }\n</div>\n", styles: [":host{display:inline-flex;align-items:center;justify-content:flex-end}\n"] }]
|
|
4547
|
+
args: [{ selector: 'ef-row-actions', standalone: true, imports: [CommonModule, TranslateModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"row-actions\">\n <button\n type=\"button\"\n class=\"row-actions__trigger\"\n [class.open]=\"open()\"\n [disabled]=\"disabled() || visibleItems().length === 0\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-label]=\"triggerAriaKey() | translate\"\n (click)=\"toggle($event)\"\n (dblclick)=\"onDblClick($event)\"\n >\n <i [class]=\"triggerIcon()\" aria-hidden=\"true\"></i>\n </button>\n\n @if (open()) {\n <div\n class=\"row-actions__menu open\"\n [class.left]=\"menuLeft()\"\n [class.up]=\"dropUp()\"\n [class.is-floating]=\"menuPosition() !== null\"\n [style.top]=\"menuPosition()?.top\"\n [style.right]=\"menuPosition()?.right\"\n [style.bottom]=\"menuPosition()?.bottom\"\n [style.left]=\"menuPosition()?.left\"\n [style.max-height]=\"menuPosition()?.maxHeight\"\n role=\"menu\"\n [attr.aria-label]=\"triggerAriaKey() | translate\"\n (dblclick)=\"onDblClick($event)\"\n >\n @for (action of visibleItems(); track trackByItem($index, action)) {\n @if (action.separator) {\n <div class=\"row-actions__divider\" role=\"separator\"></div>\n } @else {\n <button\n type=\"button\"\n class=\"row-actions__item\"\n [class.danger]=\"action.severity === 'danger'\"\n [disabled]=\"action.disabled\"\n role=\"menuitem\"\n (click)=\"runCommand(action, $event)\"\n >\n @if (action.icon) {\n <i class=\"ico\" [class]=\"'ico ' + action.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"lbl\">\n {{ action.labelKey ? (action.labelKey | translate) : action.label }}\n </span>\n @if (action.kbd) {\n <span class=\"kbd\" aria-hidden=\"true\">{{ action.kbd }}</span>\n }\n </button>\n }\n }\n </div>\n }\n</div>\n", styles: [":host{display:inline-flex;align-items:center;justify-content:flex-end}\n"] }]
|
|
4363
4548
|
}], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: false }] }], triggerIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerIcon", required: false }] }], triggerAriaKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerAriaKey", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], menuLeft: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuLeft", required: false }] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], onDocumentClick: [{
|
|
4364
4549
|
type: HostListener,
|
|
4365
4550
|
args: ['document:click', ['$event']]
|
|
@@ -5255,14 +5440,6 @@ class EfDataCardComponent {
|
|
|
5255
5440
|
// the table.
|
|
5256
5441
|
}
|
|
5257
5442
|
}
|
|
5258
|
-
/** Tracks how many `ef-row-actions` are currently open inside this
|
|
5259
|
-
* card. When > 0, `.tbl-wrap` lifts its `overflow: hidden` so the
|
|
5260
|
-
* popup can escape its rounded edges. */
|
|
5261
|
-
openMenuCount = signal(0, ...(ngDevMode ? [{ debugName: "openMenuCount" }] : /* istanbul ignore next */ []));
|
|
5262
|
-
onRowActionsToggle(event) {
|
|
5263
|
-
const detail = event.detail;
|
|
5264
|
-
this.openMenuCount.update(c => Math.max(0, c + (detail?.open ? 1 : -1)));
|
|
5265
|
-
}
|
|
5266
5443
|
/**
|
|
5267
5444
|
* Children that own their own click. Opening the row on top of them would
|
|
5268
5445
|
* run two things from one gesture.
|
|
@@ -5370,7 +5547,7 @@ class EfDataCardComponent {
|
|
|
5370
5547
|
};
|
|
5371
5548
|
trackByColumn = (_, col) => col.id;
|
|
5372
5549
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfDataCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5373
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfDataCardComponent, isStandalone: true, selector: "ef-data-card", inputs: { mobileLayout: { classPropertyName: "mobileLayout", publicName: "mobileLayout", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, trackByField: { classPropertyName: "trackByField", publicName: "trackByField", isSignal: true, isRequired: false, transformFunction: null }, pageNumber: { classPropertyName: "pageNumber", publicName: "pageNumber", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, hidePager: { classPropertyName: "hidePager", publicName: "hidePager", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, errorMsg: { classPropertyName: "errorMsg", publicName: "errorMsg", isSignal: true, isRequired: false, transformFunction: null }, loadingKey: { classPropertyName: "loadingKey", publicName: "loadingKey", isSignal: true, isRequired: false, transformFunction: null }, rowDoubleClickable: { classPropertyName: "rowDoubleClickable", publicName: "rowDoubleClickable", isSignal: true, isRequired: false, transformFunction: null }, hasActionsColumn: { classPropertyName: "hasActionsColumn", publicName: "hasActionsColumn", isSignal: true, isRequired: false, transformFunction: null }, rowActionsContext: { classPropertyName: "rowActionsContext", publicName: "rowActionsContext", isSignal: true, isRequired: false, transformFunction: null }, showViewAction: { classPropertyName: "showViewAction", publicName: "showViewAction", isSignal: true, isRequired: false, transformFunction: null }, showEditAction: { classPropertyName: "showEditAction", publicName: "showEditAction", isSignal: true, isRequired: false, transformFunction: null }, showDuplicateAction: { classPropertyName: "showDuplicateAction", publicName: "showDuplicateAction", isSignal: true, isRequired: false, transformFunction: null }, showDeleteAction: { classPropertyName: "showDeleteAction", publicName: "showDeleteAction", isSignal: true, isRequired: false, transformFunction: null }, showDensityControl: { classPropertyName: "showDensityControl", publicName: "showDensityControl", isSignal: true, isRequired: false, transformFunction: null }, showColumnPicker: { classPropertyName: "showColumnPicker", publicName: "showColumnPicker", isSignal: true, isRequired: false, transformFunction: null }, showExportControl: { classPropertyName: "showExportControl", publicName: "showExportControl", isSignal: true, isRequired: false, transformFunction: null }, exporting: { classPropertyName: "exporting", publicName: "exporting", isSignal: true, isRequired: false, transformFunction: null }, screenContext: { classPropertyName: "screenContext", publicName: "screenContext", isSignal: true, isRequired: false, transformFunction: null }, searchTerm: { classPropertyName: "searchTerm", publicName: "searchTerm", isSignal: true, isRequired: false, transformFunction: null }, tableKey: { classPropertyName: "tableKey", publicName: "tableKey", isSignal: true, isRequired: false, transformFunction: null }, defaultDensity: { classPropertyName: "defaultDensity", publicName: "defaultDensity", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, fixedLayout: { classPropertyName: "fixedLayout", publicName: "fixedLayout", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", rowDoubleClick: "rowDoubleClick", rowAction: "rowAction", exportRequest: "exportRequest", clearSearch: "clearSearch", retry: "retry", rowReorder: "rowReorder" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()", "ef-row-actions-toggle": "onRowActionsToggle($event)" } }, queries: [{ propertyName: "bodyTemplates", predicate: EfColumnTemplateDirective }, { propertyName: "headerTemplates", predicate: EfColumnHeaderTemplateDirective }], ngImport: i0, template: "<!-- One cell renderer, shared by the table, the mobile row and the mobile\n detail. A new column type is added here once, not three times. -->\n<ng-template #cellTpl let-row let-col=\"col\" let-index=\"index\">\n @if (bodyTemplate(col.id); as bodyTpl) {\n <ng-container *ngTemplateOutlet=\"bodyTpl; context: { $implicit: row, col: col, index: index }\"></ng-container>\n } @else {\n @switch (col.type) {\n @case ('number') {\n {{ $any(cellValue(row, col)) | number:numberFormat(col):col.locale }}\n }\n @case ('money') {\n {{ $any(cellValue(row, col)) | currency:(col.currencyCode || 'EUR'):(col.currencyDisplay || 'symbol'):numberFormat(col):col.locale }}\n }\n @case ('date') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy'):undefined:col.locale }}\n }\n @case ('datetime') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy HH:mm'):undefined:col.locale }}\n }\n @case ('boolean') {\n @if (cellValue(row, col)) {\n <span class=\"bool yes\"><span class=\"dot\"></span> \u2713</span>\n } @else {\n <span class=\"bool no\"><span class=\"dot\"></span> \u2014</span>\n }\n }\n @case ('mono') {\n <span class=\"ref\">{{ cellValue(row, col) }}</span>\n }\n @case ('chip') {\n <span [class]=\"'chip ' + (col.chipPrefix || 'chip-') + cellValue(row, col)\">\n \u25CF {{ cellValue(row, col) }}\n </span>\n }\n @case ('status') {\n <ef-status-chip\n [referenceKey]=\"col.referenceKey || ''\"\n [code]=\"$any(cellValue(row, col))\"\n />\n }\n @case ('reference') {\n {{ resolveReference(cellValue(row, col), col) }}\n }\n @default {\n {{ cellValue(row, col) }}\n }\n }\n }\n</ng-template>\n\n<div class=\"tbl-wrap\" [class.has-open-menu]=\"openMenuCount() > 0 || openTool() !== null\" [attr.data-density]=\"density()\">\n <!-- \u2500\u2500 HEAD ROW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"tbl-head\">\n <div class=\"count small\">\n @if (loading()) {\n <span>{{ loadingKey() | translate }}</span>\n } @else if (errorMsg()) {\n <!-- A refresh that failed on top of rows already on screen keeps the\n rows, so this chip is the only sign anything went wrong \u2014 and the\n rows it sits above may now be stale. It has to say so, and say it\n in the product's words: `errorMsg` is `err.message` from the HTTP\n layer, and \"500 Internal Server Error\" tells an operator nothing\n they can act on.\n\n The raw string stays on `data-error`, where devtools can read it\n and no reader ever meets it. `title` was the obvious place and is\n the wrong one: it renders the exception in a tooltip, which is\n precisely what this change exists to stop, and it is unreachable\n on touch besides.\n\n role=\"status\", not \"alert\": polite is right for a signal beside\n rows that are still usable, and when the body is showing the\n error state it already carries the alert \u2014 one failure must not\n interrupt a screen-reader twice. -->\n <span\n role=\"status\"\n [attr.data-error]=\"errorMsg()\"\n style=\"color: var(--st-cancelled-fg); font-weight: 600;\"\n >\u26A0 {{ 'common_error_refresh_failed' | translate }}</span\n >\n } @else {\n <ng-content select=\"[tbl-head-info]\"></ng-content>\n }\n </div>\n <div class=\"tbl-tools\">\n <ng-content select=\"[tbl-head-actions]\"></ng-content>\n\n @if (canExport()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [disabled]=\"exporting()\"\n [attr.aria-busy]=\"exporting()\"\n (click)=\"requestExport()\"\n >\n <i\n class=\"pi\"\n [class.pi-download]=\"!exporting()\"\n [class.pi-spinner]=\"exporting()\"\n [class.is-spinning]=\"exporting()\"\n aria-hidden=\"true\"\n ></i>\n {{ (exporting() ? 'common_export_running' : 'common_export') | translate }}\n </button>\n </div>\n }\n\n @if (showDensityControl()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'density'\"\n [attr.aria-expanded]=\"openTool() === 'density'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('density')\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n {{ 'common_density' | translate }}\n </button>\n @if (openTool() === 'density') {\n <div class=\"tbl-tool__menu\" role=\"menu\">\n @for (opt of densityOptions; track opt.value) {\n <button\n type=\"button\"\n role=\"menuitemradio\"\n [attr.aria-checked]=\"density() === opt.value\"\n [class.is-active]=\"density() === opt.value\"\n (click)=\"setDensity(opt.value)\"\n >\n <i class=\"pi\" [ngClass]=\"opt.icon\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ opt.labelKey | translate }}</span>\n @if (density() === opt.value) {\n <i class=\"pi pi-check tbl-tool__check\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (showColumnPicker()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'columns'\"\n [attr.aria-expanded]=\"openTool() === 'columns'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('columns')\"\n >\n <i class=\"pi pi-th-large\" aria-hidden=\"true\"></i>\n {{ 'common_columns' | translate }}\n </button>\n @if (openTool() === 'columns') {\n <div class=\"tbl-tool__menu tbl-tool__menu--wide\" role=\"menu\">\n @for (col of hideableColumns(); track col.id) {\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n [attr.aria-checked]=\"!isColumnHidden(col.id)\"\n [class.is-active]=\"!isColumnHidden(col.id)\"\n (click)=\"toggleColumn(col.id)\"\n >\n <i class=\"pi\" [class.pi-check]=\"!isColumnHidden(col.id)\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{\n col.headerKey ? (col.headerKey | translate) : (col.header || col.id)\n }}</span>\n </button>\n }\n <div class=\"tbl-tool__sep\"></div>\n <button type=\"button\" role=\"menuitem\" (click)=\"resetColumns()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ 'common_columns_reset' | translate }}</span>\n </button>\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 TABLE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isMobile()) {\n <!-- \u2500\u2500 MOBILE LIST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A six-column table on a 390px screen either overflows or\n squeezes every column to nothing, so the rows become a list.\n Same columns, same cell renderers; each column just declares\n (or is given) a role. Both shapes expand to the same detail \u2014\n they differ only in how much they show collapsed. -->\n <ul\n class=\"mlist\"\n [attr.data-layout]=\"effectiveMobileLayout()\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n >\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <li class=\"mrow\" [class.is-open]=\"isExpanded(row, i)\">\n <div\n class=\"mrow__head\"\n [class.is-tappable]=\"headIsControl()\"\n [attr.role]=\"headIsControl() ? 'button' : null\"\n [attr.tabindex]=\"headIsControl() ? 0 : null\"\n [attr.aria-expanded]=\"!rowNavigable() && hasDetail() ? isExpanded(row, i) : null\"\n (click)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.enter)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.space)=\"onRowHeadActivate(row, i, $event)\"\n >\n <div class=\"mrow__lead\">\n @if (primaryColumn(); as pc) {\n <span class=\"mrow__primary\" efOverflowTooltip efTooltip efTooltipPosition=\"top\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: pc, index: i }\"></ng-container>\n </span>\n }\n @if (effectiveMobileLayout() === 'row' && secondaryColumns().length) {\n <span class=\"mrow__secondary\">\n @for (sc of secondaryColumns(); track sc.id) {\n <span class=\"mrow__sec-part\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: sc, index: i }\"></ng-container>\n </span>\n }\n </span>\n }\n </div>\n\n <div class=\"mrow__trail\">\n @if (statusColumn(); as st) {\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: st, index: i }\"></ng-container>\n }\n @if (hasActionsColumn()) {\n <span class=\"mrow__actions\">\n <ef-row-actions [items]=\"defaultRowActions(row)\" [context]=\"rowActionsContext()\" />\n </span>\n }\n @if (hasDetail() && effectiveMobileLayout() === 'row') {\n @if (rowNavigable()) {\n <!-- The head opens the record, so expansion needs a control\n of its own. Same glyph, promoted from decoration to a\n button that carries the state it was already drawing. -->\n <button\n type=\"button\"\n class=\"mrow__disclosure\"\n [attr.aria-expanded]=\"isExpanded(row, i)\"\n [attr.aria-label]=\"(isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate\"\n (click)=\"onDisclosureToggle(row, i, $event)\"\n >\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n </button>\n } @else {\n <!-- Says the row opens, and which way it is now. Decorative:\n the whole head is the control, and it carries the state. -->\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n </div>\n </div>\n\n @if (visibleDetail(row, i).length) {\n <dl class=\"mrow__detail\">\n @for (col of visibleDetail(row, i); track col.id) {\n <div class=\"mrow__field\">\n <dt>{{ $any(col).headerKey ? ($any(col).headerKey | translate) : $any(col).header }}</dt>\n <dd [class]=\"cellAlignClass($any(col))\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"></ng-container>\n </dd>\n </div>\n }\n </dl>\n }\n\n @if (hasMoreThanPreview() && effectiveMobileLayout() === 'card') {\n <button class=\"mrow__more\" type=\"button\" (click)=\"toggleExpanded(row, i)\">\n <span>{{ (isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate }}</span>\n <i class=\"pi pi-chevron-down mrow__chevron\" [class.is-open]=\"isExpanded(row, i)\" aria-hidden=\"true\"></i>\n </button>\n }\n </li>\n } @empty {\n <!-- No rows has three meanings and they must not share a screen:\n still loading is a skeleton, the request failed is the error\n state, finished-and-empty is the empty state. Saying \"nothing\n here yet\" to someone whose data is still in flight \u2014 or whose\n request just failed \u2014 is a lie the whole time it shows. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <li class=\"mrow mrow--skeleton\">\n <ef-skeleton variant=\"text\" [rows]=\"2\" />\n </li>\n }\n } @else if (showError()) {\n <li class=\"mrow mrow--empty\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </li>\n } @else {\n <li class=\"mrow mrow--empty\">\n <ng-content select=\"[empty-state-mobile]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </li>\n }\n }\n </ul>\n } @else {\n <table class=\"tbl\" [class.tbl--fixed]=\"fixedLayout()\">\n <thead>\n <tr>\n @if (reorderable()) {\n <th class=\"tbl-drag-h\" aria-hidden=\"true\"></th>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <!-- A sortable header is a real <button> inside the <th>, not a\n click handler on the cell. The cell carries aria-sort so the\n current order is announced; the arrow stays decorative.\n Sorting was previously mouse-only and silent to assistive\n technology, on every list screen in the product. -->\n <th\n [class]=\"headerAlignClass(col)\"\n [class.is-sortable]=\"col.sortable\"\n [style.width]=\"col.width || null\"\n [attr.data-col]=\"col.id\"\n [attr.aria-sort]=\"ariaSort(col)\"\n >\n @if (col.sortable) {\n <button type=\"button\" class=\"tbl-sort\" (click)=\"onHeaderClick(col)\">\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n @if (sortIndicator(col)) {\n <span aria-hidden=\"true\" class=\"tbl-sort__arrow\">{{ sortIndicator(col) }}</span>\n }\n </button>\n } @else {\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n }\n </th>\n }\n </tr>\n </thead>\n\n <tbody [attr.aria-busy]=\"loading() ? 'true' : null\">\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <tr\n [class.is-row-clickable]=\"rowDoubleClickable()\"\n [class.is-dragging]=\"dragFrom() === i\"\n [class.is-drop-target]=\"dragFrom() !== null && dragFrom() !== i && dragTo() === i\"\n [attr.tabindex]=\"rowDoubleClickable() ? 0 : null\"\n [attr.role]=\"rowDoubleClickable() ? 'button' : null\"\n (click)=\"onRowClick(row, $event)\"\n (keydown.enter)=\"onRowKeydown(row, $event)\"\n >\n @if (reorderable()) {\n <td class=\"tbl-drag\">\n <!-- The row is draggable only by this handle, so a drag never\n starts from a cell the operator meant to click or select.\n Arrow keys move the row for anyone not using a pointer. -->\n <button\n type=\"button\"\n class=\"tbl-drag__grip\"\n [attr.aria-label]=\"'common_reorder_row' | translate\"\n (pointerdown)=\"onDragPointerDown(i, $event)\"\n (pointermove)=\"onDragPointerMove($event)\"\n (pointerup)=\"onDragPointerUp()\"\n (pointercancel)=\"onDragPointerUp()\"\n (keydown)=\"onDragKeydown(i, $event)\"\n >\n <i class=\"pi pi-ellipsis-v\" aria-hidden=\"true\"></i>\n </button>\n </td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col); let colIdx = $index) {\n <td efOverflowTooltip efTooltip efTooltipPosition=\"top\" [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n @if (col.id === 'actions' && hasActionsColumn()) {\n <!-- Auto row-actions cell: View / Edit / Duplicate / Delete,\n gated by [show*Action] flags + ScreenContext perms. -->\n <ef-row-actions\n [items]=\"defaultRowActions(row)\"\n [context]=\"rowActionsContext()\"\n />\n } @else {\n <ng-container\n *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"\n ></ng-container>\n }\n </td>\n }\n </tr>\n } @empty {\n <!-- Placeholder rows keep the real columns, so the table that\n arrives lands where the shimmer already was instead of\n reflowing the page under the reader's eyes. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <tr>\n @if (reorderable()) {\n <td class=\"tbl-drag\"></td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <td [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n <ef-skeleton variant=\"text\" [rows]=\"1\" />\n </td>\n }\n </tr>\n }\n } @else if (showError()) {\n <!-- The failure replaces the body, so it spans the full row \u2014\n crammed into column one it reads as a value, not a state. -->\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </td>\n </tr>\n } @else {\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-content select=\"[empty-state]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n }\n\n <!-- \u2500\u2500 PAGER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Paging controls for an empty result set are chrome with nothing to\n page, so they stay out of the way until there is something to move\n through. -->\n @if (!hidePager() && rows().length > 0) {\n <ef-pager\n [pageNumber]=\"pageNumber()\"\n [pageSize]=\"pageSize()\"\n [totalCount]=\"totalCount()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n (pageChange)=\"pageChange.emit($event)\"\n (pageSizeChange)=\"pageSizeChange.emit($event)\"\n />\n }\n</div>\n\n<!-- \u2500\u2500 DEFAULT EMPTY STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Rendered when a screen projects nothing into [empty-state] /\n [empty-state-mobile]. It words itself from `searchTerm`, because\n \"nothing matched what you typed\" and \"there is nothing here yet\"\n are different situations needing different ways out. -->\n<ng-template #defaultEmptyTpl>\n <div class=\"tbl-empty\">\n <i\n class=\"pi tbl-empty__icon\"\n [class.pi-search]=\"!!searchTerm()\"\n [class.pi-inbox]=\"!searchTerm()\"\n aria-hidden=\"true\"\n ></i>\n\n @if (searchTerm()) {\n <p class=\"tbl-empty__title\">\n {{ 'common_empty_no_results' | translate: { term: searchTerm() } }}\n </p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_no_results_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"clearSearch.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n {{ 'common_empty_clear_filters' | translate }}\n </button>\n } @else {\n <p class=\"tbl-empty__title\">{{ 'common_empty_none_yet' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_none_yet_hint' | translate }}</p>\n }\n </div>\n</ng-template>\n\n<!-- \u2500\u2500 ERROR STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A request that failed and came back with nothing is not an empty\n collection, and must never borrow its words: \"nothing here yet\" is\n a confident false statement about the operator's data, and they may\n act on it. So this replaces the empty state outright, projected\n [empty-state] included \u2014 a screen's own \"create your first client\"\n copy is just as wrong here as the default.\n\n It says nothing about *what* failed. `errorMsg` is `err.message`\n from the HTTP layer, and a status line or an exception string tells\n an operator nothing they can act on; it survives on the head chip's\n `data-error` attribute, for whoever is reading the DOM. This carries\n the two things that help: their data is intact, and the way to try\n again.\n\n role=\"alert\" goes on this div, never on the <tr> / <td> / <li> that\n hosts it \u2014 a role on a row or cell takes the table's semantics down\n with it. It fires on insertion, which is exactly when @if adds it. -->\n<ng-template #errorStateTpl>\n <div class=\"tbl-empty tbl-empty--error\" role=\"alert\">\n <i class=\"pi pi-exclamation-triangle tbl-empty__icon\" aria-hidden=\"true\"></i>\n <p class=\"tbl-empty__title\">{{ 'common_error_load_failed' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_error_load_failed_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"retry.emit()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n {{ 'common_retry' | translate }}\n </button>\n </div>\n</ng-template>\n\n<!-- Header label, shared by the sortable and plain header cells so the\n projection rules stay in one place. -->\n<ng-template #headerLabelTpl let-col=\"col\">\n @if (headerTemplate(col.id); as headerTpl) {\n <ng-container *ngTemplateOutlet=\"headerTpl; context: { $implicit: col, col: col }\"></ng-container>\n } @else if (col.headerKey) {\n <span>{{ col.headerKey | translate }}</span>\n } @else if (col.header) {\n <span>{{ col.header }}</span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";.tbl thead th.is-sortable{cursor:pointer;-webkit-user-select:none;user-select:none;transition:color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl thead th.is-sortable:hover{color:var(--text)}.tbl tbody tr.is-row-clickable{cursor:pointer}.tbl-tools{display:flex;gap:6px;align-items:center}.tbl-tool{position:relative}.tbl-tool>.btn.open{color:var(--text);background:var(--paper)}.tbl-tool__menu{position:absolute;top:calc(100% + 8px);inset-inline-end:0;z-index:40;min-width:212px;max-height:340px;overflow-y:auto;padding:6px;display:grid;gap:1px;background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-xl);box-shadow:var(--shadow-3);animation:row-actions-menu-in var(--t-base) var(--ease-spring)}.tbl-tool__menu--wide{min-width:248px}.tbl-tool__menu button{appearance:none;background:transparent;border:0;width:100%;text-align:start;font:inherit;font-size:13.5px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:var(--r-md);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out)}.tbl-tool__menu button:hover,.tbl-tool__menu button:focus-visible{background:var(--paper);outline:0}.tbl-tool__menu button>.pi{width:16px;flex:none;font-size:14px;color:var(--muted)}.tbl-tool__menu button.is-active>.pi{color:var(--module, var(--text))}.tbl-tool__label{flex:1 1 auto;min-width:0}.tbl-tool__check{color:var(--module, var(--text))}.tbl-tool__sep{margin:5px 2px;border-top:1px solid var(--rule)}.tbl-wrap[data-density=compact] .tbl tbody td{--tbl-cell-pad-block: 4px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.tbl-wrap[data-density=comfortable] .tbl tbody td{--tbl-cell-pad-block: 16px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.mlist{list-style:none;margin:0;padding:0;display:grid}.mrow{border-top:1px solid var(--rule)}.mrow:first-child{border-top:0}.mrow__head{display:flex;align-items:flex-start;gap:var(--s-3);padding:12px var(--s-4);min-height:var(--hit-touch)}.mrow__head.is-tappable{cursor:pointer}.mrow__head.is-tappable:active{background:var(--paper)}.mrow__lead{flex:1;min-width:0;display:grid;gap:2px}.mrow__primary{font-weight:700;font-size:14px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mrow__secondary{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 6px;font-size:12.5px;color:var(--text-mute);min-width:0}.mrow__secondary .mrow__sec-part+.mrow__sec-part:before{content:\"\\b7\";margin-inline-end:6px;color:var(--text-soft)}.mrow__trail{display:flex;align-items:center;justify-content:flex-end;gap:var(--s-2);flex-shrink:0;min-height:20px}.mrow__actions{display:inline-flex}.mrow__detail{margin:0;padding:0 var(--s-4) 12px;display:grid;gap:1px}.mrow__field{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-3);padding:7px 0;border-top:1px solid var(--rule-soft)}.mrow__field dt{font-size:11.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-mute);flex-shrink:0}.mrow__field dd{margin:0;font-size:13.5px;color:var(--text);text-align:end;min-width:0;overflow-wrap:anywhere}.mrow__more{appearance:none;width:100%;background:transparent;border:0;border-top:1px solid var(--rule-soft);padding:10px;font:inherit;font-size:13px;font-weight:600;color:var(--tenant-500);cursor:pointer}.mlist[data-layout=card]{gap:var(--s-3);padding:var(--s-3) var(--s-3) 0}.mlist[data-layout=card] .mrow{border:1px solid var(--rule);border-radius:var(--r-lg);background:var(--paper-alt);overflow:hidden}.mlist[data-layout=card] .mrow__head{border-bottom:1px solid var(--rule)}.mrow__chevron{font-size:11px;color:var(--text-soft);transition:transform var(--t-fast) var(--ease-out);flex-shrink:0}.mrow__chevron.is-open{transform:rotate(180deg);color:var(--text-mute)}.mrow__head.is-tappable:focus-visible{outline:2px solid var(--tenant-400);outline-offset:-2px}.mrow__more{display:flex;align-items:center;justify-content:center;gap:6px}.tbl-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s-2);padding:var(--s-9) var(--s-4);text-align:center}.tbl-empty__icon{font-size:28px;line-height:1;color:var(--ink-400);margin-block-end:var(--s-1)}.tbl-empty__title{margin:0;font-size:15px;font-weight:600;color:var(--text);max-inline-size:46ch;overflow-wrap:break-word}.tbl-empty__hint{margin:0;font-size:13px;color:var(--ink-600);max-inline-size:52ch;text-wrap:pretty}.tbl-empty__action{margin-block-start:var(--s-3);min-block-size:var(--hit-base)}.tbl-empty--error .tbl-empty__icon{color:var(--st-cancelled-fg)}.mrow--empty{padding:0}.mrow--skeleton{padding:12px var(--s-4)}@media(max-width:767px){.tbl-empty{padding:var(--s-7) var(--s-4)}.tbl-empty__action{min-block-size:var(--hit-touch)}}.pi.is-spinning{animation:ef-export-spin .9s linear infinite}@keyframes ef-export-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.pi.is-spinning{animation:none}}.tbl th.tbl-drag-h,.tbl td.tbl-drag{width:28px;padding-inline:4px 0;text-align:center}.tbl td.tbl-drag{cursor:default}.tbl .tbl-drag__grip{display:inline-flex;align-items:center;justify-content:center;width:22px;height:26px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--text-soft);cursor:grab;touch-action:none;transition:background var(--t-fast, .14s) var(--ease-out, ease-out),color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl .tbl-drag__grip:hover{background:var(--paper);color:var(--text)}.tbl .tbl-drag__grip:focus-visible{outline:2px solid var(--tenant, var(--rule));outline-offset:1px}.tbl .tbl-drag__grip:active{cursor:grabbing}.tbl .tbl-drag__grip i{font-size:12px}.tbl tbody tr.is-dragging>td{background:var(--paper);opacity:.55}.tbl tbody tr.is-drop-target>td{box-shadow:inset 0 2px 0 var(--tenant, var(--rule))}.tbl.tbl--fixed{table-layout:fixed}.mrow__disclosure{appearance:none;background:transparent;border:0;padding:0;margin:0;display:inline-grid;place-items:center;color:var(--text-mute);cursor:pointer;position:relative}.mrow__disclosure:after{content:\"\";position:absolute;top:50%;left:50%;width:var(--hit-touch);height:var(--hit-touch);transform:translate(-50%,-50%)}.mrow__disclosure:focus-visible{outline:2px solid color-mix(in srgb,var(--ink-active) 40%,transparent);outline-offset:2px;border-radius:var(--r-pill)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfPagerComponent, selector: "ef-pager", inputs: ["pageNumber", "pageSize", "totalCount", "pageSizeOptions", "linesPerPageLabelKey", "ofLabelKey", "prevLabelKey", "nextLabelKey", "hideSize"], outputs: ["pageChange", "pageSizeChange"] }, { kind: "component", type: EfStatusChipComponent, selector: "ef-status-chip", inputs: ["referenceKey", "code", "color", "label", "labelKey", "hideDot", "showCode"] }, { kind: "component", type: EfSkeletonComponent, selector: "ef-skeleton", inputs: ["variant", "rows", "height"] }, { kind: "component", type: EfRowActionsComponent, selector: "ef-row-actions", inputs: ["items", "context", "triggerIcon", "triggerAriaKey", "disabled", "menuLeft"], outputs: ["opened", "closed"] }, { kind: "directive", type: EfOverflowTooltipDirective, selector: "[efOverflowTooltip]" }, { kind: "directive", type: EfTooltipDirective, selector: "[efTooltip]" }, { kind: "pipe", type: i1$3.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$3.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$3.DatePipe, name: "date" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5550
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfDataCardComponent, isStandalone: true, selector: "ef-data-card", inputs: { mobileLayout: { classPropertyName: "mobileLayout", publicName: "mobileLayout", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, trackByField: { classPropertyName: "trackByField", publicName: "trackByField", isSignal: true, isRequired: false, transformFunction: null }, pageNumber: { classPropertyName: "pageNumber", publicName: "pageNumber", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, hidePager: { classPropertyName: "hidePager", publicName: "hidePager", isSignal: true, isRequired: false, transformFunction: null }, sort: { classPropertyName: "sort", publicName: "sort", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, errorMsg: { classPropertyName: "errorMsg", publicName: "errorMsg", isSignal: true, isRequired: false, transformFunction: null }, loadingKey: { classPropertyName: "loadingKey", publicName: "loadingKey", isSignal: true, isRequired: false, transformFunction: null }, rowDoubleClickable: { classPropertyName: "rowDoubleClickable", publicName: "rowDoubleClickable", isSignal: true, isRequired: false, transformFunction: null }, hasActionsColumn: { classPropertyName: "hasActionsColumn", publicName: "hasActionsColumn", isSignal: true, isRequired: false, transformFunction: null }, rowActionsContext: { classPropertyName: "rowActionsContext", publicName: "rowActionsContext", isSignal: true, isRequired: false, transformFunction: null }, showViewAction: { classPropertyName: "showViewAction", publicName: "showViewAction", isSignal: true, isRequired: false, transformFunction: null }, showEditAction: { classPropertyName: "showEditAction", publicName: "showEditAction", isSignal: true, isRequired: false, transformFunction: null }, showDuplicateAction: { classPropertyName: "showDuplicateAction", publicName: "showDuplicateAction", isSignal: true, isRequired: false, transformFunction: null }, showDeleteAction: { classPropertyName: "showDeleteAction", publicName: "showDeleteAction", isSignal: true, isRequired: false, transformFunction: null }, showDensityControl: { classPropertyName: "showDensityControl", publicName: "showDensityControl", isSignal: true, isRequired: false, transformFunction: null }, showColumnPicker: { classPropertyName: "showColumnPicker", publicName: "showColumnPicker", isSignal: true, isRequired: false, transformFunction: null }, showExportControl: { classPropertyName: "showExportControl", publicName: "showExportControl", isSignal: true, isRequired: false, transformFunction: null }, exporting: { classPropertyName: "exporting", publicName: "exporting", isSignal: true, isRequired: false, transformFunction: null }, screenContext: { classPropertyName: "screenContext", publicName: "screenContext", isSignal: true, isRequired: false, transformFunction: null }, searchTerm: { classPropertyName: "searchTerm", publicName: "searchTerm", isSignal: true, isRequired: false, transformFunction: null }, tableKey: { classPropertyName: "tableKey", publicName: "tableKey", isSignal: true, isRequired: false, transformFunction: null }, defaultDensity: { classPropertyName: "defaultDensity", publicName: "defaultDensity", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null }, fixedLayout: { classPropertyName: "fixedLayout", publicName: "fixedLayout", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pageChange: "pageChange", pageSizeChange: "pageSizeChange", sortChange: "sortChange", rowDoubleClick: "rowDoubleClick", rowAction: "rowAction", exportRequest: "exportRequest", clearSearch: "clearSearch", retry: "retry", rowReorder: "rowReorder" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" } }, queries: [{ propertyName: "bodyTemplates", predicate: EfColumnTemplateDirective }, { propertyName: "headerTemplates", predicate: EfColumnHeaderTemplateDirective }], ngImport: i0, template: "<!-- One cell renderer, shared by the table, the mobile row and the mobile\n detail. A new column type is added here once, not three times. -->\n<ng-template #cellTpl let-row let-col=\"col\" let-index=\"index\">\n @if (bodyTemplate(col.id); as bodyTpl) {\n <ng-container *ngTemplateOutlet=\"bodyTpl; context: { $implicit: row, col: col, index: index }\"></ng-container>\n } @else {\n @switch (col.type) {\n @case ('number') {\n {{ $any(cellValue(row, col)) | number:numberFormat(col):col.locale }}\n }\n @case ('money') {\n {{ $any(cellValue(row, col)) | currency:(col.currencyCode || 'EUR'):(col.currencyDisplay || 'symbol'):numberFormat(col):col.locale }}\n }\n @case ('date') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy'):undefined:col.locale }}\n }\n @case ('datetime') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy HH:mm'):undefined:col.locale }}\n }\n @case ('boolean') {\n @if (cellValue(row, col)) {\n <span class=\"bool yes\"><span class=\"dot\"></span> \u2713</span>\n } @else {\n <span class=\"bool no\"><span class=\"dot\"></span> \u2014</span>\n }\n }\n @case ('mono') {\n <span class=\"ref\">{{ cellValue(row, col) }}</span>\n }\n @case ('chip') {\n <span [class]=\"'chip ' + (col.chipPrefix || 'chip-') + cellValue(row, col)\">\n \u25CF {{ cellValue(row, col) }}\n </span>\n }\n @case ('status') {\n <ef-status-chip\n [referenceKey]=\"col.referenceKey || ''\"\n [code]=\"$any(cellValue(row, col))\"\n />\n }\n @case ('reference') {\n {{ resolveReference(cellValue(row, col), col) }}\n }\n @default {\n {{ cellValue(row, col) }}\n }\n }\n }\n</ng-template>\n\n<!-- .has-open-menu unclips the shell for the Densit\u00E9 / Colonnes menus,\n which hang below .tbl-head. Row menus are fixed to the viewport\n (ef-row-actions) and need no lift; counting them also left the shell\n unclipped for good when a row went while its menu was open. -->\n<div class=\"tbl-wrap\" [class.has-open-menu]=\"openTool() !== null\" [attr.data-density]=\"density()\">\n <!-- \u2500\u2500 HEAD ROW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"tbl-head\">\n <div class=\"count small\">\n @if (loading()) {\n <span>{{ loadingKey() | translate }}</span>\n } @else if (errorMsg()) {\n <!-- A refresh that failed on top of rows already on screen keeps the\n rows, so this chip is the only sign anything went wrong \u2014 and the\n rows it sits above may now be stale. It has to say so, and say it\n in the product's words: `errorMsg` is `err.message` from the HTTP\n layer, and \"500 Internal Server Error\" tells an operator nothing\n they can act on.\n\n The raw string stays on `data-error`, where devtools can read it\n and no reader ever meets it. `title` was the obvious place and is\n the wrong one: it renders the exception in a tooltip, which is\n precisely what this change exists to stop, and it is unreachable\n on touch besides.\n\n role=\"status\", not \"alert\": polite is right for a signal beside\n rows that are still usable, and when the body is showing the\n error state it already carries the alert \u2014 one failure must not\n interrupt a screen-reader twice. -->\n <span\n role=\"status\"\n [attr.data-error]=\"errorMsg()\"\n style=\"color: var(--st-cancelled-fg); font-weight: 600;\"\n >\u26A0 {{ 'common_error_refresh_failed' | translate }}</span\n >\n } @else {\n <ng-content select=\"[tbl-head-info]\"></ng-content>\n }\n </div>\n <div class=\"tbl-tools\">\n <ng-content select=\"[tbl-head-actions]\"></ng-content>\n\n @if (canExport()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [disabled]=\"exporting()\"\n [attr.aria-busy]=\"exporting()\"\n (click)=\"requestExport()\"\n >\n <i\n class=\"pi\"\n [class.pi-download]=\"!exporting()\"\n [class.pi-spinner]=\"exporting()\"\n [class.is-spinning]=\"exporting()\"\n aria-hidden=\"true\"\n ></i>\n {{ (exporting() ? 'common_export_running' : 'common_export') | translate }}\n </button>\n </div>\n }\n\n @if (showDensityControl()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'density'\"\n [attr.aria-expanded]=\"openTool() === 'density'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('density')\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n {{ 'common_density' | translate }}\n </button>\n @if (openTool() === 'density') {\n <div class=\"tbl-tool__menu\" role=\"menu\">\n @for (opt of densityOptions; track opt.value) {\n <button\n type=\"button\"\n role=\"menuitemradio\"\n [attr.aria-checked]=\"density() === opt.value\"\n [class.is-active]=\"density() === opt.value\"\n (click)=\"setDensity(opt.value)\"\n >\n <i class=\"pi\" [ngClass]=\"opt.icon\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ opt.labelKey | translate }}</span>\n @if (density() === opt.value) {\n <i class=\"pi pi-check tbl-tool__check\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (showColumnPicker()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'columns'\"\n [attr.aria-expanded]=\"openTool() === 'columns'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('columns')\"\n >\n <i class=\"pi pi-th-large\" aria-hidden=\"true\"></i>\n {{ 'common_columns' | translate }}\n </button>\n @if (openTool() === 'columns') {\n <div class=\"tbl-tool__menu tbl-tool__menu--wide\" role=\"menu\">\n @for (col of hideableColumns(); track col.id) {\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n [attr.aria-checked]=\"!isColumnHidden(col.id)\"\n [class.is-active]=\"!isColumnHidden(col.id)\"\n (click)=\"toggleColumn(col.id)\"\n >\n <i class=\"pi\" [class.pi-check]=\"!isColumnHidden(col.id)\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{\n col.headerKey ? (col.headerKey | translate) : (col.header || col.id)\n }}</span>\n </button>\n }\n <div class=\"tbl-tool__sep\"></div>\n <button type=\"button\" role=\"menuitem\" (click)=\"resetColumns()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ 'common_columns_reset' | translate }}</span>\n </button>\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 TABLE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isMobile()) {\n <!-- \u2500\u2500 MOBILE LIST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A six-column table on a 390px screen either overflows or\n squeezes every column to nothing, so the rows become a list.\n Same columns, same cell renderers; each column just declares\n (or is given) a role. Both shapes expand to the same detail \u2014\n they differ only in how much they show collapsed. -->\n <ul\n class=\"mlist\"\n [attr.data-layout]=\"effectiveMobileLayout()\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n >\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <li class=\"mrow\" [class.is-open]=\"isExpanded(row, i)\">\n <div\n class=\"mrow__head\"\n [class.is-tappable]=\"headIsControl()\"\n [attr.role]=\"headIsControl() ? 'button' : null\"\n [attr.tabindex]=\"headIsControl() ? 0 : null\"\n [attr.aria-expanded]=\"!rowNavigable() && hasDetail() ? isExpanded(row, i) : null\"\n (click)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.enter)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.space)=\"onRowHeadActivate(row, i, $event)\"\n >\n <div class=\"mrow__lead\">\n @if (primaryColumn(); as pc) {\n <span class=\"mrow__primary\" efOverflowTooltip efTooltip efTooltipPosition=\"top\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: pc, index: i }\"></ng-container>\n </span>\n }\n @if (effectiveMobileLayout() === 'row' && secondaryColumns().length) {\n <span class=\"mrow__secondary\">\n @for (sc of secondaryColumns(); track sc.id) {\n <span class=\"mrow__sec-part\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: sc, index: i }\"></ng-container>\n </span>\n }\n </span>\n }\n </div>\n\n <div class=\"mrow__trail\">\n @if (statusColumn(); as st) {\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: st, index: i }\"></ng-container>\n }\n @if (hasActionsColumn()) {\n <span class=\"mrow__actions\">\n <ef-row-actions [items]=\"defaultRowActions(row)\" [context]=\"rowActionsContext()\" />\n </span>\n }\n @if (hasDetail() && effectiveMobileLayout() === 'row') {\n @if (rowNavigable()) {\n <!-- The head opens the record, so expansion needs a control\n of its own. Same glyph, promoted from decoration to a\n button that carries the state it was already drawing. -->\n <button\n type=\"button\"\n class=\"mrow__disclosure\"\n [attr.aria-expanded]=\"isExpanded(row, i)\"\n [attr.aria-label]=\"(isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate\"\n (click)=\"onDisclosureToggle(row, i, $event)\"\n >\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n </button>\n } @else {\n <!-- Says the row opens, and which way it is now. Decorative:\n the whole head is the control, and it carries the state. -->\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n </div>\n </div>\n\n @if (visibleDetail(row, i).length) {\n <dl class=\"mrow__detail\">\n @for (col of visibleDetail(row, i); track col.id) {\n <div class=\"mrow__field\">\n <dt>{{ $any(col).headerKey ? ($any(col).headerKey | translate) : $any(col).header }}</dt>\n <dd [class]=\"cellAlignClass($any(col))\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"></ng-container>\n </dd>\n </div>\n }\n </dl>\n }\n\n @if (hasMoreThanPreview() && effectiveMobileLayout() === 'card') {\n <button class=\"mrow__more\" type=\"button\" (click)=\"toggleExpanded(row, i)\">\n <span>{{ (isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate }}</span>\n <i class=\"pi pi-chevron-down mrow__chevron\" [class.is-open]=\"isExpanded(row, i)\" aria-hidden=\"true\"></i>\n </button>\n }\n </li>\n } @empty {\n <!-- No rows has three meanings and they must not share a screen:\n still loading is a skeleton, the request failed is the error\n state, finished-and-empty is the empty state. Saying \"nothing\n here yet\" to someone whose data is still in flight \u2014 or whose\n request just failed \u2014 is a lie the whole time it shows. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <li class=\"mrow mrow--skeleton\">\n <ef-skeleton variant=\"text\" [rows]=\"2\" />\n </li>\n }\n } @else if (showError()) {\n <li class=\"mrow mrow--empty\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </li>\n } @else {\n <li class=\"mrow mrow--empty\">\n <ng-content select=\"[empty-state-mobile]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </li>\n }\n }\n </ul>\n } @else {\n <!-- The table scrolls sideways inside its own box. Between 768px and\n the table's natural width (about 900-1100px) the columns past the\n edge were simply clipped by .tbl-wrap, with no way to reach them.\n .tbl-wrap stays the clipping shell, so the head and the pager do\n not scroll with the rows. Row menus are fixed to the viewport\n (ef-row-actions), so this scroller never clips them. -->\n <div class=\"tbl-scroll\">\n <table class=\"tbl\" [class.tbl--fixed]=\"fixedLayout()\">\n <thead>\n <tr>\n @if (reorderable()) {\n <th class=\"tbl-drag-h\" aria-hidden=\"true\"></th>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <!-- A sortable header is a real <button> inside the <th>, not a\n click handler on the cell. The cell carries aria-sort so the\n current order is announced; the arrow stays decorative.\n Sorting was previously mouse-only and silent to assistive\n technology, on every list screen in the product. -->\n <th\n [class]=\"headerAlignClass(col)\"\n [class.is-sortable]=\"col.sortable\"\n [style.width]=\"col.width || null\"\n [attr.data-col]=\"col.id\"\n [attr.aria-sort]=\"ariaSort(col)\"\n >\n @if (col.sortable) {\n <button type=\"button\" class=\"tbl-sort\" (click)=\"onHeaderClick(col)\">\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n @if (sortIndicator(col)) {\n <span aria-hidden=\"true\" class=\"tbl-sort__arrow\">{{ sortIndicator(col) }}</span>\n }\n </button>\n } @else {\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n }\n </th>\n }\n </tr>\n </thead>\n\n <tbody [attr.aria-busy]=\"loading() ? 'true' : null\">\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <tr\n [class.is-row-clickable]=\"rowDoubleClickable()\"\n [class.is-dragging]=\"dragFrom() === i\"\n [class.is-drop-target]=\"dragFrom() !== null && dragFrom() !== i && dragTo() === i\"\n [attr.tabindex]=\"rowDoubleClickable() ? 0 : null\"\n [attr.role]=\"rowDoubleClickable() ? 'button' : null\"\n (click)=\"onRowClick(row, $event)\"\n (keydown.enter)=\"onRowKeydown(row, $event)\"\n >\n @if (reorderable()) {\n <td class=\"tbl-drag\">\n <!-- The row is draggable only by this handle, so a drag never\n starts from a cell the operator meant to click or select.\n Arrow keys move the row for anyone not using a pointer. -->\n <button\n type=\"button\"\n class=\"tbl-drag__grip\"\n [attr.aria-label]=\"'common_reorder_row' | translate\"\n (pointerdown)=\"onDragPointerDown(i, $event)\"\n (pointermove)=\"onDragPointerMove($event)\"\n (pointerup)=\"onDragPointerUp()\"\n (pointercancel)=\"onDragPointerUp()\"\n (keydown)=\"onDragKeydown(i, $event)\"\n >\n <i class=\"pi pi-ellipsis-v\" aria-hidden=\"true\"></i>\n </button>\n </td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col); let colIdx = $index) {\n <td efOverflowTooltip efTooltip efTooltipPosition=\"top\" [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n @if (col.id === 'actions' && hasActionsColumn()) {\n <!-- Auto row-actions cell: View / Edit / Duplicate / Delete,\n gated by [show*Action] flags + ScreenContext perms. -->\n <ef-row-actions\n [items]=\"defaultRowActions(row)\"\n [context]=\"rowActionsContext()\"\n />\n } @else {\n <ng-container\n *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"\n ></ng-container>\n }\n </td>\n }\n </tr>\n } @empty {\n <!-- Placeholder rows keep the real columns, so the table that\n arrives lands where the shimmer already was instead of\n reflowing the page under the reader's eyes. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <tr>\n @if (reorderable()) {\n <td class=\"tbl-drag\"></td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <td [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n <ef-skeleton variant=\"text\" [rows]=\"1\" />\n </td>\n }\n </tr>\n }\n } @else if (showError()) {\n <!-- The failure replaces the body, so it spans the full row \u2014\n crammed into column one it reads as a value, not a state. -->\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </td>\n </tr>\n } @else {\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-content select=\"[empty-state]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- \u2500\u2500 PAGER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Paging controls for an empty result set are chrome with nothing to\n page, so they stay out of the way until there is something to move\n through. -->\n @if (!hidePager() && rows().length > 0) {\n <ef-pager\n [pageNumber]=\"pageNumber()\"\n [pageSize]=\"pageSize()\"\n [totalCount]=\"totalCount()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n (pageChange)=\"pageChange.emit($event)\"\n (pageSizeChange)=\"pageSizeChange.emit($event)\"\n />\n }\n</div>\n\n<!-- \u2500\u2500 DEFAULT EMPTY STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Rendered when a screen projects nothing into [empty-state] /\n [empty-state-mobile]. It words itself from `searchTerm`, because\n \"nothing matched what you typed\" and \"there is nothing here yet\"\n are different situations needing different ways out. -->\n<ng-template #defaultEmptyTpl>\n <div class=\"tbl-empty\">\n <i\n class=\"pi tbl-empty__icon\"\n [class.pi-search]=\"!!searchTerm()\"\n [class.pi-inbox]=\"!searchTerm()\"\n aria-hidden=\"true\"\n ></i>\n\n @if (searchTerm()) {\n <p class=\"tbl-empty__title\">\n {{ 'common_empty_no_results' | translate: { term: searchTerm() } }}\n </p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_no_results_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"clearSearch.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n {{ 'common_empty_clear_filters' | translate }}\n </button>\n } @else {\n <p class=\"tbl-empty__title\">{{ 'common_empty_none_yet' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_none_yet_hint' | translate }}</p>\n }\n </div>\n</ng-template>\n\n<!-- \u2500\u2500 ERROR STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A request that failed and came back with nothing is not an empty\n collection, and must never borrow its words: \"nothing here yet\" is\n a confident false statement about the operator's data, and they may\n act on it. So this replaces the empty state outright, projected\n [empty-state] included \u2014 a screen's own \"create your first client\"\n copy is just as wrong here as the default.\n\n It says nothing about *what* failed. `errorMsg` is `err.message`\n from the HTTP layer, and a status line or an exception string tells\n an operator nothing they can act on; it survives on the head chip's\n `data-error` attribute, for whoever is reading the DOM. This carries\n the two things that help: their data is intact, and the way to try\n again.\n\n role=\"alert\" goes on this div, never on the <tr> / <td> / <li> that\n hosts it \u2014 a role on a row or cell takes the table's semantics down\n with it. It fires on insertion, which is exactly when @if adds it. -->\n<ng-template #errorStateTpl>\n <div class=\"tbl-empty tbl-empty--error\" role=\"alert\">\n <i class=\"pi pi-exclamation-triangle tbl-empty__icon\" aria-hidden=\"true\"></i>\n <p class=\"tbl-empty__title\">{{ 'common_error_load_failed' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_error_load_failed_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"retry.emit()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n {{ 'common_retry' | translate }}\n </button>\n </div>\n</ng-template>\n\n<!-- Header label, shared by the sortable and plain header cells so the\n projection rules stay in one place. -->\n<ng-template #headerLabelTpl let-col=\"col\">\n @if (headerTemplate(col.id); as headerTpl) {\n <ng-container *ngTemplateOutlet=\"headerTpl; context: { $implicit: col, col: col }\"></ng-container>\n } @else if (col.headerKey) {\n <span>{{ col.headerKey | translate }}</span>\n } @else if (col.header) {\n <span>{{ col.header }}</span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;min-width:0}.tbl thead th.is-sortable{cursor:pointer;-webkit-user-select:none;user-select:none;transition:color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl thead th.is-sortable:hover{color:var(--text)}.tbl tbody tr.is-row-clickable{cursor:pointer}.tbl-tools{display:flex;gap:6px;align-items:center}.tbl-tool{position:relative}.tbl-tool>.btn.open{color:var(--text);background:var(--paper)}.tbl-tool__menu{position:absolute;top:calc(100% + 8px);inset-inline-end:0;z-index:40;min-width:212px;max-height:340px;overflow-y:auto;padding:6px;display:grid;gap:1px;background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-xl);box-shadow:var(--shadow-3);animation:row-actions-menu-in var(--t-base) var(--ease-spring)}.tbl-tool__menu--wide{min-width:248px}.tbl-tool__menu button{appearance:none;background:transparent;border:0;width:100%;text-align:start;font:inherit;font-size:13.5px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:var(--r-md);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out)}.tbl-tool__menu button:hover,.tbl-tool__menu button:focus-visible{background:var(--paper);outline:0}.tbl-tool__menu button>.pi{width:16px;flex:none;font-size:14px;color:var(--muted)}.tbl-tool__menu button.is-active>.pi{color:var(--module, var(--text))}.tbl-tool__label{flex:1 1 auto;min-width:0}.tbl-tool__check{color:var(--module, var(--text))}.tbl-tool__sep{margin:5px 2px;border-top:1px solid var(--rule)}.tbl-wrap[data-density=compact] .tbl tbody td{--tbl-cell-pad-block: 4px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.tbl-wrap[data-density=comfortable] .tbl tbody td{--tbl-cell-pad-block: 16px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.mlist{list-style:none;margin:0;padding:0;display:grid}.mrow{border-top:1px solid var(--rule)}.mrow:first-child{border-top:0}.mrow__head{display:flex;align-items:flex-start;gap:var(--s-3);padding:12px var(--s-4);min-height:var(--hit-touch)}.mrow__head.is-tappable{cursor:pointer}.mrow__head.is-tappable:active{background:var(--paper)}.mrow__lead{flex:1;min-width:0;display:grid;gap:2px}.mrow__primary{font-weight:700;font-size:14px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mrow__secondary{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 6px;font-size:12.5px;color:var(--text-mute);min-width:0}.mrow__secondary .mrow__sec-part+.mrow__sec-part:before{content:\"\\b7\";margin-inline-end:6px;color:var(--text-soft)}.mrow__trail{display:flex;align-items:center;justify-content:flex-end;gap:var(--s-2);flex-shrink:0;min-height:20px}.mrow__actions{display:inline-flex}.mrow__detail{margin:0;padding:0 var(--s-4) 12px;display:grid;gap:1px}.mrow__field{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-3);padding:7px 0;border-top:1px solid var(--rule-soft)}.mrow__field dt{font-size:11.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-mute);flex-shrink:0}.mrow__field dd{margin:0;font-size:13.5px;color:var(--text);text-align:end;min-width:0;overflow-wrap:anywhere}.mrow__more{appearance:none;width:100%;background:transparent;border:0;border-top:1px solid var(--rule-soft);padding:10px;font:inherit;font-size:13px;font-weight:600;color:var(--tenant-500);cursor:pointer}.mlist[data-layout=card]{gap:var(--s-3);padding:var(--s-3) var(--s-3) 0}.mlist[data-layout=card] .mrow{border:1px solid var(--rule);border-radius:var(--r-lg);background:var(--paper-alt);overflow:hidden}.mlist[data-layout=card] .mrow__head{border-bottom:1px solid var(--rule)}.mrow__chevron{font-size:11px;color:var(--text-soft);transition:transform var(--t-fast) var(--ease-out);flex-shrink:0}.mrow__chevron.is-open{transform:rotate(180deg);color:var(--text-mute)}.mrow__head.is-tappable:focus-visible{outline:2px solid var(--tenant-400);outline-offset:-2px}.mrow__more{display:flex;align-items:center;justify-content:center;gap:6px}.tbl-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s-2);padding:var(--s-9) var(--s-4);text-align:center}.tbl-empty__icon{font-size:28px;line-height:1;color:var(--ink-400);margin-block-end:var(--s-1)}.tbl-empty__title{margin:0;font-size:15px;font-weight:600;color:var(--text);max-inline-size:46ch;overflow-wrap:break-word}.tbl-empty__hint{margin:0;font-size:13px;color:var(--ink-600);max-inline-size:52ch;text-wrap:pretty}.tbl-empty__action{margin-block-start:var(--s-3);min-block-size:var(--hit-base)}.tbl-empty--error .tbl-empty__icon{color:var(--st-cancelled-fg)}.mrow--empty{padding:0}.mrow--skeleton{padding:12px var(--s-4)}@media(max-width:767px){.tbl-empty{padding:var(--s-7) var(--s-4)}.tbl-empty__action{min-block-size:var(--hit-touch)}}.pi.is-spinning{animation:ef-export-spin .9s linear infinite}@keyframes ef-export-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.pi.is-spinning{animation:none}}.tbl th.tbl-drag-h,.tbl td.tbl-drag{width:28px;padding-inline:4px 0;text-align:center}.tbl td.tbl-drag{cursor:default}.tbl .tbl-drag__grip{display:inline-flex;align-items:center;justify-content:center;width:22px;height:26px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--text-soft);cursor:grab;touch-action:none;transition:background var(--t-fast, .14s) var(--ease-out, ease-out),color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl .tbl-drag__grip:hover{background:var(--paper);color:var(--text)}.tbl .tbl-drag__grip:focus-visible{outline:2px solid var(--tenant, var(--rule));outline-offset:1px}.tbl .tbl-drag__grip:active{cursor:grabbing}.tbl .tbl-drag__grip i{font-size:12px}.tbl tbody tr.is-dragging>td{background:var(--paper);opacity:.55}.tbl tbody tr.is-drop-target>td{box-shadow:inset 0 2px 0 var(--tenant, var(--rule))}.tbl.tbl--fixed{table-layout:fixed}.mrow__disclosure{appearance:none;background:transparent;border:0;padding:0;margin:0;display:inline-grid;place-items:center;color:var(--text-mute);cursor:pointer;position:relative}.mrow__disclosure:after{content:\"\";position:absolute;top:50%;left:50%;width:var(--hit-touch);height:var(--hit-touch);transform:translate(-50%,-50%)}.mrow__disclosure:focus-visible{outline:2px solid color-mix(in srgb,var(--ink-active) 40%,transparent);outline-offset:2px;border-radius:var(--r-pill)}@media(pointer:coarse){.mrow__actions+.mrow__disclosure{margin-inline-start:calc((var(--hit-touch) - 11px) / 2 - var(--s-2))}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfPagerComponent, selector: "ef-pager", inputs: ["pageNumber", "pageSize", "totalCount", "pageSizeOptions", "linesPerPageLabelKey", "ofLabelKey", "prevLabelKey", "nextLabelKey", "firstLabelKey", "lastLabelKey", "hideSize"], outputs: ["pageChange", "pageSizeChange"] }, { kind: "component", type: EfStatusChipComponent, selector: "ef-status-chip", inputs: ["referenceKey", "code", "color", "label", "labelKey", "hideDot", "showCode"] }, { kind: "component", type: EfSkeletonComponent, selector: "ef-skeleton", inputs: ["variant", "rows", "height"] }, { kind: "component", type: EfRowActionsComponent, selector: "ef-row-actions", inputs: ["items", "context", "triggerIcon", "triggerAriaKey", "disabled", "menuLeft"], outputs: ["opened", "closed"] }, { kind: "directive", type: EfOverflowTooltipDirective, selector: "[efOverflowTooltip]" }, { kind: "directive", type: EfTooltipDirective, selector: "[efTooltip]" }, { kind: "pipe", type: i1$3.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$3.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1$3.DatePipe, name: "date" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5374
5551
|
}
|
|
5375
5552
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfDataCardComponent, decorators: [{
|
|
5376
5553
|
type: Component,
|
|
@@ -5383,7 +5560,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
5383
5560
|
EfRowActionsComponent,
|
|
5384
5561
|
EfOverflowTooltipDirective,
|
|
5385
5562
|
EfTooltipDirective,
|
|
5386
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- One cell renderer, shared by the table, the mobile row and the mobile\n detail. A new column type is added here once, not three times. -->\n<ng-template #cellTpl let-row let-col=\"col\" let-index=\"index\">\n @if (bodyTemplate(col.id); as bodyTpl) {\n <ng-container *ngTemplateOutlet=\"bodyTpl; context: { $implicit: row, col: col, index: index }\"></ng-container>\n } @else {\n @switch (col.type) {\n @case ('number') {\n {{ $any(cellValue(row, col)) | number:numberFormat(col):col.locale }}\n }\n @case ('money') {\n {{ $any(cellValue(row, col)) | currency:(col.currencyCode || 'EUR'):(col.currencyDisplay || 'symbol'):numberFormat(col):col.locale }}\n }\n @case ('date') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy'):undefined:col.locale }}\n }\n @case ('datetime') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy HH:mm'):undefined:col.locale }}\n }\n @case ('boolean') {\n @if (cellValue(row, col)) {\n <span class=\"bool yes\"><span class=\"dot\"></span> \u2713</span>\n } @else {\n <span class=\"bool no\"><span class=\"dot\"></span> \u2014</span>\n }\n }\n @case ('mono') {\n <span class=\"ref\">{{ cellValue(row, col) }}</span>\n }\n @case ('chip') {\n <span [class]=\"'chip ' + (col.chipPrefix || 'chip-') + cellValue(row, col)\">\n \u25CF {{ cellValue(row, col) }}\n </span>\n }\n @case ('status') {\n <ef-status-chip\n [referenceKey]=\"col.referenceKey || ''\"\n [code]=\"$any(cellValue(row, col))\"\n />\n }\n @case ('reference') {\n {{ resolveReference(cellValue(row, col), col) }}\n }\n @default {\n {{ cellValue(row, col) }}\n }\n }\n }\n</ng-template>\n\n<div class=\"tbl-wrap\" [class.has-open-menu]=\"openMenuCount() > 0 || openTool() !== null\" [attr.data-density]=\"density()\">\n <!-- \u2500\u2500 HEAD ROW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"tbl-head\">\n <div class=\"count small\">\n @if (loading()) {\n <span>{{ loadingKey() | translate }}</span>\n } @else if (errorMsg()) {\n <!-- A refresh that failed on top of rows already on screen keeps the\n rows, so this chip is the only sign anything went wrong \u2014 and the\n rows it sits above may now be stale. It has to say so, and say it\n in the product's words: `errorMsg` is `err.message` from the HTTP\n layer, and \"500 Internal Server Error\" tells an operator nothing\n they can act on.\n\n The raw string stays on `data-error`, where devtools can read it\n and no reader ever meets it. `title` was the obvious place and is\n the wrong one: it renders the exception in a tooltip, which is\n precisely what this change exists to stop, and it is unreachable\n on touch besides.\n\n role=\"status\", not \"alert\": polite is right for a signal beside\n rows that are still usable, and when the body is showing the\n error state it already carries the alert \u2014 one failure must not\n interrupt a screen-reader twice. -->\n <span\n role=\"status\"\n [attr.data-error]=\"errorMsg()\"\n style=\"color: var(--st-cancelled-fg); font-weight: 600;\"\n >\u26A0 {{ 'common_error_refresh_failed' | translate }}</span\n >\n } @else {\n <ng-content select=\"[tbl-head-info]\"></ng-content>\n }\n </div>\n <div class=\"tbl-tools\">\n <ng-content select=\"[tbl-head-actions]\"></ng-content>\n\n @if (canExport()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [disabled]=\"exporting()\"\n [attr.aria-busy]=\"exporting()\"\n (click)=\"requestExport()\"\n >\n <i\n class=\"pi\"\n [class.pi-download]=\"!exporting()\"\n [class.pi-spinner]=\"exporting()\"\n [class.is-spinning]=\"exporting()\"\n aria-hidden=\"true\"\n ></i>\n {{ (exporting() ? 'common_export_running' : 'common_export') | translate }}\n </button>\n </div>\n }\n\n @if (showDensityControl()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'density'\"\n [attr.aria-expanded]=\"openTool() === 'density'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('density')\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n {{ 'common_density' | translate }}\n </button>\n @if (openTool() === 'density') {\n <div class=\"tbl-tool__menu\" role=\"menu\">\n @for (opt of densityOptions; track opt.value) {\n <button\n type=\"button\"\n role=\"menuitemradio\"\n [attr.aria-checked]=\"density() === opt.value\"\n [class.is-active]=\"density() === opt.value\"\n (click)=\"setDensity(opt.value)\"\n >\n <i class=\"pi\" [ngClass]=\"opt.icon\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ opt.labelKey | translate }}</span>\n @if (density() === opt.value) {\n <i class=\"pi pi-check tbl-tool__check\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (showColumnPicker()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'columns'\"\n [attr.aria-expanded]=\"openTool() === 'columns'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('columns')\"\n >\n <i class=\"pi pi-th-large\" aria-hidden=\"true\"></i>\n {{ 'common_columns' | translate }}\n </button>\n @if (openTool() === 'columns') {\n <div class=\"tbl-tool__menu tbl-tool__menu--wide\" role=\"menu\">\n @for (col of hideableColumns(); track col.id) {\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n [attr.aria-checked]=\"!isColumnHidden(col.id)\"\n [class.is-active]=\"!isColumnHidden(col.id)\"\n (click)=\"toggleColumn(col.id)\"\n >\n <i class=\"pi\" [class.pi-check]=\"!isColumnHidden(col.id)\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{\n col.headerKey ? (col.headerKey | translate) : (col.header || col.id)\n }}</span>\n </button>\n }\n <div class=\"tbl-tool__sep\"></div>\n <button type=\"button\" role=\"menuitem\" (click)=\"resetColumns()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ 'common_columns_reset' | translate }}</span>\n </button>\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 TABLE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isMobile()) {\n <!-- \u2500\u2500 MOBILE LIST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A six-column table on a 390px screen either overflows or\n squeezes every column to nothing, so the rows become a list.\n Same columns, same cell renderers; each column just declares\n (or is given) a role. Both shapes expand to the same detail \u2014\n they differ only in how much they show collapsed. -->\n <ul\n class=\"mlist\"\n [attr.data-layout]=\"effectiveMobileLayout()\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n >\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <li class=\"mrow\" [class.is-open]=\"isExpanded(row, i)\">\n <div\n class=\"mrow__head\"\n [class.is-tappable]=\"headIsControl()\"\n [attr.role]=\"headIsControl() ? 'button' : null\"\n [attr.tabindex]=\"headIsControl() ? 0 : null\"\n [attr.aria-expanded]=\"!rowNavigable() && hasDetail() ? isExpanded(row, i) : null\"\n (click)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.enter)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.space)=\"onRowHeadActivate(row, i, $event)\"\n >\n <div class=\"mrow__lead\">\n @if (primaryColumn(); as pc) {\n <span class=\"mrow__primary\" efOverflowTooltip efTooltip efTooltipPosition=\"top\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: pc, index: i }\"></ng-container>\n </span>\n }\n @if (effectiveMobileLayout() === 'row' && secondaryColumns().length) {\n <span class=\"mrow__secondary\">\n @for (sc of secondaryColumns(); track sc.id) {\n <span class=\"mrow__sec-part\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: sc, index: i }\"></ng-container>\n </span>\n }\n </span>\n }\n </div>\n\n <div class=\"mrow__trail\">\n @if (statusColumn(); as st) {\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: st, index: i }\"></ng-container>\n }\n @if (hasActionsColumn()) {\n <span class=\"mrow__actions\">\n <ef-row-actions [items]=\"defaultRowActions(row)\" [context]=\"rowActionsContext()\" />\n </span>\n }\n @if (hasDetail() && effectiveMobileLayout() === 'row') {\n @if (rowNavigable()) {\n <!-- The head opens the record, so expansion needs a control\n of its own. Same glyph, promoted from decoration to a\n button that carries the state it was already drawing. -->\n <button\n type=\"button\"\n class=\"mrow__disclosure\"\n [attr.aria-expanded]=\"isExpanded(row, i)\"\n [attr.aria-label]=\"(isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate\"\n (click)=\"onDisclosureToggle(row, i, $event)\"\n >\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n </button>\n } @else {\n <!-- Says the row opens, and which way it is now. Decorative:\n the whole head is the control, and it carries the state. -->\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n </div>\n </div>\n\n @if (visibleDetail(row, i).length) {\n <dl class=\"mrow__detail\">\n @for (col of visibleDetail(row, i); track col.id) {\n <div class=\"mrow__field\">\n <dt>{{ $any(col).headerKey ? ($any(col).headerKey | translate) : $any(col).header }}</dt>\n <dd [class]=\"cellAlignClass($any(col))\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"></ng-container>\n </dd>\n </div>\n }\n </dl>\n }\n\n @if (hasMoreThanPreview() && effectiveMobileLayout() === 'card') {\n <button class=\"mrow__more\" type=\"button\" (click)=\"toggleExpanded(row, i)\">\n <span>{{ (isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate }}</span>\n <i class=\"pi pi-chevron-down mrow__chevron\" [class.is-open]=\"isExpanded(row, i)\" aria-hidden=\"true\"></i>\n </button>\n }\n </li>\n } @empty {\n <!-- No rows has three meanings and they must not share a screen:\n still loading is a skeleton, the request failed is the error\n state, finished-and-empty is the empty state. Saying \"nothing\n here yet\" to someone whose data is still in flight \u2014 or whose\n request just failed \u2014 is a lie the whole time it shows. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <li class=\"mrow mrow--skeleton\">\n <ef-skeleton variant=\"text\" [rows]=\"2\" />\n </li>\n }\n } @else if (showError()) {\n <li class=\"mrow mrow--empty\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </li>\n } @else {\n <li class=\"mrow mrow--empty\">\n <ng-content select=\"[empty-state-mobile]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </li>\n }\n }\n </ul>\n } @else {\n <table class=\"tbl\" [class.tbl--fixed]=\"fixedLayout()\">\n <thead>\n <tr>\n @if (reorderable()) {\n <th class=\"tbl-drag-h\" aria-hidden=\"true\"></th>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <!-- A sortable header is a real <button> inside the <th>, not a\n click handler on the cell. The cell carries aria-sort so the\n current order is announced; the arrow stays decorative.\n Sorting was previously mouse-only and silent to assistive\n technology, on every list screen in the product. -->\n <th\n [class]=\"headerAlignClass(col)\"\n [class.is-sortable]=\"col.sortable\"\n [style.width]=\"col.width || null\"\n [attr.data-col]=\"col.id\"\n [attr.aria-sort]=\"ariaSort(col)\"\n >\n @if (col.sortable) {\n <button type=\"button\" class=\"tbl-sort\" (click)=\"onHeaderClick(col)\">\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n @if (sortIndicator(col)) {\n <span aria-hidden=\"true\" class=\"tbl-sort__arrow\">{{ sortIndicator(col) }}</span>\n }\n </button>\n } @else {\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n }\n </th>\n }\n </tr>\n </thead>\n\n <tbody [attr.aria-busy]=\"loading() ? 'true' : null\">\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <tr\n [class.is-row-clickable]=\"rowDoubleClickable()\"\n [class.is-dragging]=\"dragFrom() === i\"\n [class.is-drop-target]=\"dragFrom() !== null && dragFrom() !== i && dragTo() === i\"\n [attr.tabindex]=\"rowDoubleClickable() ? 0 : null\"\n [attr.role]=\"rowDoubleClickable() ? 'button' : null\"\n (click)=\"onRowClick(row, $event)\"\n (keydown.enter)=\"onRowKeydown(row, $event)\"\n >\n @if (reorderable()) {\n <td class=\"tbl-drag\">\n <!-- The row is draggable only by this handle, so a drag never\n starts from a cell the operator meant to click or select.\n Arrow keys move the row for anyone not using a pointer. -->\n <button\n type=\"button\"\n class=\"tbl-drag__grip\"\n [attr.aria-label]=\"'common_reorder_row' | translate\"\n (pointerdown)=\"onDragPointerDown(i, $event)\"\n (pointermove)=\"onDragPointerMove($event)\"\n (pointerup)=\"onDragPointerUp()\"\n (pointercancel)=\"onDragPointerUp()\"\n (keydown)=\"onDragKeydown(i, $event)\"\n >\n <i class=\"pi pi-ellipsis-v\" aria-hidden=\"true\"></i>\n </button>\n </td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col); let colIdx = $index) {\n <td efOverflowTooltip efTooltip efTooltipPosition=\"top\" [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n @if (col.id === 'actions' && hasActionsColumn()) {\n <!-- Auto row-actions cell: View / Edit / Duplicate / Delete,\n gated by [show*Action] flags + ScreenContext perms. -->\n <ef-row-actions\n [items]=\"defaultRowActions(row)\"\n [context]=\"rowActionsContext()\"\n />\n } @else {\n <ng-container\n *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"\n ></ng-container>\n }\n </td>\n }\n </tr>\n } @empty {\n <!-- Placeholder rows keep the real columns, so the table that\n arrives lands where the shimmer already was instead of\n reflowing the page under the reader's eyes. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <tr>\n @if (reorderable()) {\n <td class=\"tbl-drag\"></td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <td [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n <ef-skeleton variant=\"text\" [rows]=\"1\" />\n </td>\n }\n </tr>\n }\n } @else if (showError()) {\n <!-- The failure replaces the body, so it spans the full row \u2014\n crammed into column one it reads as a value, not a state. -->\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </td>\n </tr>\n } @else {\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-content select=\"[empty-state]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n }\n\n <!-- \u2500\u2500 PAGER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Paging controls for an empty result set are chrome with nothing to\n page, so they stay out of the way until there is something to move\n through. -->\n @if (!hidePager() && rows().length > 0) {\n <ef-pager\n [pageNumber]=\"pageNumber()\"\n [pageSize]=\"pageSize()\"\n [totalCount]=\"totalCount()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n (pageChange)=\"pageChange.emit($event)\"\n (pageSizeChange)=\"pageSizeChange.emit($event)\"\n />\n }\n</div>\n\n<!-- \u2500\u2500 DEFAULT EMPTY STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Rendered when a screen projects nothing into [empty-state] /\n [empty-state-mobile]. It words itself from `searchTerm`, because\n \"nothing matched what you typed\" and \"there is nothing here yet\"\n are different situations needing different ways out. -->\n<ng-template #defaultEmptyTpl>\n <div class=\"tbl-empty\">\n <i\n class=\"pi tbl-empty__icon\"\n [class.pi-search]=\"!!searchTerm()\"\n [class.pi-inbox]=\"!searchTerm()\"\n aria-hidden=\"true\"\n ></i>\n\n @if (searchTerm()) {\n <p class=\"tbl-empty__title\">\n {{ 'common_empty_no_results' | translate: { term: searchTerm() } }}\n </p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_no_results_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"clearSearch.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n {{ 'common_empty_clear_filters' | translate }}\n </button>\n } @else {\n <p class=\"tbl-empty__title\">{{ 'common_empty_none_yet' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_none_yet_hint' | translate }}</p>\n }\n </div>\n</ng-template>\n\n<!-- \u2500\u2500 ERROR STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A request that failed and came back with nothing is not an empty\n collection, and must never borrow its words: \"nothing here yet\" is\n a confident false statement about the operator's data, and they may\n act on it. So this replaces the empty state outright, projected\n [empty-state] included \u2014 a screen's own \"create your first client\"\n copy is just as wrong here as the default.\n\n It says nothing about *what* failed. `errorMsg` is `err.message`\n from the HTTP layer, and a status line or an exception string tells\n an operator nothing they can act on; it survives on the head chip's\n `data-error` attribute, for whoever is reading the DOM. This carries\n the two things that help: their data is intact, and the way to try\n again.\n\n role=\"alert\" goes on this div, never on the <tr> / <td> / <li> that\n hosts it \u2014 a role on a row or cell takes the table's semantics down\n with it. It fires on insertion, which is exactly when @if adds it. -->\n<ng-template #errorStateTpl>\n <div class=\"tbl-empty tbl-empty--error\" role=\"alert\">\n <i class=\"pi pi-exclamation-triangle tbl-empty__icon\" aria-hidden=\"true\"></i>\n <p class=\"tbl-empty__title\">{{ 'common_error_load_failed' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_error_load_failed_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"retry.emit()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n {{ 'common_retry' | translate }}\n </button>\n </div>\n</ng-template>\n\n<!-- Header label, shared by the sortable and plain header cells so the\n projection rules stay in one place. -->\n<ng-template #headerLabelTpl let-col=\"col\">\n @if (headerTemplate(col.id); as headerTpl) {\n <ng-container *ngTemplateOutlet=\"headerTpl; context: { $implicit: col, col: col }\"></ng-container>\n } @else if (col.headerKey) {\n <span>{{ col.headerKey | translate }}</span>\n } @else if (col.header) {\n <span>{{ col.header }}</span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";.tbl thead th.is-sortable{cursor:pointer;-webkit-user-select:none;user-select:none;transition:color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl thead th.is-sortable:hover{color:var(--text)}.tbl tbody tr.is-row-clickable{cursor:pointer}.tbl-tools{display:flex;gap:6px;align-items:center}.tbl-tool{position:relative}.tbl-tool>.btn.open{color:var(--text);background:var(--paper)}.tbl-tool__menu{position:absolute;top:calc(100% + 8px);inset-inline-end:0;z-index:40;min-width:212px;max-height:340px;overflow-y:auto;padding:6px;display:grid;gap:1px;background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-xl);box-shadow:var(--shadow-3);animation:row-actions-menu-in var(--t-base) var(--ease-spring)}.tbl-tool__menu--wide{min-width:248px}.tbl-tool__menu button{appearance:none;background:transparent;border:0;width:100%;text-align:start;font:inherit;font-size:13.5px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:var(--r-md);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out)}.tbl-tool__menu button:hover,.tbl-tool__menu button:focus-visible{background:var(--paper);outline:0}.tbl-tool__menu button>.pi{width:16px;flex:none;font-size:14px;color:var(--muted)}.tbl-tool__menu button.is-active>.pi{color:var(--module, var(--text))}.tbl-tool__label{flex:1 1 auto;min-width:0}.tbl-tool__check{color:var(--module, var(--text))}.tbl-tool__sep{margin:5px 2px;border-top:1px solid var(--rule)}.tbl-wrap[data-density=compact] .tbl tbody td{--tbl-cell-pad-block: 4px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.tbl-wrap[data-density=comfortable] .tbl tbody td{--tbl-cell-pad-block: 16px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.mlist{list-style:none;margin:0;padding:0;display:grid}.mrow{border-top:1px solid var(--rule)}.mrow:first-child{border-top:0}.mrow__head{display:flex;align-items:flex-start;gap:var(--s-3);padding:12px var(--s-4);min-height:var(--hit-touch)}.mrow__head.is-tappable{cursor:pointer}.mrow__head.is-tappable:active{background:var(--paper)}.mrow__lead{flex:1;min-width:0;display:grid;gap:2px}.mrow__primary{font-weight:700;font-size:14px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mrow__secondary{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 6px;font-size:12.5px;color:var(--text-mute);min-width:0}.mrow__secondary .mrow__sec-part+.mrow__sec-part:before{content:\"\\b7\";margin-inline-end:6px;color:var(--text-soft)}.mrow__trail{display:flex;align-items:center;justify-content:flex-end;gap:var(--s-2);flex-shrink:0;min-height:20px}.mrow__actions{display:inline-flex}.mrow__detail{margin:0;padding:0 var(--s-4) 12px;display:grid;gap:1px}.mrow__field{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-3);padding:7px 0;border-top:1px solid var(--rule-soft)}.mrow__field dt{font-size:11.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-mute);flex-shrink:0}.mrow__field dd{margin:0;font-size:13.5px;color:var(--text);text-align:end;min-width:0;overflow-wrap:anywhere}.mrow__more{appearance:none;width:100%;background:transparent;border:0;border-top:1px solid var(--rule-soft);padding:10px;font:inherit;font-size:13px;font-weight:600;color:var(--tenant-500);cursor:pointer}.mlist[data-layout=card]{gap:var(--s-3);padding:var(--s-3) var(--s-3) 0}.mlist[data-layout=card] .mrow{border:1px solid var(--rule);border-radius:var(--r-lg);background:var(--paper-alt);overflow:hidden}.mlist[data-layout=card] .mrow__head{border-bottom:1px solid var(--rule)}.mrow__chevron{font-size:11px;color:var(--text-soft);transition:transform var(--t-fast) var(--ease-out);flex-shrink:0}.mrow__chevron.is-open{transform:rotate(180deg);color:var(--text-mute)}.mrow__head.is-tappable:focus-visible{outline:2px solid var(--tenant-400);outline-offset:-2px}.mrow__more{display:flex;align-items:center;justify-content:center;gap:6px}.tbl-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s-2);padding:var(--s-9) var(--s-4);text-align:center}.tbl-empty__icon{font-size:28px;line-height:1;color:var(--ink-400);margin-block-end:var(--s-1)}.tbl-empty__title{margin:0;font-size:15px;font-weight:600;color:var(--text);max-inline-size:46ch;overflow-wrap:break-word}.tbl-empty__hint{margin:0;font-size:13px;color:var(--ink-600);max-inline-size:52ch;text-wrap:pretty}.tbl-empty__action{margin-block-start:var(--s-3);min-block-size:var(--hit-base)}.tbl-empty--error .tbl-empty__icon{color:var(--st-cancelled-fg)}.mrow--empty{padding:0}.mrow--skeleton{padding:12px var(--s-4)}@media(max-width:767px){.tbl-empty{padding:var(--s-7) var(--s-4)}.tbl-empty__action{min-block-size:var(--hit-touch)}}.pi.is-spinning{animation:ef-export-spin .9s linear infinite}@keyframes ef-export-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.pi.is-spinning{animation:none}}.tbl th.tbl-drag-h,.tbl td.tbl-drag{width:28px;padding-inline:4px 0;text-align:center}.tbl td.tbl-drag{cursor:default}.tbl .tbl-drag__grip{display:inline-flex;align-items:center;justify-content:center;width:22px;height:26px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--text-soft);cursor:grab;touch-action:none;transition:background var(--t-fast, .14s) var(--ease-out, ease-out),color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl .tbl-drag__grip:hover{background:var(--paper);color:var(--text)}.tbl .tbl-drag__grip:focus-visible{outline:2px solid var(--tenant, var(--rule));outline-offset:1px}.tbl .tbl-drag__grip:active{cursor:grabbing}.tbl .tbl-drag__grip i{font-size:12px}.tbl tbody tr.is-dragging>td{background:var(--paper);opacity:.55}.tbl tbody tr.is-drop-target>td{box-shadow:inset 0 2px 0 var(--tenant, var(--rule))}.tbl.tbl--fixed{table-layout:fixed}.mrow__disclosure{appearance:none;background:transparent;border:0;padding:0;margin:0;display:inline-grid;place-items:center;color:var(--text-mute);cursor:pointer;position:relative}.mrow__disclosure:after{content:\"\";position:absolute;top:50%;left:50%;width:var(--hit-touch);height:var(--hit-touch);transform:translate(-50%,-50%)}.mrow__disclosure:focus-visible{outline:2px solid color-mix(in srgb,var(--ink-active) 40%,transparent);outline-offset:2px;border-radius:var(--r-pill)}\n"] }]
|
|
5563
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- One cell renderer, shared by the table, the mobile row and the mobile\n detail. A new column type is added here once, not three times. -->\n<ng-template #cellTpl let-row let-col=\"col\" let-index=\"index\">\n @if (bodyTemplate(col.id); as bodyTpl) {\n <ng-container *ngTemplateOutlet=\"bodyTpl; context: { $implicit: row, col: col, index: index }\"></ng-container>\n } @else {\n @switch (col.type) {\n @case ('number') {\n {{ $any(cellValue(row, col)) | number:numberFormat(col):col.locale }}\n }\n @case ('money') {\n {{ $any(cellValue(row, col)) | currency:(col.currencyCode || 'EUR'):(col.currencyDisplay || 'symbol'):numberFormat(col):col.locale }}\n }\n @case ('date') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy'):undefined:col.locale }}\n }\n @case ('datetime') {\n {{ $any(cellValue(row, col)) | date:(col.dateFormat || 'dd/MM/yyyy HH:mm'):undefined:col.locale }}\n }\n @case ('boolean') {\n @if (cellValue(row, col)) {\n <span class=\"bool yes\"><span class=\"dot\"></span> \u2713</span>\n } @else {\n <span class=\"bool no\"><span class=\"dot\"></span> \u2014</span>\n }\n }\n @case ('mono') {\n <span class=\"ref\">{{ cellValue(row, col) }}</span>\n }\n @case ('chip') {\n <span [class]=\"'chip ' + (col.chipPrefix || 'chip-') + cellValue(row, col)\">\n \u25CF {{ cellValue(row, col) }}\n </span>\n }\n @case ('status') {\n <ef-status-chip\n [referenceKey]=\"col.referenceKey || ''\"\n [code]=\"$any(cellValue(row, col))\"\n />\n }\n @case ('reference') {\n {{ resolveReference(cellValue(row, col), col) }}\n }\n @default {\n {{ cellValue(row, col) }}\n }\n }\n }\n</ng-template>\n\n<!-- .has-open-menu unclips the shell for the Densit\u00E9 / Colonnes menus,\n which hang below .tbl-head. Row menus are fixed to the viewport\n (ef-row-actions) and need no lift; counting them also left the shell\n unclipped for good when a row went while its menu was open. -->\n<div class=\"tbl-wrap\" [class.has-open-menu]=\"openTool() !== null\" [attr.data-density]=\"density()\">\n <!-- \u2500\u2500 HEAD ROW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div class=\"tbl-head\">\n <div class=\"count small\">\n @if (loading()) {\n <span>{{ loadingKey() | translate }}</span>\n } @else if (errorMsg()) {\n <!-- A refresh that failed on top of rows already on screen keeps the\n rows, so this chip is the only sign anything went wrong \u2014 and the\n rows it sits above may now be stale. It has to say so, and say it\n in the product's words: `errorMsg` is `err.message` from the HTTP\n layer, and \"500 Internal Server Error\" tells an operator nothing\n they can act on.\n\n The raw string stays on `data-error`, where devtools can read it\n and no reader ever meets it. `title` was the obvious place and is\n the wrong one: it renders the exception in a tooltip, which is\n precisely what this change exists to stop, and it is unreachable\n on touch besides.\n\n role=\"status\", not \"alert\": polite is right for a signal beside\n rows that are still usable, and when the body is showing the\n error state it already carries the alert \u2014 one failure must not\n interrupt a screen-reader twice. -->\n <span\n role=\"status\"\n [attr.data-error]=\"errorMsg()\"\n style=\"color: var(--st-cancelled-fg); font-weight: 600;\"\n >\u26A0 {{ 'common_error_refresh_failed' | translate }}</span\n >\n } @else {\n <ng-content select=\"[tbl-head-info]\"></ng-content>\n }\n </div>\n <div class=\"tbl-tools\">\n <ng-content select=\"[tbl-head-actions]\"></ng-content>\n\n @if (canExport()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [disabled]=\"exporting()\"\n [attr.aria-busy]=\"exporting()\"\n (click)=\"requestExport()\"\n >\n <i\n class=\"pi\"\n [class.pi-download]=\"!exporting()\"\n [class.pi-spinner]=\"exporting()\"\n [class.is-spinning]=\"exporting()\"\n aria-hidden=\"true\"\n ></i>\n {{ (exporting() ? 'common_export_running' : 'common_export') | translate }}\n </button>\n </div>\n }\n\n @if (showDensityControl()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'density'\"\n [attr.aria-expanded]=\"openTool() === 'density'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('density')\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n {{ 'common_density' | translate }}\n </button>\n @if (openTool() === 'density') {\n <div class=\"tbl-tool__menu\" role=\"menu\">\n @for (opt of densityOptions; track opt.value) {\n <button\n type=\"button\"\n role=\"menuitemradio\"\n [attr.aria-checked]=\"density() === opt.value\"\n [class.is-active]=\"density() === opt.value\"\n (click)=\"setDensity(opt.value)\"\n >\n <i class=\"pi\" [ngClass]=\"opt.icon\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ opt.labelKey | translate }}</span>\n @if (density() === opt.value) {\n <i class=\"pi pi-check tbl-tool__check\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n @if (showColumnPicker()) {\n <div class=\"tbl-tool\">\n <button\n class=\"btn btn-ghost btn-sm\"\n type=\"button\"\n [class.open]=\"openTool() === 'columns'\"\n [attr.aria-expanded]=\"openTool() === 'columns'\"\n aria-haspopup=\"menu\"\n (click)=\"toggleTool('columns')\"\n >\n <i class=\"pi pi-th-large\" aria-hidden=\"true\"></i>\n {{ 'common_columns' | translate }}\n </button>\n @if (openTool() === 'columns') {\n <div class=\"tbl-tool__menu tbl-tool__menu--wide\" role=\"menu\">\n @for (col of hideableColumns(); track col.id) {\n <button\n type=\"button\"\n role=\"menuitemcheckbox\"\n [attr.aria-checked]=\"!isColumnHidden(col.id)\"\n [class.is-active]=\"!isColumnHidden(col.id)\"\n (click)=\"toggleColumn(col.id)\"\n >\n <i class=\"pi\" [class.pi-check]=\"!isColumnHidden(col.id)\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{\n col.headerKey ? (col.headerKey | translate) : (col.header || col.id)\n }}</span>\n </button>\n }\n <div class=\"tbl-tool__sep\"></div>\n <button type=\"button\" role=\"menuitem\" (click)=\"resetColumns()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n <span class=\"tbl-tool__label\">{{ 'common_columns_reset' | translate }}</span>\n </button>\n </div>\n }\n </div>\n }\n </div>\n </div>\n\n <!-- \u2500\u2500 TABLE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (isMobile()) {\n <!-- \u2500\u2500 MOBILE LIST \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A six-column table on a 390px screen either overflows or\n squeezes every column to nothing, so the rows become a list.\n Same columns, same cell renderers; each column just declares\n (or is given) a role. Both shapes expand to the same detail \u2014\n they differ only in how much they show collapsed. -->\n <ul\n class=\"mlist\"\n [attr.data-layout]=\"effectiveMobileLayout()\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n >\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <li class=\"mrow\" [class.is-open]=\"isExpanded(row, i)\">\n <div\n class=\"mrow__head\"\n [class.is-tappable]=\"headIsControl()\"\n [attr.role]=\"headIsControl() ? 'button' : null\"\n [attr.tabindex]=\"headIsControl() ? 0 : null\"\n [attr.aria-expanded]=\"!rowNavigable() && hasDetail() ? isExpanded(row, i) : null\"\n (click)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.enter)=\"onRowHeadActivate(row, i, $event)\"\n (keydown.space)=\"onRowHeadActivate(row, i, $event)\"\n >\n <div class=\"mrow__lead\">\n @if (primaryColumn(); as pc) {\n <span class=\"mrow__primary\" efOverflowTooltip efTooltip efTooltipPosition=\"top\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: pc, index: i }\"></ng-container>\n </span>\n }\n @if (effectiveMobileLayout() === 'row' && secondaryColumns().length) {\n <span class=\"mrow__secondary\">\n @for (sc of secondaryColumns(); track sc.id) {\n <span class=\"mrow__sec-part\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: sc, index: i }\"></ng-container>\n </span>\n }\n </span>\n }\n </div>\n\n <div class=\"mrow__trail\">\n @if (statusColumn(); as st) {\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: st, index: i }\"></ng-container>\n }\n @if (hasActionsColumn()) {\n <span class=\"mrow__actions\">\n <ef-row-actions [items]=\"defaultRowActions(row)\" [context]=\"rowActionsContext()\" />\n </span>\n }\n @if (hasDetail() && effectiveMobileLayout() === 'row') {\n @if (rowNavigable()) {\n <!-- The head opens the record, so expansion needs a control\n of its own. Same glyph, promoted from decoration to a\n button that carries the state it was already drawing. -->\n <button\n type=\"button\"\n class=\"mrow__disclosure\"\n [attr.aria-expanded]=\"isExpanded(row, i)\"\n [attr.aria-label]=\"(isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate\"\n (click)=\"onDisclosureToggle(row, i, $event)\"\n >\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n </button>\n } @else {\n <!-- Says the row opens, and which way it is now. Decorative:\n the whole head is the control, and it carries the state. -->\n <i\n class=\"pi pi-chevron-down mrow__chevron\"\n [class.is-open]=\"isExpanded(row, i)\"\n aria-hidden=\"true\"\n ></i>\n }\n }\n </div>\n </div>\n\n @if (visibleDetail(row, i).length) {\n <dl class=\"mrow__detail\">\n @for (col of visibleDetail(row, i); track col.id) {\n <div class=\"mrow__field\">\n <dt>{{ $any(col).headerKey ? ($any(col).headerKey | translate) : $any(col).header }}</dt>\n <dd [class]=\"cellAlignClass($any(col))\">\n <ng-container *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"></ng-container>\n </dd>\n </div>\n }\n </dl>\n }\n\n @if (hasMoreThanPreview() && effectiveMobileLayout() === 'card') {\n <button class=\"mrow__more\" type=\"button\" (click)=\"toggleExpanded(row, i)\">\n <span>{{ (isExpanded(row, i) ? 'common_view_less' : 'common_view_more') | translate }}</span>\n <i class=\"pi pi-chevron-down mrow__chevron\" [class.is-open]=\"isExpanded(row, i)\" aria-hidden=\"true\"></i>\n </button>\n }\n </li>\n } @empty {\n <!-- No rows has three meanings and they must not share a screen:\n still loading is a skeleton, the request failed is the error\n state, finished-and-empty is the empty state. Saying \"nothing\n here yet\" to someone whose data is still in flight \u2014 or whose\n request just failed \u2014 is a lie the whole time it shows. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <li class=\"mrow mrow--skeleton\">\n <ef-skeleton variant=\"text\" [rows]=\"2\" />\n </li>\n }\n } @else if (showError()) {\n <li class=\"mrow mrow--empty\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </li>\n } @else {\n <li class=\"mrow mrow--empty\">\n <ng-content select=\"[empty-state-mobile]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </li>\n }\n }\n </ul>\n } @else {\n <!-- The table scrolls sideways inside its own box. Between 768px and\n the table's natural width (about 900-1100px) the columns past the\n edge were simply clipped by .tbl-wrap, with no way to reach them.\n .tbl-wrap stays the clipping shell, so the head and the pager do\n not scroll with the rows. Row menus are fixed to the viewport\n (ef-row-actions), so this scroller never clips them. -->\n <div class=\"tbl-scroll\">\n <table class=\"tbl\" [class.tbl--fixed]=\"fixedLayout()\">\n <thead>\n <tr>\n @if (reorderable()) {\n <th class=\"tbl-drag-h\" aria-hidden=\"true\"></th>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <!-- A sortable header is a real <button> inside the <th>, not a\n click handler on the cell. The cell carries aria-sort so the\n current order is announced; the arrow stays decorative.\n Sorting was previously mouse-only and silent to assistive\n technology, on every list screen in the product. -->\n <th\n [class]=\"headerAlignClass(col)\"\n [class.is-sortable]=\"col.sortable\"\n [style.width]=\"col.width || null\"\n [attr.data-col]=\"col.id\"\n [attr.aria-sort]=\"ariaSort(col)\"\n >\n @if (col.sortable) {\n <button type=\"button\" class=\"tbl-sort\" (click)=\"onHeaderClick(col)\">\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n @if (sortIndicator(col)) {\n <span aria-hidden=\"true\" class=\"tbl-sort__arrow\">{{ sortIndicator(col) }}</span>\n }\n </button>\n } @else {\n <ng-container *ngTemplateOutlet=\"headerLabelTpl; context: { col: col }\"></ng-container>\n }\n </th>\n }\n </tr>\n </thead>\n\n <tbody [attr.aria-busy]=\"loading() ? 'true' : null\">\n @for (row of rows(); track trackByRow($index, row); let i = $index) {\n <tr\n [class.is-row-clickable]=\"rowDoubleClickable()\"\n [class.is-dragging]=\"dragFrom() === i\"\n [class.is-drop-target]=\"dragFrom() !== null && dragFrom() !== i && dragTo() === i\"\n [attr.tabindex]=\"rowDoubleClickable() ? 0 : null\"\n [attr.role]=\"rowDoubleClickable() ? 'button' : null\"\n (click)=\"onRowClick(row, $event)\"\n (keydown.enter)=\"onRowKeydown(row, $event)\"\n >\n @if (reorderable()) {\n <td class=\"tbl-drag\">\n <!-- The row is draggable only by this handle, so a drag never\n starts from a cell the operator meant to click or select.\n Arrow keys move the row for anyone not using a pointer. -->\n <button\n type=\"button\"\n class=\"tbl-drag__grip\"\n [attr.aria-label]=\"'common_reorder_row' | translate\"\n (pointerdown)=\"onDragPointerDown(i, $event)\"\n (pointermove)=\"onDragPointerMove($event)\"\n (pointerup)=\"onDragPointerUp()\"\n (pointercancel)=\"onDragPointerUp()\"\n (keydown)=\"onDragKeydown(i, $event)\"\n >\n <i class=\"pi pi-ellipsis-v\" aria-hidden=\"true\"></i>\n </button>\n </td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col); let colIdx = $index) {\n <td efOverflowTooltip efTooltip efTooltipPosition=\"top\" [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n @if (col.id === 'actions' && hasActionsColumn()) {\n <!-- Auto row-actions cell: View / Edit / Duplicate / Delete,\n gated by [show*Action] flags + ScreenContext perms. -->\n <ef-row-actions\n [items]=\"defaultRowActions(row)\"\n [context]=\"rowActionsContext()\"\n />\n } @else {\n <ng-container\n *ngTemplateOutlet=\"cellTpl; context: { $implicit: row, col: col, index: i }\"\n ></ng-container>\n }\n </td>\n }\n </tr>\n } @empty {\n <!-- Placeholder rows keep the real columns, so the table that\n arrives lands where the shimmer already was instead of\n reflowing the page under the reader's eyes. -->\n @if (showSkeleton()) {\n @for (line of skeletonLines(); track line) {\n <tr>\n @if (reorderable()) {\n <td class=\"tbl-drag\"></td>\n }\n @for (col of effectiveColumns(); track trackByColumn($index, col)) {\n <td [class]=\"cellAlignClass(col)\" [attr.data-col]=\"col.id\">\n <ef-skeleton variant=\"text\" [rows]=\"1\" />\n </td>\n }\n </tr>\n }\n } @else if (showError()) {\n <!-- The failure replaces the body, so it spans the full row \u2014\n crammed into column one it reads as a value, not a state. -->\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-container *ngTemplateOutlet=\"errorStateTpl\"></ng-container>\n </td>\n </tr>\n } @else {\n <tr>\n <td [attr.colspan]=\"effectiveColumns().length + (reorderable() ? 1 : 0)\">\n <ng-content select=\"[empty-state]\">\n <ng-container *ngTemplateOutlet=\"defaultEmptyTpl\"></ng-container>\n </ng-content>\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- \u2500\u2500 PAGER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Paging controls for an empty result set are chrome with nothing to\n page, so they stay out of the way until there is something to move\n through. -->\n @if (!hidePager() && rows().length > 0) {\n <ef-pager\n [pageNumber]=\"pageNumber()\"\n [pageSize]=\"pageSize()\"\n [totalCount]=\"totalCount()\"\n [pageSizeOptions]=\"pageSizeOptions()\"\n (pageChange)=\"pageChange.emit($event)\"\n (pageSizeChange)=\"pageSizeChange.emit($event)\"\n />\n }\n</div>\n\n<!-- \u2500\u2500 DEFAULT EMPTY STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Rendered when a screen projects nothing into [empty-state] /\n [empty-state-mobile]. It words itself from `searchTerm`, because\n \"nothing matched what you typed\" and \"there is nothing here yet\"\n are different situations needing different ways out. -->\n<ng-template #defaultEmptyTpl>\n <div class=\"tbl-empty\">\n <i\n class=\"pi tbl-empty__icon\"\n [class.pi-search]=\"!!searchTerm()\"\n [class.pi-inbox]=\"!searchTerm()\"\n aria-hidden=\"true\"\n ></i>\n\n @if (searchTerm()) {\n <p class=\"tbl-empty__title\">\n {{ 'common_empty_no_results' | translate: { term: searchTerm() } }}\n </p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_no_results_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"clearSearch.emit()\">\n <i class=\"pi pi-times\" aria-hidden=\"true\"></i>\n {{ 'common_empty_clear_filters' | translate }}\n </button>\n } @else {\n <p class=\"tbl-empty__title\">{{ 'common_empty_none_yet' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_empty_none_yet_hint' | translate }}</p>\n }\n </div>\n</ng-template>\n\n<!-- \u2500\u2500 ERROR STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A request that failed and came back with nothing is not an empty\n collection, and must never borrow its words: \"nothing here yet\" is\n a confident false statement about the operator's data, and they may\n act on it. So this replaces the empty state outright, projected\n [empty-state] included \u2014 a screen's own \"create your first client\"\n copy is just as wrong here as the default.\n\n It says nothing about *what* failed. `errorMsg` is `err.message`\n from the HTTP layer, and a status line or an exception string tells\n an operator nothing they can act on; it survives on the head chip's\n `data-error` attribute, for whoever is reading the DOM. This carries\n the two things that help: their data is intact, and the way to try\n again.\n\n role=\"alert\" goes on this div, never on the <tr> / <td> / <li> that\n hosts it \u2014 a role on a row or cell takes the table's semantics down\n with it. It fires on insertion, which is exactly when @if adds it. -->\n<ng-template #errorStateTpl>\n <div class=\"tbl-empty tbl-empty--error\" role=\"alert\">\n <i class=\"pi pi-exclamation-triangle tbl-empty__icon\" aria-hidden=\"true\"></i>\n <p class=\"tbl-empty__title\">{{ 'common_error_load_failed' | translate }}</p>\n <p class=\"tbl-empty__hint\">{{ 'common_error_load_failed_hint' | translate }}</p>\n <button class=\"btn btn-sm tbl-empty__action\" type=\"button\" (click)=\"retry.emit()\">\n <i class=\"pi pi-refresh\" aria-hidden=\"true\"></i>\n {{ 'common_retry' | translate }}\n </button>\n </div>\n</ng-template>\n\n<!-- Header label, shared by the sortable and plain header cells so the\n projection rules stay in one place. -->\n<ng-template #headerLabelTpl let-col=\"col\">\n @if (headerTemplate(col.id); as headerTpl) {\n <ng-container *ngTemplateOutlet=\"headerTpl; context: { $implicit: col, col: col }\"></ng-container>\n } @else if (col.headerKey) {\n <span>{{ col.headerKey | translate }}</span>\n } @else if (col.header) {\n <span>{{ col.header }}</span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:block;min-width:0}.tbl thead th.is-sortable{cursor:pointer;-webkit-user-select:none;user-select:none;transition:color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl thead th.is-sortable:hover{color:var(--text)}.tbl tbody tr.is-row-clickable{cursor:pointer}.tbl-tools{display:flex;gap:6px;align-items:center}.tbl-tool{position:relative}.tbl-tool>.btn.open{color:var(--text);background:var(--paper)}.tbl-tool__menu{position:absolute;top:calc(100% + 8px);inset-inline-end:0;z-index:40;min-width:212px;max-height:340px;overflow-y:auto;padding:6px;display:grid;gap:1px;background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-xl);box-shadow:var(--shadow-3);animation:row-actions-menu-in var(--t-base) var(--ease-spring)}.tbl-tool__menu--wide{min-width:248px}.tbl-tool__menu button{appearance:none;background:transparent;border:0;width:100%;text-align:start;font:inherit;font-size:13.5px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:12px;padding:9px 12px;border-radius:var(--r-md);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out)}.tbl-tool__menu button:hover,.tbl-tool__menu button:focus-visible{background:var(--paper);outline:0}.tbl-tool__menu button>.pi{width:16px;flex:none;font-size:14px;color:var(--muted)}.tbl-tool__menu button.is-active>.pi{color:var(--module, var(--text))}.tbl-tool__label{flex:1 1 auto;min-width:0}.tbl-tool__check{color:var(--module, var(--text))}.tbl-tool__sep{margin:5px 2px;border-top:1px solid var(--rule)}.tbl-wrap[data-density=compact] .tbl tbody td{--tbl-cell-pad-block: 4px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.tbl-wrap[data-density=comfortable] .tbl tbody td{--tbl-cell-pad-block: 16px;padding-top:var(--tbl-cell-pad-block);padding-bottom:var(--tbl-cell-pad-block)}.mlist{list-style:none;margin:0;padding:0;display:grid}.mrow{border-top:1px solid var(--rule)}.mrow:first-child{border-top:0}.mrow__head{display:flex;align-items:flex-start;gap:var(--s-3);padding:12px var(--s-4);min-height:var(--hit-touch)}.mrow__head.is-tappable{cursor:pointer}.mrow__head.is-tappable:active{background:var(--paper)}.mrow__lead{flex:1;min-width:0;display:grid;gap:2px}.mrow__primary{font-weight:700;font-size:14px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mrow__secondary{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 6px;font-size:12.5px;color:var(--text-mute);min-width:0}.mrow__secondary .mrow__sec-part+.mrow__sec-part:before{content:\"\\b7\";margin-inline-end:6px;color:var(--text-soft)}.mrow__trail{display:flex;align-items:center;justify-content:flex-end;gap:var(--s-2);flex-shrink:0;min-height:20px}.mrow__actions{display:inline-flex}.mrow__detail{margin:0;padding:0 var(--s-4) 12px;display:grid;gap:1px}.mrow__field{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-3);padding:7px 0;border-top:1px solid var(--rule-soft)}.mrow__field dt{font-size:11.5px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-mute);flex-shrink:0}.mrow__field dd{margin:0;font-size:13.5px;color:var(--text);text-align:end;min-width:0;overflow-wrap:anywhere}.mrow__more{appearance:none;width:100%;background:transparent;border:0;border-top:1px solid var(--rule-soft);padding:10px;font:inherit;font-size:13px;font-weight:600;color:var(--tenant-500);cursor:pointer}.mlist[data-layout=card]{gap:var(--s-3);padding:var(--s-3) var(--s-3) 0}.mlist[data-layout=card] .mrow{border:1px solid var(--rule);border-radius:var(--r-lg);background:var(--paper-alt);overflow:hidden}.mlist[data-layout=card] .mrow__head{border-bottom:1px solid var(--rule)}.mrow__chevron{font-size:11px;color:var(--text-soft);transition:transform var(--t-fast) var(--ease-out);flex-shrink:0}.mrow__chevron.is-open{transform:rotate(180deg);color:var(--text-mute)}.mrow__head.is-tappable:focus-visible{outline:2px solid var(--tenant-400);outline-offset:-2px}.mrow__more{display:flex;align-items:center;justify-content:center;gap:6px}.tbl-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--s-2);padding:var(--s-9) var(--s-4);text-align:center}.tbl-empty__icon{font-size:28px;line-height:1;color:var(--ink-400);margin-block-end:var(--s-1)}.tbl-empty__title{margin:0;font-size:15px;font-weight:600;color:var(--text);max-inline-size:46ch;overflow-wrap:break-word}.tbl-empty__hint{margin:0;font-size:13px;color:var(--ink-600);max-inline-size:52ch;text-wrap:pretty}.tbl-empty__action{margin-block-start:var(--s-3);min-block-size:var(--hit-base)}.tbl-empty--error .tbl-empty__icon{color:var(--st-cancelled-fg)}.mrow--empty{padding:0}.mrow--skeleton{padding:12px var(--s-4)}@media(max-width:767px){.tbl-empty{padding:var(--s-7) var(--s-4)}.tbl-empty__action{min-block-size:var(--hit-touch)}}.pi.is-spinning{animation:ef-export-spin .9s linear infinite}@keyframes ef-export-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.pi.is-spinning{animation:none}}.tbl th.tbl-drag-h,.tbl td.tbl-drag{width:28px;padding-inline:4px 0;text-align:center}.tbl td.tbl-drag{cursor:default}.tbl .tbl-drag__grip{display:inline-flex;align-items:center;justify-content:center;width:22px;height:26px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--text-soft);cursor:grab;touch-action:none;transition:background var(--t-fast, .14s) var(--ease-out, ease-out),color var(--t-fast, .14s) var(--ease-out, ease-out)}.tbl .tbl-drag__grip:hover{background:var(--paper);color:var(--text)}.tbl .tbl-drag__grip:focus-visible{outline:2px solid var(--tenant, var(--rule));outline-offset:1px}.tbl .tbl-drag__grip:active{cursor:grabbing}.tbl .tbl-drag__grip i{font-size:12px}.tbl tbody tr.is-dragging>td{background:var(--paper);opacity:.55}.tbl tbody tr.is-drop-target>td{box-shadow:inset 0 2px 0 var(--tenant, var(--rule))}.tbl.tbl--fixed{table-layout:fixed}.mrow__disclosure{appearance:none;background:transparent;border:0;padding:0;margin:0;display:inline-grid;place-items:center;color:var(--text-mute);cursor:pointer;position:relative}.mrow__disclosure:after{content:\"\";position:absolute;top:50%;left:50%;width:var(--hit-touch);height:var(--hit-touch);transform:translate(-50%,-50%)}.mrow__disclosure:focus-visible{outline:2px solid color-mix(in srgb,var(--ink-active) 40%,transparent);outline-offset:2px;border-radius:var(--r-pill)}@media(pointer:coarse){.mrow__actions+.mrow__disclosure{margin-inline-start:calc((var(--hit-touch) - 11px) / 2 - var(--s-2))}}\n"] }]
|
|
5387
5564
|
}], propDecorators: { mobileLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileLayout", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], trackByField: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackByField", required: false }] }], pageNumber: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageNumber", required: false }] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }], totalCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalCount", required: false }] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], hidePager: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidePager", required: false }] }], sort: [{ type: i0.Input, args: [{ isSignal: true, alias: "sort", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], errorMsg: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMsg", required: false }] }], loadingKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingKey", required: false }] }], rowDoubleClickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowDoubleClickable", required: false }] }], hasActionsColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasActionsColumn", required: false }] }], rowActionsContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowActionsContext", required: false }] }], showViewAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "showViewAction", required: false }] }], showEditAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEditAction", required: false }] }], showDuplicateAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDuplicateAction", required: false }] }], showDeleteAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDeleteAction", required: false }] }], showDensityControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDensityControl", required: false }] }], showColumnPicker: [{ type: i0.Input, args: [{ isSignal: true, alias: "showColumnPicker", required: false }] }], showExportControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExportControl", required: false }] }], exporting: [{ type: i0.Input, args: [{ isSignal: true, alias: "exporting", required: false }] }], screenContext: [{ type: i0.Input, args: [{ isSignal: true, alias: "screenContext", required: false }] }], searchTerm: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchTerm", required: false }] }], tableKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "tableKey", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], pageSizeChange: [{ type: i0.Output, args: ["pageSizeChange"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], rowDoubleClick: [{ type: i0.Output, args: ["rowDoubleClick"] }], rowAction: [{ type: i0.Output, args: ["rowAction"] }], exportRequest: [{ type: i0.Output, args: ["exportRequest"] }], clearSearch: [{ type: i0.Output, args: ["clearSearch"] }], retry: [{ type: i0.Output, args: ["retry"] }], bodyTemplates: [{
|
|
5388
5565
|
type: ContentChildren,
|
|
5389
5566
|
args: [EfColumnTemplateDirective]
|
|
@@ -5396,9 +5573,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
5396
5573
|
}], onEscape: [{
|
|
5397
5574
|
type: HostListener,
|
|
5398
5575
|
args: ['document:keydown.escape']
|
|
5399
|
-
}], onRowActionsToggle: [{
|
|
5400
|
-
type: HostListener,
|
|
5401
|
-
args: ['ef-row-actions-toggle', ['$event']]
|
|
5402
5576
|
}] } });
|
|
5403
5577
|
function toMap(list) {
|
|
5404
5578
|
const m = new Map();
|
|
@@ -6497,6 +6671,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
6497
6671
|
* The icon shows the theme you would move to, not the one you are in, which
|
|
6498
6672
|
* is what makes a single-button toggle readable: a moon means "go dark".
|
|
6499
6673
|
*
|
|
6674
|
+
* Named by `aria-label` and a pTooltip that opens on hover and on
|
|
6675
|
+
* keyboard focus (not `title`, which never shows on touch).
|
|
6676
|
+
*
|
|
6500
6677
|
* ```html
|
|
6501
6678
|
* <ef-theme-toggle />
|
|
6502
6679
|
* ```
|
|
@@ -6507,7 +6684,12 @@ class EfThemeToggleComponent {
|
|
|
6507
6684
|
darkIcon = input('pi pi-moon', ...(ngDevMode ? [{ debugName: "darkIcon" }] : /* istanbul ignore next */ []));
|
|
6508
6685
|
/** Icon shown while dark is active (i.e. "switch to light"). */
|
|
6509
6686
|
lightIcon = input('pi pi-sun', ...(ngDevMode ? [{ debugName: "lightIcon" }] : /* istanbul ignore next */ []));
|
|
6510
|
-
/**
|
|
6687
|
+
/**
|
|
6688
|
+
* Where the tooltip opens. Below suits a top bar (the app bar, the
|
|
6689
|
+
* login header); a toggle placed low on a page can open it above.
|
|
6690
|
+
*/
|
|
6691
|
+
tooltipPosition = input('bottom', ...(ngDevMode ? [{ debugName: "tooltipPosition" }] : /* istanbul ignore next */ []));
|
|
6692
|
+
/** Translation key for the accessible name (also the tooltip text). */
|
|
6511
6693
|
ariaLabelKey = input('', ...(ngDevMode ? [{ debugName: "ariaLabelKey" }] : /* istanbul ignore next */ []));
|
|
6512
6694
|
isDark = computed(() => !!this.themeConfig.appState()?.darkTheme, ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
|
|
6513
6695
|
labelKey = computed(() => this.ariaLabelKey() ||
|
|
@@ -6516,33 +6698,37 @@ class EfThemeToggleComponent {
|
|
|
6516
6698
|
this.themeConfig.appState.update(state => ({ ...state, darkTheme: !state.darkTheme }));
|
|
6517
6699
|
}
|
|
6518
6700
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfThemeToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6519
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.11", type: EfThemeToggleComponent, isStandalone: true, selector: "ef-theme-toggle", inputs: { darkIcon: { classPropertyName: "darkIcon", publicName: "darkIcon", isSignal: true, isRequired: false, transformFunction: null }, lightIcon: { classPropertyName: "lightIcon", publicName: "lightIcon", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelKey: { classPropertyName: "ariaLabelKey", publicName: "ariaLabelKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
6701
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.11", type: EfThemeToggleComponent, isStandalone: true, selector: "ef-theme-toggle", inputs: { darkIcon: { classPropertyName: "darkIcon", publicName: "darkIcon", isSignal: true, isRequired: false, transformFunction: null }, lightIcon: { classPropertyName: "lightIcon", publicName: "lightIcon", isSignal: true, isRequired: false, transformFunction: null }, tooltipPosition: { classPropertyName: "tooltipPosition", publicName: "tooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelKey: { classPropertyName: "ariaLabelKey", publicName: "ariaLabelKey", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
6520
6702
|
<button
|
|
6521
6703
|
type="button"
|
|
6522
6704
|
class="ef-theme-toggle"
|
|
6523
6705
|
[class.is-dark]="isDark()"
|
|
6524
6706
|
[attr.aria-label]="labelKey() | translate"
|
|
6525
|
-
[
|
|
6707
|
+
[pTooltip]="labelKey() | translate"
|
|
6708
|
+
[tooltipPosition]="tooltipPosition()"
|
|
6709
|
+
tooltipEvent="both"
|
|
6526
6710
|
[attr.aria-pressed]="isDark()"
|
|
6527
6711
|
(click)="toggle()"
|
|
6528
6712
|
>
|
|
6529
6713
|
<i [class]="isDark() ? lightIcon() : darkIcon()" aria-hidden="true"></i>
|
|
6530
6714
|
</button>
|
|
6531
|
-
`, isInline: true, dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6715
|
+
`, isInline: true, dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1$5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6532
6716
|
}
|
|
6533
6717
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfThemeToggleComponent, decorators: [{
|
|
6534
6718
|
type: Component,
|
|
6535
6719
|
args: [{
|
|
6536
6720
|
selector: 'ef-theme-toggle',
|
|
6537
6721
|
standalone: true,
|
|
6538
|
-
imports: [TranslateModule],
|
|
6722
|
+
imports: [TranslateModule, TooltipModule],
|
|
6539
6723
|
template: `
|
|
6540
6724
|
<button
|
|
6541
6725
|
type="button"
|
|
6542
6726
|
class="ef-theme-toggle"
|
|
6543
6727
|
[class.is-dark]="isDark()"
|
|
6544
6728
|
[attr.aria-label]="labelKey() | translate"
|
|
6545
|
-
[
|
|
6729
|
+
[pTooltip]="labelKey() | translate"
|
|
6730
|
+
[tooltipPosition]="tooltipPosition()"
|
|
6731
|
+
tooltipEvent="both"
|
|
6546
6732
|
[attr.aria-pressed]="isDark()"
|
|
6547
6733
|
(click)="toggle()"
|
|
6548
6734
|
>
|
|
@@ -6551,7 +6737,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
6551
6737
|
`,
|
|
6552
6738
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
6553
6739
|
}]
|
|
6554
|
-
}], propDecorators: { darkIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "darkIcon", required: false }] }], lightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "lightIcon", required: false }] }], ariaLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelKey", required: false }] }] } });
|
|
6740
|
+
}], propDecorators: { darkIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "darkIcon", required: false }] }], lightIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "lightIcon", required: false }] }], tooltipPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltipPosition", required: false }] }], ariaLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelKey", required: false }] }] } });
|
|
6555
6741
|
|
|
6556
6742
|
/**
|
|
6557
6743
|
* Which build is running, for a footer or an about box.
|
|
@@ -7404,17 +7590,68 @@ class EfAppTopComponent {
|
|
|
7404
7590
|
signOut() {
|
|
7405
7591
|
this.session?.logout();
|
|
7406
7592
|
}
|
|
7593
|
+
/* ── Sign-out on touch ──────────────────────────────────────────
|
|
7594
|
+
Sign-out has no undo, and nothing guards an unsaved form, so on a
|
|
7595
|
+
touch screen it must neither invite a long press nor act on one.
|
|
7596
|
+
PrimeNG's tooltip opens on touchstart and never cancels the click
|
|
7597
|
+
that follows, so "hold it to see what it does" would sign the user
|
|
7598
|
+
out on release. A screen without hover therefore gets no tooltip on
|
|
7599
|
+
this button (the aria-label still names it), and a click that ends
|
|
7600
|
+
a touch held for HOLD_MS or longer is dropped. */
|
|
7601
|
+
/** A touch or pen press at least this long is a hold, not a tap. */
|
|
7602
|
+
static HOLD_MS = 500;
|
|
7603
|
+
/** True on a screen whose primary pointer cannot hover (a phone, a tablet). */
|
|
7604
|
+
touchOnly = signal(false, ...(ngDevMode ? [{ debugName: "touchOnly" }] : /* istanbul ignore next */ []));
|
|
7605
|
+
pressStart = null;
|
|
7606
|
+
constructor() {
|
|
7607
|
+
const mql = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
7608
|
+
? window.matchMedia('(hover: none)')
|
|
7609
|
+
: null;
|
|
7610
|
+
if (mql) {
|
|
7611
|
+
this.touchOnly.set(mql.matches);
|
|
7612
|
+
const onChange = (e) => this.touchOnly.set(e.matches);
|
|
7613
|
+
mql.addEventListener?.('change', onChange);
|
|
7614
|
+
inject(DestroyRef).onDestroy(() => mql.removeEventListener?.('change', onChange));
|
|
7615
|
+
}
|
|
7616
|
+
}
|
|
7617
|
+
onSignOutPointerDown(event) {
|
|
7618
|
+
this.pressStart = { pointerType: event.pointerType, at: Date.now() };
|
|
7619
|
+
}
|
|
7620
|
+
/**
|
|
7621
|
+
* A cancelled press (the browser took the gesture) must not judge a
|
|
7622
|
+
* later click. Not on pointerleave: on touch it fires after pointerup
|
|
7623
|
+
* and before the synthesised click, so clearing there would erase
|
|
7624
|
+
* every long press just before the guard reads it.
|
|
7625
|
+
*/
|
|
7626
|
+
forgetSignOutPress() {
|
|
7627
|
+
this.pressStart = null;
|
|
7628
|
+
}
|
|
7629
|
+
onSignOutClick(event) {
|
|
7630
|
+
const press = this.pressStart;
|
|
7631
|
+
this.pressStart = null;
|
|
7632
|
+
// Enter / Space synthesise a click with detail 0: a keyboard
|
|
7633
|
+
// activation is deliberate, whatever touch came before it.
|
|
7634
|
+
if (event.detail === 0) {
|
|
7635
|
+
this.signOut();
|
|
7636
|
+
return;
|
|
7637
|
+
}
|
|
7638
|
+
const held = press !== null && press.pointerType !== 'mouse' &&
|
|
7639
|
+
Date.now() - press.at >= EfAppTopComponent.HOLD_MS;
|
|
7640
|
+
if (held)
|
|
7641
|
+
return;
|
|
7642
|
+
this.signOut();
|
|
7643
|
+
}
|
|
7407
7644
|
active = inject(EfActiveModuleService);
|
|
7408
7645
|
module = this.active.activeModule;
|
|
7409
7646
|
navItem = this.active.activeNavItem;
|
|
7410
7647
|
hasBreadcrumb = computed(() => this.module() != null, ...(ngDevMode ? [{ debugName: "hasBreadcrumb" }] : /* istanbul ignore next */ []));
|
|
7411
7648
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfAppTopComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7412
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfAppTopComponent, isStandalone: true, selector: "ef-app-top", inputs: { themeToggle: { classPropertyName: "themeToggle", publicName: "themeToggle", isSignal: true, isRequired: false, transformFunction: null }, logout: { classPropertyName: "logout", publicName: "logout", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"ef-app-top__slot ef-app-top__slot--breadcrumb\">\n <ng-content select=\"[breadcrumb]\"></ng-content>\n @if (hasBreadcrumb()) {\n <nav class=\"ef-app-top__crumb\" aria-label=\"breadcrumb\">\n <span class=\"ef-app-top__crumb-dot\" [attr.data-module]=\"module()!.id\" aria-hidden=\"true\"></span>\n <span class=\"ef-app-top__crumb-root\">{{ module()!.labelKey | translate }}</span>\n @if (navItem(); as item) {\n <span class=\"ef-app-top__crumb-sep\" aria-hidden=\"true\">/</span>\n <span class=\"ef-app-top__crumb-leaf\">{{ item.labelKey | translate }}</span>\n }\n </nav>\n }\n</div>\n\n<div class=\"ef-app-top__slot ef-app-top__slot--actions\">\n <ng-content select=\"[actions]\"></ng-content>\n @if (themeToggle()) {\n <ef-theme-toggle />\n }\n @if (logout() && session) {\n <button\n type=\"button\"\n class=\"ef-app-top__logout\"\n [attr.aria-label]=\"'common_logout' | translate\"\n [
|
|
7649
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfAppTopComponent, isStandalone: true, selector: "ef-app-top", inputs: { themeToggle: { classPropertyName: "themeToggle", publicName: "themeToggle", isSignal: true, isRequired: false, transformFunction: null }, logout: { classPropertyName: "logout", publicName: "logout", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"ef-app-top__slot ef-app-top__slot--breadcrumb\">\n <ng-content select=\"[breadcrumb]\"></ng-content>\n @if (hasBreadcrumb()) {\n <nav class=\"ef-app-top__crumb\" aria-label=\"breadcrumb\">\n <span class=\"ef-app-top__crumb-dot\" [attr.data-module]=\"module()!.id\" aria-hidden=\"true\"></span>\n <span class=\"ef-app-top__crumb-root\">{{ module()!.labelKey | translate }}</span>\n @if (navItem(); as item) {\n <span class=\"ef-app-top__crumb-sep\" aria-hidden=\"true\">/</span>\n <span class=\"ef-app-top__crumb-leaf\">{{ item.labelKey | translate }}</span>\n }\n </nav>\n }\n</div>\n\n<div class=\"ef-app-top__slot ef-app-top__slot--actions\">\n <ng-content select=\"[actions]\"></ng-content>\n @if (themeToggle()) {\n <ef-theme-toggle />\n }\n @if (logout() && session) {\n <button\n type=\"button\"\n class=\"ef-app-top__logout\"\n [attr.aria-label]=\"'common_logout' | translate\"\n [pTooltip]=\"'common_logout' | translate\"\n tooltipPosition=\"bottom\"\n tooltipEvent=\"both\"\n [tooltipDisabled]=\"touchOnly()\"\n (pointerdown)=\"onSignOutPointerDown($event)\"\n (pointercancel)=\"forgetSignOutPress()\"\n (click)=\"onSignOutClick($event)\"\n >\n <i class=\"pi pi-sign-out\" aria-hidden=\"true\"></i>\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:grid;grid-template-columns:minmax(0,auto) minmax(0,1fr);align-items:center;gap:16px;flex:0 0 auto;height:48px;padding:0 20px;background:var(--paper-alt);border-bottom:1px solid var(--rule);color:var(--text)}.ef-app-top__slot{display:flex;align-items:center;gap:8px;min-width:0}.ef-app-top__slot--actions{justify-content:flex-end;gap:8px}.ef-app-top__crumb{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-ui);font-size:13px;letter-spacing:.01em;line-height:1;color:var(--text-mute)}.ef-app-top__crumb-dot{width:8px;height:8px;border-radius:999px;background:var(--m-admin);flex-shrink:0}.ef-app-top__crumb-dot[data-module=sales]{background:var(--m-sales)}.ef-app-top__crumb-dot[data-module=purchase]{background:var(--m-purchase)}.ef-app-top__crumb-dot[data-module=stock]{background:var(--m-stock)}.ef-app-top__crumb-dot[data-module=pos]{background:var(--m-pos)}.ef-app-top__crumb-dot[data-module=marketing]{background:var(--m-marketing)}.ef-app-top__crumb-dot[data-module=store]{background:var(--m-store)}.ef-app-top__crumb-dot[data-module=finance]{background:var(--m-finance)}.ef-app-top__crumb-dot[data-module=admin]{background:var(--m-admin)}.ef-app-top__crumb-root{font-weight:600;color:var(--text)}.ef-app-top__crumb-sep{color:var(--text-soft)}.ef-app-top__crumb-leaf{color:var(--text-mute)}@media(max-width:767px){:host{grid-template-columns:auto minmax(0,1fr);gap:8px;padding:0 12px}.ef-app-top__crumb{display:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1$5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: EfThemeToggleComponent, selector: "ef-theme-toggle", inputs: ["darkIcon", "lightIcon", "tooltipPosition", "ariaLabelKey"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7413
7650
|
}
|
|
7414
7651
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfAppTopComponent, decorators: [{
|
|
7415
7652
|
type: Component,
|
|
7416
|
-
args: [{ selector: 'ef-app-top', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, TranslateModule, EfThemeToggleComponent], template: "<div class=\"ef-app-top__slot ef-app-top__slot--breadcrumb\">\n <ng-content select=\"[breadcrumb]\"></ng-content>\n @if (hasBreadcrumb()) {\n <nav class=\"ef-app-top__crumb\" aria-label=\"breadcrumb\">\n <span class=\"ef-app-top__crumb-dot\" [attr.data-module]=\"module()!.id\" aria-hidden=\"true\"></span>\n <span class=\"ef-app-top__crumb-root\">{{ module()!.labelKey | translate }}</span>\n @if (navItem(); as item) {\n <span class=\"ef-app-top__crumb-sep\" aria-hidden=\"true\">/</span>\n <span class=\"ef-app-top__crumb-leaf\">{{ item.labelKey | translate }}</span>\n }\n </nav>\n }\n</div>\n\n<div class=\"ef-app-top__slot ef-app-top__slot--actions\">\n <ng-content select=\"[actions]\"></ng-content>\n @if (themeToggle()) {\n <ef-theme-toggle />\n }\n @if (logout() && session) {\n <button\n type=\"button\"\n class=\"ef-app-top__logout\"\n [attr.aria-label]=\"'common_logout' | translate\"\n [
|
|
7417
|
-
}], propDecorators: { themeToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "themeToggle", required: false }] }], logout: [{ type: i0.Input, args: [{ isSignal: true, alias: "logout", required: false }] }] } });
|
|
7653
|
+
args: [{ selector: 'ef-app-top', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, TranslateModule, TooltipModule, EfThemeToggleComponent], template: "<div class=\"ef-app-top__slot ef-app-top__slot--breadcrumb\">\n <ng-content select=\"[breadcrumb]\"></ng-content>\n @if (hasBreadcrumb()) {\n <nav class=\"ef-app-top__crumb\" aria-label=\"breadcrumb\">\n <span class=\"ef-app-top__crumb-dot\" [attr.data-module]=\"module()!.id\" aria-hidden=\"true\"></span>\n <span class=\"ef-app-top__crumb-root\">{{ module()!.labelKey | translate }}</span>\n @if (navItem(); as item) {\n <span class=\"ef-app-top__crumb-sep\" aria-hidden=\"true\">/</span>\n <span class=\"ef-app-top__crumb-leaf\">{{ item.labelKey | translate }}</span>\n }\n </nav>\n }\n</div>\n\n<div class=\"ef-app-top__slot ef-app-top__slot--actions\">\n <ng-content select=\"[actions]\"></ng-content>\n @if (themeToggle()) {\n <ef-theme-toggle />\n }\n @if (logout() && session) {\n <button\n type=\"button\"\n class=\"ef-app-top__logout\"\n [attr.aria-label]=\"'common_logout' | translate\"\n [pTooltip]=\"'common_logout' | translate\"\n tooltipPosition=\"bottom\"\n tooltipEvent=\"both\"\n [tooltipDisabled]=\"touchOnly()\"\n (pointerdown)=\"onSignOutPointerDown($event)\"\n (pointercancel)=\"forgetSignOutPress()\"\n (click)=\"onSignOutClick($event)\"\n >\n <i class=\"pi pi-sign-out\" aria-hidden=\"true\"></i>\n </button>\n }\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:grid;grid-template-columns:minmax(0,auto) minmax(0,1fr);align-items:center;gap:16px;flex:0 0 auto;height:48px;padding:0 20px;background:var(--paper-alt);border-bottom:1px solid var(--rule);color:var(--text)}.ef-app-top__slot{display:flex;align-items:center;gap:8px;min-width:0}.ef-app-top__slot--actions{justify-content:flex-end;gap:8px}.ef-app-top__crumb{display:inline-flex;align-items:center;gap:8px;font-family:var(--font-ui);font-size:13px;letter-spacing:.01em;line-height:1;color:var(--text-mute)}.ef-app-top__crumb-dot{width:8px;height:8px;border-radius:999px;background:var(--m-admin);flex-shrink:0}.ef-app-top__crumb-dot[data-module=sales]{background:var(--m-sales)}.ef-app-top__crumb-dot[data-module=purchase]{background:var(--m-purchase)}.ef-app-top__crumb-dot[data-module=stock]{background:var(--m-stock)}.ef-app-top__crumb-dot[data-module=pos]{background:var(--m-pos)}.ef-app-top__crumb-dot[data-module=marketing]{background:var(--m-marketing)}.ef-app-top__crumb-dot[data-module=store]{background:var(--m-store)}.ef-app-top__crumb-dot[data-module=finance]{background:var(--m-finance)}.ef-app-top__crumb-dot[data-module=admin]{background:var(--m-admin)}.ef-app-top__crumb-root{font-weight:600;color:var(--text)}.ef-app-top__crumb-sep{color:var(--text-soft)}.ef-app-top__crumb-leaf{color:var(--text-mute)}@media(max-width:767px){:host{grid-template-columns:auto minmax(0,1fr);gap:8px;padding:0 12px}.ef-app-top__crumb{display:none}}\n"] }]
|
|
7654
|
+
}], ctorParameters: () => [], propDecorators: { themeToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "themeToggle", required: false }] }], logout: [{ type: i0.Input, args: [{ isSignal: true, alias: "logout", required: false }] }] } });
|
|
7418
7655
|
|
|
7419
7656
|
/**
|
|
7420
7657
|
* Content scroll container. Lives at the right of the rail+side
|
|
@@ -7482,14 +7719,14 @@ class EfBottomSheetComponent {
|
|
|
7482
7719
|
this.openChange.emit(false);
|
|
7483
7720
|
}
|
|
7484
7721
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfBottomSheetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7485
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "21.2.11", type: EfBottomSheetComponent, isStandalone: true, selector: "ef-bottom-sheet", inputs: { open: ["open", "open", booleanAttribute], dismissOnBackdrop: ["dismissOnBackdrop", "dismissOnBackdrop", booleanAttribute] }, outputs: { openChange: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape()" }, properties: { "class.is-open": "open", "attr.aria-hidden": "!open" } }, ngImport: i0, template: "<div class=\"ef-bottom-sheet__backdrop\" (click)=\"onBackdropClick()\" aria-hidden=\"true\"></div>\n\n<div class=\"ef-bottom-sheet__panel\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"ef-bottom-sheet__grabber\" aria-hidden=\"true\"></div>\n <div class=\"ef-bottom-sheet__body\">\n <ng-content></ng-content>\n </div>\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;pointer-events:none;visibility:hidden}:host(.is-open){pointer-events:auto;visibility:visible}.ef-bottom-sheet__backdrop{position:absolute;inset:0;background:color-mix(in oklab,var(--ink-950) 35%,transparent);opacity:0;transition:opacity var(--t-base) var(--ease-out)}:host(.is-open) .ef-bottom-sheet__backdrop{opacity:1}.ef-bottom-sheet__panel{position:absolute;left:0;right:0;bottom:0;max-height:85vh;background:var(--paper);color:var(--text);border-top-left-radius:var(--r-xl);border-top-right-radius:var(--r-xl);box-shadow:0 -8px 24px color-mix(in oklab,var(--ink-950) 18%,transparent);transform:translateY(100%);transition:transform var(--t-base) var(--ease-spring);display:flex;flex-direction:column;padding-bottom:env(safe-area-inset-bottom)}:host(.is-open) .ef-bottom-sheet__panel{transform:translateY(0)}.ef-bottom-sheet__grabber{flex:0 0 auto;width:36px;height:4px;margin:8px auto 4px;border-radius:var(--r-pill);background:var(--rule)}.ef-bottom-sheet__body{flex:1 1 auto;overflow:auto;padding:16px 20px 20px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7722
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "16.1.0", version: "21.2.11", type: EfBottomSheetComponent, isStandalone: true, selector: "ef-bottom-sheet", inputs: { open: ["open", "open", booleanAttribute], dismissOnBackdrop: ["dismissOnBackdrop", "dismissOnBackdrop", booleanAttribute] }, outputs: { openChange: "openChange" }, host: { listeners: { "document:keydown.escape": "onEscape()" }, properties: { "class.is-open": "open", "attr.aria-hidden": "!open" } }, ngImport: i0, template: "<div class=\"ef-bottom-sheet__backdrop\" (click)=\"onBackdropClick()\" aria-hidden=\"true\"></div>\n\n<div class=\"ef-bottom-sheet__panel\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"ef-bottom-sheet__grabber\" aria-hidden=\"true\"></div>\n <div class=\"ef-bottom-sheet__body\">\n <ng-content></ng-content>\n </div>\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;pointer-events:none;visibility:hidden}:host(.is-open){pointer-events:auto;visibility:visible}.ef-bottom-sheet__backdrop{position:absolute;inset:0;background:color-mix(in oklab,var(--ink-950) 35%,transparent);opacity:0;transition:opacity var(--t-base) var(--ease-out)}:host(.is-open) .ef-bottom-sheet__backdrop{opacity:1}.ef-bottom-sheet__panel{position:absolute;left:0;right:0;bottom:0;max-height:85vh;max-height:85dvh;background:var(--paper);color:var(--text);border-top-left-radius:var(--r-xl);border-top-right-radius:var(--r-xl);box-shadow:0 -8px 24px color-mix(in oklab,var(--ink-950) 18%,transparent);transform:translateY(100%);transition:transform var(--t-base) var(--ease-spring);display:flex;flex-direction:column;padding-bottom:env(safe-area-inset-bottom)}:host(.is-open) .ef-bottom-sheet__panel{transform:translateY(0)}.ef-bottom-sheet__grabber{flex:0 0 auto;width:36px;height:4px;margin:8px auto 4px;border-radius:var(--r-pill);background:var(--rule)}.ef-bottom-sheet__body{flex:1 1 auto;overflow:auto;padding:16px 20px 20px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7486
7723
|
}
|
|
7487
7724
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfBottomSheetComponent, decorators: [{
|
|
7488
7725
|
type: Component,
|
|
7489
7726
|
args: [{ selector: 'ef-bottom-sheet', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule], host: {
|
|
7490
7727
|
'[class.is-open]': 'open',
|
|
7491
7728
|
'[attr.aria-hidden]': '!open',
|
|
7492
|
-
}, template: "<div class=\"ef-bottom-sheet__backdrop\" (click)=\"onBackdropClick()\" aria-hidden=\"true\"></div>\n\n<div class=\"ef-bottom-sheet__panel\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"ef-bottom-sheet__grabber\" aria-hidden=\"true\"></div>\n <div class=\"ef-bottom-sheet__body\">\n <ng-content></ng-content>\n </div>\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;pointer-events:none;visibility:hidden}:host(.is-open){pointer-events:auto;visibility:visible}.ef-bottom-sheet__backdrop{position:absolute;inset:0;background:color-mix(in oklab,var(--ink-950) 35%,transparent);opacity:0;transition:opacity var(--t-base) var(--ease-out)}:host(.is-open) .ef-bottom-sheet__backdrop{opacity:1}.ef-bottom-sheet__panel{position:absolute;left:0;right:0;bottom:0;max-height:85vh;background:var(--paper);color:var(--text);border-top-left-radius:var(--r-xl);border-top-right-radius:var(--r-xl);box-shadow:0 -8px 24px color-mix(in oklab,var(--ink-950) 18%,transparent);transform:translateY(100%);transition:transform var(--t-base) var(--ease-spring);display:flex;flex-direction:column;padding-bottom:env(safe-area-inset-bottom)}:host(.is-open) .ef-bottom-sheet__panel{transform:translateY(0)}.ef-bottom-sheet__grabber{flex:0 0 auto;width:36px;height:4px;margin:8px auto 4px;border-radius:var(--r-pill);background:var(--rule)}.ef-bottom-sheet__body{flex:1 1 auto;overflow:auto;padding:16px 20px 20px}\n"] }]
|
|
7729
|
+
}, template: "<div class=\"ef-bottom-sheet__backdrop\" (click)=\"onBackdropClick()\" aria-hidden=\"true\"></div>\n\n<div class=\"ef-bottom-sheet__panel\" role=\"dialog\" aria-modal=\"true\">\n <div class=\"ef-bottom-sheet__grabber\" aria-hidden=\"true\"></div>\n <div class=\"ef-bottom-sheet__body\">\n <ng-content></ng-content>\n </div>\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;pointer-events:none;visibility:hidden}:host(.is-open){pointer-events:auto;visibility:visible}.ef-bottom-sheet__backdrop{position:absolute;inset:0;background:color-mix(in oklab,var(--ink-950) 35%,transparent);opacity:0;transition:opacity var(--t-base) var(--ease-out)}:host(.is-open) .ef-bottom-sheet__backdrop{opacity:1}.ef-bottom-sheet__panel{position:absolute;left:0;right:0;bottom:0;max-height:85vh;max-height:85dvh;background:var(--paper);color:var(--text);border-top-left-radius:var(--r-xl);border-top-right-radius:var(--r-xl);box-shadow:0 -8px 24px color-mix(in oklab,var(--ink-950) 18%,transparent);transform:translateY(100%);transition:transform var(--t-base) var(--ease-spring);display:flex;flex-direction:column;padding-bottom:env(safe-area-inset-bottom)}:host(.is-open) .ef-bottom-sheet__panel{transform:translateY(0)}.ef-bottom-sheet__grabber{flex:0 0 auto;width:36px;height:4px;margin:8px auto 4px;border-radius:var(--r-pill);background:var(--rule)}.ef-bottom-sheet__body{flex:1 1 auto;overflow:auto;padding:16px 20px 20px}\n"] }]
|
|
7493
7730
|
}], propDecorators: { open: [{
|
|
7494
7731
|
type: Input,
|
|
7495
7732
|
args: [{ transform: booleanAttribute }]
|
|
@@ -7522,7 +7759,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
7522
7759
|
*
|
|
7523
7760
|
* **Desktop / tablet:** rail (64px) + side (240px) + (top + main). The
|
|
7524
7761
|
* side folds away from a toggle at the foot of the rail (or mod+B); the
|
|
7525
|
-
* rail never does.
|
|
7762
|
+
* rail never does. A tablet starts with it folded unless the viewer has
|
|
7763
|
+
* a stored choice (`EF_SIDE_COLLAPSED_KEY`).
|
|
7526
7764
|
* **Mobile:** delegates to `<ef-app-shell-mobile>` — top bar + scrollable
|
|
7527
7765
|
* body + 5-tab bottom bar + FAB slot.
|
|
7528
7766
|
*
|
|
@@ -7551,19 +7789,38 @@ class EfAppShellComponent {
|
|
|
7551
7789
|
once it is gone, and in the same spot. Remembered per browser:
|
|
7552
7790
|
it is a reading preference, not something worth a round trip. */
|
|
7553
7791
|
prefs = inject(EF_PREFERENCE_STORE);
|
|
7554
|
-
|
|
7792
|
+
/** The viewer's explicit choice ('1' folded, '0' open), or null when they never made one. */
|
|
7793
|
+
storedSide = signal(this.prefs.get(EF_SIDE_COLLAPSED_KEY), ...(ngDevMode ? [{ debugName: "storedSide" }] : /* istanbul ignore next */ []));
|
|
7794
|
+
/**
|
|
7795
|
+
* Folded or open. An explicit choice always wins. Without one, a
|
|
7796
|
+
* tablet starts folded: at 768-1279px the 240px panel left a
|
|
7797
|
+
* phone-width column for a desktop table, so the right-hand columns
|
|
7798
|
+
* were cut off. A desktop keeps it open and a phone never renders it.
|
|
7799
|
+
*
|
|
7800
|
+
* The default is derived, never written: storing it would turn it
|
|
7801
|
+
* into a choice the viewer did not make, pin it when they rotate or
|
|
7802
|
+
* resize across the breakpoint, and with a server-backed store send
|
|
7803
|
+
* a request on every page load.
|
|
7804
|
+
*/
|
|
7805
|
+
sideCollapsed = computed(() => {
|
|
7806
|
+
const stored = this.storedSide();
|
|
7807
|
+
if (stored !== null)
|
|
7808
|
+
return stored === '1';
|
|
7809
|
+
return this.vp.isTablet();
|
|
7810
|
+
}, ...(ngDevMode ? [{ debugName: "sideCollapsed" }] : /* istanbul ignore next */ []));
|
|
7555
7811
|
/** Shown in the toggle's tooltip: the button is far from the content, the shortcut is not. */
|
|
7556
7812
|
shortcutHint = formatShortcut('mod+b');
|
|
7557
7813
|
toggleSide() {
|
|
7558
|
-
const
|
|
7559
|
-
this.
|
|
7560
|
-
this.prefs.set(EF_SIDE_COLLAPSED_KEY,
|
|
7814
|
+
const value = this.sideCollapsed() ? '0' : '1';
|
|
7815
|
+
this.storedSide.set(value);
|
|
7816
|
+
this.prefs.set(EF_SIDE_COLLAPSED_KEY, value);
|
|
7561
7817
|
}
|
|
7562
7818
|
constructor() {
|
|
7563
7819
|
// A store that hydrates after sign in announces the key it filled.
|
|
7820
|
+
// A key it removed takes the panel back to the viewport default.
|
|
7564
7821
|
this.prefs.changes?.pipe(takeUntilDestroyed()).subscribe((key) => {
|
|
7565
7822
|
if (key === EF_SIDE_COLLAPSED_KEY) {
|
|
7566
|
-
this.
|
|
7823
|
+
this.storedSide.set(this.prefs.get(EF_SIDE_COLLAPSED_KEY));
|
|
7567
7824
|
}
|
|
7568
7825
|
});
|
|
7569
7826
|
inject(EfShortcutService).register({
|
|
@@ -7634,7 +7891,7 @@ class EfAppShellComponent {
|
|
|
7634
7891
|
this.switcherOpen.set(false);
|
|
7635
7892
|
}
|
|
7636
7893
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfAppShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7637
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfAppShellComponent, isStandalone: true, selector: "ef-app-shell", inputs: { stripe: "stripe" }, host: { properties: { "attr.data-viewport": "viewport()" } }, ngImport: i0, template: "<!-- One layout, one declaration per slot.\n Angular binds projected content to the LAST matching <ng-content> in a\n template, so declaring the same selector once per viewport branch meant the\n branch that was actually rendered on a phone received nothing \u2014 every\n screen came up blank inside working chrome. The layouts differ by CSS and\n by which chrome is rendered, never by where the content is declared. -->\n\n<ef-module-rail class=\"ef-app-shell__rail\">\n <ng-content select=\"[logo]\" logo></ng-content>\n\n <!-- Shows or hides the side panel. In the rail rather than the top bar\n because the rail never folds: the button keeps one position whether\n the panel is open or closed, so a second click needs no search.\n Drawn inline because PrimeIcons has no panel glyph: the frame with\n the panel's edge, and a chevron for what a click will do. -->\n @if (showSide()) {\n <button\n rail-footer\n type=\"button\"\n class=\"ef-app-shell__side-toggle\"\n aria-controls=\"ef-app-shell-side\"\n [attr.aria-expanded]=\"!sideCollapsed()\"\n [attr.aria-label]=\"(sideCollapsed() ? 'common_sidebar_show' : 'common_sidebar_hide') | translate\"\n [pTooltip]=\"((sideCollapsed() ? 'common_sidebar_show' : 'common_sidebar_hide') | translate) + ' \u00B7 ' + shortcutHint\"\n tooltipPosition=\"right\"\n (click)=\"toggleSide()\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"3\" />\n <path d=\"M9 4v16\" />\n @if (sideCollapsed()) {\n <path d=\"m13.5 9.5 2.5 2.5-2.5 2.5\" />\n } @else {\n <path d=\"m16 9.5-2.5 2.5 2.5 2.5\" />\n }\n </svg>\n </button>\n }\n</ef-module-rail>\n\n@if (showSide()) {\n <!-- Folded by class rather than by @if: the side-footer slot (identity\n menu) is projected into it, and the panel animates closed. `inert`\n takes the hidden links out of the tab order and the a11y tree. -->\n <ef-module-side\n id=\"ef-app-shell-side\"\n class=\"ef-app-shell__side\"\n [class.is-collapsed]=\"sideCollapsed()\"\n [attr.inert]=\"sideCollapsed() ? '' : null\"\n >\n <ng-content select=\"[side-header-meta]\" side-header-meta></ng-content>\n <ng-content select=\"[side-footer]\" side-footer></ng-content>\n </ef-module-side>\n}\n\n<div class=\"ef-app-shell__column\">\n <ef-app-top>\n <!-- The menu takes the breadcrumb's slot on a phone: the breadcrumb is\n hidden there (ef-app-top), so this is the leading position, which is\n where a menu button belongs and where a thumb reaches. -->\n @if (isMobile()) {\n <button\n breadcrumb\n type=\"button\"\n class=\"ef-app-shell__menu\"\n (click)=\"openSwitcher()\"\n [attr.aria-label]=\"'common_modules' | translate\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n </button>\n }\n <ng-content select=\"[breadcrumb]\" breadcrumb></ng-content>\n <ng-content select=\"[search]\" search></ng-content>\n <ng-content select=\"[actions]\" actions></ng-content>\n </ef-app-top>\n\n <ef-app-main [stripe]=\"stripe\">\n <ng-content></ng-content>\n </ef-app-main>\n\n @if (isMobile()) {\n <nav class=\"ef-app-shell__tabs\" [attr.aria-label]=\"'common_modules' | translate\">\n @for (module of tabs(); track module.id) {\n <button\n type=\"button\"\n class=\"ef-app-shell__tab\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n [attr.aria-current]=\"activeId() === module.id ? 'page' : null\"\n (click)=\"onSelect(module)\"\n >\n <i class=\"ef-app-shell__tab-icon\" [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span class=\"ef-app-shell__tab-label\">{{ module.labelKey | translate }}</span>\n </button>\n }\n </nav>\n }\n</div>\n\n<ng-content select=\"[fab]\"></ng-content>\n\n@if (isMobile()) {\n <ef-bottom-sheet [open]=\"switcherOpen()\" (openChange)=\"onSwitcherChange($event)\">\n <!-- The screens first: the tab bar already switches modules, so this is\n the only way to reach anything but a module's default screen. -->\n @for (section of moduleSections(); track section.id) {\n @if (section.labelKey) {\n <h2 class=\"ef-app-shell__sheet-title\">{{ section.labelKey | translate }}</h2>\n }\n <ul class=\"ef-app-shell__sheet-list\">\n @for (item of section.items; track item.id) {\n <li>\n <a\n class=\"ef-app-shell__sheet-item\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"is-active\"\n (click)=\"onNavigate(item)\"\n >\n @if (item.icon) {\n <i [class]=\"item.icon\" aria-hidden=\"true\"></i>\n }\n <span>{{ item.labelKey | translate }}</span>\n @if (item.badge !== undefined && item.badge !== null) {\n <span class=\"ef-app-shell__sheet-badge\">{{ item.badge }}</span>\n }\n </a>\n </li>\n }\n </ul>\n }\n\n <h2 class=\"ef-app-shell__sheet-title\">{{ 'common_modules' | translate }}</h2>\n <ul class=\"ef-app-shell__sheet-list\">\n @for (module of allVisible(); track module.id) {\n <li>\n <button\n type=\"button\"\n class=\"ef-app-shell__sheet-item\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n (click)=\"onSelect(module)\"\n >\n <i [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span>{{ module.labelKey | translate }}</span>\n </button>\n </li>\n }\n </ul>\n\n <!-- App-level entries that live in the rail on a desktop and have nowhere\n else to go on a phone: this sheet is the phone's menu. Declared only\n in the mobile branch, so a desktop simply drops what is projected. -->\n <div class=\"ef-app-shell__sheet-footer\">\n <ng-content select=\"[sheet-footer]\" sheet-footer></ng-content>\n </div>\n </ef-bottom-sheet>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:row;width:100%;height:100vh;background:var(--paper);color:var(--text);overflow:hidden}.ef-app-shell__column{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;min-height:0;overflow:hidden}.ef-app-shell__side{overflow:hidden;transition:flex-basis var(--t-fast) var(--ease-out),width var(--t-fast) var(--ease-out)}.ef-app-shell__side.is-collapsed{flex-basis:0;width:0;border-right-width:0}.ef-app-shell__side-toggle{display:grid;place-items:center;flex:0 0 auto;width:44px;height:44px;padding:0;background:transparent;border:0;border-radius:var(--r-md);color:var(--rail-fg-mute);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out),color var(--t-fast) var(--ease-out)}.ef-app-shell__side-toggle:hover{background:var(--rail-hover);color:#fff}.ef-app-shell__side-toggle:focus-visible{outline:2px solid color-mix(in srgb,var(--tenant-300, #b87a99) 80%,transparent);outline-offset:2px}@media(prefers-reduced-motion:reduce){.ef-app-shell__side{transition:none}}.ef-app-shell__menu{display:inline-flex;align-items:center;justify-content:center;width:var(--hit-touch);height:var(--hit-touch);background:transparent;border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-size:18px}.ef-app-shell__menu:hover{background:var(--paper-alt)}.ef-app-shell__tabs{flex:0 0 auto;display:flex;align-items:stretch;height:64px;padding-bottom:env(safe-area-inset-bottom);background:var(--paper);border-top:1px solid var(--rule)}.ef-app-shell__tab{position:relative;flex:1 1 0;min-width:0;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:transparent;border:0;color:var(--text-mute);cursor:pointer;padding:6px 4px;transition:color var(--t-fast) var(--ease-out)}.ef-app-shell__tab.is-active{color:var(--text)}.ef-app-shell__tab.is-active:after{content:\"\";position:absolute;top:0;left:50%;transform:translate(-50%);width:28px;height:3px;background:var(--module);border-radius:0 0 var(--r-sm) var(--r-sm)}.ef-app-shell__tab-icon{font-size:18px;line-height:1}.ef-app-shell__tab-label{font-family:var(--font-ui);font-size:10.5px;font-weight:600;letter-spacing:-.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.ef-app-shell__sheet-title{font-size:11.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-mute);margin:0 0 10px;padding-top:16px;border-top:1px solid var(--rule)}.ef-app-shell__sheet-title:first-child{padding-top:0;border-top:0}.ef-app-shell__sheet-list{list-style:none;margin:0 0 4px;padding:0;display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.ef-app-shell__sheet-list+.ef-app-shell__sheet-title{margin-top:8px}.ef-app-shell__sheet-item{display:flex;align-items:center;gap:12px;width:100%;height:var(--hit-touch);padding:0 16px;background:var(--paper-alt);border:0;border-radius:var(--r-md);text-decoration:none;color:var(--text);cursor:pointer;font-family:var(--font-ui);font-size:14px;font-weight:500;text-align:left;transition:background-color var(--t-fast) var(--ease-out)}.ef-app-shell__sheet-item:hover{background:var(--rule)}.ef-app-shell__sheet-item.is-active{border-left:3px solid var(--module);padding-left:13px}@media(max-width:767px){.ef-app-shell__rail{display:none}}.ef-app-shell__sheet-badge{margin-inline-start:auto;font-size:11px;font-weight:700;color:var(--text-mute)}.ef-app-shell__sheet-item.is-active{background:var(--paper);color:var(--text);font-weight:700}.ef-app-shell__sheet-footer{display:grid;gap:8px;margin-top:16px;padding-top:16px;border-top:1px solid var(--rule)}.ef-app-shell__sheet-footer:empty{display:none}:host-context([dir=rtl]) .ef-app-shell__side-toggle svg{transform:scaleX(-1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: EfModuleRailComponent, selector: "ef-module-rail", inputs: ["badges"] }, { kind: "component", type: EfModuleSideComponent, selector: "ef-module-side" }, { kind: "component", type: EfAppTopComponent, selector: "ef-app-top", inputs: ["themeToggle", "logout"] }, { kind: "component", type: EfAppMainComponent, selector: "ef-app-main", inputs: ["stripe"] }, { kind: "component", type: EfBottomSheetComponent, selector: "ef-bottom-sheet", inputs: ["open", "dismissOnBackdrop"], outputs: ["openChange"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1$5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7894
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfAppShellComponent, isStandalone: true, selector: "ef-app-shell", inputs: { stripe: "stripe" }, host: { properties: { "attr.data-viewport": "viewport()" } }, ngImport: i0, template: "<!-- One layout, one declaration per slot.\n Angular binds projected content to the LAST matching <ng-content> in a\n template, so declaring the same selector once per viewport branch meant the\n branch that was actually rendered on a phone received nothing \u2014 every\n screen came up blank inside working chrome. The layouts differ by CSS and\n by which chrome is rendered, never by where the content is declared. -->\n\n<ef-module-rail class=\"ef-app-shell__rail\">\n <ng-content select=\"[logo]\" logo></ng-content>\n\n <!-- Shows or hides the side panel. In the rail rather than the top bar\n because the rail never folds: the button keeps one position whether\n the panel is open or closed, so a second click needs no search.\n Drawn inline because PrimeIcons has no panel glyph: the frame with\n the panel's edge, and a chevron for what a click will do. -->\n @if (showSide()) {\n <button\n rail-footer\n type=\"button\"\n class=\"ef-app-shell__side-toggle\"\n aria-controls=\"ef-app-shell-side\"\n [attr.aria-expanded]=\"!sideCollapsed()\"\n [attr.aria-label]=\"(sideCollapsed() ? 'common_sidebar_show' : 'common_sidebar_hide') | translate\"\n [pTooltip]=\"((sideCollapsed() ? 'common_sidebar_show' : 'common_sidebar_hide') | translate) + ' \u00B7 ' + shortcutHint\"\n tooltipPosition=\"right\"\n (click)=\"toggleSide()\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"3\" />\n <path d=\"M9 4v16\" />\n @if (sideCollapsed()) {\n <path d=\"m13.5 9.5 2.5 2.5-2.5 2.5\" />\n } @else {\n <path d=\"m16 9.5-2.5 2.5 2.5 2.5\" />\n }\n </svg>\n </button>\n }\n</ef-module-rail>\n\n@if (showSide()) {\n <!-- Folded by class rather than by @if: the side-footer slot (identity\n menu) is projected into it, and the panel animates closed. `inert`\n takes the hidden links out of the tab order and the a11y tree. -->\n <ef-module-side\n id=\"ef-app-shell-side\"\n class=\"ef-app-shell__side\"\n [class.is-collapsed]=\"sideCollapsed()\"\n [attr.inert]=\"sideCollapsed() ? '' : null\"\n >\n <ng-content select=\"[side-header-meta]\" side-header-meta></ng-content>\n <ng-content select=\"[side-footer]\" side-footer></ng-content>\n </ef-module-side>\n}\n\n<div class=\"ef-app-shell__column\">\n <ef-app-top>\n <!-- The menu takes the breadcrumb's slot on a phone: the breadcrumb is\n hidden there (ef-app-top), so this is the leading position, which is\n where a menu button belongs and where a thumb reaches. -->\n @if (isMobile()) {\n <button\n breadcrumb\n type=\"button\"\n class=\"ef-app-shell__menu\"\n (click)=\"openSwitcher()\"\n [attr.aria-label]=\"'common_modules' | translate\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n </button>\n }\n <ng-content select=\"[breadcrumb]\" breadcrumb></ng-content>\n <ng-content select=\"[search]\" search></ng-content>\n <ng-content select=\"[actions]\" actions></ng-content>\n </ef-app-top>\n\n <ef-app-main [stripe]=\"stripe\">\n <ng-content></ng-content>\n </ef-app-main>\n\n @if (isMobile()) {\n <nav class=\"ef-app-shell__tabs\" [attr.aria-label]=\"'common_modules' | translate\">\n @for (module of tabs(); track module.id) {\n <button\n type=\"button\"\n class=\"ef-app-shell__tab\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n [attr.aria-current]=\"activeId() === module.id ? 'page' : null\"\n (click)=\"onSelect(module)\"\n >\n <i class=\"ef-app-shell__tab-icon\" [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span class=\"ef-app-shell__tab-label\">{{ module.labelKey | translate }}</span>\n </button>\n }\n </nav>\n }\n</div>\n\n<ng-content select=\"[fab]\"></ng-content>\n\n@if (isMobile()) {\n <ef-bottom-sheet [open]=\"switcherOpen()\" (openChange)=\"onSwitcherChange($event)\">\n <!-- The screens first: the tab bar already switches modules, so this is\n the only way to reach anything but a module's default screen. -->\n @for (section of moduleSections(); track section.id) {\n @if (section.labelKey) {\n <h2 class=\"ef-app-shell__sheet-title\">{{ section.labelKey | translate }}</h2>\n }\n <ul class=\"ef-app-shell__sheet-list\">\n @for (item of section.items; track item.id) {\n <li>\n <a\n class=\"ef-app-shell__sheet-item\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"is-active\"\n (click)=\"onNavigate(item)\"\n >\n @if (item.icon) {\n <i [class]=\"item.icon\" aria-hidden=\"true\"></i>\n }\n <span>{{ item.labelKey | translate }}</span>\n @if (item.badge !== undefined && item.badge !== null) {\n <span class=\"ef-app-shell__sheet-badge\">{{ item.badge }}</span>\n }\n </a>\n </li>\n }\n </ul>\n }\n\n <h2 class=\"ef-app-shell__sheet-title\">{{ 'common_modules' | translate }}</h2>\n <ul class=\"ef-app-shell__sheet-list\">\n @for (module of allVisible(); track module.id) {\n <li>\n <button\n type=\"button\"\n class=\"ef-app-shell__sheet-item\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n (click)=\"onSelect(module)\"\n >\n <i [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span>{{ module.labelKey | translate }}</span>\n </button>\n </li>\n }\n </ul>\n\n <!-- App-level entries that live in the rail on a desktop and have nowhere\n else to go on a phone: this sheet is the phone's menu. Declared only\n in the mobile branch, so a desktop simply drops what is projected. -->\n <div class=\"ef-app-shell__sheet-footer\">\n <ng-content select=\"[sheet-footer]\" sheet-footer></ng-content>\n </div>\n </ef-bottom-sheet>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:row;width:100%;height:100vh;height:100dvh;background:var(--paper);color:var(--text);overflow:hidden}.ef-app-shell__column{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;min-height:0;overflow:hidden}.ef-app-shell__side{overflow:hidden;transition:flex-basis var(--t-fast) var(--ease-out),width var(--t-fast) var(--ease-out)}.ef-app-shell__side.is-collapsed{flex-basis:0;width:0;border-right-width:0}.ef-app-shell__side-toggle{display:grid;place-items:center;flex:0 0 auto;width:44px;height:44px;padding:0;background:transparent;border:0;border-radius:var(--r-md);color:var(--rail-fg-mute);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out),color var(--t-fast) var(--ease-out)}.ef-app-shell__side-toggle:hover{background:var(--rail-hover);color:#fff}.ef-app-shell__side-toggle:focus-visible{outline:2px solid color-mix(in srgb,var(--tenant-300, #b87a99) 80%,transparent);outline-offset:2px}@media(prefers-reduced-motion:reduce){.ef-app-shell__side{transition:none}}.ef-app-shell__menu{display:inline-flex;align-items:center;justify-content:center;width:var(--hit-touch);height:var(--hit-touch);background:transparent;border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-size:18px}.ef-app-shell__menu:hover{background:var(--paper-alt)}.ef-app-shell__tabs{flex:0 0 auto;display:flex;align-items:stretch;height:64px;padding-bottom:env(safe-area-inset-bottom);background:var(--paper);border-top:1px solid var(--rule)}.ef-app-shell__tab{position:relative;flex:1 1 0;min-width:0;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:transparent;border:0;color:var(--text-mute);cursor:pointer;padding:6px 4px;transition:color var(--t-fast) var(--ease-out)}.ef-app-shell__tab.is-active{color:var(--text)}.ef-app-shell__tab.is-active:after{content:\"\";position:absolute;top:0;left:50%;transform:translate(-50%);width:28px;height:3px;background:var(--module);border-radius:0 0 var(--r-sm) var(--r-sm)}.ef-app-shell__tab-icon{font-size:18px;line-height:1}.ef-app-shell__tab-label{font-family:var(--font-ui);font-size:10.5px;font-weight:600;letter-spacing:-.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.ef-app-shell__sheet-title{font-size:11.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-mute);margin:0 0 10px;padding-top:16px;border-top:1px solid var(--rule)}.ef-app-shell__sheet-title:first-child{padding-top:0;border-top:0}.ef-app-shell__sheet-list{list-style:none;margin:0 0 4px;padding:0;display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.ef-app-shell__sheet-list+.ef-app-shell__sheet-title{margin-top:8px}.ef-app-shell__sheet-item{display:flex;align-items:center;gap:12px;width:100%;height:var(--hit-touch);padding:0 16px;background:var(--paper-alt);border:0;border-radius:var(--r-md);text-decoration:none;color:var(--text);cursor:pointer;font-family:var(--font-ui);font-size:14px;font-weight:500;text-align:left;transition:background-color var(--t-fast) var(--ease-out)}.ef-app-shell__sheet-item:hover{background:var(--rule)}.ef-app-shell__sheet-item.is-active{border-left:3px solid var(--module);padding-left:13px}@media(max-width:767px){.ef-app-shell__rail{display:none}}.ef-app-shell__sheet-badge{margin-inline-start:auto;font-size:11px;font-weight:700;color:var(--text-mute)}.ef-app-shell__sheet-item.is-active{background:var(--paper);color:var(--text);font-weight:700}.ef-app-shell__sheet-footer{display:grid;gap:8px;margin-top:16px;padding-top:16px;border-top:1px solid var(--rule)}.ef-app-shell__sheet-footer:empty{display:none}:host-context([dir=rtl]) .ef-app-shell__side-toggle svg{transform:scaleX(-1)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: EfModuleRailComponent, selector: "ef-module-rail", inputs: ["badges"] }, { kind: "component", type: EfModuleSideComponent, selector: "ef-module-side" }, { kind: "component", type: EfAppTopComponent, selector: "ef-app-top", inputs: ["themeToggle", "logout"] }, { kind: "component", type: EfAppMainComponent, selector: "ef-app-main", inputs: ["stripe"] }, { kind: "component", type: EfBottomSheetComponent, selector: "ef-bottom-sheet", inputs: ["open", "dismissOnBackdrop"], outputs: ["openChange"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i1$5.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7638
7895
|
}
|
|
7639
7896
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfAppShellComponent, decorators: [{
|
|
7640
7897
|
type: Component,
|
|
@@ -7650,7 +7907,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
7650
7907
|
RouterLink,
|
|
7651
7908
|
], host: {
|
|
7652
7909
|
'[attr.data-viewport]': 'viewport()',
|
|
7653
|
-
}, template: "<!-- One layout, one declaration per slot.\n Angular binds projected content to the LAST matching <ng-content> in a\n template, so declaring the same selector once per viewport branch meant the\n branch that was actually rendered on a phone received nothing \u2014 every\n screen came up blank inside working chrome. The layouts differ by CSS and\n by which chrome is rendered, never by where the content is declared. -->\n\n<ef-module-rail class=\"ef-app-shell__rail\">\n <ng-content select=\"[logo]\" logo></ng-content>\n\n <!-- Shows or hides the side panel. In the rail rather than the top bar\n because the rail never folds: the button keeps one position whether\n the panel is open or closed, so a second click needs no search.\n Drawn inline because PrimeIcons has no panel glyph: the frame with\n the panel's edge, and a chevron for what a click will do. -->\n @if (showSide()) {\n <button\n rail-footer\n type=\"button\"\n class=\"ef-app-shell__side-toggle\"\n aria-controls=\"ef-app-shell-side\"\n [attr.aria-expanded]=\"!sideCollapsed()\"\n [attr.aria-label]=\"(sideCollapsed() ? 'common_sidebar_show' : 'common_sidebar_hide') | translate\"\n [pTooltip]=\"((sideCollapsed() ? 'common_sidebar_show' : 'common_sidebar_hide') | translate) + ' \u00B7 ' + shortcutHint\"\n tooltipPosition=\"right\"\n (click)=\"toggleSide()\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"3\" />\n <path d=\"M9 4v16\" />\n @if (sideCollapsed()) {\n <path d=\"m13.5 9.5 2.5 2.5-2.5 2.5\" />\n } @else {\n <path d=\"m16 9.5-2.5 2.5 2.5 2.5\" />\n }\n </svg>\n </button>\n }\n</ef-module-rail>\n\n@if (showSide()) {\n <!-- Folded by class rather than by @if: the side-footer slot (identity\n menu) is projected into it, and the panel animates closed. `inert`\n takes the hidden links out of the tab order and the a11y tree. -->\n <ef-module-side\n id=\"ef-app-shell-side\"\n class=\"ef-app-shell__side\"\n [class.is-collapsed]=\"sideCollapsed()\"\n [attr.inert]=\"sideCollapsed() ? '' : null\"\n >\n <ng-content select=\"[side-header-meta]\" side-header-meta></ng-content>\n <ng-content select=\"[side-footer]\" side-footer></ng-content>\n </ef-module-side>\n}\n\n<div class=\"ef-app-shell__column\">\n <ef-app-top>\n <!-- The menu takes the breadcrumb's slot on a phone: the breadcrumb is\n hidden there (ef-app-top), so this is the leading position, which is\n where a menu button belongs and where a thumb reaches. -->\n @if (isMobile()) {\n <button\n breadcrumb\n type=\"button\"\n class=\"ef-app-shell__menu\"\n (click)=\"openSwitcher()\"\n [attr.aria-label]=\"'common_modules' | translate\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n </button>\n }\n <ng-content select=\"[breadcrumb]\" breadcrumb></ng-content>\n <ng-content select=\"[search]\" search></ng-content>\n <ng-content select=\"[actions]\" actions></ng-content>\n </ef-app-top>\n\n <ef-app-main [stripe]=\"stripe\">\n <ng-content></ng-content>\n </ef-app-main>\n\n @if (isMobile()) {\n <nav class=\"ef-app-shell__tabs\" [attr.aria-label]=\"'common_modules' | translate\">\n @for (module of tabs(); track module.id) {\n <button\n type=\"button\"\n class=\"ef-app-shell__tab\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n [attr.aria-current]=\"activeId() === module.id ? 'page' : null\"\n (click)=\"onSelect(module)\"\n >\n <i class=\"ef-app-shell__tab-icon\" [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span class=\"ef-app-shell__tab-label\">{{ module.labelKey | translate }}</span>\n </button>\n }\n </nav>\n }\n</div>\n\n<ng-content select=\"[fab]\"></ng-content>\n\n@if (isMobile()) {\n <ef-bottom-sheet [open]=\"switcherOpen()\" (openChange)=\"onSwitcherChange($event)\">\n <!-- The screens first: the tab bar already switches modules, so this is\n the only way to reach anything but a module's default screen. -->\n @for (section of moduleSections(); track section.id) {\n @if (section.labelKey) {\n <h2 class=\"ef-app-shell__sheet-title\">{{ section.labelKey | translate }}</h2>\n }\n <ul class=\"ef-app-shell__sheet-list\">\n @for (item of section.items; track item.id) {\n <li>\n <a\n class=\"ef-app-shell__sheet-item\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"is-active\"\n (click)=\"onNavigate(item)\"\n >\n @if (item.icon) {\n <i [class]=\"item.icon\" aria-hidden=\"true\"></i>\n }\n <span>{{ item.labelKey | translate }}</span>\n @if (item.badge !== undefined && item.badge !== null) {\n <span class=\"ef-app-shell__sheet-badge\">{{ item.badge }}</span>\n }\n </a>\n </li>\n }\n </ul>\n }\n\n <h2 class=\"ef-app-shell__sheet-title\">{{ 'common_modules' | translate }}</h2>\n <ul class=\"ef-app-shell__sheet-list\">\n @for (module of allVisible(); track module.id) {\n <li>\n <button\n type=\"button\"\n class=\"ef-app-shell__sheet-item\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n (click)=\"onSelect(module)\"\n >\n <i [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span>{{ module.labelKey | translate }}</span>\n </button>\n </li>\n }\n </ul>\n\n <!-- App-level entries that live in the rail on a desktop and have nowhere\n else to go on a phone: this sheet is the phone's menu. Declared only\n in the mobile branch, so a desktop simply drops what is projected. -->\n <div class=\"ef-app-shell__sheet-footer\">\n <ng-content select=\"[sheet-footer]\" sheet-footer></ng-content>\n </div>\n </ef-bottom-sheet>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:row;width:100%;height:100vh;background:var(--paper);color:var(--text);overflow:hidden}.ef-app-shell__column{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;min-height:0;overflow:hidden}.ef-app-shell__side{overflow:hidden;transition:flex-basis var(--t-fast) var(--ease-out),width var(--t-fast) var(--ease-out)}.ef-app-shell__side.is-collapsed{flex-basis:0;width:0;border-right-width:0}.ef-app-shell__side-toggle{display:grid;place-items:center;flex:0 0 auto;width:44px;height:44px;padding:0;background:transparent;border:0;border-radius:var(--r-md);color:var(--rail-fg-mute);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out),color var(--t-fast) var(--ease-out)}.ef-app-shell__side-toggle:hover{background:var(--rail-hover);color:#fff}.ef-app-shell__side-toggle:focus-visible{outline:2px solid color-mix(in srgb,var(--tenant-300, #b87a99) 80%,transparent);outline-offset:2px}@media(prefers-reduced-motion:reduce){.ef-app-shell__side{transition:none}}.ef-app-shell__menu{display:inline-flex;align-items:center;justify-content:center;width:var(--hit-touch);height:var(--hit-touch);background:transparent;border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-size:18px}.ef-app-shell__menu:hover{background:var(--paper-alt)}.ef-app-shell__tabs{flex:0 0 auto;display:flex;align-items:stretch;height:64px;padding-bottom:env(safe-area-inset-bottom);background:var(--paper);border-top:1px solid var(--rule)}.ef-app-shell__tab{position:relative;flex:1 1 0;min-width:0;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:transparent;border:0;color:var(--text-mute);cursor:pointer;padding:6px 4px;transition:color var(--t-fast) var(--ease-out)}.ef-app-shell__tab.is-active{color:var(--text)}.ef-app-shell__tab.is-active:after{content:\"\";position:absolute;top:0;left:50%;transform:translate(-50%);width:28px;height:3px;background:var(--module);border-radius:0 0 var(--r-sm) var(--r-sm)}.ef-app-shell__tab-icon{font-size:18px;line-height:1}.ef-app-shell__tab-label{font-family:var(--font-ui);font-size:10.5px;font-weight:600;letter-spacing:-.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.ef-app-shell__sheet-title{font-size:11.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-mute);margin:0 0 10px;padding-top:16px;border-top:1px solid var(--rule)}.ef-app-shell__sheet-title:first-child{padding-top:0;border-top:0}.ef-app-shell__sheet-list{list-style:none;margin:0 0 4px;padding:0;display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.ef-app-shell__sheet-list+.ef-app-shell__sheet-title{margin-top:8px}.ef-app-shell__sheet-item{display:flex;align-items:center;gap:12px;width:100%;height:var(--hit-touch);padding:0 16px;background:var(--paper-alt);border:0;border-radius:var(--r-md);text-decoration:none;color:var(--text);cursor:pointer;font-family:var(--font-ui);font-size:14px;font-weight:500;text-align:left;transition:background-color var(--t-fast) var(--ease-out)}.ef-app-shell__sheet-item:hover{background:var(--rule)}.ef-app-shell__sheet-item.is-active{border-left:3px solid var(--module);padding-left:13px}@media(max-width:767px){.ef-app-shell__rail{display:none}}.ef-app-shell__sheet-badge{margin-inline-start:auto;font-size:11px;font-weight:700;color:var(--text-mute)}.ef-app-shell__sheet-item.is-active{background:var(--paper);color:var(--text);font-weight:700}.ef-app-shell__sheet-footer{display:grid;gap:8px;margin-top:16px;padding-top:16px;border-top:1px solid var(--rule)}.ef-app-shell__sheet-footer:empty{display:none}:host-context([dir=rtl]) .ef-app-shell__side-toggle svg{transform:scaleX(-1)}\n"] }]
|
|
7910
|
+
}, template: "<!-- One layout, one declaration per slot.\n Angular binds projected content to the LAST matching <ng-content> in a\n template, so declaring the same selector once per viewport branch meant the\n branch that was actually rendered on a phone received nothing \u2014 every\n screen came up blank inside working chrome. The layouts differ by CSS and\n by which chrome is rendered, never by where the content is declared. -->\n\n<ef-module-rail class=\"ef-app-shell__rail\">\n <ng-content select=\"[logo]\" logo></ng-content>\n\n <!-- Shows or hides the side panel. In the rail rather than the top bar\n because the rail never folds: the button keeps one position whether\n the panel is open or closed, so a second click needs no search.\n Drawn inline because PrimeIcons has no panel glyph: the frame with\n the panel's edge, and a chevron for what a click will do. -->\n @if (showSide()) {\n <button\n rail-footer\n type=\"button\"\n class=\"ef-app-shell__side-toggle\"\n aria-controls=\"ef-app-shell-side\"\n [attr.aria-expanded]=\"!sideCollapsed()\"\n [attr.aria-label]=\"(sideCollapsed() ? 'common_sidebar_show' : 'common_sidebar_hide') | translate\"\n [pTooltip]=\"((sideCollapsed() ? 'common_sidebar_show' : 'common_sidebar_hide') | translate) + ' \u00B7 ' + shortcutHint\"\n tooltipPosition=\"right\"\n (click)=\"toggleSide()\"\n >\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"3\" />\n <path d=\"M9 4v16\" />\n @if (sideCollapsed()) {\n <path d=\"m13.5 9.5 2.5 2.5-2.5 2.5\" />\n } @else {\n <path d=\"m16 9.5-2.5 2.5 2.5 2.5\" />\n }\n </svg>\n </button>\n }\n</ef-module-rail>\n\n@if (showSide()) {\n <!-- Folded by class rather than by @if: the side-footer slot (identity\n menu) is projected into it, and the panel animates closed. `inert`\n takes the hidden links out of the tab order and the a11y tree. -->\n <ef-module-side\n id=\"ef-app-shell-side\"\n class=\"ef-app-shell__side\"\n [class.is-collapsed]=\"sideCollapsed()\"\n [attr.inert]=\"sideCollapsed() ? '' : null\"\n >\n <ng-content select=\"[side-header-meta]\" side-header-meta></ng-content>\n <ng-content select=\"[side-footer]\" side-footer></ng-content>\n </ef-module-side>\n}\n\n<div class=\"ef-app-shell__column\">\n <ef-app-top>\n <!-- The menu takes the breadcrumb's slot on a phone: the breadcrumb is\n hidden there (ef-app-top), so this is the leading position, which is\n where a menu button belongs and where a thumb reaches. -->\n @if (isMobile()) {\n <button\n breadcrumb\n type=\"button\"\n class=\"ef-app-shell__menu\"\n (click)=\"openSwitcher()\"\n [attr.aria-label]=\"'common_modules' | translate\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n </button>\n }\n <ng-content select=\"[breadcrumb]\" breadcrumb></ng-content>\n <ng-content select=\"[search]\" search></ng-content>\n <ng-content select=\"[actions]\" actions></ng-content>\n </ef-app-top>\n\n <ef-app-main [stripe]=\"stripe\">\n <ng-content></ng-content>\n </ef-app-main>\n\n @if (isMobile()) {\n <nav class=\"ef-app-shell__tabs\" [attr.aria-label]=\"'common_modules' | translate\">\n @for (module of tabs(); track module.id) {\n <button\n type=\"button\"\n class=\"ef-app-shell__tab\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n [attr.aria-current]=\"activeId() === module.id ? 'page' : null\"\n (click)=\"onSelect(module)\"\n >\n <i class=\"ef-app-shell__tab-icon\" [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span class=\"ef-app-shell__tab-label\">{{ module.labelKey | translate }}</span>\n </button>\n }\n </nav>\n }\n</div>\n\n<ng-content select=\"[fab]\"></ng-content>\n\n@if (isMobile()) {\n <ef-bottom-sheet [open]=\"switcherOpen()\" (openChange)=\"onSwitcherChange($event)\">\n <!-- The screens first: the tab bar already switches modules, so this is\n the only way to reach anything but a module's default screen. -->\n @for (section of moduleSections(); track section.id) {\n @if (section.labelKey) {\n <h2 class=\"ef-app-shell__sheet-title\">{{ section.labelKey | translate }}</h2>\n }\n <ul class=\"ef-app-shell__sheet-list\">\n @for (item of section.items; track item.id) {\n <li>\n <a\n class=\"ef-app-shell__sheet-item\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"is-active\"\n (click)=\"onNavigate(item)\"\n >\n @if (item.icon) {\n <i [class]=\"item.icon\" aria-hidden=\"true\"></i>\n }\n <span>{{ item.labelKey | translate }}</span>\n @if (item.badge !== undefined && item.badge !== null) {\n <span class=\"ef-app-shell__sheet-badge\">{{ item.badge }}</span>\n }\n </a>\n </li>\n }\n </ul>\n }\n\n <h2 class=\"ef-app-shell__sheet-title\">{{ 'common_modules' | translate }}</h2>\n <ul class=\"ef-app-shell__sheet-list\">\n @for (module of allVisible(); track module.id) {\n <li>\n <button\n type=\"button\"\n class=\"ef-app-shell__sheet-item\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n (click)=\"onSelect(module)\"\n >\n <i [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span>{{ module.labelKey | translate }}</span>\n </button>\n </li>\n }\n </ul>\n\n <!-- App-level entries that live in the rail on a desktop and have nowhere\n else to go on a phone: this sheet is the phone's menu. Declared only\n in the mobile branch, so a desktop simply drops what is projected. -->\n <div class=\"ef-app-shell__sheet-footer\">\n <ng-content select=\"[sheet-footer]\" sheet-footer></ng-content>\n </div>\n </ef-bottom-sheet>\n}\n", styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:row;width:100%;height:100vh;height:100dvh;background:var(--paper);color:var(--text);overflow:hidden}.ef-app-shell__column{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;min-height:0;overflow:hidden}.ef-app-shell__side{overflow:hidden;transition:flex-basis var(--t-fast) var(--ease-out),width var(--t-fast) var(--ease-out)}.ef-app-shell__side.is-collapsed{flex-basis:0;width:0;border-right-width:0}.ef-app-shell__side-toggle{display:grid;place-items:center;flex:0 0 auto;width:44px;height:44px;padding:0;background:transparent;border:0;border-radius:var(--r-md);color:var(--rail-fg-mute);cursor:pointer;transition:background-color var(--t-fast) var(--ease-out),color var(--t-fast) var(--ease-out)}.ef-app-shell__side-toggle:hover{background:var(--rail-hover);color:#fff}.ef-app-shell__side-toggle:focus-visible{outline:2px solid color-mix(in srgb,var(--tenant-300, #b87a99) 80%,transparent);outline-offset:2px}@media(prefers-reduced-motion:reduce){.ef-app-shell__side{transition:none}}.ef-app-shell__menu{display:inline-flex;align-items:center;justify-content:center;width:var(--hit-touch);height:var(--hit-touch);background:transparent;border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-size:18px}.ef-app-shell__menu:hover{background:var(--paper-alt)}.ef-app-shell__tabs{flex:0 0 auto;display:flex;align-items:stretch;height:64px;padding-bottom:env(safe-area-inset-bottom);background:var(--paper);border-top:1px solid var(--rule)}.ef-app-shell__tab{position:relative;flex:1 1 0;min-width:0;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:transparent;border:0;color:var(--text-mute);cursor:pointer;padding:6px 4px;transition:color var(--t-fast) var(--ease-out)}.ef-app-shell__tab.is-active{color:var(--text)}.ef-app-shell__tab.is-active:after{content:\"\";position:absolute;top:0;left:50%;transform:translate(-50%);width:28px;height:3px;background:var(--module);border-radius:0 0 var(--r-sm) var(--r-sm)}.ef-app-shell__tab-icon{font-size:18px;line-height:1}.ef-app-shell__tab-label{font-family:var(--font-ui);font-size:10.5px;font-weight:600;letter-spacing:-.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.ef-app-shell__sheet-title{font-size:11.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text-mute);margin:0 0 10px;padding-top:16px;border-top:1px solid var(--rule)}.ef-app-shell__sheet-title:first-child{padding-top:0;border-top:0}.ef-app-shell__sheet-list{list-style:none;margin:0 0 4px;padding:0;display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.ef-app-shell__sheet-list+.ef-app-shell__sheet-title{margin-top:8px}.ef-app-shell__sheet-item{display:flex;align-items:center;gap:12px;width:100%;height:var(--hit-touch);padding:0 16px;background:var(--paper-alt);border:0;border-radius:var(--r-md);text-decoration:none;color:var(--text);cursor:pointer;font-family:var(--font-ui);font-size:14px;font-weight:500;text-align:left;transition:background-color var(--t-fast) var(--ease-out)}.ef-app-shell__sheet-item:hover{background:var(--rule)}.ef-app-shell__sheet-item.is-active{border-left:3px solid var(--module);padding-left:13px}@media(max-width:767px){.ef-app-shell__rail{display:none}}.ef-app-shell__sheet-badge{margin-inline-start:auto;font-size:11px;font-weight:700;color:var(--text-mute)}.ef-app-shell__sheet-item.is-active{background:var(--paper);color:var(--text);font-weight:700}.ef-app-shell__sheet-footer{display:grid;gap:8px;margin-top:16px;padding-top:16px;border-top:1px solid var(--rule)}.ef-app-shell__sheet-footer:empty{display:none}:host-context([dir=rtl]) .ef-app-shell__side-toggle svg{transform:scaleX(-1)}\n"] }]
|
|
7654
7911
|
}], ctorParameters: () => [], propDecorators: { stripe: [{
|
|
7655
7912
|
type: Input
|
|
7656
7913
|
}] } });
|
|
@@ -7685,7 +7942,7 @@ class EfAppShellMobileComponent {
|
|
|
7685
7942
|
this.switcherOpen.set(false);
|
|
7686
7943
|
}
|
|
7687
7944
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfAppShellMobileComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7688
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfAppShellMobileComponent, isStandalone: true, selector: "ef-app-shell-mobile", ngImport: i0, template: "<ef-app-top>\n <ng-content select=\"[breadcrumb]\" breadcrumb></ng-content>\n <ng-content select=\"[search]\" search></ng-content>\n <button\n actions\n type=\"button\"\n class=\"ef-app-shell-mobile__menu\"\n (click)=\"openSwitcher()\"\n aria-label=\"Modules\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n </button>\n <ng-content select=\"[actions]\" actions></ng-content>\n</ef-app-top>\n\n<ef-app-main [stripe]=\"true\">\n <ng-content></ng-content>\n</ef-app-main>\n\n<nav class=\"ef-app-shell-mobile__tabs\" aria-label=\"Modules\">\n @for (module of tabs(); track module.id) {\n <button\n type=\"button\"\n class=\"ef-app-shell-mobile__tab\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n [attr.aria-current]=\"activeId() === module.id ? 'page' : null\"\n (click)=\"onSelect(module)\"\n >\n <i class=\"ef-app-shell-mobile__tab-icon\" [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span class=\"ef-app-shell-mobile__tab-label\">{{ module.labelKey | translate }}</span>\n </button>\n }\n</nav>\n\n<ng-content select=\"[fab]\"></ng-content>\n\n<ef-bottom-sheet [open]=\"switcherOpen()\" (openChange)=\"onSwitcherChange($event)\">\n <h2 class=\"ef-app-shell-mobile__sheet-title\">{{ 'modules.all' | translate }}</h2>\n <ul class=\"ef-app-shell-mobile__sheet-list\">\n @for (module of allVisible(); track module.id) {\n <li>\n <button\n type=\"button\"\n class=\"ef-app-shell-mobile__sheet-item\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n (click)=\"onSelect(module)\"\n >\n <i [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span>{{ module.labelKey | translate }}</span>\n </button>\n </li>\n }\n </ul>\n</ef-bottom-sheet>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100vh;background:var(--paper);color:var(--text);overflow:hidden}.ef-app-shell-mobile__menu{display:inline-flex;align-items:center;justify-content:center;width:var(--hit-touch);height:var(--hit-touch);background:transparent;border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-size:18px}.ef-app-shell-mobile__menu:hover{background:var(--paper-alt)}.ef-app-shell-mobile__tabs{flex:0 0 auto;display:flex;align-items:stretch;height:64px;padding-bottom:env(safe-area-inset-bottom);background:var(--paper);border-top:1px solid var(--rule)}.ef-app-shell-mobile__tab{position:relative;flex:1 1 0;min-width:0;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:transparent;border:0;color:var(--text-mute);cursor:pointer;padding:6px 4px;transition:color var(--t-fast) var(--ease-out)}.ef-app-shell-mobile__tab.is-active{color:var(--text)}.ef-app-shell-mobile__tab.is-active:after{content:\"\";position:absolute;top:0;left:50%;transform:translate(-50%);width:28px;height:3px;background:var(--module);border-radius:0 0 var(--r-sm) var(--r-sm)}.ef-app-shell-mobile__tab-icon{font-size:18px;line-height:1}.ef-app-shell-mobile__tab-label{font-family:var(--font-ui);font-size:10.5px;font-weight:600;letter-spacing:-.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.ef-app-shell-mobile__sheet-title{font-family:var(--font-display);font-variation-settings:\"opsz\" 96,\"wght\" 600,\"wdth\" 95;font-size:22px;letter-spacing:-.025em;margin:0 0 12px;color:var(--text)}.ef-app-shell-mobile__sheet-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.ef-app-shell-mobile__sheet-item{display:flex;align-items:center;gap:12px;width:100%;height:var(--hit-touch);padding:0 16px;background:var(--paper-alt);border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-family:var(--font-ui);font-size:14px;font-weight:500;text-align:left;transition:background-color var(--t-fast) var(--ease-out)}.ef-app-shell-mobile__sheet-item:hover{background:var(--rule)}.ef-app-shell-mobile__sheet-item.is-active{border-left:3px solid var(--module);padding-left:13px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: TooltipModule }, { kind: "component", type: EfAppTopComponent, selector: "ef-app-top", inputs: ["themeToggle", "logout"] }, { kind: "component", type: EfAppMainComponent, selector: "ef-app-main", inputs: ["stripe"] }, { kind: "component", type: EfBottomSheetComponent, selector: "ef-bottom-sheet", inputs: ["open", "dismissOnBackdrop"], outputs: ["openChange"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7945
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfAppShellMobileComponent, isStandalone: true, selector: "ef-app-shell-mobile", ngImport: i0, template: "<ef-app-top>\n <ng-content select=\"[breadcrumb]\" breadcrumb></ng-content>\n <ng-content select=\"[search]\" search></ng-content>\n <button\n actions\n type=\"button\"\n class=\"ef-app-shell-mobile__menu\"\n (click)=\"openSwitcher()\"\n aria-label=\"Modules\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n </button>\n <ng-content select=\"[actions]\" actions></ng-content>\n</ef-app-top>\n\n<ef-app-main [stripe]=\"true\">\n <ng-content></ng-content>\n</ef-app-main>\n\n<nav class=\"ef-app-shell-mobile__tabs\" aria-label=\"Modules\">\n @for (module of tabs(); track module.id) {\n <button\n type=\"button\"\n class=\"ef-app-shell-mobile__tab\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n [attr.aria-current]=\"activeId() === module.id ? 'page' : null\"\n (click)=\"onSelect(module)\"\n >\n <i class=\"ef-app-shell-mobile__tab-icon\" [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span class=\"ef-app-shell-mobile__tab-label\">{{ module.labelKey | translate }}</span>\n </button>\n }\n</nav>\n\n<ng-content select=\"[fab]\"></ng-content>\n\n<ef-bottom-sheet [open]=\"switcherOpen()\" (openChange)=\"onSwitcherChange($event)\">\n <h2 class=\"ef-app-shell-mobile__sheet-title\">{{ 'modules.all' | translate }}</h2>\n <ul class=\"ef-app-shell-mobile__sheet-list\">\n @for (module of allVisible(); track module.id) {\n <li>\n <button\n type=\"button\"\n class=\"ef-app-shell-mobile__sheet-item\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n (click)=\"onSelect(module)\"\n >\n <i [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span>{{ module.labelKey | translate }}</span>\n </button>\n </li>\n }\n </ul>\n</ef-bottom-sheet>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100vh;height:100dvh;background:var(--paper);color:var(--text);overflow:hidden}.ef-app-shell-mobile__menu{display:inline-flex;align-items:center;justify-content:center;width:var(--hit-touch);height:var(--hit-touch);background:transparent;border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-size:18px}.ef-app-shell-mobile__menu:hover{background:var(--paper-alt)}.ef-app-shell-mobile__tabs{flex:0 0 auto;display:flex;align-items:stretch;height:64px;padding-bottom:env(safe-area-inset-bottom);background:var(--paper);border-top:1px solid var(--rule)}.ef-app-shell-mobile__tab{position:relative;flex:1 1 0;min-width:0;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:transparent;border:0;color:var(--text-mute);cursor:pointer;padding:6px 4px;transition:color var(--t-fast) var(--ease-out)}.ef-app-shell-mobile__tab.is-active{color:var(--text)}.ef-app-shell-mobile__tab.is-active:after{content:\"\";position:absolute;top:0;left:50%;transform:translate(-50%);width:28px;height:3px;background:var(--module);border-radius:0 0 var(--r-sm) var(--r-sm)}.ef-app-shell-mobile__tab-icon{font-size:18px;line-height:1}.ef-app-shell-mobile__tab-label{font-family:var(--font-ui);font-size:10.5px;font-weight:600;letter-spacing:-.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.ef-app-shell-mobile__sheet-title{font-family:var(--font-display);font-variation-settings:\"opsz\" 96,\"wght\" 600,\"wdth\" 95;font-size:22px;letter-spacing:-.025em;margin:0 0 12px;color:var(--text)}.ef-app-shell-mobile__sheet-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.ef-app-shell-mobile__sheet-item{display:flex;align-items:center;gap:12px;width:100%;height:var(--hit-touch);padding:0 16px;background:var(--paper-alt);border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-family:var(--font-ui);font-size:14px;font-weight:500;text-align:left;transition:background-color var(--t-fast) var(--ease-out)}.ef-app-shell-mobile__sheet-item:hover{background:var(--rule)}.ef-app-shell-mobile__sheet-item.is-active{border-left:3px solid var(--module);padding-left:13px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: TooltipModule }, { kind: "component", type: EfAppTopComponent, selector: "ef-app-top", inputs: ["themeToggle", "logout"] }, { kind: "component", type: EfAppMainComponent, selector: "ef-app-main", inputs: ["stripe"] }, { kind: "component", type: EfBottomSheetComponent, selector: "ef-bottom-sheet", inputs: ["open", "dismissOnBackdrop"], outputs: ["openChange"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
7689
7946
|
}
|
|
7690
7947
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfAppShellMobileComponent, decorators: [{
|
|
7691
7948
|
type: Component,
|
|
@@ -7696,7 +7953,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
|
|
|
7696
7953
|
EfAppTopComponent,
|
|
7697
7954
|
EfAppMainComponent,
|
|
7698
7955
|
EfBottomSheetComponent,
|
|
7699
|
-
], template: "<ef-app-top>\n <ng-content select=\"[breadcrumb]\" breadcrumb></ng-content>\n <ng-content select=\"[search]\" search></ng-content>\n <button\n actions\n type=\"button\"\n class=\"ef-app-shell-mobile__menu\"\n (click)=\"openSwitcher()\"\n aria-label=\"Modules\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n </button>\n <ng-content select=\"[actions]\" actions></ng-content>\n</ef-app-top>\n\n<ef-app-main [stripe]=\"true\">\n <ng-content></ng-content>\n</ef-app-main>\n\n<nav class=\"ef-app-shell-mobile__tabs\" aria-label=\"Modules\">\n @for (module of tabs(); track module.id) {\n <button\n type=\"button\"\n class=\"ef-app-shell-mobile__tab\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n [attr.aria-current]=\"activeId() === module.id ? 'page' : null\"\n (click)=\"onSelect(module)\"\n >\n <i class=\"ef-app-shell-mobile__tab-icon\" [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span class=\"ef-app-shell-mobile__tab-label\">{{ module.labelKey | translate }}</span>\n </button>\n }\n</nav>\n\n<ng-content select=\"[fab]\"></ng-content>\n\n<ef-bottom-sheet [open]=\"switcherOpen()\" (openChange)=\"onSwitcherChange($event)\">\n <h2 class=\"ef-app-shell-mobile__sheet-title\">{{ 'modules.all' | translate }}</h2>\n <ul class=\"ef-app-shell-mobile__sheet-list\">\n @for (module of allVisible(); track module.id) {\n <li>\n <button\n type=\"button\"\n class=\"ef-app-shell-mobile__sheet-item\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n (click)=\"onSelect(module)\"\n >\n <i [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span>{{ module.labelKey | translate }}</span>\n </button>\n </li>\n }\n </ul>\n</ef-bottom-sheet>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100vh;background:var(--paper);color:var(--text);overflow:hidden}.ef-app-shell-mobile__menu{display:inline-flex;align-items:center;justify-content:center;width:var(--hit-touch);height:var(--hit-touch);background:transparent;border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-size:18px}.ef-app-shell-mobile__menu:hover{background:var(--paper-alt)}.ef-app-shell-mobile__tabs{flex:0 0 auto;display:flex;align-items:stretch;height:64px;padding-bottom:env(safe-area-inset-bottom);background:var(--paper);border-top:1px solid var(--rule)}.ef-app-shell-mobile__tab{position:relative;flex:1 1 0;min-width:0;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:transparent;border:0;color:var(--text-mute);cursor:pointer;padding:6px 4px;transition:color var(--t-fast) var(--ease-out)}.ef-app-shell-mobile__tab.is-active{color:var(--text)}.ef-app-shell-mobile__tab.is-active:after{content:\"\";position:absolute;top:0;left:50%;transform:translate(-50%);width:28px;height:3px;background:var(--module);border-radius:0 0 var(--r-sm) var(--r-sm)}.ef-app-shell-mobile__tab-icon{font-size:18px;line-height:1}.ef-app-shell-mobile__tab-label{font-family:var(--font-ui);font-size:10.5px;font-weight:600;letter-spacing:-.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.ef-app-shell-mobile__sheet-title{font-family:var(--font-display);font-variation-settings:\"opsz\" 96,\"wght\" 600,\"wdth\" 95;font-size:22px;letter-spacing:-.025em;margin:0 0 12px;color:var(--text)}.ef-app-shell-mobile__sheet-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.ef-app-shell-mobile__sheet-item{display:flex;align-items:center;gap:12px;width:100%;height:var(--hit-touch);padding:0 16px;background:var(--paper-alt);border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-family:var(--font-ui);font-size:14px;font-weight:500;text-align:left;transition:background-color var(--t-fast) var(--ease-out)}.ef-app-shell-mobile__sheet-item:hover{background:var(--rule)}.ef-app-shell-mobile__sheet-item.is-active{border-left:3px solid var(--module);padding-left:13px}\n"] }]
|
|
7956
|
+
], template: "<ef-app-top>\n <ng-content select=\"[breadcrumb]\" breadcrumb></ng-content>\n <ng-content select=\"[search]\" search></ng-content>\n <button\n actions\n type=\"button\"\n class=\"ef-app-shell-mobile__menu\"\n (click)=\"openSwitcher()\"\n aria-label=\"Modules\"\n >\n <i class=\"pi pi-bars\" aria-hidden=\"true\"></i>\n </button>\n <ng-content select=\"[actions]\" actions></ng-content>\n</ef-app-top>\n\n<ef-app-main [stripe]=\"true\">\n <ng-content></ng-content>\n</ef-app-main>\n\n<nav class=\"ef-app-shell-mobile__tabs\" aria-label=\"Modules\">\n @for (module of tabs(); track module.id) {\n <button\n type=\"button\"\n class=\"ef-app-shell-mobile__tab\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n [attr.aria-current]=\"activeId() === module.id ? 'page' : null\"\n (click)=\"onSelect(module)\"\n >\n <i class=\"ef-app-shell-mobile__tab-icon\" [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span class=\"ef-app-shell-mobile__tab-label\">{{ module.labelKey | translate }}</span>\n </button>\n }\n</nav>\n\n<ng-content select=\"[fab]\"></ng-content>\n\n<ef-bottom-sheet [open]=\"switcherOpen()\" (openChange)=\"onSwitcherChange($event)\">\n <h2 class=\"ef-app-shell-mobile__sheet-title\">{{ 'modules.all' | translate }}</h2>\n <ul class=\"ef-app-shell-mobile__sheet-list\">\n @for (module of allVisible(); track module.id) {\n <li>\n <button\n type=\"button\"\n class=\"ef-app-shell-mobile__sheet-item\"\n [class.is-active]=\"activeId() === module.id\"\n [attr.data-module]=\"module.id\"\n (click)=\"onSelect(module)\"\n >\n <i [class]=\"module.icon\" aria-hidden=\"true\"></i>\n <span>{{ module.labelKey | translate }}</span>\n </button>\n </li>\n }\n </ul>\n</ef-bottom-sheet>\n", styles: [":host{display:flex;flex-direction:column;width:100%;height:100vh;height:100dvh;background:var(--paper);color:var(--text);overflow:hidden}.ef-app-shell-mobile__menu{display:inline-flex;align-items:center;justify-content:center;width:var(--hit-touch);height:var(--hit-touch);background:transparent;border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-size:18px}.ef-app-shell-mobile__menu:hover{background:var(--paper-alt)}.ef-app-shell-mobile__tabs{flex:0 0 auto;display:flex;align-items:stretch;height:64px;padding-bottom:env(safe-area-inset-bottom);background:var(--paper);border-top:1px solid var(--rule)}.ef-app-shell-mobile__tab{position:relative;flex:1 1 0;min-width:0;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;background:transparent;border:0;color:var(--text-mute);cursor:pointer;padding:6px 4px;transition:color var(--t-fast) var(--ease-out)}.ef-app-shell-mobile__tab.is-active{color:var(--text)}.ef-app-shell-mobile__tab.is-active:after{content:\"\";position:absolute;top:0;left:50%;transform:translate(-50%);width:28px;height:3px;background:var(--module);border-radius:0 0 var(--r-sm) var(--r-sm)}.ef-app-shell-mobile__tab-icon{font-size:18px;line-height:1}.ef-app-shell-mobile__tab-label{font-family:var(--font-ui);font-size:10.5px;font-weight:600;letter-spacing:-.005em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}.ef-app-shell-mobile__sheet-title{font-family:var(--font-display);font-variation-settings:\"opsz\" 96,\"wght\" 600,\"wdth\" 95;font-size:22px;letter-spacing:-.025em;margin:0 0 12px;color:var(--text)}.ef-app-shell-mobile__sheet-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(2,1fr);gap:8px}.ef-app-shell-mobile__sheet-item{display:flex;align-items:center;gap:12px;width:100%;height:var(--hit-touch);padding:0 16px;background:var(--paper-alt);border:0;border-radius:var(--r-md);color:var(--text);cursor:pointer;font-family:var(--font-ui);font-size:14px;font-weight:500;text-align:left;transition:background-color var(--t-fast) var(--ease-out)}.ef-app-shell-mobile__sheet-item:hover{background:var(--rule)}.ef-app-shell-mobile__sheet-item.is-active{border-left:3px solid var(--module);padding-left:13px}\n"] }]
|
|
7700
7957
|
}] });
|
|
7701
7958
|
|
|
7702
7959
|
/**
|