@nexterajs/ui 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +279 -34
  2. package/fesm2022/nexterajs-ui-charts.mjs +1096 -0
  3. package/fesm2022/nexterajs-ui-charts.mjs.map +1 -0
  4. package/fesm2022/nexterajs-ui-core.mjs +652 -12
  5. package/fesm2022/nexterajs-ui-core.mjs.map +1 -1
  6. package/fesm2022/nexterajs-ui-display.mjs +1142 -0
  7. package/fesm2022/nexterajs-ui-display.mjs.map +1 -0
  8. package/fesm2022/nexterajs-ui-dropdowns.mjs +321 -33
  9. package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -1
  10. package/fesm2022/nexterajs-ui-file-uploader.mjs +572 -10
  11. package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -1
  12. package/fesm2022/nexterajs-ui-forms.mjs +2027 -136
  13. package/fesm2022/nexterajs-ui-forms.mjs.map +1 -1
  14. package/fesm2022/nexterajs-ui-grid.mjs +1222 -188
  15. package/fesm2022/nexterajs-ui-grid.mjs.map +1 -1
  16. package/fesm2022/nexterajs-ui-intl.mjs +103 -11
  17. package/fesm2022/nexterajs-ui-intl.mjs.map +1 -1
  18. package/fesm2022/nexterajs-ui-navigation.mjs +1054 -0
  19. package/fesm2022/nexterajs-ui-navigation.mjs.map +1 -0
  20. package/fesm2022/nexterajs-ui-notify.mjs +46 -44
  21. package/fesm2022/nexterajs-ui-notify.mjs.map +1 -1
  22. package/fesm2022/nexterajs-ui-overlay.mjs +1671 -0
  23. package/fesm2022/nexterajs-ui-overlay.mjs.map +1 -0
  24. package/fesm2022/nexterajs-ui-pivot.mjs +51 -26
  25. package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -1
  26. package/fesm2022/nexterajs-ui-shell.mjs +542 -0
  27. package/fesm2022/nexterajs-ui-shell.mjs.map +1 -0
  28. package/fesm2022/nexterajs-ui-tree-view.mjs +5 -2
  29. package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -1
  30. package/fesm2022/nexterajs-ui-validation.mjs +42 -11
  31. package/fesm2022/nexterajs-ui-validation.mjs.map +1 -1
  32. package/fesm2022/nexterajs-ui.mjs +5 -0
  33. package/fesm2022/nexterajs-ui.mjs.map +1 -1
  34. package/package.json +33 -2
  35. package/styles/_dropdown.scss +5 -5
  36. package/styles/_editor.scss +52 -17
  37. package/styles/_mixins.scss +1 -1
  38. package/styles/_scrollbar.scss +5 -5
  39. package/styles/_status.scss +21 -0
  40. package/styles/_toast.scss +14 -14
  41. package/styles/_tokens.scss +420 -3
  42. package/styles/_widget.scss +14 -3
  43. package/styles/index.scss +6 -4
  44. package/types/nexterajs-ui-charts.d.ts +446 -0
  45. package/types/nexterajs-ui-core.d.ts +387 -22
  46. package/types/nexterajs-ui-display.d.ts +720 -0
  47. package/types/nexterajs-ui-dropdowns.d.ts +191 -48
  48. package/types/nexterajs-ui-file-uploader.d.ts +236 -7
  49. package/types/nexterajs-ui-forms.d.ts +675 -30
  50. package/types/nexterajs-ui-grid.d.ts +448 -93
  51. package/types/nexterajs-ui-intl.d.ts +39 -4
  52. package/types/nexterajs-ui-navigation.d.ts +531 -0
  53. package/types/nexterajs-ui-notify.d.ts +4 -2
  54. package/types/nexterajs-ui-overlay.d.ts +703 -0
  55. package/types/nexterajs-ui-pivot.d.ts +56 -48
  56. package/types/nexterajs-ui-shell.d.ts +294 -0
  57. package/types/nexterajs-ui-tree-view.d.ts +6 -3
  58. package/types/nexterajs-ui-validation.d.ts +20 -11
  59. package/types/nexterajs-ui.d.ts +5 -0
@@ -1,13 +1,18 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, input, booleanAttribute, computed, output, ViewEncapsulation, ChangeDetectionStrategy, Component, signal, viewChild, linkedSignal, forwardRef, ViewContainerRef } from '@angular/core';
3
- import { NxeValidationGroupRegistry, NXE_VALIDATION_GROUP, NxeFormControlBase, nxeOptionalBoolean, NXE_VALIDATION_TARGET } from '@nexterajs/ui/core';
4
- import { NG_VALUE_ACCESSOR } from '@angular/forms';
5
- import { parseNumberFormat, NXE_NUMBER_SEPARATORS, formatNumber, parseNumber, roundHalfAwayFromZero, parseDateValue, NXE_DEFAULT_DISPLAY_FORMAT, NXE_DEFAULT_DATETIME_DISPLAY_FORMAT, NXE_DEFAULT_TIME_DISPLAY_FORMAT, formatDisplay, buildCalendarWeeks, buildPeriodCells, parseDisplay, serializeDate } from '@nexterajs/ui/intl';
2
+ import { inject, ElementRef, input, booleanAttribute, computed, output, ViewEncapsulation, ChangeDetectionStrategy, Component, linkedSignal, signal, viewChild, forwardRef, numberAttribute, afterRenderEffect, DestroyRef, ViewContainerRef, InjectionToken, Injector, afterNextRender, makeEnvironmentProviders, contentChild, contentChildren, TemplateRef, Directive } from '@angular/core';
3
+ import { NxeValidationGroupRegistry, NXE_VALIDATION_GROUP, NxeFormControlBase, nxeOptionalBoolean, NXE_VALIDATION_TARGET, NxeMessageService, nxeFormatFileSize, nxeResolveExpr, nxeResolveText } from '@nexterajs/ui/core';
4
+ import { NG_VALUE_ACCESSOR, ControlContainer, NgControl, FormSubmittedEvent, FormResetEvent, Validators } from '@angular/forms';
5
+ import { nxeCurrencyCode, parseNumberFormat, nxeCurrencyNumberFormat, NXE_NUMBER_SEPARATORS, formatCurrency, formatNumber, parseNumber, roundHalfAwayFromZero, parseDateValue, NXE_DEFAULT_DISPLAY_FORMAT, NXE_DEFAULT_DATETIME_DISPLAY_FORMAT, NXE_DEFAULT_TIME_DISPLAY_FORMAT, formatDisplay, buildCalendarWeeks, buildPeriodCells, parseDisplay, serializeDate } from '@nexterajs/ui/intl';
6
6
  import { Overlay } from '@angular/cdk/overlay';
7
7
  import { TemplatePortal } from '@angular/cdk/portal';
8
- import { DateTime } from 'luxon';
9
- import { merge, filter } from 'rxjs';
8
+ import { DateTime, Info } from 'luxon';
9
+ import { merge, filter, map } from 'rxjs';
10
10
  import { nxePane, trackPaneFlip } from '@nexterajs/ui/styling';
11
+ import { CdkTrapFocus } from '@angular/cdk/a11y';
12
+ import { BreakpointObserver } from '@angular/cdk/layout';
13
+ import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
14
+ import { FormField } from '@angular/forms/signals';
15
+ import { NgTemplateOutlet } from '@angular/common';
11
16
 
12
17
  /**
13
18
  * dx size coercion: `height="35"` and `[height]="35"` both mean 35px, while `'100%'` or `'3rem'`
@@ -31,6 +36,10 @@ function cssSize(value) {
31
36
  * Content is projected (`<nxe-icon> + <span>`, the dominant pattern). The legacy `text`/`type`
32
37
  * inputs render a `.nxe-button-text` label for the ~25 attribute-styled buttons.
33
38
  *
39
+ * `stylingMode`, `size`, `loading` and `block` are additive: with none of them set the host renders
40
+ * exactly as before. An icon can be projected into the leading slot as
41
+ * `<svg nxeButtonIcon ...>` (or `[nxeIcon]`), placed before the label.
42
+ *
34
43
  * `onClick` emits {@link NxeButtonClickEvent}; native `(click)` also fires (27 sites bind it).
35
44
  */
36
45
  class NxeButtonComponent {
@@ -43,6 +52,19 @@ class NxeButtonComponent {
43
52
  /** Style intent (`default`=blue, `success`=green, `danger`=red, `normal`=white). */
44
53
  type = input('normal', /* @ts-ignore */
45
54
  ...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
55
+ /** Fill treatment: `contained` (default), `outlined` (transparent with a coloured border) or `text` (no border). */
56
+ stylingMode = input('contained', /* @ts-ignore */
57
+ ...(ngDevMode ? [{ debugName: "stylingMode" }] : /* istanbul ignore next */ []));
58
+ /** Scale: `sm`, `md` (default, the legacy size) or `lg`. */
59
+ size = input('md', /* @ts-ignore */
60
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
61
+ /**
62
+ * In-flight state: shows a spinner over the (hidden but still laid out, so the width holds)
63
+ * content, sets `aria-busy="true"` and swallows clicks and Enter/Space until it clears.
64
+ */
65
+ loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
66
+ /** Full-width button (`display: flex; width: 100%`). An explicit `width` still wins. */
67
+ block = input(false, { ...(ngDevMode ? { debugName: "block" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
46
68
  /** Disabled state (`[disabled]`); blocks `onClick` and dims the button. */
47
69
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
48
70
  /** Named validation group resolved via {@link NxeValidationGroupRegistry} on click. */
@@ -75,7 +97,7 @@ class NxeButtonComponent {
75
97
  this.handleClick(event);
76
98
  }
77
99
  handleClick(event) {
78
- if (this.disabled()) {
100
+ if (this.disabled() || this.loading()) {
79
101
  event.preventDefault();
80
102
  event.stopImmediatePropagation();
81
103
  return;
@@ -92,17 +114,21 @@ class NxeButtonComponent {
92
114
  });
93
115
  }
94
116
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
95
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeButtonComponent, isStandalone: true, selector: "nxe-button", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, validationGroup: { classPropertyName: "validationGroup", publicName: "validationGroup", isSignal: true, isRequired: false, transformFunction: null }, focusStateEnabled: { classPropertyName: "focusStateEnabled", publicName: "focusStateEnabled", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClick: "onClick" }, host: { attributes: { "role": "button" }, listeners: { "click": "handleClick($event)", "keydown.enter": "handleKeydown($event)", "keydown.space": "handleKeydown($event)" }, properties: { "attr.tabindex": "disabled() || !focusStateEnabled() ? null : 0", "attr.aria-disabled": "disabled() ? true : null", "class.nxe-button-default": "type() === 'default'", "class.nxe-button-success": "type() === 'success'", "class.nxe-button-danger": "type() === 'danger'", "class.nxe-button-has-text": "!!text()", "class.nxe-button-has-icon": "!!icon()", "style.height": "heightStyle()", "style.width": "widthStyle()", "class.nxe-disabled": "disabled()" }, classAttribute: "nxe-widget" }, ngImport: i0, template: `
117
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeButtonComponent, isStandalone: true, selector: "nxe-button", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, stylingMode: { classPropertyName: "stylingMode", publicName: "stylingMode", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, block: { classPropertyName: "block", publicName: "block", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, validationGroup: { classPropertyName: "validationGroup", publicName: "validationGroup", isSignal: true, isRequired: false, transformFunction: null }, focusStateEnabled: { classPropertyName: "focusStateEnabled", publicName: "focusStateEnabled", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClick: "onClick" }, host: { attributes: { "role": "button" }, listeners: { "click": "handleClick($event)", "keydown.enter": "handleKeydown($event)", "keydown.space": "handleKeydown($event)" }, properties: { "attr.tabindex": "disabled() || !focusStateEnabled() ? null : 0", "attr.aria-disabled": "disabled() || loading() ? true : null", "attr.aria-busy": "loading() ? true : null", "class.nxe-button-mode-outlined": "stylingMode() === 'outlined'", "class.nxe-button-mode-text": "stylingMode() === 'text'", "class.nxe-button-sm": "size() === 'sm'", "class.nxe-button-lg": "size() === 'lg'", "class.nxe-button-block": "block()", "class.nxe-button-loading": "loading()", "class.nxe-button-default": "type() === 'default'", "class.nxe-button-success": "type() === 'success'", "class.nxe-button-danger": "type() === 'danger'", "class.nxe-button-has-text": "!!text()", "class.nxe-button-has-icon": "!!icon()", "style.height": "heightStyle()", "style.width": "widthStyle()", "class.nxe-disabled": "disabled()" }, classAttribute: "nxe-widget" }, ngImport: i0, template: `
96
118
  <span class="nxe-button-content">
97
119
  @if (icon()) {
98
120
  <i class="nxe-icon nxe-icon-{{ icon() }}"></i>
99
121
  }
122
+ <ng-content select="[nxeButtonIcon], [nxeIcon]" />
100
123
  <ng-content />
101
124
  @if (text()) {
102
125
  <span class="nxe-button-text">{{ text() }}</span>
103
126
  }
104
127
  </span>
105
- `, isInline: true, styles: ["nxe-button{display:inline-flex;box-sizing:border-box;cursor:pointer;max-width:100%;overflow:hidden;text-align:center;vertical-align:middle;-webkit-user-select:none;user-select:none;border:1px solid #ddd;border-radius:4px;background-color:#fff;color:#333;font-weight:400;line-height:1.35715}nxe-button:focus{outline:none}nxe-button .nxe-button-content{flex:1 1 auto;display:flex;align-items:center;justify-content:center;gap:var(--nxe-ui-button-gap, 0px);line-height:1.35715;padding:8px}nxe-button.nxe-button-has-text .nxe-button-content{padding-inline:12px}nxe-button.nxe-button-has-text .nxe-button-content>i.nxe-icon{margin-inline-start:0;margin-inline-end:calc(9px - var(--nxe-ui-button-gap, 0px))}nxe-button .nxe-button-text{line-height:1.2858;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-button:hover{background-color:#f4f4f4}nxe-button:active{background-color:#c2c2c2}nxe-button.nxe-button-default{background-color:#337ab7;border-color:transparent;color:#fff}nxe-button.nxe-button-default:hover{background-color:#2d6ca3}nxe-button.nxe-button-default:active{background-color:#19405b}nxe-button.nxe-button-success{background-color:#5cb85c;border-color:transparent;color:#fff}nxe-button.nxe-button-success:hover{background-color:#46a046}nxe-button.nxe-button-success:active{background-color:#306e30}nxe-button.nxe-button-danger{background-color:#d9534f;border-color:transparent;color:#fff}nxe-button.nxe-button-danger:hover{background-color:#cd312c}nxe-button.nxe-button-danger:active{background-color:#932320}nxe-button[disabled]{opacity:.5;cursor:default}nxe-button.nxe-disabled{opacity:.5;cursor:default;pointer-events:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
128
+ @if (loading()) {
129
+ <span class="nxe-button-spinner" aria-hidden="true"></span>
130
+ }
131
+ `, isInline: true, styles: ["nxe-button{display:inline-flex;box-sizing:border-box;cursor:pointer;max-width:100%;overflow:hidden;text-align:center;vertical-align:middle;-webkit-user-select:none;user-select:none;border:1px solid var(--nxe-ui-button-border-secondary, var(--nxe-ui-border, #ddd));border-radius:var(--nxe-ui-radius, 4px);background-color:var(--nxe-ui-button-bg-secondary, var(--nxe-ui-surface, #fff));color:var(--nxe-ui-button-fg-secondary, var(--nxe-ui-text, #333));font-weight:400;line-height:1.35715}nxe-button:focus{outline:none}nxe-button:where(:focus-visible){box-shadow:var(--nxe-ui-button-focus-ring, none)}nxe-button .nxe-button-content{box-sizing:border-box;flex:1 1 auto;height:100%;max-height:100%;display:flex;align-items:center;justify-content:center;gap:var(--nxe-ui-button-gap, 0px);line-height:1.35715;padding:var(--nxe-ui-button-padding, 8px)}nxe-button.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-button-padding-inline, 12px)}nxe-button.nxe-button-has-text .nxe-button-content>i.nxe-icon{margin-inline-start:0;margin-inline-end:calc(9px - var(--nxe-ui-button-gap, 0px))}nxe-button .nxe-button-text{line-height:1.2858;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-button:hover{background-color:var(--nxe-ui-button-hover-bg-secondary, #f4f4f4)}nxe-button:active{background-color:var(--nxe-ui-button-active-bg-secondary, #c2c2c2)}nxe-button.nxe-button-default{background-color:var(--nxe-ui-button-bg-primary, var(--nxe-ui-accent, #337ab7));border-color:var(--nxe-ui-button-border-primary, transparent);color:var(--nxe-ui-button-fg-primary, var(--nxe-ui-accent-contrast, #fff))}nxe-button.nxe-button-default:hover{background-color:var(--nxe-ui-button-hover-bg-primary, #2d6ca3)}nxe-button.nxe-button-default:active{background-color:var(--nxe-ui-button-active-bg-primary, #19405b)}nxe-button.nxe-button-success{background-color:var(--nxe-ui-button-bg-success, var(--nxe-ui-success, #5cb85c));border-color:var(--nxe-ui-button-border-success, transparent);color:var(--nxe-ui-button-fg-success, #fff)}nxe-button.nxe-button-success:hover{background-color:var(--nxe-ui-button-hover-bg-success, #46a046)}nxe-button.nxe-button-success:active{background-color:var(--nxe-ui-button-active-bg-success, #306e30)}nxe-button.nxe-button-danger{background-color:var(--nxe-ui-button-bg-danger, var(--nxe-ui-danger, #d9534f));border-color:var(--nxe-ui-button-border-danger, transparent);color:var(--nxe-ui-button-fg-danger, #fff)}nxe-button.nxe-button-danger:hover{background-color:var(--nxe-ui-button-hover-bg-danger, #cd312c)}nxe-button.nxe-button-danger:active{background-color:var(--nxe-ui-button-active-bg-danger, #932320)}nxe-button[disabled]{opacity:var(--nxe-ui-disabled-opacity, .5);cursor:default}nxe-button.nxe-disabled{opacity:var(--nxe-ui-disabled-opacity, .5);cursor:default;pointer-events:none}nxe-button .nxe-button-content>[nxeButtonIcon],nxe-button .nxe-button-content>[nxeIcon]{flex:none;width:1.25em;height:1.25em}nxe-button .nxe-button-content>[nxeButtonIcon]:not(:last-child),nxe-button .nxe-button-content>[nxeIcon]:not(:last-child){margin-inline-end:var(--nxe-ui-space-2, 8px)}nxe-button.nxe-button-sm{font-size:var(--nxe-ui-font-size-sm, 13px)}nxe-button.nxe-button-sm .nxe-button-content{padding:var(--nxe-ui-space-1, 4px)}nxe-button.nxe-button-sm.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-space-2, 8px)}nxe-button.nxe-button-lg{font-size:var(--nxe-ui-font-size-lg, 16px)}nxe-button.nxe-button-lg .nxe-button-content{padding:var(--nxe-ui-space-3, 12px)}nxe-button.nxe-button-lg.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-space-5, 20px)}nxe-button.nxe-button-block{display:flex;width:100%}nxe-button.nxe-button-mode-outlined,nxe-button.nxe-button-mode-text{--nxe-button-tone: var(--nxe-ui-button-fg-secondary, var(--nxe-ui-text, #333));background-color:transparent;color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-outlined:hover,nxe-button.nxe-button-mode-text:hover{background-color:color-mix(in srgb,var(--nxe-button-tone) 8%,transparent)}nxe-button.nxe-button-mode-outlined:active,nxe-button.nxe-button-mode-text:active{background-color:color-mix(in srgb,var(--nxe-button-tone) 16%,transparent)}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-default{--nxe-button-tone: var(--nxe-ui-button-bg-primary, var(--nxe-ui-accent, #337ab7))}nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-success{--nxe-button-tone: var(--nxe-ui-button-bg-success, var(--nxe-ui-success, #5cb85c))}nxe-button.nxe-button-mode-outlined.nxe-button-danger,nxe-button.nxe-button-mode-text.nxe-button-danger{--nxe-button-tone: var(--nxe-ui-button-bg-danger, var(--nxe-ui-danger, #d9534f))}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-outlined.nxe-button-danger,nxe-button.nxe-button-mode-text.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-danger{background-color:transparent;color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-outlined.nxe-button-default:hover,nxe-button.nxe-button-mode-outlined.nxe-button-success:hover,nxe-button.nxe-button-mode-outlined.nxe-button-danger:hover,nxe-button.nxe-button-mode-text.nxe-button-default:hover,nxe-button.nxe-button-mode-text.nxe-button-success:hover,nxe-button.nxe-button-mode-text.nxe-button-danger:hover{background-color:color-mix(in srgb,var(--nxe-button-tone) 8%,transparent)}nxe-button.nxe-button-mode-outlined.nxe-button-default:active,nxe-button.nxe-button-mode-outlined.nxe-button-success:active,nxe-button.nxe-button-mode-outlined.nxe-button-danger:active,nxe-button.nxe-button-mode-text.nxe-button-default:active,nxe-button.nxe-button-mode-text.nxe-button-success:active,nxe-button.nxe-button-mode-text.nxe-button-danger:active{background-color:color-mix(in srgb,var(--nxe-button-tone) 16%,transparent)}nxe-button.nxe-button-mode-outlined{border-color:var(--nxe-ui-button-border-secondary, var(--nxe-ui-border, #ddd))}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-outlined.nxe-button-danger{border-color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-text,nxe-button.nxe-button-mode-text.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-danger{border-color:transparent}nxe-button.nxe-button-loading{position:relative;cursor:progress}nxe-button.nxe-button-loading .nxe-button-content{visibility:hidden}nxe-button .nxe-button-spinner{position:absolute;inset:0;margin:auto;box-sizing:border-box;width:1.25em;height:1.25em;border:2px solid currentColor;border-inline-end-color:transparent;border-radius:50%;animation:nxe-button-spin .7s linear infinite}@keyframes nxe-button-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){nxe-button .nxe-button-spinner{animation-duration:2s}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
106
132
  }
107
133
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeButtonComponent, decorators: [{
108
134
  type: Component,
@@ -111,16 +137,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
111
137
  @if (icon()) {
112
138
  <i class="nxe-icon nxe-icon-{{ icon() }}"></i>
113
139
  }
140
+ <ng-content select="[nxeButtonIcon], [nxeIcon]" />
114
141
  <ng-content />
115
142
  @if (text()) {
116
143
  <span class="nxe-button-text">{{ text() }}</span>
117
144
  }
118
145
  </span>
146
+ @if (loading()) {
147
+ <span class="nxe-button-spinner" aria-hidden="true"></span>
148
+ }
119
149
  `, host: {
120
150
  class: 'nxe-widget',
121
151
  role: 'button',
122
152
  '[attr.tabindex]': 'disabled() || !focusStateEnabled() ? null : 0',
123
- '[attr.aria-disabled]': 'disabled() ? true : null',
153
+ '[attr.aria-disabled]': 'disabled() || loading() ? true : null',
154
+ '[attr.aria-busy]': 'loading() ? true : null',
155
+ '[class.nxe-button-mode-outlined]': "stylingMode() === 'outlined'",
156
+ '[class.nxe-button-mode-text]': "stylingMode() === 'text'",
157
+ '[class.nxe-button-sm]': "size() === 'sm'",
158
+ '[class.nxe-button-lg]': "size() === 'lg'",
159
+ '[class.nxe-button-block]': 'block()',
160
+ '[class.nxe-button-loading]': 'loading()',
124
161
  '[class.nxe-button-default]': "type() === 'default'",
125
162
  '[class.nxe-button-success]': "type() === 'success'",
126
163
  '[class.nxe-button-danger]': "type() === 'danger'",
@@ -132,8 +169,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
132
169
  '(click)': 'handleClick($event)',
133
170
  '(keydown.enter)': 'handleKeydown($event)',
134
171
  '(keydown.space)': 'handleKeydown($event)',
135
- }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-button{display:inline-flex;box-sizing:border-box;cursor:pointer;max-width:100%;overflow:hidden;text-align:center;vertical-align:middle;-webkit-user-select:none;user-select:none;border:1px solid #ddd;border-radius:4px;background-color:#fff;color:#333;font-weight:400;line-height:1.35715}nxe-button:focus{outline:none}nxe-button .nxe-button-content{flex:1 1 auto;display:flex;align-items:center;justify-content:center;gap:var(--nxe-ui-button-gap, 0px);line-height:1.35715;padding:8px}nxe-button.nxe-button-has-text .nxe-button-content{padding-inline:12px}nxe-button.nxe-button-has-text .nxe-button-content>i.nxe-icon{margin-inline-start:0;margin-inline-end:calc(9px - var(--nxe-ui-button-gap, 0px))}nxe-button .nxe-button-text{line-height:1.2858;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-button:hover{background-color:#f4f4f4}nxe-button:active{background-color:#c2c2c2}nxe-button.nxe-button-default{background-color:#337ab7;border-color:transparent;color:#fff}nxe-button.nxe-button-default:hover{background-color:#2d6ca3}nxe-button.nxe-button-default:active{background-color:#19405b}nxe-button.nxe-button-success{background-color:#5cb85c;border-color:transparent;color:#fff}nxe-button.nxe-button-success:hover{background-color:#46a046}nxe-button.nxe-button-success:active{background-color:#306e30}nxe-button.nxe-button-danger{background-color:#d9534f;border-color:transparent;color:#fff}nxe-button.nxe-button-danger:hover{background-color:#cd312c}nxe-button.nxe-button-danger:active{background-color:#932320}nxe-button[disabled]{opacity:.5;cursor:default}nxe-button.nxe-disabled{opacity:.5;cursor:default;pointer-events:none}\n"] }]
136
- }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], validationGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationGroup", required: false }] }], focusStateEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusStateEnabled", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], onClick: [{ type: i0.Output, args: ["onClick"] }] } });
172
+ }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-button{display:inline-flex;box-sizing:border-box;cursor:pointer;max-width:100%;overflow:hidden;text-align:center;vertical-align:middle;-webkit-user-select:none;user-select:none;border:1px solid var(--nxe-ui-button-border-secondary, var(--nxe-ui-border, #ddd));border-radius:var(--nxe-ui-radius, 4px);background-color:var(--nxe-ui-button-bg-secondary, var(--nxe-ui-surface, #fff));color:var(--nxe-ui-button-fg-secondary, var(--nxe-ui-text, #333));font-weight:400;line-height:1.35715}nxe-button:focus{outline:none}nxe-button:where(:focus-visible){box-shadow:var(--nxe-ui-button-focus-ring, none)}nxe-button .nxe-button-content{box-sizing:border-box;flex:1 1 auto;height:100%;max-height:100%;display:flex;align-items:center;justify-content:center;gap:var(--nxe-ui-button-gap, 0px);line-height:1.35715;padding:var(--nxe-ui-button-padding, 8px)}nxe-button.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-button-padding-inline, 12px)}nxe-button.nxe-button-has-text .nxe-button-content>i.nxe-icon{margin-inline-start:0;margin-inline-end:calc(9px - var(--nxe-ui-button-gap, 0px))}nxe-button .nxe-button-text{line-height:1.2858;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}nxe-button:hover{background-color:var(--nxe-ui-button-hover-bg-secondary, #f4f4f4)}nxe-button:active{background-color:var(--nxe-ui-button-active-bg-secondary, #c2c2c2)}nxe-button.nxe-button-default{background-color:var(--nxe-ui-button-bg-primary, var(--nxe-ui-accent, #337ab7));border-color:var(--nxe-ui-button-border-primary, transparent);color:var(--nxe-ui-button-fg-primary, var(--nxe-ui-accent-contrast, #fff))}nxe-button.nxe-button-default:hover{background-color:var(--nxe-ui-button-hover-bg-primary, #2d6ca3)}nxe-button.nxe-button-default:active{background-color:var(--nxe-ui-button-active-bg-primary, #19405b)}nxe-button.nxe-button-success{background-color:var(--nxe-ui-button-bg-success, var(--nxe-ui-success, #5cb85c));border-color:var(--nxe-ui-button-border-success, transparent);color:var(--nxe-ui-button-fg-success, #fff)}nxe-button.nxe-button-success:hover{background-color:var(--nxe-ui-button-hover-bg-success, #46a046)}nxe-button.nxe-button-success:active{background-color:var(--nxe-ui-button-active-bg-success, #306e30)}nxe-button.nxe-button-danger{background-color:var(--nxe-ui-button-bg-danger, var(--nxe-ui-danger, #d9534f));border-color:var(--nxe-ui-button-border-danger, transparent);color:var(--nxe-ui-button-fg-danger, #fff)}nxe-button.nxe-button-danger:hover{background-color:var(--nxe-ui-button-hover-bg-danger, #cd312c)}nxe-button.nxe-button-danger:active{background-color:var(--nxe-ui-button-active-bg-danger, #932320)}nxe-button[disabled]{opacity:var(--nxe-ui-disabled-opacity, .5);cursor:default}nxe-button.nxe-disabled{opacity:var(--nxe-ui-disabled-opacity, .5);cursor:default;pointer-events:none}nxe-button .nxe-button-content>[nxeButtonIcon],nxe-button .nxe-button-content>[nxeIcon]{flex:none;width:1.25em;height:1.25em}nxe-button .nxe-button-content>[nxeButtonIcon]:not(:last-child),nxe-button .nxe-button-content>[nxeIcon]:not(:last-child){margin-inline-end:var(--nxe-ui-space-2, 8px)}nxe-button.nxe-button-sm{font-size:var(--nxe-ui-font-size-sm, 13px)}nxe-button.nxe-button-sm .nxe-button-content{padding:var(--nxe-ui-space-1, 4px)}nxe-button.nxe-button-sm.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-space-2, 8px)}nxe-button.nxe-button-lg{font-size:var(--nxe-ui-font-size-lg, 16px)}nxe-button.nxe-button-lg .nxe-button-content{padding:var(--nxe-ui-space-3, 12px)}nxe-button.nxe-button-lg.nxe-button-has-text .nxe-button-content{padding-inline:var(--nxe-ui-space-5, 20px)}nxe-button.nxe-button-block{display:flex;width:100%}nxe-button.nxe-button-mode-outlined,nxe-button.nxe-button-mode-text{--nxe-button-tone: var(--nxe-ui-button-fg-secondary, var(--nxe-ui-text, #333));background-color:transparent;color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-outlined:hover,nxe-button.nxe-button-mode-text:hover{background-color:color-mix(in srgb,var(--nxe-button-tone) 8%,transparent)}nxe-button.nxe-button-mode-outlined:active,nxe-button.nxe-button-mode-text:active{background-color:color-mix(in srgb,var(--nxe-button-tone) 16%,transparent)}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-default{--nxe-button-tone: var(--nxe-ui-button-bg-primary, var(--nxe-ui-accent, #337ab7))}nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-success{--nxe-button-tone: var(--nxe-ui-button-bg-success, var(--nxe-ui-success, #5cb85c))}nxe-button.nxe-button-mode-outlined.nxe-button-danger,nxe-button.nxe-button-mode-text.nxe-button-danger{--nxe-button-tone: var(--nxe-ui-button-bg-danger, var(--nxe-ui-danger, #d9534f))}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-outlined.nxe-button-danger,nxe-button.nxe-button-mode-text.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-danger{background-color:transparent;color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-outlined.nxe-button-default:hover,nxe-button.nxe-button-mode-outlined.nxe-button-success:hover,nxe-button.nxe-button-mode-outlined.nxe-button-danger:hover,nxe-button.nxe-button-mode-text.nxe-button-default:hover,nxe-button.nxe-button-mode-text.nxe-button-success:hover,nxe-button.nxe-button-mode-text.nxe-button-danger:hover{background-color:color-mix(in srgb,var(--nxe-button-tone) 8%,transparent)}nxe-button.nxe-button-mode-outlined.nxe-button-default:active,nxe-button.nxe-button-mode-outlined.nxe-button-success:active,nxe-button.nxe-button-mode-outlined.nxe-button-danger:active,nxe-button.nxe-button-mode-text.nxe-button-default:active,nxe-button.nxe-button-mode-text.nxe-button-success:active,nxe-button.nxe-button-mode-text.nxe-button-danger:active{background-color:color-mix(in srgb,var(--nxe-button-tone) 16%,transparent)}nxe-button.nxe-button-mode-outlined{border-color:var(--nxe-ui-button-border-secondary, var(--nxe-ui-border, #ddd))}nxe-button.nxe-button-mode-outlined.nxe-button-default,nxe-button.nxe-button-mode-outlined.nxe-button-success,nxe-button.nxe-button-mode-outlined.nxe-button-danger{border-color:var(--nxe-button-tone)}nxe-button.nxe-button-mode-text,nxe-button.nxe-button-mode-text.nxe-button-default,nxe-button.nxe-button-mode-text.nxe-button-success,nxe-button.nxe-button-mode-text.nxe-button-danger{border-color:transparent}nxe-button.nxe-button-loading{position:relative;cursor:progress}nxe-button.nxe-button-loading .nxe-button-content{visibility:hidden}nxe-button .nxe-button-spinner{position:absolute;inset:0;margin:auto;box-sizing:border-box;width:1.25em;height:1.25em;border:2px solid currentColor;border-inline-end-color:transparent;border-radius:50%;animation:nxe-button-spin .7s linear infinite}@keyframes nxe-button-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){nxe-button .nxe-button-spinner{animation-duration:2s}}\n"] }]
173
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], stylingMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "stylingMode", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], validationGroup: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationGroup", required: false }] }], focusStateEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusStateEnabled", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], onClick: [{ type: i0.Output, args: ["onClick"] }] } });
137
174
 
138
175
  /**
139
176
  * `nxe-text-box` — drop-in replacement for DevExtreme `dx-text-box`.
@@ -163,6 +200,18 @@ class NxeTextBoxComponent extends NxeFormControlBase {
163
200
  showClearButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showClearButtonInput" } : /* istanbul ignore next */ {}), alias: 'showClearButton', transform: nxeOptionalBoolean });
164
201
  showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.textBox?.showClearButton ?? false, /* @ts-ignore */
165
202
  ...(ngDevMode ? [{ debugName: "showClearButton" }] : /* istanbul ignore next */ []));
203
+ /**
204
+ * With `mode="password"`, an icon button after the field that shows the password as text and
205
+ * hides it again. Off by default; it has no effect in any other mode.
206
+ */
207
+ showPasswordToggle = input(false, { ...(ngDevMode ? { debugName: "showPasswordToggle" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
208
+ passwordToggleVisible = computed(() => this.showPasswordToggle() && this.mode() === 'password' && !this.disabled(), /* @ts-ignore */
209
+ ...(ngDevMode ? [{ debugName: "passwordToggleVisible" }] : /* istanbul ignore next */ []));
210
+ /** Whether the password is showing as text right now; hidden again whenever the toggle goes away. */
211
+ passwordRevealed = linkedSignal({ ...(ngDevMode ? { debugName: "passwordRevealed" } : /* istanbul ignore next */ {}), source: this.passwordToggleVisible,
212
+ computation: () => false });
213
+ passwordToggleLabel = computed(() => this.nxeMessages.t(this.passwordRevealed() ? 'forms.password.hide' : 'forms.password.show'), /* @ts-ignore */
214
+ ...(ngDevMode ? [{ debugName: "passwordToggleLabel" }] : /* istanbul ignore next */ []));
166
215
  focused = signal(false, /* @ts-ignore */
167
216
  ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
168
217
  displayValue = computed(() => this.value() ?? '', /* @ts-ignore */
@@ -200,6 +249,9 @@ class NxeTextBoxComponent extends NxeFormControlBase {
200
249
  onTypedKeyup(event) {
201
250
  this.commitOn('keyup', event.target.value, event);
202
251
  }
252
+ commitPending(event) {
253
+ this.commitValue(this.fieldRef().nativeElement.value, event);
254
+ }
203
255
  onBlur(event) {
204
256
  // Blur commits whatever the mode: a value typed and left must not be discarded because
205
257
  // the configured event never fired.
@@ -208,7 +260,7 @@ class NxeTextBoxComponent extends NxeFormControlBase {
208
260
  this.markAsTouched();
209
261
  }
210
262
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
211
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTextBoxComponent, isStandalone: true, selector: "nxe-text-box", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hoverStateEnabledInput: { classPropertyName: "hoverStateEnabledInput", publicName: "hoverStateEnabled", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()" }, classAttribute: "nxe-widget nxe-editor nxe-textbox" }, providers: [
263
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTextBoxComponent, isStandalone: true, selector: "nxe-text-box", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hoverStateEnabledInput: { classPropertyName: "hoverStateEnabledInput", publicName: "hoverStateEnabled", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, showPasswordToggle: { classPropertyName: "showPasswordToggle", publicName: "showPasswordToggle", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-editor nxe-textbox" }, providers: [
212
264
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextBoxComponent), multi: true },
213
265
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTextBoxComponent) },
214
266
  ], viewQueries: [{ propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
@@ -220,9 +272,9 @@ class NxeTextBoxComponent extends NxeFormControlBase {
220
272
  [id]="controlId()"
221
273
  [attr.aria-label]="ariaLabel() || null"
222
274
  [attr.aria-labelledby]="ariaLabelledBy() || null"
223
- [attr.aria-invalid]="validationError() ? true : null"
275
+ [attr.aria-invalid]="invalidState() ? true : null"
224
276
  [attr.aria-required]="required() ? true : null"
225
- [type]="mode()"
277
+ [type]="passwordRevealed() ? 'text' : mode()"
226
278
  [value]="displayValue()"
227
279
  [readOnly]="readOnly()"
228
280
  [disabled]="disabled()"
@@ -245,6 +297,24 @@ class NxeTextBoxComponent extends NxeFormControlBase {
245
297
  </svg>
246
298
  </span>
247
299
  }
300
+ @if (passwordToggleVisible()) {
301
+ <!-- mousedown is held back so a click does not pull focus out of the field. -->
302
+ <button
303
+ type="button"
304
+ class="nxe-password-toggle"
305
+ [attr.aria-label]="passwordToggleLabel()"
306
+ [attr.aria-pressed]="passwordRevealed()"
307
+ [attr.aria-controls]="controlId()"
308
+ (mousedown)="$event.preventDefault()"
309
+ (click)="passwordRevealed.set(!passwordRevealed())">
310
+ <svg viewBox="0 0 18 18" focusable="false" aria-hidden="true">
311
+ <path d="M9 3.5C5.2 3.5 2.2 6 1 9c1.2 3 4.2 5.5 8 5.5s6.8-2.5 8-5.5c-1.2-3-4.2-5.5-8-5.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0-5.2a1.7 1.7 0 1 0 0 3.4 1.7 1.7 0 0 0 0-3.4Z" />
312
+ @if (passwordRevealed()) {
313
+ <path class="nxe-password-toggle-slash" d="M3 2.4 15.6 15l-1 1L2 3.4l1-1Z" />
314
+ }
315
+ </svg>
316
+ </button>
317
+ }
248
318
  </div>
249
319
  <ng-content />
250
320
  @if (validationError(); as error) {
@@ -268,9 +338,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
268
338
  [id]="controlId()"
269
339
  [attr.aria-label]="ariaLabel() || null"
270
340
  [attr.aria-labelledby]="ariaLabelledBy() || null"
271
- [attr.aria-invalid]="validationError() ? true : null"
341
+ [attr.aria-invalid]="invalidState() ? true : null"
272
342
  [attr.aria-required]="required() ? true : null"
273
- [type]="mode()"
343
+ [type]="passwordRevealed() ? 'text' : mode()"
274
344
  [value]="displayValue()"
275
345
  [readOnly]="readOnly()"
276
346
  [disabled]="disabled()"
@@ -293,6 +363,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
293
363
  </svg>
294
364
  </span>
295
365
  }
366
+ @if (passwordToggleVisible()) {
367
+ <!-- mousedown is held back so a click does not pull focus out of the field. -->
368
+ <button
369
+ type="button"
370
+ class="nxe-password-toggle"
371
+ [attr.aria-label]="passwordToggleLabel()"
372
+ [attr.aria-pressed]="passwordRevealed()"
373
+ [attr.aria-controls]="controlId()"
374
+ (mousedown)="$event.preventDefault()"
375
+ (click)="passwordRevealed.set(!passwordRevealed())">
376
+ <svg viewBox="0 0 18 18" focusable="false" aria-hidden="true">
377
+ <path d="M9 3.5C5.2 3.5 2.2 6 1 9c1.2 3 4.2 5.5 8 5.5s6.8-2.5 8-5.5c-1.2-3-4.2-5.5-8-5.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0-5.2a1.7 1.7 0 1 0 0 3.4 1.7 1.7 0 0 0 0-3.4Z" />
378
+ @if (passwordRevealed()) {
379
+ <path class="nxe-password-toggle-slash" d="M3 2.4 15.6 15l-1 1L2 3.4l1-1Z" />
380
+ }
381
+ </svg>
382
+ </button>
383
+ }
296
384
  </div>
297
385
  <ng-content />
298
386
  @if (validationError(); as error) {
@@ -304,7 +392,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
304
392
  host: {
305
393
  class: 'nxe-widget nxe-editor nxe-textbox',
306
394
  '[class.nxe-focused]': 'focused()',
307
- '[class.nxe-invalid]': '!!validationError()',
395
+ '[class.nxe-invalid]': 'invalidState()',
308
396
  },
309
397
  providers: [
310
398
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextBoxComponent), multi: true },
@@ -313,7 +401,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
313
401
  changeDetection: ChangeDetectionStrategy.OnPush,
314
402
  encapsulation: ViewEncapsulation.None,
315
403
  }]
316
- }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hoverStateEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverStateEnabled", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
404
+ }], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hoverStateEnabledInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverStateEnabled", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], showPasswordToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPasswordToggle", required: false }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
317
405
 
318
406
  /**
319
407
  * `nxe-text-area` — drop-in replacement for DevExtreme `dx-text-area`.
@@ -330,17 +418,94 @@ class NxeTextAreaComponent extends NxeFormControlBase {
330
418
  heightInput = input(undefined, { ...(ngDevMode ? { debugName: "heightInput" } : /* istanbul ignore next */ {}), alias: 'height' });
331
419
  height = computed(() => this.heightInput() ?? this.nxeDefaults.textArea?.height ?? 80, /* @ts-ignore */
332
420
  ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
421
+ /**
422
+ * dx `maxLength`: the most characters the field takes. Sets the native `maxlength`, so typing
423
+ * and pasting stop at the limit, and shows a live "12/200" count under the text. Unset (the
424
+ * default), there is no limit and no counter.
425
+ */
426
+ maxLength = input(undefined, { ...(ngDevMode ? { debugName: "maxLength" } : /* istanbul ignore next */ {}), transform: (v) => (v === null || v === undefined || v === '' ? undefined : numberAttribute(v, undefined)) });
427
+ /**
428
+ * Grow with the text between {@link minRows} and {@link maxRows}, scrolling past the maximum.
429
+ * The fixed `height` is ignored while this is on. Off by default.
430
+ */
431
+ autoGrow = input(false, { ...(ngDevMode ? { debugName: "autoGrow" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
432
+ /** Rows the field never shrinks below while `autoGrow` is on. */
433
+ minRows = input(3, { ...(ngDevMode ? { debugName: "minRows" } : /* istanbul ignore next */ {}), transform: (v) => Math.max(1, numberAttribute(v, 3)) });
434
+ /** Rows after which an auto-growing field scrolls instead of growing; `0` means never. */
435
+ maxRows = input(10, { ...(ngDevMode ? { debugName: "maxRows" } : /* istanbul ignore next */ {}), transform: (v) => Math.max(0, numberAttribute(v, 10)) });
333
436
  focused = signal(false, /* @ts-ignore */
334
437
  ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
335
438
  displayValue = computed(() => this.value() ?? '', /* @ts-ignore */
336
439
  ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
440
+ /** The text in the field right now, typed or not: the counter and the height follow every keystroke. */
441
+ fieldText = linkedSignal(() => this.displayValue(), /* @ts-ignore */
442
+ ...(ngDevMode ? [{ debugName: "fieldText" }] : /* istanbul ignore next */ []));
443
+ remaining = computed(() => (this.maxLength() ?? 0) - this.fieldText().length, /* @ts-ignore */
444
+ ...(ngDevMode ? [{ debugName: "remaining" }] : /* istanbul ignore next */ []));
445
+ /** Close enough to the limit to say so: the last 10 characters, or 10% of a long limit. */
446
+ nearLimit = computed(() => {
447
+ const max = this.maxLength();
448
+ return max !== undefined && this.remaining() <= Math.max(10, Math.ceil(max * 0.1));
449
+ }, /* @ts-ignore */
450
+ ...(ngDevMode ? [{ debugName: "nearLimit" }] : /* istanbul ignore next */ []));
451
+ /** Spoken by the polite status; empty (so silent) until {@link nearLimit}. */
452
+ limitStatus = computed(() => {
453
+ if (!this.nearLimit()) {
454
+ return '';
455
+ }
456
+ const left = this.remaining();
457
+ return left >= 0
458
+ ? this.nxeMessages.t('forms.textArea.charactersLeft', left)
459
+ : this.nxeMessages.t('forms.textArea.charactersOver', -left);
460
+ }, /* @ts-ignore */
461
+ ...(ngDevMode ? [{ debugName: "limitStatus" }] : /* istanbul ignore next */ []));
462
+ growState = computed(() => this.autoGrow()
463
+ ? { text: this.fieldText(), minRows: this.minRows(), maxRows: this.maxRows() }
464
+ : null, /* @ts-ignore */
465
+ ...(ngDevMode ? [{ debugName: "growState" }] : /* istanbul ignore next */ []));
466
+ constructor() {
467
+ super();
468
+ // Reads exactly `growState()`; the measuring below reads no signal.
469
+ afterRenderEffect(() => this.resize(this.growState()));
470
+ }
337
471
  /** dx `valueChangeEvent: 'input'` — the search-as-you-type case. */
338
472
  onTypedInput(event) {
339
- this.commitOn('input', event.target.value, event);
473
+ const text = event.target.value;
474
+ this.fieldText.set(text);
475
+ this.commitOn('input', text, event);
476
+ }
477
+ /** Size the textarea to its text: collapse it to measure, then clamp to the row limits. */
478
+ resize(state) {
479
+ const el = this.elementRef.nativeElement.querySelector('textarea');
480
+ if (!el || !state) {
481
+ return;
482
+ }
483
+ const style = getComputedStyle(el);
484
+ const px = (v) => parseFloat(v) || 0;
485
+ const lineHeight = px(style.lineHeight) || px(style.fontSize) * 1.35715;
486
+ const padding = px(style.paddingTop) + px(style.paddingBottom);
487
+ const border = px(style.borderTopWidth) + px(style.borderBottomWidth);
488
+ const chrome = style.boxSizing === 'border-box' ? padding + border : 0;
489
+ el.style.height = '0px';
490
+ const content = el.scrollHeight - padding;
491
+ const min = state.minRows * lineHeight;
492
+ const max = state.maxRows > 0 ? state.maxRows * lineHeight : Infinity;
493
+ el.style.height = `${Math.min(Math.max(content, min), max) + chrome}px`;
494
+ el.style.overflowY = content > max ? 'auto' : 'hidden';
340
495
  }
341
496
  onTypedKeyup(event) {
342
497
  this.commitOn('keyup', event.target.value, event);
343
498
  }
499
+ commitPending(event) {
500
+ const field = this.elementRef.nativeElement.querySelector('textarea');
501
+ if (field) {
502
+ this.commitValue(field.value, event);
503
+ }
504
+ }
505
+ /** Enter types a newline here and submits nothing; Ctrl/Cmd+Enter is the submit chord. */
506
+ commitsOnEnter(event) {
507
+ return event.ctrlKey || event.metaKey;
508
+ }
344
509
  onBlur(event) {
345
510
  // Blur commits whatever the mode: a value typed and left must not be discarded because
346
511
  // the configured event never fired.
@@ -348,75 +513,101 @@ class NxeTextAreaComponent extends NxeFormControlBase {
348
513
  this.focused.set(false);
349
514
  this.markAsTouched();
350
515
  }
351
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextAreaComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
352
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTextAreaComponent, isStandalone: true, selector: "nxe-text-area", inputs: { placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, heightInput: { classPropertyName: "heightInput", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()" }, classAttribute: "nxe-widget nxe-editor nxe-textarea" }, providers: [
516
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextAreaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
517
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTextAreaComponent, isStandalone: true, selector: "nxe-text-area", inputs: { placeholderInput: { classPropertyName: "placeholderInput", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, heightInput: { classPropertyName: "heightInput", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, maxLength: { classPropertyName: "maxLength", publicName: "maxLength", isSignal: true, isRequired: false, transformFunction: null }, autoGrow: { classPropertyName: "autoGrow", publicName: "autoGrow", isSignal: true, isRequired: false, transformFunction: null }, minRows: { classPropertyName: "minRows", publicName: "minRows", isSignal: true, isRequired: false, transformFunction: null }, maxRows: { classPropertyName: "maxRows", publicName: "maxRows", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-editor nxe-textarea" }, providers: [
353
518
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextAreaComponent), multi: true },
354
519
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTextAreaComponent) },
355
- ], usesInheritance: true, ngImport: i0, template: `
356
- <div class="nxe-editor-container">
357
- <textarea
358
- class="nxe-editor-input nxe-textarea-input"
359
- [attr.tabindex]="tabIndex()"
360
- [id]="controlId()"
361
- [attr.aria-label]="ariaLabel() || null"
362
- [attr.aria-labelledby]="ariaLabelledBy() || null"
363
- [attr.aria-invalid]="validationError() ? true : null"
364
- [attr.aria-required]="required() ? true : null"
365
- [value]="displayValue()"
366
- [readOnly]="readOnly()"
367
- [disabled]="disabled()"
368
- [attr.placeholder]="placeholder() || null"
369
- [style.height.px]="height() || null"
370
- (input)="onTypedInput($event)"
371
- (keyup)="onTypedKeyup($event)"
372
- (blur)="onBlur($event)"
373
- (focus)="focused.set(true)"></textarea>
374
- </div>
375
- <ng-content />
376
- @if (validationError(); as error) {
377
- @if (focused()) {
378
- <div class="nxe-invalid-message">{{ error }}</div>
379
- }
380
- }
381
- `, isInline: true, styles: ["nxe-text-area .nxe-textarea-input{display:block;resize:none;overflow:auto;white-space:pre-wrap;line-height:1.35715}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
520
+ ], usesInheritance: true, ngImport: i0, template: `
521
+ <div class="nxe-editor-container">
522
+ <textarea
523
+ class="nxe-editor-input nxe-textarea-input"
524
+ [attr.tabindex]="tabIndex()"
525
+ [id]="controlId()"
526
+ [attr.aria-label]="ariaLabel() || null"
527
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
528
+ [attr.aria-invalid]="invalidState() ? true : null"
529
+ [attr.aria-required]="required() ? true : null"
530
+ [value]="displayValue()"
531
+ [readOnly]="readOnly()"
532
+ [disabled]="disabled()"
533
+ [attr.placeholder]="placeholder() || null"
534
+ [attr.maxlength]="maxLength() ?? null"
535
+ [style.height.px]="autoGrow() ? null : height() || null"
536
+ (input)="onTypedInput($event)"
537
+ (keyup)="onTypedKeyup($event)"
538
+ (blur)="onBlur($event)"
539
+ (focus)="focused.set(true)"></textarea>
540
+ </div>
541
+ @if (maxLength() !== undefined) {
542
+ <!-- The visible count is not read out character by character; the status below speaks
543
+ only once the limit is close. -->
544
+ <div
545
+ class="nxe-textarea-counter"
546
+ aria-hidden="true"
547
+ [class.nxe-textarea-counter-near]="nearLimit()"
548
+ [class.nxe-textarea-counter-over]="remaining() < 0">
549
+ {{ fieldText().length }}/{{ maxLength() }}
550
+ </div>
551
+ <span class="nxe-textarea-counter-status" aria-live="polite" aria-atomic="true">{{ limitStatus() }}</span>
552
+ }
553
+ <ng-content />
554
+ @if (validationError(); as error) {
555
+ @if (focused()) {
556
+ <div class="nxe-invalid-message">{{ error }}</div>
557
+ }
558
+ }
559
+ `, isInline: true, styles: ["nxe-text-area .nxe-textarea-input{display:block;resize:none;overflow:auto;white-space:pre-wrap;line-height:1.35715}nxe-text-area .nxe-textarea-counter{padding:0 var(--nxe-ui-space-2, 8px) var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-text-muted, #999);font-size:var(--nxe-ui-font-size-xs, 12px);line-height:1.35715;text-align:end;font-variant-numeric:tabular-nums}nxe-text-area .nxe-textarea-counter-near{color:var(--nxe-ui-text, #333)}nxe-text-area .nxe-textarea-counter-over{color:var(--nxe-ui-invalid, #d9534f)}nxe-text-area .nxe-textarea-counter-status{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
382
560
  }
383
561
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTextAreaComponent, decorators: [{
384
562
  type: Component,
385
- args: [{ selector: 'nxe-text-area', standalone: true, template: `
386
- <div class="nxe-editor-container">
387
- <textarea
388
- class="nxe-editor-input nxe-textarea-input"
389
- [attr.tabindex]="tabIndex()"
390
- [id]="controlId()"
391
- [attr.aria-label]="ariaLabel() || null"
392
- [attr.aria-labelledby]="ariaLabelledBy() || null"
393
- [attr.aria-invalid]="validationError() ? true : null"
394
- [attr.aria-required]="required() ? true : null"
395
- [value]="displayValue()"
396
- [readOnly]="readOnly()"
397
- [disabled]="disabled()"
398
- [attr.placeholder]="placeholder() || null"
399
- [style.height.px]="height() || null"
400
- (input)="onTypedInput($event)"
401
- (keyup)="onTypedKeyup($event)"
402
- (blur)="onBlur($event)"
403
- (focus)="focused.set(true)"></textarea>
404
- </div>
405
- <ng-content />
406
- @if (validationError(); as error) {
407
- @if (focused()) {
408
- <div class="nxe-invalid-message">{{ error }}</div>
409
- }
410
- }
563
+ args: [{ selector: 'nxe-text-area', standalone: true, template: `
564
+ <div class="nxe-editor-container">
565
+ <textarea
566
+ class="nxe-editor-input nxe-textarea-input"
567
+ [attr.tabindex]="tabIndex()"
568
+ [id]="controlId()"
569
+ [attr.aria-label]="ariaLabel() || null"
570
+ [attr.aria-labelledby]="ariaLabelledBy() || null"
571
+ [attr.aria-invalid]="invalidState() ? true : null"
572
+ [attr.aria-required]="required() ? true : null"
573
+ [value]="displayValue()"
574
+ [readOnly]="readOnly()"
575
+ [disabled]="disabled()"
576
+ [attr.placeholder]="placeholder() || null"
577
+ [attr.maxlength]="maxLength() ?? null"
578
+ [style.height.px]="autoGrow() ? null : height() || null"
579
+ (input)="onTypedInput($event)"
580
+ (keyup)="onTypedKeyup($event)"
581
+ (blur)="onBlur($event)"
582
+ (focus)="focused.set(true)"></textarea>
583
+ </div>
584
+ @if (maxLength() !== undefined) {
585
+ <!-- The visible count is not read out character by character; the status below speaks
586
+ only once the limit is close. -->
587
+ <div
588
+ class="nxe-textarea-counter"
589
+ aria-hidden="true"
590
+ [class.nxe-textarea-counter-near]="nearLimit()"
591
+ [class.nxe-textarea-counter-over]="remaining() < 0">
592
+ {{ fieldText().length }}/{{ maxLength() }}
593
+ </div>
594
+ <span class="nxe-textarea-counter-status" aria-live="polite" aria-atomic="true">{{ limitStatus() }}</span>
595
+ }
596
+ <ng-content />
597
+ @if (validationError(); as error) {
598
+ @if (focused()) {
599
+ <div class="nxe-invalid-message">{{ error }}</div>
600
+ }
601
+ }
411
602
  `, host: {
412
603
  class: 'nxe-widget nxe-editor nxe-textarea',
413
604
  '[class.nxe-focused]': 'focused()',
414
- '[class.nxe-invalid]': '!!validationError()',
605
+ '[class.nxe-invalid]': 'invalidState()',
415
606
  }, providers: [
416
607
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeTextAreaComponent), multi: true },
417
608
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeTextAreaComponent) },
418
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-text-area .nxe-textarea-input{display:block;resize:none;overflow:auto;white-space:pre-wrap;line-height:1.35715}\n"] }]
419
- }], propDecorators: { placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], heightInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
609
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-text-area .nxe-textarea-input{display:block;resize:none;overflow:auto;white-space:pre-wrap;line-height:1.35715}nxe-text-area .nxe-textarea-counter{padding:0 var(--nxe-ui-space-2, 8px) var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-text-muted, #999);font-size:var(--nxe-ui-font-size-xs, 12px);line-height:1.35715;text-align:end;font-variant-numeric:tabular-nums}nxe-text-area .nxe-textarea-counter-near{color:var(--nxe-ui-text, #333)}nxe-text-area .nxe-textarea-counter-over{color:var(--nxe-ui-invalid, #d9534f)}nxe-text-area .nxe-textarea-counter-status{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n"] }]
610
+ }], ctorParameters: () => [], propDecorators: { placeholderInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], heightInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], autoGrow: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoGrow", required: false }] }], minRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "minRows", required: false }] }], maxRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRows", required: false }] }] } });
420
611
 
421
612
  /**
422
613
  * `nxe-check-box` — drop-in replacement for DevExtreme `dx-check-box`.
@@ -470,36 +661,36 @@ class NxeCheckBoxComponent extends NxeFormControlBase {
470
661
  this.toggle(event);
471
662
  }
472
663
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeCheckBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
473
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeCheckBoxComponent, isStandalone: true, selector: "nxe-check-box", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "checkbox" }, listeners: { "click": "toggle($event)", "keydown.space": "onSpace($event)", "blur": "markAsTouched()" }, properties: { "attr.aria-checked": "indeterminate() ? 'mixed' : !!value()", "attr.aria-disabled": "disabled() ? true : null", "attr.aria-readonly": "readOnly() ? true : null", "attr.aria-invalid": "validationError() ? true : null", "attr.aria-required": "required() ? true : null", "attr.id": "inputId() ? controlId() : null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "ariaLabelledBy() || null", "attr.tabindex": "disabled() ? null : 0", "class.nxe-checkbox-checked": "!!value()", "class.nxe-checkbox-indeterminate": "indeterminate()", "class.nxe-disabled": "disabled()", "class.nxe-readonly": "readOnly()", "class.nxe-invalid": "!!validationError()" }, classAttribute: "nxe-widget nxe-checkbox" }, providers: [
664
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeCheckBoxComponent, isStandalone: true, selector: "nxe-check-box", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "checkbox" }, listeners: { "click": "toggle($event)", "keydown.space": "onSpace($event)", "blur": "markAsTouched()" }, properties: { "attr.aria-checked": "indeterminate() ? 'mixed' : !!value()", "attr.aria-disabled": "disabled() ? true : null", "attr.aria-readonly": "readOnly() ? true : null", "attr.aria-invalid": "invalidState() ? true : null", "attr.aria-required": "required() ? true : null", "attr.id": "inputId() ? controlId() : null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "ariaLabelledBy() || null", "attr.tabindex": "disabled() ? null : 0", "class.nxe-checkbox-checked": "!!value()", "class.nxe-checkbox-indeterminate": "indeterminate()", "class.nxe-disabled": "disabled()", "class.nxe-readonly": "readOnly()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-checkbox" }, providers: [
474
665
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeCheckBoxComponent), multi: true },
475
666
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeCheckBoxComponent) },
476
- ], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
477
- <span class="nxe-checkbox-container">
478
- <span class="nxe-checkbox-icon" aria-hidden="true"></span>
479
- @if (text()) {
480
- <span class="nxe-checkbox-text">{{ text() }}</span>
481
- }
482
- </span>
483
- <ng-content />
484
- `, isInline: true, styles: ["nxe-check-box{display:inline-flex;align-items:center;box-sizing:border-box;cursor:pointer;vertical-align:middle;-webkit-user-select:none;user-select:none;line-height:normal}nxe-check-box:focus{outline:none}nxe-check-box .nxe-checkbox-container{display:inline-flex;align-items:center;max-width:100%;overflow:hidden}nxe-check-box .nxe-checkbox-icon{box-sizing:border-box;position:relative;flex-shrink:0;width:22px;height:22px;border:1px solid #ddd;border-radius:2px;background-color:#fff}nxe-check-box .nxe-checkbox-text{padding-inline-start:5px;white-space:pre-wrap;word-wrap:break-word;line-height:normal}nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:5px;inset-inline-start:2.5px;width:15px;height:11px;background-color:var(--nxe-ui-accent, #337ab7);clip-path:polygon(0 5.586px,5.414px 11px,15px 1.414px,13.586px 0,5.414px 8.172px,1.414px 4.172px)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:50%;inset-inline-start:50%;width:12px;height:12px;margin-top:-6px;margin-inline-start:-6px;border-radius:2px;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-readonly{cursor:default}nxe-check-box.nxe-readonly .nxe-checkbox-icon{border-color:#f4f4f4;background-color:#fff}nxe-check-box:not(.nxe-disabled):hover .nxe-checkbox-icon{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}nxe-check-box:not(.nxe-disabled):focus .nxe-checkbox-icon{border-color:var(--nxe-ui-accent, #337ab7)}nxe-check-box:not(.nxe-disabled):active .nxe-checkbox-icon{background-color:#60606033}nxe-check-box.nxe-disabled{cursor:default}nxe-check-box.nxe-disabled .nxe-checkbox-icon,nxe-check-box.nxe-disabled .nxe-checkbox-text{opacity:.4}nxe-check-box.nxe-invalid .nxe-checkbox-icon,nxe-check-box.nxe-invalid:hover .nxe-checkbox-icon{border-color:#d9534f66}nxe-check-box.nxe-invalid:focus .nxe-checkbox-icon{border-color:#d9534f}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
667
+ ], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: `
668
+ <span class="nxe-checkbox-container">
669
+ <span class="nxe-checkbox-icon" aria-hidden="true"></span>
670
+ @if (text()) {
671
+ <span class="nxe-checkbox-text">{{ text() }}</span>
672
+ }
673
+ </span>
674
+ <ng-content />
675
+ `, isInline: true, styles: ["nxe-check-box{display:inline-flex;align-items:center;box-sizing:border-box;cursor:pointer;vertical-align:middle;-webkit-user-select:none;user-select:none;line-height:normal}nxe-check-box:focus{outline:none}nxe-check-box .nxe-checkbox-container{display:inline-flex;align-items:center;max-width:100%;overflow:hidden}nxe-check-box .nxe-checkbox-icon{box-sizing:border-box;position:relative;flex-shrink:0;width:22px;height:22px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:2px;background-color:var(--nxe-ui-surface, #fff)}nxe-check-box .nxe-checkbox-text{padding-inline-start:5px;white-space:pre-wrap;word-wrap:break-word;line-height:normal}nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:5px;inset-inline-start:2.5px;width:15px;height:11px;background-color:var(--nxe-ui-accent, #337ab7);clip-path:polygon(0 5.586px,5.414px 11px,15px 1.414px,13.586px 0,5.414px 8.172px,1.414px 4.172px)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:50%;inset-inline-start:50%;width:12px;height:12px;margin-top:-6px;margin-inline-start:-6px;border-radius:2px;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-readonly{cursor:default}nxe-check-box.nxe-readonly .nxe-checkbox-icon{border-color:var(--nxe-ui-neutral-80, #f4f4f4);background-color:var(--nxe-ui-surface, #fff)}nxe-check-box:not(.nxe-disabled):hover .nxe-checkbox-icon{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}nxe-check-box:not(.nxe-disabled):focus .nxe-checkbox-icon{border-color:var(--nxe-ui-accent, #337ab7)}nxe-check-box:not(.nxe-disabled):active .nxe-checkbox-icon{background-color:var(--nxe-ui-checkbox-active-bg, rgba(96, 96, 96, .2))}nxe-check-box.nxe-disabled{cursor:default}nxe-check-box.nxe-disabled .nxe-checkbox-icon,nxe-check-box.nxe-disabled .nxe-checkbox-text{opacity:.4}nxe-check-box.nxe-invalid .nxe-checkbox-icon,nxe-check-box.nxe-invalid:hover .nxe-checkbox-icon{border-color:var(--nxe-ui-invalid-faded, rgba(217, 83, 79, .4))}nxe-check-box.nxe-invalid:focus .nxe-checkbox-icon{border-color:var(--nxe-ui-invalid, #d9534f)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
485
676
  }
486
677
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeCheckBoxComponent, decorators: [{
487
678
  type: Component,
488
- args: [{ selector: 'nxe-check-box', standalone: true, template: `
489
- <span class="nxe-checkbox-container">
490
- <span class="nxe-checkbox-icon" aria-hidden="true"></span>
491
- @if (text()) {
492
- <span class="nxe-checkbox-text">{{ text() }}</span>
493
- }
494
- </span>
495
- <ng-content />
679
+ args: [{ selector: 'nxe-check-box', standalone: true, template: `
680
+ <span class="nxe-checkbox-container">
681
+ <span class="nxe-checkbox-icon" aria-hidden="true"></span>
682
+ @if (text()) {
683
+ <span class="nxe-checkbox-text">{{ text() }}</span>
684
+ }
685
+ </span>
686
+ <ng-content />
496
687
  `, host: {
497
688
  class: 'nxe-widget nxe-checkbox',
498
689
  role: 'checkbox',
499
690
  '[attr.aria-checked]': "indeterminate() ? 'mixed' : !!value()",
500
691
  '[attr.aria-disabled]': 'disabled() ? true : null',
501
692
  '[attr.aria-readonly]': 'readOnly() ? true : null',
502
- '[attr.aria-invalid]': 'validationError() ? true : null',
693
+ '[attr.aria-invalid]': 'invalidState() ? true : null',
503
694
  '[attr.aria-required]': 'required() ? true : null',
504
695
  // The host IS the control here, so the id and name belong on it rather than on a child.
505
696
  '[attr.id]': 'inputId() ? controlId() : null',
@@ -511,16 +702,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
511
702
  '[class.nxe-checkbox-indeterminate]': 'indeterminate()',
512
703
  '[class.nxe-disabled]': 'disabled()',
513
704
  '[class.nxe-readonly]': 'readOnly()',
514
- '[class.nxe-invalid]': '!!validationError()',
705
+ '[class.nxe-invalid]': 'invalidState()',
515
706
  '(click)': 'toggle($event)',
516
707
  '(keydown.space)': 'onSpace($event)',
517
708
  '(blur)': 'markAsTouched()',
518
709
  }, providers: [
519
710
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeCheckBoxComponent), multi: true },
520
711
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeCheckBoxComponent) },
521
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-check-box{display:inline-flex;align-items:center;box-sizing:border-box;cursor:pointer;vertical-align:middle;-webkit-user-select:none;user-select:none;line-height:normal}nxe-check-box:focus{outline:none}nxe-check-box .nxe-checkbox-container{display:inline-flex;align-items:center;max-width:100%;overflow:hidden}nxe-check-box .nxe-checkbox-icon{box-sizing:border-box;position:relative;flex-shrink:0;width:22px;height:22px;border:1px solid #ddd;border-radius:2px;background-color:#fff}nxe-check-box .nxe-checkbox-text{padding-inline-start:5px;white-space:pre-wrap;word-wrap:break-word;line-height:normal}nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:5px;inset-inline-start:2.5px;width:15px;height:11px;background-color:var(--nxe-ui-accent, #337ab7);clip-path:polygon(0 5.586px,5.414px 11px,15px 1.414px,13.586px 0,5.414px 8.172px,1.414px 4.172px)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:50%;inset-inline-start:50%;width:12px;height:12px;margin-top:-6px;margin-inline-start:-6px;border-radius:2px;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-readonly{cursor:default}nxe-check-box.nxe-readonly .nxe-checkbox-icon{border-color:#f4f4f4;background-color:#fff}nxe-check-box:not(.nxe-disabled):hover .nxe-checkbox-icon{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}nxe-check-box:not(.nxe-disabled):focus .nxe-checkbox-icon{border-color:var(--nxe-ui-accent, #337ab7)}nxe-check-box:not(.nxe-disabled):active .nxe-checkbox-icon{background-color:#60606033}nxe-check-box.nxe-disabled{cursor:default}nxe-check-box.nxe-disabled .nxe-checkbox-icon,nxe-check-box.nxe-disabled .nxe-checkbox-text{opacity:.4}nxe-check-box.nxe-invalid .nxe-checkbox-icon,nxe-check-box.nxe-invalid:hover .nxe-checkbox-icon{border-color:#d9534f66}nxe-check-box.nxe-invalid:focus .nxe-checkbox-icon{border-color:#d9534f}\n"] }]
712
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-check-box{display:inline-flex;align-items:center;box-sizing:border-box;cursor:pointer;vertical-align:middle;-webkit-user-select:none;user-select:none;line-height:normal}nxe-check-box:focus{outline:none}nxe-check-box .nxe-checkbox-container{display:inline-flex;align-items:center;max-width:100%;overflow:hidden}nxe-check-box .nxe-checkbox-icon{box-sizing:border-box;position:relative;flex-shrink:0;width:22px;height:22px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:2px;background-color:var(--nxe-ui-surface, #fff)}nxe-check-box .nxe-checkbox-text{padding-inline-start:5px;white-space:pre-wrap;word-wrap:break-word;line-height:normal}nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:5px;inset-inline-start:2.5px;width:15px;height:11px;background-color:var(--nxe-ui-accent, #337ab7);clip-path:polygon(0 5.586px,5.414px 11px,15px 1.414px,13.586px 0,5.414px 8.172px,1.414px 4.172px)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-checked .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{content:\"\";position:absolute;box-sizing:border-box;top:50%;inset-inline-start:50%;width:12px;height:12px;margin-top:-6px;margin-inline-start:-6px;border-radius:2px;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){nxe-check-box.nxe-checkbox-indeterminate .nxe-checkbox-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-check-box.nxe-readonly{cursor:default}nxe-check-box.nxe-readonly .nxe-checkbox-icon{border-color:var(--nxe-ui-neutral-80, #f4f4f4);background-color:var(--nxe-ui-surface, #fff)}nxe-check-box:not(.nxe-disabled):hover .nxe-checkbox-icon{border-color:var(--nxe-ui-checkbox-hover-border, #265a87)}nxe-check-box:not(.nxe-disabled):focus .nxe-checkbox-icon{border-color:var(--nxe-ui-accent, #337ab7)}nxe-check-box:not(.nxe-disabled):active .nxe-checkbox-icon{background-color:var(--nxe-ui-checkbox-active-bg, rgba(96, 96, 96, .2))}nxe-check-box.nxe-disabled{cursor:default}nxe-check-box.nxe-disabled .nxe-checkbox-icon,nxe-check-box.nxe-disabled .nxe-checkbox-text{opacity:.4}nxe-check-box.nxe-invalid .nxe-checkbox-icon,nxe-check-box.nxe-invalid:hover .nxe-checkbox-icon{border-color:var(--nxe-ui-invalid-faded, rgba(217, 83, 79, .4))}nxe-check-box.nxe-invalid:focus .nxe-checkbox-icon{border-color:var(--nxe-ui-invalid, #d9534f)}\n"] }]
522
713
  }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }] } });
523
714
 
715
+ /**
716
+ * Decimal-safe stepping for nxe-number-box.
717
+ *
718
+ * `0.1 + 0.2` is `0.30000000000000004` in binary floating point, and a spin button that walks a
719
+ * 0.1 step through that arithmetic shows it within a few clicks. Both operands are scaled to
720
+ * integers by the larger of their decimal places, added as integers, and scaled back, which is
721
+ * exact for every value a person can type.
722
+ */
723
+ /** Decimal places in the shortest decimal form of `value` (`0.1` is 1, `1e-7` is 7, `120` is 0). */
724
+ function decimalPlaces(value) {
725
+ if (!Number.isFinite(value)) {
726
+ return 0;
727
+ }
728
+ const text = String(value);
729
+ const exponent = /e([+-]\d+)$/i.exec(text);
730
+ const mantissa = exponent ? text.slice(0, exponent.index) : text;
731
+ const dot = mantissa.indexOf('.');
732
+ const fraction = dot === -1 ? 0 : mantissa.length - dot - 1;
733
+ return Math.max(0, fraction - (exponent ? Number(exponent[1]) : 0));
734
+ }
735
+ /** `a + b` without binary floating point artefacts. */
736
+ function addDecimal(a, b) {
737
+ const places = Math.max(decimalPlaces(a), decimalPlaces(b));
738
+ if (places === 0 || places > 15) {
739
+ return a + b;
740
+ }
741
+ const factor = Math.pow(10, places);
742
+ return (Math.round(a * factor) + Math.round(b * factor)) / factor;
743
+ }
744
+ /** Clamp to the optional bounds. */
745
+ function clampNumber(value, min, max) {
746
+ let next = value;
747
+ if (min != null && next < min) {
748
+ next = min;
749
+ }
750
+ if (max != null && next > max) {
751
+ next = max;
752
+ }
753
+ return next;
754
+ }
755
+ /**
756
+ * The value after `direction` steps of `step` from `current` (`null` counts as 0, or as the
757
+ * nearest bound when 0 is outside them), clamped to `[min, max]`.
758
+ */
759
+ function stepNumber(current, step, direction, min, max) {
760
+ const base = current ?? clampNumber(0, min, max);
761
+ return clampNumber(addDecimal(base, direction * step), min, max);
762
+ }
763
+
764
+ /** Delay before a held spin button starts repeating, and the repeat interval (ms). */
765
+ const SPIN_REPEAT_DELAY = 400;
766
+ const SPIN_REPEAT_INTERVAL = 70;
524
767
  /** Coerce a static (`min="1"`) or bound (`[min]="0"`) attribute to a number, or undefined. */
525
768
  function toNumber(value) {
526
769
  if (value == null || value === '') {
@@ -540,6 +783,17 @@ function toNumber(value) {
540
783
  * Supports `formControlName`/`[(ngModel)]`/`[(value)]`/`[value]`, `[readOnly]`, `[min]`/`[max]`,
541
784
  * `format` (string LDML pattern), `(valueChange)` (numeric), and a projected `nxe-validator`.
542
785
  *
786
+ * Stepping: `step` (default 1) moves the value by exact decimal arithmetic (`0.1` steps never show
787
+ * `0.30000000000000004`) on ArrowUp/ArrowDown (`largeStep`, default 10 steps, on PageUp/PageDown),
788
+ * and on the optional spin buttons (`showSpinButtons`; held buttons repeat unless
789
+ * `[holdToRepeat]="false"`). Results are clamped to `[min, max]` and rounded to the format precision.
790
+ * The input carries `role="spinbutton"` with `aria-valuenow`/`aria-valuemin`/`aria-valuemax`.
791
+ *
792
+ * `currency` (an ISO 4217 code) shows `IDR 1.351.000` / `USD 1,234.50` (separators from
793
+ * `NXE_NUMBER_SEPARATORS`, so it follows the locale) and takes the currency's minor units as the
794
+ * precision. An explicit `format` still sets the number pattern; `currencyDecimals` overrides the
795
+ * minor units (reference data). Focused, the field holds the bare editable number.
796
+ *
543
797
  * `showClearButton` (dx default `false`) adds dx's clear area after the input while the field
544
798
  * holds text and is neither read-only nor disabled; it sets the value to `null`, as dx's
545
799
  * `clear()` does for a number box, and focuses the input.
@@ -555,14 +809,61 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
555
809
  showClearButtonInput = input(undefined, { ...(ngDevMode ? { debugName: "showClearButtonInput" } : /* istanbul ignore next */ {}), alias: 'showClearButton', transform: nxeOptionalBoolean });
556
810
  showClearButton = computed(() => this.showClearButtonInput() ?? this.nxeDefaults.numberBox?.showClearButton ?? false, /* @ts-ignore */
557
811
  ...(ngDevMode ? [{ debugName: "showClearButton" }] : /* istanbul ignore next */ []));
812
+ /** Amount of one step (ArrowUp/ArrowDown, spin buttons). Default 1; zero, negative or invalid means 1. */
813
+ step = input(undefined, { ...(ngDevMode ? { debugName: "step" } : /* istanbul ignore next */ {}), transform: toNumber });
814
+ /** PageUp/PageDown amount. Default 10 steps. */
815
+ largeStep = input(undefined, { ...(ngDevMode ? { debugName: "largeStep" } : /* istanbul ignore next */ {}), transform: toNumber });
816
+ /** Show the increase/decrease buttons after the input. Default off, as in dx. */
817
+ showSpinButtons = input(false, { ...(ngDevMode ? { debugName: "showSpinButtons" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
818
+ /** Keep stepping while a spin button is held down. Default on. */
819
+ holdToRepeat = input(true, { ...(ngDevMode ? { debugName: "holdToRepeat" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
820
+ /** ISO 4217 code: shows `CODE amount` and takes the currency's minor units as precision. */
821
+ currency = input(undefined, /* @ts-ignore */
822
+ ...(ngDevMode ? [{ debugName: "currency" }] : /* istanbul ignore next */ []));
823
+ /** The currency's minor units from reference data; overrides the built-in table. */
824
+ currencyDecimals = input(undefined, { ...(ngDevMode ? { debugName: "currencyDecimals" } : /* istanbul ignore next */ {}), transform: toNumber });
558
825
  focused = signal(false, /* @ts-ignore */
559
826
  ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
560
- parsedFormat = computed(() => parseNumberFormat(this.format()), /* @ts-ignore */
827
+ currencyCode = computed(() => nxeCurrencyCode(this.currency()), /* @ts-ignore */
828
+ ...(ngDevMode ? [{ debugName: "currencyCode" }] : /* istanbul ignore next */ []));
829
+ /** The explicit `format` input only, so a currency does not inherit the app-wide default pattern. */
830
+ explicitFormat = computed(() => parseNumberFormat(this.formatInput() ?? ''), /* @ts-ignore */
831
+ ...(ngDevMode ? [{ debugName: "explicitFormat" }] : /* istanbul ignore next */ []));
832
+ parsedFormat = computed(() => {
833
+ if (this.currencyCode()) {
834
+ return this.explicitFormat() ?? nxeCurrencyNumberFormat(this.currencyCode(), this.currencyDecimals());
835
+ }
836
+ return parseNumberFormat(this.format());
837
+ }, /* @ts-ignore */
561
838
  ...(ngDevMode ? [{ debugName: "parsedFormat" }] : /* istanbul ignore next */ []));
839
+ stepAmount = computed(() => {
840
+ const step = this.step();
841
+ return step != null && step > 0 ? step : 1;
842
+ }, /* @ts-ignore */
843
+ ...(ngDevMode ? [{ debugName: "stepAmount" }] : /* istanbul ignore next */ []));
844
+ largeStepAmount = computed(() => {
845
+ const large = this.largeStep();
846
+ return large != null && large > 0 ? large : this.stepAmount() * 10;
847
+ }, /* @ts-ignore */
848
+ ...(ngDevMode ? [{ debugName: "largeStepAmount" }] : /* istanbul ignore next */ []));
562
849
  fieldRef = viewChild.required('field', /* @ts-ignore */
563
850
  ...(ngDevMode ? [{ debugName: "fieldRef" }] : /* istanbul ignore next */ []));
564
851
  /** Locale characters, configured once by the application. Defaults to en-US. */
565
852
  separators = inject(NXE_NUMBER_SEPARATORS);
853
+ /** The blurred, formatted text of the committed value. */
854
+ formattedValue = computed(() => {
855
+ if (this.currencyCode()) {
856
+ return formatCurrency(this.value(), this.currencyCode(), this.separators, {
857
+ decimals: this.currencyDecimals(),
858
+ pattern: this.explicitFormat(),
859
+ });
860
+ }
861
+ return formatNumber(this.value(), this.parsedFormat(), this.separators);
862
+ }, /* @ts-ignore */
863
+ ...(ngDevMode ? [{ debugName: "formattedValue" }] : /* istanbul ignore next */ []));
864
+ /** `aria-valuetext`: the formatted amount, only when a format or currency makes it differ from the number. */
865
+ valueText = computed(() => this.value() == null || (!this.currencyCode() && !this.format()) ? null : this.formattedValue(), /* @ts-ignore */
866
+ ...(ngDevMode ? [{ debugName: "valueText" }] : /* istanbul ignore next */ []));
566
867
  displayValue = computed(() => {
567
868
  const value = this.value();
568
869
  if (this.focused()) {
@@ -570,7 +871,7 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
570
871
  // a JS-style dot, and parseNumber would read that as a group separator and commit 125.
571
872
  return value == null ? '' : String(value).replace('.', this.separators.decimal);
572
873
  }
573
- return formatNumber(value, this.parsedFormat(), this.separators);
874
+ return this.formattedValue();
574
875
  }, /* @ts-ignore */
575
876
  ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
576
877
  /** The text in the field right now; dx shows the clear button by the field's emptiness. */
@@ -578,6 +879,92 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
578
879
  ...(ngDevMode ? [{ debugName: "fieldText" }] : /* istanbul ignore next */ []));
579
880
  clearButtonVisible = computed(() => this.showClearButton() && !this.readOnly() && !this.disabled() && this.fieldText() !== '', /* @ts-ignore */
580
881
  ...(ngDevMode ? [{ debugName: "clearButtonVisible" }] : /* istanbul ignore next */ []));
882
+ incrementDisabled = computed(() => {
883
+ const max = this.max();
884
+ return this.readOnly() || this.disabled() || (max != null && (this.value() ?? -Infinity) >= max);
885
+ }, /* @ts-ignore */
886
+ ...(ngDevMode ? [{ debugName: "incrementDisabled" }] : /* istanbul ignore next */ []));
887
+ decrementDisabled = computed(() => {
888
+ const min = this.min();
889
+ return this.readOnly() || this.disabled() || (min != null && (this.value() ?? Infinity) <= min);
890
+ }, /* @ts-ignore */
891
+ ...(ngDevMode ? [{ debugName: "decrementDisabled" }] : /* istanbul ignore next */ []));
892
+ spinDelay;
893
+ spinTimer;
894
+ constructor() {
895
+ super();
896
+ inject(DestroyRef).onDestroy(() => this.stopSpin());
897
+ }
898
+ onKeyDownStep(event) {
899
+ if (event.ctrlKey || event.altKey || event.metaKey || this.readOnly() || this.disabled()) {
900
+ return;
901
+ }
902
+ let amount;
903
+ let direction;
904
+ switch (event.key) {
905
+ case 'ArrowUp':
906
+ [amount, direction] = [this.stepAmount(), 1];
907
+ break;
908
+ case 'ArrowDown':
909
+ [amount, direction] = [this.stepAmount(), -1];
910
+ break;
911
+ case 'PageUp':
912
+ [amount, direction] = [this.largeStepAmount(), 1];
913
+ break;
914
+ case 'PageDown':
915
+ [amount, direction] = [this.largeStepAmount(), -1];
916
+ break;
917
+ default:
918
+ return;
919
+ }
920
+ event.preventDefault();
921
+ this.stepBy(amount, direction, event);
922
+ }
923
+ onSpinPointerDown(event, direction) {
924
+ if (event.button !== 0 || this.readOnly() || this.disabled()) {
925
+ return;
926
+ }
927
+ // Keep focus (and the typed text) in the input.
928
+ event.preventDefault();
929
+ this.stopSpin();
930
+ if (!this.stepBy(this.stepAmount(), direction, event) || !this.holdToRepeat()) {
931
+ return;
932
+ }
933
+ this.spinDelay = setTimeout(() => {
934
+ this.spinTimer = setInterval(() => {
935
+ if (!this.stepBy(this.stepAmount(), direction, event)) {
936
+ this.stopSpin();
937
+ }
938
+ }, SPIN_REPEAT_INTERVAL);
939
+ }, SPIN_REPEAT_DELAY);
940
+ }
941
+ /** Keyboard and assistive-technology activation (`detail` 0); pointer presses stepped on pointerdown. */
942
+ onSpinClick(event, direction) {
943
+ if (event.detail === 0) {
944
+ this.stepBy(this.stepAmount(), direction, event);
945
+ }
946
+ }
947
+ stopSpin() {
948
+ clearTimeout(this.spinDelay);
949
+ clearInterval(this.spinTimer);
950
+ this.spinDelay = this.spinTimer = undefined;
951
+ }
952
+ /** Step the value; returns false when it did not move (a bound, or nothing to do). */
953
+ stepBy(amount, direction, event) {
954
+ const field = this.fieldRef().nativeElement;
955
+ // Typed-but-uncommitted text counts as the current value while the field has focus.
956
+ const current = this.focused() ? parseNumber(field.value, this.separators) : this.value();
957
+ let next = stepNumber(current, amount, direction, this.min(), this.max());
958
+ const fmt = this.parsedFormat();
959
+ if (fmt) {
960
+ next = clampNumber(roundHalfAwayFromZero(next, fmt.maxFrac), this.min(), this.max());
961
+ }
962
+ const moved = next !== current;
963
+ this.commitValue(next, event);
964
+ field.value = this.displayValue();
965
+ this.fieldText.set(field.value);
966
+ return moved;
967
+ }
581
968
  onFieldInput(event) {
582
969
  this.fieldText.set(event.target.value);
583
970
  }
@@ -594,8 +981,16 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
594
981
  field.focus();
595
982
  }
596
983
  }
984
+ commitPending(event) {
985
+ this.commitText(this.fieldRef().nativeElement.value, event);
986
+ }
597
987
  onBlur(event) {
598
- let num = parseNumber(event.target.value, this.separators);
988
+ this.commitText(event.target.value, event);
989
+ this.focused.set(false);
990
+ this.markAsTouched();
991
+ }
992
+ commitText(text, event) {
993
+ let num = parseNumber(text, this.separators);
599
994
  if (num != null) {
600
995
  const min = this.min();
601
996
  const max = this.max();
@@ -611,11 +1006,9 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
611
1006
  }
612
1007
  }
613
1008
  this.commitValue(num, event);
614
- this.focused.set(false);
615
- this.markAsTouched();
616
1009
  }
617
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeNumberBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
618
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeNumberBoxComponent, isStandalone: true, selector: "nxe-number-box", inputs: { formatInput: { classPropertyName: "formatInput", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()" }, classAttribute: "nxe-widget nxe-editor nxe-numberbox" }, providers: [
1010
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeNumberBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1011
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeNumberBoxComponent, isStandalone: true, selector: "nxe-number-box", inputs: { formatInput: { classPropertyName: "formatInput", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, largeStep: { classPropertyName: "largeStep", publicName: "largeStep", isSignal: true, isRequired: false, transformFunction: null }, showSpinButtons: { classPropertyName: "showSpinButtons", publicName: "showSpinButtons", isSignal: true, isRequired: false, transformFunction: null }, holdToRepeat: { classPropertyName: "holdToRepeat", publicName: "holdToRepeat", isSignal: true, isRequired: false, transformFunction: null }, currency: { classPropertyName: "currency", publicName: "currency", isSignal: true, isRequired: false, transformFunction: null }, currencyDecimals: { classPropertyName: "currencyDecimals", publicName: "currencyDecimals", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-editor nxe-numberbox" }, providers: [
619
1012
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeNumberBoxComponent), multi: true },
620
1013
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeNumberBoxComponent) },
621
1014
  ], viewQueries: [{ propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
@@ -627,15 +1020,21 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
627
1020
  [id]="controlId()"
628
1021
  [attr.aria-label]="ariaLabel() || null"
629
1022
  [attr.aria-labelledby]="ariaLabelledBy() || null"
630
- [attr.aria-invalid]="validationError() ? true : null"
1023
+ [attr.aria-invalid]="invalidState() ? true : null"
631
1024
  [attr.aria-required]="required() ? true : null"
632
1025
  type="text"
633
1026
  inputmode="decimal"
1027
+ role="spinbutton"
1028
+ [attr.aria-valuenow]="value() ?? null"
1029
+ [attr.aria-valuemin]="min() ?? null"
1030
+ [attr.aria-valuemax]="max() ?? null"
1031
+ [attr.aria-valuetext]="valueText()"
634
1032
  [value]="displayValue()"
635
1033
  [readOnly]="readOnly()"
636
1034
  [disabled]="disabled()"
637
1035
  autocomplete="off"
638
1036
  (input)="onFieldInput($event)"
1037
+ (keydown)="onKeyDownStep($event)"
639
1038
  (focus)="focused.set(true)"
640
1039
  (blur)="onBlur($event)" />
641
1040
  @if (clearButtonVisible()) {
@@ -651,6 +1050,44 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
651
1050
  </svg>
652
1051
  </span>
653
1052
  }
1053
+ @if (showSpinButtons()) {
1054
+ <span class="nxe-spin-container">
1055
+ <!-- Not tab stops: the input is the spinbutton and takes ArrowUp/ArrowDown itself.
1056
+ A pointer press keeps focus in the input; a click with detail 0 is keyboard or AT. -->
1057
+ <button
1058
+ type="button"
1059
+ class="nxe-spin-button nxe-spin-up"
1060
+ tabindex="-1"
1061
+ [attr.aria-label]="nxeMessages.t('forms.numberBox.increase')"
1062
+ [attr.aria-controls]="controlId()"
1063
+ [disabled]="incrementDisabled()"
1064
+ (pointerdown)="onSpinPointerDown($event, 1)"
1065
+ (pointerup)="stopSpin()"
1066
+ (pointerleave)="stopSpin()"
1067
+ (pointercancel)="stopSpin()"
1068
+ (click)="onSpinClick($event, 1)">
1069
+ <svg viewBox="0 0 10 6" focusable="false" aria-hidden="true">
1070
+ <path d="M1 5l4-4 4 4" />
1071
+ </svg>
1072
+ </button>
1073
+ <button
1074
+ type="button"
1075
+ class="nxe-spin-button nxe-spin-down"
1076
+ tabindex="-1"
1077
+ [attr.aria-label]="nxeMessages.t('forms.numberBox.decrease')"
1078
+ [attr.aria-controls]="controlId()"
1079
+ [disabled]="decrementDisabled()"
1080
+ (pointerdown)="onSpinPointerDown($event, -1)"
1081
+ (pointerup)="stopSpin()"
1082
+ (pointerleave)="stopSpin()"
1083
+ (pointercancel)="stopSpin()"
1084
+ (click)="onSpinClick($event, -1)">
1085
+ <svg viewBox="0 0 10 6" focusable="false" aria-hidden="true">
1086
+ <path d="M1 1l4 4 4-4" />
1087
+ </svg>
1088
+ </button>
1089
+ </span>
1090
+ }
654
1091
  </div>
655
1092
  <ng-content />
656
1093
  @if (validationError(); as error) {
@@ -658,7 +1095,7 @@ class NxeNumberBoxComponent extends NxeFormControlBase {
658
1095
  <div class="nxe-invalid-message">{{ error }}</div>
659
1096
  }
660
1097
  }
661
- `, isInline: true, styles: ["nxe-number-box.text-right .nxe-numberbox-input{text-align:right}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1098
+ `, isInline: true, styles: ["nxe-number-box.text-right .nxe-numberbox-input{text-align:right}nxe-number-box .nxe-spin-container{box-sizing:border-box;flex:0 0 24px;align-self:stretch;display:flex;flex-direction:column;width:24px;min-width:24px}nxe-number-box .nxe-editor-container:has(>.nxe-spin-container){--nxe-invalid-badge-inset: 28px}nxe-number-box .nxe-spin-button{appearance:none;box-sizing:border-box;flex:1 1 50%;display:inline-flex;align-items:center;justify-content:center;margin:0;padding:0;border:0;background:transparent;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-number-box .nxe-spin-button svg{display:block;width:10px;height:6px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}nxe-number-box .nxe-spin-button:hover:not(:disabled){color:var(--nxe-ui-text, #333);background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .05))}nxe-number-box .nxe-spin-button:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color, #5c95c5);outline-offset:-2px}nxe-number-box .nxe-spin-button:disabled{cursor:default;opacity:.4}nxe-number-box.nxe-disabled .nxe-spin-button,nxe-number-box.nxe-readonly .nxe-spin-button{cursor:default}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
662
1099
  }
663
1100
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeNumberBoxComponent, decorators: [{
664
1101
  type: Component,
@@ -671,15 +1108,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
671
1108
  [id]="controlId()"
672
1109
  [attr.aria-label]="ariaLabel() || null"
673
1110
  [attr.aria-labelledby]="ariaLabelledBy() || null"
674
- [attr.aria-invalid]="validationError() ? true : null"
1111
+ [attr.aria-invalid]="invalidState() ? true : null"
675
1112
  [attr.aria-required]="required() ? true : null"
676
1113
  type="text"
677
1114
  inputmode="decimal"
1115
+ role="spinbutton"
1116
+ [attr.aria-valuenow]="value() ?? null"
1117
+ [attr.aria-valuemin]="min() ?? null"
1118
+ [attr.aria-valuemax]="max() ?? null"
1119
+ [attr.aria-valuetext]="valueText()"
678
1120
  [value]="displayValue()"
679
1121
  [readOnly]="readOnly()"
680
1122
  [disabled]="disabled()"
681
1123
  autocomplete="off"
682
1124
  (input)="onFieldInput($event)"
1125
+ (keydown)="onKeyDownStep($event)"
683
1126
  (focus)="focused.set(true)"
684
1127
  (blur)="onBlur($event)" />
685
1128
  @if (clearButtonVisible()) {
@@ -695,6 +1138,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
695
1138
  </svg>
696
1139
  </span>
697
1140
  }
1141
+ @if (showSpinButtons()) {
1142
+ <span class="nxe-spin-container">
1143
+ <!-- Not tab stops: the input is the spinbutton and takes ArrowUp/ArrowDown itself.
1144
+ A pointer press keeps focus in the input; a click with detail 0 is keyboard or AT. -->
1145
+ <button
1146
+ type="button"
1147
+ class="nxe-spin-button nxe-spin-up"
1148
+ tabindex="-1"
1149
+ [attr.aria-label]="nxeMessages.t('forms.numberBox.increase')"
1150
+ [attr.aria-controls]="controlId()"
1151
+ [disabled]="incrementDisabled()"
1152
+ (pointerdown)="onSpinPointerDown($event, 1)"
1153
+ (pointerup)="stopSpin()"
1154
+ (pointerleave)="stopSpin()"
1155
+ (pointercancel)="stopSpin()"
1156
+ (click)="onSpinClick($event, 1)">
1157
+ <svg viewBox="0 0 10 6" focusable="false" aria-hidden="true">
1158
+ <path d="M1 5l4-4 4 4" />
1159
+ </svg>
1160
+ </button>
1161
+ <button
1162
+ type="button"
1163
+ class="nxe-spin-button nxe-spin-down"
1164
+ tabindex="-1"
1165
+ [attr.aria-label]="nxeMessages.t('forms.numberBox.decrease')"
1166
+ [attr.aria-controls]="controlId()"
1167
+ [disabled]="decrementDisabled()"
1168
+ (pointerdown)="onSpinPointerDown($event, -1)"
1169
+ (pointerup)="stopSpin()"
1170
+ (pointerleave)="stopSpin()"
1171
+ (pointercancel)="stopSpin()"
1172
+ (click)="onSpinClick($event, -1)">
1173
+ <svg viewBox="0 0 10 6" focusable="false" aria-hidden="true">
1174
+ <path d="M1 1l4 4 4-4" />
1175
+ </svg>
1176
+ </button>
1177
+ </span>
1178
+ }
698
1179
  </div>
699
1180
  <ng-content />
700
1181
  @if (validationError(); as error) {
@@ -705,12 +1186,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
705
1186
  `, host: {
706
1187
  class: 'nxe-widget nxe-editor nxe-numberbox',
707
1188
  '[class.nxe-focused]': 'focused()',
708
- '[class.nxe-invalid]': '!!validationError()',
1189
+ '[class.nxe-invalid]': 'invalidState()',
709
1190
  }, providers: [
710
1191
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeNumberBoxComponent), multi: true },
711
1192
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeNumberBoxComponent) },
712
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-number-box.text-right .nxe-numberbox-input{text-align:right}\n"] }]
713
- }], propDecorators: { formatInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
1193
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-number-box.text-right .nxe-numberbox-input{text-align:right}nxe-number-box .nxe-spin-container{box-sizing:border-box;flex:0 0 24px;align-self:stretch;display:flex;flex-direction:column;width:24px;min-width:24px}nxe-number-box .nxe-editor-container:has(>.nxe-spin-container){--nxe-invalid-badge-inset: 28px}nxe-number-box .nxe-spin-button{appearance:none;box-sizing:border-box;flex:1 1 50%;display:inline-flex;align-items:center;justify-content:center;margin:0;padding:0;border:0;background:transparent;color:var(--nxe-ui-text-muted, #999);cursor:pointer}nxe-number-box .nxe-spin-button svg{display:block;width:10px;height:6px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}nxe-number-box .nxe-spin-button:hover:not(:disabled){color:var(--nxe-ui-text, #333);background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .05))}nxe-number-box .nxe-spin-button:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color, #5c95c5);outline-offset:-2px}nxe-number-box .nxe-spin-button:disabled{cursor:default;opacity:.4}nxe-number-box.nxe-disabled .nxe-spin-button,nxe-number-box.nxe-readonly .nxe-spin-button{cursor:default}\n"] }]
1194
+ }], ctorParameters: () => [], propDecorators: { formatInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], largeStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "largeStep", required: false }] }], showSpinButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSpinButtons", required: false }] }], holdToRepeat: [{ type: i0.Input, args: [{ isSignal: true, alias: "holdToRepeat", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], currencyDecimals: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencyDecimals", required: false }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
714
1195
 
715
1196
  /** Normalize a `[min]`/`[max]` binding (Date object or serialized string) to a DateTime. */
716
1197
  function toBoundDate(bound) {
@@ -774,7 +1255,12 @@ class NxeDateBoxComponent extends NxeFormControlBase {
774
1255
  /** Calendar zoom level; caption clicks zoom out, cell picks zoom back in (dx-calendar parity). */
775
1256
  zoomLevel = signal('month', /* @ts-ignore */
776
1257
  ...(ngDevMode ? [{ debugName: "zoomLevel" }] : /* istanbul ignore next */ []));
777
- weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1258
+ /** Short weekday names for the active locale, Sunday first (the grid below starts on Sunday). */
1259
+ weekdays = computed(() => {
1260
+ const monday = Info.weekdays('short', { locale: this.nxeMessages.locale() });
1261
+ return [monday[6], ...monday.slice(0, 6)];
1262
+ }, /* @ts-ignore */
1263
+ ...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
778
1264
  /** The format actually used, from `displayFormat` or the default for this `type`. */
779
1265
  effectiveDisplayFormat = computed(() => {
780
1266
  const stated = this.displayFormat() ?? this.nxeDefaults.dateBox?.displayFormat;
@@ -794,7 +1280,8 @@ class NxeDateBoxComponent extends NxeFormControlBase {
794
1280
  /** dx's default placeholder is '', but orbit's global default is the format itself. */
795
1281
  effectivePlaceholder = computed(() => this.placeholder() ?? this.nxeDefaults.dateBox?.placeholder ?? this.effectiveDisplayFormat(), /* @ts-ignore */
796
1282
  ...(ngDevMode ? [{ debugName: "effectivePlaceholder" }] : /* istanbul ignore next */ []));
797
- selected = computed(() => parseDateValue(this.value()), /* @ts-ignore */
1283
+ /** The value as a Luxon date in the active locale, so `MMM` and `MMMM` follow the language. */
1284
+ selected = computed(() => parseDateValue(this.value())?.setLocale(this.nxeMessages.locale()) ?? null, /* @ts-ignore */
798
1285
  ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
799
1286
  displayValue = computed(() => formatDisplay(this.selected(), this.effectiveDisplayFormat()), /* @ts-ignore */
800
1287
  ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
@@ -816,7 +1303,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
816
1303
  */
817
1304
  textAtFocus = '';
818
1305
  caption = computed(() => {
819
- const view = this.viewMonth();
1306
+ const view = this.viewMonth().setLocale(this.nxeMessages.locale());
820
1307
  switch (this.zoomLevel()) {
821
1308
  case 'month':
822
1309
  return view.toFormat('MMMM yyyy');
@@ -842,7 +1329,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
842
1329
  const min = this.minDate();
843
1330
  const max = this.maxDate();
844
1331
  const span = zoom === 'year' ? { months: 1 } : zoom === 'decade' ? { years: 1 } : { years: 10 };
845
- return buildPeriodCells(zoom, this.viewMonth()).map((cell) => {
1332
+ return buildPeriodCells(zoom, this.viewMonth().setLocale(this.nxeMessages.locale())).map((cell) => {
846
1333
  const end = cell.anchor.plus(span).minus({ days: 1 }).endOf('day');
847
1334
  return {
848
1335
  ...cell,
@@ -953,6 +1440,15 @@ class NxeDateBoxComponent extends NxeFormControlBase {
953
1440
  const hadBaseline = this.focused();
954
1441
  this.focused.set(false);
955
1442
  this.markAsTouched();
1443
+ this.commitTypedText(element, hadBaseline, event);
1444
+ }
1445
+ /** Enter commits what was typed, before a surrounding form submits; nothing typed means nothing to do. */
1446
+ commitPending(event) {
1447
+ if (this.focused()) {
1448
+ this.commitTypedText(this.fieldRef().nativeElement, true, event);
1449
+ }
1450
+ }
1451
+ commitTypedText(element, hadBaseline, event) {
956
1452
  if (hadBaseline && element.value === this.textAtFocus) {
957
1453
  return;
958
1454
  }
@@ -961,7 +1457,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
961
1457
  this.syncField();
962
1458
  return;
963
1459
  }
964
- const dt = parseDisplay(element.value, this.effectiveDisplayFormat(), this.selected());
1460
+ const dt = parseDisplay(element.value, this.effectiveDisplayFormat(), this.selected(), this.nxeMessages.locale());
965
1461
  if (dt) {
966
1462
  this.commitValue(serializeDate(this.clampToBounds(dt)), event);
967
1463
  }
@@ -1066,7 +1562,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
1066
1562
  this.overlayRef?.dispose();
1067
1563
  }
1068
1564
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDateBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
1069
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDateBoxComponent, isStandalone: true, selector: "nxe-date-box", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "!!validationError()" }, classAttribute: "nxe-widget nxe-editor nxe-datebox" }, providers: [
1565
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDateBoxComponent, isStandalone: true, selector: "nxe-date-box", inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, showClearButtonInput: { classPropertyName: "showClearButtonInput", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-editor nxe-datebox" }, providers: [
1070
1566
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDateBoxComponent), multi: true },
1071
1567
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDateBoxComponent) },
1072
1568
  ], viewQueries: [{ propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
@@ -1078,7 +1574,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
1078
1574
  [id]="controlId()"
1079
1575
  [attr.aria-label]="ariaLabel() || null"
1080
1576
  [attr.aria-labelledby]="ariaLabelledBy() || null"
1081
- [attr.aria-invalid]="validationError() ? true : null"
1577
+ [attr.aria-invalid]="invalidState() ? true : null"
1082
1578
  [attr.aria-required]="required() ? true : null"
1083
1579
  type="text"
1084
1580
  [value]="displayValue()"
@@ -1180,7 +1676,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
1180
1676
  </div>
1181
1677
  @if (zoomLevel() === 'month') {
1182
1678
  <div class="nxe-calendar-weekdays">
1183
- @for (wd of weekdays; track wd) {
1679
+ @for (wd of weekdays(); track $index) {
1184
1680
  <span>{{ wd }}</span>
1185
1681
  }
1186
1682
  </div>
@@ -1225,7 +1721,7 @@ class NxeDateBoxComponent extends NxeFormControlBase {
1225
1721
  type="text"
1226
1722
  inputmode="numeric"
1227
1723
  maxlength="2"
1228
- aria-label="Hour"
1724
+ [attr.aria-label]="nxeMessages.t('date.hour')"
1229
1725
  [value]="hourText()"
1230
1726
  (change)="setTimePart('hour', $event)" />
1231
1727
  <span class="nxe-calendar-time-sep" aria-hidden="true">:</span>
@@ -1234,18 +1730,18 @@ class NxeDateBoxComponent extends NxeFormControlBase {
1234
1730
  type="text"
1235
1731
  inputmode="numeric"
1236
1732
  maxlength="2"
1237
- aria-label="Minute"
1733
+ [attr.aria-label]="nxeMessages.t('date.minute')"
1238
1734
  [value]="minuteText()"
1239
1735
  (change)="setTimePart('minute', $event)" />
1240
- <button type="button" class="nxe-calendar-time-btn" (click)="setNow()">Now</button>
1736
+ <button type="button" class="nxe-calendar-time-btn" (click)="setNow()">{{ nxeMessages.t('date.now') }}</button>
1241
1737
  <button type="button" class="nxe-calendar-time-btn nxe-calendar-time-done" (click)="close()">
1242
- Done
1738
+ {{ nxeMessages.t('date.done') }}
1243
1739
  </button>
1244
1740
  </div>
1245
1741
  }
1246
1742
  </div>
1247
1743
  </ng-template>
1248
- `, isInline: true, styles: ["nxe-date-box .nxe-datebox-button{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:#333;cursor:pointer}nxe-date-box .nxe-datebox-button svg{display:block;width:18px;height:18px;fill:currentColor}nxe-date-box:hover:not(.nxe-disabled) .nxe-datebox-button,nxe-date-box:active:not(.nxe-disabled) .nxe-datebox-button{background-color:#e6e6e6}nxe-date-box .nxe-datebox-button.nxe-datebox-open{background-color:#b3b3b3}nxe-date-box .nxe-datebox-icon{width:18px;height:18px;fill:currentColor}nxe-date-box .nxe-editor-container:has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 38px}nxe-date-box .nxe-editor-container:has(>.nxe-clear-button-area):has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 72px}nxe-date-box.nxe-readonly .nxe-datebox-button,nxe-date-box.nxe-disabled .nxe-datebox-button{cursor:default}nxe-date-box.nxe-readonly .nxe-datebox-button:hover,nxe-date-box.nxe-disabled .nxe-datebox-button:hover{background-color:transparent}.nxe-calendar{width:298px;box-sizing:border-box;padding:8px;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 6px 12px #0000002e;color:#333;font-size:var(--nxe-ui-font-size, 12px);-webkit-user-select:none;user-select:none}.nxe-calendar-navigator{display:flex;height:36px;margin-bottom:8px}.nxe-calendar-nav,.nxe-calendar-caption{display:inline-flex;align-items:center;justify-content:center;border:1px solid #ddd;background:transparent;color:#333;cursor:pointer;padding:0}.nxe-calendar-nav:hover,.nxe-calendar-caption:hover{background:#0000000a}.nxe-calendar-nav:active,.nxe-calendar-caption:active{background:#0000003d}.nxe-calendar-nav{flex:0 0 36px}.nxe-calendar-nav:first-child{border-radius:4px 0 0 4px}.nxe-calendar-nav:last-child{border-radius:0 4px 4px 0}.nxe-calendar-caption{flex:1;margin:0 -1px;font-size:inherit;font-weight:600}.nxe-calendar-chevron{width:18px;height:18px;fill:currentColor}.nxe-calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);align-items:center;height:24px;text-align:center;color:#333;font-size:10px;font-weight:500}.nxe-calendar-grid{display:grid;grid-template-columns:repeat(7,1fr)}.nxe-calendar-day,.nxe-calendar-period{display:flex;align-items:center;justify-content:center;border:0;background:transparent;cursor:pointer;padding:0;font-size:14px;line-height:1;color:#333}.nxe-calendar-day span,.nxe-calendar-period span{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border-radius:4px}.nxe-calendar-day:hover:not(:disabled) span,.nxe-calendar-period:hover:not(:disabled) span{background:#337ab73d}.nxe-calendar-day:disabled,.nxe-calendar-period:disabled{cursor:default;color:#33333361}.nxe-calendar-day{height:33px}.nxe-calendar-day span{width:28px;height:28px}.nxe-calendar-periods{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:74px}.nxe-calendar-period span{min-width:54px;height:54px;padding:0 4px}.nxe-calendar-periods-century .nxe-calendar-period span{width:40px;min-width:0;height:auto;min-height:56px;padding:4px 0;white-space:normal;text-align:center;line-height:1.4}.nxe-calendar-other{color:#33333361}.nxe-calendar-today span{border:2px solid #337ab7}.nxe-calendar-selected span,.nxe-calendar-selected:hover:not(:disabled) span{background:#337ab7;color:#fff;box-shadow:0 0 0 2px #337ab780;border:0}.nxe-calendar-time{display:flex;align-items:center;gap:4px;margin-top:8px;padding-top:8px;border-top:1px solid #ddd}.nxe-calendar-time-part{width:2.6em;height:2.2em;box-sizing:border-box;padding:0 .25em;border:1px solid #ddd;border-radius:4px;background:#fff;color:#333;font-size:inherit;font-variant-numeric:tabular-nums;text-align:center}.nxe-calendar-time-part:focus{outline:2px solid #337ab7;outline-offset:-2px}.nxe-calendar-time-sep{font-weight:600}.nxe-calendar-time-btn{height:2.2em;padding:0 .75em;border:1px solid #ddd;border-radius:4px;background:transparent;color:#333;font-size:inherit;cursor:pointer}.nxe-calendar-time-btn:hover{background:#0000000a}.nxe-calendar-time-btn:focus-visible{outline:2px solid #337ab7;outline-offset:-2px}.nxe-calendar-time-done{margin-left:auto}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1744
+ `, isInline: true, styles: ["nxe-date-box .nxe-datebox-button{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-date-box .nxe-datebox-button svg{display:block;width:18px;height:18px;fill:currentColor}nxe-date-box:hover:not(.nxe-disabled) .nxe-datebox-button,nxe-date-box:active:not(.nxe-disabled) .nxe-datebox-button{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-date-box .nxe-datebox-button.nxe-datebox-open{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-date-box .nxe-datebox-icon{width:18px;height:18px;fill:currentColor}nxe-date-box .nxe-editor-container:has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 38px}nxe-date-box .nxe-editor-container:has(>.nxe-clear-button-area):has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 72px}nxe-date-box.nxe-readonly .nxe-datebox-button,nxe-date-box.nxe-disabled .nxe-datebox-button{cursor:default}nxe-date-box.nxe-readonly .nxe-datebox-button:hover,nxe-date-box.nxe-disabled .nxe-datebox-button:hover{background-color:transparent}.nxe-calendar{width:298px;box-sizing:border-box;padding:8px;background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);border-radius:6px;box-shadow:var(--nxe-ui-popup-shadow, 0 6px 12px rgba(0, 0, 0, .18));color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size, 12px);-webkit-user-select:none;user-select:none}.nxe-calendar-navigator{display:flex;height:36px;margin-bottom:8px}.nxe-calendar-nav,.nxe-calendar-caption{display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--nxe-ui-border, #ddd);background:transparent;color:var(--nxe-ui-text, #333);cursor:pointer;padding:0}.nxe-calendar-nav:hover,.nxe-calendar-caption:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}.nxe-calendar-nav:active,.nxe-calendar-caption:active{background-color:var(--nxe-ui-active-bg, rgba(0, 0, 0, .24))}.nxe-calendar-nav{flex:0 0 36px}.nxe-calendar-nav:first-child{border-radius:4px 0 0 4px}.nxe-calendar-nav:last-child{border-radius:0 4px 4px 0}.nxe-calendar-caption{flex:1;margin:0 -1px;font-size:inherit;font-weight:600}.nxe-calendar-chevron{width:18px;height:18px;fill:currentColor}.nxe-calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);align-items:center;height:24px;text-align:center;color:var(--nxe-ui-text, #333);font-size:10px;font-weight:500}.nxe-calendar-grid{display:grid;grid-template-columns:repeat(7,1fr)}.nxe-calendar-day,.nxe-calendar-period{display:flex;align-items:center;justify-content:center;border:0;background:transparent;cursor:pointer;padding:0;font-size:14px;line-height:1;color:var(--nxe-ui-text, #333)}.nxe-calendar-day span,.nxe-calendar-period span{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border-radius:4px}.nxe-calendar-day:hover:not(:disabled) span,.nxe-calendar-period:hover:not(:disabled) span{background:var(--nxe-ui-accent-tint, rgba(51, 122, 183, .24))}.nxe-calendar-day:disabled,.nxe-calendar-period:disabled{cursor:default;color:var(--nxe-ui-text-disabled, rgba(51, 51, 51, .38))}.nxe-calendar-day{height:33px}.nxe-calendar-day span{width:28px;height:28px}.nxe-calendar-periods{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:74px}.nxe-calendar-period span{min-width:54px;height:54px;padding:0 4px}.nxe-calendar-periods-century .nxe-calendar-period span{width:40px;min-width:0;height:auto;min-height:56px;padding:4px 0;white-space:normal;text-align:center;line-height:1.4}.nxe-calendar-other{color:var(--nxe-ui-text-disabled, rgba(51, 51, 51, .38))}.nxe-calendar-today span{border:2px solid var(--nxe-ui-accent, #337ab7)}.nxe-calendar-selected span,.nxe-calendar-selected:hover:not(:disabled) span{background:var(--nxe-ui-accent, #337ab7);color:var(--nxe-ui-accent-contrast, #fff);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5));border:0}.nxe-calendar-time{display:flex;align-items:center;gap:4px;margin-top:8px;padding-top:8px;border-top:1px solid var(--nxe-ui-border, #ddd)}.nxe-calendar-time-part{width:2.6em;height:2.2em;box-sizing:border-box;padding:0 .25em;border:1px solid var(--nxe-ui-border, #ddd);border-radius:4px;background:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);font-size:inherit;font-variant-numeric:tabular-nums;text-align:center}.nxe-calendar-time-part:focus{outline:2px solid var(--nxe-ui-accent, #337ab7);outline-offset:-2px}.nxe-calendar-time-sep{font-weight:600}.nxe-calendar-time-btn{height:2.2em;padding:0 .75em;border:1px solid var(--nxe-ui-border, #ddd);border-radius:4px;background:transparent;color:var(--nxe-ui-text, #333);font-size:inherit;cursor:pointer}.nxe-calendar-time-btn:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}.nxe-calendar-time-btn:focus-visible{outline:2px solid var(--nxe-ui-accent, #337ab7);outline-offset:-2px}.nxe-calendar-time-done{margin-left:auto}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1249
1745
  }
1250
1746
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDateBoxComponent, decorators: [{
1251
1747
  type: Component,
@@ -1258,7 +1754,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1258
1754
  [id]="controlId()"
1259
1755
  [attr.aria-label]="ariaLabel() || null"
1260
1756
  [attr.aria-labelledby]="ariaLabelledBy() || null"
1261
- [attr.aria-invalid]="validationError() ? true : null"
1757
+ [attr.aria-invalid]="invalidState() ? true : null"
1262
1758
  [attr.aria-required]="required() ? true : null"
1263
1759
  type="text"
1264
1760
  [value]="displayValue()"
@@ -1360,7 +1856,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1360
1856
  </div>
1361
1857
  @if (zoomLevel() === 'month') {
1362
1858
  <div class="nxe-calendar-weekdays">
1363
- @for (wd of weekdays; track wd) {
1859
+ @for (wd of weekdays(); track $index) {
1364
1860
  <span>{{ wd }}</span>
1365
1861
  }
1366
1862
  </div>
@@ -1405,7 +1901,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1405
1901
  type="text"
1406
1902
  inputmode="numeric"
1407
1903
  maxlength="2"
1408
- aria-label="Hour"
1904
+ [attr.aria-label]="nxeMessages.t('date.hour')"
1409
1905
  [value]="hourText()"
1410
1906
  (change)="setTimePart('hour', $event)" />
1411
1907
  <span class="nxe-calendar-time-sep" aria-hidden="true">:</span>
@@ -1414,12 +1910,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1414
1910
  type="text"
1415
1911
  inputmode="numeric"
1416
1912
  maxlength="2"
1417
- aria-label="Minute"
1913
+ [attr.aria-label]="nxeMessages.t('date.minute')"
1418
1914
  [value]="minuteText()"
1419
1915
  (change)="setTimePart('minute', $event)" />
1420
- <button type="button" class="nxe-calendar-time-btn" (click)="setNow()">Now</button>
1916
+ <button type="button" class="nxe-calendar-time-btn" (click)="setNow()">{{ nxeMessages.t('date.now') }}</button>
1421
1917
  <button type="button" class="nxe-calendar-time-btn nxe-calendar-time-done" (click)="close()">
1422
- Done
1918
+ {{ nxeMessages.t('date.done') }}
1423
1919
  </button>
1424
1920
  </div>
1425
1921
  }
@@ -1428,13 +1924,634 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
1428
1924
  `, host: {
1429
1925
  class: 'nxe-widget nxe-editor nxe-datebox',
1430
1926
  '[class.nxe-focused]': 'focused()',
1431
- '[class.nxe-invalid]': '!!validationError()',
1927
+ '[class.nxe-invalid]': 'invalidState()',
1432
1928
  }, providers: [
1433
1929
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDateBoxComponent), multi: true },
1434
1930
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDateBoxComponent) },
1435
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-date-box .nxe-datebox-button{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:#333;cursor:pointer}nxe-date-box .nxe-datebox-button svg{display:block;width:18px;height:18px;fill:currentColor}nxe-date-box:hover:not(.nxe-disabled) .nxe-datebox-button,nxe-date-box:active:not(.nxe-disabled) .nxe-datebox-button{background-color:#e6e6e6}nxe-date-box .nxe-datebox-button.nxe-datebox-open{background-color:#b3b3b3}nxe-date-box .nxe-datebox-icon{width:18px;height:18px;fill:currentColor}nxe-date-box .nxe-editor-container:has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 38px}nxe-date-box .nxe-editor-container:has(>.nxe-clear-button-area):has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 72px}nxe-date-box.nxe-readonly .nxe-datebox-button,nxe-date-box.nxe-disabled .nxe-datebox-button{cursor:default}nxe-date-box.nxe-readonly .nxe-datebox-button:hover,nxe-date-box.nxe-disabled .nxe-datebox-button:hover{background-color:transparent}.nxe-calendar{width:298px;box-sizing:border-box;padding:8px;background:#fff;border:1px solid #ddd;border-radius:6px;box-shadow:0 6px 12px #0000002e;color:#333;font-size:var(--nxe-ui-font-size, 12px);-webkit-user-select:none;user-select:none}.nxe-calendar-navigator{display:flex;height:36px;margin-bottom:8px}.nxe-calendar-nav,.nxe-calendar-caption{display:inline-flex;align-items:center;justify-content:center;border:1px solid #ddd;background:transparent;color:#333;cursor:pointer;padding:0}.nxe-calendar-nav:hover,.nxe-calendar-caption:hover{background:#0000000a}.nxe-calendar-nav:active,.nxe-calendar-caption:active{background:#0000003d}.nxe-calendar-nav{flex:0 0 36px}.nxe-calendar-nav:first-child{border-radius:4px 0 0 4px}.nxe-calendar-nav:last-child{border-radius:0 4px 4px 0}.nxe-calendar-caption{flex:1;margin:0 -1px;font-size:inherit;font-weight:600}.nxe-calendar-chevron{width:18px;height:18px;fill:currentColor}.nxe-calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);align-items:center;height:24px;text-align:center;color:#333;font-size:10px;font-weight:500}.nxe-calendar-grid{display:grid;grid-template-columns:repeat(7,1fr)}.nxe-calendar-day,.nxe-calendar-period{display:flex;align-items:center;justify-content:center;border:0;background:transparent;cursor:pointer;padding:0;font-size:14px;line-height:1;color:#333}.nxe-calendar-day span,.nxe-calendar-period span{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border-radius:4px}.nxe-calendar-day:hover:not(:disabled) span,.nxe-calendar-period:hover:not(:disabled) span{background:#337ab73d}.nxe-calendar-day:disabled,.nxe-calendar-period:disabled{cursor:default;color:#33333361}.nxe-calendar-day{height:33px}.nxe-calendar-day span{width:28px;height:28px}.nxe-calendar-periods{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:74px}.nxe-calendar-period span{min-width:54px;height:54px;padding:0 4px}.nxe-calendar-periods-century .nxe-calendar-period span{width:40px;min-width:0;height:auto;min-height:56px;padding:4px 0;white-space:normal;text-align:center;line-height:1.4}.nxe-calendar-other{color:#33333361}.nxe-calendar-today span{border:2px solid #337ab7}.nxe-calendar-selected span,.nxe-calendar-selected:hover:not(:disabled) span{background:#337ab7;color:#fff;box-shadow:0 0 0 2px #337ab780;border:0}.nxe-calendar-time{display:flex;align-items:center;gap:4px;margin-top:8px;padding-top:8px;border-top:1px solid #ddd}.nxe-calendar-time-part{width:2.6em;height:2.2em;box-sizing:border-box;padding:0 .25em;border:1px solid #ddd;border-radius:4px;background:#fff;color:#333;font-size:inherit;font-variant-numeric:tabular-nums;text-align:center}.nxe-calendar-time-part:focus{outline:2px solid #337ab7;outline-offset:-2px}.nxe-calendar-time-sep{font-weight:600}.nxe-calendar-time-btn{height:2.2em;padding:0 .75em;border:1px solid #ddd;border-radius:4px;background:transparent;color:#333;font-size:inherit;cursor:pointer}.nxe-calendar-time-btn:hover{background:#0000000a}.nxe-calendar-time-btn:focus-visible{outline:2px solid #337ab7;outline-offset:-2px}.nxe-calendar-time-done{margin-left:auto}\n"] }]
1931
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-date-box .nxe-datebox-button{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-date-box .nxe-datebox-button svg{display:block;width:18px;height:18px;fill:currentColor}nxe-date-box:hover:not(.nxe-disabled) .nxe-datebox-button,nxe-date-box:active:not(.nxe-disabled) .nxe-datebox-button{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-date-box .nxe-datebox-button.nxe-datebox-open{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-date-box .nxe-datebox-icon{width:18px;height:18px;fill:currentColor}nxe-date-box .nxe-editor-container:has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 38px}nxe-date-box .nxe-editor-container:has(>.nxe-clear-button-area):has(>.nxe-datebox-button){--nxe-invalid-badge-inset: 72px}nxe-date-box.nxe-readonly .nxe-datebox-button,nxe-date-box.nxe-disabled .nxe-datebox-button{cursor:default}nxe-date-box.nxe-readonly .nxe-datebox-button:hover,nxe-date-box.nxe-disabled .nxe-datebox-button:hover{background-color:transparent}.nxe-calendar{width:298px;box-sizing:border-box;padding:8px;background:var(--nxe-ui-surface, #fff);border:1px solid var(--nxe-ui-border, #ddd);border-radius:6px;box-shadow:var(--nxe-ui-popup-shadow, 0 6px 12px rgba(0, 0, 0, .18));color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size, 12px);-webkit-user-select:none;user-select:none}.nxe-calendar-navigator{display:flex;height:36px;margin-bottom:8px}.nxe-calendar-nav,.nxe-calendar-caption{display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--nxe-ui-border, #ddd);background:transparent;color:var(--nxe-ui-text, #333);cursor:pointer;padding:0}.nxe-calendar-nav:hover,.nxe-calendar-caption:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}.nxe-calendar-nav:active,.nxe-calendar-caption:active{background-color:var(--nxe-ui-active-bg, rgba(0, 0, 0, .24))}.nxe-calendar-nav{flex:0 0 36px}.nxe-calendar-nav:first-child{border-radius:4px 0 0 4px}.nxe-calendar-nav:last-child{border-radius:0 4px 4px 0}.nxe-calendar-caption{flex:1;margin:0 -1px;font-size:inherit;font-weight:600}.nxe-calendar-chevron{width:18px;height:18px;fill:currentColor}.nxe-calendar-weekdays{display:grid;grid-template-columns:repeat(7,1fr);align-items:center;height:24px;text-align:center;color:var(--nxe-ui-text, #333);font-size:10px;font-weight:500}.nxe-calendar-grid{display:grid;grid-template-columns:repeat(7,1fr)}.nxe-calendar-day,.nxe-calendar-period{display:flex;align-items:center;justify-content:center;border:0;background:transparent;cursor:pointer;padding:0;font-size:14px;line-height:1;color:var(--nxe-ui-text, #333)}.nxe-calendar-day span,.nxe-calendar-period span{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border-radius:4px}.nxe-calendar-day:hover:not(:disabled) span,.nxe-calendar-period:hover:not(:disabled) span{background:var(--nxe-ui-accent-tint, rgba(51, 122, 183, .24))}.nxe-calendar-day:disabled,.nxe-calendar-period:disabled{cursor:default;color:var(--nxe-ui-text-disabled, rgba(51, 51, 51, .38))}.nxe-calendar-day{height:33px}.nxe-calendar-day span{width:28px;height:28px}.nxe-calendar-periods{display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:74px}.nxe-calendar-period span{min-width:54px;height:54px;padding:0 4px}.nxe-calendar-periods-century .nxe-calendar-period span{width:40px;min-width:0;height:auto;min-height:56px;padding:4px 0;white-space:normal;text-align:center;line-height:1.4}.nxe-calendar-other{color:var(--nxe-ui-text-disabled, rgba(51, 51, 51, .38))}.nxe-calendar-today span{border:2px solid var(--nxe-ui-accent, #337ab7)}.nxe-calendar-selected span,.nxe-calendar-selected:hover:not(:disabled) span{background:var(--nxe-ui-accent, #337ab7);color:var(--nxe-ui-accent-contrast, #fff);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5));border:0}.nxe-calendar-time{display:flex;align-items:center;gap:4px;margin-top:8px;padding-top:8px;border-top:1px solid var(--nxe-ui-border, #ddd)}.nxe-calendar-time-part{width:2.6em;height:2.2em;box-sizing:border-box;padding:0 .25em;border:1px solid var(--nxe-ui-border, #ddd);border-radius:4px;background:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);font-size:inherit;font-variant-numeric:tabular-nums;text-align:center}.nxe-calendar-time-part:focus{outline:2px solid var(--nxe-ui-accent, #337ab7);outline-offset:-2px}.nxe-calendar-time-sep{font-weight:600}.nxe-calendar-time-btn{height:2.2em;padding:0 .75em;border:1px solid var(--nxe-ui-border, #ddd);border-radius:4px;background:transparent;color:var(--nxe-ui-text, #333);font-size:inherit;cursor:pointer}.nxe-calendar-time-btn:hover{background:var(--nxe-ui-hover-bg, rgba(0, 0, 0, .04))}.nxe-calendar-time-btn:focus-visible{outline:2px solid var(--nxe-ui-accent, #337ab7);outline-offset:-2px}.nxe-calendar-time-done{margin-left:auto}\n"] }]
1436
1932
  }], propDecorators: { calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], showClearButtonInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }] } });
1437
1933
 
1934
+ /**
1935
+ * The clock the box reads for "today": the highlighted day, where the popup opens and what the
1936
+ * built-in presets count back from. Provide a fixed one in a test:
1937
+ *
1938
+ * ```ts
1939
+ * { provide: NXE_DATE_RANGE_NOW, useValue: () => DateTime.fromISO('2026-10-07T09:30:00') }
1940
+ * ```
1941
+ */
1942
+ const NXE_DATE_RANGE_NOW = new InjectionToken('NXE_DATE_RANGE_NOW', {
1943
+ providedIn: 'root',
1944
+ factory: () => () => DateTime.now(),
1945
+ });
1946
+ /** Default `displayFormat`: `1 Okt 2026` in Indonesian, `1 Oct 2026` in English. */
1947
+ const NXE_DATE_RANGE_DISPLAY_FORMAT = 'd MMM yyyy';
1948
+ /** Default `valueFormat`: date-only. */
1949
+ const NXE_DATE_RANGE_VALUE_FORMAT = 'yyyy-MM-dd';
1950
+ /**
1951
+ * The built-in presets. Each call returns a fresh entry whose label comes from the message bundle
1952
+ * and whose range counts from the box's clock. Weeks are not offered: the first day of the week is
1953
+ * a locale question this library does not decide.
1954
+ */
1955
+ const nxeDateRangePresets = {
1956
+ today: () => ({
1957
+ labelKey: 'dateRange.preset.today',
1958
+ range: (now) => ({ start: now.startOf('day'), end: now.startOf('day') }),
1959
+ }),
1960
+ yesterday: () => ({
1961
+ labelKey: 'dateRange.preset.yesterday',
1962
+ range: (now) => {
1963
+ const day = now.startOf('day').minus({ days: 1 });
1964
+ return { start: day, end: day };
1965
+ },
1966
+ }),
1967
+ /** Today and the six days before it. */
1968
+ last7Days: () => ({
1969
+ labelKey: 'dateRange.preset.last7Days',
1970
+ range: (now) => ({ start: now.startOf('day').minus({ days: 6 }), end: now.startOf('day') }),
1971
+ }),
1972
+ /** Today and the 29 days before it. */
1973
+ last30Days: () => ({
1974
+ labelKey: 'dateRange.preset.last30Days',
1975
+ range: (now) => ({ start: now.startOf('day').minus({ days: 29 }), end: now.startOf('day') }),
1976
+ }),
1977
+ /** The first of the month to its last day (not to today). */
1978
+ thisMonth: () => ({
1979
+ labelKey: 'dateRange.preset.thisMonth',
1980
+ range: (now) => ({ start: now.startOf('month'), end: now.endOf('month').startOf('day') }),
1981
+ }),
1982
+ lastMonth: () => ({
1983
+ labelKey: 'dateRange.preset.lastMonth',
1984
+ range: (now) => {
1985
+ const last = now.minus({ months: 1 });
1986
+ return { start: last.startOf('month'), end: last.endOf('month').startOf('day') };
1987
+ },
1988
+ }),
1989
+ /** 1 January to 31 December. */
1990
+ thisYear: () => ({
1991
+ labelKey: 'dateRange.preset.thisYear',
1992
+ range: (now) => ({ start: now.startOf('year'), end: now.endOf('year').startOf('day') }),
1993
+ }),
1994
+ /** All seven, in the order above. */
1995
+ all: () => [
1996
+ nxeDateRangePresets.today(),
1997
+ nxeDateRangePresets.yesterday(),
1998
+ nxeDateRangePresets.last7Days(),
1999
+ nxeDateRangePresets.last30Days(),
2000
+ nxeDateRangePresets.thisMonth(),
2001
+ nxeDateRangePresets.lastMonth(),
2002
+ nxeDateRangePresets.thisYear(),
2003
+ ],
2004
+ };
2005
+
2006
+ /** Below this width the popup shows one month and stacks the presets above it. */
2007
+ const NARROW_QUERY = '(max-width: 719.98px)';
2008
+ let rangeBoxSequence = 0;
2009
+ function toBound(bound) {
2010
+ if (!bound) {
2011
+ return null;
2012
+ }
2013
+ return parseDateValue(bound)?.startOf('day') ?? null;
2014
+ }
2015
+ function dayKey(dt) {
2016
+ return dt.toFormat('yyyy-MM-dd');
2017
+ }
2018
+ /**
2019
+ * `nxe-date-range-box` picks a start and an end date in one field.
2020
+ *
2021
+ * ```html
2022
+ * <nxe-form-field label="Tanggal PO">
2023
+ * <nxe-date-range-box formControlName="poDate" [presets]="presets" min="2020-01-01" />
2024
+ * </nxe-form-field>
2025
+ * ```
2026
+ *
2027
+ * The value is `{ start, end }`, each a string in `valueFormat` (`yyyy-MM-dd` by default; with a time
2028
+ * format the start is the first second of its day and the end the last), `null` while that end is
2029
+ * empty, and `null` as a whole when both are. The field shows `1 Okt 2026 - 31 Okt 2026` and opens
2030
+ * a popup: two months side by side (one under 720px), a quick-range list, and a typed input for each
2031
+ * end. The first click picks the start, the second the end; the days in between preview under the
2032
+ * pointer, and a second click before the first swaps the two.
2033
+ *
2034
+ * Month and weekday names follow the active language of `NxeMessageService`. The field is a
2035
+ * read-only combobox, so a label, `aria-invalid` and `aria-describedby` from `nxe-form-field` reach
2036
+ * it; the popup is a dialog with the days in an ARIA grid (arrows, Home and End by week, PageUp and
2037
+ * PageDown by month, Enter or Space to pick, Esc closes and returns focus to the field).
2038
+ */
2039
+ class NxeDateRangeBoxComponent extends NxeFormControlBase {
2040
+ overlay = inject(Overlay);
2041
+ viewContainerRef = inject(ViewContainerRef);
2042
+ injector = inject(Injector);
2043
+ clock = inject(NXE_DATE_RANGE_NOW);
2044
+ breakpoints = inject(BreakpointObserver);
2045
+ popupTemplate = viewChild.required('popupTemplate', /* @ts-ignore */
2046
+ ...(ngDevMode ? [{ debugName: "popupTemplate" }] : /* istanbul ignore next */ []));
2047
+ fieldRef = viewChild.required('field', /* @ts-ignore */
2048
+ ...(ngDevMode ? [{ debugName: "fieldRef" }] : /* istanbul ignore next */ []));
2049
+ /** How the dates read in the field and in the typed inputs: a Luxon format. */
2050
+ displayFormat = input(NXE_DATE_RANGE_DISPLAY_FORMAT, /* @ts-ignore */
2051
+ ...(ngDevMode ? [{ debugName: "displayFormat" }] : /* istanbul ignore next */ []));
2052
+ /** How each end is written to the value. Date-only unless it carries time tokens. */
2053
+ valueFormat = input(NXE_DATE_RANGE_VALUE_FORMAT, /* @ts-ignore */
2054
+ ...(ngDevMode ? [{ debugName: "valueFormat" }] : /* istanbul ignore next */ []));
2055
+ /** Days before this are disabled, and a typed or preset date before it moves to it. */
2056
+ min = input(null, /* @ts-ignore */
2057
+ ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
2058
+ /** Days after this are disabled, and a typed or preset date after it moves to it. */
2059
+ max = input(null, /* @ts-ignore */
2060
+ ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
2061
+ /** The quick-range list. Empty draws none. See `nxeDateRangePresets` for the built-in ones. */
2062
+ presets = input([], /* @ts-ignore */
2063
+ ...(ngDevMode ? [{ debugName: "presets" }] : /* istanbul ignore next */ []));
2064
+ /** Shown in the field and the start input while the start is empty. Defaults to the message bundle's. */
2065
+ startPlaceholder = input(undefined, /* @ts-ignore */
2066
+ ...(ngDevMode ? [{ debugName: "startPlaceholder" }] : /* istanbul ignore next */ []));
2067
+ /** Shown in the field and the end input while the end is empty. Defaults to the message bundle's. */
2068
+ endPlaceholder = input(undefined, /* @ts-ignore */
2069
+ ...(ngDevMode ? [{ debugName: "endPlaceholder" }] : /* istanbul ignore next */ []));
2070
+ /** The clear area in the field. On by default: a filter is cleared more often than a form field. */
2071
+ showClearButton = input(true, { ...(ngDevMode ? { debugName: "showClearButton" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
2072
+ focused = signal(false, /* @ts-ignore */
2073
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
2074
+ /** Whether the popup is showing. */
2075
+ opened = signal(false, /* @ts-ignore */
2076
+ ...(ngDevMode ? [{ debugName: "opened" }] : /* istanbul ignore next */ []));
2077
+ uid = `nxe-daterange-${++rangeBoxSequence}`;
2078
+ popupId = `${this.uid}-popup`;
2079
+ isNarrow = toSignal(this.breakpoints.observe(NARROW_QUERY).pipe(map((state) => state.matches)), {
2080
+ initialValue: this.breakpoints.isMatched(NARROW_QUERY),
2081
+ });
2082
+ monthsShown = computed(() => (this.isNarrow() ? 1 : 2), /* @ts-ignore */
2083
+ ...(ngDevMode ? [{ debugName: "monthsShown" }] : /* istanbul ignore next */ []));
2084
+ locale = computed(() => this.nxeMessages.locale(), /* @ts-ignore */
2085
+ ...(ngDevMode ? [{ debugName: "locale" }] : /* istanbul ignore next */ []));
2086
+ minDate = computed(() => toBound(this.min()), /* @ts-ignore */
2087
+ ...(ngDevMode ? [{ debugName: "minDate" }] : /* istanbul ignore next */ []));
2088
+ maxDate = computed(() => toBound(this.max()), /* @ts-ignore */
2089
+ ...(ngDevMode ? [{ debugName: "maxDate" }] : /* istanbul ignore next */ []));
2090
+ /** The committed value as days in the active locale. */
2091
+ range = computed(() => {
2092
+ const value = this.value();
2093
+ const locale = this.locale();
2094
+ return {
2095
+ start: parseDateValue(value?.start)?.startOf('day').setLocale(locale) ?? null,
2096
+ end: parseDateValue(value?.end)?.startOf('day').setLocale(locale) ?? null,
2097
+ };
2098
+ }, /* @ts-ignore */
2099
+ ...(ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []));
2100
+ startText = computed(() => formatDisplay(this.range().start, this.displayFormat(), this.locale()), /* @ts-ignore */
2101
+ ...(ngDevMode ? [{ debugName: "startText" }] : /* istanbul ignore next */ []));
2102
+ endText = computed(() => formatDisplay(this.range().end, this.displayFormat(), this.locale()), /* @ts-ignore */
2103
+ ...(ngDevMode ? [{ debugName: "endText" }] : /* istanbul ignore next */ []));
2104
+ startHint = computed(() => this.startPlaceholder() ?? this.nxeMessages.t('dateRange.startPlaceholder'), /* @ts-ignore */
2105
+ ...(ngDevMode ? [{ debugName: "startHint" }] : /* istanbul ignore next */ []));
2106
+ endHint = computed(() => this.endPlaceholder() ?? this.nxeMessages.t('dateRange.endPlaceholder'), /* @ts-ignore */
2107
+ ...(ngDevMode ? [{ debugName: "endHint" }] : /* istanbul ignore next */ []));
2108
+ /** `1 Okt 2026 - 31 Okt 2026`; an empty end leaves the dash hanging so a half-set range reads as one. */
2109
+ displayValue = computed(() => {
2110
+ const start = this.startText();
2111
+ const end = this.endText();
2112
+ return start || end ? `${start} – ${end}`.trim() : '';
2113
+ }, /* @ts-ignore */
2114
+ ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
2115
+ fieldPlaceholder = computed(() => `${this.startHint()} – ${this.endHint()}`, /* @ts-ignore */
2116
+ ...(ngDevMode ? [{ debugName: "fieldPlaceholder" }] : /* istanbul ignore next */ []));
2117
+ clearButtonVisible = computed(() => this.showClearButton() && !this.readOnly() && !this.disabled() && this.displayValue() !== '', /* @ts-ignore */
2118
+ ...(ngDevMode ? [{ debugName: "clearButtonVisible" }] : /* istanbul ignore next */ []));
2119
+ // --- popup state ---
2120
+ /** The first click of a range in progress; null when none is. */
2121
+ draftStart = signal(null, /* @ts-ignore */
2122
+ ...(ngDevMode ? [{ debugName: "draftStart" }] : /* istanbul ignore next */ []));
2123
+ /** The day under the pointer or the keyboard while a range is in progress. */
2124
+ hovered = signal(null, /* @ts-ignore */
2125
+ ...(ngDevMode ? [{ debugName: "hovered" }] : /* istanbul ignore next */ []));
2126
+ /** The day the grid's single tab stop is on. */
2127
+ activeDate = signal(DateTime.now(), /* @ts-ignore */
2128
+ ...(ngDevMode ? [{ debugName: "activeDate" }] : /* istanbul ignore next */ []));
2129
+ /** The first month shown. */
2130
+ viewMonth = signal(DateTime.now().startOf('month'), /* @ts-ignore */
2131
+ ...(ngDevMode ? [{ debugName: "viewMonth" }] : /* istanbul ignore next */ []));
2132
+ weekdays = computed(() => {
2133
+ const locale = this.locale();
2134
+ const short = Info.weekdays('short', { locale });
2135
+ const long = Info.weekdays('long', { locale });
2136
+ // Luxon starts on Monday; the grid starts on Sunday, as nxe-date-box's does.
2137
+ return [{ short: short[6], long: long[6] }, ...short.slice(0, 6).map((s, i) => ({ short: s, long: long[i] }))];
2138
+ }, /* @ts-ignore */
2139
+ ...(ngDevMode ? [{ debugName: "weekdays" }] : /* istanbul ignore next */ []));
2140
+ presetResults = computed(() => {
2141
+ const now = this.clock().setLocale(this.locale());
2142
+ return this.presets().map((preset) => {
2143
+ const bounds = preset.range(now);
2144
+ return {
2145
+ label: preset.label ?? (preset.labelKey ? this.nxeMessages.t(preset.labelKey) : ''),
2146
+ start: this.clampDay(this.toDay(bounds.start)),
2147
+ end: this.clampDay(this.toDay(bounds.end)),
2148
+ };
2149
+ });
2150
+ }, /* @ts-ignore */
2151
+ ...(ngDevMode ? [{ debugName: "presetResults" }] : /* istanbul ignore next */ []));
2152
+ presetItems = computed(() => {
2153
+ const { start, end } = this.range();
2154
+ return this.presetResults().map((preset, index) => ({
2155
+ index,
2156
+ label: preset.label,
2157
+ active: !!start &&
2158
+ !!end &&
2159
+ !!preset.start &&
2160
+ !!preset.end &&
2161
+ dayKey(preset.start) === dayKey(start) &&
2162
+ dayKey(preset.end) === dayKey(end),
2163
+ }));
2164
+ }, /* @ts-ignore */
2165
+ ...(ngDevMode ? [{ debugName: "presetItems" }] : /* istanbul ignore next */ []));
2166
+ /** The key of the grid's tab stop: the active day while it is on screen, else the first day shown. */
2167
+ tabKey = computed(() => {
2168
+ const first = this.viewMonth().startOf('month');
2169
+ const last = first.plus({ months: this.monthsShown() - 1 }).endOf('month');
2170
+ const active = this.activeDate();
2171
+ return active >= first && active <= last ? dayKey(active) : dayKey(first);
2172
+ }, /* @ts-ignore */
2173
+ ...(ngDevMode ? [{ debugName: "tabKey" }] : /* istanbul ignore next */ []));
2174
+ months = computed(() => {
2175
+ const locale = this.locale();
2176
+ const today = dayKey(this.clock());
2177
+ const min = this.minDate();
2178
+ const max = this.maxDate();
2179
+ const tabKey = this.tabKey();
2180
+ const draft = this.draftStart();
2181
+ const hover = this.hovered();
2182
+ let low;
2183
+ let high;
2184
+ if (draft) {
2185
+ low = draft;
2186
+ high = hover ?? draft;
2187
+ if (high < low) {
2188
+ [low, high] = [high, low];
2189
+ }
2190
+ }
2191
+ else {
2192
+ const { start, end } = this.range();
2193
+ low = start ?? end;
2194
+ high = end ?? start;
2195
+ }
2196
+ const lowKey = low ? dayKey(low) : null;
2197
+ const highKey = high ? dayKey(high) : null;
2198
+ return Array.from({ length: this.monthsShown() }, (_, offset) => {
2199
+ const month = this.viewMonth().plus({ months: offset }).setLocale(locale);
2200
+ const weeks = buildCalendarWeeks(month)
2201
+ .map((week) => week.map((day) => {
2202
+ if (day.month !== month.month) {
2203
+ return null;
2204
+ }
2205
+ const key = dayKey(day);
2206
+ return {
2207
+ date: day,
2208
+ key,
2209
+ day: day.day,
2210
+ label: day.setLocale(locale).toLocaleString(DateTime.DATE_FULL),
2211
+ disabled: (!!min && day < min) || (!!max && day > max),
2212
+ today: key === today,
2213
+ start: key === lowKey,
2214
+ end: key === highKey,
2215
+ inRange: !!lowKey && !!highKey && key >= lowKey && key <= highKey,
2216
+ preview: !!draft,
2217
+ tabbable: key === tabKey,
2218
+ };
2219
+ }))
2220
+ .filter((week) => week.some((cell) => cell !== null));
2221
+ return { key: dayKey(month.startOf('month')), caption: month.toFormat('LLLL yyyy'), weeks };
2222
+ });
2223
+ }, /* @ts-ignore */
2224
+ ...(ngDevMode ? [{ debugName: "months" }] : /* istanbul ignore next */ []));
2225
+ canGoBack = computed(() => {
2226
+ const min = this.minDate();
2227
+ return !min || this.viewMonth().startOf('month') > min.startOf('month');
2228
+ }, /* @ts-ignore */
2229
+ ...(ngDevMode ? [{ debugName: "canGoBack" }] : /* istanbul ignore next */ []));
2230
+ canGoForward = computed(() => {
2231
+ const max = this.maxDate();
2232
+ return !max || this.viewMonth().plus({ months: this.monthsShown() - 1 }).endOf('month') < max.startOf('month');
2233
+ }, /* @ts-ignore */
2234
+ ...(ngDevMode ? [{ debugName: "canGoForward" }] : /* istanbul ignore next */ []));
2235
+ /** The line under the grid, read out as it changes. */
2236
+ status = computed(() => this.nxeMessages.t(this.draftStart() ? 'dateRange.pickEnd' : 'dateRange.pickStart'), /* @ts-ignore */
2237
+ ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
2238
+ overlayRef;
2239
+ closeSub;
2240
+ flipSub;
2241
+ // --- field ---
2242
+ onFieldClick() {
2243
+ if (this.opened()) {
2244
+ this.close(true);
2245
+ }
2246
+ else {
2247
+ this.open();
2248
+ }
2249
+ }
2250
+ onFieldKeydown(event) {
2251
+ if (event.ctrlKey || event.metaKey) {
2252
+ return;
2253
+ }
2254
+ switch (event.key) {
2255
+ case 'Enter':
2256
+ case ' ':
2257
+ case 'ArrowDown':
2258
+ event.preventDefault();
2259
+ this.open();
2260
+ break;
2261
+ case 'Backspace':
2262
+ case 'Delete':
2263
+ if (!this.readOnly() && !this.disabled()) {
2264
+ event.preventDefault();
2265
+ this.clear(event);
2266
+ }
2267
+ break;
2268
+ }
2269
+ }
2270
+ onFocus() {
2271
+ this.focused.set(true);
2272
+ }
2273
+ onBlur() {
2274
+ this.focused.set(false);
2275
+ // Focus moving into the popup is not leaving the field; the popup marks it touched on close.
2276
+ if (!this.opened()) {
2277
+ this.markAsTouched();
2278
+ }
2279
+ }
2280
+ onClearClick(event) {
2281
+ this.clear(event);
2282
+ this.fieldRef().nativeElement.focus();
2283
+ }
2284
+ /** Empties the range. The value becomes `null`. */
2285
+ clear(event) {
2286
+ this.draftStart.set(null);
2287
+ this.hovered.set(null);
2288
+ this.commitRange(null, null, event);
2289
+ }
2290
+ // --- popup: day grid ---
2291
+ pick(cell, event) {
2292
+ if (cell.disabled) {
2293
+ return;
2294
+ }
2295
+ this.activeDate.set(cell.date);
2296
+ const draft = this.draftStart();
2297
+ if (!draft) {
2298
+ this.draftStart.set(cell.date);
2299
+ this.hovered.set(cell.date);
2300
+ return;
2301
+ }
2302
+ this.draftStart.set(null);
2303
+ this.hovered.set(null);
2304
+ this.commitRange(draft, cell.date, event);
2305
+ this.close(true);
2306
+ }
2307
+ onDayEnter(cell) {
2308
+ if (this.draftStart()) {
2309
+ this.hovered.set(cell.date);
2310
+ }
2311
+ }
2312
+ onDayFocus(cell) {
2313
+ this.activeDate.set(cell.date);
2314
+ if (this.draftStart()) {
2315
+ this.hovered.set(cell.date);
2316
+ }
2317
+ }
2318
+ /** The pointer left the grids: the preview falls back to the keyboard's day. */
2319
+ onGridLeave() {
2320
+ this.hovered.set(this.draftStart() ? this.activeDate() : null);
2321
+ }
2322
+ onDayKeydown(event, cell) {
2323
+ if (event.ctrlKey || event.metaKey || event.altKey) {
2324
+ return;
2325
+ }
2326
+ let target;
2327
+ switch (event.key) {
2328
+ case 'ArrowLeft':
2329
+ target = cell.date.minus({ days: 1 });
2330
+ break;
2331
+ case 'ArrowRight':
2332
+ target = cell.date.plus({ days: 1 });
2333
+ break;
2334
+ case 'ArrowUp':
2335
+ target = cell.date.minus({ days: 7 });
2336
+ break;
2337
+ case 'ArrowDown':
2338
+ target = cell.date.plus({ days: 7 });
2339
+ break;
2340
+ case 'Home':
2341
+ target = cell.date.minus({ days: cell.date.weekday % 7 });
2342
+ break;
2343
+ case 'End':
2344
+ target = cell.date.plus({ days: 6 - (cell.date.weekday % 7) });
2345
+ break;
2346
+ case 'PageUp':
2347
+ target = cell.date.minus(event.shiftKey ? { years: 1 } : { months: 1 });
2348
+ break;
2349
+ case 'PageDown':
2350
+ target = cell.date.plus(event.shiftKey ? { years: 1 } : { months: 1 });
2351
+ break;
2352
+ case 'Enter':
2353
+ case ' ':
2354
+ event.preventDefault();
2355
+ this.pick(cell, event);
2356
+ return;
2357
+ default:
2358
+ return;
2359
+ }
2360
+ event.preventDefault();
2361
+ this.moveTo(target);
2362
+ }
2363
+ moveTo(date) {
2364
+ this.activeDate.set(date);
2365
+ if (this.draftStart()) {
2366
+ this.hovered.set(date);
2367
+ }
2368
+ this.ensureVisible(date);
2369
+ this.focusDay(date);
2370
+ }
2371
+ ensureVisible(date) {
2372
+ const first = this.viewMonth().startOf('month');
2373
+ const last = first.plus({ months: this.monthsShown() - 1 }).endOf('month');
2374
+ if (date < first) {
2375
+ this.viewMonth.set(date.startOf('month'));
2376
+ }
2377
+ else if (date > last) {
2378
+ this.viewMonth.set(date.startOf('month').minus({ months: this.monthsShown() - 1 }));
2379
+ }
2380
+ }
2381
+ focusDay(date) {
2382
+ const selector = `[data-date="${dayKey(date)}"]`;
2383
+ const find = () => this.overlayRef?.overlayElement.querySelector(selector);
2384
+ const existing = find();
2385
+ if (existing) {
2386
+ existing.focus();
2387
+ }
2388
+ else {
2389
+ afterNextRender(() => find()?.focus(), { injector: this.injector });
2390
+ }
2391
+ }
2392
+ navigate(delta) {
2393
+ this.viewMonth.update((month) => month.plus({ months: delta }));
2394
+ }
2395
+ // --- popup: presets and typed input ---
2396
+ applyPreset(item, event) {
2397
+ const preset = this.presetResults()[item.index];
2398
+ if (!preset) {
2399
+ return;
2400
+ }
2401
+ this.draftStart.set(null);
2402
+ this.hovered.set(null);
2403
+ this.commitRange(preset.start, preset.end, event);
2404
+ this.close(true);
2405
+ }
2406
+ /** Commit the text typed into one end. Empty clears that end; unreadable text is put back. */
2407
+ commitTyped(which, event) {
2408
+ const element = event.target;
2409
+ const text = element.value.trim();
2410
+ const { start, end } = this.range();
2411
+ this.draftStart.set(null);
2412
+ this.hovered.set(null);
2413
+ if (!text) {
2414
+ this.commitRange(which === 'start' ? null : start, which === 'end' ? null : end, event);
2415
+ }
2416
+ else {
2417
+ const parsed = parseDisplay(text, this.displayFormat(), null, this.locale());
2418
+ if (parsed) {
2419
+ const day = this.clampDay(parsed.startOf('day').setLocale(this.locale()));
2420
+ this.commitRange(which === 'start' ? day : start, which === 'end' ? day : end, event);
2421
+ this.activeDate.set(day);
2422
+ this.ensureVisible(day);
2423
+ }
2424
+ }
2425
+ // The bound text is unchanged when the entry was rejected or clamped back, so repaint it.
2426
+ element.value = which === 'start' ? this.startText() : this.endText();
2427
+ }
2428
+ onTypedKeydown(which, event) {
2429
+ if (event.key === 'Enter') {
2430
+ event.preventDefault();
2431
+ this.commitTyped(which, event);
2432
+ }
2433
+ }
2434
+ onClearInPopup(event) {
2435
+ this.clear(event);
2436
+ this.close(true);
2437
+ }
2438
+ // --- value ---
2439
+ toDay(bound) {
2440
+ return parseDateValue(bound)?.startOf('day').setLocale(this.locale()) ?? null;
2441
+ }
2442
+ clampDay(day) {
2443
+ if (!day) {
2444
+ return null;
2445
+ }
2446
+ const min = this.minDate();
2447
+ const max = this.maxDate();
2448
+ if (min && day < min) {
2449
+ return min.setLocale(this.locale());
2450
+ }
2451
+ if (max && day > max) {
2452
+ return max.setLocale(this.locale());
2453
+ }
2454
+ return day;
2455
+ }
2456
+ serialize(day, edge) {
2457
+ const stamp = edge === 'start' ? day.startOf('day') : day.endOf('day').set({ millisecond: 0 });
2458
+ return stamp.setLocale('en').toFormat(this.valueFormat());
2459
+ }
2460
+ commitRange(start, end, event) {
2461
+ if (start && end && end < start) {
2462
+ [start, end] = [end, start];
2463
+ }
2464
+ const next = start || end
2465
+ ? {
2466
+ start: start ? this.serialize(start, 'start') : null,
2467
+ end: end ? this.serialize(end, 'end') : null,
2468
+ }
2469
+ : null;
2470
+ const current = this.value();
2471
+ if ((current?.start ?? null) === (next?.start ?? null) && (current?.end ?? null) === (next?.end ?? null)) {
2472
+ return;
2473
+ }
2474
+ this.commitValue(next, event);
2475
+ }
2476
+ // --- popup lifecycle ---
2477
+ /** Opens the popup on the start of the range (or today). No-op while open, disabled or read-only. */
2478
+ open() {
2479
+ if (this.disabled() || this.readOnly() || this.opened()) {
2480
+ return;
2481
+ }
2482
+ const { start, end } = this.range();
2483
+ const anchor = this.clampDay(start ?? end ?? this.clock().startOf('day').setLocale(this.locale()));
2484
+ this.draftStart.set(null);
2485
+ this.hovered.set(null);
2486
+ this.activeDate.set(anchor);
2487
+ this.viewMonth.set(anchor.startOf('month'));
2488
+ if (!this.overlayRef) {
2489
+ this.overlayRef = this.overlay.create({
2490
+ positionStrategy: this.overlay
2491
+ .position()
2492
+ .flexibleConnectedTo(this.elementRef)
2493
+ .withPositions([
2494
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top' },
2495
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom' },
2496
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top' },
2497
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom' },
2498
+ ])
2499
+ .withFlexibleDimensions(false)
2500
+ .withPush(true)
2501
+ .withViewportMargin(8),
2502
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
2503
+ hasBackdrop: true,
2504
+ backdropClass: 'cdk-overlay-transparent-backdrop',
2505
+ panelClass: nxePane('nxe-daterange-pane'),
2506
+ });
2507
+ this.flipSub = trackPaneFlip(this.overlayRef);
2508
+ }
2509
+ this.overlayRef.attach(new TemplatePortal(this.popupTemplate(), this.viewContainerRef));
2510
+ this.closeSub = merge(this.overlayRef.backdropClick().pipe(map(() => false)), this.overlayRef.keydownEvents().pipe(filter((event) => event.key === 'Escape'), map((event) => {
2511
+ event.preventDefault();
2512
+ return true;
2513
+ }))).subscribe((restoreFocus) => this.close(restoreFocus));
2514
+ this.opened.set(true);
2515
+ this.focusDay(anchor);
2516
+ }
2517
+ /** Closes the popup, dropping a range that was only half picked. `restoreFocus` returns focus to the field. */
2518
+ close(restoreFocus = true) {
2519
+ if (!this.overlayRef?.hasAttached()) {
2520
+ return;
2521
+ }
2522
+ this.overlayRef.detach();
2523
+ this.closeSub?.unsubscribe();
2524
+ this.draftStart.set(null);
2525
+ this.hovered.set(null);
2526
+ this.opened.set(false);
2527
+ this.markAsTouched();
2528
+ if (restoreFocus) {
2529
+ this.fieldRef().nativeElement.focus();
2530
+ }
2531
+ }
2532
+ ngOnDestroy() {
2533
+ this.closeSub?.unsubscribe();
2534
+ this.flipSub?.unsubscribe();
2535
+ this.overlayRef?.dispose();
2536
+ }
2537
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDateRangeBoxComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
2538
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeDateRangeBoxComponent, isStandalone: true, selector: "nxe-date-range-box", inputs: { displayFormat: { classPropertyName: "displayFormat", publicName: "displayFormat", isSignal: true, isRequired: false, transformFunction: null }, valueFormat: { classPropertyName: "valueFormat", publicName: "valueFormat", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, presets: { classPropertyName: "presets", publicName: "presets", isSignal: true, isRequired: false, transformFunction: null }, startPlaceholder: { classPropertyName: "startPlaceholder", publicName: "startPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, endPlaceholder: { classPropertyName: "endPlaceholder", publicName: "endPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, showClearButton: { classPropertyName: "showClearButton", publicName: "showClearButton", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-editor nxe-daterangebox" }, providers: [
2539
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDateRangeBoxComponent), multi: true },
2540
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDateRangeBoxComponent) },
2541
+ ], viewQueries: [{ propertyName: "popupTemplate", first: true, predicate: ["popupTemplate"], descendants: true, isSignal: true }, { propertyName: "fieldRef", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"nxe-editor-container\">\n <!-- A read-only combobox rather than a button: a native input takes its caption from\n <label for>, and nxe-form-field writes aria-invalid / aria-describedby onto it. -->\n <input\n #field\n class=\"nxe-editor-input nxe-daterange-input\"\n type=\"text\"\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n autocomplete=\"off\"\n readonly\n [id]=\"controlId()\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-expanded]=\"opened()\"\n [attr.aria-controls]=\"opened() ? popupId : null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.placeholder]=\"fieldPlaceholder()\"\n [value]=\"displayValue()\"\n [disabled]=\"disabled()\"\n (click)=\"onFieldClick()\"\n (keydown)=\"onFieldKeydown($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\" />\n @if (clearButtonVisible()) {\n <span class=\"nxe-clear-button-area\" aria-hidden=\"true\" (mousedown)=\"$event.preventDefault()\" (click)=\"onClearClick($event)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path\n fill-rule=\"evenodd\"\n transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n @if (!readOnly()) {\n <span class=\"nxe-daterange-button\" [class.nxe-daterange-open]=\"opened()\" aria-hidden=\"true\" (mousedown)=\"$event.preventDefault()\" (click)=\"onFieldClick()\">\n <svg class=\"nxe-daterange-icon\" viewBox=\"0 0 18 18\" focusable=\"false\">\n <rect x=\"3\" y=\"3\" width=\"12\" height=\"12\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <rect x=\"3\" y=\"3\" width=\"12\" height=\"3\" />\n <rect x=\"4\" y=\"1\" width=\"2\" height=\"1.5\" />\n <rect x=\"12\" y=\"1\" width=\"2\" height=\"1.5\" />\n <rect x=\"5\" y=\"9\" width=\"8\" height=\"2\" />\n </svg>\n </span>\n }\n</div>\n<ng-content />\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #popupTemplate>\n <div\n class=\"nxe-range-popup\"\n role=\"dialog\"\n cdkTrapFocus\n [id]=\"popupId\"\n [class.nxe-range-narrow]=\"isNarrow()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.dialog')\">\n @if (presetItems().length) {\n <div class=\"nxe-range-presets\" role=\"group\" [attr.aria-label]=\"nxeMessages.t('dateRange.presets')\">\n @for (preset of presetItems(); track preset.index) {\n <button\n type=\"button\"\n class=\"nxe-range-preset\"\n [class.nxe-range-preset-active]=\"preset.active\"\n [attr.aria-pressed]=\"preset.active\"\n (click)=\"applyPreset(preset, $event)\">\n {{ preset.label }}\n </button>\n }\n </div>\n }\n <div class=\"nxe-range-main\">\n <div class=\"nxe-range-inputs\">\n <input\n class=\"nxe-range-text\"\n type=\"text\"\n autocomplete=\"off\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.start')\"\n [attr.placeholder]=\"startHint()\"\n [value]=\"startText()\"\n (change)=\"commitTyped('start', $event)\"\n (keydown)=\"onTypedKeydown('start', $event)\" />\n <span class=\"nxe-range-dash\" aria-hidden=\"true\">&ndash;</span>\n <input\n class=\"nxe-range-text\"\n type=\"text\"\n autocomplete=\"off\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.end')\"\n [attr.placeholder]=\"endHint()\"\n [value]=\"endText()\"\n (change)=\"commitTyped('end', $event)\"\n (keydown)=\"onTypedKeydown('end', $event)\" />\n </div>\n <div class=\"nxe-range-months\" (mouseleave)=\"onGridLeave()\">\n @for (month of months(); track month.key; let first = $first; let last = $last) {\n <div class=\"nxe-range-month\">\n <div class=\"nxe-range-header\">\n @if (first) {\n <button\n type=\"button\"\n class=\"nxe-range-nav\"\n [disabled]=\"!canGoBack()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.previousMonth')\"\n (click)=\"navigate(-1)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\" aria-hidden=\"true\">\n <polygon points=\"4.875 9 11.875 2 13.125 3.25 7.375 9 13.125 14.75 11.875 16\" />\n </svg>\n </button>\n }\n <div class=\"nxe-range-caption\" aria-live=\"polite\">{{ month.caption }}</div>\n @if (last) {\n <button\n type=\"button\"\n class=\"nxe-range-nav\"\n [disabled]=\"!canGoForward()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.nextMonth')\"\n (click)=\"navigate(1)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\" aria-hidden=\"true\">\n <polygon points=\"13.125 9 6.125 2 4.875 3.25 10.625 9 4.875 14.75 6.125 16\" />\n </svg>\n </button>\n }\n </div>\n <table class=\"nxe-range-grid\" role=\"grid\" [attr.aria-label]=\"month.caption\">\n <thead>\n <tr>\n @for (weekday of weekdays(); track $index) {\n <th scope=\"col\" [attr.abbr]=\"weekday.long\">{{ weekday.short }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (week of month.weeks; track $index) {\n <tr>\n @for (cell of week; track $index) {\n @if (cell) {\n <td\n role=\"gridcell\"\n class=\"nxe-range-cell\"\n [class.nxe-range-in]=\"cell.inRange\"\n [class.nxe-range-start]=\"cell.start && cell.inRange\"\n [class.nxe-range-end]=\"cell.end && cell.inRange\"\n [class.nxe-range-preview]=\"cell.preview && cell.inRange\"\n [attr.aria-selected]=\"cell.inRange\">\n <button\n type=\"button\"\n class=\"nxe-range-day\"\n [class.nxe-range-today]=\"cell.today\"\n [class.nxe-range-edge]=\"(cell.start || cell.end) && cell.inRange\"\n [attr.data-date]=\"cell.key\"\n [attr.tabindex]=\"cell.tabbable ? 0 : -1\"\n [attr.aria-label]=\"cell.label\"\n [attr.aria-disabled]=\"cell.disabled ? true : null\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"pick(cell, $event)\"\n (mouseenter)=\"onDayEnter(cell)\"\n (focus)=\"onDayFocus(cell)\"\n (keydown)=\"onDayKeydown($event, cell)\"\n (keyup.space)=\"$event.preventDefault()\">\n <span>{{ cell.day }}</span>\n </button>\n </td>\n } @else {\n <td class=\"nxe-range-gap\" role=\"presentation\"></td>\n }\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n </div>\n <div class=\"nxe-range-footer\">\n <span class=\"nxe-range-status\" role=\"status\">{{ status() }}</span>\n <button type=\"button\" class=\"nxe-range-clear\" (click)=\"onClearInPopup($event)\">{{ nxeMessages.t('common.clear') }}</button>\n </div>\n </div>\n </div>\n</ng-template>\n", styles: ["nxe-date-range-box .nxe-daterange-input{cursor:pointer;text-overflow:ellipsis}nxe-date-range-box .nxe-daterange-button{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-date-range-box .nxe-daterange-button svg{display:block;width:18px;height:18px;fill:currentColor}nxe-date-range-box:hover:not(.nxe-disabled) .nxe-daterange-button,nxe-date-range-box:active:not(.nxe-disabled) .nxe-daterange-button{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-date-range-box .nxe-daterange-button.nxe-daterange-open{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-date-range-box .nxe-daterange-icon{width:18px;height:18px;fill:currentColor}nxe-date-range-box .nxe-editor-container:has(>.nxe-daterange-button){--nxe-invalid-badge-inset: 38px}nxe-date-range-box .nxe-editor-container:has(>.nxe-clear-button-area):has(>.nxe-daterange-button){--nxe-invalid-badge-inset: 72px}nxe-date-range-box.nxe-disabled .nxe-daterange-input,nxe-date-range-box.nxe-readonly .nxe-daterange-input{cursor:default}.cdk-overlay-pane.nxe-daterange-pane{margin:0}.nxe-range-popup{--nxe-range-cell: 36px;box-sizing:border-box;display:flex;max-width:calc(100vw - var(--nxe-ui-space-4));max-height:calc(100vh - var(--nxe-ui-space-4) * 2);overflow:auto;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-popup-radius);box-shadow:var(--nxe-ui-popup-shadow);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);-webkit-user-select:none;user-select:none}.nxe-range-popup.nxe-range-narrow{--nxe-range-cell: 38px;flex-direction:column}.nxe-range-presets{display:flex;flex:none;flex-direction:column;gap:2px;box-sizing:border-box;min-width:140px;padding:var(--nxe-ui-space-2);border-inline-end:1px solid var(--nxe-ui-border);background:var(--nxe-ui-surface-alt)}.nxe-range-narrow .nxe-range-presets{flex-flow:row wrap;min-width:0;border-inline-end:0;border-bottom:1px solid var(--nxe-ui-border)}.nxe-range-preset{padding:var(--nxe-ui-space-1) var(--nxe-ui-space-2);border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;text-align:start;white-space:nowrap;cursor:pointer}.nxe-range-preset:hover{background:var(--nxe-ui-hover-bg)}.nxe-range-preset:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-preset.nxe-range-preset-active{background:var(--nxe-ui-accent);color:var(--nxe-ui-accent-contrast)}.nxe-range-main{display:flex;flex-direction:column;gap:var(--nxe-ui-space-2);padding:var(--nxe-ui-space-2)}.nxe-range-inputs{display:flex;align-items:center;gap:var(--nxe-ui-space-2)}.nxe-range-text{flex:1 1 0;min-width:0;box-sizing:border-box;height:2.2em;padding:0 .5em;border:1px solid var(--nxe-ui-border);border-radius:4px;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);font:inherit;-webkit-user-select:text;user-select:text}.nxe-range-text:focus{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-text::placeholder{color:var(--nxe-ui-text-muted)}.nxe-range-months{display:flex;gap:var(--nxe-ui-space-3)}.nxe-range-month{flex:none}.nxe-range-header{display:flex;align-items:center;height:32px}.nxe-range-caption{flex:1;font-weight:600;text-align:center}.nxe-range-nav{display:inline-flex;flex:none;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer}.nxe-range-nav svg{width:18px;height:18px;fill:currentColor}.nxe-range-nav:hover:not(:disabled){background:var(--nxe-ui-hover-bg)}.nxe-range-nav:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-nav:disabled{color:var(--nxe-ui-text-disabled);cursor:default}.nxe-range-month:first-child:not(:last-child) .nxe-range-header:after,.nxe-range-month:last-child:not(:first-child) .nxe-range-header:before{content:\"\";flex:none;width:32px}.nxe-range-grid{border-collapse:collapse;table-layout:fixed;width:calc(var(--nxe-range-cell) * 7)}.nxe-range-grid th{height:24px;padding:0;color:var(--nxe-ui-text-muted);font-size:.85em;font-weight:500;text-align:center}.nxe-range-grid td{height:calc(var(--nxe-range-cell) - 4px);padding:0;text-align:center}.nxe-range-cell.nxe-range-in{background:var(--nxe-ui-accent-tint)}.nxe-range-cell.nxe-range-in.nxe-range-start{border-start-start-radius:16px;border-end-start-radius:16px}.nxe-range-cell.nxe-range-in.nxe-range-end{border-start-end-radius:16px;border-end-end-radius:16px}.nxe-range-day{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;padding:0;border:0;border-radius:16px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxe-range-day:hover:not([aria-disabled=true]){background:var(--nxe-ui-accent-tint)}.nxe-range-day:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-day[aria-disabled=true]{color:var(--nxe-ui-text-disabled);cursor:default}.nxe-range-day.nxe-range-today{box-shadow:inset 0 0 0 2px var(--nxe-ui-accent)}.nxe-range-day.nxe-range-edge,.nxe-range-day.nxe-range-edge:hover:not([aria-disabled=true]){background:var(--nxe-ui-accent);color:var(--nxe-ui-accent-contrast)}.nxe-range-footer{display:flex;align-items:center;justify-content:space-between;gap:var(--nxe-ui-space-2);padding-top:var(--nxe-ui-space-2);border-top:1px solid var(--nxe-ui-border)}.nxe-range-status{color:var(--nxe-ui-text-muted)}.nxe-range-clear{height:2.2em;padding:0 .75em;border:1px solid var(--nxe-ui-border);border-radius:4px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxe-range-clear:hover{background:var(--nxe-ui-hover-bg)}.nxe-range-clear:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}\n"], dependencies: [{ kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2542
+ }
2543
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDateRangeBoxComponent, decorators: [{
2544
+ type: Component,
2545
+ args: [{ selector: 'nxe-date-range-box', imports: [CdkTrapFocus], host: {
2546
+ class: 'nxe-widget nxe-editor nxe-daterangebox',
2547
+ '[class.nxe-focused]': 'focused()',
2548
+ '[class.nxe-invalid]': 'invalidState()',
2549
+ }, providers: [
2550
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeDateRangeBoxComponent), multi: true },
2551
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeDateRangeBoxComponent) },
2552
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-editor-container\">\n <!-- A read-only combobox rather than a button: a native input takes its caption from\n <label for>, and nxe-form-field writes aria-invalid / aria-describedby onto it. -->\n <input\n #field\n class=\"nxe-editor-input nxe-daterange-input\"\n type=\"text\"\n role=\"combobox\"\n aria-haspopup=\"dialog\"\n autocomplete=\"off\"\n readonly\n [id]=\"controlId()\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-expanded]=\"opened()\"\n [attr.aria-controls]=\"opened() ? popupId : null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [attr.placeholder]=\"fieldPlaceholder()\"\n [value]=\"displayValue()\"\n [disabled]=\"disabled()\"\n (click)=\"onFieldClick()\"\n (keydown)=\"onFieldKeydown($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\" />\n @if (clearButtonVisible()) {\n <span class=\"nxe-clear-button-area\" aria-hidden=\"true\" (mousedown)=\"$event.preventDefault()\" (click)=\"onClearClick($event)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\">\n <path\n fill-rule=\"evenodd\"\n transform=\"rotate(45 9 9)\"\n d=\"M9 1a8 8 0 1 1 0 16A8 8 0 1 1 9 1ZM8 3.343h2V8h4.657v2H10v4.657H8V10H3.343V8H8Z\" />\n </svg>\n </span>\n }\n @if (!readOnly()) {\n <span class=\"nxe-daterange-button\" [class.nxe-daterange-open]=\"opened()\" aria-hidden=\"true\" (mousedown)=\"$event.preventDefault()\" (click)=\"onFieldClick()\">\n <svg class=\"nxe-daterange-icon\" viewBox=\"0 0 18 18\" focusable=\"false\">\n <rect x=\"3\" y=\"3\" width=\"12\" height=\"12\" rx=\"1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" />\n <rect x=\"3\" y=\"3\" width=\"12\" height=\"3\" />\n <rect x=\"4\" y=\"1\" width=\"2\" height=\"1.5\" />\n <rect x=\"12\" y=\"1\" width=\"2\" height=\"1.5\" />\n <rect x=\"5\" y=\"9\" width=\"8\" height=\"2\" />\n </svg>\n </span>\n }\n</div>\n<ng-content />\n@if (validationError(); as error) {\n @if (focused()) {\n <div class=\"nxe-invalid-message\">{{ error }}</div>\n }\n}\n\n<ng-template #popupTemplate>\n <div\n class=\"nxe-range-popup\"\n role=\"dialog\"\n cdkTrapFocus\n [id]=\"popupId\"\n [class.nxe-range-narrow]=\"isNarrow()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.dialog')\">\n @if (presetItems().length) {\n <div class=\"nxe-range-presets\" role=\"group\" [attr.aria-label]=\"nxeMessages.t('dateRange.presets')\">\n @for (preset of presetItems(); track preset.index) {\n <button\n type=\"button\"\n class=\"nxe-range-preset\"\n [class.nxe-range-preset-active]=\"preset.active\"\n [attr.aria-pressed]=\"preset.active\"\n (click)=\"applyPreset(preset, $event)\">\n {{ preset.label }}\n </button>\n }\n </div>\n }\n <div class=\"nxe-range-main\">\n <div class=\"nxe-range-inputs\">\n <input\n class=\"nxe-range-text\"\n type=\"text\"\n autocomplete=\"off\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.start')\"\n [attr.placeholder]=\"startHint()\"\n [value]=\"startText()\"\n (change)=\"commitTyped('start', $event)\"\n (keydown)=\"onTypedKeydown('start', $event)\" />\n <span class=\"nxe-range-dash\" aria-hidden=\"true\">&ndash;</span>\n <input\n class=\"nxe-range-text\"\n type=\"text\"\n autocomplete=\"off\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.end')\"\n [attr.placeholder]=\"endHint()\"\n [value]=\"endText()\"\n (change)=\"commitTyped('end', $event)\"\n (keydown)=\"onTypedKeydown('end', $event)\" />\n </div>\n <div class=\"nxe-range-months\" (mouseleave)=\"onGridLeave()\">\n @for (month of months(); track month.key; let first = $first; let last = $last) {\n <div class=\"nxe-range-month\">\n <div class=\"nxe-range-header\">\n @if (first) {\n <button\n type=\"button\"\n class=\"nxe-range-nav\"\n [disabled]=\"!canGoBack()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.previousMonth')\"\n (click)=\"navigate(-1)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\" aria-hidden=\"true\">\n <polygon points=\"4.875 9 11.875 2 13.125 3.25 7.375 9 13.125 14.75 11.875 16\" />\n </svg>\n </button>\n }\n <div class=\"nxe-range-caption\" aria-live=\"polite\">{{ month.caption }}</div>\n @if (last) {\n <button\n type=\"button\"\n class=\"nxe-range-nav\"\n [disabled]=\"!canGoForward()\"\n [attr.aria-label]=\"nxeMessages.t('dateRange.nextMonth')\"\n (click)=\"navigate(1)\">\n <svg viewBox=\"0 0 18 18\" focusable=\"false\" aria-hidden=\"true\">\n <polygon points=\"13.125 9 6.125 2 4.875 3.25 10.625 9 4.875 14.75 6.125 16\" />\n </svg>\n </button>\n }\n </div>\n <table class=\"nxe-range-grid\" role=\"grid\" [attr.aria-label]=\"month.caption\">\n <thead>\n <tr>\n @for (weekday of weekdays(); track $index) {\n <th scope=\"col\" [attr.abbr]=\"weekday.long\">{{ weekday.short }}</th>\n }\n </tr>\n </thead>\n <tbody>\n @for (week of month.weeks; track $index) {\n <tr>\n @for (cell of week; track $index) {\n @if (cell) {\n <td\n role=\"gridcell\"\n class=\"nxe-range-cell\"\n [class.nxe-range-in]=\"cell.inRange\"\n [class.nxe-range-start]=\"cell.start && cell.inRange\"\n [class.nxe-range-end]=\"cell.end && cell.inRange\"\n [class.nxe-range-preview]=\"cell.preview && cell.inRange\"\n [attr.aria-selected]=\"cell.inRange\">\n <button\n type=\"button\"\n class=\"nxe-range-day\"\n [class.nxe-range-today]=\"cell.today\"\n [class.nxe-range-edge]=\"(cell.start || cell.end) && cell.inRange\"\n [attr.data-date]=\"cell.key\"\n [attr.tabindex]=\"cell.tabbable ? 0 : -1\"\n [attr.aria-label]=\"cell.label\"\n [attr.aria-disabled]=\"cell.disabled ? true : null\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"pick(cell, $event)\"\n (mouseenter)=\"onDayEnter(cell)\"\n (focus)=\"onDayFocus(cell)\"\n (keydown)=\"onDayKeydown($event, cell)\"\n (keyup.space)=\"$event.preventDefault()\">\n <span>{{ cell.day }}</span>\n </button>\n </td>\n } @else {\n <td class=\"nxe-range-gap\" role=\"presentation\"></td>\n }\n }\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n </div>\n <div class=\"nxe-range-footer\">\n <span class=\"nxe-range-status\" role=\"status\">{{ status() }}</span>\n <button type=\"button\" class=\"nxe-range-clear\" (click)=\"onClearInPopup($event)\">{{ nxeMessages.t('common.clear') }}</button>\n </div>\n </div>\n </div>\n</ng-template>\n", styles: ["nxe-date-range-box .nxe-daterange-input{cursor:pointer;text-overflow:ellipsis}nxe-date-range-box .nxe-daterange-button{box-sizing:border-box;position:relative;flex:0 0 34px;align-self:stretch;display:inline-flex;align-items:center;justify-content:center;width:34px;min-width:34px;padding:1px;border-radius:4px;background-clip:content-box;color:var(--nxe-ui-text, #333);cursor:pointer}nxe-date-range-box .nxe-daterange-button svg{display:block;width:18px;height:18px;fill:currentColor}nxe-date-range-box:hover:not(.nxe-disabled) .nxe-daterange-button,nxe-date-range-box:active:not(.nxe-disabled) .nxe-daterange-button{background-color:var(--nxe-ui-neutral-150, #e6e6e6)}nxe-date-range-box .nxe-daterange-button.nxe-daterange-open{background-color:var(--nxe-ui-neutral-350, #b3b3b3)}nxe-date-range-box .nxe-daterange-icon{width:18px;height:18px;fill:currentColor}nxe-date-range-box .nxe-editor-container:has(>.nxe-daterange-button){--nxe-invalid-badge-inset: 38px}nxe-date-range-box .nxe-editor-container:has(>.nxe-clear-button-area):has(>.nxe-daterange-button){--nxe-invalid-badge-inset: 72px}nxe-date-range-box.nxe-disabled .nxe-daterange-input,nxe-date-range-box.nxe-readonly .nxe-daterange-input{cursor:default}.cdk-overlay-pane.nxe-daterange-pane{margin:0}.nxe-range-popup{--nxe-range-cell: 36px;box-sizing:border-box;display:flex;max-width:calc(100vw - var(--nxe-ui-space-4));max-height:calc(100vh - var(--nxe-ui-space-4) * 2);overflow:auto;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-popup-radius);box-shadow:var(--nxe-ui-popup-shadow);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);-webkit-user-select:none;user-select:none}.nxe-range-popup.nxe-range-narrow{--nxe-range-cell: 38px;flex-direction:column}.nxe-range-presets{display:flex;flex:none;flex-direction:column;gap:2px;box-sizing:border-box;min-width:140px;padding:var(--nxe-ui-space-2);border-inline-end:1px solid var(--nxe-ui-border);background:var(--nxe-ui-surface-alt)}.nxe-range-narrow .nxe-range-presets{flex-flow:row wrap;min-width:0;border-inline-end:0;border-bottom:1px solid var(--nxe-ui-border)}.nxe-range-preset{padding:var(--nxe-ui-space-1) var(--nxe-ui-space-2);border:0;border-radius:4px;background:transparent;color:inherit;font:inherit;text-align:start;white-space:nowrap;cursor:pointer}.nxe-range-preset:hover{background:var(--nxe-ui-hover-bg)}.nxe-range-preset:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-preset.nxe-range-preset-active{background:var(--nxe-ui-accent);color:var(--nxe-ui-accent-contrast)}.nxe-range-main{display:flex;flex-direction:column;gap:var(--nxe-ui-space-2);padding:var(--nxe-ui-space-2)}.nxe-range-inputs{display:flex;align-items:center;gap:var(--nxe-ui-space-2)}.nxe-range-text{flex:1 1 0;min-width:0;box-sizing:border-box;height:2.2em;padding:0 .5em;border:1px solid var(--nxe-ui-border);border-radius:4px;background:var(--nxe-ui-surface);color:var(--nxe-ui-text);font:inherit;-webkit-user-select:text;user-select:text}.nxe-range-text:focus{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-text::placeholder{color:var(--nxe-ui-text-muted)}.nxe-range-months{display:flex;gap:var(--nxe-ui-space-3)}.nxe-range-month{flex:none}.nxe-range-header{display:flex;align-items:center;height:32px}.nxe-range-caption{flex:1;font-weight:600;text-align:center}.nxe-range-nav{display:inline-flex;flex:none;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:0;border-radius:4px;background:transparent;color:inherit;cursor:pointer}.nxe-range-nav svg{width:18px;height:18px;fill:currentColor}.nxe-range-nav:hover:not(:disabled){background:var(--nxe-ui-hover-bg)}.nxe-range-nav:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-nav:disabled{color:var(--nxe-ui-text-disabled);cursor:default}.nxe-range-month:first-child:not(:last-child) .nxe-range-header:after,.nxe-range-month:last-child:not(:first-child) .nxe-range-header:before{content:\"\";flex:none;width:32px}.nxe-range-grid{border-collapse:collapse;table-layout:fixed;width:calc(var(--nxe-range-cell) * 7)}.nxe-range-grid th{height:24px;padding:0;color:var(--nxe-ui-text-muted);font-size:.85em;font-weight:500;text-align:center}.nxe-range-grid td{height:calc(var(--nxe-range-cell) - 4px);padding:0;text-align:center}.nxe-range-cell.nxe-range-in{background:var(--nxe-ui-accent-tint)}.nxe-range-cell.nxe-range-in.nxe-range-start{border-start-start-radius:16px;border-end-start-radius:16px}.nxe-range-cell.nxe-range-in.nxe-range-end{border-start-end-radius:16px;border-end-end-radius:16px}.nxe-range-day{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;padding:0;border:0;border-radius:16px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxe-range-day:hover:not([aria-disabled=true]){background:var(--nxe-ui-accent-tint)}.nxe-range-day:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}.nxe-range-day[aria-disabled=true]{color:var(--nxe-ui-text-disabled);cursor:default}.nxe-range-day.nxe-range-today{box-shadow:inset 0 0 0 2px var(--nxe-ui-accent)}.nxe-range-day.nxe-range-edge,.nxe-range-day.nxe-range-edge:hover:not([aria-disabled=true]){background:var(--nxe-ui-accent);color:var(--nxe-ui-accent-contrast)}.nxe-range-footer{display:flex;align-items:center;justify-content:space-between;gap:var(--nxe-ui-space-2);padding-top:var(--nxe-ui-space-2);border-top:1px solid var(--nxe-ui-border)}.nxe-range-status{color:var(--nxe-ui-text-muted)}.nxe-range-clear{height:2.2em;padding:0 .75em;border:1px solid var(--nxe-ui-border);border-radius:4px;background:transparent;color:inherit;font:inherit;cursor:pointer}.nxe-range-clear:hover{background:var(--nxe-ui-hover-bg)}.nxe-range-clear:focus-visible{outline:2px solid var(--nxe-ui-accent);outline-offset:-2px}\n"] }]
2553
+ }], propDecorators: { popupTemplate: [{ type: i0.ViewChild, args: ['popupTemplate', { isSignal: true }] }], fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], displayFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayFormat", required: false }] }], valueFormat: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueFormat", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], presets: [{ type: i0.Input, args: [{ isSignal: true, alias: "presets", required: false }] }], startPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "startPlaceholder", required: false }] }], endPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "endPlaceholder", required: false }] }], showClearButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClearButton", required: false }] }] } });
2554
+
1438
2555
  /**
1439
2556
  * `nxe-scroll-view` — drop-in replacement for DevExtreme `dx-scroll-view`.
1440
2557
  *
@@ -1455,18 +2572,792 @@ class NxeScrollViewComponent {
1455
2572
  showScrollbar = input('onScroll', /* @ts-ignore */
1456
2573
  ...(ngDevMode ? [{ debugName: "showScrollbar" }] : /* istanbul ignore next */ []));
1457
2574
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeScrollViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1458
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeScrollViewComponent, isStandalone: true, selector: "nxe-scroll-view", inputs: { scrollByContent: { classPropertyName: "scrollByContent", publicName: "scrollByContent", isSignal: true, isRequired: false, transformFunction: null }, scrollByThumb: { classPropertyName: "scrollByThumb", publicName: "scrollByThumb", isSignal: true, isRequired: false, transformFunction: null }, showScrollbar: { classPropertyName: "showScrollbar", publicName: "showScrollbar", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "nxe-widget nxe-scroll-view" }, ngImport: i0, template: '<div class="nxe-scrollview-content"><ng-content /></div>', isInline: true, styles: ["nxe-scroll-view{display:block;height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(191,191,191,.7) transparent}@supports not (scrollbar-width: thin){nxe-scroll-view::-webkit-scrollbar{width:8px;height:8px}nxe-scroll-view::-webkit-scrollbar-track{background:transparent}nxe-scroll-view::-webkit-scrollbar-thumb{background-color:#bfbfbfb3;border-radius:4px}nxe-scroll-view:hover::-webkit-scrollbar-thumb{background-color:#bfbfbf}}nxe-scroll-view>.nxe-scrollview-content{display:flex;flex-direction:column;min-height:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2575
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.5", type: NxeScrollViewComponent, isStandalone: true, selector: "nxe-scroll-view", inputs: { scrollByContent: { classPropertyName: "scrollByContent", publicName: "scrollByContent", isSignal: true, isRequired: false, transformFunction: null }, scrollByThumb: { classPropertyName: "scrollByThumb", publicName: "scrollByThumb", isSignal: true, isRequired: false, transformFunction: null }, showScrollbar: { classPropertyName: "showScrollbar", publicName: "showScrollbar", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "nxe-widget nxe-scroll-view" }, ngImport: i0, template: '<div class="nxe-scrollview-content"><ng-content /></div>', isInline: true, styles: ["nxe-scroll-view{display:block;height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--nxe-ui-scroll-thumb, rgba(191, 191, 191, .7)) transparent}@supports not (scrollbar-width: thin){nxe-scroll-view::-webkit-scrollbar{width:8px;height:8px}nxe-scroll-view::-webkit-scrollbar-track{background:transparent}nxe-scroll-view::-webkit-scrollbar-thumb{background-color:var(--nxe-ui-scroll-thumb, rgba(191, 191, 191, .7));border-radius:4px}nxe-scroll-view:hover::-webkit-scrollbar-thumb{background-color:var(--nxe-ui-scroll-thumb-hover, rgb(191, 191, 191))}}nxe-scroll-view>.nxe-scrollview-content{display:flex;flex-direction:column;min-height:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1459
2576
  }
1460
2577
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeScrollViewComponent, decorators: [{
1461
2578
  type: Component,
1462
- args: [{ selector: 'nxe-scroll-view', standalone: true, template: '<div class="nxe-scrollview-content"><ng-content /></div>', host: { class: 'nxe-widget nxe-scroll-view' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-scroll-view{display:block;height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(191,191,191,.7) transparent}@supports not (scrollbar-width: thin){nxe-scroll-view::-webkit-scrollbar{width:8px;height:8px}nxe-scroll-view::-webkit-scrollbar-track{background:transparent}nxe-scroll-view::-webkit-scrollbar-thumb{background-color:#bfbfbfb3;border-radius:4px}nxe-scroll-view:hover::-webkit-scrollbar-thumb{background-color:#bfbfbf}}nxe-scroll-view>.nxe-scrollview-content{display:flex;flex-direction:column;min-height:100%}\n"] }]
2579
+ args: [{ selector: 'nxe-scroll-view', standalone: true, template: '<div class="nxe-scrollview-content"><ng-content /></div>', host: { class: 'nxe-widget nxe-scroll-view' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, styles: ["nxe-scroll-view{display:block;height:100%;overflow-x:hidden;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--nxe-ui-scroll-thumb, rgba(191, 191, 191, .7)) transparent}@supports not (scrollbar-width: thin){nxe-scroll-view::-webkit-scrollbar{width:8px;height:8px}nxe-scroll-view::-webkit-scrollbar-track{background:transparent}nxe-scroll-view::-webkit-scrollbar-thumb{background-color:var(--nxe-ui-scroll-thumb, rgba(191, 191, 191, .7));border-radius:4px}nxe-scroll-view:hover::-webkit-scrollbar-thumb{background-color:var(--nxe-ui-scroll-thumb-hover, rgb(191, 191, 191))}}nxe-scroll-view>.nxe-scrollview-content{display:flex;flex-direction:column;min-height:100%}\n"] }]
1463
2580
  }], propDecorators: { scrollByContent: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollByContent", required: false }] }], scrollByThumb: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollByThumb", required: false }] }], showScrollbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showScrollbar", required: false }] }] } });
1464
2581
 
2582
+ /** Key used when a failing validator has no entry and carries no message of its own. */
2583
+ const NXE_FALLBACK_ERROR_KEY = 'invalid';
2584
+ /** English defaults for Angular's built-in validators. */
2585
+ const NXE_DEFAULT_ERROR_MESSAGES = {
2586
+ required: 'This field is required.',
2587
+ email: 'Enter a valid email address.',
2588
+ minlength: (e) => `Enter at least ${e?.requiredLength} characters.`,
2589
+ maxlength: (e) => `Enter no more than ${e?.requiredLength} characters.`,
2590
+ min: (e) => `Must be at least ${e?.min}.`,
2591
+ max: (e) => `Must be at most ${e?.max}.`,
2592
+ pattern: 'Enter a value in the expected format.',
2593
+ [NXE_FALLBACK_ERROR_KEY]: 'This value is not valid.',
2594
+ };
2595
+ /**
2596
+ * Validator-key to message map read by `nxe-form-field`.
2597
+ *
2598
+ * Provide a plain object to replace it; keys you leave out fall back to
2599
+ * {@link NXE_DEFAULT_ERROR_MESSAGES}, so a partial map is fine. {@link provideNxeErrorMessages} is
2600
+ * the short way to write the provider. A custom validator is covered by adding its key:
2601
+ * `{ uen: 'Enter a valid UEN.' }`.
2602
+ */
2603
+ const NXE_ERROR_MESSAGES = new InjectionToken('NXE_ERROR_MESSAGES', {
2604
+ providedIn: 'root',
2605
+ factory: () => NXE_DEFAULT_ERROR_MESSAGES,
2606
+ });
2607
+ /**
2608
+ * Registers messages for `nxe-form-field`, layered over the English defaults:
2609
+ *
2610
+ * ```ts
2611
+ * provideNxeErrorMessages({
2612
+ * required: 'Wajib diisi.',
2613
+ * minlength: (e) => `Minimal ${e.requiredLength} karakter.`,
2614
+ * })
2615
+ * ```
2616
+ */
2617
+ function provideNxeErrorMessages(messages) {
2618
+ return makeEnvironmentProviders([
2619
+ { provide: NXE_ERROR_MESSAGES, useValue: { ...NXE_DEFAULT_ERROR_MESSAGES, ...messages } },
2620
+ ]);
2621
+ }
2622
+
2623
+ let nextFieldId = 0;
2624
+ function toList(value) {
2625
+ if (value == null) {
2626
+ return [];
2627
+ }
2628
+ return (typeof value === 'string' ? [value] : [...value]).filter((m) => !!m);
2629
+ }
2630
+ /** Signal Forms error kinds to the validator keys the message tables use. */
2631
+ const SIGNAL_KIND_TO_KEY = {
2632
+ minLength: 'minlength',
2633
+ maxLength: 'maxlength',
2634
+ };
2635
+ /**
2636
+ * `nxe-form-field` wraps one `nxe-*` editor that is bound to a form control (`formControlName`,
2637
+ * `[formControl]` or `ngModel`) and gives it the chrome a form needs:
2638
+ *
2639
+ * - a `label`, associated with the editor's inner control (`for` = the editor's `inputId`);
2640
+ * - a required marker, from `Validators.required` on the control or the `required` input;
2641
+ * - a `hint`, replaced by the error while one shows;
2642
+ * - the message for the FIRST failing validator once the control is touched or its form submitted
2643
+ * (`submitted` forces it), looked up in {@link NXE_ERROR_MESSAGES};
2644
+ * - `serverErrors` / `serverError` from an API response, shown regardless of touched.
2645
+ *
2646
+ * It sets `aria-invalid`, `aria-required` and `aria-describedby` on the editor's focusable element
2647
+ * and the editor host's `nxe-invalid` class, so the border, badge and colour come from the same
2648
+ * `--nxe-ui-invalid*` tokens as an editor invalidated by `nxe-validator`.
2649
+ *
2650
+ * Reactivity is the control's own `events` stream (value, status, touched, submit, reset) feeding
2651
+ * signals; the DOM sync runs through `afterRenderEffect`, which names the one computed it reads.
2652
+ */
2653
+ class NxeFormFieldComponent {
2654
+ destroyRef = inject(DestroyRef);
2655
+ tokenMessages = inject(NXE_ERROR_MESSAGES);
2656
+ messageService = inject(NxeMessageService);
2657
+ /** The enclosing form, when there is one: a template-driven control joins it a tick late. */
2658
+ container = inject(ControlContainer, { optional: true });
2659
+ /** Caption above the editor. */
2660
+ label = input(undefined, /* @ts-ignore */
2661
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
2662
+ /** Help text under the editor; hidden while an error is showing. */
2663
+ hint = input(undefined, /* @ts-ignore */
2664
+ ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
2665
+ /**
2666
+ * Force the required marker on or off. Unset, it follows `Validators.required` (and
2667
+ * `requiredTrue`) on the control. Needed for a rule the control does not declare as a validator.
2668
+ */
2669
+ required = input(undefined, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: nxeOptionalBoolean });
2670
+ /**
2671
+ * API field errors, shown whether or not the control was touched. A string or a list of them;
2672
+ * the app clears them (set `null`) when the user edits the field or resubmits.
2673
+ */
2674
+ serverErrors = input(undefined, /* @ts-ignore */
2675
+ ...(ngDevMode ? [{ debugName: "serverErrors" }] : /* istanbul ignore next */ []));
2676
+ /** Singular spelling of {@link serverErrors}; both may be set, their messages are joined. */
2677
+ serverError = input(undefined, /* @ts-ignore */
2678
+ ...(ngDevMode ? [{ debugName: "serverError" }] : /* istanbul ignore next */ []));
2679
+ /**
2680
+ * Show client-side errors now (`true`) or never (`false`). Unset, they show once the control is
2681
+ * touched or the surrounding form was submitted.
2682
+ */
2683
+ submitted = input(undefined, { ...(ngDevMode ? { debugName: "submitted" } : /* istanbul ignore next */ {}), transform: nxeOptionalBoolean });
2684
+ /** Messages for this field only, by validator key; override {@link NXE_ERROR_MESSAGES}. */
2685
+ errorMessages = input(undefined, /* @ts-ignore */
2686
+ ...(ngDevMode ? [{ debugName: "errorMessages" }] : /* istanbul ignore next */ []));
2687
+ ngControl = contentChild(NgControl, /* @ts-ignore */
2688
+ ...(ngDevMode ? [{ debugName: "ngControl" }] : /* istanbul ignore next */ []));
2689
+ ngControlHost = contentChild(NgControl, { ...(ngDevMode ? { debugName: "ngControlHost" } : /* istanbul ignore next */ {}), read: ElementRef });
2690
+ /**
2691
+ * Set when the projected editor carries Angular's Signal Forms `[formField]`. That directive
2692
+ * provides a stand-in `NgControl` with no `events` stream, so the field then reads the field
2693
+ * state's signals instead of subscribing to a control.
2694
+ */
2695
+ formField = contentChild(FormField, /* @ts-ignore */
2696
+ ...(ngDevMode ? [{ debugName: "formField" }] : /* istanbul ignore next */ []));
2697
+ formFieldHost = contentChild(FormField, { ...(ngDevMode ? { debugName: "formFieldHost" } : /* istanbul ignore next */ {}), read: ElementRef });
2698
+ editorHost = computed(() => this.formFieldHost() ?? this.ngControlHost(), /* @ts-ignore */
2699
+ ...(ngDevMode ? [{ debugName: "editorHost" }] : /* istanbul ignore next */ []));
2700
+ fieldState = computed(() => this.formField()?.state() ?? null, /* @ts-ignore */
2701
+ ...(ngDevMode ? [{ debugName: "fieldState" }] : /* istanbul ignore next */ []));
2702
+ /** Only read in dev mode, to catch a second editor the field would silently not bind. */
2703
+ allControls = contentChildren(NgControl, { ...(ngDevMode ? { debugName: "allControls" } : /* istanbul ignore next */ {}), descendants: true });
2704
+ // Mirrors of the control's state. The control is not a signal, so each event on it copies the
2705
+ // parts the template reads into these.
2706
+ touched = signal(false, /* @ts-ignore */
2707
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
2708
+ controlErrors = signal(null, /* @ts-ignore */
2709
+ ...(ngDevMode ? [{ debugName: "controlErrors" }] : /* istanbul ignore next */ []));
2710
+ validatorRequired = signal(false, /* @ts-ignore */
2711
+ ...(ngDevMode ? [{ debugName: "validatorRequired" }] : /* istanbul ignore next */ []));
2712
+ formSubmitted = signal(false, /* @ts-ignore */
2713
+ ...(ngDevMode ? [{ debugName: "formSubmitted" }] : /* istanbul ignore next */ []));
2714
+ editor = signal(null, /* @ts-ignore */
2715
+ ...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
2716
+ uid = `nxe-form-field-${++nextFieldId}`;
2717
+ labelId = `${this.uid}-label`;
2718
+ hintId = `${this.uid}-hint`;
2719
+ errorId = `${this.uid}-error`;
2720
+ /** The id the label points at: the editor's inner control. Null for a non-nxe editor. */
2721
+ controlId = computed(() => this.editor()?.controlId() ?? null, /* @ts-ignore */
2722
+ ...(ngDevMode ? [{ debugName: "controlId" }] : /* istanbul ignore next */ []));
2723
+ isRequired = computed(() => this.required() ?? (this.fieldState()?.required() ?? this.validatorRequired()), /* @ts-ignore */
2724
+ ...(ngDevMode ? [{ debugName: "isRequired" }] : /* istanbul ignore next */ []));
2725
+ reveal = computed(() => {
2726
+ const state = this.fieldState();
2727
+ // A Signal Forms submit marks every field touched, so touched covers a submitted form.
2728
+ return this.submitted() ?? (state ? state.touched() : this.touched() || this.formSubmitted());
2729
+ }, /* @ts-ignore */
2730
+ ...(ngDevMode ? [{ debugName: "reveal" }] : /* istanbul ignore next */ []));
2731
+ clientMessage = computed(() => {
2732
+ if (!this.reveal()) {
2733
+ return null;
2734
+ }
2735
+ const state = this.fieldState();
2736
+ if (state) {
2737
+ const first = state.errors()[0];
2738
+ return first ? this.resolveSignalError(first) : null;
2739
+ }
2740
+ const errors = this.controlErrors();
2741
+ const key = errors ? Object.keys(errors)[0] : undefined;
2742
+ return key === undefined ? null : this.resolve(key, errors[key]);
2743
+ }, /* @ts-ignore */
2744
+ ...(ngDevMode ? [{ debugName: "clientMessage" }] : /* istanbul ignore next */ []));
2745
+ /** Everything shown under the editor as an error: the client message, then the server's. */
2746
+ messages = computed(() => {
2747
+ const client = this.clientMessage();
2748
+ return [
2749
+ ...(client ? [client] : []),
2750
+ ...toList(this.serverErrors()),
2751
+ ...toList(this.serverError()),
2752
+ ];
2753
+ }, /* @ts-ignore */
2754
+ ...(ngDevMode ? [{ debugName: "messages" }] : /* istanbul ignore next */ []));
2755
+ invalid = computed(() => this.messages().length > 0, /* @ts-ignore */
2756
+ ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
2757
+ showHint = computed(() => !!this.hint() && !this.invalid(), /* @ts-ignore */
2758
+ ...(ngDevMode ? [{ debugName: "showHint" }] : /* istanbul ignore next */ []));
2759
+ domState = computed(() => ({
2760
+ host: this.editorHost()?.nativeElement ?? null,
2761
+ controlId: this.controlId(),
2762
+ invalid: this.invalid(),
2763
+ required: this.isRequired(),
2764
+ describedBy: [this.showHint() ? this.hintId : null, this.invalid() ? this.errorId : null]
2765
+ .filter((id) => !!id)
2766
+ .join(' ') || null,
2767
+ labelledBy: this.label() ? this.labelId : null,
2768
+ }), /* @ts-ignore */
2769
+ ...(ngDevMode ? [{ debugName: "domState" }] : /* istanbul ignore next */ []));
2770
+ /** Attributes this field put on the editor's element, so it only ever removes its own. */
2771
+ applied = null;
2772
+ invalidClassHost = null;
2773
+ constructor() {
2774
+ // Reads exactly `domState()`; the DOM writes below read no signal.
2775
+ afterRenderEffect(() => this.syncDom(this.domState()));
2776
+ }
2777
+ ngAfterContentInit() {
2778
+ if (typeof ngDevMode !== 'undefined' && ngDevMode && this.allControls().length > 1) {
2779
+ console.warn(`nxe-form-field binds only its first form control, but ${this.allControls().length} are projected. ` +
2780
+ 'Wrap each editor in its own nxe-form-field.');
2781
+ }
2782
+ const formField = this.formField();
2783
+ if (formField) {
2784
+ // Signal Forms: the state is read through signals (fieldState); there is nothing to subscribe to.
2785
+ const accessors = formField.injector.get(NG_VALUE_ACCESSOR, null);
2786
+ this.editor.set((accessors ?? []).find((a) => a instanceof NxeFormControlBase) ?? null);
2787
+ return;
2788
+ }
2789
+ const ngControl = this.ngControl();
2790
+ const control = ngControl?.control ?? null;
2791
+ const accessor = ngControl?.valueAccessor;
2792
+ if (accessor instanceof NxeFormControlBase) {
2793
+ this.editor.set(accessor);
2794
+ }
2795
+ if (!control) {
2796
+ return;
2797
+ }
2798
+ this.syncFromControl(control);
2799
+ control.events
2800
+ .pipe(takeUntilDestroyed(this.destroyRef))
2801
+ .subscribe(() => this.syncFromControl(control));
2802
+ // Submit and reset are emitted on the ROOT of the form, not on each control.
2803
+ (this.container?.control ?? control).root.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => {
2804
+ if (event instanceof FormSubmittedEvent) {
2805
+ this.formSubmitted.set(true);
2806
+ }
2807
+ else if (event instanceof FormResetEvent) {
2808
+ this.formSubmitted.set(false);
2809
+ }
2810
+ this.syncFromControl(control);
2811
+ });
2812
+ }
2813
+ syncFromControl(control) {
2814
+ this.touched.set(control.touched);
2815
+ this.controlErrors.set(control.errors);
2816
+ this.validatorRequired.set(control.hasValidator(Validators.required) || control.hasValidator(Validators.requiredTrue));
2817
+ }
2818
+ /**
2819
+ * Message for one failing validator: this field's `errorMessages`, then an entry the app put in
2820
+ * the {@link NXE_ERROR_MESSAGES} token, then the active `NxeMessageService` bundle (English
2821
+ * unless the app switched language), then a string the validator itself returned
2822
+ * (`{ uen: 'Not a valid UEN' }` or `{ uen: { message } }`), then the generic fallback.
2823
+ *
2824
+ * A token entry that IS the built-in default (the same reference, which is what
2825
+ * `provideNxeErrorMessages` leaves for the keys it was not given) counts as unset, so it does
2826
+ * not pin English over the bundle. Called from the `clientMessage` computed, so a language
2827
+ * switch re-renders the error.
2828
+ */
2829
+ resolve(key, error) {
2830
+ const entry = this.errorMessages()?.[key] ?? this.explicitToken(key) ?? this.bundled(key, error);
2831
+ if (entry !== undefined) {
2832
+ return typeof entry === 'function' ? entry(error) : entry;
2833
+ }
2834
+ if (typeof error === 'string' && error) {
2835
+ return error;
2836
+ }
2837
+ const own = error?.message;
2838
+ if (typeof own === 'string' && own) {
2839
+ return own;
2840
+ }
2841
+ const fallback = this.errorMessages()?.[NXE_FALLBACK_ERROR_KEY] ??
2842
+ this.explicitToken(NXE_FALLBACK_ERROR_KEY) ??
2843
+ this.messageService.t('validation.field.invalid');
2844
+ return typeof fallback === 'function' ? fallback(error) : fallback;
2845
+ }
2846
+ /**
2847
+ * Message for a Signal Forms error `{ kind, message? }`. The kind is mapped to the validator keys
2848
+ * above (`minLength` to `minlength`, `maxLength` to `maxlength`; `required`, `min`, `max`, `pattern`
2849
+ * and `email` as they are). Lookup order: this field's `errorMessages`, an app token entry, the
2850
+ * error's own `message`, the library bundle, then the fallback (`invalid`). So a rule written
2851
+ * with a message (`required(p.name, { message: '...' })`) shows it, and a custom kind does too.
2852
+ */
2853
+ resolveSignalError(error) {
2854
+ const key = SIGNAL_KIND_TO_KEY[error.kind] ?? error.kind;
2855
+ const e = error;
2856
+ // A validator written for Reactive Forms (nxe-file-input's, or an app's) reaches the field as
2857
+ // `{ kind, context }`, where `context` is what it returned under its key.
2858
+ const context = e['context'];
2859
+ const fromValidator = typeof context === 'object' && context !== null ? context : {};
2860
+ // Shape the payload like the Reactive Forms error of the same key, which the tables read.
2861
+ const payload = { ...fromValidator, ...e, requiredLength: e['minLength'] ?? e['maxLength'] ?? fromValidator['requiredLength'] };
2862
+ const ownMessage = error.message || (typeof context === 'string' ? context : undefined) || (typeof fromValidator['message'] === 'string' ? fromValidator['message'] : undefined);
2863
+ const entry = this.errorMessages()?.[key] ?? this.explicitToken(key);
2864
+ if (entry !== undefined) {
2865
+ return typeof entry === 'function' ? entry(payload) : entry;
2866
+ }
2867
+ if (ownMessage) {
2868
+ return ownMessage;
2869
+ }
2870
+ const bundled = this.bundled(key, payload);
2871
+ if (bundled !== undefined) {
2872
+ return bundled;
2873
+ }
2874
+ const fallback = this.errorMessages()?.[NXE_FALLBACK_ERROR_KEY] ??
2875
+ this.explicitToken(NXE_FALLBACK_ERROR_KEY) ??
2876
+ this.messageService.t('validation.field.invalid');
2877
+ return typeof fallback === 'function' ? fallback(payload) : fallback;
2878
+ }
2879
+ explicitToken(key) {
2880
+ const entry = this.tokenMessages[key];
2881
+ return entry === NXE_DEFAULT_ERROR_MESSAGES[key] ? undefined : entry;
2882
+ }
2883
+ /** The library bundle's wording for Angular's built-in validators. */
2884
+ bundled(key, error) {
2885
+ const e = error;
2886
+ switch (key) {
2887
+ case 'required':
2888
+ case 'email':
2889
+ case 'pattern':
2890
+ return this.messageService.t(`validation.field.${key}`);
2891
+ case 'minlength':
2892
+ return this.messageService.t('validation.field.minlength', e?.requiredLength);
2893
+ case 'maxlength':
2894
+ return this.messageService.t('validation.field.maxlength', e?.requiredLength);
2895
+ case 'min':
2896
+ return this.messageService.t('validation.field.min', e?.min);
2897
+ case 'max':
2898
+ return this.messageService.t('validation.field.max', e?.max);
2899
+ // nxe-file-input's errors: the file type allow-list, the size and count limits, and uploads.
2900
+ case 'fileType':
2901
+ return this.messageService.t('validation.field.fileType', error?.accept);
2902
+ case 'fileSize':
2903
+ return this.messageService.t('validation.field.fileSize', nxeFormatFileSize(Number(error?.maxSize), this.messageService));
2904
+ case 'maxFiles':
2905
+ return this.messageService.t('validation.field.maxFiles', error?.maxFiles);
2906
+ case 'uploading':
2907
+ case 'uploadFailed':
2908
+ return this.messageService.t(`validation.field.${key}`);
2909
+ default:
2910
+ return undefined;
2911
+ }
2912
+ }
2913
+ /** The element a screen reader should treat as the field: the inner control, not the host. */
2914
+ focusable(host, controlId) {
2915
+ if (controlId) {
2916
+ const byId = host.querySelector(`#${CSS.escape(controlId)}`);
2917
+ if (byId) {
2918
+ return byId;
2919
+ }
2920
+ }
2921
+ if (host.matches('[tabindex], input, textarea, select')) {
2922
+ return host;
2923
+ }
2924
+ return (host.querySelector('input, textarea, select, [tabindex]:not([tabindex="-1"])') ??
2925
+ host);
2926
+ }
2927
+ setAttr(name, value) {
2928
+ const { el, attrs } = this.applied;
2929
+ if (value === null) {
2930
+ if (attrs.delete(name)) {
2931
+ el.removeAttribute(name);
2932
+ }
2933
+ }
2934
+ else {
2935
+ el.setAttribute(name, value);
2936
+ attrs.add(name);
2937
+ }
2938
+ }
2939
+ syncDom(state) {
2940
+ if (!state.host) {
2941
+ return;
2942
+ }
2943
+ const el = this.focusable(state.host, state.controlId);
2944
+ if (this.applied?.el !== el) {
2945
+ this.applied = { el, attrs: new Set() };
2946
+ }
2947
+ this.setAttr('aria-invalid', state.invalid ? 'true' : null);
2948
+ this.setAttr('aria-required', state.required ? 'true' : null);
2949
+ this.setAttr('aria-describedby', state.describedBy);
2950
+ // A native input is labelled by the `for` association; the host of an editor without one
2951
+ // (check-box, drop-down) needs the label named explicitly, unless the app already did.
2952
+ const own = this.applied.attrs.has('aria-labelledby');
2953
+ const needsLabelledBy = !!state.labelledBy &&
2954
+ !/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) &&
2955
+ (own || (!el.hasAttribute('aria-label') && !el.hasAttribute('aria-labelledby')));
2956
+ this.setAttr('aria-labelledby', needsLabelledBy ? state.labelledBy : null);
2957
+ // The editor's own invalid look (border, badge) is keyed off `.nxe-invalid` on its host.
2958
+ if (state.invalid) {
2959
+ state.host.classList.add('nxe-invalid');
2960
+ this.invalidClassHost = state.host;
2961
+ }
2962
+ else if (this.invalidClassHost) {
2963
+ this.invalidClassHost.classList.remove('nxe-invalid');
2964
+ this.invalidClassHost = null;
2965
+ }
2966
+ }
2967
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeFormFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2968
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeFormFieldComponent, isStandalone: true, selector: "nxe-form-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, serverErrors: { classPropertyName: "serverErrors", publicName: "serverErrors", isSignal: true, isRequired: false, transformFunction: null }, serverError: { classPropertyName: "serverError", publicName: "serverError", isSignal: true, isRequired: false, transformFunction: null }, submitted: { classPropertyName: "submitted", publicName: "submitted", isSignal: true, isRequired: false, transformFunction: null }, errorMessages: { classPropertyName: "errorMessages", publicName: "errorMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-form-field-invalid": "invalid()" }, classAttribute: "nxe-form-field" }, queries: [{ propertyName: "ngControl", first: true, predicate: NgControl, descendants: true, isSignal: true }, { propertyName: "ngControlHost", first: true, predicate: NgControl, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "formField", first: true, predicate: FormField, descendants: true, isSignal: true }, { propertyName: "formFieldHost", first: true, predicate: FormField, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "allControls", predicate: NgControl, descendants: true, isSignal: true }], ngImport: i0, template: "@if (label()) {\n <label class=\"nxe-form-field-label\" [id]=\"labelId\" [attr.for]=\"controlId()\">\n {{ label() }}\n @if (isRequired()) {\n <span class=\"nxe-form-field-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n}\n<div class=\"nxe-form-field-control\">\n <ng-content />\n</div>\n@if (showHint()) {\n <div class=\"nxe-form-field-hint\" [id]=\"hintId\">{{ hint() }}</div>\n}\n<div class=\"nxe-form-field-messages\" aria-live=\"polite\">\n @if (invalid()) {\n <div class=\"nxe-form-field-error\" [id]=\"errorId\">\n @for (message of messages(); track $index) {\n <div>{{ message }}</div>\n }\n </div>\n }\n</div>\n", styles: ["nxe-form-field{display:block;min-width:0}nxe-form-field .nxe-form-field-label{display:block;margin-bottom:var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size-sm, 13px);font-weight:var(--nxe-ui-font-weight-medium, 500);line-height:var(--nxe-ui-line-height-sm, 18px)}nxe-form-field .nxe-form-field-required{margin-inline-start:var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-invalid, #d9534f)}nxe-form-field .nxe-form-field-hint,nxe-form-field .nxe-form-field-error{margin-top:var(--nxe-ui-space-1, 4px);font-size:var(--nxe-ui-font-size-xs, 12px);line-height:var(--nxe-ui-line-height-xs, 16px)}nxe-form-field .nxe-form-field-hint{color:color-mix(in srgb,var(--nxe-ui-text, #333) 70%,var(--nxe-ui-surface, #fff))}nxe-form-field .nxe-form-field-error{color:var(--nxe-ui-invalid, #d9534f)}nxe-form-field .nxe-form-field-control .nxe-invalid-message{display:none}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
2969
+ }
2970
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeFormFieldComponent, decorators: [{
2971
+ type: Component,
2972
+ args: [{ selector: 'nxe-form-field', host: {
2973
+ class: 'nxe-form-field',
2974
+ '[class.nxe-form-field-invalid]': 'invalid()',
2975
+ }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "@if (label()) {\n <label class=\"nxe-form-field-label\" [id]=\"labelId\" [attr.for]=\"controlId()\">\n {{ label() }}\n @if (isRequired()) {\n <span class=\"nxe-form-field-required\" aria-hidden=\"true\">*</span>\n }\n </label>\n}\n<div class=\"nxe-form-field-control\">\n <ng-content />\n</div>\n@if (showHint()) {\n <div class=\"nxe-form-field-hint\" [id]=\"hintId\">{{ hint() }}</div>\n}\n<div class=\"nxe-form-field-messages\" aria-live=\"polite\">\n @if (invalid()) {\n <div class=\"nxe-form-field-error\" [id]=\"errorId\">\n @for (message of messages(); track $index) {\n <div>{{ message }}</div>\n }\n </div>\n }\n</div>\n", styles: ["nxe-form-field{display:block;min-width:0}nxe-form-field .nxe-form-field-label{display:block;margin-bottom:var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size-sm, 13px);font-weight:var(--nxe-ui-font-weight-medium, 500);line-height:var(--nxe-ui-line-height-sm, 18px)}nxe-form-field .nxe-form-field-required{margin-inline-start:var(--nxe-ui-space-1, 4px);color:var(--nxe-ui-invalid, #d9534f)}nxe-form-field .nxe-form-field-hint,nxe-form-field .nxe-form-field-error{margin-top:var(--nxe-ui-space-1, 4px);font-size:var(--nxe-ui-font-size-xs, 12px);line-height:var(--nxe-ui-line-height-xs, 16px)}nxe-form-field .nxe-form-field-hint{color:color-mix(in srgb,var(--nxe-ui-text, #333) 70%,var(--nxe-ui-surface, #fff))}nxe-form-field .nxe-form-field-error{color:var(--nxe-ui-invalid, #d9534f)}nxe-form-field .nxe-form-field-control .nxe-invalid-message{display:none}\n"] }]
2976
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], serverErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverErrors", required: false }] }], serverError: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverError", required: false }] }], submitted: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitted", required: false }] }], errorMessages: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessages", required: false }] }], ngControl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgControl), { isSignal: true }] }], ngControlHost: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NgControl), { ...{ read: ElementRef }, isSignal: true }] }], formField: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FormField), { isSignal: true }] }], formFieldHost: [{ type: i0.ContentChild, args: [i0.forwardRef(() => FormField), { ...{ read: ElementRef }, isSignal: true }] }], allControls: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NgControl), { ...{ descendants: true }, isSignal: true }] }] } });
2977
+
2978
+ /**
2979
+ * Marks the `<ng-template>` that renders one radio's content in `nxe-radio-group`, in place of the
2980
+ * `displayExpr` text:
2981
+ *
2982
+ * ```html
2983
+ * <nxe-radio-group [items]="plans" valueExpr="id">
2984
+ * <ng-template nxeRadioItem let-plan let-selected="selected">
2985
+ * <strong>{{ plan.name }}</strong> <small>{{ plan.price }}</small>
2986
+ * </ng-template>
2987
+ * </nxe-radio-group>
2988
+ * ```
2989
+ *
2990
+ * The radio's accessible name is the template's text, so keep it to text and inline markup.
2991
+ */
2992
+ class NxeRadioItemDirective {
2993
+ /** The template to render for each item. */
2994
+ template = inject(TemplateRef);
2995
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeRadioItemDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
2996
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.5", type: NxeRadioItemDirective, isStandalone: true, selector: "ng-template[nxeRadioItem]", ngImport: i0 });
2997
+ }
2998
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeRadioItemDirective, decorators: [{
2999
+ type: Directive,
3000
+ args: [{
3001
+ selector: 'ng-template[nxeRadioItem]',
3002
+ }]
3003
+ }] });
3004
+
3005
+ /**
3006
+ * `nxe-radio-group`: single choice among a few options, as the WAI-ARIA radio group pattern.
3007
+ *
3008
+ * A signal-CVA editor (extends {@link NxeFormControlBase}) working with `[(value)]`, `[(ngModel)]`
3009
+ * and Reactive Forms. The options are `items`; `displayExpr` says what each shows (default: the
3010
+ * item itself, for a list of strings), `valueExpr` what it puts in the control (default: the item
3011
+ * itself) and `disabledExpr` (default `'disabled'`) which are not selectable. An
3012
+ * `<ng-template nxeRadioItem>` replaces the text with your own content.
3013
+ *
3014
+ * Keyboard: the group is one tab stop, on the selected radio (or the first enabled one). Arrow keys
3015
+ * move to the next or previous enabled radio, wrap around, and select it; Space selects the focused
3016
+ * one. Left and Right are swapped in right-to-left text. A read-only group moves focus but never
3017
+ * changes the value.
3018
+ *
3019
+ * The inner `role="radiogroup"` element carries the `inputId`, `aria-label` and `aria-labelledby`,
3020
+ * so an `nxe-form-field` label names the group and its `aria-invalid` / `aria-describedby` land on
3021
+ * it. Provides {@link NXE_VALIDATION_TARGET} for a projected `nxe-validator`.
3022
+ */
3023
+ class NxeRadioGroupComponent extends NxeFormControlBase {
3024
+ /** The options. */
3025
+ items = input([], /* @ts-ignore */
3026
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
3027
+ /** Text of an item; unset, the item itself. */
3028
+ displayExpr = input(undefined, /* @ts-ignore */
3029
+ ...(ngDevMode ? [{ debugName: "displayExpr" }] : /* istanbul ignore next */ []));
3030
+ /** The value an item stands for; unset, the item itself. */
3031
+ valueExpr = input(undefined, /* @ts-ignore */
3032
+ ...(ngDevMode ? [{ debugName: "valueExpr" }] : /* istanbul ignore next */ []));
3033
+ /** Truthy for an item that cannot be chosen. */
3034
+ disabledExpr = input('disabled', /* @ts-ignore */
3035
+ ...(ngDevMode ? [{ debugName: "disabledExpr" }] : /* istanbul ignore next */ []));
3036
+ /** `vertical` stacks the radios, `horizontal` puts them in a row that wraps. */
3037
+ layout = input('vertical', /* @ts-ignore */
3038
+ ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
3039
+ /** Custom radio content. */
3040
+ itemTemplate = contentChild(NxeRadioItemDirective, /* @ts-ignore */
3041
+ ...(ngDevMode ? [{ debugName: "itemTemplate" }] : /* istanbul ignore next */ []));
3042
+ entries = computed(() => this.items().map((item, index) => ({
3043
+ item,
3044
+ index,
3045
+ id: `${this.controlId()}-radio-${index}`,
3046
+ value: nxeResolveExpr(this.valueExpr(), item),
3047
+ text: nxeResolveText(this.displayExpr(), item),
3048
+ disabled: !!nxeResolveExpr(this.disabledExpr(), item),
3049
+ })), /* @ts-ignore */
3050
+ ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
3051
+ /** Index of the checked radio, or -1. */
3052
+ selectedIndex = computed(() => {
3053
+ const current = this.value();
3054
+ return this.entries().findIndex((entry) => Object.is(entry.value, current));
3055
+ }, /* @ts-ignore */
3056
+ ...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
3057
+ /** The one radio in the tab order: the checked one when it can be reached, else the first enabled. */
3058
+ tabbableIndex = computed(() => {
3059
+ if (this.disabled()) {
3060
+ return -1;
3061
+ }
3062
+ const entries = this.entries();
3063
+ const selected = this.selectedIndex();
3064
+ if (selected >= 0 && !entries[selected].disabled) {
3065
+ return selected;
3066
+ }
3067
+ return entries.findIndex((entry) => !entry.disabled);
3068
+ }, /* @ts-ignore */
3069
+ ...(ngDevMode ? [{ debugName: "tabbableIndex" }] : /* istanbul ignore next */ []));
3070
+ onItemClick(index, event) {
3071
+ this.select(index, event);
3072
+ }
3073
+ onItemKeydown(index, event) {
3074
+ if (event.altKey || event.ctrlKey || event.metaKey) {
3075
+ return;
3076
+ }
3077
+ const rtl = getComputedStyle(this.element).direction === 'rtl';
3078
+ switch (event.key) {
3079
+ case 'ArrowDown':
3080
+ this.move(index, 1, event);
3081
+ break;
3082
+ case 'ArrowUp':
3083
+ this.move(index, -1, event);
3084
+ break;
3085
+ case 'ArrowRight':
3086
+ this.move(index, rtl ? -1 : 1, event);
3087
+ break;
3088
+ case 'ArrowLeft':
3089
+ this.move(index, rtl ? 1 : -1, event);
3090
+ break;
3091
+ case ' ':
3092
+ event.preventDefault();
3093
+ this.select(index, event);
3094
+ break;
3095
+ }
3096
+ }
3097
+ /** Leaving the group, not moving between its radios, is what touches the control. */
3098
+ onGroupFocusOut(event) {
3099
+ const group = event.currentTarget;
3100
+ if (!group.contains(event.relatedTarget)) {
3101
+ this.markAsTouched();
3102
+ }
3103
+ }
3104
+ /** Focus (and, unless read-only, select) the next enabled radio in `step` direction, wrapping. */
3105
+ move(from, step, event) {
3106
+ event.preventDefault();
3107
+ if (this.disabled()) {
3108
+ return;
3109
+ }
3110
+ const entries = this.entries();
3111
+ for (let n = 1; n <= entries.length; n++) {
3112
+ const next = (((from + step * n) % entries.length) + entries.length) % entries.length;
3113
+ if (!entries[next].disabled) {
3114
+ this.element.querySelectorAll('[role="radio"]')[next]?.focus();
3115
+ this.select(next, event);
3116
+ return;
3117
+ }
3118
+ }
3119
+ }
3120
+ select(index, event) {
3121
+ const entry = this.entries()[index];
3122
+ if (!entry || entry.disabled || this.disabled() || this.readOnly()) {
3123
+ return;
3124
+ }
3125
+ this.commitValue(entry.value, event);
3126
+ this.markAsTouched();
3127
+ }
3128
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeRadioGroupComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3129
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeRadioGroupComponent, isStandalone: true, selector: "nxe-radio-group", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, displayExpr: { classPropertyName: "displayExpr", publicName: "displayExpr", isSignal: true, isRequired: false, transformFunction: null }, valueExpr: { classPropertyName: "valueExpr", publicName: "valueExpr", isSignal: true, isRequired: false, transformFunction: null }, disabledExpr: { classPropertyName: "disabledExpr", publicName: "disabledExpr", isSignal: true, isRequired: false, transformFunction: null }, layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.nxe-radio-group-horizontal": "layout() === 'horizontal'", "class.nxe-radio-group-vertical": "layout() === 'vertical'", "class.nxe-invalid": "invalidState()", "attr.aria-label": "null", "attr.aria-labelledby": "null" }, classAttribute: "nxe-widget nxe-radio-group" }, providers: [
3130
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeRadioGroupComponent), multi: true },
3131
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeRadioGroupComponent) },
3132
+ ], queries: [{ propertyName: "itemTemplate", first: true, predicate: NxeRadioItemDirective, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div\n class=\"nxe-radio-group-items\"\n role=\"radiogroup\"\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-disabled]=\"disabled() ? true : null\"\n [attr.aria-readonly]=\"readOnly() ? true : null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (focusout)=\"onGroupFocusOut($event)\">\n @for (entry of entries(); track entry.index) {\n <div\n class=\"nxe-radio\"\n role=\"radio\"\n [id]=\"entry.id\"\n [class.nxe-radio-checked]=\"selectedIndex() === entry.index\"\n [class.nxe-radio-disabled]=\"entry.disabled\"\n [attr.aria-checked]=\"selectedIndex() === entry.index\"\n [attr.aria-disabled]=\"entry.disabled || disabled() ? true : null\"\n [attr.tabindex]=\"tabbableIndex() === entry.index ? 0 : -1\"\n (click)=\"onItemClick(entry.index, $event)\"\n (keydown)=\"onItemKeydown(entry.index, $event)\">\n <span class=\"nxe-radio-icon\" aria-hidden=\"true\"></span>\n <span class=\"nxe-radio-label\">\n @if (itemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: entry.item,\n item: entry.item,\n index: entry.index,\n selected: selectedIndex() === entry.index,\n disabled: entry.disabled,\n }\" />\n } @else {\n {{ entry.text }}\n }\n </span>\n </div>\n }\n</div>\n<ng-content />\n", styles: ["nxe-radio-group{display:block;min-width:0}nxe-radio-group .nxe-radio-group-items{display:flex;flex-direction:column;gap:var(--nxe-ui-space-2, 8px)}nxe-radio-group.nxe-radio-group-horizontal .nxe-radio-group-items{flex-flow:row wrap;gap:var(--nxe-ui-space-2, 8px) var(--nxe-ui-space-5, 20px)}nxe-radio-group .nxe-radio{display:inline-flex;align-items:flex-start;gap:var(--nxe-ui-space-2, 8px);align-self:flex-start;max-width:100%;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:var(--nxe-ui-line-height, 1.35715)}nxe-radio-group .nxe-radio:focus{outline:none}nxe-radio-group .nxe-radio-icon{position:relative;box-sizing:border-box;flex-shrink:0;margin-block-start:1px;width:18px;height:18px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:50%;background-color:var(--nxe-ui-surface, #fff)}nxe-radio-group .nxe-radio-checked .nxe-radio-icon{border-color:var(--nxe-ui-accent, #337ab7)}nxe-radio-group .nxe-radio-checked .nxe-radio-icon:after{content:\"\";position:absolute;inset:3px;border-radius:50%;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){nxe-radio-group .nxe-radio-checked .nxe-radio-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-radio-group .nxe-radio-label{min-width:0;overflow-wrap:anywhere}nxe-radio-group .nxe-radio:not(.nxe-radio-disabled):hover .nxe-radio-icon{border-color:var(--nxe-ui-accent-strong, #265a87)}nxe-radio-group .nxe-radio:focus-visible .nxe-radio-icon{border-color:var(--nxe-ui-accent, #337ab7);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-radio-group .nxe-radio-disabled{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-radio-group.nxe-disabled .nxe-radio{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-radio-group.nxe-readonly .nxe-radio{cursor:default}nxe-radio-group.nxe-invalid .nxe-radio-icon{border-color:var(--nxe-ui-invalid, #d9534f)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3133
+ }
3134
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeRadioGroupComponent, decorators: [{
3135
+ type: Component,
3136
+ args: [{ selector: 'nxe-radio-group', imports: [NgTemplateOutlet], host: {
3137
+ class: 'nxe-widget nxe-radio-group',
3138
+ '[class.nxe-radio-group-horizontal]': "layout() === 'horizontal'",
3139
+ '[class.nxe-radio-group-vertical]': "layout() === 'vertical'",
3140
+ '[class.nxe-invalid]': 'invalidState()',
3141
+ // The names belong on the inner radiogroup; a copy on this generic host would be prohibited.
3142
+ '[attr.aria-label]': 'null',
3143
+ '[attr.aria-labelledby]': 'null',
3144
+ }, providers: [
3145
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeRadioGroupComponent), multi: true },
3146
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeRadioGroupComponent) },
3147
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div\n class=\"nxe-radio-group-items\"\n role=\"radiogroup\"\n [id]=\"controlId()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-disabled]=\"disabled() ? true : null\"\n [attr.aria-readonly]=\"readOnly() ? true : null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n (focusout)=\"onGroupFocusOut($event)\">\n @for (entry of entries(); track entry.index) {\n <div\n class=\"nxe-radio\"\n role=\"radio\"\n [id]=\"entry.id\"\n [class.nxe-radio-checked]=\"selectedIndex() === entry.index\"\n [class.nxe-radio-disabled]=\"entry.disabled\"\n [attr.aria-checked]=\"selectedIndex() === entry.index\"\n [attr.aria-disabled]=\"entry.disabled || disabled() ? true : null\"\n [attr.tabindex]=\"tabbableIndex() === entry.index ? 0 : -1\"\n (click)=\"onItemClick(entry.index, $event)\"\n (keydown)=\"onItemKeydown(entry.index, $event)\">\n <span class=\"nxe-radio-icon\" aria-hidden=\"true\"></span>\n <span class=\"nxe-radio-label\">\n @if (itemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: entry.item,\n item: entry.item,\n index: entry.index,\n selected: selectedIndex() === entry.index,\n disabled: entry.disabled,\n }\" />\n } @else {\n {{ entry.text }}\n }\n </span>\n </div>\n }\n</div>\n<ng-content />\n", styles: ["nxe-radio-group{display:block;min-width:0}nxe-radio-group .nxe-radio-group-items{display:flex;flex-direction:column;gap:var(--nxe-ui-space-2, 8px)}nxe-radio-group.nxe-radio-group-horizontal .nxe-radio-group-items{flex-flow:row wrap;gap:var(--nxe-ui-space-2, 8px) var(--nxe-ui-space-5, 20px)}nxe-radio-group .nxe-radio{display:inline-flex;align-items:flex-start;gap:var(--nxe-ui-space-2, 8px);align-self:flex-start;max-width:100%;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:var(--nxe-ui-line-height, 1.35715)}nxe-radio-group .nxe-radio:focus{outline:none}nxe-radio-group .nxe-radio-icon{position:relative;box-sizing:border-box;flex-shrink:0;margin-block-start:1px;width:18px;height:18px;border:1px solid var(--nxe-ui-border, #ddd);border-radius:50%;background-color:var(--nxe-ui-surface, #fff)}nxe-radio-group .nxe-radio-checked .nxe-radio-icon{border-color:var(--nxe-ui-accent, #337ab7)}nxe-radio-group .nxe-radio-checked .nxe-radio-icon:after{content:\"\";position:absolute;inset:3px;border-radius:50%;background-color:var(--nxe-ui-accent, #337ab7)}@media(forced-colors:active){nxe-radio-group .nxe-radio-checked .nxe-radio-icon:after{forced-color-adjust:none;background-color:CanvasText}}nxe-radio-group .nxe-radio-label{min-width:0;overflow-wrap:anywhere}nxe-radio-group .nxe-radio:not(.nxe-radio-disabled):hover .nxe-radio-icon{border-color:var(--nxe-ui-accent-strong, #265a87)}nxe-radio-group .nxe-radio:focus-visible .nxe-radio-icon{border-color:var(--nxe-ui-accent, #337ab7);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-radio-group .nxe-radio-disabled{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-radio-group.nxe-disabled .nxe-radio{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-radio-group.nxe-readonly .nxe-radio{cursor:default}nxe-radio-group.nxe-invalid .nxe-radio-icon{border-color:var(--nxe-ui-invalid, #d9534f)}\n"] }]
3148
+ }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], displayExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayExpr", required: false }] }], valueExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueExpr", required: false }] }], disabledExpr: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabledExpr", required: false }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], itemTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeRadioItemDirective), { isSignal: true }] }] } });
3149
+
3150
+ /**
3151
+ * `nxe-switch`: an on/off setting that takes effect as soon as it is flipped.
3152
+ *
3153
+ * A signal-CVA editor (extends {@link NxeFormControlBase}) over `boolean`; `null` and unset read as
3154
+ * off. Works with `[(value)]`, `[(ngModel)]` and Reactive Forms. The host is the control:
3155
+ * `role="switch"`, `aria-checked`, one tab stop; click and Space toggle it. A read-only switch is
3156
+ * focusable but never flips; a disabled one leaves the tab order and dims.
3157
+ *
3158
+ * `text` is the label beside the track and names the switch (inside an `nxe-form-field` the field's
3159
+ * label does instead). `onText` and `offText` are optional words drawn inside the track; they are
3160
+ * decoration, hidden from assistive tech, because `aria-checked` already says which state it is in.
3161
+ */
3162
+ class NxeSwitchComponent extends NxeFormControlBase {
3163
+ /** Label beside the track. */
3164
+ text = input(undefined, /* @ts-ignore */
3165
+ ...(ngDevMode ? [{ debugName: "text" }] : /* istanbul ignore next */ []));
3166
+ /** Word shown inside the track while on. */
3167
+ onText = input(undefined, /* @ts-ignore */
3168
+ ...(ngDevMode ? [{ debugName: "onText" }] : /* istanbul ignore next */ []));
3169
+ /** Word shown inside the track while off. */
3170
+ offText = input(undefined, /* @ts-ignore */
3171
+ ...(ngDevMode ? [{ debugName: "offText" }] : /* istanbul ignore next */ []));
3172
+ /** Track size. */
3173
+ size = input('medium', /* @ts-ignore */
3174
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
3175
+ checked = computed(() => !!this.value(), /* @ts-ignore */
3176
+ ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
3177
+ toggle(event) {
3178
+ if (this.disabled() || this.readOnly()) {
3179
+ return;
3180
+ }
3181
+ this.commitValue(!this.checked(), event);
3182
+ this.markAsTouched();
3183
+ }
3184
+ onSpace(event) {
3185
+ // Space would otherwise scroll the page.
3186
+ event.preventDefault();
3187
+ this.toggle(event);
3188
+ }
3189
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSwitchComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3190
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeSwitchComponent, isStandalone: true, selector: "nxe-switch", inputs: { text: { classPropertyName: "text", publicName: "text", isSignal: true, isRequired: false, transformFunction: null }, onText: { classPropertyName: "onText", publicName: "onText", isSignal: true, isRequired: false, transformFunction: null }, offText: { classPropertyName: "offText", publicName: "offText", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "switch" }, listeners: { "click": "toggle($event)", "keydown.space": "onSpace($event)", "blur": "markAsTouched()" }, properties: { "attr.aria-checked": "checked()", "attr.aria-disabled": "disabled() ? true : null", "attr.aria-readonly": "readOnly() ? true : null", "attr.aria-invalid": "invalidState() ? true : null", "attr.aria-required": "required() ? true : null", "attr.id": "inputId() ? controlId() : null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "ariaLabelledBy() || null", "attr.tabindex": "disabled() ? null : (tabIndex() ?? 0)", "class.nxe-switch-checked": "checked()", "class.nxe-switch-small": "size() === 'small'", "class.nxe-switch-large": "size() === 'large'", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-switch" }, providers: [
3191
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeSwitchComponent), multi: true },
3192
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeSwitchComponent) },
3193
+ ], usesInheritance: true, ngImport: i0, template: "<span class=\"nxe-switch-track\" aria-hidden=\"true\">\n @if (onText() || offText()) {\n <span class=\"nxe-switch-text nxe-switch-on-text\">{{ onText() }}</span>\n <span class=\"nxe-switch-text nxe-switch-off-text\">{{ offText() }}</span>\n }\n <span class=\"nxe-switch-thumb\"></span>\n</span>\n@if (text()) {\n <span class=\"nxe-switch-label\">{{ text() }}</span>\n}\n<ng-content />\n", styles: ["nxe-switch{--_track-w: var(--nxe-switch-track-w, 40px);--_track-h: var(--nxe-switch-track-h, 22px);--_thumb: var(--nxe-switch-thumb, 16px);--_pad: calc((var(--_track-h) - var(--_thumb)) / 2);display:inline-flex;align-items:center;gap:var(--nxe-ui-space-2, 8px);vertical-align:middle;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:normal}nxe-switch:focus{outline:none}nxe-switch.nxe-switch-small{--_track-w: var(--nxe-switch-track-w, 30px);--_track-h: var(--nxe-switch-track-h, 18px);--_thumb: var(--nxe-switch-thumb, 12px)}nxe-switch.nxe-switch-large{--_track-w: var(--nxe-switch-track-w, 52px);--_track-h: var(--nxe-switch-track-h, 28px);--_thumb: var(--nxe-switch-thumb, 22px)}nxe-switch .nxe-switch-track{position:relative;display:inline-grid;flex-shrink:0;align-items:center;box-sizing:border-box;min-width:var(--_track-w);height:var(--_track-h);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius-pill, 999px);background-color:var(--nxe-ui-neutral-80, #f4f4f4);font-size:var(--nxe-ui-font-size-xs, 12px);font-weight:var(--nxe-ui-font-weight-medium, 500);color:var(--nxe-ui-text, #333);white-space:nowrap}nxe-switch .nxe-switch-text{grid-area:1/1;text-align:center;padding-inline:var(--_pad)}nxe-switch .nxe-switch-on-text{padding-inline-end:calc(var(--_thumb) + 2 * var(--_pad));visibility:hidden}nxe-switch .nxe-switch-off-text{padding-inline-start:calc(var(--_thumb) + 2 * var(--_pad))}nxe-switch .nxe-switch-thumb{position:absolute;top:50%;inset-inline-start:calc(var(--_pad) - 1px);width:var(--_thumb);height:var(--_thumb);margin-block-start:calc(var(--_thumb) / -2);border-radius:50%;background-color:var(--nxe-ui-surface, #fff);box-shadow:0 1px 2px #0000004d}@media(prefers-reduced-motion:no-preference){nxe-switch .nxe-switch-thumb{transition:inset-inline-start .15s ease}}nxe-switch.nxe-switch-checked .nxe-switch-track{border-color:var(--nxe-ui-accent, #337ab7);background-color:var(--nxe-ui-accent, #337ab7);color:var(--nxe-ui-accent-contrast, #fff)}nxe-switch.nxe-switch-checked .nxe-switch-thumb{inset-inline-start:calc(100% - var(--_thumb) - var(--_pad) + 1px)}nxe-switch.nxe-switch-checked .nxe-switch-on-text{visibility:visible}nxe-switch.nxe-switch-checked .nxe-switch-off-text{visibility:hidden}nxe-switch:not(.nxe-disabled):not(.nxe-switch-checked):hover .nxe-switch-track{border-color:var(--nxe-ui-accent-strong, #265a87)}nxe-switch:focus-visible .nxe-switch-track{box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-switch.nxe-disabled{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-switch.nxe-readonly{cursor:default}nxe-switch.nxe-invalid .nxe-switch-track{border-color:var(--nxe-ui-invalid, #d9534f)}@media(forced-colors:active){nxe-switch .nxe-switch-track{forced-color-adjust:none;border-color:CanvasText;background-color:Canvas;color:CanvasText}nxe-switch .nxe-switch-thumb{background-color:CanvasText}nxe-switch.nxe-switch-checked .nxe-switch-track{background-color:Highlight;color:HighlightText}nxe-switch.nxe-switch-checked .nxe-switch-thumb{background-color:HighlightText}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3194
+ }
3195
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeSwitchComponent, decorators: [{
3196
+ type: Component,
3197
+ args: [{ selector: 'nxe-switch', host: {
3198
+ class: 'nxe-widget nxe-switch',
3199
+ role: 'switch',
3200
+ '[attr.aria-checked]': 'checked()',
3201
+ '[attr.aria-disabled]': 'disabled() ? true : null',
3202
+ '[attr.aria-readonly]': 'readOnly() ? true : null',
3203
+ '[attr.aria-invalid]': 'invalidState() ? true : null',
3204
+ '[attr.aria-required]': 'required() ? true : null',
3205
+ // The host IS the control, so the id belongs on it rather than on a child.
3206
+ '[attr.id]': 'inputId() ? controlId() : null',
3207
+ '[attr.aria-label]': 'ariaLabel() || null',
3208
+ '[attr.aria-labelledby]': 'ariaLabelledBy() || null',
3209
+ '[attr.tabindex]': 'disabled() ? null : (tabIndex() ?? 0)',
3210
+ '[class.nxe-switch-checked]': 'checked()',
3211
+ '[class.nxe-switch-small]': "size() === 'small'",
3212
+ '[class.nxe-switch-large]': "size() === 'large'",
3213
+ '[class.nxe-invalid]': 'invalidState()',
3214
+ '(click)': 'toggle($event)',
3215
+ '(keydown.space)': 'onSpace($event)',
3216
+ '(blur)': 'markAsTouched()',
3217
+ }, providers: [
3218
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeSwitchComponent), multi: true },
3219
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeSwitchComponent) },
3220
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<span class=\"nxe-switch-track\" aria-hidden=\"true\">\n @if (onText() || offText()) {\n <span class=\"nxe-switch-text nxe-switch-on-text\">{{ onText() }}</span>\n <span class=\"nxe-switch-text nxe-switch-off-text\">{{ offText() }}</span>\n }\n <span class=\"nxe-switch-thumb\"></span>\n</span>\n@if (text()) {\n <span class=\"nxe-switch-label\">{{ text() }}</span>\n}\n<ng-content />\n", styles: ["nxe-switch{--_track-w: var(--nxe-switch-track-w, 40px);--_track-h: var(--nxe-switch-track-h, 22px);--_thumb: var(--nxe-switch-thumb, 16px);--_pad: calc((var(--_track-h) - var(--_thumb)) / 2);display:inline-flex;align-items:center;gap:var(--nxe-ui-space-2, 8px);vertical-align:middle;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:normal}nxe-switch:focus{outline:none}nxe-switch.nxe-switch-small{--_track-w: var(--nxe-switch-track-w, 30px);--_track-h: var(--nxe-switch-track-h, 18px);--_thumb: var(--nxe-switch-thumb, 12px)}nxe-switch.nxe-switch-large{--_track-w: var(--nxe-switch-track-w, 52px);--_track-h: var(--nxe-switch-track-h, 28px);--_thumb: var(--nxe-switch-thumb, 22px)}nxe-switch .nxe-switch-track{position:relative;display:inline-grid;flex-shrink:0;align-items:center;box-sizing:border-box;min-width:var(--_track-w);height:var(--_track-h);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius-pill, 999px);background-color:var(--nxe-ui-neutral-80, #f4f4f4);font-size:var(--nxe-ui-font-size-xs, 12px);font-weight:var(--nxe-ui-font-weight-medium, 500);color:var(--nxe-ui-text, #333);white-space:nowrap}nxe-switch .nxe-switch-text{grid-area:1/1;text-align:center;padding-inline:var(--_pad)}nxe-switch .nxe-switch-on-text{padding-inline-end:calc(var(--_thumb) + 2 * var(--_pad));visibility:hidden}nxe-switch .nxe-switch-off-text{padding-inline-start:calc(var(--_thumb) + 2 * var(--_pad))}nxe-switch .nxe-switch-thumb{position:absolute;top:50%;inset-inline-start:calc(var(--_pad) - 1px);width:var(--_thumb);height:var(--_thumb);margin-block-start:calc(var(--_thumb) / -2);border-radius:50%;background-color:var(--nxe-ui-surface, #fff);box-shadow:0 1px 2px #0000004d}@media(prefers-reduced-motion:no-preference){nxe-switch .nxe-switch-thumb{transition:inset-inline-start .15s ease}}nxe-switch.nxe-switch-checked .nxe-switch-track{border-color:var(--nxe-ui-accent, #337ab7);background-color:var(--nxe-ui-accent, #337ab7);color:var(--nxe-ui-accent-contrast, #fff)}nxe-switch.nxe-switch-checked .nxe-switch-thumb{inset-inline-start:calc(100% - var(--_thumb) - var(--_pad) + 1px)}nxe-switch.nxe-switch-checked .nxe-switch-on-text{visibility:visible}nxe-switch.nxe-switch-checked .nxe-switch-off-text{visibility:hidden}nxe-switch:not(.nxe-disabled):not(.nxe-switch-checked):hover .nxe-switch-track{border-color:var(--nxe-ui-accent-strong, #265a87)}nxe-switch:focus-visible .nxe-switch-track{box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-switch.nxe-disabled{cursor:default;opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-switch.nxe-readonly{cursor:default}nxe-switch.nxe-invalid .nxe-switch-track{border-color:var(--nxe-ui-invalid, #d9534f)}@media(forced-colors:active){nxe-switch .nxe-switch-track{forced-color-adjust:none;border-color:CanvasText;background-color:Canvas;color:CanvasText}nxe-switch .nxe-switch-thumb{background-color:CanvasText}nxe-switch.nxe-switch-checked .nxe-switch-track{background-color:Highlight;color:HighlightText}nxe-switch.nxe-switch-checked .nxe-switch-thumb{background-color:HighlightText}}\n"] }]
3221
+ }], propDecorators: { text: [{ type: i0.Input, args: [{ isSignal: true, alias: "text", required: false }] }], onText: [{ type: i0.Input, args: [{ isSignal: true, alias: "onText", required: false }] }], offText: [{ type: i0.Input, args: [{ isSignal: true, alias: "offText", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
3222
+
3223
+ const STRIP = {
3224
+ numeric: /\D/g,
3225
+ alphanumeric: /[^A-Za-z0-9]/g,
3226
+ };
3227
+ /**
3228
+ * `nxe-otp-box`: a one-time code (2FA sign-in, e-mail verification, recovery code) drawn as one
3229
+ * cell per character.
3230
+ *
3231
+ * It is ONE control, not N: a single transparent `<input>` lies over the cells and they only draw
3232
+ * its text. That is what keeps a screen reader on one named field with one value, gives native
3233
+ * paste, and lets the browser fill the whole code at once from an SMS or password manager
3234
+ * (`autocomplete="one-time-code"`). Typing fills the next cell, Backspace empties the last one, and
3235
+ * the caret is kept at the end, so the arrow keys do nothing. Pasting a code of `length` characters
3236
+ * (or more) replaces the value, with separators such as a dash or space dropped; a shorter paste is
3237
+ * appended.
3238
+ *
3239
+ * A signal-CVA over `string` (extends {@link NxeFormControlBase}); the value follows every
3240
+ * keystroke, so a `minLength` validator sees a partial code and `completed` fires once with the full
3241
+ * code. Name it with `aria-label`, `aria-labelledby` or an `nxe-form-field` label.
3242
+ */
3243
+ class NxeOtpBoxComponent extends NxeFormControlBase {
3244
+ /** Number of characters in the code. */
3245
+ length = input(6, { ...(ngDevMode ? { debugName: "length" } : /* istanbul ignore next */ {}), transform: (v) => Math.max(1, Math.floor(numberAttribute(v, 6))) });
3246
+ /** `numeric` (default) takes digits and opens the numeric keypad; `alphanumeric` takes letters and digits. */
3247
+ mode = input('numeric', /* @ts-ignore */
3248
+ ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
3249
+ /** Focus the field when it first renders. */
3250
+ autoFocus = input(false, { ...(ngDevMode ? { debugName: "autoFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
3251
+ /** The full code, once, when the last cell is filled (typed, pasted or auto-filled). */
3252
+ completed = output();
3253
+ focused = signal(false, /* @ts-ignore */
3254
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
3255
+ field = viewChild.required('field', /* @ts-ignore */
3256
+ ...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
3257
+ displayValue = computed(() => this.clean(this.value() ?? ''), /* @ts-ignore */
3258
+ ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
3259
+ cells = computed(() => {
3260
+ const text = this.displayValue();
3261
+ const length = this.length();
3262
+ // The cell the next character lands in, or the last one while the code is full.
3263
+ const active = this.focused() && !this.disabled() ? Math.min(text.length, length - 1) : -1;
3264
+ return Array.from({ length }, (_, index) => ({ index, char: text[index] ?? '', active: index === active }));
3265
+ }, /* @ts-ignore */
3266
+ ...(ngDevMode ? [{ debugName: "cells" }] : /* istanbul ignore next */ []));
3267
+ constructor() {
3268
+ super();
3269
+ afterNextRender(() => {
3270
+ if (this.autoFocus()) {
3271
+ this.field().nativeElement.focus();
3272
+ }
3273
+ });
3274
+ }
3275
+ /** Drop what the mode does not allow and cut to `length`. */
3276
+ clean(text) {
3277
+ return text.replace(STRIP[this.mode()], '').slice(0, this.length());
3278
+ }
3279
+ /** Typing, deleting and autofill: the field's text is the new value. */
3280
+ handleInput(event) {
3281
+ const el = event.target;
3282
+ const text = this.clean(el.value);
3283
+ // A rejected character or an overflowing one leaves the model unchanged, so the field has to be put right by hand.
3284
+ if (el.value !== text) {
3285
+ el.value = text;
3286
+ }
3287
+ this.apply(text, event);
3288
+ this.keepCaretAtEnd();
3289
+ }
3290
+ /** A whole code replaces the value; a fragment is appended. */
3291
+ onPaste(event) {
3292
+ event.preventDefault();
3293
+ if (this.readOnly() || this.disabled()) {
3294
+ return;
3295
+ }
3296
+ const pasted = (event.clipboardData?.getData('text') ?? '').replace(STRIP[this.mode()], '');
3297
+ if (!pasted) {
3298
+ return;
3299
+ }
3300
+ const el = event.target;
3301
+ const next = this.clean(pasted.length >= this.length() ? pasted : el.value + pasted);
3302
+ el.value = next;
3303
+ this.apply(next, event);
3304
+ this.keepCaretAtEnd();
3305
+ }
3306
+ /** Keys that would move the caret off the end do nothing: the cells have no caret to move. */
3307
+ onKeydown(event) {
3308
+ if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
3309
+ event.preventDefault();
3310
+ }
3311
+ }
3312
+ onFocus() {
3313
+ this.focused.set(true);
3314
+ this.keepCaretAtEnd();
3315
+ }
3316
+ onBlur() {
3317
+ this.focused.set(false);
3318
+ this.markAsTouched();
3319
+ }
3320
+ /** A click or a drag could put the caret mid-code or select it; it always belongs at the end. */
3321
+ keepCaretAtEnd() {
3322
+ const el = this.field().nativeElement;
3323
+ const end = el.value.length;
3324
+ if (el.selectionStart !== end || el.selectionEnd !== end) {
3325
+ el.setSelectionRange(end, end);
3326
+ }
3327
+ }
3328
+ apply(text, event) {
3329
+ const before = this.value() ?? '';
3330
+ this.commitValue(text, event);
3331
+ if (text.length === this.length() && text !== before) {
3332
+ this.completed.emit(text);
3333
+ }
3334
+ }
3335
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeOtpBoxComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
3336
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeOtpBoxComponent, isStandalone: true, selector: "nxe-otp-box", inputs: { length: { classPropertyName: "length", publicName: "length", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, autoFocus: { classPropertyName: "autoFocus", publicName: "autoFocus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { completed: "completed" }, host: { properties: { "class.nxe-focused": "focused()", "class.nxe-invalid": "invalidState()", "attr.aria-label": "null", "attr.aria-labelledby": "null" }, classAttribute: "nxe-widget nxe-otp-box" }, providers: [
3337
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeOtpBoxComponent), multi: true },
3338
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeOtpBoxComponent) },
3339
+ ], viewQueries: [{ propertyName: "field", first: true, predicate: ["field"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "<div class=\"nxe-otp-cells\" aria-hidden=\"true\">\n @for (cell of cells(); track cell.index) {\n <span\n class=\"nxe-otp-cell\"\n [class.nxe-otp-cell-filled]=\"cell.char !== ''\"\n [class.nxe-otp-cell-active]=\"cell.active\">{{ cell.char }}</span>\n }\n</div>\n<!--\n One real input sits over the cells, so the code is a single control: one tab stop, one name, one\n value for a screen reader, native paste and the one-time-code autofill. The cells only draw it.\n-->\n<input\n #field\n class=\"nxe-otp-input\"\n type=\"text\"\n autocomplete=\"one-time-code\"\n autocapitalize=\"off\"\n autocorrect=\"off\"\n spellcheck=\"false\"\n [id]=\"controlId()\"\n [attr.inputmode]=\"mode() === 'numeric' ? 'numeric' : 'text'\"\n [attr.pattern]=\"mode() === 'numeric' ? '[0-9]*' : null\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [value]=\"displayValue()\"\n [readOnly]=\"readOnly()\"\n [disabled]=\"disabled()\"\n (input)=\"handleInput($event)\"\n (paste)=\"onPaste($event)\"\n (keydown)=\"onKeydown($event)\"\n (focus)=\"onFocus()\"\n (click)=\"keepCaretAtEnd()\"\n (select)=\"keepCaretAtEnd()\"\n (blur)=\"onBlur()\" />\n<ng-content />\n", styles: ["nxe-otp-box{--_cell-w: var(--nxe-otp-cell-w, 40px);--_cell-h: var(--nxe-otp-cell-h, 48px);--_gap: var(--nxe-otp-gap, 8px);position:relative;display:inline-block;max-width:100%;vertical-align:middle}nxe-otp-box .nxe-otp-cells{display:flex;gap:var(--_gap)}nxe-otp-box .nxe-otp-cell{box-sizing:border-box;display:inline-flex;flex:0 0 var(--_cell-w);align-items:center;justify-content:center;width:var(--_cell-w);height:var(--_cell-h);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background-color:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size-xl, 20px);font-weight:var(--nxe-ui-font-weight-medium, 500);font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none}nxe-otp-box .nxe-otp-input{position:absolute;inset:0;box-sizing:border-box;width:100%;height:100%;margin:0;padding:0;border:0;outline:0;background:transparent;color:transparent;caret-color:transparent;font-size:16px;opacity:0;cursor:text}nxe-otp-box:hover .nxe-otp-cell{border-color:var(--nxe-ui-accent-faded, rgba(51, 122, 183, .4))}nxe-otp-box.nxe-focused .nxe-otp-cell-active{border-color:var(--nxe-ui-accent, #337ab7);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-otp-box.nxe-invalid .nxe-otp-cell,nxe-otp-box.nxe-invalid:hover .nxe-otp-cell{border-color:var(--nxe-ui-invalid, #d9534f)}nxe-otp-box.nxe-disabled{opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-otp-box.nxe-disabled .nxe-otp-input{cursor:default}nxe-otp-box.nxe-readonly .nxe-otp-cell{border-style:dashed}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3340
+ }
3341
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeOtpBoxComponent, decorators: [{
3342
+ type: Component,
3343
+ args: [{ selector: 'nxe-otp-box', host: {
3344
+ class: 'nxe-widget nxe-otp-box',
3345
+ '[class.nxe-focused]': 'focused()',
3346
+ '[class.nxe-invalid]': 'invalidState()',
3347
+ // The names belong on the inner input; a copy on this generic host would be prohibited.
3348
+ '[attr.aria-label]': 'null',
3349
+ '[attr.aria-labelledby]': 'null',
3350
+ }, providers: [
3351
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeOtpBoxComponent), multi: true },
3352
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeOtpBoxComponent) },
3353
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-otp-cells\" aria-hidden=\"true\">\n @for (cell of cells(); track cell.index) {\n <span\n class=\"nxe-otp-cell\"\n [class.nxe-otp-cell-filled]=\"cell.char !== ''\"\n [class.nxe-otp-cell-active]=\"cell.active\">{{ cell.char }}</span>\n }\n</div>\n<!--\n One real input sits over the cells, so the code is a single control: one tab stop, one name, one\n value for a screen reader, native paste and the one-time-code autofill. The cells only draw it.\n-->\n<input\n #field\n class=\"nxe-otp-input\"\n type=\"text\"\n autocomplete=\"one-time-code\"\n autocapitalize=\"off\"\n autocorrect=\"off\"\n spellcheck=\"false\"\n [id]=\"controlId()\"\n [attr.inputmode]=\"mode() === 'numeric' ? 'numeric' : 'text'\"\n [attr.pattern]=\"mode() === 'numeric' ? '[0-9]*' : null\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-label]=\"ariaLabel() || null\"\n [attr.aria-labelledby]=\"ariaLabelledBy() || null\"\n [attr.aria-invalid]=\"invalidState() ? true : null\"\n [attr.aria-required]=\"required() ? true : null\"\n [value]=\"displayValue()\"\n [readOnly]=\"readOnly()\"\n [disabled]=\"disabled()\"\n (input)=\"handleInput($event)\"\n (paste)=\"onPaste($event)\"\n (keydown)=\"onKeydown($event)\"\n (focus)=\"onFocus()\"\n (click)=\"keepCaretAtEnd()\"\n (select)=\"keepCaretAtEnd()\"\n (blur)=\"onBlur()\" />\n<ng-content />\n", styles: ["nxe-otp-box{--_cell-w: var(--nxe-otp-cell-w, 40px);--_cell-h: var(--nxe-otp-cell-h, 48px);--_gap: var(--nxe-otp-gap, 8px);position:relative;display:inline-block;max-width:100%;vertical-align:middle}nxe-otp-box .nxe-otp-cells{display:flex;gap:var(--_gap)}nxe-otp-box .nxe-otp-cell{box-sizing:border-box;display:inline-flex;flex:0 0 var(--_cell-w);align-items:center;justify-content:center;width:var(--_cell-w);height:var(--_cell-h);border:1px solid var(--nxe-ui-border, #ddd);border-radius:var(--nxe-ui-radius, 4px);background-color:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);font-size:var(--nxe-ui-font-size-xl, 20px);font-weight:var(--nxe-ui-font-weight-medium, 500);font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none}nxe-otp-box .nxe-otp-input{position:absolute;inset:0;box-sizing:border-box;width:100%;height:100%;margin:0;padding:0;border:0;outline:0;background:transparent;color:transparent;caret-color:transparent;font-size:16px;opacity:0;cursor:text}nxe-otp-box:hover .nxe-otp-cell{border-color:var(--nxe-ui-accent-faded, rgba(51, 122, 183, .4))}nxe-otp-box.nxe-focused .nxe-otp-cell-active{border-color:var(--nxe-ui-accent, #337ab7);box-shadow:var(--nxe-ui-focus-ring, 0 0 0 2px rgba(51, 122, 183, .5))}nxe-otp-box.nxe-invalid .nxe-otp-cell,nxe-otp-box.nxe-invalid:hover .nxe-otp-cell{border-color:var(--nxe-ui-invalid, #d9534f)}nxe-otp-box.nxe-disabled{opacity:var(--nxe-ui-disabled-opacity, .5)}nxe-otp-box.nxe-disabled .nxe-otp-input{cursor:default}nxe-otp-box.nxe-readonly .nxe-otp-cell{border-style:dashed}\n"] }]
3354
+ }], ctorParameters: () => [], propDecorators: { length: [{ type: i0.Input, args: [{ isSignal: true, alias: "length", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], autoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocus", required: false }] }], completed: [{ type: i0.Output, args: ["completed"] }], field: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }] } });
3355
+
1465
3356
  // @nexterajs/ui/forms — public API.
1466
3357
 
1467
3358
  /**
1468
3359
  * Generated bundle index. Do not edit.
1469
3360
  */
1470
3361
 
1471
- export { NxeButtonComponent, NxeCheckBoxComponent, NxeDateBoxComponent, NxeNumberBoxComponent, NxeScrollViewComponent, NxeTextAreaComponent, NxeTextBoxComponent };
3362
+ export { NXE_DATE_RANGE_DISPLAY_FORMAT, NXE_DATE_RANGE_NOW, NXE_DATE_RANGE_VALUE_FORMAT, NXE_DEFAULT_ERROR_MESSAGES, NXE_ERROR_MESSAGES, NXE_FALLBACK_ERROR_KEY, NxeButtonComponent, NxeCheckBoxComponent, NxeDateBoxComponent, NxeDateRangeBoxComponent, NxeFormFieldComponent, NxeNumberBoxComponent, NxeOtpBoxComponent, NxeRadioGroupComponent, NxeRadioItemDirective, NxeScrollViewComponent, NxeSwitchComponent, NxeTextAreaComponent, NxeTextBoxComponent, nxeDateRangePresets, provideNxeErrorMessages };
1472
3363
  //# sourceMappingURL=nexterajs-ui-forms.mjs.map