@elasticias/ui 1.0.41 → 1.0.42

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.
@@ -1338,7 +1338,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
1338
1338
  * Built-in presets (computed from `today` at open time):
1339
1339
  * `today`, `this_week`, `this_month`, `last_30_days`, `last_90_days`,
1340
1340
  * `this_quarter`, `this_year`. Override with `[presets]="customList"`
1341
- * if you need a different set.
1341
+ * if you need a different set. `all_time` (no bounds, emitted as null
1342
+ * start and end) is also recognised, and offered first in the default
1343
+ * list with `allowAllTime`.
1342
1344
  */
1343
1345
  class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
1344
1346
  host = inject(ElementRef);
@@ -1414,6 +1416,15 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
1414
1416
  /* `disabled` inherited from AbstractEfFormControl (effective = isDisabled). */
1415
1417
  /** Show a `'Personnalisé…'` row that opens the calendar view. */
1416
1418
  allowCustomRange = input(true, { ...(ngDevMode ? { debugName: "allowCustomRange" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1419
+ /**
1420
+ * Offer an `all_time` row ("Toute la période") at the top of the
1421
+ * default preset list. Picking it emits `start: null, end: null`.
1422
+ * Off by default because a report screen needs a bounded period;
1423
+ * list screens whose search treats null dates as "no filter" turn
1424
+ * it on so the user can get back to the unfiltered list. Ignored
1425
+ * when `[presets]` is supplied.
1426
+ */
1427
+ allowAllTime = input(false, { ...(ngDevMode ? { debugName: "allowAllTime" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1417
1428
  /* ── Field mode (form control) ──────────────────────────────────
1418
1429
  `'filter'` (default) is the existing smart-bar period selector:
1419
1430
  preset list + custom range, output-only via `rangeChange`.
@@ -1463,7 +1474,10 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
1463
1474
  draftStart = signal(null, ...(ngDevMode ? [{ debugName: "draftStart" }] : /* istanbul ignore next */ []));
1464
1475
  draftEnd = signal(null, ...(ngDevMode ? [{ debugName: "draftEnd" }] : /* istanbul ignore next */ []));
1465
1476
  /** Effective preset list — defaults if none provided. */
1466
- effectivePresets = computed(() => this.presets() ?? this.defaultPresets, ...(ngDevMode ? [{ debugName: "effectivePresets" }] : /* istanbul ignore next */ []));
1477
+ effectivePresets = computed(() => this.presets() ??
1478
+ (this.allowAllTime()
1479
+ ? [{ key: 'all_time', labelKey: 'date_preset_all_time' }, ...this.defaultPresets]
1480
+ : this.defaultPresets), ...(ngDevMode ? [{ debugName: "effectivePresets" }] : /* istanbul ignore next */ []));
1467
1481
  /** Current value resolved against today's date. */
1468
1482
  resolvedRange = computed(() => {
1469
1483
  const key = this.activePreset() ?? 'last_30_days';
@@ -2030,8 +2044,9 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
2030
2044
  return;
2031
2045
  }
2032
2046
  const range = this.resolvedRange();
2033
- this.draftStart.set(this.startOfDay(range.start));
2034
- this.draftEnd.set(this.startOfDay(range.end));
2047
+ // An open range has nothing to seed; the calendar opens on today.
2048
+ this.draftStart.set(range.start ? this.startOfDay(range.start) : null);
2049
+ this.draftEnd.set(range.end ? this.startOfDay(range.end) : null);
2035
2050
  }
2036
2051
  /** Compute the date range for a preset key. */
2037
2052
  computePresetRange(key) {
@@ -2045,7 +2060,7 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
2045
2060
  /** Assemble an EfDateRange — exposes `labelKey` (for the trigger
2046
2061
  * to render via `| translate`) and a literal `label` fallback. */
2047
2062
  buildRange(key, start, end) {
2048
- if (key === 'custom') {
2063
+ if (key === 'custom' && start && end) {
2049
2064
  const sameYear = start.getFullYear() === end.getFullYear();
2050
2065
  const fmt = sameYear ? 'd MMM' : 'd MMM yyyy';
2051
2066
  return {
@@ -2058,13 +2073,21 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
2058
2073
  return {
2059
2074
  start, end,
2060
2075
  presetKey: key,
2061
- labelKey: preset?.labelKey,
2076
+ // A built-in key keeps its label when the list on show does not
2077
+ // carry it (e.g. `all_time` restored on a picker without the row).
2078
+ labelKey: preset?.labelKey ?? (EfDatepickerAdvancedComponent.BUILT_IN_KEYS.has(key) ? `date_preset_${key}` : undefined),
2062
2079
  label: preset?.label ?? this.builtInHint(key),
2063
2080
  };
2064
2081
  }
2082
+ static BUILT_IN_KEYS = new Set([
2083
+ 'all_time', 'today', 'this_week', 'this_month', 'last_30_days',
2084
+ 'last_90_days', 'this_quarter', 'this_year',
2085
+ ]);
2065
2086
  builtInRange(key, today) {
2066
2087
  const t = this.startOfDay(today);
2067
2088
  switch (key) {
2089
+ case 'all_time':
2090
+ return { start: null, end: null };
2068
2091
  case 'today':
2069
2092
  return { start: t, end: t };
2070
2093
  case 'this_week': {
@@ -2093,6 +2116,8 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
2093
2116
  builtInHint(key) {
2094
2117
  const today = new Date();
2095
2118
  const r = this.builtInRange(key, today);
2119
+ if (!r.start || !r.end)
2120
+ return '';
2096
2121
  switch (key) {
2097
2122
  case 'today':
2098
2123
  return formatDate(today, 'd MMM', this.locale);
@@ -2201,7 +2226,7 @@ class EfDatepickerAdvancedComponent extends AbstractEfFormControl {
2201
2226
  trackByDay = (_, day) => day.date.getTime();
2202
2227
  trackByDow = (_, label) => label;
2203
2228
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfDatepickerAdvancedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2204
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfDatepickerAdvancedComponent, isStandalone: true, selector: "ef-datepicker-advanced", inputs: { triggerLabelKey: { classPropertyName: "triggerLabelKey", publicName: "triggerLabelKey", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, activePreset: { classPropertyName: "activePreset", publicName: "activePreset", isSignal: true, isRequired: false, transformFunction: null }, start: { classPropertyName: "start", publicName: "start", isSignal: true, isRequired: false, transformFunction: null }, end: { classPropertyName: "end", publicName: "end", isSignal: true, isRequired: false, transformFunction: null }, allowCustomRange: { classPropertyName: "allowCustomRange", publicName: "allowCustomRange", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, showTime: { classPropertyName: "showTime", publicName: "showTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rangeChange: "rangeChange", opened: "opened", closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" }, properties: { "class.dp-host--field": "mode() === 'field'" } }, viewQueries: [{ propertyName: "anchorEl", first: true, predicate: ["anchorEl"], descendants: true, static: true }, { propertyName: "panelTpl", first: true, predicate: ["panelTpl"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"dp\" [class.dp--field]=\"mode() === 'field'\" #anchorEl>\n @if (mode() === 'field' && variant() === 'field') {\n <!-- Field mode \u2014 labelled field-box trigger (form control) -->\n @if (labelKey() || label()) {\n <ef-label\n [labelKey]=\"labelKey()\"\n [label]=\"label()\"\n [for]=\"effectiveId\"\n [required]=\"required()\"\n />\n }\n <div class=\"ef-field-control\">\n <button\n type=\"button\"\n class=\"dp__field-trigger\"\n [id]=\"effectiveId\"\n [class.open]=\"open()\"\n [class.is-invalid]=\"isInvalid\"\n [class.is-placeholder]=\"!hasFieldValue()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"(showRequired || serverErrors) ? errorsId : null\"\n [attr.aria-label]=\"(labelKey() || label()) ? null : (ariaLabelKey() ? (ariaLabelKey() | translate) : (ariaLabel() || null))\"\n (click)=\"toggle($event)\"\n >\n <i class=\"ico pi pi-calendar\" aria-hidden=\"true\"></i>\n <span class=\"val\">\n {{ hasFieldValue() ? fieldDisplay() : (placeholderKey() ? (placeholderKey() | translate) : placeholder()) }}\n </span>\n <svg class=\"caret ef-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\" /></svg>\n </button>\n @if (canClear) {\n <!-- Overlaid sibling (not nested in the trigger <button>) so it\n clears instead of toggling the calendar. -->\n <ef-clear-button\n class=\"dp__field-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n @if (showRequired || serverErrors) {\n <div [id]=\"errorsId\" role=\"alert\" aria-live=\"polite\">\n @if (showRequired) {\n <small class=\"text-danger\">{{ 'validation.required' | translate }}</small>\n }\n @if (serverErrors; as errors) {\n @for (error of errors; track error) {\n <small class=\"text-danger block\">{{ error }}</small>\n }\n }\n </div>\n }\n </div>\n } @else {\n <!-- Pill trigger \u2014 filter mode, or field mode with variant=\"pill\" -->\n <button\n type=\"button\"\n class=\"dp__trigger\"\n [class.open]=\"open()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-label]=\"mode() === 'field' && !(labelKey() || label()) ? (ariaLabelKey() ? (ariaLabelKey() | translate) : (ariaLabel() || null)) : null\"\n (click)=\"toggle($event)\"\n >\n <i class=\"ico pi pi-calendar\" aria-hidden=\"true\"></i>\n @if (mode() === 'field') {\n <span class=\"val\">\n {{ hasFieldValue() ? fieldDisplay() : (placeholderKey() ? (placeholderKey() | translate) : placeholder()) }}\n </span>\n } @else {\n <span class=\"lbl\">\n {{ triggerLabelKey() ? (triggerLabelKey() | translate) : triggerLabel() }}\n </span>\n <span class=\"val\">\n @if (resolvedRange().labelKey; as key) {\n {{ key | translate }}\n } @else {\n {{ resolvedRange().label }}\n }\n </span>\n }\n <svg class=\"caret ef-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\" /></svg>\n </button>\n }\n</div>\n\n<!--\n Popover panel \u2014 instantiated via ViewContainerRef + appended to\n <body> when `open()` flips on. Anchored absolutely to the trigger\n via positionPanel(); escapes ancestor overflow / stacking contexts.\n-->\n<ng-template #panelTpl>\n <div\n class=\"dp__panel open\"\n [class.wide]=\"view() === 'custom'\"\n role=\"dialog\"\n [attr.aria-label]=\"triggerLabelKey() | translate\"\n >\n <!-- \u2500\u2500 View 1 \u2014 preset list \u2500\u2500 -->\n <div class=\"dp__view\" [class.active]=\"view() === 'presets'\">\n @for (preset of effectivePresets(); track trackByPreset($index, preset)) {\n <button\n type=\"button\"\n class=\"dp__preset\"\n [class.active]=\"isActivePreset(preset.key)\"\n (click)=\"selectPreset(preset)\"\n >\n {{ preset.labelKey ? (preset.labelKey | translate) : preset.label }}\n <span class=\"hint\">{{ presetHint(preset) }}</span>\n </button>\n }\n\n @if (allowCustomRange()) {\n <div class=\"dp__divider\" role=\"separator\"></div>\n <button\n type=\"button\"\n class=\"dp__preset muted\"\n (click)=\"openCustomView()\"\n >\n <span style=\"display:inline-flex;align-items:center;gap:8px\">\n <i class=\"ico-lead pi pi-calendar\" aria-hidden=\"true\"></i>\n {{ 'date_preset_custom' | translate }}\n </span>\n <span class=\"hint\">\u21E7</span>\n </button>\n }\n </div>\n\n <!-- \u2500\u2500 View 2 \u2014 custom range with calendar \u2500\u2500 -->\n <div class=\"dp__view\" [class.active]=\"view() === 'custom'\">\n <div class=\"dp__custom-head\">\n @if (mode() !== 'field') {\n <button\n type=\"button\"\n class=\"dp__back\"\n [attr.aria-label]=\"'common_back' | translate\"\n (click)=\"backToPresets()\"\n >\n <i class=\"pi pi-chevron-left\" aria-hidden=\"true\"></i>\n </button>\n } @else {\n <span></span>\n }\n <div class=\"dp__custom-title\">\n <span class=\"pulse-dot\"></span>\n {{ (mode() === 'field' && selectionMode() === 'single' ? 'date_select_date' : 'date_custom_range') | translate }}\n </div>\n <span></span>\n </div>\n\n <div class=\"dp__cal\">\n <div class=\"dp__cal-head\">\n <button\n type=\"button\"\n class=\"dp__cal-nav\"\n [attr.aria-label]=\"'date_prev_month' | translate\"\n (click)=\"headerPrev()\"\n >\n <i class=\"pi pi-chevron-left\" aria-hidden=\"true\"></i>\n </button>\n <div class=\"dp__cal-title\">\n @switch (calMode()) {\n @case ('years') {\n <span class=\"dp__cal-pick is-static\">{{ yearWindowLabel() }}</span>\n }\n @case ('months') {\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openYearPicker()\">\n {{ yearLabel() }}\n </button>\n }\n @default {\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openMonthPicker()\">\n {{ monthLabel() }}\n </button>\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openYearPicker()\">\n {{ yearLabel() }}\n </button>\n }\n }\n </div>\n <button\n type=\"button\"\n class=\"dp__cal-nav\"\n [attr.aria-label]=\"'date_next_month' | translate\"\n (click)=\"headerNext()\"\n >\n <i class=\"pi pi-chevron-right\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n @switch (calMode()) {\n @case ('months') {\n <div class=\"dp__cal-cells\">\n @for (cell of monthCells(); track cell.month) {\n <button\n type=\"button\"\n class=\"dp__cal-cell\"\n [class.active]=\"cell.active\"\n [class.today]=\"cell.today\"\n (click)=\"selectMonth(cell.month)\"\n >\n {{ cell.label }}\n </button>\n }\n </div>\n }\n @case ('years') {\n <div class=\"dp__cal-cells\">\n @for (cell of yearCells(); track cell.year) {\n <button\n type=\"button\"\n class=\"dp__cal-cell\"\n [class.active]=\"cell.active\"\n [class.today]=\"cell.today\"\n (click)=\"selectYear(cell.year)\"\n >\n {{ cell.year }}\n </button>\n }\n </div>\n }\n @default {\n <div class=\"dp__cal-dow-row\">\n @for (label of dowLabels(); track trackByDow($index, label)) {\n <div class=\"dp__cal-dow\">{{ label }}</div>\n }\n </div>\n\n <div class=\"dp__cal-grid\">\n @for (day of calendarGrid(); track trackByDay($index, day)) {\n <button\n type=\"button\"\n class=\"dp__cal-day\"\n [class.out]=\"day.out\"\n [class.today]=\"day.today\"\n [class.in-range]=\"day.inRange\"\n [class.range-start]=\"day.rangeStart\"\n [class.range-end]=\"day.rangeEnd\"\n [attr.aria-label]=\"day.date | date:'fullDate'\"\n (click)=\"selectDay(day.date)\"\n >\n <span class=\"day-num\">{{ day.day }}</span>\n </button>\n }\n </div>\n }\n }\n </div>\n\n @if (mode() === 'field' && selectionMode() === 'single') {\n <!-- Single-date field: date (+ time when enabled). Without time,\n picking a day commits & closes; with time, Apply commits. -->\n <div class=\"dp__inputs\" [class.dp__inputs--single]=\"!showTime()\">\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-single'\">{{ 'date_select_date' | translate }}</label>\n <input\n #singleInput\n [id]=\"effectiveId + '-single'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"fromText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, singleInput)\"\n (change)=\"commitSingleInput(singleInput)\"\n />\n </div>\n @if (showTime()) {\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-single-time'\">{{ 'date_time' | translate }}</label>\n <input\n #singleTime\n [id]=\"effectiveId + '-single-time'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"fromTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, singleTime)\"\n (change)=\"commitTimeInput('start', singleTime)\"\n />\n </div>\n }\n </div>\n } @else {\n <div class=\"dp__inputs\">\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-from'\">{{ 'date_from' | translate }}</label>\n <input\n #fromInput\n [id]=\"effectiveId + '-from'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"fromText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, fromInput)\"\n (change)=\"commitDateInput('start', fromInput)\"\n />\n @if (mode() === 'field' && showTime()) {\n <input\n #fromTime\n [attr.aria-label]=\"'date_time' | translate\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"fromTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, fromTime)\"\n (change)=\"commitTimeInput('start', fromTime)\"\n />\n }\n </div>\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-to'\">{{ 'date_to' | translate }}</label>\n <input\n #toInput\n [id]=\"effectiveId + '-to'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"toText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, toInput)\"\n (change)=\"commitDateInput('end', toInput)\"\n />\n @if (mode() === 'field' && showTime()) {\n <input\n #toTime\n [attr.aria-label]=\"'date_time' | translate\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"toTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, toTime)\"\n (change)=\"commitTimeInput('end', toTime)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Footer: hidden only for single field WITHOUT time (commit on pick). -->\n @if (!(mode() === 'field' && selectionMode() === 'single' && !showTime())) {\n <div class=\"dp__actions\">\n @if (mode() === 'field' && selectionMode() === 'single') {\n <span></span>\n } @else {\n <span class=\"duration\">\n <span class=\"num\">{{ draftDurationDays() }}</span>\n {{ 'date_days' | translate }}\n </span>\n }\n <div class=\"right\">\n @if (mode() !== 'field') {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-sm\"\n (click)=\"backToPresets()\"\n >\n {{ 'common_back' | translate }}\n </button>\n }\n <button\n type=\"button\"\n class=\"btn btn-primary btn-sm\"\n [disabled]=\"!draftStart()\"\n (click)=\"applyCustomRange()\"\n >\n {{ 'common_apply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:inline-block}:host(.dp-host--field){display:block}.hint{unicode-bidi:plaintext}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "for", "styleClass", "required"] }, { kind: "component", type: EfClearButtonComponent, selector: "ef-clear-button", inputs: ["ariaLabel", "disabled"], outputs: ["clear"] }, { kind: "pipe", type: i1$3.DatePipe, name: "date" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2229
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfDatepickerAdvancedComponent, isStandalone: true, selector: "ef-datepicker-advanced", inputs: { triggerLabelKey: { classPropertyName: "triggerLabelKey", publicName: "triggerLabelKey", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, activePreset: { classPropertyName: "activePreset", publicName: "activePreset", isSignal: true, isRequired: false, transformFunction: null }, start: { classPropertyName: "start", publicName: "start", isSignal: true, isRequired: false, transformFunction: null }, end: { classPropertyName: "end", publicName: "end", isSignal: true, isRequired: false, transformFunction: null }, allowCustomRange: { classPropertyName: "allowCustomRange", publicName: "allowCustomRange", isSignal: true, isRequired: false, transformFunction: null }, allowAllTime: { classPropertyName: "allowAllTime", publicName: "allowAllTime", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, showTime: { classPropertyName: "showTime", publicName: "showTime", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rangeChange: "rangeChange", opened: "opened", closed: "closed" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" }, properties: { "class.dp-host--field": "mode() === 'field'" } }, viewQueries: [{ propertyName: "anchorEl", first: true, predicate: ["anchorEl"], descendants: true, static: true }, { propertyName: "panelTpl", first: true, predicate: ["panelTpl"], descendants: true, static: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"dp\" [class.dp--field]=\"mode() === 'field'\" #anchorEl>\n @if (mode() === 'field' && variant() === 'field') {\n <!-- Field mode \u2014 labelled field-box trigger (form control) -->\n @if (labelKey() || label()) {\n <ef-label\n [labelKey]=\"labelKey()\"\n [label]=\"label()\"\n [for]=\"effectiveId\"\n [required]=\"required()\"\n />\n }\n <div class=\"ef-field-control\">\n <button\n type=\"button\"\n class=\"dp__field-trigger\"\n [id]=\"effectiveId\"\n [class.open]=\"open()\"\n [class.is-invalid]=\"isInvalid\"\n [class.is-placeholder]=\"!hasFieldValue()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"(showRequired || serverErrors) ? errorsId : null\"\n [attr.aria-label]=\"(labelKey() || label()) ? null : (ariaLabelKey() ? (ariaLabelKey() | translate) : (ariaLabel() || null))\"\n (click)=\"toggle($event)\"\n >\n <i class=\"ico pi pi-calendar\" aria-hidden=\"true\"></i>\n <span class=\"val\">\n {{ hasFieldValue() ? fieldDisplay() : (placeholderKey() ? (placeholderKey() | translate) : placeholder()) }}\n </span>\n <svg class=\"caret ef-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\" /></svg>\n </button>\n @if (canClear) {\n <!-- Overlaid sibling (not nested in the trigger <button>) so it\n clears instead of toggling the calendar. -->\n <ef-clear-button\n class=\"dp__field-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n @if (showRequired || serverErrors) {\n <div [id]=\"errorsId\" role=\"alert\" aria-live=\"polite\">\n @if (showRequired) {\n <small class=\"text-danger\">{{ 'validation.required' | translate }}</small>\n }\n @if (serverErrors; as errors) {\n @for (error of errors; track error) {\n <small class=\"text-danger block\">{{ error }}</small>\n }\n }\n </div>\n }\n </div>\n } @else {\n <!-- Pill trigger \u2014 filter mode, or field mode with variant=\"pill\" -->\n <button\n type=\"button\"\n class=\"dp__trigger\"\n [class.open]=\"open()\"\n [disabled]=\"isDisabled()\"\n [attr.aria-haspopup]=\"'dialog'\"\n [attr.aria-expanded]=\"open()\"\n [attr.aria-label]=\"mode() === 'field' && !(labelKey() || label()) ? (ariaLabelKey() ? (ariaLabelKey() | translate) : (ariaLabel() || null)) : null\"\n (click)=\"toggle($event)\"\n >\n <i class=\"ico pi pi-calendar\" aria-hidden=\"true\"></i>\n @if (mode() === 'field') {\n <span class=\"val\">\n {{ hasFieldValue() ? fieldDisplay() : (placeholderKey() ? (placeholderKey() | translate) : placeholder()) }}\n </span>\n } @else {\n <span class=\"lbl\">\n {{ triggerLabelKey() ? (triggerLabelKey() | translate) : triggerLabel() }}\n </span>\n <span class=\"val\">\n @if (resolvedRange().labelKey; as key) {\n {{ key | translate }}\n } @else {\n {{ resolvedRange().label }}\n }\n </span>\n }\n <svg class=\"caret ef-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polyline points=\"6 9 12 15 18 9\" /></svg>\n </button>\n }\n</div>\n\n<!--\n Popover panel \u2014 instantiated via ViewContainerRef + appended to\n <body> when `open()` flips on. Anchored absolutely to the trigger\n via positionPanel(); escapes ancestor overflow / stacking contexts.\n-->\n<ng-template #panelTpl>\n <div\n class=\"dp__panel open\"\n [class.wide]=\"view() === 'custom'\"\n role=\"dialog\"\n [attr.aria-label]=\"triggerLabelKey() | translate\"\n >\n <!-- \u2500\u2500 View 1 \u2014 preset list \u2500\u2500 -->\n <div class=\"dp__view\" [class.active]=\"view() === 'presets'\">\n @for (preset of effectivePresets(); track trackByPreset($index, preset)) {\n <button\n type=\"button\"\n class=\"dp__preset\"\n [class.active]=\"isActivePreset(preset.key)\"\n (click)=\"selectPreset(preset)\"\n >\n {{ preset.labelKey ? (preset.labelKey | translate) : preset.label }}\n <span class=\"hint\">{{ presetHint(preset) }}</span>\n </button>\n }\n\n @if (allowCustomRange()) {\n <div class=\"dp__divider\" role=\"separator\"></div>\n <button\n type=\"button\"\n class=\"dp__preset muted\"\n (click)=\"openCustomView()\"\n >\n <span style=\"display:inline-flex;align-items:center;gap:8px\">\n <i class=\"ico-lead pi pi-calendar\" aria-hidden=\"true\"></i>\n {{ 'date_preset_custom' | translate }}\n </span>\n <span class=\"hint\">\u21E7</span>\n </button>\n }\n </div>\n\n <!-- \u2500\u2500 View 2 \u2014 custom range with calendar \u2500\u2500 -->\n <div class=\"dp__view\" [class.active]=\"view() === 'custom'\">\n <div class=\"dp__custom-head\">\n @if (mode() !== 'field') {\n <button\n type=\"button\"\n class=\"dp__back\"\n [attr.aria-label]=\"'common_back' | translate\"\n (click)=\"backToPresets()\"\n >\n <i class=\"pi pi-chevron-left\" aria-hidden=\"true\"></i>\n </button>\n } @else {\n <span></span>\n }\n <div class=\"dp__custom-title\">\n <span class=\"pulse-dot\"></span>\n {{ (mode() === 'field' && selectionMode() === 'single' ? 'date_select_date' : 'date_custom_range') | translate }}\n </div>\n <span></span>\n </div>\n\n <div class=\"dp__cal\">\n <div class=\"dp__cal-head\">\n <button\n type=\"button\"\n class=\"dp__cal-nav\"\n [attr.aria-label]=\"'date_prev_month' | translate\"\n (click)=\"headerPrev()\"\n >\n <i class=\"pi pi-chevron-left\" aria-hidden=\"true\"></i>\n </button>\n <div class=\"dp__cal-title\">\n @switch (calMode()) {\n @case ('years') {\n <span class=\"dp__cal-pick is-static\">{{ yearWindowLabel() }}</span>\n }\n @case ('months') {\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openYearPicker()\">\n {{ yearLabel() }}\n </button>\n }\n @default {\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openMonthPicker()\">\n {{ monthLabel() }}\n </button>\n <button type=\"button\" class=\"dp__cal-pick\" (click)=\"openYearPicker()\">\n {{ yearLabel() }}\n </button>\n }\n }\n </div>\n <button\n type=\"button\"\n class=\"dp__cal-nav\"\n [attr.aria-label]=\"'date_next_month' | translate\"\n (click)=\"headerNext()\"\n >\n <i class=\"pi pi-chevron-right\" aria-hidden=\"true\"></i>\n </button>\n </div>\n\n @switch (calMode()) {\n @case ('months') {\n <div class=\"dp__cal-cells\">\n @for (cell of monthCells(); track cell.month) {\n <button\n type=\"button\"\n class=\"dp__cal-cell\"\n [class.active]=\"cell.active\"\n [class.today]=\"cell.today\"\n (click)=\"selectMonth(cell.month)\"\n >\n {{ cell.label }}\n </button>\n }\n </div>\n }\n @case ('years') {\n <div class=\"dp__cal-cells\">\n @for (cell of yearCells(); track cell.year) {\n <button\n type=\"button\"\n class=\"dp__cal-cell\"\n [class.active]=\"cell.active\"\n [class.today]=\"cell.today\"\n (click)=\"selectYear(cell.year)\"\n >\n {{ cell.year }}\n </button>\n }\n </div>\n }\n @default {\n <div class=\"dp__cal-dow-row\">\n @for (label of dowLabels(); track trackByDow($index, label)) {\n <div class=\"dp__cal-dow\">{{ label }}</div>\n }\n </div>\n\n <div class=\"dp__cal-grid\">\n @for (day of calendarGrid(); track trackByDay($index, day)) {\n <button\n type=\"button\"\n class=\"dp__cal-day\"\n [class.out]=\"day.out\"\n [class.today]=\"day.today\"\n [class.in-range]=\"day.inRange\"\n [class.range-start]=\"day.rangeStart\"\n [class.range-end]=\"day.rangeEnd\"\n [attr.aria-label]=\"day.date | date:'fullDate'\"\n (click)=\"selectDay(day.date)\"\n >\n <span class=\"day-num\">{{ day.day }}</span>\n </button>\n }\n </div>\n }\n }\n </div>\n\n @if (mode() === 'field' && selectionMode() === 'single') {\n <!-- Single-date field: date (+ time when enabled). Without time,\n picking a day commits & closes; with time, Apply commits. -->\n <div class=\"dp__inputs\" [class.dp__inputs--single]=\"!showTime()\">\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-single'\">{{ 'date_select_date' | translate }}</label>\n <input\n #singleInput\n [id]=\"effectiveId + '-single'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"fromText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, singleInput)\"\n (change)=\"commitSingleInput(singleInput)\"\n />\n </div>\n @if (showTime()) {\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-single-time'\">{{ 'date_time' | translate }}</label>\n <input\n #singleTime\n [id]=\"effectiveId + '-single-time'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"fromTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, singleTime)\"\n (change)=\"commitTimeInput('start', singleTime)\"\n />\n </div>\n }\n </div>\n } @else {\n <div class=\"dp__inputs\">\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-from'\">{{ 'date_from' | translate }}</label>\n <input\n #fromInput\n [id]=\"effectiveId + '-from'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"fromText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, fromInput)\"\n (change)=\"commitDateInput('start', fromInput)\"\n />\n @if (mode() === 'field' && showTime()) {\n <input\n #fromTime\n [attr.aria-label]=\"'date_time' | translate\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"fromTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, fromTime)\"\n (change)=\"commitTimeInput('start', fromTime)\"\n />\n }\n </div>\n <div class=\"dp__field\">\n <label [attr.for]=\"effectiveId + '-to'\">{{ 'date_to' | translate }}</label>\n <input\n #toInput\n [id]=\"effectiveId + '-to'\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"10\"\n [value]=\"toText()\"\n placeholder=\"JJ/MM/AAAA\"\n (keydown)=\"onDateKeydown($event, toInput)\"\n (change)=\"commitDateInput('end', toInput)\"\n />\n @if (mode() === 'field' && showTime()) {\n <input\n #toTime\n [attr.aria-label]=\"'date_time' | translate\"\n type=\"text\"\n inputmode=\"numeric\"\n maxlength=\"5\"\n [value]=\"toTimeText()\"\n placeholder=\"HH:mm\"\n (keydown)=\"onTimeKeydown($event, toTime)\"\n (change)=\"commitTimeInput('end', toTime)\"\n />\n }\n </div>\n </div>\n }\n\n <!-- Footer: hidden only for single field WITHOUT time (commit on pick). -->\n @if (!(mode() === 'field' && selectionMode() === 'single' && !showTime())) {\n <div class=\"dp__actions\">\n @if (mode() === 'field' && selectionMode() === 'single') {\n <span></span>\n } @else {\n <span class=\"duration\">\n <span class=\"num\">{{ draftDurationDays() }}</span>\n {{ 'date_days' | translate }}\n </span>\n }\n <div class=\"right\">\n @if (mode() !== 'field') {\n <button\n type=\"button\"\n class=\"btn btn-ghost btn-sm\"\n (click)=\"backToPresets()\"\n >\n {{ 'common_back' | translate }}\n </button>\n }\n <button\n type=\"button\"\n class=\"btn btn-primary btn-sm\"\n [disabled]=\"!draftStart()\"\n (click)=\"applyCustomRange()\"\n >\n {{ 'common_apply' | translate }}\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:inline-block}:host(.dp-host--field){display:block}.hint{unicode-bidi:plaintext}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "for", "styleClass", "required"] }, { kind: "component", type: EfClearButtonComponent, selector: "ef-clear-button", inputs: ["ariaLabel", "disabled"], outputs: ["clear"] }, { kind: "pipe", type: i1$3.DatePipe, name: "date" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2205
2230
  }
2206
2231
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfDatepickerAdvancedComponent, decorators: [{
2207
2232
  type: Component,
@@ -2212,7 +2237,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
2212
2237
  }], panelTpl: [{
2213
2238
  type: ViewChild,
2214
2239
  args: ['panelTpl', { static: true }]
2215
- }], triggerLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabelKey", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], activePreset: [{ type: i0.Input, args: [{ isSignal: true, alias: "activePreset", required: false }] }], start: [{ type: i0.Input, args: [{ isSignal: true, alias: "start", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }], allowCustomRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomRange", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], showTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTime", required: false }] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], onDocumentClick: [{
2240
+ }], triggerLabelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabelKey", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], activePreset: [{ type: i0.Input, args: [{ isSignal: true, alias: "activePreset", required: false }] }], start: [{ type: i0.Input, args: [{ isSignal: true, alias: "start", required: false }] }], end: [{ type: i0.Input, args: [{ isSignal: true, alias: "end", required: false }] }], allowCustomRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomRange", required: false }] }], allowAllTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowAllTime", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], showTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTime", required: false }] }], rangeChange: [{ type: i0.Output, args: ["rangeChange"] }], opened: [{ type: i0.Output, args: ["opened"] }], closed: [{ type: i0.Output, args: ["closed"] }], onDocumentClick: [{
2216
2241
  type: HostListener,
2217
2242
  args: ['document:click', ['$event']]
2218
2243
  }], onEscape: [{
@@ -5334,7 +5359,9 @@ class EfDataCardComponent {
5334
5359
  return;
5335
5360
  const field = col.sortField ?? col.field ?? col.id;
5336
5361
  const current = this.sort();
5337
- const direction = current?.field === field && current.direction === 'desc' ? 'asc' : 'desc';
5362
+ const direction = current?.field === field
5363
+ ? current.direction === 'desc' ? 'asc' : 'desc'
5364
+ : col.firstSortDirection ?? 'desc';
5338
5365
  this.sortChange.emit({ field, direction });
5339
5366
  }
5340
5367
  trackByRow = (_, row) => {
@@ -5343,7 +5370,7 @@ class EfDataCardComponent {
5343
5370
  };
5344
5371
  trackByColumn = (_, col) => col.id;
5345
5372
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfDataCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5346
- 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{padding-top:4px;padding-bottom:4px}.tbl-wrap[data-density=comfortable] .tbl tbody td{padding-top:16px;padding-bottom:16px}.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 });
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 });
5347
5374
  }
5348
5375
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfDataCardComponent, decorators: [{
5349
5376
  type: Component,
@@ -5356,7 +5383,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
5356
5383
  EfRowActionsComponent,
5357
5384
  EfOverflowTooltipDirective,
5358
5385
  EfTooltipDirective,
5359
- ], 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{padding-top:4px;padding-bottom:4px}.tbl-wrap[data-density=comfortable] .tbl tbody td{padding-top:16px;padding-bottom:16px}.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"] }]
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"] }]
5360
5387
  }], 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: [{
5361
5388
  type: ContentChildren,
5362
5389
  args: [EfColumnTemplateDirective]
@@ -5857,7 +5884,7 @@ class EfOrderBuilderComponent {
5857
5884
  return OrderEntityHelper.calculateTotals(this.order());
5858
5885
  }
5859
5886
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfOrderBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5860
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfOrderBuilderComponent, isStandalone: true, selector: "ef-order-builder", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, order: { classPropertyName: "order", publicName: "order", isSignal: true, isRequired: true, transformFunction: null }, products: { classPropertyName: "products", publicName: "products", isSignal: true, isRequired: true, transformFunction: null }, customers: { classPropertyName: "customers", publicName: "customers", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, countGroupLabels: { classPropertyName: "countGroupLabels", publicName: "countGroupLabels", isSignal: true, isRequired: false, transformFunction: null }, productKeyField: { classPropertyName: "productKeyField", publicName: "productKeyField", isSignal: true, isRequired: false, transformFunction: null }, backendErrors: { classPropertyName: "backendErrors", publicName: "backendErrors", isSignal: true, isRequired: false, transformFunction: null }, auditEntries: { classPropertyName: "auditEntries", publicName: "auditEntries", isSignal: true, isRequired: false, transformFunction: null }, focusMode: { classPropertyName: "focusMode", publicName: "focusMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { order: "orderChange", openCatalogue: "openCatalogue", orderChanged: "orderChanged", orderValidated: "orderValidated", productsAdded: "productsAdded", focusMode: "focusModeChange" }, viewQueries: [{ propertyName: "typeahead", first: true, predicate: EfProductTypeaheadComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"order-builder\"\n [class.ob-collapsed]=\"focusMode()\"\n [class.ob-rail-open]=\"railOpen()\">\n <div class=\"ob-grid\">\n <div class=\"ob-main\">\n <!-- Order header: customer + date + express add (one card) -->\n <ef-card\n titleKey=\"order_builder.section_order\"\n [metaKey]=\"readonly() ? '' : 'order_builder.quick_add_hint'\"\n [overflowVisible]=\"true\">\n <div class=\"ob-order-head\">\n <div class=\"form-grid\">\n <div class=\"field col-6\">\n <ef-label labelKey=\"label.customer\" for=\"ob-cust\" [required]=\"true\" />\n <ef-select\n inputId=\"ob-cust\"\n [options]=\"customers()\"\n optionLabel=\"companyName\"\n optionValue=\"customerId\"\n [ngModel]=\"order().customerId\"\n (ngModelChange)=\"updateCustomer($event)\"\n [disabled]=\"readonly()\"\n [showClear]=\"true\"\n [filter]=\"true\"\n filterBy=\"companyName\"\n filterPlaceholderKey=\"label.search_customer\"\n appendTo=\"body\"\n placeholderKey=\"label.select_customer\" />\n </div>\n <div class=\"field col-3\">\n <ef-label labelKey=\"label.order_date\" for=\"ob-date\" [required]=\"true\" />\n <ef-datepicker-advanced\n mode=\"field\"\n selectionMode=\"single\"\n inputId=\"ob-date\"\n placeholderKey=\"date_select_date\"\n [ngModel]=\"order().orderDate\"\n (ngModelChange)=\"updateOrderDate($event)\"\n [disabled]=\"readonly()\" />\n </div>\n </div>\n\n @if (!readonly()) {\n <div class=\"qa-grid\">\n <div class=\"qa-combo-wrap\">\n <ef-label labelKey=\"label.article\" />\n <ef-product-typeahead\n [products]=\"products()\"\n [productKeyField]=\"productKeyField()\"\n placeholderKey=\"label.search_product\"\n [currency]=\"currency()\"\n [locale]=\"locale()\"\n (productSelect)=\"onTypeaheadSelect($event)\"\n [showVariants]=\"false\" />\n </div>\n <div class=\"qa-price\">\n <ef-label labelKey=\"label.sale_price\" />\n <ef-inputnumber\n variant=\"comptoir\"\n [value]=\"quickAddPrice()\"\n (valueChangeEvent)=\"quickAddPrice.set($event ?? 0)\"\n [minFractionDigits]=\"2\"\n [maxFractionDigits]=\"2\"\n [locale]=\"locale()\"\n [disabled]=\"readonly()\" />\n </div>\n <div class=\"qa-qtyf\">\n <ef-label labelKey=\"label.quantity\" />\n <ef-inputnumber\n variant=\"comptoir\"\n [ngModel]=\"quickAddQuantity()\"\n (ngModelChange)=\"quickAddQuantity.set($event)\"\n [min]=\"0\"\n [maxFractionDigits]=\"4\"\n [locale]=\"locale()\"\n [disabled]=\"readonly()\"\n />\n </div>\n <div class=\"qa-go\">\n <!-- Ghost while the preconditions are unmet, tenant once the\n line is addable. A disabled control painted in a faded\n tenant fill reads as an enabled button that lost its\n colour, which is the greyed-out accent the design brief\n bans; ghost reads as \"not yet\". -->\n <ef-button\n labelKey=\"action.add\"\n icon=\"pi pi-plus\"\n variant=\"comptoir\"\n size=\"base\"\n [severity]=\"quickAddDisabled() ? 'ghost' : 'tenant'\"\n [disabled]=\"quickAddDisabled()\"\n (clickEvent)=\"addQuickProduct()\" />\n </div>\n </div>\n }\n </div>\n </ef-card>\n\n <!-- Lines.\n\n ef-data-card owns the panel here instead of sitting inside an\n ef-card: `.tbl-wrap` already draws a bordered, rounded surface, so\n nesting it in another one would be a card inside a card. The title\n and the card's actions move into the head slots it provides.\n\n The columns are declared in TS (`lineColumns`) so discount and tax\n follow the document config, and so one definition drives both the\n desktop table and the mobile cards. Only the article column is\n elastic; every other column holds a number of known magnitude and\n gets a fixed track, which keeps the decimals aligned down the\n table rather than shifting with the longest description. -->\n <ef-data-card\n [columns]=\"lineColumns()\"\n [rows]=\"orderLines()\"\n trackByField=\"id\"\n [hidePager]=\"true\"\n defaultDensity=\"compact\"\n [reorderable]=\"!readonly()\"\n [fixedLayout]=\"true\"\n (rowReorder)=\"reorderLine($event)\"\n mobileLayout=\"card\">\n\n <div tbl-head-info class=\"ob-lines-head\">\n <span class=\"strong\">{{ 'order_builder.lines' | translate }}</span>\n <span class=\"meta\">\u00B7 {{ orderLines().length }}</span>\n </div>\n\n <div tbl-head-actions class=\"head-actions\">\n @if (config().showCatalogue && !readonly()) {\n <ef-button\n labelKey=\"action.open_catalogue\"\n icon=\"pi pi-box\"\n severity=\"secondary\"\n [text]=\"true\"\n (clickEvent)=\"handleOpenCatalogue()\" />\n }\n <ef-button\n [labelKey]=\"focusMode() ? 'order_builder.show_summary' : 'order_builder.fullscreen'\"\n icon=\"pi pi-window-maximize\"\n severity=\"secondary\"\n [text]=\"true\"\n (clickEvent)=\"toggleFocus()\" />\n </div>\n\n <span empty-state>{{ 'order_builder.empty' | translate }}</span>\n <span empty-state-mobile>{{ 'order_builder.empty' | translate }}</span>\n\n <ng-template efColumnTemplate=\"product\" let-item let-i=\"index\">\n <div class=\"p\">\n <span class=\"ln\">{{ i + 1 }}</span>\n <span class=\"pp\">\n <span class=\"nm\">{{ item.productDescription }}</span>\n </span>\n </div>\n </ng-template>\n\n <ng-template efColumnTemplate=\"price\" let-item>\n @if (!readonly() && isEditingCell(item, 'price')) {\n <ef-inputnumber\n [(ngModel)]=\"item.productUnitPrice\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [minFractionDigits]=\"2\"\n [maxFractionDigits]=\"2\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'price')\">\n {{ item.productUnitPrice | number: '1.2-2' : locale() }}\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"qty\" let-item>\n @if (!readonly() && isEditingCell(item, 'qty')) {\n <ef-inputnumber\n [(ngModel)]=\"item.quantity\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [maxFractionDigits]=\"4\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'qty')\">\n {{ item.quantity | number: '1.0-4' : locale() }}\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"discount\" let-item>\n @if (!readonly() && isEditingCell(item, 'discount')) {\n <ef-inputnumber\n [(ngModel)]=\"item.discount\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [max]=\"100\"\n [maxFractionDigits]=\"2\"\n suffix=\" %\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'discount')\">\n {{ item.discount | number: '1.0-2' : locale() }} %\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"tax\" let-item>\n @if (!readonly() && isEditingCell(item, 'tva')) {\n <ef-inputnumber\n [(ngModel)]=\"item.taxRate\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [max]=\"100\"\n [maxFractionDigits]=\"2\"\n suffix=\" %\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'tva')\">\n {{ (item.taxRate ?? 0) | number: '1.0-2' : locale() }} %\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"subtotal\" let-item>\n {{ calculateSubtotal(item) | number: '1.2-2' : locale() }}\n </ng-template>\n\n <ng-template efColumnTemplate=\"remove\" let-item>\n <ef-button\n icon=\"pi pi-trash\"\n severity=\"danger\"\n [text]=\"true\"\n styleClass=\"ob-del\"\n (clickEvent)=\"removeOrderLine(item.id)\" />\n </ng-template>\n </ef-data-card>\n\n </div>\n\n <!-- CONTEXT RAIL (1/3) -->\n <aside class=\"ob-rail\">\n <!-- Totaux (top) -->\n <ef-card titleKey=\"order_builder.totals\" [meta]=\"currency()\">\n <div class=\"ob-totals\">\n <div class=\"row\">\n <span class=\"lbl\">{{ 'order_builder.gross' | translate }}</span>\n <span class=\"v\">{{ grossTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <div class=\"row discount\">\n <span class=\"lbl\">{{ 'order_builder.discount' | translate }}</span>\n <span class=\"v\">\u2212{{ discountTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <div class=\"row\">\n <span class=\"lbl\">{{ 'order_builder.tax' | translate }}</span>\n <span class=\"v\">{{ tvaTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <hr />\n <div class=\"row grand\">\n <span class=\"lbl\">{{ 'order_builder.grand_total' | translate }}</span>\n <span class=\"v\">{{ finalTotal() | number: '1.2-2' : locale() }}<span class=\"cur\">{{ currency() }}</span></span>\n </div>\n </div>\n </ef-card>\n\n <!-- R\u00E9capitulatif par format -->\n @if (productsSummary().items.length > 0) {\n <ef-card titleKey=\"order_builder.recap\" metaKey=\"order_builder.auto\">\n <div class=\"ob-recap\">\n <div class=\"cols\">\n @for (col of recapColumns(); track $index) {\n <div class=\"grp\">\n @for (seg of col; track seg.code) {\n <div class=\"seg\">\n <span class=\"fmt\">{{ seg.label }}</span>\n <span class=\"n\">{{ seg.count | number: '1.0-0' : locale() }}</span>\n </div>\n }\n </div>\n }\n </div>\n <div class=\"tot\">\n <span class=\"l\">{{ 'order_builder.total_units' | translate }}</span>\n <span class=\"n\">{{ productsSummary().totalCount | number: '1.0-0' : locale() }}</span>\n </div>\n </div>\n </ef-card>\n }\n\n <!-- Historique (optional, component-owned) -->\n @if (hasAudit()) {\n <ef-change-history [entries]=\"auditEntries()\" />\n }\n </aside>\n </div>\n\n <!-- Floating summary FAB + backdrop -->\n <button type=\"button\" class=\"ob-backdrop\" aria-hidden=\"true\" tabindex=\"-1\" (click)=\"toggleRail(false)\"></button>\n <div class=\"ob-fab\" role=\"status\">\n <div class=\"ft\">\n <span class=\"k\">{{ 'order_builder.total_units' | translate }} \u00B7 {{ productsSummary().totalCount }}</span>\n <span class=\"vv\">{{ finalTotal() | number: '1.2-2' : locale() }}<span class=\"cur\">{{ currency() }}</span></span>\n </div>\n <button type=\"button\" class=\"open\" (click)=\"toggleRail(true)\">\n {{ 'order_builder.summary' | translate }}\n </button>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}:host .order-builder{display:grid;gap:var(--s-3)}:host .order-builder .head-actions{display:flex;align-items:center;gap:6px}:host .order-builder .form-grid{display:grid;gap:var(--s-3);grid-template-columns:repeat(12,minmax(0,1fr))}:host .order-builder .form-grid>.col-3{grid-column:span 3}:host .order-builder .form-grid>.col-4{grid-column:span 4}:host .order-builder .form-grid>.col-5{grid-column:span 5}:host .order-builder .form-grid>.col-6{grid-column:span 6}@media(max-width:767px){:host .order-builder .form-grid>[class*=col-]{grid-column:span 12}}:host .order-builder .field{display:grid;gap:4px}:host .order-builder .field>label{font-size:11px;font-weight:700;color:var(--text-mute);text-transform:uppercase;letter-spacing:.06em}:host .order-builder .ob-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);align-items:start}@media(min-width:1280px){:host .order-builder .ob-grid{grid-template-columns:minmax(0,2fr) minmax(0,1fr)}}:host .order-builder .ob-main{display:grid;gap:var(--s-3);min-width:0}:host .order-builder .ob-rail{display:grid;gap:var(--s-3);align-content:start}@media(min-width:1280px){:host .order-builder .ob-rail{position:sticky;top:44px}}:host .order-builder .ob-order-head{display:grid;gap:var(--s-4)}:host .order-builder .qa-grid{display:flex;flex-wrap:wrap;gap:10px;align-items:end;padding-top:var(--s-3);border-top:1px solid var(--rule-soft)}:host .order-builder .qa-combo-wrap{flex:1 1 260px;min-width:0}:host .order-builder .qa-price{flex:0 1 132px}:host .order-builder .qa-qtyf{flex:0 0 132px}:host .order-builder .qa-go{flex:0 0 auto}@media(max-width:560px){:host .order-builder .qa-combo-wrap,:host .order-builder .qa-price,:host .order-builder .qa-qtyf,:host .order-builder .qa-go{flex:1 1 100%}}:host .order-builder ef-data-card{font-size:13px}:host .order-builder ef-data-card .p{display:flex;align-items:baseline;gap:9px;line-height:1.25}:host .order-builder ef-data-card .p .ln{flex:0 0 auto;min-width:15px;text-align:end;font-family:var(--font-mono);font-size:10.5px;font-weight:700;color:var(--text-soft)}:host .order-builder ef-data-card .p .pp{display:grid;gap:1px;min-width:0}:host .order-builder ef-data-card .p .nm{font-weight:600;letter-spacing:-.005em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host .order-builder ef-data-card .p .nm em{font-style:normal;color:var(--text-mute);font-weight:600}:host .order-builder ef-data-card ef-inputnumber,:host .order-builder ef-data-card ef-inputnumber .ef-field-control,:host .order-builder ef-data-card ef-inputnumber input{width:100%;min-width:0}:host .order-builder ef-data-card .num,:host .order-builder ef-data-card .total{font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums}:host .order-builder ef-data-card .ln-val{appearance:none;width:100%;margin:0;padding:5px 8px;border:1px solid transparent;border-radius:7px;background:transparent;font:inherit;font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums;color:inherit;text-align:end;cursor:text;transition:background .12s ease,border-color .12s ease}:host .order-builder ef-data-card .ln-val:hover:not(:disabled){background:var(--paper);border-color:var(--rule)}:host .order-builder ef-data-card .ln-val:focus-visible{outline:none;background:var(--paper);border-color:var(--tenant, var(--rule))}:host .order-builder ef-data-card .ln-val:disabled{cursor:default;background:transparent;border-color:transparent}:host .order-builder ef-data-card .total{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:13.5px}:host .order-builder ef-data-card .cur{font-size:9.5px;color:var(--text-soft);margin-inline-start:3px}:host .order-builder .ob-lines-head{display:flex;align-items:baseline;gap:6px;min-width:0}:host .order-builder .ob-lines-head .strong{font-weight:700;color:var(--text)}:host .order-builder .ob-lines-head .meta{color:var(--text-soft)}:host .order-builder .ob-del{color:var(--text-soft)}:host .order-builder .ob-del:hover{color:var(--st-cancelled-fg);border-color:var(--st-cancelled-bg)}:host .order-builder .ob-totals{display:grid;gap:6px}:host .order-builder .ob-totals .row{display:flex;justify-content:space-between;align-items:baseline;gap:14px}:host .order-builder .ob-totals .row .lbl{font-size:12px;font-weight:600;color:var(--text-mute)}:host .order-builder .ob-totals .row .v{font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums;font-weight:700;font-size:12.5px}:host .order-builder .ob-totals .row.discount .v{color:var(--st-pending-fg)}:host .order-builder .ob-totals hr{border:0;border-top:1px solid var(--rule);margin:5px 0}:host .order-builder .ob-totals .grand .lbl{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:13.5px;color:var(--text)}:host .order-builder .ob-totals .grand .v{font-family:var(--font-display);font-variation-settings:\"opsz\" 96,\"wght\" 700,\"wdth\" 80;font-size:26px;letter-spacing:-.03em;color:var(--text)}:host .order-builder .ob-totals .grand .cur{font-family:var(--font-mono);font-size:.4em;color:var(--text-mute);margin-inline-start:4px}:host .order-builder .ob-recap .cols{display:grid;grid-template-columns:1fr 1fr}:host .order-builder .ob-recap .grp{display:flex;flex-direction:column;gap:3px;min-width:0}:host .order-builder .ob-recap .grp:first-child{padding-inline-end:14px}:host .order-builder .ob-recap .grp+.grp{padding-inline-start:14px;border-inline-start:1px solid var(--rule)}:host .order-builder .ob-recap .seg{display:flex;justify-content:space-between;align-items:baseline;gap:8px}:host .order-builder .ob-recap .seg .fmt{font-size:11.5px;font-weight:700;color:var(--text);letter-spacing:-.005em;min-width:0}:host .order-builder .ob-recap .seg .fmt:after{content:\":\";color:var(--text-soft)}:host .order-builder .ob-recap .seg .n{font-family:var(--font-mono);font-weight:700;font-size:12px;color:var(--text);font-variant-numeric:tabular-nums;flex-shrink:0}:host .order-builder .ob-recap .tot{display:flex;justify-content:space-between;align-items:baseline;margin-top:12px;padding-top:10px;border-top:1px solid var(--rule)}:host .order-builder .ob-recap .tot .l{font-weight:700;font-size:12px}:host .order-builder .ob-recap .tot .n{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:18px;font-variant-numeric:tabular-nums}:host .order-builder .ob-fab{display:none;position:fixed;inset-inline-end:20px;bottom:calc(var(--app-bottom-bar, 0px) + env(safe-area-inset-bottom,0px) + 20px);z-index:30;align-items:center;gap:10px;--ob-fab-ground: var(--ink-active);--ob-fab-on: var(--paper);background:var(--ob-fab-ground);color:var(--ob-fab-on);border-radius:var(--r-pill);padding:8px 10px 8px 16px;box-shadow:var(--shadow-3)}:host .order-builder .ob-fab .ft{display:grid;line-height:1.1}:host .order-builder .ob-fab .ft .k{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;opacity:.65}:host .order-builder .ob-fab .ft .vv{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 82;font-size:16px}:host .order-builder .ob-fab .ft .vv .cur{font-family:var(--font-mono);font-size:.6em;opacity:.7;margin-inline-start:3px}:host .order-builder .ob-fab .open{appearance:none;cursor:pointer;border:0;background:color-mix(in srgb,var(--ob-fab-on) 14%,transparent);color:var(--ob-fab-on);font:inherit;font-size:12px;font-weight:700;border-radius:var(--r-pill);padding:8px 14px;display:inline-flex;align-items:center;gap:6px}:host .order-builder .ob-fab .open:hover{background:color-mix(in srgb,var(--ob-fab-on) 24%,transparent)}:host .order-builder .ob-fab .open:focus-visible{outline:2px solid var(--ob-fab-on);outline-offset:2px}:host .order-builder .ob-backdrop{display:none;position:fixed;inset:0;z-index:38;background:#0f11156b;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}:host .order-builder.ob-collapsed .ob-grid{grid-template-columns:minmax(0,1fr)}:host .order-builder.ob-collapsed .ob-fab{display:flex}:host .order-builder.ob-collapsed .ob-rail{position:fixed;top:0;inset-inline-end:0;height:100dvh;width:min(360px,92vw);z-index:40;background:var(--paper);border-inline-start:1px solid var(--rule);box-shadow:var(--shadow-3);padding:14px;overflow:auto;gap:var(--s-3);transform:translate(101%);transition:transform var(--t-base) var(--ease-out)}[dir=rtl] :host .order-builder.ob-collapsed .ob-rail{transform:translate(-101%)}:host .order-builder.ob-collapsed.ob-rail-open .ob-rail{transform:none}:host .order-builder.ob-collapsed.ob-rail-open .ob-backdrop{display:block}@media(max-width:1279px){:host .order-builder .ob-grid{grid-template-columns:minmax(0,1fr)}:host .order-builder .ob-fab{display:flex}:host .order-builder .ob-rail{position:fixed;top:0;inset-inline-end:0;height:100dvh;width:min(360px,92vw);z-index:40;background:var(--paper);border-inline-start:1px solid var(--rule);box-shadow:var(--shadow-3);padding:14px;overflow:auto;gap:var(--s-3);transform:translate(101%);transition:transform var(--t-base) var(--ease-out)}[dir=rtl] :host .order-builder .ob-rail{transform:translate(-101%)}:host .order-builder.ob-rail-open .ob-rail{transform:none}:host .order-builder.ob-rail-open .ob-backdrop{display:block}}:host ef-data-card .nm,:host ef-data-card .num,:host ef-data-card .ln-val,:host .ob-totals .v{unicode-bidi:plaintext}@media(max-width:767px){:host .order-builder{padding-bottom:calc(var(--app-bottom-bar, 0px) + env(safe-area-inset-bottom,0px) + 20px + 56px + var(--s-4))}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { 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: "component", type: EfDatepickerAdvancedComponent, selector: "ef-datepicker-advanced", inputs: ["triggerLabelKey", "triggerLabel", "presets", "activePreset", "start", "end", "allowCustomRange", "mode", "selectionMode", "variant", "showTime"], outputs: ["rangeChange", "opened", "closed"] }, { kind: "component", type: EfInputNumberComponent, selector: "ef-inputnumber", inputs: ["value", "invalid", "format", "showButtons", "buttonLayout", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "prefix", "suffix", "currency", "currencyDisplay", "locale", "localeMatcher", "mode", "useGrouping", "minFractionDigits", "maxFractionDigits", "min", "max", "step", "allowEmpty", "inputSize", "size", "maxlength", "minlength", "pattern", "tabindex", "title", "ariaLabelledBy", "inputStyle", "inputStyleClass", "style", "styleClass", "primeNgVariant", "autofocus", "autocomplete", "fluid", "inline", "variant", "pt", "ptOptions", "dt", "unstyled"], outputs: ["inputEvent", "valueChangeEvent", "focusEvent", "blurEvent", "keyDownEvent", "clearEvent"] }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "for", "styleClass", "required"] }, { kind: "component", type: EfButtonComponent, selector: "ef-button", inputs: ["label", "labelKey", "icon", "variant", "severity", "size", "disabled", "rounded", "outlined", "raised", "text", "loading", "styleClass", "type", "badge", "badgeSeverity", "tooltip", "tooltipKey", "tooltipPosition", "autofocus"], outputs: ["clickEvent"] }, { kind: "component", type: EfCardComponent, selector: "ef-card", inputs: ["titleKey", "title", "metaKey", "meta", "tone", "forceHead", "hasFoot", "collapsible", "overflowVisible", "loading", "skeleton", "skeletonRows", "skeletonHeight", "collapsed"], outputs: ["collapsedChange"] }, { kind: "component", type: EfChangeHistoryComponent, selector: "ef-change-history", inputs: ["entries", "titleKey", "emptyKey", "limit", "previewLimit", "collapsible", "startCollapsed"] }, { kind: "component", type: EfProductTypeaheadComponent, selector: "ef-product-typeahead", inputs: ["products", "productKeyField", "placeholderKey", "disabled", "currency", "locale", "showVariants"], outputs: ["productSelect"] }, { kind: "component", type: EfDataCardComponent, selector: "ef-data-card", inputs: ["mobileLayout", "columns", "rows", "trackByField", "pageNumber", "pageSize", "totalCount", "pageSizeOptions", "hidePager", "sort", "loading", "errorMsg", "loadingKey", "rowDoubleClickable", "hasActionsColumn", "rowActionsContext", "showViewAction", "showEditAction", "showDuplicateAction", "showDeleteAction", "showDensityControl", "showColumnPicker", "showExportControl", "exporting", "screenContext", "searchTerm", "tableKey", "defaultDensity", "reorderable", "fixedLayout"], outputs: ["pageChange", "pageSizeChange", "sortChange", "rowDoubleClick", "rowAction", "exportRequest", "clearSearch", "retry", "rowReorder"] }, { kind: "directive", type: EfColumnTemplateDirective, selector: "[efColumnTemplate]", inputs: ["efColumnTemplate"] }, { kind: "pipe", type: i1$3.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }] });
5887
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfOrderBuilderComponent, isStandalone: true, selector: "ef-order-builder", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, order: { classPropertyName: "order", publicName: "order", isSignal: true, isRequired: true, transformFunction: null }, products: { classPropertyName: "products", publicName: "products", isSignal: true, isRequired: true, transformFunction: null }, customers: { classPropertyName: "customers", publicName: "customers", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, countGroupLabels: { classPropertyName: "countGroupLabels", publicName: "countGroupLabels", isSignal: true, isRequired: false, transformFunction: null }, productKeyField: { classPropertyName: "productKeyField", publicName: "productKeyField", isSignal: true, isRequired: false, transformFunction: null }, backendErrors: { classPropertyName: "backendErrors", publicName: "backendErrors", isSignal: true, isRequired: false, transformFunction: null }, auditEntries: { classPropertyName: "auditEntries", publicName: "auditEntries", isSignal: true, isRequired: false, transformFunction: null }, focusMode: { classPropertyName: "focusMode", publicName: "focusMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { order: "orderChange", openCatalogue: "openCatalogue", orderChanged: "orderChanged", orderValidated: "orderValidated", productsAdded: "productsAdded", focusMode: "focusModeChange" }, viewQueries: [{ propertyName: "typeahead", first: true, predicate: EfProductTypeaheadComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"order-builder\"\n [class.ob-collapsed]=\"focusMode()\"\n [class.ob-rail-open]=\"railOpen()\">\n <div class=\"ob-grid\">\n <div class=\"ob-main\">\n <!-- Order header: customer + date + express add (one card) -->\n <ef-card\n titleKey=\"order_builder.section_order\"\n [metaKey]=\"readonly() ? '' : 'order_builder.quick_add_hint'\"\n [overflowVisible]=\"true\">\n <div class=\"ob-order-head\">\n <div class=\"form-grid\">\n <div class=\"field col-6\">\n <ef-label labelKey=\"label.customer\" for=\"ob-cust\" [required]=\"true\" />\n <ef-select\n inputId=\"ob-cust\"\n [options]=\"customers()\"\n optionLabel=\"companyName\"\n optionValue=\"customerId\"\n [ngModel]=\"order().customerId\"\n (ngModelChange)=\"updateCustomer($event)\"\n [disabled]=\"readonly()\"\n [showClear]=\"true\"\n [filter]=\"true\"\n filterBy=\"companyName\"\n filterPlaceholderKey=\"label.search_customer\"\n appendTo=\"body\"\n placeholderKey=\"label.select_customer\" />\n </div>\n <div class=\"field col-3\">\n <ef-label labelKey=\"label.order_date\" for=\"ob-date\" [required]=\"true\" />\n <ef-datepicker-advanced\n mode=\"field\"\n selectionMode=\"single\"\n inputId=\"ob-date\"\n placeholderKey=\"date_select_date\"\n [ngModel]=\"order().orderDate\"\n (ngModelChange)=\"updateOrderDate($event)\"\n [disabled]=\"readonly()\" />\n </div>\n </div>\n\n @if (!readonly()) {\n <div class=\"qa-grid\">\n <div class=\"qa-combo-wrap\">\n <ef-label labelKey=\"label.article\" />\n <ef-product-typeahead\n [products]=\"products()\"\n [productKeyField]=\"productKeyField()\"\n placeholderKey=\"label.search_product\"\n [currency]=\"currency()\"\n [locale]=\"locale()\"\n (productSelect)=\"onTypeaheadSelect($event)\"\n [showVariants]=\"false\" />\n </div>\n <div class=\"qa-price\">\n <ef-label labelKey=\"label.sale_price\" />\n <ef-inputnumber\n variant=\"comptoir\"\n [value]=\"quickAddPrice()\"\n (valueChangeEvent)=\"quickAddPrice.set($event ?? 0)\"\n [minFractionDigits]=\"2\"\n [maxFractionDigits]=\"2\"\n [locale]=\"locale()\"\n [disabled]=\"readonly()\" />\n </div>\n <div class=\"qa-qtyf\">\n <ef-label labelKey=\"label.quantity\" />\n <ef-inputnumber\n variant=\"comptoir\"\n [ngModel]=\"quickAddQuantity()\"\n (ngModelChange)=\"quickAddQuantity.set($event)\"\n [min]=\"0\"\n [maxFractionDigits]=\"4\"\n [locale]=\"locale()\"\n [disabled]=\"readonly()\"\n />\n </div>\n <div class=\"qa-go\">\n <!-- Ghost while the preconditions are unmet, tenant once the\n line is addable. A disabled control painted in a faded\n tenant fill reads as an enabled button that lost its\n colour, which is the greyed-out accent the design brief\n bans; ghost reads as \"not yet\". -->\n <ef-button\n labelKey=\"action.add\"\n icon=\"pi pi-plus\"\n variant=\"comptoir\"\n size=\"base\"\n [severity]=\"quickAddDisabled() ? 'ghost' : 'tenant'\"\n [disabled]=\"quickAddDisabled()\"\n (clickEvent)=\"addQuickProduct()\" />\n </div>\n </div>\n }\n </div>\n </ef-card>\n\n <!-- Lines.\n\n ef-data-card owns the panel here instead of sitting inside an\n ef-card: `.tbl-wrap` already draws a bordered, rounded surface, so\n nesting it in another one would be a card inside a card. The title\n and the card's actions move into the head slots it provides.\n\n The columns are declared in TS (`lineColumns`) so discount and tax\n follow the document config, and so one definition drives both the\n desktop table and the mobile cards. Only the article column is\n elastic; every other column holds a number of known magnitude and\n gets a fixed track, which keeps the decimals aligned down the\n table rather than shifting with the longest description. -->\n <ef-data-card\n [columns]=\"lineColumns()\"\n [rows]=\"orderLines()\"\n trackByField=\"id\"\n [hidePager]=\"true\"\n defaultDensity=\"compact\"\n [reorderable]=\"!readonly()\"\n [fixedLayout]=\"true\"\n (rowReorder)=\"reorderLine($event)\"\n mobileLayout=\"card\">\n\n <div tbl-head-info class=\"ob-lines-head\">\n <span class=\"strong\">{{ 'order_builder.lines' | translate }}</span>\n <span class=\"meta\">\u00B7 {{ orderLines().length }}</span>\n </div>\n\n <div tbl-head-actions class=\"head-actions\">\n @if (config().showCatalogue && !readonly()) {\n <ef-button\n labelKey=\"action.open_catalogue\"\n icon=\"pi pi-box\"\n severity=\"secondary\"\n [text]=\"true\"\n (clickEvent)=\"handleOpenCatalogue()\" />\n }\n <ef-button\n [labelKey]=\"focusMode() ? 'order_builder.show_summary' : 'order_builder.fullscreen'\"\n icon=\"pi pi-window-maximize\"\n severity=\"secondary\"\n [text]=\"true\"\n (clickEvent)=\"toggleFocus()\" />\n </div>\n\n <span empty-state>{{ 'order_builder.empty' | translate }}</span>\n <span empty-state-mobile>{{ 'order_builder.empty' | translate }}</span>\n\n <ng-template efColumnTemplate=\"product\" let-item let-i=\"index\">\n <div class=\"p\">\n <span class=\"ln\">{{ i + 1 }}</span>\n <span class=\"pp\">\n <span class=\"nm\">{{ item.productDescription }}</span>\n </span>\n </div>\n </ng-template>\n\n <ng-template efColumnTemplate=\"price\" let-item>\n @if (!readonly() && isEditingCell(item, 'price')) {\n <ef-inputnumber\n [(ngModel)]=\"item.productUnitPrice\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [minFractionDigits]=\"2\"\n [maxFractionDigits]=\"2\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'price')\">\n {{ item.productUnitPrice | number: '1.2-2' : locale() }}\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"qty\" let-item>\n @if (!readonly() && isEditingCell(item, 'qty')) {\n <ef-inputnumber\n [(ngModel)]=\"item.quantity\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [maxFractionDigits]=\"4\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'qty')\">\n {{ item.quantity | number: '1.0-4' : locale() }}\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"discount\" let-item>\n @if (!readonly() && isEditingCell(item, 'discount')) {\n <ef-inputnumber\n [(ngModel)]=\"item.discount\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [max]=\"100\"\n [maxFractionDigits]=\"2\"\n suffix=\" %\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'discount')\">\n {{ item.discount | number: '1.0-2' : locale() }} %\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"tax\" let-item>\n @if (!readonly() && isEditingCell(item, 'tva')) {\n <ef-inputnumber\n [(ngModel)]=\"item.taxRate\"\n (ngModelChange)=\"onFieldChange(item)\"\n [showClear]=\"false\"\n [autofocus]=\"true\"\n (blurEvent)=\"stopEditCell()\"\n (keyDownEvent)=\"onEditCellKeydown($event)\"\n [min]=\"0\"\n [max]=\"100\"\n [maxFractionDigits]=\"2\"\n suffix=\" %\"\n [locale]=\"locale()\"\n size=\"small\"\n class=\"!text-end\" />\n } @else {\n <button type=\"button\" class=\"ln-val\" [disabled]=\"readonly()\" (click)=\"startEditCell(item, 'tva')\">\n {{ (item.taxRate ?? 0) | number: '1.0-2' : locale() }} %\n </button>\n }\n </ng-template>\n\n <ng-template efColumnTemplate=\"subtotal\" let-item>\n {{ calculateSubtotal(item) | number: '1.2-2' : locale() }}\n </ng-template>\n\n <ng-template efColumnTemplate=\"remove\" let-item>\n <ef-button\n icon=\"pi pi-trash\"\n severity=\"danger\"\n [text]=\"true\"\n styleClass=\"ob-del\"\n (clickEvent)=\"removeOrderLine(item.id)\" />\n </ng-template>\n </ef-data-card>\n\n </div>\n\n <!-- CONTEXT RAIL (1/3) -->\n <aside class=\"ob-rail\">\n <!-- Totaux (top) -->\n <ef-card titleKey=\"order_builder.totals\" [meta]=\"currency()\">\n <div class=\"ob-totals\">\n <div class=\"row\">\n <span class=\"lbl\">{{ 'order_builder.gross' | translate }}</span>\n <span class=\"v\">{{ grossTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <div class=\"row discount\">\n <span class=\"lbl\">{{ 'order_builder.discount' | translate }}</span>\n <span class=\"v\">\u2212{{ discountTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <div class=\"row\">\n <span class=\"lbl\">{{ 'order_builder.tax' | translate }}</span>\n <span class=\"v\">{{ tvaTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n <hr />\n <div class=\"row grand\">\n <span class=\"lbl\">{{ 'order_builder.grand_total' | translate }}</span>\n <span class=\"v\">{{ finalTotal() | number: '1.2-2' : locale() }}<span class=\"cur\">{{ currency() }}</span></span>\n </div>\n </div>\n </ef-card>\n\n <!-- R\u00E9capitulatif par format -->\n @if (productsSummary().items.length > 0) {\n <ef-card titleKey=\"order_builder.recap\" metaKey=\"order_builder.auto\">\n <div class=\"ob-recap\">\n <div class=\"cols\">\n @for (col of recapColumns(); track $index) {\n <div class=\"grp\">\n @for (seg of col; track seg.code) {\n <div class=\"seg\">\n <span class=\"fmt\">{{ seg.label }}</span>\n <span class=\"n\">{{ seg.count | number: '1.0-0' : locale() }}</span>\n </div>\n }\n </div>\n }\n </div>\n <div class=\"tot\">\n <span class=\"l\">{{ 'order_builder.total_units' | translate }}</span>\n <span class=\"n\">{{ productsSummary().totalCount | number: '1.0-0' : locale() }}</span>\n </div>\n </div>\n </ef-card>\n }\n\n <!-- Historique (optional, component-owned) -->\n @if (hasAudit()) {\n <ef-change-history [entries]=\"auditEntries()\" />\n }\n </aside>\n </div>\n\n <!-- Floating summary FAB + backdrop -->\n <button type=\"button\" class=\"ob-backdrop\" aria-hidden=\"true\" tabindex=\"-1\" (click)=\"toggleRail(false)\"></button>\n <div class=\"ob-fab\" role=\"status\">\n <div class=\"ft\">\n <span class=\"k\">{{ 'order_builder.total_units' | translate }} \u00B7 {{ productsSummary().totalCount }}</span>\n <span class=\"vv\">{{ finalTotal() | number: '1.2-2' : locale() }}<span class=\"cur\">{{ currency() }}</span></span>\n </div>\n <button type=\"button\" class=\"open\" (click)=\"toggleRail(true)\">\n {{ 'order_builder.summary' | translate }}\n </button>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}:host .order-builder{display:grid;gap:var(--s-3)}:host .order-builder .head-actions{display:flex;align-items:center;gap:6px}:host .order-builder .form-grid{display:grid;gap:var(--s-3);grid-template-columns:repeat(12,minmax(0,1fr))}:host .order-builder .form-grid>.col-3{grid-column:span 3}:host .order-builder .form-grid>.col-4{grid-column:span 4}:host .order-builder .form-grid>.col-5{grid-column:span 5}:host .order-builder .form-grid>.col-6{grid-column:span 6}@media(max-width:767px){:host .order-builder .form-grid>[class*=col-]{grid-column:span 12}}:host .order-builder .field{display:grid;gap:4px}:host .order-builder .field>label{font-size:11px;font-weight:700;color:var(--text-mute);text-transform:uppercase;letter-spacing:.06em}:host .order-builder .ob-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);align-items:start}@media(min-width:1280px){:host .order-builder .ob-grid{grid-template-columns:minmax(0,2fr) minmax(0,1fr)}}:host .order-builder .ob-main{display:grid;gap:var(--s-3);min-width:0}:host .order-builder .ob-rail{display:grid;gap:var(--s-3);align-content:start}@media(min-width:1280px){:host .order-builder .ob-rail{position:sticky;top:44px}}:host .order-builder .ob-order-head{display:grid;gap:var(--s-4)}:host .order-builder .qa-grid{display:flex;flex-wrap:wrap;gap:10px;align-items:end;padding-top:var(--s-3);border-top:1px solid var(--rule-soft)}:host .order-builder .qa-combo-wrap{flex:1 1 260px;min-width:0}:host .order-builder .qa-price{flex:0 1 132px}:host .order-builder .qa-qtyf{flex:0 0 132px}:host .order-builder .qa-go{flex:0 0 auto}@media(max-width:560px){:host .order-builder .qa-combo-wrap,:host .order-builder .qa-price,:host .order-builder .qa-qtyf,:host .order-builder .qa-go{flex:1 1 100%}}:host .order-builder ef-data-card{font-size:13px}:host .order-builder ef-data-card .p{display:flex;align-items:baseline;gap:9px;line-height:1.25}:host .order-builder ef-data-card .p .ln{flex:0 0 auto;min-width:15px;text-align:end;font-family:var(--font-mono);font-size:10.5px;font-weight:700;color:var(--text-soft)}:host .order-builder ef-data-card .p .pp{display:grid;gap:1px;min-width:0}:host .order-builder ef-data-card .p .nm{font-weight:600;letter-spacing:-.005em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host .order-builder ef-data-card .p .nm em{font-style:normal;color:var(--text-mute);font-weight:600}:host .order-builder ef-data-card ef-inputnumber,:host .order-builder ef-data-card ef-inputnumber .ef-field-control,:host .order-builder ef-data-card ef-inputnumber input{width:100%;min-width:0}:host .order-builder ef-data-card .num,:host .order-builder ef-data-card .total{font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums}:host .order-builder ef-data-card .ln-val{appearance:none;width:100%;margin:0;padding:5px 8px;border:1px solid transparent;border-radius:7px;background:transparent;font:inherit;font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums;color:inherit;text-align:end;cursor:text;transition:background .12s ease,border-color .12s ease}:host .order-builder ef-data-card .ln-val:hover:not(:disabled){background:var(--paper);border-color:var(--rule)}:host .order-builder ef-data-card .ln-val:focus-visible{outline:none;background:var(--paper);border-color:var(--tenant, var(--rule))}:host .order-builder ef-data-card .ln-val:disabled{cursor:default;background:transparent;border-color:transparent}:host .order-builder ef-data-card .total{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:13.5px}:host .order-builder ef-data-card .cur{font-size:9.5px;color:var(--text-soft);margin-inline-start:3px}:host .order-builder .ob-lines-head{display:flex;align-items:baseline;gap:6px;min-width:0}:host .order-builder .ob-lines-head .strong{font-weight:700;color:var(--text)}:host .order-builder .ob-lines-head .meta{color:var(--text-soft)}:host .order-builder .ob-del{color:var(--text-soft)}:host .order-builder .ob-del:hover{color:var(--st-cancelled-fg);border-color:var(--st-cancelled-bg)}:host .order-builder .ob-totals{display:grid;gap:6px}:host .order-builder .ob-totals .row{display:flex;justify-content:space-between;align-items:baseline;gap:14px}:host .order-builder .ob-totals .row .lbl{font-size:12px;font-weight:600;color:var(--text-mute)}:host .order-builder .ob-totals .row .v{font-family:var(--font-mono);font-feature-settings:\"tnum\";font-variant-numeric:tabular-nums;font-weight:700;font-size:12.5px}:host .order-builder .ob-totals .row.discount .v{color:var(--st-pending-fg)}:host .order-builder .ob-totals hr{border:0;border-top:1px solid var(--rule);margin:5px 0}:host .order-builder .ob-totals .grand .lbl{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:13.5px;color:var(--text)}:host .order-builder .ob-totals .grand .v{font-family:var(--font-display);font-variation-settings:\"opsz\" 96,\"wght\" 700,\"wdth\" 80;font-size:26px;letter-spacing:-.03em;color:var(--text)}:host .order-builder .ob-totals .grand .cur{font-family:var(--font-mono);font-size:.4em;color:var(--text-mute);margin-inline-start:4px}:host .order-builder .ob-recap .cols{display:grid;grid-template-columns:1fr 1fr}:host .order-builder .ob-recap .grp{display:flex;flex-direction:column;gap:3px;min-width:0}:host .order-builder .ob-recap .grp:first-child{padding-inline-end:14px}:host .order-builder .ob-recap .grp+.grp{padding-inline-start:14px;border-inline-start:1px solid var(--rule)}:host .order-builder .ob-recap .seg{display:flex;justify-content:space-between;align-items:baseline;gap:8px}:host .order-builder .ob-recap .seg .fmt{font-size:11.5px;font-weight:700;color:var(--text);letter-spacing:-.005em;min-width:0}:host .order-builder .ob-recap .seg .fmt:after{content:\":\";color:var(--text-soft)}:host .order-builder .ob-recap .seg .n{font-family:var(--font-mono);font-weight:700;font-size:12px;color:var(--text);font-variant-numeric:tabular-nums;flex-shrink:0}:host .order-builder .ob-recap .tot{display:flex;justify-content:space-between;align-items:baseline;margin-top:12px;padding-top:10px;border-top:1px solid var(--rule)}:host .order-builder .ob-recap .tot .l{font-weight:700;font-size:12px}:host .order-builder .ob-recap .tot .n{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 85;font-size:18px;font-variant-numeric:tabular-nums}:host .order-builder .ob-fab{display:none;position:fixed;inset-inline-end:20px;bottom:calc(var(--app-bottom-bar, 0px) + env(safe-area-inset-bottom,0px) + 20px);z-index:30;align-items:center;gap:10px;--ob-fab-ground: var(--ink-active);--ob-fab-on: var(--paper);background:var(--ob-fab-ground);color:var(--ob-fab-on);border-radius:var(--r-pill);padding:8px 10px 8px 16px;box-shadow:var(--shadow-3)}:host .order-builder .ob-fab .ft{display:grid;line-height:1.1}:host .order-builder .ob-fab .ft .k{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;opacity:.65}:host .order-builder .ob-fab .ft .vv{font-family:var(--font-display);font-variation-settings:\"opsz\" 12,\"wght\" 700,\"wdth\" 82;font-size:16px}:host .order-builder .ob-fab .ft .vv .cur{font-family:var(--font-mono);font-size:.6em;opacity:.7;margin-inline-start:3px}:host .order-builder .ob-fab .open{appearance:none;cursor:pointer;border:0;background:color-mix(in srgb,var(--ob-fab-on) 14%,transparent);color:var(--ob-fab-on);font:inherit;font-size:12px;font-weight:700;border-radius:var(--r-pill);padding:8px 14px;display:inline-flex;align-items:center;gap:6px}:host .order-builder .ob-fab .open:hover{background:color-mix(in srgb,var(--ob-fab-on) 24%,transparent)}:host .order-builder .ob-fab .open:focus-visible{outline:2px solid var(--ob-fab-on);outline-offset:2px}:host .order-builder .ob-backdrop{display:none;position:fixed;inset:0;z-index:38;background:#0f11156b;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}:host .order-builder.ob-collapsed .ob-grid{grid-template-columns:minmax(0,1fr)}:host .order-builder.ob-collapsed .ob-fab{display:flex}:host .order-builder.ob-collapsed .ob-rail{position:fixed;top:0;inset-inline-end:0;height:100dvh;width:min(360px,92vw);z-index:40;background:var(--paper);border-inline-start:1px solid var(--rule);box-shadow:var(--shadow-3);padding:14px;overflow:auto;gap:var(--s-3);transform:translate(101%);transition:transform var(--t-base) var(--ease-out)}[dir=rtl] :host .order-builder.ob-collapsed .ob-rail{transform:translate(-101%)}:host .order-builder.ob-collapsed.ob-rail-open .ob-rail{transform:none}:host .order-builder.ob-collapsed.ob-rail-open .ob-backdrop{display:block}@media(max-width:1279px){:host .order-builder .ob-grid{grid-template-columns:minmax(0,1fr)}:host .order-builder .ob-fab{display:flex}:host .order-builder .ob-rail{position:fixed;top:0;inset-inline-end:0;height:100dvh;width:min(360px,92vw);z-index:40;background:var(--paper);border-inline-start:1px solid var(--rule);box-shadow:var(--shadow-3);padding:14px;overflow:auto;gap:var(--s-3);transform:translate(101%);transition:transform var(--t-base) var(--ease-out)}[dir=rtl] :host .order-builder .ob-rail{transform:translate(-101%)}:host .order-builder.ob-rail-open .ob-rail{transform:none}:host .order-builder.ob-rail-open .ob-backdrop{display:block}}:host ef-data-card .nm,:host ef-data-card .num,:host ef-data-card .ln-val,:host .ob-totals .v{unicode-bidi:plaintext}@media(max-width:767px){:host .order-builder{padding-bottom:calc(var(--app-bottom-bar, 0px) + env(safe-area-inset-bottom,0px) + 20px + 56px + var(--s-4))}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { 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: "component", type: EfDatepickerAdvancedComponent, selector: "ef-datepicker-advanced", inputs: ["triggerLabelKey", "triggerLabel", "presets", "activePreset", "start", "end", "allowCustomRange", "allowAllTime", "mode", "selectionMode", "variant", "showTime"], outputs: ["rangeChange", "opened", "closed"] }, { kind: "component", type: EfInputNumberComponent, selector: "ef-inputnumber", inputs: ["value", "invalid", "format", "showButtons", "buttonLayout", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "prefix", "suffix", "currency", "currencyDisplay", "locale", "localeMatcher", "mode", "useGrouping", "minFractionDigits", "maxFractionDigits", "min", "max", "step", "allowEmpty", "inputSize", "size", "maxlength", "minlength", "pattern", "tabindex", "title", "ariaLabelledBy", "inputStyle", "inputStyleClass", "style", "styleClass", "primeNgVariant", "autofocus", "autocomplete", "fluid", "inline", "variant", "pt", "ptOptions", "dt", "unstyled"], outputs: ["inputEvent", "valueChangeEvent", "focusEvent", "blurEvent", "keyDownEvent", "clearEvent"] }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "for", "styleClass", "required"] }, { kind: "component", type: EfButtonComponent, selector: "ef-button", inputs: ["label", "labelKey", "icon", "variant", "severity", "size", "disabled", "rounded", "outlined", "raised", "text", "loading", "styleClass", "type", "badge", "badgeSeverity", "tooltip", "tooltipKey", "tooltipPosition", "autofocus"], outputs: ["clickEvent"] }, { kind: "component", type: EfCardComponent, selector: "ef-card", inputs: ["titleKey", "title", "metaKey", "meta", "tone", "forceHead", "hasFoot", "collapsible", "overflowVisible", "loading", "skeleton", "skeletonRows", "skeletonHeight", "collapsed"], outputs: ["collapsedChange"] }, { kind: "component", type: EfChangeHistoryComponent, selector: "ef-change-history", inputs: ["entries", "titleKey", "emptyKey", "limit", "previewLimit", "collapsible", "startCollapsed"] }, { kind: "component", type: EfProductTypeaheadComponent, selector: "ef-product-typeahead", inputs: ["products", "productKeyField", "placeholderKey", "disabled", "currency", "locale", "showVariants"], outputs: ["productSelect"] }, { kind: "component", type: EfDataCardComponent, selector: "ef-data-card", inputs: ["mobileLayout", "columns", "rows", "trackByField", "pageNumber", "pageSize", "totalCount", "pageSizeOptions", "hidePager", "sort", "loading", "errorMsg", "loadingKey", "rowDoubleClickable", "hasActionsColumn", "rowActionsContext", "showViewAction", "showEditAction", "showDuplicateAction", "showDeleteAction", "showDensityControl", "showColumnPicker", "showExportControl", "exporting", "screenContext", "searchTerm", "tableKey", "defaultDensity", "reorderable", "fixedLayout"], outputs: ["pageChange", "pageSizeChange", "sortChange", "rowDoubleClick", "rowAction", "exportRequest", "clearSearch", "retry", "rowReorder"] }, { kind: "directive", type: EfColumnTemplateDirective, selector: "[efColumnTemplate]", inputs: ["efColumnTemplate"] }, { kind: "pipe", type: i1$3.DecimalPipe, name: "number" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }] });
5861
5888
  }
5862
5889
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfOrderBuilderComponent, decorators: [{
5863
5890
  type: Component,