@elasticias/ui 1.0.53 → 1.0.55

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.
@@ -1,12 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Pipe, input, Component, output, booleanAttribute, Input, Directive, signal, computed, inject, EventEmitter, Injector, forwardRef, Output, ContentChild, HostBinding, ChangeDetectionStrategy, ElementRef, LOCALE_ID, ViewContainerRef, DestroyRef, ChangeDetectorRef, effect, HostListener, ViewChild, isDevMode, model, InjectionToken, afterNextRender, TemplateRef, ContentChildren, viewChild, untracked, contentChild, ViewEncapsulation, linkedSignal, ViewChildren, afterEveryRender, PLATFORM_ID } from '@angular/core';
2
+ import { Pipe, input, Component, output, booleanAttribute, Input, Directive, signal, computed, inject, EventEmitter, Injector, forwardRef, Output, ContentChild, HostBinding, ChangeDetectionStrategy, ElementRef, LOCALE_ID, ViewContainerRef, DestroyRef, ChangeDetectorRef, effect, HostListener, ViewChild, isDevMode, model, InjectionToken, afterNextRender, TemplateRef, ContentChildren, viewChild, untracked, contentChild, ViewEncapsulation, linkedSignal, ViewChildren, afterEveryRender, inputBinding, outputBinding, PLATFORM_ID } from '@angular/core';
3
3
  import { NumberUtils, UuidUtils, PhoneUtils } from '@elasticias/utils';
4
4
  import * as i1 from 'primeng/button';
5
5
  import { ButtonModule } from 'primeng/button';
6
6
  import * as i1$3 from '@angular/common';
7
7
  import { CommonModule, formatDate, CurrencyPipe, DatePipe, DecimalPipe } from '@angular/common';
8
8
  import * as i2$1 from '@angular/forms';
9
- import { NgControl, Validators, FormControl, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
9
+ import { NgControl, Validators, FormControl, FormsModule, NG_VALUE_ACCESSOR, FormGroupDirective, NgForm, NG_VALIDATORS } from '@angular/forms';
10
10
  import * as i1$1 from '@ngx-translate/core';
11
11
  import { TranslateModule, TranslateService } from '@ngx-translate/core';
12
12
  import * as i1$2 from 'primeng/select';
@@ -16,7 +16,7 @@ import { MultiSelectModule } from 'primeng/multiselect';
16
16
  import { ObjectUtils, ZIndexUtils } from 'primeng/utils';
17
17
  import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
18
18
  import { merge, Subject, tap, switchMap, catchError, EMPTY } from 'rxjs';
19
- import { EfViewportService, EfShortcutService, EF_PREFERENCE_STORE, formatShortcut, efTablePrefsKey, EfThemeConfigService, EF_BUILD_INFO, EfSessionIdleService, EF_SESSION, EfPermissionService, EfActiveModuleService, EF_SIDE_COLLAPSED_KEY, EfToastService } from '@elasticias/core';
19
+ import { EfViewportService, EfShortcutService, EF_PREFERENCE_STORE, formatShortcut, efTablePrefsKey, EfThemeConfigService, EF_BUILD_INFO, EfSessionIdleService, EF_SESSION, EfPermissionService, EfActiveModuleService, EF_SIDE_COLLAPSED_KEY, EfToastService, EF_CONTENT_CULTURES, cleanLocalizedText, EF_DEFAULT_CONTENT_CULTURES } from '@elasticias/core';
20
20
  import * as i1$4 from 'primeng/inputnumber';
21
21
  import { InputNumberModule } from 'primeng/inputnumber';
22
22
  import * as i1$5 from 'primeng/tooltip';
@@ -674,6 +674,24 @@ class EfServerErrorsDirective {
674
674
  return null;
675
675
  return map[name] ?? null;
676
676
  }
677
+ /**
678
+ * Messages for every key that starts with `prefix`, keyed by what follows
679
+ * it: `errorsWithPrefix('title.')` turns `title.ar` into `ar`. For a control
680
+ * that owns several backend keys, such as a text per language. Empty lists
681
+ * are left out.
682
+ */
683
+ errorsWithPrefix(prefix) {
684
+ const out = {};
685
+ const map = this.errors();
686
+ if (!prefix || !map)
687
+ return out;
688
+ for (const [key, messages] of Object.entries(map)) {
689
+ if (key.length > prefix.length && key.startsWith(prefix) && messages?.length) {
690
+ out[key.slice(prefix.length)] = messages;
691
+ }
692
+ }
693
+ return out;
694
+ }
677
695
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfServerErrorsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
678
696
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.11", type: EfServerErrorsDirective, isStandalone: true, selector: "[efServerErrors]", inputs: { errors: { classPropertyName: "errors", publicName: "efServerErrors", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
679
697
  }
@@ -701,6 +719,9 @@ function resolveExternalErrors(explicit, scope, name) {
701
719
  }
702
720
 
703
721
  /**
722
+ * Exported for `@elasticias/ui/rich-text` only, not a supported extension
723
+ * point: it may change without a major version.
724
+ *
704
725
  * Shared base for every Comptoir form control (`ef-input-text`,
705
726
  * `ef-textarea`, `ef-select`, `ef-input-number`, `ef-checkbox`, …).
706
727
  *
@@ -4260,12 +4281,55 @@ const OP_META = {
4260
4281
  note: { cls: 'op-note' },
4261
4282
  };
4262
4283
  const DEFAULT_META = { cls: 'op-default' };
4284
+ /** A path whose last (`notes.ar`) or first (`fr.subject`) segment is two
4285
+ * lower-case letters; a culture only when the bundle names it. */
4286
+ const TRAILING_CULTURE = /^(.+)\.([a-z]{2})$/;
4287
+ const LEADING_CULTURE = /^([a-z]{2})\.(.+)$/;
4288
+ /**
4289
+ * The i18n key segment of a backend entity type: PascalCase to snake_case,
4290
+ * an acronym kept as one word. `Product` gives `product`, `EmailTemplate`
4291
+ * gives `email_template`, `POSSession` gives `pos_session`; a segment
4292
+ * already in snake_case is kept.
4293
+ */
4294
+ function auditEntityKeySegment(entityType) {
4295
+ return entityType
4296
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
4297
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
4298
+ .replace(/[\s.-]+/g, '_')
4299
+ .toLowerCase();
4300
+ }
4263
4301
  /**
4264
4302
  * Standardized change-history box. Drop it on any detail screen and feed it the
4265
4303
  * record's audit entries (from `GET /api/audit/{entityType}/{id}` via the NSwag
4266
4304
  * `AuditClient`). Renders a vertical timeline inside an {@link EfCardComponent},
4267
4305
  * resolving operation labels through `audit_op_*` i18n keys and field labels
4268
- * through `audit_field_*` (falling back to the raw field key when unmapped).
4306
+ * through `audit_field_*` (falling back to the humanized field path when
4307
+ * unmapped).
4308
+ *
4309
+ * **Field labels.** For a field path (dots become `_` in the key) the box
4310
+ * tries, in order:
4311
+ *
4312
+ * 1. `audit_field_<entity>_<field>`, when `entityType` is set, with the type
4313
+ * converted by {@link auditEntityKeySegment} (`EmailTemplate` gives
4314
+ * `email_template`); the field keeps its camelCase (`htmlBody`), as
4315
+ * `ChangeDiff` records it;
4316
+ * 2. `audit_field_<field>`;
4317
+ * 3. when the last segment (`notes.ar`, as ChangeDiff writes a
4318
+ * LocalizedText) or, for older entries, the first segment (`fr.subject`)
4319
+ * is a known culture: the label of the rest of the path, found by steps 1
4320
+ * and 2 or humanized, then the culture's name in brackets: "Notes
4321
+ * olfactives (Arabe)". A culture is known when the app's bundle has
4322
+ * `ef_localized_input_culture_<xx>`, the key that also names it. The
4323
+ * bundle, not `EF_CONTENT_CULTURES`, decides, because a history outlives
4324
+ * the tenant's current culture list: a language the tenant dropped still
4325
+ * labels its old entries. Any other two-letter segment (`dimensions.cm`)
4326
+ * is part of the path;
4327
+ * 4. the humanized path (`clientCode` gives "Client code").
4328
+ *
4329
+ * So one `audit_field_product_notes` key labels every language of
4330
+ * `Product.Notes`, and an entity can name a field differently from the
4331
+ * global key used by others. On a V2 detail screen bind
4332
+ * `[entityType]="auditEntityType()"`.
4269
4333
  *
4270
4334
  * ```html
4271
4335
  * <ef-change-history [entries]="auditEntries()" />
@@ -4306,6 +4370,10 @@ class EfChangeHistoryComponent {
4306
4370
  * or null to keep the raw note. Rendered as plain text (never HTML). A
4307
4371
  * screen uses it to translate coded notes such as `order_email_resent:order_confirmed`. */
4308
4372
  noteFormatter = input(null, ...(ngDevMode ? [{ debugName: "noteFormatter" }] : /* istanbul ignore next */ []));
4373
+ /** The backend entity type (`ScreenConfig.AUDIT_ENTITY_TYPE`, e.g.
4374
+ * `EmailTemplate`), for the entity's own `audit_field_<entity>_<field>`
4375
+ * labels. Unset uses the global keys only. */
4376
+ entityType = input(undefined, ...(ngDevMode ? [{ debugName: "entityType" }] : /* istanbul ignore next */ []));
4309
4377
  /** "Show more" dialog visibility. */
4310
4378
  dialogVisible = signal(false, ...(ngDevMode ? [{ debugName: "dialogVisible" }] : /* istanbul ignore next */ []));
4311
4379
  /** First `previewLimit` rows shown inline in the card. */
@@ -4332,17 +4400,50 @@ class EfChangeHistoryComponent {
4332
4400
  };
4333
4401
  });
4334
4402
  }, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
4335
- /** Resolve a field key to its label. Adding an `audit_field_<field>` key is
4336
- * OPTIONAL — when it's missing the raw field name is humanized
4337
- * (`clientCode` → "Client code"), so new entities/fields work with zero
4338
- * i18n maintenance. Add a key only for a properly localized (fr/ar) term. */
4403
+ /** Resolve a field path to its label (order in the class docs). Keys are
4404
+ * OPTIONAL: without one the path is humanized (`clientCode` gives
4405
+ * "Client code"), so new fields work with zero i18n maintenance. Add a
4406
+ * key only for a properly localized (fr/ar) term. */
4339
4407
  fieldLabel(field) {
4340
4408
  if (!field)
4341
4409
  return '';
4342
- // Dotted paths (address.city, contacts.added) → underscore i18n key,
4343
- // since ngx-translate treats dots as nested lookups.
4344
- const key = `audit_field_${field.replace(/\./g, '_')}`;
4345
- return this.resolveLabel(key, field);
4410
+ const exact = this.lookupField(field);
4411
+ if (exact !== null)
4412
+ return exact;
4413
+ // [base, culture code] for a trailing, then a leading, culture segment.
4414
+ const trailing = TRAILING_CULTURE.exec(field);
4415
+ const leading = LEADING_CULTURE.exec(field);
4416
+ const candidates = [];
4417
+ if (trailing)
4418
+ candidates.push([trailing[1], trailing[2]]);
4419
+ if (leading)
4420
+ candidates.push([leading[2], leading[1]]);
4421
+ for (const [base, code] of candidates) {
4422
+ const name = this.cultureName(code);
4423
+ if (name)
4424
+ return `${this.lookupField(base) ?? this.humanize(base)} (${name})`;
4425
+ }
4426
+ return this.humanize(field);
4427
+ }
4428
+ /** The culture's name from `ef_localized_input_culture_<code>`, or null when the bundle does not know it. */
4429
+ cultureName(code) {
4430
+ const key = `ef_localized_input_culture_${code}`;
4431
+ const name = this.translate.instant(key);
4432
+ return name && name !== key ? name : null;
4433
+ }
4434
+ /** The entity key, then the global key, for a path; null when neither exists. */
4435
+ lookupField(path) {
4436
+ // Dotted paths (address.city, contacts.added) become an underscore
4437
+ // key, since ngx-translate treats dots as nested lookups.
4438
+ const field = path.replace(/\./g, '_');
4439
+ const type = this.entityType();
4440
+ const entity = type ? auditEntityKeySegment(type) : '';
4441
+ for (const key of entity ? [`audit_field_${entity}_${field}`, `audit_field_${field}`] : [`audit_field_${field}`]) {
4442
+ const translated = this.translate.instant(key);
4443
+ if (translated && translated !== key)
4444
+ return translated;
4445
+ }
4446
+ return null;
4346
4447
  }
4347
4448
  /** The note as shown: the formatter's text, else the raw note. */
4348
4449
  noteText(note) {
@@ -4395,7 +4496,7 @@ class EfChangeHistoryComponent {
4395
4496
  return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
4396
4497
  }
4397
4498
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfChangeHistoryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
4398
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfChangeHistoryComponent, isStandalone: true, selector: "ef-change-history", inputs: { entries: { classPropertyName: "entries", publicName: "entries", isSignal: true, isRequired: false, transformFunction: null }, titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, emptyKey: { classPropertyName: "emptyKey", publicName: "emptyKey", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, previewLimit: { classPropertyName: "previewLimit", publicName: "previewLimit", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, startCollapsed: { classPropertyName: "startCollapsed", publicName: "startCollapsed", isSignal: true, isRequired: false, transformFunction: null }, valueLists: { classPropertyName: "valueLists", publicName: "valueLists", isSignal: true, isRequired: false, transformFunction: null }, noteFormatter: { classPropertyName: "noteFormatter", publicName: "noteFormatter", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ef-card\n [titleKey]=\"titleKey()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"startCollapsed()\"\n>\n <ng-container head-extra>\n @if (hasMore()) {\n <ef-button\n variant=\"comptoir\"\n severity=\"ghost\"\n size=\"small\"\n icon=\"pi pi-window-maximize\"\n tooltipKey=\"audit_view_all\"\n (clickEvent)=\"openDialog($event)\"\n />\n }\n </ng-container>\n\n <ng-container\n *ngTemplateOutlet=\"timeline; context: { $implicit: previewRows(), compact: true }\"\n ></ng-container>\n\n @if (hasMore()) {\n <button type=\"button\" class=\"ch-more\" (click)=\"openDialog()\">\n {{ 'audit_view_all' | translate }} ({{ rows().length }})\n </button>\n }\n</ef-card>\n\n<!-- \"Show more\" dialog \u2014 full timeline, roomy + scrollable -->\n<ef-dialog\n [visible]=\"dialogVisible()\"\n (visibleChange)=\"dialogVisible.set($event)\"\n [headerKey]=\"titleKey()\"\n [defaultActions]=\"false\"\n size=\"lg\"\n [modal]=\"true\"\n position=\"center\"\n>\n <ng-container\n *ngTemplateOutlet=\"timeline; context: { $implicit: rows(), compact: false }\"\n ></ng-container>\n</ef-dialog>\n\n<ng-template #timeline let-items let-compact=\"compact\">\n @if (items.length === 0) {\n <p class=\"text-mute small\">{{ emptyKey() | translate }}</p>\n } @else {\n <div class=\"timeline\" [class.timeline--compact]=\"compact\">\n @for (row of items; track $index) {\n <div class=\"item\">\n <span class=\"dot\" [class]=\"row.meta.cls\" aria-hidden=\"true\"></span>\n <div class=\"body\">\n <div class=\"label\">{{ row.operationLabel }}</div>\n <div class=\"meta\">\n @if (row.entry.userName) {\n <span class=\"strong\">{{ row.entry.userName }}</span> \u00B7\n }\n {{ formatWhen(row.entry.occurredAt) }}\n </div>\n\n @if (row.entry.operation === 'status_changed' && (row.entry.fromStatus || row.entry.toStatus)) {\n <div class=\"ch-status small\">\n <span class=\"text-mute\" dir=\"auto\">{{ valueLabel('status', row.entry.fromStatus) || '\u2014' }}</span>\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"strong\" dir=\"auto\">{{ valueLabel('status', row.entry.toStatus) || '\u2014' }}</span>\n </div>\n }\n\n @if (row.entry.changes?.length) {\n <ul class=\"ch-changes small\">\n @for (c of row.entry.changes; track c.field) {\n <li>\n <span class=\"ch-field\">{{ fieldLabel(c.field) }}</span>\n <span class=\"text-mute\" dir=\"auto\">{{ valueLabel(c.field, c.oldValue) || '\u2014' }}</span>\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"strong\" dir=\"auto\">{{ valueLabel(c.field, c.newValue) || '\u2014' }}</span>\n </li>\n }\n </ul>\n }\n\n @if (row.entry.note) {\n <div class=\"ch-note small\">{{ noteText(row.entry.note) }}</div>\n }\n </div>\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: [":host{display:block}.dot.op-created{background:var(--st-delivered-fg, #16a34a);border-color:var(--st-delivered-fg, #16a34a)}.dot.op-updated{background:#2563eb;border-color:#2563eb}.dot.op-status{background:#f59e0b;border-color:#f59e0b}.dot.op-deleted{background:#dc2626;border-color:#dc2626}.dot.op-note{background:#64748b;border-color:#64748b}.dot.op-default{background:var(--ink-300, #94a3b8);border-color:var(--ink-300, #94a3b8)}.ch-status{display:flex;align-items:center;gap:6px;margin-top:4px}.item .body{min-width:0}.ch-changes{list-style:none;padding:0;margin:4px 0 0}.ch-changes li{display:flex;align-items:center;gap:6px;padding:1px 0;min-width:0}.ch-field{font-weight:600;min-width:120px}.timeline--compact .ch-changes .ch-field{flex:0 0 auto}.timeline--compact .ch-changes .text-mute,.timeline--compact .ch-changes .strong{flex:1 1 0;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.timeline:not(.timeline--compact) .ch-changes li{flex-wrap:wrap}.timeline:not(.timeline--compact) .ch-changes .text-mute,.timeline:not(.timeline--compact) .ch-changes .strong{overflow-wrap:anywhere}.ch-changes .pi,.ch-status .pi{font-size:10px;opacity:.6}.ch-note{margin-top:4px;padding-left:8px;border-left:2px solid var(--rule-soft, #e2e8f0);color:var(--ink-600, #475569)}.ch-more{margin-top:8px;border:0;background:transparent;cursor:pointer;padding:2px 0;font-size:12px;font-weight:600;color:var(--module, #2563eb)}.ch-more:hover{text-decoration:underline}.ch-changes .text-mute,.ch-changes .strong,.ch-status .text-mute,.ch-status .strong{text-align:start}:host-context([dir=rtl]) .ch-changes .pi-arrow-right,:host-context([dir=rtl]) .ch-status .pi-arrow-right{transform:scaleX(-1)}@media(max-width:767px){.timeline--compact .ch-changes li{flex-wrap:wrap}.timeline--compact .ch-changes .text-mute,.timeline--compact .ch-changes .strong{flex:1 1 auto;overflow:visible;white-space:normal;text-overflow:clip;overflow-wrap:anywhere}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslateModule }, { 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: EfDialogComponent, selector: "ef-dialog", inputs: ["header", "headerKey", "subtitle", "subtitleKey", "visible", "modal", "closable", "draggable", "resizable", "dismissableMask", "closeOnEscape", "severity", "icon", "size", "style", "styleClass", "position", "appendTo", "variant", "actions", "defaultActions", "saveLabelKey", "cancelLabelKey", "saveLabel", "cancelLabel", "saveIcon", "cancelIcon", "saveDisabled", "saveStyleClass", "cancelStyleClass", "saveSeverity", "cancelSeverity"], outputs: ["visibleChange", "showEvent", "hideEvent", "saveAction", "cancelAction", "actionClick"] }, { kind: "component", type: EfButtonComponent, selector: "ef-button", inputs: ["label", "labelKey", "icon", "ariaLabelKey", "variant", "severity", "size", "disabled", "rounded", "outlined", "raised", "text", "loading", "styleClass", "type", "badge", "badgeSeverity", "tooltip", "tooltipKey", "tooltipPosition", "autofocus"], outputs: ["clickEvent"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4499
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfChangeHistoryComponent, isStandalone: true, selector: "ef-change-history", inputs: { entries: { classPropertyName: "entries", publicName: "entries", isSignal: true, isRequired: false, transformFunction: null }, titleKey: { classPropertyName: "titleKey", publicName: "titleKey", isSignal: true, isRequired: false, transformFunction: null }, emptyKey: { classPropertyName: "emptyKey", publicName: "emptyKey", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, previewLimit: { classPropertyName: "previewLimit", publicName: "previewLimit", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, startCollapsed: { classPropertyName: "startCollapsed", publicName: "startCollapsed", isSignal: true, isRequired: false, transformFunction: null }, valueLists: { classPropertyName: "valueLists", publicName: "valueLists", isSignal: true, isRequired: false, transformFunction: null }, noteFormatter: { classPropertyName: "noteFormatter", publicName: "noteFormatter", isSignal: true, isRequired: false, transformFunction: null }, entityType: { classPropertyName: "entityType", publicName: "entityType", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<ef-card\n [titleKey]=\"titleKey()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"startCollapsed()\"\n>\n <ng-container head-extra>\n @if (hasMore()) {\n <ef-button\n variant=\"comptoir\"\n severity=\"ghost\"\n size=\"small\"\n icon=\"pi pi-window-maximize\"\n tooltipKey=\"audit_view_all\"\n (clickEvent)=\"openDialog($event)\"\n />\n }\n </ng-container>\n\n <ng-container\n *ngTemplateOutlet=\"timeline; context: { $implicit: previewRows(), compact: true }\"\n ></ng-container>\n\n @if (hasMore()) {\n <button type=\"button\" class=\"ch-more\" (click)=\"openDialog()\">\n {{ 'audit_view_all' | translate }} ({{ rows().length }})\n </button>\n }\n</ef-card>\n\n<!-- \"Show more\" dialog \u2014 full timeline, roomy + scrollable -->\n<ef-dialog\n [visible]=\"dialogVisible()\"\n (visibleChange)=\"dialogVisible.set($event)\"\n [headerKey]=\"titleKey()\"\n [defaultActions]=\"false\"\n size=\"lg\"\n [modal]=\"true\"\n position=\"center\"\n>\n <ng-container\n *ngTemplateOutlet=\"timeline; context: { $implicit: rows(), compact: false }\"\n ></ng-container>\n</ef-dialog>\n\n<ng-template #timeline let-items let-compact=\"compact\">\n @if (items.length === 0) {\n <p class=\"text-mute small\">{{ emptyKey() | translate }}</p>\n } @else {\n <div class=\"timeline\" [class.timeline--compact]=\"compact\">\n @for (row of items; track $index) {\n <div class=\"item\">\n <span class=\"dot\" [class]=\"row.meta.cls\" aria-hidden=\"true\"></span>\n <div class=\"body\">\n <div class=\"label\">{{ row.operationLabel }}</div>\n <div class=\"meta\">\n @if (row.entry.userName) {\n <span class=\"strong\">{{ row.entry.userName }}</span> \u00B7\n }\n {{ formatWhen(row.entry.occurredAt) }}\n </div>\n\n @if (row.entry.operation === 'status_changed' && (row.entry.fromStatus || row.entry.toStatus)) {\n <div class=\"ch-status small\">\n <span class=\"text-mute\" dir=\"auto\">{{ valueLabel('status', row.entry.fromStatus) || '\u2014' }}</span>\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"strong\" dir=\"auto\">{{ valueLabel('status', row.entry.toStatus) || '\u2014' }}</span>\n </div>\n }\n\n @if (row.entry.changes?.length) {\n <ul class=\"ch-changes small\">\n @for (c of row.entry.changes; track c.field) {\n <li>\n <span class=\"ch-field\">{{ fieldLabel(c.field) }}</span>\n <span class=\"text-mute\" dir=\"auto\">{{ valueLabel(c.field, c.oldValue) || '\u2014' }}</span>\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"strong\" dir=\"auto\">{{ valueLabel(c.field, c.newValue) || '\u2014' }}</span>\n </li>\n }\n </ul>\n }\n\n @if (row.entry.note) {\n <div class=\"ch-note small\">{{ noteText(row.entry.note) }}</div>\n }\n </div>\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: [":host{display:block}.dot.op-created{background:var(--st-delivered-fg, #16a34a);border-color:var(--st-delivered-fg, #16a34a)}.dot.op-updated{background:#2563eb;border-color:#2563eb}.dot.op-status{background:#f59e0b;border-color:#f59e0b}.dot.op-deleted{background:#dc2626;border-color:#dc2626}.dot.op-note{background:#64748b;border-color:#64748b}.dot.op-default{background:var(--ink-300, #94a3b8);border-color:var(--ink-300, #94a3b8)}.ch-status{display:flex;align-items:center;gap:6px;margin-top:4px}.item .body{min-width:0}.ch-changes{list-style:none;padding:0;margin:4px 0 0}.ch-changes li{display:flex;align-items:center;gap:6px;padding:1px 0;min-width:0}.ch-field{font-weight:600;min-width:120px}.timeline--compact .ch-changes .ch-field{flex:0 0 auto}.timeline--compact .ch-changes .text-mute,.timeline--compact .ch-changes .strong{flex:1 1 0;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.timeline:not(.timeline--compact) .ch-changes li{flex-wrap:wrap}.timeline:not(.timeline--compact) .ch-changes .text-mute,.timeline:not(.timeline--compact) .ch-changes .strong{overflow-wrap:anywhere}.ch-changes .pi,.ch-status .pi{font-size:10px;opacity:.6}.ch-note{margin-top:4px;padding-left:8px;border-left:2px solid var(--rule-soft, #e2e8f0);color:var(--ink-600, #475569)}.ch-more{margin-top:8px;border:0;background:transparent;cursor:pointer;padding:2px 0;font-size:12px;font-weight:600;color:var(--module, #2563eb)}.ch-more:hover{text-decoration:underline}.ch-changes .text-mute,.ch-changes .strong,.ch-status .text-mute,.ch-status .strong{text-align:start}:host-context([dir=rtl]) .ch-changes .pi-arrow-right,:host-context([dir=rtl]) .ch-status .pi-arrow-right{transform:scaleX(-1)}@media(max-width:767px){.timeline--compact .ch-changes li{flex-wrap:wrap}.timeline--compact .ch-changes .text-mute,.timeline--compact .ch-changes .strong{flex:1 1 auto;overflow:visible;white-space:normal;text-overflow:clip;overflow-wrap:anywhere}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslateModule }, { 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: EfDialogComponent, selector: "ef-dialog", inputs: ["header", "headerKey", "subtitle", "subtitleKey", "visible", "modal", "closable", "draggable", "resizable", "dismissableMask", "closeOnEscape", "severity", "icon", "size", "style", "styleClass", "position", "appendTo", "variant", "actions", "defaultActions", "saveLabelKey", "cancelLabelKey", "saveLabel", "cancelLabel", "saveIcon", "cancelIcon", "saveDisabled", "saveStyleClass", "cancelStyleClass", "saveSeverity", "cancelSeverity"], outputs: ["visibleChange", "showEvent", "hideEvent", "saveAction", "cancelAction", "actionClick"] }, { kind: "component", type: EfButtonComponent, selector: "ef-button", inputs: ["label", "labelKey", "icon", "ariaLabelKey", "variant", "severity", "size", "disabled", "rounded", "outlined", "raised", "text", "loading", "styleClass", "type", "badge", "badgeSeverity", "tooltip", "tooltipKey", "tooltipPosition", "autofocus"], outputs: ["clickEvent"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4399
4500
  }
4400
4501
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfChangeHistoryComponent, decorators: [{
4401
4502
  type: Component,
@@ -4406,7 +4507,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
4406
4507
  EfDialogComponent,
4407
4508
  EfButtonComponent,
4408
4509
  ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ef-card\n [titleKey]=\"titleKey()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"startCollapsed()\"\n>\n <ng-container head-extra>\n @if (hasMore()) {\n <ef-button\n variant=\"comptoir\"\n severity=\"ghost\"\n size=\"small\"\n icon=\"pi pi-window-maximize\"\n tooltipKey=\"audit_view_all\"\n (clickEvent)=\"openDialog($event)\"\n />\n }\n </ng-container>\n\n <ng-container\n *ngTemplateOutlet=\"timeline; context: { $implicit: previewRows(), compact: true }\"\n ></ng-container>\n\n @if (hasMore()) {\n <button type=\"button\" class=\"ch-more\" (click)=\"openDialog()\">\n {{ 'audit_view_all' | translate }} ({{ rows().length }})\n </button>\n }\n</ef-card>\n\n<!-- \"Show more\" dialog \u2014 full timeline, roomy + scrollable -->\n<ef-dialog\n [visible]=\"dialogVisible()\"\n (visibleChange)=\"dialogVisible.set($event)\"\n [headerKey]=\"titleKey()\"\n [defaultActions]=\"false\"\n size=\"lg\"\n [modal]=\"true\"\n position=\"center\"\n>\n <ng-container\n *ngTemplateOutlet=\"timeline; context: { $implicit: rows(), compact: false }\"\n ></ng-container>\n</ef-dialog>\n\n<ng-template #timeline let-items let-compact=\"compact\">\n @if (items.length === 0) {\n <p class=\"text-mute small\">{{ emptyKey() | translate }}</p>\n } @else {\n <div class=\"timeline\" [class.timeline--compact]=\"compact\">\n @for (row of items; track $index) {\n <div class=\"item\">\n <span class=\"dot\" [class]=\"row.meta.cls\" aria-hidden=\"true\"></span>\n <div class=\"body\">\n <div class=\"label\">{{ row.operationLabel }}</div>\n <div class=\"meta\">\n @if (row.entry.userName) {\n <span class=\"strong\">{{ row.entry.userName }}</span> \u00B7\n }\n {{ formatWhen(row.entry.occurredAt) }}\n </div>\n\n @if (row.entry.operation === 'status_changed' && (row.entry.fromStatus || row.entry.toStatus)) {\n <div class=\"ch-status small\">\n <span class=\"text-mute\" dir=\"auto\">{{ valueLabel('status', row.entry.fromStatus) || '\u2014' }}</span>\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"strong\" dir=\"auto\">{{ valueLabel('status', row.entry.toStatus) || '\u2014' }}</span>\n </div>\n }\n\n @if (row.entry.changes?.length) {\n <ul class=\"ch-changes small\">\n @for (c of row.entry.changes; track c.field) {\n <li>\n <span class=\"ch-field\">{{ fieldLabel(c.field) }}</span>\n <span class=\"text-mute\" dir=\"auto\">{{ valueLabel(c.field, c.oldValue) || '\u2014' }}</span>\n <i class=\"pi pi-arrow-right\"></i>\n <span class=\"strong\" dir=\"auto\">{{ valueLabel(c.field, c.newValue) || '\u2014' }}</span>\n </li>\n }\n </ul>\n }\n\n @if (row.entry.note) {\n <div class=\"ch-note small\">{{ noteText(row.entry.note) }}</div>\n }\n </div>\n </div>\n }\n </div>\n }\n</ng-template>\n", styles: [":host{display:block}.dot.op-created{background:var(--st-delivered-fg, #16a34a);border-color:var(--st-delivered-fg, #16a34a)}.dot.op-updated{background:#2563eb;border-color:#2563eb}.dot.op-status{background:#f59e0b;border-color:#f59e0b}.dot.op-deleted{background:#dc2626;border-color:#dc2626}.dot.op-note{background:#64748b;border-color:#64748b}.dot.op-default{background:var(--ink-300, #94a3b8);border-color:var(--ink-300, #94a3b8)}.ch-status{display:flex;align-items:center;gap:6px;margin-top:4px}.item .body{min-width:0}.ch-changes{list-style:none;padding:0;margin:4px 0 0}.ch-changes li{display:flex;align-items:center;gap:6px;padding:1px 0;min-width:0}.ch-field{font-weight:600;min-width:120px}.timeline--compact .ch-changes .ch-field{flex:0 0 auto}.timeline--compact .ch-changes .text-mute,.timeline--compact .ch-changes .strong{flex:1 1 0;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.timeline:not(.timeline--compact) .ch-changes li{flex-wrap:wrap}.timeline:not(.timeline--compact) .ch-changes .text-mute,.timeline:not(.timeline--compact) .ch-changes .strong{overflow-wrap:anywhere}.ch-changes .pi,.ch-status .pi{font-size:10px;opacity:.6}.ch-note{margin-top:4px;padding-left:8px;border-left:2px solid var(--rule-soft, #e2e8f0);color:var(--ink-600, #475569)}.ch-more{margin-top:8px;border:0;background:transparent;cursor:pointer;padding:2px 0;font-size:12px;font-weight:600;color:var(--module, #2563eb)}.ch-more:hover{text-decoration:underline}.ch-changes .text-mute,.ch-changes .strong,.ch-status .text-mute,.ch-status .strong{text-align:start}:host-context([dir=rtl]) .ch-changes .pi-arrow-right,:host-context([dir=rtl]) .ch-status .pi-arrow-right{transform:scaleX(-1)}@media(max-width:767px){.timeline--compact .ch-changes li{flex-wrap:wrap}.timeline--compact .ch-changes .text-mute,.timeline--compact .ch-changes .strong{flex:1 1 auto;overflow:visible;white-space:normal;text-overflow:clip;overflow-wrap:anywhere}}\n"] }]
4409
- }], propDecorators: { entries: [{ type: i0.Input, args: [{ isSignal: true, alias: "entries", required: false }] }], titleKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleKey", required: false }] }], emptyKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyKey", required: false }] }], limit: [{ type: i0.Input, args: [{ isSignal: true, alias: "limit", required: false }] }], previewLimit: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewLimit", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], startCollapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "startCollapsed", required: false }] }], valueLists: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueLists", required: false }] }], noteFormatter: [{ type: i0.Input, args: [{ isSignal: true, alias: "noteFormatter", required: false }] }] } });
4510
+ }], propDecorators: { entries: [{ type: i0.Input, args: [{ isSignal: true, alias: "entries", required: false }] }], titleKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleKey", required: false }] }], emptyKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyKey", required: false }] }], limit: [{ type: i0.Input, args: [{ isSignal: true, alias: "limit", required: false }] }], previewLimit: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewLimit", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], startCollapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "startCollapsed", required: false }] }], valueLists: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueLists", required: false }] }], noteFormatter: [{ type: i0.Input, args: [{ isSignal: true, alias: "noteFormatter", required: false }] }], entityType: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityType", required: false }] }] } });
4410
4511
 
4411
4512
  /**
4412
4513
  * Keyboard-navigable product search. Filters products by display name,
@@ -7051,7 +7152,7 @@ class EfOrderBuilderComponent {
7051
7152
  return OrderEntityHelper.calculateTotals(fee > 0 ? { ...order, shippingTotal: fee } : order);
7052
7153
  }
7053
7154
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfOrderBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7054
- 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 }, recipient: { classPropertyName: "recipient", publicName: "recipient", isSignal: true, isRequired: false, transformFunction: null }, defaultCallingCode: { classPropertyName: "defaultCallingCode", publicName: "defaultCallingCode", isSignal: true, isRequired: false, 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 }, priceList: { classPropertyName: "priceList", publicName: "priceList", isSignal: true, isRequired: false, transformFunction: null }, backendErrors: { classPropertyName: "backendErrors", publicName: "backendErrors", isSignal: true, isRequired: false, transformFunction: null }, shippingTotal: { classPropertyName: "shippingTotal", publicName: "shippingTotal", isSignal: true, isRequired: false, transformFunction: null }, auditEntries: { classPropertyName: "auditEntries", publicName: "auditEntries", isSignal: true, isRequired: false, transformFunction: null }, auditValueLists: { classPropertyName: "auditValueLists", publicName: "auditValueLists", isSignal: true, isRequired: false, transformFunction: null }, auditNoteFormatter: { classPropertyName: "auditNoteFormatter", publicName: "auditNoteFormatter", 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 @if (recipient(); as r) {\n <!-- No client to pick (a storefront order): who it goes to,\n read-only, in the customer's place. -->\n <div class=\"field col-6\">\n <ef-label labelKey=\"order_builder.recipient\" />\n <div\n class=\"ob-recipient\"\n role=\"group\"\n [attr.aria-label]=\"'order_builder.recipient' | translate\">\n <span class=\"ob-recipient-name\">{{ r.name }}</span>\n @if (recipientTel(); as tel) {\n <a class=\"ob-recipient-phone\" [href]=\"tel\" dir=\"ltr\">{{ r.phone }}</a>\n }\n @if (r.city) {\n <span class=\"ob-recipient-city\">{{ r.city }}</span>\n }\n @if (r.address) {\n <span class=\"ob-recipient-address\" [title]=\"r.address\">{{ r.address }}</span>\n }\n </div>\n </div>\n } @else {\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 }\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 [priceList]=\"priceList()\"\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 ariaLabelKey=\"common_delete\"\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 @if (shippingTotal() > 0) {\n <div class=\"row shipping\">\n <span class=\"lbl\">{{ 'order_builder.shipping' | translate }}</span>\n <span class=\"v\">{{ shippingTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n }\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 <!-- Host cards that belong beside the totals (delivery, notes): a column\n at desktop width, below the totals in the drawer otherwise. -->\n <ng-content select=\"[obRail]\" />\n\n <!-- Historique (optional, component-owned) -->\n @if (hasAudit()) {\n <ef-change-history\n [entries]=\"auditEntries()\"\n [valueLists]=\"auditValueLists()\"\n [noteFormatter]=\"auditNoteFormatter()\"\n />\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 .ob-recipient{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--s-3);row-gap:2px;min-width:0;font-size:13px;line-height:1.45;color:var(--text-soft)}:host .order-builder .ob-recipient>*{flex:0 1 auto;min-width:0;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;unicode-bidi:plaintext}:host .order-builder .ob-recipient-name{font-weight:600;color:var(--text)}:host .order-builder .ob-recipient-phone{color:var(--tenant-700);text-decoration:none;font-variant-numeric:tabular-nums}:host .order-builder .ob-recipient-phone:hover{text-decoration:underline}:host .order-builder .ob-recipient-address{flex-basis:100%;color:var(--text-mute)}: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;box-sizing:border-box;block-size:var(--ef-order-fab-h);--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-block:0;padding-inline:16px 10px;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(pointer:coarse){:host .order-builder .ob-fab .open{min-block-size:var(--hit-touch)}:host ef-data-card .ln-val{min-block-size:var(--hit-field)}:host .order-builder .ob-recipient-phone{line-height:var(--hit-touch)}}\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", "valueFormat"], 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", "labelParams", "for", "styleClass", "required"] }, { kind: "component", type: EfButtonComponent, selector: "ef-button", inputs: ["label", "labelKey", "icon", "ariaLabelKey", "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", "valueLists", "noteFormatter"] }, { kind: "component", type: EfProductTypeaheadComponent, selector: "ef-product-typeahead", inputs: ["products", "productKeyField", "placeholderKey", "disabled", "currency", "locale", "showVariants", "priceList"], 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", "rowPressed", "rowDisabled", "hasActionsColumn", "rowActionsContext", "showViewAction", "showEditAction", "showDuplicateAction", "showDeleteAction", "rowActionState", "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" }] });
7155
+ 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 }, recipient: { classPropertyName: "recipient", publicName: "recipient", isSignal: true, isRequired: false, transformFunction: null }, defaultCallingCode: { classPropertyName: "defaultCallingCode", publicName: "defaultCallingCode", isSignal: true, isRequired: false, 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 }, priceList: { classPropertyName: "priceList", publicName: "priceList", isSignal: true, isRequired: false, transformFunction: null }, backendErrors: { classPropertyName: "backendErrors", publicName: "backendErrors", isSignal: true, isRequired: false, transformFunction: null }, shippingTotal: { classPropertyName: "shippingTotal", publicName: "shippingTotal", isSignal: true, isRequired: false, transformFunction: null }, auditEntries: { classPropertyName: "auditEntries", publicName: "auditEntries", isSignal: true, isRequired: false, transformFunction: null }, auditValueLists: { classPropertyName: "auditValueLists", publicName: "auditValueLists", isSignal: true, isRequired: false, transformFunction: null }, auditNoteFormatter: { classPropertyName: "auditNoteFormatter", publicName: "auditNoteFormatter", 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 @if (recipient(); as r) {\n <!-- No client to pick (a storefront order): who it goes to,\n read-only, in the customer's place. -->\n <div class=\"field col-6\">\n <ef-label labelKey=\"order_builder.recipient\" />\n <div\n class=\"ob-recipient\"\n role=\"group\"\n [attr.aria-label]=\"'order_builder.recipient' | translate\">\n <span class=\"ob-recipient-name\">{{ r.name }}</span>\n @if (recipientTel(); as tel) {\n <a class=\"ob-recipient-phone\" [href]=\"tel\" dir=\"ltr\">{{ r.phone }}</a>\n }\n @if (r.city) {\n <span class=\"ob-recipient-city\">{{ r.city }}</span>\n }\n @if (r.address) {\n <span class=\"ob-recipient-address\" [title]=\"r.address\">{{ r.address }}</span>\n }\n </div>\n </div>\n } @else {\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 }\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 [priceList]=\"priceList()\"\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 ariaLabelKey=\"common_delete\"\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 @if (shippingTotal() > 0) {\n <div class=\"row shipping\">\n <span class=\"lbl\">{{ 'order_builder.shipping' | translate }}</span>\n <span class=\"v\">{{ shippingTotal() | number: '1.2-2' : locale() }}</span>\n </div>\n }\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 <!-- Host cards that belong beside the totals (delivery, notes): a column\n at desktop width, below the totals in the drawer otherwise. -->\n <ng-content select=\"[obRail]\" />\n\n <!-- Historique (optional, component-owned) -->\n @if (hasAudit()) {\n <ef-change-history\n [entries]=\"auditEntries()\"\n [valueLists]=\"auditValueLists()\"\n [noteFormatter]=\"auditNoteFormatter()\"\n />\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 .ob-recipient{display:flex;flex-wrap:wrap;align-items:baseline;column-gap:var(--s-3);row-gap:2px;min-width:0;font-size:13px;line-height:1.45;color:var(--text-soft)}:host .order-builder .ob-recipient>*{flex:0 1 auto;min-width:0;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;unicode-bidi:plaintext}:host .order-builder .ob-recipient-name{font-weight:600;color:var(--text)}:host .order-builder .ob-recipient-phone{color:var(--tenant-700);text-decoration:none;font-variant-numeric:tabular-nums}:host .order-builder .ob-recipient-phone:hover{text-decoration:underline}:host .order-builder .ob-recipient-address{flex-basis:100%;color:var(--text-mute)}: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;box-sizing:border-box;block-size:var(--ef-order-fab-h);--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-block:0;padding-inline:16px 10px;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(pointer:coarse){:host .order-builder .ob-fab .open{min-block-size:var(--hit-touch)}:host ef-data-card .ln-val{min-block-size:var(--hit-field)}:host .order-builder .ob-recipient-phone{line-height:var(--hit-touch)}}\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", "valueFormat"], 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", "labelParams", "for", "styleClass", "required"] }, { kind: "component", type: EfButtonComponent, selector: "ef-button", inputs: ["label", "labelKey", "icon", "ariaLabelKey", "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", "valueLists", "noteFormatter", "entityType"] }, { kind: "component", type: EfProductTypeaheadComponent, selector: "ef-product-typeahead", inputs: ["products", "productKeyField", "placeholderKey", "disabled", "currency", "locale", "showVariants", "priceList"], 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", "rowPressed", "rowDisabled", "hasActionsColumn", "rowActionsContext", "showViewAction", "showEditAction", "showDuplicateAction", "showDeleteAction", "rowActionState", "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" }] });
7055
7156
  }
7056
7157
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfOrderBuilderComponent, decorators: [{
7057
7158
  type: Component,
@@ -9906,6 +10007,7 @@ class EfTabsComponent {
9906
10007
  * `fr.json` / `ar.json`. */
9907
10008
  dirtyLabelKey = 'common.dirty';
9908
10009
  valueChange = new EventEmitter();
10010
+ host = inject(ElementRef);
9909
10011
  tabEls;
9910
10012
  /* ── Content children (optional projected panels) ────────────── */
9911
10013
  panels;
@@ -9967,6 +10069,13 @@ class EfTabsComponent {
9967
10069
  default:
9968
10070
  return;
9969
10071
  }
10072
+ // In a right-to-left tablist the next tab sits to the left, so the
10073
+ // arrows swap (WAI-ARIA tabs pattern); Home and End keep their meaning.
10074
+ if ((event.key === 'ArrowRight' || event.key === 'ArrowLeft') && this.isRtl(event.currentTarget)) {
10075
+ nextPos = event.key === 'ArrowRight'
10076
+ ? enabledPositions[(here - 1 + enabledPositions.length) % enabledPositions.length]
10077
+ : enabledPositions[(here + 1) % enabledPositions.length];
10078
+ }
9970
10079
  event.preventDefault();
9971
10080
  const target = all[nextPos];
9972
10081
  this.select(target);
@@ -9975,11 +10084,19 @@ class EfTabsComponent {
9975
10084
  const el = this.tabEls?.toArray()[nextPos]?.nativeElement;
9976
10085
  el?.focus();
9977
10086
  }
10087
+ isRtl(target) {
10088
+ const el = target instanceof HTMLElement ? target : this.host.nativeElement;
10089
+ if (getComputedStyle(el).direction === 'rtl')
10090
+ return true;
10091
+ // Only reached in jsdom, which does not inherit `direction`; a browser
10092
+ // answers above.
10093
+ return el.closest('[dir]')?.getAttribute('dir') === 'rtl';
10094
+ }
9978
10095
  trackValue(_, item) {
9979
10096
  return item.value;
9980
10097
  }
9981
10098
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9982
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfTabsComponent, isStandalone: true, selector: "ef-tabs", inputs: { items: "items", value: "value", variant: "variant", module: "module", overflow: ["overflow", "overflow", booleanAttribute], hideCounts: ["hideCounts", "hideCounts", booleanAttribute], ariaLabel: "ariaLabel", ariaLabelKey: "ariaLabelKey", dirtyLabelKey: "dirtyLabelKey" }, outputs: { valueChange: "valueChange" }, host: { properties: { "class.ef-tabs--pill": "variant === 'pill'", "class.ef-tabs--underline": "variant === 'underline'", "class.ef-tabs--overflow": "overflow", "attr.data-module": "module ?? null" } }, queries: [{ propertyName: "panels", predicate: EfTabPanelDirective }], viewQueries: [{ propertyName: "tabEls", predicate: ["tabEl"], descendants: true }], ngImport: i0, template: "<div class=\"ef-tabs__list\" role=\"tablist\" [attr.aria-label]=\"ariaLabelKey ? (ariaLabelKey | translate) : ariaLabel\">\n @for (item of items; track trackValue($index, item); let i = $index) {\n <button\n #tabEl\n type=\"button\"\n class=\"ef-tabs__tab\"\n role=\"tab\"\n [class.is-active]=\"item.value === value\"\n [class.is-disabled]=\"!!item.disabled\"\n [attr.aria-selected]=\"item.value === value\"\n [attr.aria-disabled]=\"item.disabled ? 'true' : null\"\n [disabled]=\"item.disabled\"\n [tabindex]=\"item.value === value ? 0 : -1\"\n (click)=\"select(item)\"\n (keydown)=\"onKeydown($event, i)\"\n >\n @if (item.icon) {\n <i class=\"ef-tabs__ico\" [class]=\"item.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"ef-tabs__label\">\n @if (item.labelKey) {\n {{ item.labelKey | translate }}\n } @else {\n {{ item.label }}\n }\n </span>\n @if (!hideCounts && item.count !== undefined && item.count !== null) {\n <span class=\"ef-tabs__count\">{{ item.count }}</span>\n }\n @if (item.dirty) {\n <span class=\"ef-tabs__dot\" [attr.aria-label]=\"dirtyLabelKey | translate\"></span>\n }\n </button>\n }\n</div>\n\n@if (hasPanels()) {\n <div class=\"ef-tabs__panels\" role=\"tabpanel\">\n @if (activePanel(); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n }\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;min-width:0}.ef-tabs__list{position:relative;display:flex;align-items:stretch;gap:2px;min-width:0;max-width:100%;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.ef-tabs__list>*{flex:0 0 auto}.ef-tabs__panels{display:block;padding-top:var(--s-5);min-width:0;animation:ef-tabs-panel-in var(--t-base, .26s) var(--ease-out, ease-out)}@keyframes ef-tabs-panel-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.ef-tabs__list::-webkit-scrollbar{display:none}.ef-tabs__tab{appearance:none;background:transparent;border:0;font:inherit;font-size:12.5px;font-weight:600;letter-spacing:-.005em;color:var(--text-mute);cursor:pointer;display:inline-flex;align-items:center;gap:8px;white-space:nowrap;transition:color var(--t-fast, .14s) var(--ease-out, ease-out),background-color var(--t-fast, .14s) var(--ease-out, ease-out),border-color var(--t-base, .26s) var(--ease-spring, cubic-bezier(.34, 1.56, .64, 1))}.ef-tabs__tab:focus-visible{outline:0}.ef-tabs__tab.is-disabled,.ef-tabs__tab[disabled]{opacity:.4;cursor:not-allowed}.ef-tabs__ico{font-size:13px;opacity:.75;flex-shrink:0}.ef-tabs__tab.is-active .ef-tabs__ico{opacity:1}.ef-tabs__count{font-family:var(--font-mono);font-size:10.5px;font-weight:700;background:var(--paper);border:1px solid var(--rule);color:var(--text-mute);padding:1px 7px;border-radius:var(--r-pill);min-width:20px;text-align:center;line-height:1.5;font-variant-numeric:tabular-nums}.ef-tabs__dot{width:6px;height:6px;border-radius:50%;background:var(--tenant-500);flex-shrink:0}:host(.ef-tabs--pill) .ef-tabs__list{background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-pill);padding:4px;gap:2px}:host(.ef-tabs--pill) .ef-tabs__tab{min-height:var(--hit, 32px);padding:7px 14px;border-radius:var(--r-pill)}:host(.ef-tabs--pill) .ef-tabs__tab:hover:not(.is-active):not(.is-disabled){color:var(--text)}:host(.ef-tabs--pill) .ef-tabs__tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb,var(--ink-active, #0f1115) 40%,transparent)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active{background:var(--ink-active, #0f1115);color:var(--paper)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active .ef-tabs__count{background:#ffffff2e;border-color:transparent;color:var(--paper)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active .ef-tabs__dot{background:var(--paper)}:host(.ef-tabs--underline) .ef-tabs__list{display:flex;border-bottom:1px solid var(--rule);padding:0;gap:2px}:host(.ef-tabs--underline) .ef-tabs__tab{min-height:42px;padding:11px 14px;border-bottom:2px solid transparent;margin-bottom:-1px;border-radius:var(--r-sm) var(--r-sm) 0 0;font-size:13.5px}:host(.ef-tabs--underline) .ef-tabs__tab:hover:not(.is-active):not(.is-disabled){color:var(--text);background:var(--paper)}:host(.ef-tabs--underline) .ef-tabs__tab:focus-visible{background:var(--paper-alt);box-shadow:inset 0 0 0 2px var(--tenant-200)}:host(.ef-tabs--underline) .ef-tabs__tab.is-active{color:var(--text);border-bottom-color:var(--module, var(--ink-active))}:host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__count{background:color-mix(in srgb,var(--module, var(--ink-active)) 14%,var(--paper-alt));border-color:transparent;color:var(--module, var(--ink-active))}:host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__dot{background:var(--module, var(--tenant-500))}:host[data-module=sales]{--module: var(--m-sales)}:host[data-module=purchase]{--module: var(--m-purchase)}:host[data-module=stock]{--module: var(--m-stock)}:host[data-module=pos]{--module: var(--m-pos)}:host[data-module=marketing]{--module: var(--m-marketing)}:host[data-module=store]{--module: var(--m-store)}:host[data-module=finance]{--module: var(--m-finance)}:host[data-module=admin]{--module: var(--m-admin)}:host(.ef-tabs--overflow){position:relative;display:block}:host(.ef-tabs--overflow) .ef-tabs__list{display:flex;width:100%}:host(.ef-tabs--overflow):before,:host(.ef-tabs--overflow):after{content:\"\";position:absolute;top:0;bottom:0;width:36px;pointer-events:none;z-index:2}:host(.ef-tabs--overflow):before{inset-inline-start:0;background:linear-gradient(to right,var(--paper-alt),transparent)}:host(.ef-tabs--overflow):after{inset-inline-end:0;background:linear-gradient(to left,var(--paper-alt),transparent)}[data-theme=dark] :host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__count{background:color-mix(in srgb,var(--module, var(--ink-active)) 28%,transparent)}@media(pointer:coarse){:host(.ef-tabs--pill) .ef-tabs__tab,:host(.ef-tabs--underline) .ef-tabs__tab{min-height:var(--hit-touch)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10099
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfTabsComponent, isStandalone: true, selector: "ef-tabs", inputs: { items: "items", value: "value", variant: "variant", module: "module", overflow: ["overflow", "overflow", booleanAttribute], hideCounts: ["hideCounts", "hideCounts", booleanAttribute], ariaLabel: "ariaLabel", ariaLabelKey: "ariaLabelKey", dirtyLabelKey: "dirtyLabelKey" }, outputs: { valueChange: "valueChange" }, host: { properties: { "class.ef-tabs--pill": "variant === 'pill'", "class.ef-tabs--underline": "variant === 'underline'", "class.ef-tabs--overflow": "overflow", "attr.data-module": "module ?? null" } }, queries: [{ propertyName: "panels", predicate: EfTabPanelDirective }], viewQueries: [{ propertyName: "tabEls", predicate: ["tabEl"], descendants: true }], ngImport: i0, template: "<div class=\"ef-tabs__list\" role=\"tablist\" [attr.aria-label]=\"ariaLabelKey ? (ariaLabelKey | translate) : ariaLabel\">\n @for (item of items; track trackValue($index, item); let i = $index) {\n <button\n #tabEl\n type=\"button\"\n class=\"ef-tabs__tab\"\n role=\"tab\"\n [class.is-active]=\"item.value === value\"\n [class.is-disabled]=\"!!item.disabled\"\n [class.is-danger]=\"item.tone === 'danger'\"\n [attr.id]=\"item.id || null\"\n [attr.aria-controls]=\"item.panelId || null\"\n [attr.aria-selected]=\"item.value === value\"\n [attr.aria-disabled]=\"item.disabled ? 'true' : null\"\n [disabled]=\"item.disabled\"\n [tabindex]=\"item.value === value ? 0 : -1\"\n (click)=\"select(item)\"\n (keydown)=\"onKeydown($event, i)\"\n >\n @if (item.icon) {\n <i class=\"ef-tabs__ico\" [class]=\"item.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"ef-tabs__label\">\n @if (item.labelKey) {\n {{ item.labelKey | translate }}\n } @else {\n {{ item.label }}\n }\n </span>\n @if (!hideCounts && item.count !== undefined && item.count !== null) {\n <span class=\"ef-tabs__count\">{{ item.count }}</span>\n }\n @if (item.noteKey) {\n <span class=\"ef-tabs__note\">{{ item.noteKey | translate }}</span>\n }\n @if (item.badgeKey) {\n <!-- Whitespace inside, as in the label, so the accessible name\n reads \"manquant personnalis\u00E9\", not one word. -->\n <span class=\"ef-tabs__badge\">\n {{ item.badgeKey | translate }}\n </span>\n }\n @if (item.dirty) {\n <span class=\"ef-tabs__dot\" [attr.aria-label]=\"dirtyLabelKey | translate\"></span>\n }\n </button>\n }\n</div>\n\n@if (hasPanels()) {\n <div class=\"ef-tabs__panels\" role=\"tabpanel\">\n @if (activePanel(); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n }\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;min-width:0}.ef-tabs__list{position:relative;display:flex;align-items:stretch;gap:2px;min-width:0;max-width:100%;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.ef-tabs__list>*{flex:0 0 auto}.ef-tabs__panels{display:block;padding-top:var(--s-5);min-width:0;animation:ef-tabs-panel-in var(--t-base, .26s) var(--ease-out, ease-out)}@keyframes ef-tabs-panel-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.ef-tabs__list::-webkit-scrollbar{display:none}.ef-tabs__tab{appearance:none;background:transparent;border:0;font:inherit;font-size:12.5px;font-weight:600;letter-spacing:-.005em;color:var(--text-mute);cursor:pointer;display:inline-flex;align-items:center;gap:8px;white-space:nowrap;transition:color var(--t-fast, .14s) var(--ease-out, ease-out),background-color var(--t-fast, .14s) var(--ease-out, ease-out),border-color var(--t-base, .26s) var(--ease-spring, cubic-bezier(.34, 1.56, .64, 1))}.ef-tabs__tab:focus-visible{outline:0}.ef-tabs__tab.is-disabled,.ef-tabs__tab[disabled]{opacity:.4;cursor:not-allowed}.ef-tabs__ico{font-size:13px;opacity:.75;flex-shrink:0}.ef-tabs__tab.is-active .ef-tabs__ico{opacity:1}.ef-tabs__count{font-family:var(--font-mono);font-size:10.5px;font-weight:700;background:var(--paper);border:1px solid var(--rule);color:var(--text-mute);padding:1px 7px;border-radius:var(--r-pill);min-width:20px;text-align:center;line-height:1.5;font-variant-numeric:tabular-nums}.ef-tabs__dot{width:6px;height:6px;border-radius:50%;background:var(--tenant-500);flex-shrink:0}.ef-tabs__note{font-size:11px;font-weight:500}.ef-tabs__badge{font-size:10.5px;font-weight:600;line-height:1.5;padding:0 6px;border:1px solid currentColor;border-radius:var(--r-pill);white-space:nowrap}:host(.ef-tabs--pill) .ef-tabs__list{background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-pill);padding:4px;gap:2px}:host(.ef-tabs--pill) .ef-tabs__tab{min-height:var(--hit, 32px);padding:7px 14px;border-radius:var(--r-pill)}:host(.ef-tabs--pill) .ef-tabs__tab:hover:not(.is-active):not(.is-disabled){color:var(--text)}:host(.ef-tabs--pill) .ef-tabs__tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb,var(--ink-active, #0f1115) 40%,transparent)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active{background:var(--ink-active, #0f1115);color:var(--paper)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active .ef-tabs__count{background:#ffffff2e;border-color:transparent;color:var(--paper)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active .ef-tabs__dot{background:var(--paper)}:host(.ef-tabs--underline) .ef-tabs__list{display:flex;border-bottom:1px solid var(--rule);padding:0;gap:2px}:host(.ef-tabs--underline) .ef-tabs__tab{min-height:42px;padding:11px 14px;border-bottom:2px solid transparent;margin-bottom:-1px;border-radius:var(--r-sm) var(--r-sm) 0 0;font-size:13.5px}:host(.ef-tabs--underline) .ef-tabs__tab:hover:not(.is-active):not(.is-disabled){color:var(--text);background:var(--paper)}:host(.ef-tabs--underline) .ef-tabs__tab:focus-visible{background:var(--paper-alt);box-shadow:inset 0 0 0 2px var(--tenant-200)}:host(.ef-tabs--underline) .ef-tabs__tab.is-active{color:var(--text);border-bottom-color:var(--module, var(--ink-active))}:host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__count{background:color-mix(in srgb,var(--module, var(--ink-active)) 14%,var(--paper-alt));border-color:transparent;color:var(--module, var(--ink-active))}:host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__dot{background:var(--module, var(--tenant-500))}.ef-tabs__tab.is-danger,:host(.ef-tabs--pill) .ef-tabs__tab.is-danger:hover:not(.is-active):not(.is-disabled),:host(.ef-tabs--underline) .ef-tabs__tab.is-danger:hover:not(.is-active):not(.is-disabled){color:var(--st-cancelled-fg)}:host(.ef-tabs--pill) .ef-tabs__tab.is-danger.is-active{color:var(--paper);box-shadow:inset 0 0 0 2px var(--st-cancelled-fg)}:host(.ef-tabs--underline) .ef-tabs__tab.is-danger.is-active{color:var(--st-cancelled-fg);border-bottom-color:var(--st-cancelled-fg)}:host(.ef-tabs--pill) .ef-tabs__tab.is-danger.is-active:focus-visible{box-shadow:inset 0 0 0 2px var(--st-cancelled-fg),0 0 0 2px color-mix(in srgb,var(--ink-active, #0f1115) 40%,transparent)}:host[data-module=sales]{--module: var(--m-sales)}:host[data-module=purchase]{--module: var(--m-purchase)}:host[data-module=stock]{--module: var(--m-stock)}:host[data-module=pos]{--module: var(--m-pos)}:host[data-module=marketing]{--module: var(--m-marketing)}:host[data-module=store]{--module: var(--m-store)}:host[data-module=finance]{--module: var(--m-finance)}:host[data-module=admin]{--module: var(--m-admin)}:host(.ef-tabs--overflow){position:relative;display:block}:host(.ef-tabs--overflow) .ef-tabs__list{display:flex;width:100%}:host(.ef-tabs--overflow):before,:host(.ef-tabs--overflow):after{content:\"\";position:absolute;top:0;bottom:0;width:36px;pointer-events:none;z-index:2}:host(.ef-tabs--overflow):before{inset-inline-start:0;background:linear-gradient(to right,var(--paper-alt),transparent)}:host(.ef-tabs--overflow):after{inset-inline-end:0;background:linear-gradient(to left,var(--paper-alt),transparent)}[data-theme=dark] :host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__count{background:color-mix(in srgb,var(--module, var(--ink-active)) 28%,transparent)}@media(pointer:coarse){:host(.ef-tabs--pill) .ef-tabs__tab,:host(.ef-tabs--underline) .ef-tabs__tab{min-height:var(--hit-touch)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9983
10100
  }
9984
10101
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfTabsComponent, decorators: [{
9985
10102
  type: Component,
@@ -9988,7 +10105,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
9988
10105
  '[class.ef-tabs--underline]': "variant === 'underline'",
9989
10106
  '[class.ef-tabs--overflow]': 'overflow',
9990
10107
  '[attr.data-module]': 'module ?? null',
9991
- }, template: "<div class=\"ef-tabs__list\" role=\"tablist\" [attr.aria-label]=\"ariaLabelKey ? (ariaLabelKey | translate) : ariaLabel\">\n @for (item of items; track trackValue($index, item); let i = $index) {\n <button\n #tabEl\n type=\"button\"\n class=\"ef-tabs__tab\"\n role=\"tab\"\n [class.is-active]=\"item.value === value\"\n [class.is-disabled]=\"!!item.disabled\"\n [attr.aria-selected]=\"item.value === value\"\n [attr.aria-disabled]=\"item.disabled ? 'true' : null\"\n [disabled]=\"item.disabled\"\n [tabindex]=\"item.value === value ? 0 : -1\"\n (click)=\"select(item)\"\n (keydown)=\"onKeydown($event, i)\"\n >\n @if (item.icon) {\n <i class=\"ef-tabs__ico\" [class]=\"item.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"ef-tabs__label\">\n @if (item.labelKey) {\n {{ item.labelKey | translate }}\n } @else {\n {{ item.label }}\n }\n </span>\n @if (!hideCounts && item.count !== undefined && item.count !== null) {\n <span class=\"ef-tabs__count\">{{ item.count }}</span>\n }\n @if (item.dirty) {\n <span class=\"ef-tabs__dot\" [attr.aria-label]=\"dirtyLabelKey | translate\"></span>\n }\n </button>\n }\n</div>\n\n@if (hasPanels()) {\n <div class=\"ef-tabs__panels\" role=\"tabpanel\">\n @if (activePanel(); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n }\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;min-width:0}.ef-tabs__list{position:relative;display:flex;align-items:stretch;gap:2px;min-width:0;max-width:100%;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.ef-tabs__list>*{flex:0 0 auto}.ef-tabs__panels{display:block;padding-top:var(--s-5);min-width:0;animation:ef-tabs-panel-in var(--t-base, .26s) var(--ease-out, ease-out)}@keyframes ef-tabs-panel-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.ef-tabs__list::-webkit-scrollbar{display:none}.ef-tabs__tab{appearance:none;background:transparent;border:0;font:inherit;font-size:12.5px;font-weight:600;letter-spacing:-.005em;color:var(--text-mute);cursor:pointer;display:inline-flex;align-items:center;gap:8px;white-space:nowrap;transition:color var(--t-fast, .14s) var(--ease-out, ease-out),background-color var(--t-fast, .14s) var(--ease-out, ease-out),border-color var(--t-base, .26s) var(--ease-spring, cubic-bezier(.34, 1.56, .64, 1))}.ef-tabs__tab:focus-visible{outline:0}.ef-tabs__tab.is-disabled,.ef-tabs__tab[disabled]{opacity:.4;cursor:not-allowed}.ef-tabs__ico{font-size:13px;opacity:.75;flex-shrink:0}.ef-tabs__tab.is-active .ef-tabs__ico{opacity:1}.ef-tabs__count{font-family:var(--font-mono);font-size:10.5px;font-weight:700;background:var(--paper);border:1px solid var(--rule);color:var(--text-mute);padding:1px 7px;border-radius:var(--r-pill);min-width:20px;text-align:center;line-height:1.5;font-variant-numeric:tabular-nums}.ef-tabs__dot{width:6px;height:6px;border-radius:50%;background:var(--tenant-500);flex-shrink:0}:host(.ef-tabs--pill) .ef-tabs__list{background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-pill);padding:4px;gap:2px}:host(.ef-tabs--pill) .ef-tabs__tab{min-height:var(--hit, 32px);padding:7px 14px;border-radius:var(--r-pill)}:host(.ef-tabs--pill) .ef-tabs__tab:hover:not(.is-active):not(.is-disabled){color:var(--text)}:host(.ef-tabs--pill) .ef-tabs__tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb,var(--ink-active, #0f1115) 40%,transparent)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active{background:var(--ink-active, #0f1115);color:var(--paper)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active .ef-tabs__count{background:#ffffff2e;border-color:transparent;color:var(--paper)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active .ef-tabs__dot{background:var(--paper)}:host(.ef-tabs--underline) .ef-tabs__list{display:flex;border-bottom:1px solid var(--rule);padding:0;gap:2px}:host(.ef-tabs--underline) .ef-tabs__tab{min-height:42px;padding:11px 14px;border-bottom:2px solid transparent;margin-bottom:-1px;border-radius:var(--r-sm) var(--r-sm) 0 0;font-size:13.5px}:host(.ef-tabs--underline) .ef-tabs__tab:hover:not(.is-active):not(.is-disabled){color:var(--text);background:var(--paper)}:host(.ef-tabs--underline) .ef-tabs__tab:focus-visible{background:var(--paper-alt);box-shadow:inset 0 0 0 2px var(--tenant-200)}:host(.ef-tabs--underline) .ef-tabs__tab.is-active{color:var(--text);border-bottom-color:var(--module, var(--ink-active))}:host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__count{background:color-mix(in srgb,var(--module, var(--ink-active)) 14%,var(--paper-alt));border-color:transparent;color:var(--module, var(--ink-active))}:host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__dot{background:var(--module, var(--tenant-500))}:host[data-module=sales]{--module: var(--m-sales)}:host[data-module=purchase]{--module: var(--m-purchase)}:host[data-module=stock]{--module: var(--m-stock)}:host[data-module=pos]{--module: var(--m-pos)}:host[data-module=marketing]{--module: var(--m-marketing)}:host[data-module=store]{--module: var(--m-store)}:host[data-module=finance]{--module: var(--m-finance)}:host[data-module=admin]{--module: var(--m-admin)}:host(.ef-tabs--overflow){position:relative;display:block}:host(.ef-tabs--overflow) .ef-tabs__list{display:flex;width:100%}:host(.ef-tabs--overflow):before,:host(.ef-tabs--overflow):after{content:\"\";position:absolute;top:0;bottom:0;width:36px;pointer-events:none;z-index:2}:host(.ef-tabs--overflow):before{inset-inline-start:0;background:linear-gradient(to right,var(--paper-alt),transparent)}:host(.ef-tabs--overflow):after{inset-inline-end:0;background:linear-gradient(to left,var(--paper-alt),transparent)}[data-theme=dark] :host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__count{background:color-mix(in srgb,var(--module, var(--ink-active)) 28%,transparent)}@media(pointer:coarse){:host(.ef-tabs--pill) .ef-tabs__tab,:host(.ef-tabs--underline) .ef-tabs__tab{min-height:var(--hit-touch)}}\n"] }]
10108
+ }, template: "<div class=\"ef-tabs__list\" role=\"tablist\" [attr.aria-label]=\"ariaLabelKey ? (ariaLabelKey | translate) : ariaLabel\">\n @for (item of items; track trackValue($index, item); let i = $index) {\n <button\n #tabEl\n type=\"button\"\n class=\"ef-tabs__tab\"\n role=\"tab\"\n [class.is-active]=\"item.value === value\"\n [class.is-disabled]=\"!!item.disabled\"\n [class.is-danger]=\"item.tone === 'danger'\"\n [attr.id]=\"item.id || null\"\n [attr.aria-controls]=\"item.panelId || null\"\n [attr.aria-selected]=\"item.value === value\"\n [attr.aria-disabled]=\"item.disabled ? 'true' : null\"\n [disabled]=\"item.disabled\"\n [tabindex]=\"item.value === value ? 0 : -1\"\n (click)=\"select(item)\"\n (keydown)=\"onKeydown($event, i)\"\n >\n @if (item.icon) {\n <i class=\"ef-tabs__ico\" [class]=\"item.icon\" aria-hidden=\"true\"></i>\n }\n <span class=\"ef-tabs__label\">\n @if (item.labelKey) {\n {{ item.labelKey | translate }}\n } @else {\n {{ item.label }}\n }\n </span>\n @if (!hideCounts && item.count !== undefined && item.count !== null) {\n <span class=\"ef-tabs__count\">{{ item.count }}</span>\n }\n @if (item.noteKey) {\n <span class=\"ef-tabs__note\">{{ item.noteKey | translate }}</span>\n }\n @if (item.badgeKey) {\n <!-- Whitespace inside, as in the label, so the accessible name\n reads \"manquant personnalis\u00E9\", not one word. -->\n <span class=\"ef-tabs__badge\">\n {{ item.badgeKey | translate }}\n </span>\n }\n @if (item.dirty) {\n <span class=\"ef-tabs__dot\" [attr.aria-label]=\"dirtyLabelKey | translate\"></span>\n }\n </button>\n }\n</div>\n\n@if (hasPanels()) {\n <div class=\"ef-tabs__panels\" role=\"tabpanel\">\n @if (activePanel(); as tpl) {\n <ng-container [ngTemplateOutlet]=\"tpl\"></ng-container>\n }\n </div>\n}\n", styles: ["@charset \"UTF-8\";:host{display:block;min-width:0}.ef-tabs__list{position:relative;display:flex;align-items:stretch;gap:2px;min-width:0;max-width:100%;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.ef-tabs__list>*{flex:0 0 auto}.ef-tabs__panels{display:block;padding-top:var(--s-5);min-width:0;animation:ef-tabs-panel-in var(--t-base, .26s) var(--ease-out, ease-out)}@keyframes ef-tabs-panel-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.ef-tabs__list::-webkit-scrollbar{display:none}.ef-tabs__tab{appearance:none;background:transparent;border:0;font:inherit;font-size:12.5px;font-weight:600;letter-spacing:-.005em;color:var(--text-mute);cursor:pointer;display:inline-flex;align-items:center;gap:8px;white-space:nowrap;transition:color var(--t-fast, .14s) var(--ease-out, ease-out),background-color var(--t-fast, .14s) var(--ease-out, ease-out),border-color var(--t-base, .26s) var(--ease-spring, cubic-bezier(.34, 1.56, .64, 1))}.ef-tabs__tab:focus-visible{outline:0}.ef-tabs__tab.is-disabled,.ef-tabs__tab[disabled]{opacity:.4;cursor:not-allowed}.ef-tabs__ico{font-size:13px;opacity:.75;flex-shrink:0}.ef-tabs__tab.is-active .ef-tabs__ico{opacity:1}.ef-tabs__count{font-family:var(--font-mono);font-size:10.5px;font-weight:700;background:var(--paper);border:1px solid var(--rule);color:var(--text-mute);padding:1px 7px;border-radius:var(--r-pill);min-width:20px;text-align:center;line-height:1.5;font-variant-numeric:tabular-nums}.ef-tabs__dot{width:6px;height:6px;border-radius:50%;background:var(--tenant-500);flex-shrink:0}.ef-tabs__note{font-size:11px;font-weight:500}.ef-tabs__badge{font-size:10.5px;font-weight:600;line-height:1.5;padding:0 6px;border:1px solid currentColor;border-radius:var(--r-pill);white-space:nowrap}:host(.ef-tabs--pill) .ef-tabs__list{background:var(--paper-alt);border:1px solid var(--rule);border-radius:var(--r-pill);padding:4px;gap:2px}:host(.ef-tabs--pill) .ef-tabs__tab{min-height:var(--hit, 32px);padding:7px 14px;border-radius:var(--r-pill)}:host(.ef-tabs--pill) .ef-tabs__tab:hover:not(.is-active):not(.is-disabled){color:var(--text)}:host(.ef-tabs--pill) .ef-tabs__tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb,var(--ink-active, #0f1115) 40%,transparent)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active{background:var(--ink-active, #0f1115);color:var(--paper)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active .ef-tabs__count{background:#ffffff2e;border-color:transparent;color:var(--paper)}:host(.ef-tabs--pill) .ef-tabs__tab.is-active .ef-tabs__dot{background:var(--paper)}:host(.ef-tabs--underline) .ef-tabs__list{display:flex;border-bottom:1px solid var(--rule);padding:0;gap:2px}:host(.ef-tabs--underline) .ef-tabs__tab{min-height:42px;padding:11px 14px;border-bottom:2px solid transparent;margin-bottom:-1px;border-radius:var(--r-sm) var(--r-sm) 0 0;font-size:13.5px}:host(.ef-tabs--underline) .ef-tabs__tab:hover:not(.is-active):not(.is-disabled){color:var(--text);background:var(--paper)}:host(.ef-tabs--underline) .ef-tabs__tab:focus-visible{background:var(--paper-alt);box-shadow:inset 0 0 0 2px var(--tenant-200)}:host(.ef-tabs--underline) .ef-tabs__tab.is-active{color:var(--text);border-bottom-color:var(--module, var(--ink-active))}:host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__count{background:color-mix(in srgb,var(--module, var(--ink-active)) 14%,var(--paper-alt));border-color:transparent;color:var(--module, var(--ink-active))}:host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__dot{background:var(--module, var(--tenant-500))}.ef-tabs__tab.is-danger,:host(.ef-tabs--pill) .ef-tabs__tab.is-danger:hover:not(.is-active):not(.is-disabled),:host(.ef-tabs--underline) .ef-tabs__tab.is-danger:hover:not(.is-active):not(.is-disabled){color:var(--st-cancelled-fg)}:host(.ef-tabs--pill) .ef-tabs__tab.is-danger.is-active{color:var(--paper);box-shadow:inset 0 0 0 2px var(--st-cancelled-fg)}:host(.ef-tabs--underline) .ef-tabs__tab.is-danger.is-active{color:var(--st-cancelled-fg);border-bottom-color:var(--st-cancelled-fg)}:host(.ef-tabs--pill) .ef-tabs__tab.is-danger.is-active:focus-visible{box-shadow:inset 0 0 0 2px var(--st-cancelled-fg),0 0 0 2px color-mix(in srgb,var(--ink-active, #0f1115) 40%,transparent)}:host[data-module=sales]{--module: var(--m-sales)}:host[data-module=purchase]{--module: var(--m-purchase)}:host[data-module=stock]{--module: var(--m-stock)}:host[data-module=pos]{--module: var(--m-pos)}:host[data-module=marketing]{--module: var(--m-marketing)}:host[data-module=store]{--module: var(--m-store)}:host[data-module=finance]{--module: var(--m-finance)}:host[data-module=admin]{--module: var(--m-admin)}:host(.ef-tabs--overflow){position:relative;display:block}:host(.ef-tabs--overflow) .ef-tabs__list{display:flex;width:100%}:host(.ef-tabs--overflow):before,:host(.ef-tabs--overflow):after{content:\"\";position:absolute;top:0;bottom:0;width:36px;pointer-events:none;z-index:2}:host(.ef-tabs--overflow):before{inset-inline-start:0;background:linear-gradient(to right,var(--paper-alt),transparent)}:host(.ef-tabs--overflow):after{inset-inline-end:0;background:linear-gradient(to left,var(--paper-alt),transparent)}[data-theme=dark] :host(.ef-tabs--underline) .ef-tabs__tab.is-active .ef-tabs__count{background:color-mix(in srgb,var(--module, var(--ink-active)) 28%,transparent)}@media(pointer:coarse){:host(.ef-tabs--pill) .ef-tabs__tab,:host(.ef-tabs--underline) .ef-tabs__tab{min-height:var(--hit-touch)}}\n"] }]
9992
10109
  }], propDecorators: { items: [{
9993
10110
  type: Input,
9994
10111
  args: [{ required: true }]
@@ -11644,6 +11761,10 @@ class EfInputTextComponent extends AbstractEfFormControl {
11644
11761
  * follow the value's first strong character. The label and messages keep
11645
11762
  * the page direction. Null or absent leaves the input unchanged. */
11646
11763
  dir = null;
11764
+ /** Language of the typed value (`'ar'` for an Arabic title on a French
11765
+ * screen), applied to the native input only: the label and hint are page
11766
+ * text and keep the page language. Null or absent renders no attribute. */
11767
+ lang = null;
11647
11768
  /** Clearable (showClear / canClear) is inherited from AbstractEfFormControl;
11648
11769
  * we only supply the value predicate + the reset. */
11649
11770
  get hasValue() {
@@ -11755,11 +11876,11 @@ class EfInputTextComponent extends AbstractEfFormControl {
11755
11876
  this.onTouched();
11756
11877
  }
11757
11878
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfInputTextComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11758
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfInputTextComponent, isStandalone: true, selector: "ef-input-text", inputs: { size: "size", type: "type", maxlength: "maxlength", fluid: ["fluid", "fluid", booleanAttribute], inline: ["inline", "inline", booleanAttribute], autocomplete: "autocomplete", dir: "dir", variant: "variant", prefix: "prefix", suffix: "suffix", value: "value" }, outputs: { valueChangeEvent: "valueChangeEvent" }, host: { listeners: { "focusin": "markFocused($event)" }, properties: { "class.ef-inline": "this.isInline" } }, usesInheritance: true, ngImport: i0, template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n @if (variant === 'comptoir') {\n @if (prefix || suffix) {\n <!-- Comptoir variant with prefix/suffix \u2014 wraps the input in\n an .ef-input-group so the addons share the border + focus\n ring with the input. -->\n <div class=\"ef-input-group\" [class.is-invalid]=\"isInvalid\">\n @if (prefix) {\n <span class=\"ef-input-addon is-prefix\" aria-hidden=\"true\">{{ prefix }}</span>\n }\n <input\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [type]=\"type\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n />\n @if (canClear) {\n <ef-clear-button\n class=\"ef-group-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n @if (suffix) {\n <span class=\"ef-input-addon is-suffix\" aria-hidden=\"true\">{{ suffix }}</span>\n }\n </div>\n } @else {\n <!-- Comptoir variant \u2014 bare native input (+ optional inline clear). -->\n <div class=\"ef-input-wrap\">\n <input\n class=\"ef-input\"\n [class.has-clear]=\"canClear\"\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [type]=\"type\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [class.is-invalid]=\"isInvalid\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n />\n @if (canClear) {\n <ef-clear-button\n class=\"ef-input-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n </div>\n }\n } @else {\n <!-- PrimeNG variant (default) \u2014 v1 styling. -->\n <input\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [type]=\"type\"\n pInputText\n [pSize]=\"size\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [class.ng-invalid]=\"isInvalid\"\n [class.ng-touched]=\"ngControl?.touched\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n fluid\n />\n }\n @if (effectiveHint) {\n <small class=\"ef-field-hint\" [id]=\"hintId\">{{ effectiveHint }}</small>\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}:host{display:block}:host(.ef-inline){display:flex;align-items:baseline;gap:.5rem}:host(.ef-inline)>label{white-space:nowrap;flex-shrink:0}:host(.ef-inline)>.ef-field-control{flex:1;min-width:0}\n"], dependencies: [{ kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i9.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "labelParams", "for", "styleClass", "required"] }, { kind: "component", type: EfClearButtonComponent, selector: "ef-clear-button", inputs: ["ariaLabel", "disabled"], outputs: ["clear"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }] });
11879
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfInputTextComponent, isStandalone: true, selector: "ef-input-text", inputs: { size: "size", type: "type", maxlength: "maxlength", fluid: ["fluid", "fluid", booleanAttribute], inline: ["inline", "inline", booleanAttribute], autocomplete: "autocomplete", dir: "dir", lang: "lang", variant: "variant", prefix: "prefix", suffix: "suffix", value: "value" }, outputs: { valueChangeEvent: "valueChangeEvent" }, host: { listeners: { "focusin": "markFocused($event)" }, properties: { "attr.lang": "null", "class.ef-inline": "this.isInline" } }, usesInheritance: true, ngImport: i0, template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n @if (variant === 'comptoir') {\n @if (prefix || suffix) {\n <!-- Comptoir variant with prefix/suffix \u2014 wraps the input in\n an .ef-input-group so the addons share the border + focus\n ring with the input. -->\n <div class=\"ef-input-group\" [class.is-invalid]=\"isInvalid\">\n @if (prefix) {\n <span class=\"ef-input-addon is-prefix\" aria-hidden=\"true\">{{ prefix }}</span>\n }\n <input\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [attr.lang]=\"lang || null\"\n [type]=\"type\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n />\n @if (canClear) {\n <ef-clear-button\n class=\"ef-group-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n @if (suffix) {\n <span class=\"ef-input-addon is-suffix\" aria-hidden=\"true\">{{ suffix }}</span>\n }\n </div>\n } @else {\n <!-- Comptoir variant \u2014 bare native input (+ optional inline clear). -->\n <div class=\"ef-input-wrap\">\n <input\n class=\"ef-input\"\n [class.has-clear]=\"canClear\"\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [attr.lang]=\"lang || null\"\n [type]=\"type\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [class.is-invalid]=\"isInvalid\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n />\n @if (canClear) {\n <ef-clear-button\n class=\"ef-input-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n </div>\n }\n } @else {\n <!-- PrimeNG variant (default) \u2014 v1 styling. -->\n <input\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [attr.lang]=\"lang || null\"\n [type]=\"type\"\n pInputText\n [pSize]=\"size\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [class.ng-invalid]=\"isInvalid\"\n [class.ng-touched]=\"ngControl?.touched\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n fluid\n />\n }\n @if (effectiveHint) {\n <small class=\"ef-field-hint\" [id]=\"hintId\">{{ effectiveHint }}</small>\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}:host{display:block}:host(.ef-inline){display:flex;align-items:baseline;gap:.5rem}:host(.ef-inline)>label{white-space:nowrap;flex-shrink:0}:host(.ef-inline)>.ef-field-control{flex:1;min-width:0}\n"], dependencies: [{ kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i9.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "labelParams", "for", "styleClass", "required"] }, { kind: "component", type: EfClearButtonComponent, selector: "ef-clear-button", inputs: ["ariaLabel", "disabled"], outputs: ["clear"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }] });
11759
11880
  }
11760
11881
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfInputTextComponent, decorators: [{
11761
11882
  type: Component,
11762
- args: [{ selector: 'ef-input-text', standalone: true, imports: [InputTextModule, FormsModule, TranslateModule, EfLabelComponent, EfClearButtonComponent], template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n @if (variant === 'comptoir') {\n @if (prefix || suffix) {\n <!-- Comptoir variant with prefix/suffix \u2014 wraps the input in\n an .ef-input-group so the addons share the border + focus\n ring with the input. -->\n <div class=\"ef-input-group\" [class.is-invalid]=\"isInvalid\">\n @if (prefix) {\n <span class=\"ef-input-addon is-prefix\" aria-hidden=\"true\">{{ prefix }}</span>\n }\n <input\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [type]=\"type\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n />\n @if (canClear) {\n <ef-clear-button\n class=\"ef-group-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n @if (suffix) {\n <span class=\"ef-input-addon is-suffix\" aria-hidden=\"true\">{{ suffix }}</span>\n }\n </div>\n } @else {\n <!-- Comptoir variant \u2014 bare native input (+ optional inline clear). -->\n <div class=\"ef-input-wrap\">\n <input\n class=\"ef-input\"\n [class.has-clear]=\"canClear\"\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [type]=\"type\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [class.is-invalid]=\"isInvalid\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n />\n @if (canClear) {\n <ef-clear-button\n class=\"ef-input-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n </div>\n }\n } @else {\n <!-- PrimeNG variant (default) \u2014 v1 styling. -->\n <input\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [type]=\"type\"\n pInputText\n [pSize]=\"size\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [class.ng-invalid]=\"isInvalid\"\n [class.ng-touched]=\"ngControl?.touched\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n fluid\n />\n }\n @if (effectiveHint) {\n <small class=\"ef-field-hint\" [id]=\"hintId\">{{ effectiveHint }}</small>\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}:host{display:block}:host(.ef-inline){display:flex;align-items:baseline;gap:.5rem}:host(.ef-inline)>label{white-space:nowrap;flex-shrink:0}:host(.ef-inline)>.ef-field-control{flex:1;min-width:0}\n"] }]
11883
+ args: [{ selector: 'ef-input-text', standalone: true, imports: [InputTextModule, FormsModule, TranslateModule, EfLabelComponent, EfClearButtonComponent], host: { '[attr.lang]': 'null' }, template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n @if (variant === 'comptoir') {\n @if (prefix || suffix) {\n <!-- Comptoir variant with prefix/suffix \u2014 wraps the input in\n an .ef-input-group so the addons share the border + focus\n ring with the input. -->\n <div class=\"ef-input-group\" [class.is-invalid]=\"isInvalid\">\n @if (prefix) {\n <span class=\"ef-input-addon is-prefix\" aria-hidden=\"true\">{{ prefix }}</span>\n }\n <input\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [attr.lang]=\"lang || null\"\n [type]=\"type\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n />\n @if (canClear) {\n <ef-clear-button\n class=\"ef-group-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n @if (suffix) {\n <span class=\"ef-input-addon is-suffix\" aria-hidden=\"true\">{{ suffix }}</span>\n }\n </div>\n } @else {\n <!-- Comptoir variant \u2014 bare native input (+ optional inline clear). -->\n <div class=\"ef-input-wrap\">\n <input\n class=\"ef-input\"\n [class.has-clear]=\"canClear\"\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [attr.lang]=\"lang || null\"\n [type]=\"type\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [class.is-invalid]=\"isInvalid\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n />\n @if (canClear) {\n <ef-clear-button\n class=\"ef-input-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n </div>\n }\n } @else {\n <!-- PrimeNG variant (default) \u2014 v1 styling. -->\n <input\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.dir]=\"dir || null\"\n [attr.lang]=\"lang || null\"\n [type]=\"type\"\n pInputText\n [pSize]=\"size\"\n [value]=\"value\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [attr.autocomplete]=\"autocomplete ?? null\"\n [class.ng-invalid]=\"isInvalid\"\n [class.ng-touched]=\"ngControl?.touched\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n fluid\n />\n }\n @if (effectiveHint) {\n <small class=\"ef-field-hint\" [id]=\"hintId\">{{ effectiveHint }}</small>\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}:host{display:block}:host(.ef-inline){display:flex;align-items:baseline;gap:.5rem}:host(.ef-inline)>label{white-space:nowrap;flex-shrink:0}:host(.ef-inline)>.ef-field-control{flex:1;min-width:0}\n"] }]
11763
11884
  }], ctorParameters: () => [], propDecorators: { size: [{
11764
11885
  type: Input
11765
11886
  }], type: [{
@@ -11776,6 +11897,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
11776
11897
  type: Input
11777
11898
  }], dir: [{
11778
11899
  type: Input
11900
+ }], lang: [{
11901
+ type: Input
11779
11902
  }], variant: [{
11780
11903
  type: Input
11781
11904
  }], prefix: [{
@@ -12349,6 +12472,19 @@ class EfMultiSelectComponent extends AbstractEfFormControl {
12349
12472
  selectionLimit;
12350
12473
  filters;
12351
12474
  inline = false;
12475
+ /**
12476
+ * Rendering variant, as on `ef-select`.
12477
+ * - `'primeng'` (default): PrimeNG's own multiselect skin, unchanged.
12478
+ * - `'comptoir'`: the shared `.es` skin with `.es--comptoir`, the same hook
12479
+ * `ef-select variant="comptoir"` uses: a trigger on
12480
+ * `--hit-field` (`--hit-base` 40px, `--hit-touch` 48px on a
12481
+ * coarse pointer) and no minimum width, so a select and a
12482
+ * multi-select in one row align and both shrink with it.
12483
+ */
12484
+ variant = 'primeng';
12485
+ get effectiveStyleClass() {
12486
+ return this.variant === 'comptoir' ? 'es es--comptoir' : undefined;
12487
+ }
12352
12488
  get isInline() { return this.inline; }
12353
12489
  get isRequired() { return this.required(); }
12354
12490
  selectedValues = [];
@@ -12413,11 +12549,11 @@ class EfMultiSelectComponent extends AbstractEfFormControl {
12413
12549
  this.selectedValues = [];
12414
12550
  }
12415
12551
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfMultiSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
12416
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfMultiSelectComponent, isStandalone: true, selector: "ef-multi-select", inputs: { optionLabel: "optionLabel", optionValue: "optionValue", options: "options", filteredOptions: "filteredOptions", maxSelectedLabels: "maxSelectedLabels", display: "display", size: "size", selectionLimit: "selectionLimit", filters: "filters", inline: ["inline", "inline", booleanAttribute] }, outputs: { changeEvent: "changeEvent" }, host: { properties: { "class.ef-inline": "this.isInline", "class.ef-required": "this.isRequired" } }, usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n <p-multiSelect\n [id]=\"effectiveId\"\n [name]=\"name()\"\n [placeholder]=\"effectivePlaceholder\"\n [options]=\"filteredOptions\"\n [optionLabel]=\"optionLabel\"\n [optionValue]=\"optionValue\"\n [maxSelectedLabels]=\"maxSelectedLabels\"\n [ngModel]=\"selectedValues\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"onInternalModelChange($event)\"\n (onClear)=\"handleClear()\"\n [disabled]=\"isDisabled()\"\n [display]=\"display\"\n [size]=\"size\"\n [selectionLimit]=\"selectionLimit\"\n [showClear]=\"showClear()\"\n fluid\n />\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}:host{display:block}:host:not(.ef-required) ::ng-deep .p-multiselect.ng-invalid.ng-dirty{border-color:var(--p-multiselect-border-color, #d1d5db)}:host(.ef-inline){display:flex;align-items:baseline;gap:.5rem}:host(.ef-inline)>label{white-space:nowrap;flex-shrink:0}:host(.ef-inline)>.ef-field-control{flex:1;min-width:0}\n"], dependencies: [{ 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: MultiSelectModule }, { kind: "component", type: i2.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "styleClass", "panelStyle", "panelStyleClass", "inputId", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "dataKey", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "placeholder", "options", "filterValue", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect", "size", "variant", "fluid", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "labelParams", "for", "styleClass", "required"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }] });
12552
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfMultiSelectComponent, isStandalone: true, selector: "ef-multi-select", inputs: { optionLabel: "optionLabel", optionValue: "optionValue", options: "options", filteredOptions: "filteredOptions", maxSelectedLabels: "maxSelectedLabels", display: "display", size: "size", selectionLimit: "selectionLimit", filters: "filters", inline: ["inline", "inline", booleanAttribute], variant: "variant" }, outputs: { changeEvent: "changeEvent" }, host: { properties: { "class.ef-inline": "this.isInline", "class.ef-required": "this.isRequired" } }, usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n <p-multiSelect\n [id]=\"effectiveId\"\n [name]=\"name()\"\n [placeholder]=\"effectivePlaceholder\"\n [options]=\"filteredOptions\"\n [optionLabel]=\"optionLabel\"\n [optionValue]=\"optionValue\"\n [maxSelectedLabels]=\"maxSelectedLabels\"\n [ngModel]=\"selectedValues\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"onInternalModelChange($event)\"\n (onClear)=\"handleClear()\"\n [disabled]=\"isDisabled()\"\n [display]=\"display\"\n [size]=\"size\"\n [selectionLimit]=\"selectionLimit\"\n [showClear]=\"showClear()\"\n [styleClass]=\"effectiveStyleClass\"\n fluid\n />\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}:host{display:block}:host:not(.ef-required) ::ng-deep .p-multiselect.ng-invalid.ng-dirty{border-color:var(--p-multiselect-border-color, #d1d5db)}:host(.ef-inline){display:flex;align-items:baseline;gap:.5rem}:host(.ef-inline)>label{white-space:nowrap;flex-shrink:0}:host(.ef-inline)>.ef-field-control{flex:1;min-width:0}\n"], dependencies: [{ 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: MultiSelectModule }, { kind: "component", type: i2.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "styleClass", "panelStyle", "panelStyleClass", "inputId", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "dataKey", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "placeholder", "options", "filterValue", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect", "size", "variant", "fluid", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "labelParams", "for", "styleClass", "required"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }] });
12417
12553
  }
12418
12554
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfMultiSelectComponent, decorators: [{
12419
12555
  type: Component,
12420
- args: [{ selector: 'ef-multi-select', standalone: true, imports: [FormsModule, MultiSelectModule, TranslateModule, EfLabelComponent], template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n <p-multiSelect\n [id]=\"effectiveId\"\n [name]=\"name()\"\n [placeholder]=\"effectivePlaceholder\"\n [options]=\"filteredOptions\"\n [optionLabel]=\"optionLabel\"\n [optionValue]=\"optionValue\"\n [maxSelectedLabels]=\"maxSelectedLabels\"\n [ngModel]=\"selectedValues\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"onInternalModelChange($event)\"\n (onClear)=\"handleClear()\"\n [disabled]=\"isDisabled()\"\n [display]=\"display\"\n [size]=\"size\"\n [selectionLimit]=\"selectionLimit\"\n [showClear]=\"showClear()\"\n fluid\n />\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}:host{display:block}:host:not(.ef-required) ::ng-deep .p-multiselect.ng-invalid.ng-dirty{border-color:var(--p-multiselect-border-color, #d1d5db)}:host(.ef-inline){display:flex;align-items:baseline;gap:.5rem}:host(.ef-inline)>label{white-space:nowrap;flex-shrink:0}:host(.ef-inline)>.ef-field-control{flex:1;min-width:0}\n"] }]
12556
+ args: [{ selector: 'ef-multi-select', standalone: true, imports: [FormsModule, MultiSelectModule, TranslateModule, EfLabelComponent], template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n <p-multiSelect\n [id]=\"effectiveId\"\n [name]=\"name()\"\n [placeholder]=\"effectivePlaceholder\"\n [options]=\"filteredOptions\"\n [optionLabel]=\"optionLabel\"\n [optionValue]=\"optionValue\"\n [maxSelectedLabels]=\"maxSelectedLabels\"\n [ngModel]=\"selectedValues\"\n [ngModelOptions]=\"{standalone: true}\"\n (ngModelChange)=\"onInternalModelChange($event)\"\n (onClear)=\"handleClear()\"\n [disabled]=\"isDisabled()\"\n [display]=\"display\"\n [size]=\"size\"\n [selectionLimit]=\"selectionLimit\"\n [showClear]=\"showClear()\"\n [styleClass]=\"effectiveStyleClass\"\n fluid\n />\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}:host{display:block}:host:not(.ef-required) ::ng-deep .p-multiselect.ng-invalid.ng-dirty{border-color:var(--p-multiselect-border-color, #d1d5db)}:host(.ef-inline){display:flex;align-items:baseline;gap:.5rem}:host(.ef-inline)>label{white-space:nowrap;flex-shrink:0}:host(.ef-inline)>.ef-field-control{flex:1;min-width:0}\n"] }]
12421
12557
  }], ctorParameters: () => [], propDecorators: { changeEvent: [{
12422
12558
  type: Output
12423
12559
  }], optionLabel: [{
@@ -12441,6 +12577,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
12441
12577
  }], inline: [{
12442
12578
  type: Input,
12443
12579
  args: [{ transform: booleanAttribute }]
12580
+ }], variant: [{
12581
+ type: Input
12444
12582
  }], isInline: [{
12445
12583
  type: HostBinding,
12446
12584
  args: ['class.ef-inline']
@@ -13330,6 +13468,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
13330
13468
  class EfTextareaComponent extends AbstractEfFormControl {
13331
13469
  rows = 4;
13332
13470
  maxlength;
13471
+ /** Language of the typed value, applied to the native textarea only (the
13472
+ * label and hint keep the page language). Null or absent renders none. */
13473
+ lang = null;
13333
13474
  autofocus = false;
13334
13475
  /** Monospace face for source text (an HTML body, a JSON snippet), where
13335
13476
  * tags and indentation must stay readable. */
@@ -13430,15 +13571,17 @@ class EfTextareaComponent extends AbstractEfFormControl {
13430
13571
  this.onTouched();
13431
13572
  }
13432
13573
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfTextareaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13433
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfTextareaComponent, isStandalone: true, selector: "ef-textarea", inputs: { rows: "rows", maxlength: "maxlength", autofocus: ["autofocus", "autofocus", booleanAttribute], mono: ["mono", "mono", booleanAttribute], value: "value" }, outputs: { valueChangeEvent: "valueChangeEvent" }, host: { listeners: { "focusin": "markFocused($event)" } }, usesInheritance: true, ngImport: i0, template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n <div class=\"ef-textarea-wrap\">\n <textarea\n class=\"ef-textarea\"\n [class.has-clear]=\"canClear\"\n [class.ef-textarea--mono]=\"mono\"\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [rows]=\"rows\"\n [value]=\"value\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [class.is-invalid]=\"isInvalid\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n ></textarea>\n @if (canClear) {\n <ef-clear-button\n class=\"ef-textarea-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n </div>\n @if (effectiveHint) {\n <small class=\"ef-field-hint\" [id]=\"hintId\">{{ effectiveHint }}</small>\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}.ef-textarea.ef-textarea--mono{font-family:var(--font-mono);font-size:12.5px;line-height:1.55}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "labelParams", "for", "styleClass", "required"] }, { kind: "component", type: EfClearButtonComponent, selector: "ef-clear-button", inputs: ["ariaLabel", "disabled"], outputs: ["clear"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }] });
13574
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfTextareaComponent, isStandalone: true, selector: "ef-textarea", inputs: { rows: "rows", maxlength: "maxlength", lang: "lang", autofocus: ["autofocus", "autofocus", booleanAttribute], mono: ["mono", "mono", booleanAttribute], value: "value" }, outputs: { valueChangeEvent: "valueChangeEvent" }, host: { listeners: { "focusin": "markFocused($event)" }, properties: { "attr.lang": "null" } }, usesInheritance: true, ngImport: i0, template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n <div class=\"ef-textarea-wrap\">\n <textarea\n class=\"ef-textarea\"\n [class.has-clear]=\"canClear\"\n [class.ef-textarea--mono]=\"mono\"\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.lang]=\"lang || null\"\n [rows]=\"rows\"\n [value]=\"value\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [class.is-invalid]=\"isInvalid\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n ></textarea>\n @if (canClear) {\n <ef-clear-button\n class=\"ef-textarea-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n </div>\n @if (effectiveHint) {\n <small class=\"ef-field-hint\" [id]=\"hintId\">{{ effectiveHint }}</small>\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}.ef-textarea.ef-textarea--mono{font-family:var(--font-mono);font-size:12.5px;line-height:1.55}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "labelParams", "for", "styleClass", "required"] }, { kind: "component", type: EfClearButtonComponent, selector: "ef-clear-button", inputs: ["ariaLabel", "disabled"], outputs: ["clear"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }] });
13434
13575
  }
13435
13576
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfTextareaComponent, decorators: [{
13436
13577
  type: Component,
13437
- args: [{ selector: 'ef-textarea', standalone: true, imports: [FormsModule, TranslateModule, EfLabelComponent, EfClearButtonComponent], template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n <div class=\"ef-textarea-wrap\">\n <textarea\n class=\"ef-textarea\"\n [class.has-clear]=\"canClear\"\n [class.ef-textarea--mono]=\"mono\"\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [rows]=\"rows\"\n [value]=\"value\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [class.is-invalid]=\"isInvalid\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n ></textarea>\n @if (canClear) {\n <ef-clear-button\n class=\"ef-textarea-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n </div>\n @if (effectiveHint) {\n <small class=\"ef-field-hint\" [id]=\"hintId\">{{ effectiveHint }}</small>\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}.ef-textarea.ef-textarea--mono{font-family:var(--font-mono);font-size:12.5px;line-height:1.55}\n"] }]
13578
+ args: [{ selector: 'ef-textarea', standalone: true, imports: [FormsModule, TranslateModule, EfLabelComponent, EfClearButtonComponent], host: { '[attr.lang]': 'null' }, template: "@if (labelKey() || label()) {\n <ef-label [labelKey]=\"labelKey()\" [labelParams]=\"labelParams()\" [label]=\"label()\" [for]=\"effectiveId\" [required]=\"required()\" />\n}\n<div class=\"ef-field-control\">\n <div class=\"ef-textarea-wrap\">\n <textarea\n class=\"ef-textarea\"\n [class.has-clear]=\"canClear\"\n [class.ef-textarea--mono]=\"mono\"\n [id]=\"effectiveId\"\n [attr.name]=\"name() || null\"\n [attr.lang]=\"lang || null\"\n [rows]=\"rows\"\n [value]=\"value\"\n [attr.placeholder]=\"effectivePlaceholder || null\"\n [attr.maxlength]=\"maxlength ?? null\"\n [readonly]=\"readonly()\"\n [disabled]=\"isDisabled()\"\n [class.is-invalid]=\"isInvalid\"\n [attr.aria-label]=\"effectiveAriaLabel\"\n [attr.aria-required]=\"required() ? 'true' : null\"\n [attr.aria-invalid]=\"isInvalid ? 'true' : null\"\n [attr.aria-describedby]=\"describedBy(showRequired || !!serverErrors)\"\n (input)=\"handleInput($any($event.target).value)\"\n (blur)=\"handleBlur()\"\n ></textarea>\n @if (canClear) {\n <ef-clear-button\n class=\"ef-textarea-clear\"\n [ariaLabel]=\"'action.clear' | translate\"\n (clear)=\"clearValue()\"\n />\n }\n </div>\n @if (effectiveHint) {\n <small class=\"ef-field-hint\" [id]=\"hintId\">{{ effectiveHint }}</small>\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", styles: [":host([dir]){direction:inherit}:host([dir=rtl]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:rtl}:host([dir=ltr]) .ef-field-control>:not([role=alert]):not(.ef-field-hint){direction:ltr}:host([dir=auto]) .ef-field-control input,:host([dir=auto]) .ef-field-control textarea{unicode-bidi:plaintext}.ef-textarea.ef-textarea--mono{font-family:var(--font-mono);font-size:12.5px;line-height:1.55}\n"] }]
13438
13579
  }], ctorParameters: () => [], propDecorators: { rows: [{
13439
13580
  type: Input
13440
13581
  }], maxlength: [{
13441
13582
  type: Input
13583
+ }], lang: [{
13584
+ type: Input
13442
13585
  }], autofocus: [{
13443
13586
  type: Input,
13444
13587
  args: [{ transform: booleanAttribute }]
@@ -13454,82 +13597,699 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
13454
13597
  args: ['focusin', ['$event']]
13455
13598
  }] } });
13456
13599
 
13600
+ /**
13601
+ * Loads the rich text editor component that `ef-localized-input`'s `rich`
13602
+ * mode renders. The editor lives in the `@elasticias/ui/rich-text` entry
13603
+ * point so an app that never edits rich text neither bundles PrimeNG's Editor
13604
+ * nor needs `quill` installed. Provide it with `provideEfRichText()` from that
13605
+ * entry (the editor code is in the app's main bundle, Quill itself still loads
13606
+ * lazily the first time an editor renders), or with your own loader to defer
13607
+ * the editor code too:
13608
+ *
13609
+ * ```ts
13610
+ * { provide: EF_RICH_TEXT, useValue: () => import('@elasticias/ui/rich-text').then(m => m.EfRichTextComponent) }
13611
+ * ```
13612
+ */
13613
+ const EF_RICH_TEXT = new InjectionToken('EF_RICH_TEXT');
13614
+ /** Length of the text a reader sees in an HTML fragment, tags not counted. Browser only: it parses with `document`. */
13615
+ function plainTextLength(html) {
13616
+ if (!html)
13617
+ return 0;
13618
+ const tpl = document.createElement('template');
13619
+ tpl.innerHTML = html;
13620
+ return (tpl.content.textContent ?? '').length;
13621
+ }
13622
+
13457
13623
  let nextId = 0;
13458
- const EF_SEO_DEFAULT_LOCALES = [
13459
- { code: 'fr', labelKey: 'ef_seo_fields_locale_fr', dir: 'auto' },
13460
- { code: 'ar', labelKey: 'ef_seo_fields_locale_ar', dir: 'rtl' },
13461
- { code: 'en', labelKey: 'ef_seo_fields_locale_en', dir: 'ltr' },
13462
- ];
13624
+ /** Same texts once blanks are dropped. */
13625
+ function sameText(a, b) {
13626
+ const ka = Object.keys(a);
13627
+ return ka.length === Object.keys(b).length && ka.every(k => a[k] === b[k]);
13628
+ }
13463
13629
  /**
13464
- * SEO title and description per language, each with a counter against what a
13465
- * search engine shows (60 / 160), capped at the server's limits (256 / 512),
13466
- * and the shared `ef-seo-preview` of the French result with the entity's own
13467
- * title and description as the fallback the shop uses. Card-less: each screen
13468
- * wraps it in its own `ef-card`.
13630
+ * One owner-written text (a title, a description, an HTML body) in every
13631
+ * content language: a pill tab per culture, the active culture's field below
13632
+ * it, and the whole object out on every edit with blank cultures removed
13633
+ * (ADR-050 section 7).
13634
+ *
13635
+ * - **Cultures** come from `EF_CONTENT_CULTURES` (the ERP provides them from
13636
+ * the tenant profile) unless `cultures` is set. The active culture starts
13637
+ * on the default and is two-way through `[(activeCulture)]`.
13638
+ * - **One tab row for several texts** (an email's subject and body): bind the
13639
+ * same signal to `[(activeCulture)]` on each and set `[showTabs]="false"`
13640
+ * on all but one, and list those in the row owner's `[linkedInputs]` so
13641
+ * the row also shows their error tone, required tone and dirty dot. A new
13642
+ * set of server errors moves the shared culture to the first culture in
13643
+ * error in the row's order.
13644
+ * - **Tabs** show the language name only (no "missing" note: an empty culture
13645
+ * is visible from its empty field), the error tone on the default culture of a `required` field left
13646
+ * empty once the field is touched, changed or its form submitted, and the
13647
+ * dirty dot on cultures edited since the value arrived. Dirty detection
13648
+ * relies on the parent storing the emitted object by reference (the V2
13649
+ * signal pattern): a parent that clones the value resets the dots on every
13650
+ * edit, though it never loses text.
13651
+ * - **Direction.** Each field gets its culture's `dir` and `lang`. In `html`
13652
+ * mode the source stays `ltr` for every culture (raw HTML in an `rtl`
13653
+ * textarea reads reversed) and the sanitised preview takes the culture's
13654
+ * direction.
13655
+ * - **Rich mode** (`mode="rich"`) edits each culture with `ef-rich-text`, the
13656
+ * shared editor in `@elasticias/ui/rich-text`. The app turns it on once with
13657
+ * `provideEfRichText()` (and installs `quill`); without it the field falls
13658
+ * back to the `html` source and logs why in development, as it does when
13659
+ * the editor's chunk fails to load. With `advisedLength` the counter
13660
+ * counts the text a reader sees; with only `maxLength` it counts the
13661
+ * stored HTML, which is what the server measures. Neither caps typing in
13662
+ * this mode.
13663
+ * - **Read-only cultures** are shown read-only (still focusable, so the hint
13664
+ * is read and the text can be copied) with
13665
+ * `ef_localized_input_read_only_hint` and the projected `[read-only-link]`;
13666
+ * their text still travels in the emitted object. `[efReadOnly]`, `[disabled]`
13667
+ * and a form's `disable()` disable every culture; the tabs stay usable so the
13668
+ * other languages can still be read.
13669
+ * - **Forms.** A ControlValueAccessor (`[formControl]`, `formControlName`,
13670
+ * `[(ngModel)]`) as well as `[value]` + `(valueChangeEvent)`. With
13671
+ * `required`, the form control reports `required` while the default culture
13672
+ * is empty, and once the field is touched, changed or submitted the default
13673
+ * culture's field shows `validation.required` and is `aria-invalid`.
13674
+ * - **Server errors** (ADR-010) come from the enclosing `[efServerErrors]`
13675
+ * scope. `<name>.<culture>` errors tint that culture's tab, show under its
13676
+ * field while it is active (the field links itself by name) and, from another
13677
+ * tab, show as a hint naming the languages in error; a new set of errors
13678
+ * moves to the first of them. `<name>` errors, or an explicit `[errors]`,
13679
+ * show under the field on every tab, as do errors for a language the field
13680
+ * does not list (`title.xx`), prefixed with that language's name (or its
13681
+ * code) through `ef_localized_input_error_in_culture`. Messages are
13682
+ * translated as keys. Keys: `ef_localized_input_errors_in_other`,
13683
+ * `ef_localized_input_error_in_culture`.
13684
+ * - **Counter.** With `advisedLength` it reads as advice through
13685
+ * `ef_localized_input_counter_advised` (`{{count}}`, `{{max}}`), so 5123 of
13686
+ * 5000 does not look like a cap; with `maxLength` alone it stays `n / max`.
13687
+ * - **Spoken limit.** `counterHintKey` is read with the counter (through
13688
+ * `aria-describedby`) and `counterOverKey` is announced politely when the
13689
+ * text passes the limit; both take `{{max}}`. On a field with several
13690
+ * cultures the announcement is prefixed with the culture's name through
13691
+ * `ef_localized_input_error_in_culture`, so `counterOverKey` should not
13692
+ * name it.
13693
+ * - **Names.** Each culture's field is named `<name>.<culture>` (`title.ar`),
13694
+ * the key the backend reports a per-culture error under.
13695
+ *
13696
+ * i18n keys the app's bundles must define (the libs ship no JSON):
13697
+ *
13698
+ * | key | fr | ar | en |
13699
+ * |---|---|---|---|
13700
+ * | `ef_localized_input_culture_fr` | Français | الفرنسية | French |
13701
+ * | `ef_localized_input_culture_ar` | Arabe | العربية | Arabic |
13702
+ * | `ef_localized_input_culture_en` | Anglais | الإنجليزية | English |
13703
+ * | `ef_localized_input_cultures` | Langues du texte | لغات النص | Text languages |
13704
+ * | `ef_localized_input_errors_in_other` | Erreur dans : | خطأ في: | Error in: |
13705
+ * | `ef_localized_input_counter_advised` | {{count}} / {{max}} conseillés | {{count}} / {{max}} موصى بها | {{count}} / {{max}} advised |
13706
+ * | `ef_localized_input_error_in_culture` | {{culture}} : {{message}} | {{culture}}: {{message}} | {{culture}}: {{message}} |
13707
+ * | `ef_localized_input_read_only_hint` | Ce texte se modifie ailleurs. | يُعدَّل هذا النص في مكان آخر. | This text is edited elsewhere. |
13708
+ *
13709
+ * ```html
13710
+ * <ef-localized-input name="title" labelKey="product_title" required
13711
+ * [value]="product().title" (valueChangeEvent)="patch({ title: $event })"
13712
+ * [advisedLength]="60" [maxLength]="256" />
13469
13713
  *
13470
- * The French controls carry the backend names (`seoTitle`, `seoDescription`)
13471
- * so `[efServerErrors]` links their errors; the others (`seoTitleAr`, ...) are
13472
- * dictionary entries whose errors are toasted (ADR-010).
13714
+ * <ef-localized-input name="subject" labelKey="email_subject" [maxLength]="200" [linkedInputs]="[body]" [(activeCulture)]="culture" ... />
13715
+ * <ef-localized-input #body name="htmlBody" labelKey="email_body" mode="html" [showTabs]="false" [(activeCulture)]="culture" ... />
13716
+ *
13717
+ * <ef-localized-input name="title" [readOnlyCultures]="['fr']" ...>
13718
+ * <a read-only-link [routerLink]="catalogLink()">{{ 'store_open_catalog' | translate }}</a>
13719
+ * </ef-localized-input>
13720
+ * ```
13721
+ */
13722
+ class EfLocalizedInputComponent {
13723
+ /** The camelCase backend property (`title`); each field is `<name>.<culture>`. */
13724
+ name = input.required(...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
13725
+ value = input(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
13726
+ mode = input('text', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
13727
+ rows = input(4, ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
13728
+ /** Hard cap, the server's limit. */
13729
+ maxLength = input(undefined, ...(ngDevMode ? [{ debugName: "maxLength" }] : /* istanbul ignore next */ []));
13730
+ /** What the counter measures against (what a search engine shows, say). */
13731
+ advisedLength = input(undefined, ...(ngDevMode ? [{ debugName: "advisedLength" }] : /* istanbul ignore next */ []));
13732
+ /** The default culture must be filled. */
13733
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13734
+ /** Cultures shown but not editable here, with the `[read-only-link]` slot. */
13735
+ readOnlyCultures = input([], ...(ngDevMode ? [{ debugName: "readOnlyCultures" }] : /* istanbul ignore next */ []));
13736
+ /** Labels the field and names its tablist; without it the tablist is
13737
+ * named `ef_localized_input_cultures`. */
13738
+ labelKey = input('', ...(ngDevMode ? [{ debugName: "labelKey" }] : /* istanbul ignore next */ []));
13739
+ placeholderKey = input('', ...(ngDevMode ? [{ debugName: "placeholderKey" }] : /* istanbul ignore next */ []));
13740
+ hintKey = input('', ...(ngDevMode ? [{ debugName: "hintKey" }] : /* istanbul ignore next */ []));
13741
+ /** i18n key (with `{{max}}`) read out with the counter, for a limit that is advice rather than a cap. */
13742
+ counterHintKey = input('', ...(ngDevMode ? [{ debugName: "counterHintKey" }] : /* istanbul ignore next */ []));
13743
+ /** i18n key (with `{{max}}`) announced politely when the text goes past the limit. */
13744
+ counterOverKey = input('', ...(ngDevMode ? [{ debugName: "counterOverKey" }] : /* istanbul ignore next */ []));
13745
+ /** Field style in `text` mode only, named like ef-input-text's (`primeng`
13746
+ * or `comptoir`); the textarea, html and rich modes have one style.
13747
+ * `default` is the former name of `primeng`, kept for one release. */
13748
+ variant = input('comptoir', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
13749
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13750
+ /**
13751
+ * Messages for the whole field. Without it they come from the enclosing
13752
+ * `[efServerErrors]` scope under `name`; an explicit list wins. The
13753
+ * per-language messages (`<name>.<culture>`) always come from the scope.
13754
+ */
13755
+ errors = input(undefined, ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
13756
+ /** Overrides the `EF_CONTENT_CULTURES` list for this field. */
13757
+ cultures = input(undefined, ...(ngDevMode ? [{ debugName: "cultures" }] : /* istanbul ignore next */ []));
13758
+ /**
13759
+ * The culture on show, two-way (`[(activeCulture)]`). `null` or a culture
13760
+ * the field does not list means the default; the field then writes back
13761
+ * the culture it settled on, so the parent always reads a listed code.
13762
+ * Bind one signal to several inputs to give them one tab row.
13763
+ */
13764
+ activeCulture = model(null, ...(ngDevMode ? [{ debugName: "activeCulture" }] : /* istanbul ignore next */ []));
13765
+ /**
13766
+ * Draws the culture tab row. Off for a field that follows another
13767
+ * field's row through a shared `activeCulture`: its culture is then
13768
+ * named to assistive tech through `aria-describedby` and the hint still
13769
+ * names the cultures in error. List it in the row owner's
13770
+ * `linkedInputs` so the row draws its error tone, required tone and
13771
+ * dirty dot too. Without tabs it never rewrites `activeCulture` to fit
13772
+ * its own culture list (it shows its own fallback instead), and it moves
13773
+ * the shared culture to its first culture in error only when no row
13774
+ * owner links it.
13775
+ */
13776
+ showTabs = input(true, { ...(ngDevMode ? { debugName: "showTabs" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13777
+ /**
13778
+ * On the input that draws the shared tab row: the inputs that follow it
13779
+ * (`[showTabs]="false"`), whose `cultureFlags` the row merges into its
13780
+ * tones and dots. A new set of server errors on any of them moves the
13781
+ * shared culture to the first culture in error in this row's order.
13782
+ * Badges stay this input's own. A template reference works forward:
13783
+ * `<ef-localized-input [linkedInputs]="[body]" ... /> <ef-localized-input #body [showTabs]="false" ... />`.
13784
+ */
13785
+ linkedInputs = input([], ...(ngDevMode ? [{ debugName: "linkedInputs" }] : /* istanbul ignore next */ []));
13786
+ /** `html` mode only: the sanitised preview under the source. Off when
13787
+ * the screen renders its own preview (an email rendered by the server,
13788
+ * with its tokens filled in). */
13789
+ preview = input(true, { ...(ngDevMode ? { debugName: "preview" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
13790
+ /** A badge per culture tab, culture code to i18n key (`{ ar:
13791
+ * 'email_templates_customized' }`): a state of that language worth
13792
+ * seeing from the tab row. Drawn by the input that shows the tabs. */
13793
+ tabBadges = input({}, ...(ngDevMode ? [{ debugName: "tabBadges" }] : /* istanbul ignore next */ []));
13794
+ /** The whole new object, trimmed, with blank cultures removed. */
13795
+ valueChangeEvent = output();
13796
+ contentCultures = inject(EF_CONTENT_CULTURES);
13797
+ readOnlyScope = inject(EfReadOnlyDirective, { optional: true });
13798
+ ngControl = inject(NgControl, { self: true, optional: true });
13799
+ serverErrorsScope = inject(EfServerErrorsDirective, { optional: true });
13800
+ richLoader = inject(EF_RICH_TEXT, { optional: true });
13801
+ richType = signal(null, ...(ngDevMode ? [{ debugName: "richType" }] : /* istanbul ignore next */ []));
13802
+ /** The editor's chunk failed to load (offline, a stale deploy). */
13803
+ richFailed = signal(false, ...(ngDevMode ? [{ debugName: "richFailed" }] : /* istanbul ignore next */ []));
13804
+ richHost = viewChild('richHost', { ...(ngDevMode ? { debugName: "richHost" } : /* istanbul ignore next */ {}), read: ViewContainerRef });
13805
+ richRef = null;
13806
+ textField = viewChild(EfInputTextComponent, ...(ngDevMode ? [{ debugName: "textField" }] : /* istanbul ignore next */ []));
13807
+ areaField = viewChild(EfTextareaComponent, ...(ngDevMode ? [{ debugName: "areaField" }] : /* istanbul ignore next */ []));
13808
+ /** `rich` without a provided editor renders the html source instead. */
13809
+ effectiveMode = computed(() => this.mode() === 'rich' && (!this.richLoader || this.richFailed()) ? 'html' : this.mode(), ...(ngDevMode ? [{ debugName: "effectiveMode" }] : /* istanbul ignore next */ []));
13810
+ parentForm = inject(FormGroupDirective, { optional: true }) ?? inject(NgForm, { optional: true });
13811
+ host = inject(ElementRef);
13812
+ translateService = inject(TranslateService);
13813
+ /** The user typed in, or left, the field. */
13814
+ interacted = signal(false, ...(ngDevMode ? [{ debugName: "interacted" }] : /* istanbul ignore next */ []));
13815
+ /** The form control is touched or dirty, or its form was submitted; read on every check. */
13816
+ formTouched = signal(false, ...(ngDevMode ? [{ debugName: "formTouched" }] : /* istanbul ignore next */ []));
13817
+ /** Errors wait for touch, a change or a submit, like every ef-* control. */
13818
+ showErrors = computed(() => this.interacted() || this.formTouched(), ...(ngDevMode ? [{ debugName: "showErrors" }] : /* istanbul ignore next */ []));
13819
+ uid = `ef-localized-input-${nextId++}`;
13820
+ /** The object this component last emitted; an echo of it is not new data. */
13821
+ lastEmitted = null;
13822
+ /** The text as typed, blanks and spaces kept, so an echo never rewrites a field mid-word. */
13823
+ raw = linkedSignal({ ...(ngDevMode ? { debugName: "raw" } : /* istanbul ignore next */ {}), source: this.value,
13824
+ computation: (v, prev) => {
13825
+ if (prev && (v === this.lastEmitted || sameText(cleanLocalizedText(v), cleanLocalizedText(prev.value)))) {
13826
+ return prev.value;
13827
+ }
13828
+ return { ...(v ?? {}) };
13829
+ } });
13830
+ /** What the dirty dots compare against: the last value that came from outside. */
13831
+ baseline = linkedSignal({ ...(ngDevMode ? { debugName: "baseline" } : /* istanbul ignore next */ {}), source: this.value,
13832
+ computation: (v, prev) => (prev && v === this.lastEmitted ? prev.value : cleanLocalizedText(v)) });
13833
+ cleaned = computed(() => cleanLocalizedText(this.raw()), ...(ngDevMode ? [{ debugName: "cleaned" }] : /* istanbul ignore next */ []));
13834
+ list = computed(() => this.cultures() ?? this.contentCultures().cultures, ...(ngDevMode ? [{ debugName: "list" }] : /* istanbul ignore next */ []));
13835
+ defaultCode = computed(() => {
13836
+ const preferred = this.contentCultures().defaultCulture;
13837
+ const list = this.list();
13838
+ return list.some(c => c.code === preferred) ? preferred : (list[0]?.code ?? preferred);
13839
+ }, ...(ngDevMode ? [{ debugName: "defaultCode" }] : /* istanbul ignore next */ []));
13840
+ active = computed(() => {
13841
+ const list = this.list();
13842
+ const code = this.activeCulture();
13843
+ return list.find(c => c.code === code) ?? list.find(c => c.code === this.defaultCode()) ?? list[0];
13844
+ }, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
13845
+ /** The active culture's code: unlike active(), equal when an equal culture list arrives. */
13846
+ activeCode = computed(() => this.active()?.code, ...(ngDevMode ? [{ debugName: "activeCode" }] : /* istanbul ignore next */ []));
13847
+ /** Server messages for the whole field (`title`), and those of unlisted
13848
+ * cultures, translated here (never through the pipe a second time). */
13849
+ fieldErrors = computed(() => {
13850
+ this.textsVersion();
13851
+ const t = (key) => this.translateService.instant(key);
13852
+ const messages = [
13853
+ ...(resolveExternalErrors(this.errors(), this.serverErrorsScope, this.name()) ?? []).map(t),
13854
+ ...this.unlistedErrors(),
13855
+ ];
13856
+ return messages.length ? messages : null;
13857
+ }, ...(ngDevMode ? [{ debugName: "fieldErrors" }] : /* istanbul ignore next */ []));
13858
+ /** Every `<name>.<culture>` message the scope holds. */
13859
+ allCultureErrors = computed(() => this.serverErrorsScope?.errorsWithPrefix(`${this.name()}.`) ?? {}, ...(ngDevMode ? [{ debugName: "allCultureErrors" }] : /* istanbul ignore next */ []));
13860
+ /** Server messages per language (`title.ar` gives `ar`), only for listed cultures. */
13861
+ cultureErrors = computed(() => {
13862
+ const all = this.allCultureErrors();
13863
+ const out = {};
13864
+ for (const c of this.list())
13865
+ if (all[c.code])
13866
+ out[c.code] = all[c.code];
13867
+ return out;
13868
+ }, ...(ngDevMode ? [{ debugName: "cultureErrors" }] : /* istanbul ignore next */ []));
13869
+ /** Messages for a language this field does not list (`title.xx`): shown
13870
+ * with the whole field, never dropped, prefixed with the culture
13871
+ * (`xx : message`) since no tab says which language they concern. */
13872
+ unlistedErrors = computed(() => {
13873
+ this.textsVersion();
13874
+ const t = this.translateService;
13875
+ const listed = new Set(this.list().map(c => c.code));
13876
+ return Object.entries(this.allCultureErrors()).flatMap(([code, messages]) => {
13877
+ if (listed.has(code))
13878
+ return [];
13879
+ // The culture's name when the bundle has one, else its code.
13880
+ const nameKey = `ef_localized_input_culture_${code}`;
13881
+ const name = t.instant(nameKey);
13882
+ const culture = name && name !== nameKey ? name : code;
13883
+ return messages.map(m => t.instant('ef_localized_input_error_in_culture', { culture, message: t.instant(m) }));
13884
+ });
13885
+ }, ...(ngDevMode ? [{ debugName: "unlistedErrors" }] : /* istanbul ignore next */ []));
13886
+ /** Bumps when texts change, so translated messages follow the language. */
13887
+ textsVersion = signal(0, ...(ngDevMode ? [{ debugName: "textsVersion" }] : /* istanbul ignore next */ []));
13888
+ /**
13889
+ * The active language's messages, translated once here for the field
13890
+ * below. Without one, a required default culture left empty says so
13891
+ * once the field is touched, changed or submitted (`validation.required`,
13892
+ * the family's message), which also sets `aria-invalid` on it.
13893
+ */
13894
+ activeErrors = computed(() => {
13895
+ this.textsVersion();
13896
+ const code = this.active()?.code ?? '';
13897
+ const messages = this.cultureErrors()[code];
13898
+ if (messages?.length)
13899
+ return messages.map(m => this.translateService.instant(m));
13900
+ if (this.requiredTone(code)) {
13901
+ return [this.translateService.instant('validation.required')];
13902
+ }
13903
+ return null;
13904
+ }, ...(ngDevMode ? [{ debugName: "activeErrors" }] : /* istanbul ignore next */ []));
13905
+ /** Cultures with a server error, in tab order. */
13906
+ culturesInError = computed(() => this.list().filter(c => this.cultureErrors()[c.code]), ...(ngDevMode ? [{ debugName: "culturesInError" }] : /* istanbul ignore next */ []));
13907
+ /** Cultures in error other than the one on show, for the hint under the tabs. */
13908
+ errorsElsewhere = computed(() => this.culturesInError().filter(c => c.code !== this.active()?.code), ...(ngDevMode ? [{ debugName: "errorsElsewhere" }] : /* istanbul ignore next */ []));
13909
+ /** Changes only when the set of errors does, so a tab the user picked stays picked. */
13910
+ errorSignature = computed(() => {
13911
+ const own = JSON.stringify(this.cultureErrors());
13912
+ if (!this.showTabs())
13913
+ return own;
13914
+ const linked = this.linkedInputs().map(l => Object.entries(l.cultureFlags()).filter(([, f]) => f.serverError).map(([code]) => code));
13915
+ return JSON.stringify([own, linked]);
13916
+ }, ...(ngDevMode ? [{ debugName: "errorSignature" }] : /* istanbul ignore next */ []));
13917
+ /** How many row owners list this input in their `linkedInputs`. */
13918
+ linkedBy = signal(0, ...(ngDevMode ? [{ debugName: "linkedBy" }] : /* istanbul ignore next */ []));
13919
+ /** Danger tone on the default culture: required, empty, and touched, changed or submitted. */
13920
+ requiredTone(code) {
13921
+ return this.required() && code === this.defaultCode() && !this.cleaned()[code] && this.showErrors();
13922
+ }
13923
+ /**
13924
+ * What this input's tab would show for each culture it lists, read by
13925
+ * the input that draws a shared row (`linkedInputs`). Read-only.
13926
+ */
13927
+ cultureFlags = computed(() => {
13928
+ const text = this.cleaned();
13929
+ const base = this.baseline();
13930
+ const errors = this.cultureErrors();
13931
+ const out = {};
13932
+ for (const c of this.list()) {
13933
+ const serverError = !!errors[c.code];
13934
+ out[c.code] = {
13935
+ error: serverError || this.requiredTone(c.code),
13936
+ serverError,
13937
+ dirty: (text[c.code] ?? '') !== (base[c.code] ?? ''),
13938
+ };
13939
+ }
13940
+ return out;
13941
+ }, ...(ngDevMode ? [{ debugName: "cultureFlags" }] : /* istanbul ignore next */ []));
13942
+ tabItems = computed(() => {
13943
+ const badges = this.tabBadges() ?? {};
13944
+ const flags = [this.cultureFlags(), ...this.linkedInputs().map(l => l.cultureFlags())];
13945
+ return this.list().map(c => {
13946
+ return {
13947
+ value: c.code,
13948
+ labelKey: c.labelKey,
13949
+ badgeKey: badges[c.code] || undefined,
13950
+ tone: flags.some(f => f[c.code]?.error) ? 'danger' : undefined,
13951
+ dirty: flags.some(f => f[c.code]?.dirty),
13952
+ id: this.tabId(c.code),
13953
+ panelId: this.panelId,
13954
+ };
13955
+ });
13956
+ }, ...(ngDevMode ? [{ debugName: "tabItems" }] : /* istanbul ignore next */ []));
13957
+ cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
13958
+ /** Every culture is disabled: `[disabled]`, a form's `disable()`, or `[efReadOnly]`. */
13959
+ isDisabled = computed(() => this.disabled() || this.cvaDisabled() || (this.readOnlyScope?.readOnly() ?? false), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
13960
+ activeReadOnly = computed(() => {
13961
+ const code = this.active()?.code;
13962
+ return !!code && this.readOnlyCultures().includes(code);
13963
+ }, ...(ngDevMode ? [{ debugName: "activeReadOnly" }] : /* istanbul ignore next */ []));
13964
+ activeText = computed(() => this.raw()[this.active()?.code ?? ''] ?? '', ...(ngDevMode ? [{ debugName: "activeText" }] : /* istanbul ignore next */ []));
13965
+ /**
13966
+ * What the counter counts. Rich text against `advisedLength` counts the
13967
+ * characters a reader sees; against `maxLength` alone it counts the stored
13968
+ * HTML, because that is what the server's limit measures.
13969
+ */
13970
+ activeLength = computed(() => this.effectiveMode() === 'rich' && this.advisedLength() != null
13971
+ ? plainTextLength(this.activeText())
13972
+ : this.activeText().length, ...(ngDevMode ? [{ debugName: "activeLength" }] : /* istanbul ignore next */ []));
13973
+ labelId = `${this.uid}-label`;
13974
+ counterLimit = computed(() => this.advisedLength() ?? this.maxLength(), ...(ngDevMode ? [{ debugName: "counterLimit" }] : /* istanbul ignore next */ []));
13975
+ /**
13976
+ * The polite over-limit announcement: empty under the limit, else the
13977
+ * `counterOverKey` text, prefixed with the culture's name when the field
13978
+ * has more than one. Its text changes only with the (culture, over)
13979
+ * pair, so typing past the limit is announced once, and a switch to
13980
+ * another language that is also over is announced and names it.
13981
+ */
13982
+ overMessage = computed(() => {
13983
+ this.textsVersion();
13984
+ const key = this.counterOverKey();
13985
+ const limit = this.counterLimit();
13986
+ const culture = this.active();
13987
+ if (!key || limit == null || !culture || this.activeLength() <= limit)
13988
+ return '';
13989
+ const message = this.translateService.instant(key, { max: limit });
13990
+ if (this.list().length < 2)
13991
+ return message;
13992
+ return this.translateService.instant('ef_localized_input_error_in_culture', {
13993
+ culture: this.translateService.instant(culture.labelKey),
13994
+ message,
13995
+ });
13996
+ }, ...(ngDevMode ? [{ debugName: "overMessage" }] : /* istanbul ignore next */ []));
13997
+ panelId = `${this.uid}-panel`;
13998
+ counterId = `${this.uid}-counter`;
13999
+ /** The culture's name, read with a field that has no tab row. */
14000
+ cultureNameId = `${this.uid}-culture`;
14001
+ /** Extra `aria-describedby` ids for the active field: the counter, and
14002
+ * the culture's name when no tab says it. */
14003
+ describedByExtra = computed(() => [this.counterLimit() ? this.counterId : null, this.showTabs() ? null : this.cultureNameId].filter(Boolean).join(' '), ...(ngDevMode ? [{ debugName: "describedByExtra" }] : /* istanbul ignore next */ []));
14004
+ onChange = () => { };
14005
+ touched = () => { };
14006
+ constructor() {
14007
+ if (this.ngControl)
14008
+ this.ngControl.valueAccessor = this;
14009
+ merge(this.translateService.onLangChange, this.translateService.onTranslationChange)
14010
+ .pipe(takeUntilDestroyed())
14011
+ .subscribe(() => this.textsVersion.update(v => v + 1));
14012
+ // The validator reads required and the default culture, which can
14013
+ // change without the value changing (tenant cultures arrive late).
14014
+ effect(() => {
14015
+ this.required();
14016
+ this.defaultCode();
14017
+ untracked(() => this.ngControl?.control?.updateValueAndValidity({ emitEvent: false }));
14018
+ });
14019
+ // The parent always reads the culture on show, never null or one the
14020
+ // row does not list. Only the input that draws tabs writes it back:
14021
+ // two inputs with different lists would otherwise keep overwriting
14022
+ // each other.
14023
+ effect(() => {
14024
+ const code = this.activeCode();
14025
+ if (this.showTabs() && code && this.activeCulture() !== code)
14026
+ this.activeCulture.set(code);
14027
+ });
14028
+ // A row owner tells the inputs it links, so they leave the error jump to it.
14029
+ effect(onCleanup => {
14030
+ const linked = this.showTabs() ? this.linkedInputs() : [];
14031
+ for (const l of linked)
14032
+ l.linkedBy.update(n => n + 1);
14033
+ onCleanup(() => {
14034
+ for (const l of linked)
14035
+ l.linkedBy.update(n => n - 1);
14036
+ });
14037
+ });
14038
+ // A new set of server errors brings the first language in error into
14039
+ // view, through activeCulture. The row owner picks it in its own tab
14040
+ // order across itself and its linked inputs; an input without tabs
14041
+ // that no owner links moves it itself.
14042
+ effect(() => {
14043
+ this.errorSignature();
14044
+ untracked(() => {
14045
+ if (!this.showTabs() && this.linkedBy() > 0)
14046
+ return;
14047
+ const linked = this.showTabs() ? this.linkedInputs() : [];
14048
+ const first = this.list().find(c => this.cultureErrors()[c.code] || linked.some(l => l.cultureFlags()[c.code]?.serverError));
14049
+ if (first)
14050
+ this.activeCulture.set(first.code);
14051
+ });
14052
+ });
14053
+ effect(() => {
14054
+ if (this.mode() !== 'rich')
14055
+ return;
14056
+ if (!this.richLoader) {
14057
+ if (isDevMode()) {
14058
+ console.error(`ef-localized-input "${untracked(this.name)}": mode="rich" needs provideEfRichText() from @elasticias/ui/rich-text; showing the html source instead.`);
14059
+ }
14060
+ return;
14061
+ }
14062
+ if (untracked(this.richType) || untracked(this.richFailed))
14063
+ return;
14064
+ this.richLoader()
14065
+ .then(t => this.richType.set(t))
14066
+ .catch((err) => {
14067
+ if (isDevMode()) {
14068
+ console.error(`ef-localized-input "${untracked(this.name)}": could not load the rich text editor; showing the html source instead.`, err);
14069
+ }
14070
+ this.richFailed.set(true);
14071
+ });
14072
+ });
14073
+ // One editor per active culture: the @for recreates the host on a
14074
+ // culture switch, and the editor is created in it with live bindings.
14075
+ effect(() => {
14076
+ const vcr = this.richHost();
14077
+ const type = this.richType();
14078
+ const code = this.activeCode();
14079
+ if (!vcr || !type || !code || this.effectiveMode() !== 'rich')
14080
+ return;
14081
+ untracked(() => this.createRich(vcr, type, code));
14082
+ });
14083
+ }
14084
+ createRich(vcr, type, code) {
14085
+ vcr.clear();
14086
+ this.richRef = null;
14087
+ const culture = () => this.list().find(c => c.code === code);
14088
+ const readOnly = () => this.readOnlyCultures().includes(code);
14089
+ this.richRef = vcr.createComponent(type, {
14090
+ bindings: [
14091
+ inputBinding('value', () => this.raw()[code] ?? ''),
14092
+ inputBinding('name', () => `${this.name()}.${code}`),
14093
+ inputBinding('inputId', () => this.controlId(code)),
14094
+ inputBinding('dir', () => culture()?.dir ?? null),
14095
+ inputBinding('lang', () => code),
14096
+ inputBinding('rows', () => this.rows()),
14097
+ inputBinding('labelledBy', () => (this.labelKey() ? this.labelId : null)),
14098
+ inputBinding('required', () => this.required() && code === this.defaultCode()),
14099
+ inputBinding('errors', () => this.activeErrors()),
14100
+ inputBinding('disabled', () => this.isDisabled()),
14101
+ inputBinding('readonly', readOnly),
14102
+ inputBinding('placeholderKey', () => this.placeholderKey()),
14103
+ inputBinding('hintKey', () => (readOnly() ? 'ef_localized_input_read_only_hint' : this.hintKey())),
14104
+ inputBinding('describedByExtra', () => this.describedByExtra()),
14105
+ outputBinding('valueChangeEvent', v => this.setText(code, v)),
14106
+ ],
14107
+ });
14108
+ }
14109
+ /**
14110
+ * Inserts `text` (a `{{token}}`) into the culture on show, at the caret,
14111
+ * replacing a selection, or at the end when its field was never entered
14112
+ * or was just recreated by a culture switch; then emits the whole object
14113
+ * once, as typing would. Returns false, and changes nothing, when the
14114
+ * field is disabled, the culture is read-only, the result would pass
14115
+ * `maxLength`, there is no active field (no culture listed), or the
14116
+ * field is the rich editor (which has no insert yet). The field says
14117
+ * nothing itself on a refusal, so the caller should tell the user (a
14118
+ * toast such as "the variable would pass the length limit"); otherwise
14119
+ * a click on a variable silently does nothing.
14120
+ *
14121
+ * On a phone, the focus that follows the insert opens the keyboard.
14122
+ *
14123
+ * A caller tracking "the field last focused" should ignore focus on the
14124
+ * culture tabs (`[role="tab"]`): picking a language is not choosing where
14125
+ * the token goes.
14126
+ */
14127
+ insertAtCaret(text) {
14128
+ const code = this.activeCode();
14129
+ if (!code || this.isDisabled() || this.activeReadOnly())
14130
+ return false;
14131
+ const target = (this.effectiveMode() === 'rich' ? this.richRef?.instance : (this.textField() ?? this.areaField()));
14132
+ if (typeof target?.insertAtCursor !== 'function')
14133
+ return false;
14134
+ const before = this.raw()[code] ?? '';
14135
+ target.insertAtCursor(text);
14136
+ return (this.raw()[code] ?? '') !== before;
14137
+ }
14138
+ tabId(code) {
14139
+ return `${this.uid}-tab-${code}`;
14140
+ }
14141
+ controlId(code) {
14142
+ return `${this.uid}-${code}`;
14143
+ }
14144
+ select(code) {
14145
+ this.activeCulture.set(code);
14146
+ }
14147
+ setText(code, text) {
14148
+ this.raw.update(r => ({ ...r, [code]: text }));
14149
+ const out = this.cleaned();
14150
+ this.lastEmitted = out;
14151
+ this.interacted.set(true);
14152
+ this.onChange(out);
14153
+ this.valueChangeEvent.emit(out);
14154
+ }
14155
+ /** Touched when focus leaves the whole field, not when it moves from a tab to the text. */
14156
+ onFocusOut(event) {
14157
+ const next = event.relatedTarget;
14158
+ if (next instanceof Node && this.host.nativeElement.contains(next))
14159
+ return;
14160
+ this.interacted.set(true);
14161
+ this.touched();
14162
+ }
14163
+ ngDoCheck() {
14164
+ // A form's markAllAsTouched() or submit does not reach the accessor,
14165
+ // so the control's state is read each time the parent is checked.
14166
+ const c = this.ngControl;
14167
+ this.formTouched.set(!!(c?.touched || c?.dirty || this.parentForm?.submitted));
14168
+ }
14169
+ /** `{ required: true }` while a required field's default culture has no text. */
14170
+ validateRequired(control) {
14171
+ if (!this.required())
14172
+ return null;
14173
+ return cleanLocalizedText(control.value)[this.defaultCode()] ? null : { required: true };
14174
+ }
14175
+ writeValue(v) {
14176
+ this.raw.set({ ...(v ?? {}) });
14177
+ this.baseline.set(cleanLocalizedText(v));
14178
+ }
14179
+ registerOnChange(fn) {
14180
+ this.onChange = fn;
14181
+ }
14182
+ registerOnTouched(fn) {
14183
+ this.touched = fn;
14184
+ }
14185
+ setDisabledState(disabled) {
14186
+ this.cvaDisabled.set(disabled);
14187
+ }
14188
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfLocalizedInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14189
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfLocalizedInputComponent, isStandalone: true, selector: "ef-localized-input", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, advisedLength: { classPropertyName: "advisedLength", publicName: "advisedLength", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, readOnlyCultures: { classPropertyName: "readOnlyCultures", publicName: "readOnlyCultures", isSignal: true, isRequired: false, transformFunction: null }, labelKey: { classPropertyName: "labelKey", publicName: "labelKey", isSignal: true, isRequired: false, transformFunction: null }, placeholderKey: { classPropertyName: "placeholderKey", publicName: "placeholderKey", isSignal: true, isRequired: false, transformFunction: null }, hintKey: { classPropertyName: "hintKey", publicName: "hintKey", isSignal: true, isRequired: false, transformFunction: null }, counterHintKey: { classPropertyName: "counterHintKey", publicName: "counterHintKey", isSignal: true, isRequired: false, transformFunction: null }, counterOverKey: { classPropertyName: "counterOverKey", publicName: "counterOverKey", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, cultures: { classPropertyName: "cultures", publicName: "cultures", isSignal: true, isRequired: false, transformFunction: null }, activeCulture: { classPropertyName: "activeCulture", publicName: "activeCulture", isSignal: true, isRequired: false, transformFunction: null }, showTabs: { classPropertyName: "showTabs", publicName: "showTabs", isSignal: true, isRequired: false, transformFunction: null }, linkedInputs: { classPropertyName: "linkedInputs", publicName: "linkedInputs", isSignal: true, isRequired: false, transformFunction: null }, preview: { classPropertyName: "preview", publicName: "preview", isSignal: true, isRequired: false, transformFunction: null }, tabBadges: { classPropertyName: "tabBadges", publicName: "tabBadges", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeCulture: "activeCultureChange", valueChangeEvent: "valueChangeEvent" }, host: { listeners: { "focusout": "onFocusOut($event)" } }, providers: [
14190
+ {
14191
+ // A form's required validator reads an empty object as filled (it
14192
+ // checks length), so the field validates its default culture
14193
+ // itself. The component is looked up when the form validates, not
14194
+ // here: it injects NgControl, which injects these validators.
14195
+ provide: NG_VALIDATORS,
14196
+ multi: true,
14197
+ useFactory: () => {
14198
+ const injector = inject(Injector);
14199
+ return (control) => injector.get(EfLocalizedInputComponent).validateRequired(control);
14200
+ },
14201
+ },
14202
+ ], viewQueries: [{ propertyName: "richHost", first: true, predicate: ["richHost"], descendants: true, read: ViewContainerRef, isSignal: true }, { propertyName: "textField", first: true, predicate: EfInputTextComponent, descendants: true, isSignal: true }, { propertyName: "areaField", first: true, predicate: EfTextareaComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"eli-head\">\n @if (labelKey()) {\n <span class=\"eli-label\" [id]=\"labelId\">\n <ef-label [labelKey]=\"labelKey()\" [for]=\"controlId(active()?.code ?? '')\" [required]=\"required()\" />\n </span>\n }\n @if (showTabs()) {\n <div class=\"eli-tabs\">\n <ef-tabs\n variant=\"pill\"\n [items]=\"tabItems()\"\n [value]=\"active()?.code ?? null\"\n [ariaLabelKey]=\"labelKey() || 'ef_localized_input_cultures'\"\n (valueChange)=\"select($event)\"\n />\n </div>\n }\n</div>\n\n<!-- Without a tab row the panel is a plain group: a tabpanel would point\n at a tab that does not exist. -->\n<div\n class=\"eli-panel\"\n [id]=\"panelId\"\n [attr.role]=\"showTabs() ? 'tabpanel' : null\"\n [attr.aria-labelledby]=\"showTabs() ? tabId(active()?.code ?? '') : null\"\n>\n @if (!showTabs()) {\n @if (active(); as c) {\n <span class=\"eli-sr\" [id]=\"cultureNameId\">{{ c.labelKey | translate }}</span>\n }\n }\n @for (c of active() ? [active()!] : []; track c.code) {\n @switch (effectiveMode()) {\n @case ('text') {\n <ef-input-text\n [variant]=\"variant() === 'comptoir' ? 'comptoir' : 'primeng'\"\n [inputId]=\"controlId(c.code)\"\n [name]=\"name() + '.' + c.code\"\n [dir]=\"c.dir\"\n [lang]=\"c.code\"\n [value]=\"activeText()\"\n [maxlength]=\"maxLength()\"\n [required]=\"required() && c.code === defaultCode()\"\n [errors]=\"activeErrors()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"activeReadOnly()\"\n [placeholderKey]=\"placeholderKey()\"\n [hintKey]=\"activeReadOnly() ? 'ef_localized_input_read_only_hint' : hintKey()\"\n [describedByExtra]=\"describedByExtra()\"\n (valueChangeEvent)=\"setText(c.code, $event)\"\n />\n }\n @case ('textarea') {\n <ef-textarea\n [inputId]=\"controlId(c.code)\"\n [name]=\"name() + '.' + c.code\"\n [dir]=\"c.dir\"\n [lang]=\"c.code\"\n [rows]=\"rows()\"\n [value]=\"activeText()\"\n [maxlength]=\"maxLength()\"\n [required]=\"required() && c.code === defaultCode()\"\n [errors]=\"activeErrors()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"activeReadOnly()\"\n [placeholderKey]=\"placeholderKey()\"\n [hintKey]=\"activeReadOnly() ? 'ef_localized_input_read_only_hint' : hintKey()\"\n [describedByExtra]=\"describedByExtra()\"\n (valueChangeEvent)=\"setText(c.code, $event)\"\n />\n }\n @case ('rich') {\n <!-- ef-rich-text, created from EF_RICH_TEXT (see createRich). -->\n <ng-container #richHost />\n }\n @case ('html') {\n <!-- The source stays ltr for every culture: raw HTML in an rtl\n textarea reads reversed (ADR-036). The preview takes the\n culture's direction. -->\n <ef-textarea\n mono\n dir=\"ltr\"\n [inputId]=\"controlId(c.code)\"\n [name]=\"name() + '.' + c.code\"\n [lang]=\"c.code\"\n [rows]=\"rows()\"\n [value]=\"activeText()\"\n [maxlength]=\"maxLength()\"\n [required]=\"required() && c.code === defaultCode()\"\n [errors]=\"activeErrors()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"activeReadOnly()\"\n [placeholderKey]=\"placeholderKey()\"\n [hintKey]=\"activeReadOnly() ? 'ef_localized_input_read_only_hint' : hintKey()\"\n [describedByExtra]=\"describedByExtra()\"\n (valueChangeEvent)=\"setText(c.code, $event)\"\n />\n }\n }\n }\n\n @if (counterLimit(); as limit) {\n <span class=\"eli-counter\" data-counter [id]=\"counterId\" [class.is-over]=\"activeLength() > limit\">\n @if (advisedLength() !== null && advisedLength() !== undefined) {\n <!-- Advice, worded so (\"5123 / 5000 conseill\u00E9s\"), so it does not\n read as a cap. Words around the numbers: the text takes the\n page's direction, and an rtl reader meets the count first. -->\n <span data-counter-text>{{ 'ef_localized_input_counter_advised' | translate: { count: activeLength(), max: limit } }}</span>\n } @else {\n <bdi dir=\"ltr\">{{ activeLength() }} / {{ limit }}</bdi>\n }\n @if (counterHintKey()) {\n <span class=\"eli-counter__sr\">{{ counterHintKey() | translate: { max: limit } }}</span>\n }\n </span>\n @if (counterOverKey()) {\n <!-- Always present, so a screen reader hears the text appear. -->\n <span class=\"eli-counter__sr\" data-over-message aria-live=\"polite\">{{ overMessage() }}</span>\n }\n }\n\n @if (effectiveMode() === 'html' && preview() && activeText().trim()) {\n @if (active(); as c) {\n <!-- Angular's default [innerHTML] sanitising strips scripts and handlers. -->\n <div class=\"eli-preview\" data-preview [attr.dir]=\"c.dir\" [attr.lang]=\"c.code\" [innerHTML]=\"activeText()\"></div>\n }\n }\n\n @if (fieldErrors(); as messages) {\n <div class=\"eli-errors\" data-field-errors role=\"alert\" aria-live=\"polite\">\n @for (message of messages; track message) {\n <small class=\"text-danger block\">{{ message }}</small>\n }\n </div>\n }\n\n @if (errorsElsewhere(); as others) {\n @if (others.length) {\n <small class=\"eli-elsewhere text-danger block\" data-errors-elsewhere role=\"status\">\n {{ 'ef_localized_input_errors_in_other' | translate }}\n @for (c of others; track c.code; let last = $last) {\n {{ c.labelKey | translate }}{{ last ? '' : ',' }}\n }\n </small>\n }\n }\n\n @if (activeReadOnly()) {\n <div class=\"eli-link\">\n <ng-content select=\"[read-only-link]\" />\n </div>\n }\n</div>\n", styles: [":host{position:relative;display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-2);min-width:0}.eli-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--s-2);min-width:0}.eli-label{min-width:0}.eli-tabs{min-width:0;max-width:100%;overflow-x:auto}.eli-panel{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-2);min-width:0}.eli-counter{position:relative;font-size:12px;color:var(--text-mute);text-align:end;font-variant-numeric:tabular-nums}.eli-counter.is-over{color:var(--st-pending-fg);font-weight:600}.eli-counter__sr,.eli-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.eli-preview{min-width:0;overflow-x:auto;overflow-wrap:anywhere;padding:var(--s-3);border:1px solid var(--rule);border-radius:var(--r-sm);background:var(--paper-alt);color:var(--text);font-size:13.5px;line-height:1.55}.eli-link{font-size:12px}@media(max-width:767px){.eli-tabs{--hit: var(--hit-touch);flex:1 1 100%}}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLabelComponent, selector: "ef-label", inputs: ["label", "labelKey", "labelParams", "for", "styleClass", "required"] }, { kind: "component", type: EfTabsComponent, selector: "ef-tabs", inputs: ["items", "value", "variant", "module", "overflow", "hideCounts", "ariaLabel", "ariaLabelKey", "dirtyLabelKey"], outputs: ["valueChange"] }, { kind: "component", type: EfInputTextComponent, selector: "ef-input-text", inputs: ["size", "type", "maxlength", "fluid", "inline", "autocomplete", "dir", "lang", "variant", "prefix", "suffix", "value"], outputs: ["valueChangeEvent"] }, { kind: "component", type: EfTextareaComponent, selector: "ef-textarea", inputs: ["rows", "maxlength", "lang", "autofocus", "mono", "value"], outputs: ["valueChangeEvent"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14203
+ }
14204
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfLocalizedInputComponent, decorators: [{
14205
+ type: Component,
14206
+ args: [{ selector: 'ef-localized-input', standalone: true, imports: [TranslateModule, EfLabelComponent, EfTabsComponent, EfInputTextComponent, EfTextareaComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: { '(focusout)': 'onFocusOut($event)' }, providers: [
14207
+ {
14208
+ // A form's required validator reads an empty object as filled (it
14209
+ // checks length), so the field validates its default culture
14210
+ // itself. The component is looked up when the form validates, not
14211
+ // here: it injects NgControl, which injects these validators.
14212
+ provide: NG_VALIDATORS,
14213
+ multi: true,
14214
+ useFactory: () => {
14215
+ const injector = inject(Injector);
14216
+ return (control) => injector.get(EfLocalizedInputComponent).validateRequired(control);
14217
+ },
14218
+ },
14219
+ ], template: "<div class=\"eli-head\">\n @if (labelKey()) {\n <span class=\"eli-label\" [id]=\"labelId\">\n <ef-label [labelKey]=\"labelKey()\" [for]=\"controlId(active()?.code ?? '')\" [required]=\"required()\" />\n </span>\n }\n @if (showTabs()) {\n <div class=\"eli-tabs\">\n <ef-tabs\n variant=\"pill\"\n [items]=\"tabItems()\"\n [value]=\"active()?.code ?? null\"\n [ariaLabelKey]=\"labelKey() || 'ef_localized_input_cultures'\"\n (valueChange)=\"select($event)\"\n />\n </div>\n }\n</div>\n\n<!-- Without a tab row the panel is a plain group: a tabpanel would point\n at a tab that does not exist. -->\n<div\n class=\"eli-panel\"\n [id]=\"panelId\"\n [attr.role]=\"showTabs() ? 'tabpanel' : null\"\n [attr.aria-labelledby]=\"showTabs() ? tabId(active()?.code ?? '') : null\"\n>\n @if (!showTabs()) {\n @if (active(); as c) {\n <span class=\"eli-sr\" [id]=\"cultureNameId\">{{ c.labelKey | translate }}</span>\n }\n }\n @for (c of active() ? [active()!] : []; track c.code) {\n @switch (effectiveMode()) {\n @case ('text') {\n <ef-input-text\n [variant]=\"variant() === 'comptoir' ? 'comptoir' : 'primeng'\"\n [inputId]=\"controlId(c.code)\"\n [name]=\"name() + '.' + c.code\"\n [dir]=\"c.dir\"\n [lang]=\"c.code\"\n [value]=\"activeText()\"\n [maxlength]=\"maxLength()\"\n [required]=\"required() && c.code === defaultCode()\"\n [errors]=\"activeErrors()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"activeReadOnly()\"\n [placeholderKey]=\"placeholderKey()\"\n [hintKey]=\"activeReadOnly() ? 'ef_localized_input_read_only_hint' : hintKey()\"\n [describedByExtra]=\"describedByExtra()\"\n (valueChangeEvent)=\"setText(c.code, $event)\"\n />\n }\n @case ('textarea') {\n <ef-textarea\n [inputId]=\"controlId(c.code)\"\n [name]=\"name() + '.' + c.code\"\n [dir]=\"c.dir\"\n [lang]=\"c.code\"\n [rows]=\"rows()\"\n [value]=\"activeText()\"\n [maxlength]=\"maxLength()\"\n [required]=\"required() && c.code === defaultCode()\"\n [errors]=\"activeErrors()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"activeReadOnly()\"\n [placeholderKey]=\"placeholderKey()\"\n [hintKey]=\"activeReadOnly() ? 'ef_localized_input_read_only_hint' : hintKey()\"\n [describedByExtra]=\"describedByExtra()\"\n (valueChangeEvent)=\"setText(c.code, $event)\"\n />\n }\n @case ('rich') {\n <!-- ef-rich-text, created from EF_RICH_TEXT (see createRich). -->\n <ng-container #richHost />\n }\n @case ('html') {\n <!-- The source stays ltr for every culture: raw HTML in an rtl\n textarea reads reversed (ADR-036). The preview takes the\n culture's direction. -->\n <ef-textarea\n mono\n dir=\"ltr\"\n [inputId]=\"controlId(c.code)\"\n [name]=\"name() + '.' + c.code\"\n [lang]=\"c.code\"\n [rows]=\"rows()\"\n [value]=\"activeText()\"\n [maxlength]=\"maxLength()\"\n [required]=\"required() && c.code === defaultCode()\"\n [errors]=\"activeErrors()\"\n [disabled]=\"isDisabled()\"\n [readonly]=\"activeReadOnly()\"\n [placeholderKey]=\"placeholderKey()\"\n [hintKey]=\"activeReadOnly() ? 'ef_localized_input_read_only_hint' : hintKey()\"\n [describedByExtra]=\"describedByExtra()\"\n (valueChangeEvent)=\"setText(c.code, $event)\"\n />\n }\n }\n }\n\n @if (counterLimit(); as limit) {\n <span class=\"eli-counter\" data-counter [id]=\"counterId\" [class.is-over]=\"activeLength() > limit\">\n @if (advisedLength() !== null && advisedLength() !== undefined) {\n <!-- Advice, worded so (\"5123 / 5000 conseill\u00E9s\"), so it does not\n read as a cap. Words around the numbers: the text takes the\n page's direction, and an rtl reader meets the count first. -->\n <span data-counter-text>{{ 'ef_localized_input_counter_advised' | translate: { count: activeLength(), max: limit } }}</span>\n } @else {\n <bdi dir=\"ltr\">{{ activeLength() }} / {{ limit }}</bdi>\n }\n @if (counterHintKey()) {\n <span class=\"eli-counter__sr\">{{ counterHintKey() | translate: { max: limit } }}</span>\n }\n </span>\n @if (counterOverKey()) {\n <!-- Always present, so a screen reader hears the text appear. -->\n <span class=\"eli-counter__sr\" data-over-message aria-live=\"polite\">{{ overMessage() }}</span>\n }\n }\n\n @if (effectiveMode() === 'html' && preview() && activeText().trim()) {\n @if (active(); as c) {\n <!-- Angular's default [innerHTML] sanitising strips scripts and handlers. -->\n <div class=\"eli-preview\" data-preview [attr.dir]=\"c.dir\" [attr.lang]=\"c.code\" [innerHTML]=\"activeText()\"></div>\n }\n }\n\n @if (fieldErrors(); as messages) {\n <div class=\"eli-errors\" data-field-errors role=\"alert\" aria-live=\"polite\">\n @for (message of messages; track message) {\n <small class=\"text-danger block\">{{ message }}</small>\n }\n </div>\n }\n\n @if (errorsElsewhere(); as others) {\n @if (others.length) {\n <small class=\"eli-elsewhere text-danger block\" data-errors-elsewhere role=\"status\">\n {{ 'ef_localized_input_errors_in_other' | translate }}\n @for (c of others; track c.code; let last = $last) {\n {{ c.labelKey | translate }}{{ last ? '' : ',' }}\n }\n </small>\n }\n }\n\n @if (activeReadOnly()) {\n <div class=\"eli-link\">\n <ng-content select=\"[read-only-link]\" />\n </div>\n }\n</div>\n", styles: [":host{position:relative;display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-2);min-width:0}.eli-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--s-2);min-width:0}.eli-label{min-width:0}.eli-tabs{min-width:0;max-width:100%;overflow-x:auto}.eli-panel{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-2);min-width:0}.eli-counter{position:relative;font-size:12px;color:var(--text-mute);text-align:end;font-variant-numeric:tabular-nums}.eli-counter.is-over{color:var(--st-pending-fg);font-weight:600}.eli-counter__sr,.eli-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.eli-preview{min-width:0;overflow-x:auto;overflow-wrap:anywhere;padding:var(--s-3);border:1px solid var(--rule);border-radius:var(--r-sm);background:var(--paper-alt);color:var(--text);font-size:13.5px;line-height:1.55}.eli-link{font-size:12px}@media(max-width:767px){.eli-tabs{--hit: var(--hit-touch);flex:1 1 100%}}\n"] }]
14220
+ }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], advisedLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "advisedLength", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], readOnlyCultures: [{ type: i0.Input, args: [{ isSignal: true, alias: "readOnlyCultures", required: false }] }], labelKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelKey", required: false }] }], placeholderKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholderKey", required: false }] }], hintKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintKey", required: false }] }], counterHintKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "counterHintKey", required: false }] }], counterOverKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "counterOverKey", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], cultures: [{ type: i0.Input, args: [{ isSignal: true, alias: "cultures", required: false }] }], activeCulture: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeCulture", required: false }] }, { type: i0.Output, args: ["activeCultureChange"] }], showTabs: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTabs", required: false }] }], linkedInputs: [{ type: i0.Input, args: [{ isSignal: true, alias: "linkedInputs", required: false }] }], preview: [{ type: i0.Input, args: [{ isSignal: true, alias: "preview", required: false }] }], tabBadges: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabBadges", required: false }] }], valueChangeEvent: [{ type: i0.Output, args: ["valueChangeEvent"] }], richHost: [{ type: i0.ViewChild, args: ['richHost', { ...{ read: ViewContainerRef }, isSignal: true }] }], textField: [{ type: i0.ViewChild, args: [i0.forwardRef(() => EfInputTextComponent), { isSignal: true }] }], areaField: [{ type: i0.ViewChild, args: [i0.forwardRef(() => EfTextareaComponent), { isSignal: true }] }] } });
14221
+
14222
+ /**
14223
+ * The content cultures the SEO fields start from, shared with `ef-localized-input`.
14224
+ * @deprecated use `EF_DEFAULT_CONTENT_CULTURES` from `@elasticias/core`.
14225
+ */
14226
+ const EF_SEO_DEFAULT_LOCALES = EF_DEFAULT_CONTENT_CULTURES;
14227
+ /**
14228
+ * SEO title and description in every content language, each an
14229
+ * `ef-localized-input` with a counter against what a search engine shows
14230
+ * (60 / 160, read out to screen readers), capped at the server's limits
14231
+ * (256 / 512), and the shared `ef-seo-preview` of the default language's
14232
+ * result with the entity's own title and description as the fallback the shop
14233
+ * uses. Card-less: each screen wraps it in its own `ef-card`.
14234
+ *
14235
+ * The controls are named `seoTitle` and `seoDescription`, so under
14236
+ * `[efServerErrors]` a `seoTitle.ar` error lands on the Arabic tab and shows
14237
+ * under its field. Languages come from `EF_CONTENT_CULTURES` unless `cultures`
14238
+ * is set.
14239
+ *
14240
+ * i18n keys the app's bundles must define, besides those of
14241
+ * `ef-localized-input`:
14242
+ *
14243
+ * | key | fr | ar | en |
14244
+ * |---|---|---|---|
14245
+ * | `ef_seo_fields_title_label` | Titre SEO | عنوان SEO | SEO title |
14246
+ * | `ef_seo_fields_description_label` | Description SEO | وصف SEO | SEO description |
14247
+ * | `ef_seo_fields_counter_hint` | Au-delà de {{max}} caractères, Google coupe le texte. | بعد {{max}} حرفا، يقتطع Google النص. | Past {{max}} characters, Google cuts the text. |
14248
+ * | `ef_seo_fields_over_hint` | Le texte dépasse {{max}} caractères. | النص يتجاوز {{max}} حرفا. | The text is over {{max}} characters. |
14249
+ * | `ef_seo_fields_preview` | Aperçu dans Google | المعاينة في Google | Google preview |
14250
+ * | `ef_seo_fields_fallback_hint` | Vide : la boutique reprend le titre et la description. | فارغ: يستخدم المتجر العنوان والوصف. | Empty: the shop uses the title and description. |
13473
14251
  *
13474
14252
  * ```html
13475
- * <ef-seo-fields [seoTitle]="p.seoTitle" ... [previewUrl]="url()" (seoChange)="patch($event)" />
14253
+ * <ef-seo-fields [seoTitle]="p.seoTitle" [seoDescription]="p.seoDescription"
14254
+ * [fallbackTitle]="p.title" [fallbackDescription]="p.description"
14255
+ * [previewUrl]="url()" (seoChange)="patch($event)" />
13476
14256
  * ```
13477
14257
  */
13478
14258
  class EfSeoFieldsComponent {
13479
- seoTitle = input('', ...(ngDevMode ? [{ debugName: "seoTitle" }] : /* istanbul ignore next */ []));
13480
- seoDescription = input('', ...(ngDevMode ? [{ debugName: "seoDescription" }] : /* istanbul ignore next */ []));
13481
- seoTitleTranslations = input({}, ...(ngDevMode ? [{ debugName: "seoTitleTranslations" }] : /* istanbul ignore next */ []));
13482
- seoDescriptionTranslations = input({}, ...(ngDevMode ? [{ debugName: "seoDescriptionTranslations" }] : /* istanbul ignore next */ []));
14259
+ seoTitle = input(null, ...(ngDevMode ? [{ debugName: "seoTitle" }] : /* istanbul ignore next */ []));
14260
+ seoDescription = input(null, ...(ngDevMode ? [{ debugName: "seoDescription" }] : /* istanbul ignore next */ []));
13483
14261
  /** The entity's own texts, which the shop uses when SEO is empty. */
13484
- fallbackTitle = input('', ...(ngDevMode ? [{ debugName: "fallbackTitle" }] : /* istanbul ignore next */ []));
13485
- fallbackDescription = input('', ...(ngDevMode ? [{ debugName: "fallbackDescription" }] : /* istanbul ignore next */ []));
14262
+ fallbackTitle = input(null, ...(ngDevMode ? [{ debugName: "fallbackTitle" }] : /* istanbul ignore next */ []));
14263
+ fallbackDescription = input(null, ...(ngDevMode ? [{ debugName: "fallbackDescription" }] : /* istanbul ignore next */ []));
13486
14264
  /** The page address shown in the preview, computed by the app. */
13487
14265
  previewUrl = input('', ...(ngDevMode ? [{ debugName: "previewUrl" }] : /* istanbul ignore next */ []));
13488
14266
  titleMax = input(256, ...(ngDevMode ? [{ debugName: "titleMax" }] : /* istanbul ignore next */ []));
13489
14267
  descriptionMax = input(512, ...(ngDevMode ? [{ debugName: "descriptionMax" }] : /* istanbul ignore next */ []));
13490
14268
  titleAdvised = input(60, ...(ngDevMode ? [{ debugName: "titleAdvised" }] : /* istanbul ignore next */ []));
13491
14269
  descriptionAdvised = input(160, ...(ngDevMode ? [{ debugName: "descriptionAdvised" }] : /* istanbul ignore next */ []));
13492
- locales = input(EF_SEO_DEFAULT_LOCALES, ...(ngDevMode ? [{ debugName: "locales" }] : /* istanbul ignore next */ []));
14270
+ /** Overrides the `EF_CONTENT_CULTURES` list for both fields. */
14271
+ cultures = input(undefined, ...(ngDevMode ? [{ debugName: "cultures" }] : /* istanbul ignore next */ []));
13493
14272
  seoChange = output();
13494
- text(locale, field) {
13495
- if (locale === 'fr')
13496
- return (field === 'title' ? this.seoTitle() : this.seoDescription()) ?? '';
13497
- const dict = field === 'title' ? this.seoTitleTranslations() : this.seoDescriptionTranslations();
13498
- return dict?.[locale] ?? '';
13499
- }
13500
- /** Id of a counter, which the field it counts points at with `aria-describedby`. */
13501
- counterId(locale, field) {
13502
- return `${this.uid}-${this.name(locale, field)}-counter`;
13503
- }
13504
- uid = `ef-seo-fields-${nextId++}`;
13505
- /** French matches the backend property; the others are unique for the label link. */
13506
- name(locale, field) {
13507
- const base = field === 'title' ? 'seoTitle' : 'seoDescription';
13508
- return locale === 'fr' ? base : `${base}${locale.charAt(0).toUpperCase()}${locale.slice(1)}`;
13509
- }
13510
- setText(locale, field, value) {
13511
- if (locale === 'fr') {
13512
- this.seoChange.emit(field === 'title' ? { seoTitle: value } : { seoDescription: value });
13513
- return;
13514
- }
13515
- const key = field === 'title' ? 'seoTitleTranslations' : 'seoDescriptionTranslations';
13516
- const next = { ...((field === 'title' ? this.seoTitleTranslations() : this.seoDescriptionTranslations()) ?? {}) };
13517
- // A cleared field removes the entry, so the shop falls back instead of showing nothing.
13518
- if (value)
13519
- next[locale] = value;
13520
- else
13521
- delete next[locale];
13522
- this.seoChange.emit({ [key]: next });
14273
+ contentCultures = inject(EF_CONTENT_CULTURES);
14274
+ /** The language the preview shows: the tenant's default if listed, else the first listed. */
14275
+ previewCode = computed(() => {
14276
+ const list = this.cultures() ?? this.contentCultures().cultures;
14277
+ const preferred = this.contentCultures().defaultCulture;
14278
+ return list.some(c => c.code === preferred) ? preferred : (list[0]?.code ?? preferred);
14279
+ }, ...(ngDevMode ? [{ debugName: "previewCode" }] : /* istanbul ignore next */ []));
14280
+ previewTitle = computed(() => this.shown(this.seoTitle(), this.fallbackTitle()), ...(ngDevMode ? [{ debugName: "previewTitle" }] : /* istanbul ignore next */ []));
14281
+ previewDescription = computed(() => this.shown(this.seoDescription(), this.fallbackDescription()), ...(ngDevMode ? [{ debugName: "previewDescription" }] : /* istanbul ignore next */ []));
14282
+ shown(seo, fallback) {
14283
+ const code = this.previewCode();
14284
+ return (seo?.[code]?.trim() || fallback?.[code]?.trim() || '');
13523
14285
  }
13524
- previewTitle = computed(() => (this.seoTitle() || this.fallbackTitle() || '').trim(), ...(ngDevMode ? [{ debugName: "previewTitle" }] : /* istanbul ignore next */ []));
13525
- previewDescription = computed(() => (this.seoDescription() || this.fallbackDescription() || '').trim(), ...(ngDevMode ? [{ debugName: "previewDescription" }] : /* istanbul ignore next */ []));
13526
14286
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfSeoFieldsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13527
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: EfSeoFieldsComponent, isStandalone: true, selector: "ef-seo-fields", inputs: { seoTitle: { classPropertyName: "seoTitle", publicName: "seoTitle", isSignal: true, isRequired: false, transformFunction: null }, seoDescription: { classPropertyName: "seoDescription", publicName: "seoDescription", isSignal: true, isRequired: false, transformFunction: null }, seoTitleTranslations: { classPropertyName: "seoTitleTranslations", publicName: "seoTitleTranslations", isSignal: true, isRequired: false, transformFunction: null }, seoDescriptionTranslations: { classPropertyName: "seoDescriptionTranslations", publicName: "seoDescriptionTranslations", isSignal: true, isRequired: false, transformFunction: null }, fallbackTitle: { classPropertyName: "fallbackTitle", publicName: "fallbackTitle", isSignal: true, isRequired: false, transformFunction: null }, fallbackDescription: { classPropertyName: "fallbackDescription", publicName: "fallbackDescription", isSignal: true, isRequired: false, transformFunction: null }, previewUrl: { classPropertyName: "previewUrl", publicName: "previewUrl", isSignal: true, isRequired: false, transformFunction: null }, titleMax: { classPropertyName: "titleMax", publicName: "titleMax", isSignal: true, isRequired: false, transformFunction: null }, descriptionMax: { classPropertyName: "descriptionMax", publicName: "descriptionMax", isSignal: true, isRequired: false, transformFunction: null }, titleAdvised: { classPropertyName: "titleAdvised", publicName: "titleAdvised", isSignal: true, isRequired: false, transformFunction: null }, descriptionAdvised: { classPropertyName: "descriptionAdvised", publicName: "descriptionAdvised", isSignal: true, isRequired: false, transformFunction: null }, locales: { classPropertyName: "locales", publicName: "locales", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { seoChange: "seoChange" }, ngImport: i0, template: "<div class=\"seo-body\">\n <p class=\"seo-note text-mute\">{{ 'ef_seo_fields_fallback_hint' | translate }}</p>\n\n @for (locale of locales(); track locale.code) {\n <ef-fieldset variant=\"plain\" [legendKey]=\"locale.labelKey\" [attr.lang]=\"locale.code\">\n <ef-form-grid>\n <ef-input-text\n class=\"col-12\"\n variant=\"comptoir\"\n [dir]=\"locale.dir\"\n [name]=\"name(locale.code, 'title')\"\n [describedByExtra]=\"counterId(locale.code, 'title')\"\n labelKey=\"ef_seo_fields_title_label\"\n [maxlength]=\"titleMax()\"\n [value]=\"text(locale.code, 'title')\"\n (valueChangeEvent)=\"setText(locale.code, 'title', $event)\"\n />\n <span\n class=\"col-12 seo-counter\"\n [id]=\"counterId(locale.code, 'title')\"\n [attr.data-counter]=\"locale.code + '-title'\"\n [class.is-over]=\"text(locale.code, 'title').length > titleAdvised()\"\n >\n <bdi dir=\"ltr\">{{ text(locale.code, 'title').length }} / {{ titleAdvised() }}</bdi>\n <span class=\"seo-counter__sr\">\n {{ 'ef_seo_fields_counter_hint' | translate: { max: titleAdvised() } }}\n @if (text(locale.code, 'title').length > titleAdvised()) {\n {{ 'ef_seo_fields_over_hint' | translate: { max: titleAdvised() } }}\n }\n </span>\n </span>\n\n <ef-textarea\n class=\"col-12\"\n [dir]=\"locale.dir\"\n [name]=\"name(locale.code, 'description')\"\n [describedByExtra]=\"counterId(locale.code, 'description')\"\n labelKey=\"ef_seo_fields_description_label\"\n [rows]=\"3\"\n [maxlength]=\"descriptionMax()\"\n [value]=\"text(locale.code, 'description')\"\n (valueChangeEvent)=\"setText(locale.code, 'description', $event)\"\n />\n <span\n class=\"col-12 seo-counter\"\n [id]=\"counterId(locale.code, 'description')\"\n [attr.data-counter]=\"locale.code + '-description'\"\n [class.is-over]=\"text(locale.code, 'description').length > descriptionAdvised()\"\n >\n <bdi dir=\"ltr\">{{ text(locale.code, 'description').length }} / {{ descriptionAdvised() }}</bdi>\n <span class=\"seo-counter__sr\">\n {{ 'ef_seo_fields_counter_hint' | translate: { max: descriptionAdvised() } }}\n @if (text(locale.code, 'description').length > descriptionAdvised()) {\n {{ 'ef_seo_fields_over_hint' | translate: { max: descriptionAdvised() } }}\n }\n </span>\n </span>\n </ef-form-grid>\n </ef-fieldset>\n }\n\n <ef-seo-preview\n labelKey=\"ef_seo_fields_preview\"\n contentLang=\"fr\"\n [pageTitle]=\"previewTitle()\"\n [url]=\"previewUrl()\"\n [description]=\"previewDescription()\"\n />\n</div>\n", styles: [":host{position:relative;display:block;min-width:0}.seo-body{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);min-width:0}.seo-note{margin:0;font-size:12px}.seo-counter{margin-top:calc(-1 * var(--s-2));font-size:12px;color:var(--text-mute);text-align:end;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.seo-counter.is-over{color:var(--st-pending-fg);font-weight:600}.seo-counter__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfFieldsetComponent, selector: "ef-fieldset", inputs: ["legend", "legendKey", "variant", "toggleable", "collapsed", "styleClass"] }, { kind: "component", type: EfFormGridComponent, selector: "ef-form-grid" }, { kind: "component", type: EfInputTextComponent, selector: "ef-input-text", inputs: ["size", "type", "maxlength", "fluid", "inline", "autocomplete", "dir", "variant", "prefix", "suffix", "value"], outputs: ["valueChangeEvent"] }, { kind: "component", type: EfTextareaComponent, selector: "ef-textarea", inputs: ["rows", "maxlength", "autofocus", "mono", "value"], outputs: ["valueChangeEvent"] }, { kind: "component", type: EfSeoPreviewComponent, selector: "ef-seo-preview", inputs: ["pageTitle", "url", "description", "labelKey", "titleMaxLength", "descriptionMaxLength", "contentLang"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14287
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.11", type: EfSeoFieldsComponent, isStandalone: true, selector: "ef-seo-fields", inputs: { seoTitle: { classPropertyName: "seoTitle", publicName: "seoTitle", isSignal: true, isRequired: false, transformFunction: null }, seoDescription: { classPropertyName: "seoDescription", publicName: "seoDescription", isSignal: true, isRequired: false, transformFunction: null }, fallbackTitle: { classPropertyName: "fallbackTitle", publicName: "fallbackTitle", isSignal: true, isRequired: false, transformFunction: null }, fallbackDescription: { classPropertyName: "fallbackDescription", publicName: "fallbackDescription", isSignal: true, isRequired: false, transformFunction: null }, previewUrl: { classPropertyName: "previewUrl", publicName: "previewUrl", isSignal: true, isRequired: false, transformFunction: null }, titleMax: { classPropertyName: "titleMax", publicName: "titleMax", isSignal: true, isRequired: false, transformFunction: null }, descriptionMax: { classPropertyName: "descriptionMax", publicName: "descriptionMax", isSignal: true, isRequired: false, transformFunction: null }, titleAdvised: { classPropertyName: "titleAdvised", publicName: "titleAdvised", isSignal: true, isRequired: false, transformFunction: null }, descriptionAdvised: { classPropertyName: "descriptionAdvised", publicName: "descriptionAdvised", isSignal: true, isRequired: false, transformFunction: null }, cultures: { classPropertyName: "cultures", publicName: "cultures", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { seoChange: "seoChange" }, ngImport: i0, template: "<div class=\"seo-body\">\n <p class=\"seo-note text-mute\">{{ 'ef_seo_fields_fallback_hint' | translate }}</p>\n\n <ef-localized-input\n name=\"seoTitle\"\n labelKey=\"ef_seo_fields_title_label\"\n [value]=\"seoTitle()\"\n [cultures]=\"cultures()\"\n [maxLength]=\"titleMax()\"\n [advisedLength]=\"titleAdvised()\"\n counterHintKey=\"ef_seo_fields_counter_hint\"\n counterOverKey=\"ef_seo_fields_over_hint\"\n (valueChangeEvent)=\"seoChange.emit({ seoTitle: $event })\"\n />\n\n <ef-localized-input\n name=\"seoDescription\"\n mode=\"textarea\"\n [rows]=\"3\"\n labelKey=\"ef_seo_fields_description_label\"\n [value]=\"seoDescription()\"\n [cultures]=\"cultures()\"\n [maxLength]=\"descriptionMax()\"\n [advisedLength]=\"descriptionAdvised()\"\n counterHintKey=\"ef_seo_fields_counter_hint\"\n counterOverKey=\"ef_seo_fields_over_hint\"\n (valueChangeEvent)=\"seoChange.emit({ seoDescription: $event })\"\n />\n\n <ef-seo-preview\n labelKey=\"ef_seo_fields_preview\"\n [contentLang]=\"previewCode()\"\n [pageTitle]=\"previewTitle()\"\n [url]=\"previewUrl()\"\n [description]=\"previewDescription()\"\n />\n</div>\n", styles: [":host{display:block;min-width:0}.seo-body{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);min-width:0}.seo-note{margin:0;font-size:12px}\n"], dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "component", type: EfLocalizedInputComponent, selector: "ef-localized-input", inputs: ["name", "value", "mode", "rows", "maxLength", "advisedLength", "required", "readOnlyCultures", "labelKey", "placeholderKey", "hintKey", "counterHintKey", "counterOverKey", "variant", "disabled", "errors", "cultures", "activeCulture", "showTabs", "linkedInputs", "preview", "tabBadges"], outputs: ["activeCultureChange", "valueChangeEvent"] }, { kind: "component", type: EfSeoPreviewComponent, selector: "ef-seo-preview", inputs: ["pageTitle", "url", "description", "labelKey", "titleMaxLength", "descriptionMaxLength", "contentLang"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
13528
14288
  }
13529
14289
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: EfSeoFieldsComponent, decorators: [{
13530
14290
  type: Component,
13531
- args: [{ selector: 'ef-seo-fields', standalone: true, imports: [TranslateModule, EfFieldsetComponent, EfFormGridComponent, EfInputTextComponent, EfTextareaComponent, EfSeoPreviewComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"seo-body\">\n <p class=\"seo-note text-mute\">{{ 'ef_seo_fields_fallback_hint' | translate }}</p>\n\n @for (locale of locales(); track locale.code) {\n <ef-fieldset variant=\"plain\" [legendKey]=\"locale.labelKey\" [attr.lang]=\"locale.code\">\n <ef-form-grid>\n <ef-input-text\n class=\"col-12\"\n variant=\"comptoir\"\n [dir]=\"locale.dir\"\n [name]=\"name(locale.code, 'title')\"\n [describedByExtra]=\"counterId(locale.code, 'title')\"\n labelKey=\"ef_seo_fields_title_label\"\n [maxlength]=\"titleMax()\"\n [value]=\"text(locale.code, 'title')\"\n (valueChangeEvent)=\"setText(locale.code, 'title', $event)\"\n />\n <span\n class=\"col-12 seo-counter\"\n [id]=\"counterId(locale.code, 'title')\"\n [attr.data-counter]=\"locale.code + '-title'\"\n [class.is-over]=\"text(locale.code, 'title').length > titleAdvised()\"\n >\n <bdi dir=\"ltr\">{{ text(locale.code, 'title').length }} / {{ titleAdvised() }}</bdi>\n <span class=\"seo-counter__sr\">\n {{ 'ef_seo_fields_counter_hint' | translate: { max: titleAdvised() } }}\n @if (text(locale.code, 'title').length > titleAdvised()) {\n {{ 'ef_seo_fields_over_hint' | translate: { max: titleAdvised() } }}\n }\n </span>\n </span>\n\n <ef-textarea\n class=\"col-12\"\n [dir]=\"locale.dir\"\n [name]=\"name(locale.code, 'description')\"\n [describedByExtra]=\"counterId(locale.code, 'description')\"\n labelKey=\"ef_seo_fields_description_label\"\n [rows]=\"3\"\n [maxlength]=\"descriptionMax()\"\n [value]=\"text(locale.code, 'description')\"\n (valueChangeEvent)=\"setText(locale.code, 'description', $event)\"\n />\n <span\n class=\"col-12 seo-counter\"\n [id]=\"counterId(locale.code, 'description')\"\n [attr.data-counter]=\"locale.code + '-description'\"\n [class.is-over]=\"text(locale.code, 'description').length > descriptionAdvised()\"\n >\n <bdi dir=\"ltr\">{{ text(locale.code, 'description').length }} / {{ descriptionAdvised() }}</bdi>\n <span class=\"seo-counter__sr\">\n {{ 'ef_seo_fields_counter_hint' | translate: { max: descriptionAdvised() } }}\n @if (text(locale.code, 'description').length > descriptionAdvised()) {\n {{ 'ef_seo_fields_over_hint' | translate: { max: descriptionAdvised() } }}\n }\n </span>\n </span>\n </ef-form-grid>\n </ef-fieldset>\n }\n\n <ef-seo-preview\n labelKey=\"ef_seo_fields_preview\"\n contentLang=\"fr\"\n [pageTitle]=\"previewTitle()\"\n [url]=\"previewUrl()\"\n [description]=\"previewDescription()\"\n />\n</div>\n", styles: [":host{position:relative;display:block;min-width:0}.seo-body{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);min-width:0}.seo-note{margin:0;font-size:12px}.seo-counter{margin-top:calc(-1 * var(--s-2));font-size:12px;color:var(--text-mute);text-align:end;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.seo-counter.is-over{color:var(--st-pending-fg);font-weight:600}.seo-counter__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"] }]
13532
- }], propDecorators: { seoTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "seoTitle", required: false }] }], seoDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "seoDescription", required: false }] }], seoTitleTranslations: [{ type: i0.Input, args: [{ isSignal: true, alias: "seoTitleTranslations", required: false }] }], seoDescriptionTranslations: [{ type: i0.Input, args: [{ isSignal: true, alias: "seoDescriptionTranslations", required: false }] }], fallbackTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallbackTitle", required: false }] }], fallbackDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallbackDescription", required: false }] }], previewUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewUrl", required: false }] }], titleMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleMax", required: false }] }], descriptionMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "descriptionMax", required: false }] }], titleAdvised: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleAdvised", required: false }] }], descriptionAdvised: [{ type: i0.Input, args: [{ isSignal: true, alias: "descriptionAdvised", required: false }] }], locales: [{ type: i0.Input, args: [{ isSignal: true, alias: "locales", required: false }] }], seoChange: [{ type: i0.Output, args: ["seoChange"] }] } });
14291
+ args: [{ selector: 'ef-seo-fields', standalone: true, imports: [TranslateModule, EfLocalizedInputComponent, EfSeoPreviewComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"seo-body\">\n <p class=\"seo-note text-mute\">{{ 'ef_seo_fields_fallback_hint' | translate }}</p>\n\n <ef-localized-input\n name=\"seoTitle\"\n labelKey=\"ef_seo_fields_title_label\"\n [value]=\"seoTitle()\"\n [cultures]=\"cultures()\"\n [maxLength]=\"titleMax()\"\n [advisedLength]=\"titleAdvised()\"\n counterHintKey=\"ef_seo_fields_counter_hint\"\n counterOverKey=\"ef_seo_fields_over_hint\"\n (valueChangeEvent)=\"seoChange.emit({ seoTitle: $event })\"\n />\n\n <ef-localized-input\n name=\"seoDescription\"\n mode=\"textarea\"\n [rows]=\"3\"\n labelKey=\"ef_seo_fields_description_label\"\n [value]=\"seoDescription()\"\n [cultures]=\"cultures()\"\n [maxLength]=\"descriptionMax()\"\n [advisedLength]=\"descriptionAdvised()\"\n counterHintKey=\"ef_seo_fields_counter_hint\"\n counterOverKey=\"ef_seo_fields_over_hint\"\n (valueChangeEvent)=\"seoChange.emit({ seoDescription: $event })\"\n />\n\n <ef-seo-preview\n labelKey=\"ef_seo_fields_preview\"\n [contentLang]=\"previewCode()\"\n [pageTitle]=\"previewTitle()\"\n [url]=\"previewUrl()\"\n [description]=\"previewDescription()\"\n />\n</div>\n", styles: [":host{display:block;min-width:0}.seo-body{display:grid;grid-template-columns:minmax(0,1fr);gap:var(--s-3);min-width:0}.seo-note{margin:0;font-size:12px}\n"] }]
14292
+ }], propDecorators: { seoTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "seoTitle", required: false }] }], seoDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "seoDescription", required: false }] }], fallbackTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallbackTitle", required: false }] }], fallbackDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "fallbackDescription", required: false }] }], previewUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "previewUrl", required: false }] }], titleMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleMax", required: false }] }], descriptionMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "descriptionMax", required: false }] }], titleAdvised: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleAdvised", required: false }] }], descriptionAdvised: [{ type: i0.Input, args: [{ isSignal: true, alias: "descriptionAdvised", required: false }] }], cultures: [{ type: i0.Input, args: [{ isSignal: true, alias: "cultures", required: false }] }], seoChange: [{ type: i0.Output, args: ["seoChange"] }] } });
13533
14293
 
13534
14294
  /**
13535
14295
  * A button that opens the native file picker and emits the chosen `File`.
@@ -14439,5 +15199,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
14439
15199
  * Generated bundle index. Do not edit.
14440
15200
  */
14441
15201
 
14442
- export { EF_BADGE_BG_COLORS, EF_BADGE_BG_LIGHT_COLORS, EF_BADGE_BORDER_COLORS, EF_BADGE_DEFAULTS, EF_BADGE_TEXT_COLORS, EF_CHART_PALETTE, EF_DATATABLE_ACTIONS_DEFAULTS, EF_DATATABLE_COLUMN_DEFAULTS, EF_DATATABLE_DEFAULTS, EF_DATA_CARD_MOBILE_LAYOUT, EF_HTML_PREVIEW_DEFAULT_TITLE_KEY, EF_SEO_DEFAULT_LOCALES, EF_STATUS_COLORS, EF_STATUS_COLOR_ALIASES, EfAboutDialogComponent, EfActivationFilterComponent, EfAppMainComponent, EfAppShellComponent, EfAppShellMobileComponent, EfAppTopComponent, EfBadgeComponent, EfBlockUiComponent, EfBlockableDivComponent, EfBottomSheetComponent, EfBuildStampComponent, EfBulkBarComponent, EfButtonComponent, EfButtonGroupComponent, EfCardComponent, EfChangeHistoryComponent, EfChangePasswordComponent, EfChartComponent, EfCheckboxComponent, EfClearButtonComponent, EfColumnHeaderTemplateDirective, EfColumnTemplateDirective, EfCompactPipe, EfConfirmDialogComponent, EfCurrencyPipe, EfDataCardComponent, EfDatatableActionBarComponent, EfDatatableComponent, EfDatepickerAdvancedComponent, EfDatepickerComponent, EfDetailToolbarComponent, EfDialogComponent, EfEmailMetricsCardComponent, EfEmptyStateComponent, EfFabComponent, EfFieldsetComponent, EfFileUploadComponent, EfFilterDrawerComponent, EfFilterPillComponent, EfFormGridComponent, EfHtmlPreviewComponent, EfIdentityThumbComponent, EfInputNumberComponent, EfInputTextComponent, EfKpiCardComponent, EfLabelComponent, EfModuleRailComponent, EfModuleSideComponent, EfMultiSelectComponent, EfOrderBuilderComponent, EfOrderSummaryComponent, EfOverflowTooltipDirective, EfPagerComponent, EfPasswordComponent, EfPdfPreviewComponent, EfPillGroupComponent, EfPresetPillComponent, EfPriceDisplayComponent, EfProductCatalogueComponent, EfProductCatalogueFilterComponent, EfProductPickerComponent, EfProductTypeaheadComponent, EfProfileChipComponent, EfProfileMenuComponent, EfPulseComponent, EfQuantitySelectorComponent, EfQuantityStepperComponent, EfRadioComponent, EfRankListComponent, EfReadOnlyDirective, EfRowActionsComponent, EfSearchToolbarComponent, EfSelectButtonComponent, EfSelectComponent, EfSeoFieldsComponent, EfSeoPreviewComponent, EfServerErrorsDirective, EfSessionIdleDialogComponent, EfShortcutsDialogComponent, EfSkeletonComponent, EfSmartBarComponent, EfStatsComponent, EfStatusChipComponent, EfTabPanelDirective, EfTabsComponent, EfTextareaComponent, EfThemeConfiguratorComponent, EfThemeToggleComponent, EfTimelineComponent, EfToastRegionComponent, EfToggleSwitchComponent, EfToolbarComponent, EfTooltipDirective, EfTotalsComponent, OrderEntityHelper, OrderLineItemHelper, TruncatePipe, buildChartConfig, defaultUnitPrice, getColumnAlignment, mergeColumnDefaults, resolveEfStatusColor };
15202
+ export { AbstractEfFormControl, EF_BADGE_BG_COLORS, EF_BADGE_BG_LIGHT_COLORS, EF_BADGE_BORDER_COLORS, EF_BADGE_DEFAULTS, EF_BADGE_TEXT_COLORS, EF_CHART_PALETTE, EF_DATATABLE_ACTIONS_DEFAULTS, EF_DATATABLE_COLUMN_DEFAULTS, EF_DATATABLE_DEFAULTS, EF_DATA_CARD_MOBILE_LAYOUT, EF_HTML_PREVIEW_DEFAULT_TITLE_KEY, EF_RICH_TEXT, EF_SEO_DEFAULT_LOCALES, EF_STATUS_COLORS, EF_STATUS_COLOR_ALIASES, EfAboutDialogComponent, EfActivationFilterComponent, EfAppMainComponent, EfAppShellComponent, EfAppShellMobileComponent, EfAppTopComponent, EfBadgeComponent, EfBlockUiComponent, EfBlockableDivComponent, EfBottomSheetComponent, EfBuildStampComponent, EfBulkBarComponent, EfButtonComponent, EfButtonGroupComponent, EfCardComponent, EfChangeHistoryComponent, EfChangePasswordComponent, EfChartComponent, EfCheckboxComponent, EfClearButtonComponent, EfColumnHeaderTemplateDirective, EfColumnTemplateDirective, EfCompactPipe, EfConfirmDialogComponent, EfCurrencyPipe, EfDataCardComponent, EfDatatableActionBarComponent, EfDatatableComponent, EfDatepickerAdvancedComponent, EfDatepickerComponent, EfDetailToolbarComponent, EfDialogComponent, EfEmailMetricsCardComponent, EfEmptyStateComponent, EfFabComponent, EfFieldsetComponent, EfFileUploadComponent, EfFilterDrawerComponent, EfFilterPillComponent, EfFormGridComponent, EfHtmlPreviewComponent, EfIdentityThumbComponent, EfInputNumberComponent, EfInputTextComponent, EfKpiCardComponent, EfLabelComponent, EfLocalizedInputComponent, EfModuleRailComponent, EfModuleSideComponent, EfMultiSelectComponent, EfOrderBuilderComponent, EfOrderSummaryComponent, EfOverflowTooltipDirective, EfPagerComponent, EfPasswordComponent, EfPdfPreviewComponent, EfPillGroupComponent, EfPresetPillComponent, EfPriceDisplayComponent, EfProductCatalogueComponent, EfProductCatalogueFilterComponent, EfProductPickerComponent, EfProductTypeaheadComponent, EfProfileChipComponent, EfProfileMenuComponent, EfPulseComponent, EfQuantitySelectorComponent, EfQuantityStepperComponent, EfRadioComponent, EfRankListComponent, EfReadOnlyDirective, EfRowActionsComponent, EfSearchToolbarComponent, EfSelectButtonComponent, EfSelectComponent, EfSeoFieldsComponent, EfSeoPreviewComponent, EfServerErrorsDirective, EfSessionIdleDialogComponent, EfShortcutsDialogComponent, EfSkeletonComponent, EfSmartBarComponent, EfStatsComponent, EfStatusChipComponent, EfTabPanelDirective, EfTabsComponent, EfTextareaComponent, EfThemeConfiguratorComponent, EfThemeToggleComponent, EfTimelineComponent, EfToastRegionComponent, EfToggleSwitchComponent, EfToolbarComponent, EfTooltipDirective, EfTotalsComponent, OrderEntityHelper, OrderLineItemHelper, TruncatePipe, auditEntityKeySegment, buildChartConfig, defaultUnitPrice, getColumnAlignment, mergeColumnDefaults, plainTextLength, resolveEfStatusColor };
14443
15203
  //# sourceMappingURL=elasticias-ui.mjs.map