@egose/shadcn-theme-ng 0.3.0 → 0.5.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 (68) hide show
  1. package/README.md +1 -1
  2. package/form-autocomplete/README.md +20 -19
  3. package/form-autocomplete/fesm2022/form-autocomplete.mjs +44 -9
  4. package/form-autocomplete/types/form-autocomplete.d.ts +22 -1
  5. package/form-checkbox/README.md +20 -19
  6. package/form-checkbox/fesm2022/form-checkbox.mjs +99 -15
  7. package/form-checkbox/types/form-checkbox.d.ts +43 -3
  8. package/form-combobox/README.md +21 -20
  9. package/form-combobox/fesm2022/form-combobox.mjs +44 -9
  10. package/form-combobox/types/form-combobox.d.ts +22 -1
  11. package/form-date-picker/README.md +28 -27
  12. package/form-date-picker/fesm2022/form-date-picker.mjs +45 -15
  13. package/form-date-picker/types/form-date-picker.d.ts +22 -1
  14. package/form-date-picker-multi/README.md +28 -27
  15. package/form-date-picker-multi/fesm2022/form-date-picker-multi.mjs +45 -15
  16. package/form-date-picker-multi/types/form-date-picker-multi.d.ts +22 -1
  17. package/form-date-range-picker/README.md +28 -27
  18. package/form-date-range-picker/fesm2022/form-date-range-picker.mjs +45 -15
  19. package/form-date-range-picker/types/form-date-range-picker.d.ts +22 -1
  20. package/form-field/README.md +9 -0
  21. package/form-field/fesm2022/form-field.mjs +97 -5
  22. package/form-field/types/form-field.d.ts +96 -2
  23. package/form-field-simple/fesm2022/form-field-simple.mjs +15 -1
  24. package/form-field-simple/types/form-field-simple.d.ts +9 -2
  25. package/form-input-otp/README.md +17 -16
  26. package/form-input-otp/fesm2022/form-input-otp.mjs +43 -8
  27. package/form-input-otp/types/form-input-otp.d.ts +22 -1
  28. package/form-month-year-picker/README.md +26 -25
  29. package/form-month-year-picker/fesm2022/form-month-year-picker.mjs +45 -15
  30. package/form-month-year-picker/types/form-month-year-picker.d.ts +22 -1
  31. package/form-native-select/README.md +18 -17
  32. package/form-native-select/fesm2022/form-native-select.mjs +44 -14
  33. package/form-native-select/types/form-native-select.d.ts +22 -1
  34. package/form-phone-input/README.md +23 -22
  35. package/form-phone-input/fesm2022/form-phone-input.mjs +44 -14
  36. package/form-phone-input/types/form-phone-input.d.ts +22 -1
  37. package/form-radio-group/README.md +18 -17
  38. package/form-radio-group/fesm2022/form-radio-group.mjs +44 -14
  39. package/form-radio-group/types/form-radio-group.d.ts +22 -1
  40. package/form-searchable-multiselect/README.md +18 -17
  41. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +44 -14
  42. package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +22 -1
  43. package/form-select/README.md +20 -19
  44. package/form-select/fesm2022/form-select.mjs +44 -14
  45. package/form-select/types/form-select.d.ts +22 -1
  46. package/form-slider/README.md +19 -18
  47. package/form-slider/fesm2022/form-slider.mjs +43 -8
  48. package/form-slider/types/form-slider.d.ts +22 -1
  49. package/form-switch/README.md +16 -15
  50. package/form-switch/fesm2022/form-switch.mjs +44 -14
  51. package/form-switch/types/form-switch.d.ts +22 -1
  52. package/form-text-input/README.md +27 -26
  53. package/form-text-input/fesm2022/form-text-input.mjs +44 -14
  54. package/form-text-input/types/form-text-input.d.ts +22 -1
  55. package/form-textarea/README.md +23 -22
  56. package/form-textarea/fesm2022/form-textarea.mjs +44 -14
  57. package/form-textarea/types/form-textarea.d.ts +22 -1
  58. package/form-toggle/README.md +17 -16
  59. package/form-toggle/fesm2022/form-toggle.mjs +47 -10
  60. package/form-toggle/types/form-toggle.d.ts +22 -3
  61. package/form-toggle-group/README.md +20 -19
  62. package/form-toggle-group/fesm2022/form-toggle-group.mjs +44 -14
  63. package/form-toggle-group/types/form-toggle-group.d.ts +22 -1
  64. package/package.json +5 -1
  65. package/stepper/README.md +353 -0
  66. package/stepper/fesm2022/stepper.mjs +396 -0
  67. package/stepper/package.json +24 -0
  68. package/stepper/types/stepper.d.ts +116 -0
@@ -84,33 +84,34 @@ Selector (from source): `eg-form-date-range-picker` (standalone component, host
84
84
 
85
85
  ### `EgFormDateRangePicker` — `eg-form-date-range-picker`
86
86
 
87
- | Input | Type | Default | Description |
88
- | ------------------------- | ------------------------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------- |
89
- | `controlName` | `string` | `''` | `formControlName` key inside the parent `FormGroup`. **Required.** |
90
- | `label` | `string \| undefined` | — | Label text (hidden when omitted). |
91
- | `error` | `string \| undefined` | — | Error text shown when invalid. |
92
- | `hint` | `string \| undefined` | — | Hint text shown otherwise. |
93
- | `controlId` | `string \| undefined` | — | Explicit id (first priority for `effectiveId`). |
94
- | `id` | `string \| undefined` | — | Fallback id (second priority). |
95
- | `name` | `string \| undefined` | — | Native `name` forwarded to the inner input (`controlName` wins when set, like `eg-form-text-input`). |
96
- | `placeholder` | `string` | `'Pick a date range'` | Input placeholder. |
97
- | `readonly` | `boolean` | `false` | Native `readonly` forwarded to the inner input (locks typed text; use `disabled` to lock the calendar too). |
98
- | `disabled` | `boolean` | `false` | Locks interaction via picker `disabled` (form control stays enabled). |
99
- | `required` | `boolean` | `false` | Shows a red `*` next to the label. |
100
- | `min` | `Date \| string \| null` | `null` | Minimum date forwarded to `hlm-date-range-picker`. |
101
- | `max` | `Date \| string \| null` | `null` | Maximum date forwarded to `hlm-date-range-picker`. |
102
- | `autoCloseOnEndSelection` | `boolean` | config default | Forwarded to `hlm-date-range-picker` (falls back to `provideHlmDateRangePickerConfig`, default `false`). |
103
- | `captionLayout` | union | `'label'` | Forwarded to `hlm-date-range-picker` (`'dropdown' \| 'label' \| 'dropdown-months' \| 'dropdown-years'`). |
104
- | `formatDates` | `(dates: [Date \| null, Date \| null]) => string` | config default | Display format, forwarded to `hlm-date-range-picker` (falls back to `provideHlmDateRangePickerConfig`). |
105
- | `transformDates` | `(dates: [Date, Date]) => [Date, Date]` | config default | Model transform, forwarded to `hlm-date-range-picker` (falls back to `provideHlmDateRangePickerConfig`). |
106
- | `parseDate` | `(value: string) => [Date, Date] \| null` | config default | Input parsing, forwarded to `hlm-date-range-input` (falls back to `provideHlmDateRangePickerConfig`). |
107
- | `formatInputDates` | `(dates: [Date \| null, Date \| null]) => string` | config default | Edit format while focused, forwarded to `hlm-date-range-input` (falls back to `provideHlmDateRangePickerConfig`). |
108
- | `class` | `ClassValue` | `''` | Extra host classes (base `tw:w-full`). |
109
- | `labelClass` | `string` | `''` | Extra label classes (base `tw:mb-1`). |
110
- | `pickerClass` | `string` | `''` | Extra picker-container classes (base `tw:mb-1`). |
111
- | `inputClass` | `string` | `''` | Extra inner-input classes (e.g. `tw:text-lg` for text size). |
112
- | `errorClass` | `string` | `''` | Extra error classes (base `tw:mt-0`). |
113
- | `hintClass` | `string` | `''` | Extra hint classes (base `tw:mt-0`). |
87
+ | Input | Type | Default | Description |
88
+ | ------------------------- | ------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
89
+ | `controlName` | `string` | `''` | `formControlName` key inside the parent `FormGroup`. **Required.** |
90
+ | `label` | `string \| undefined` | — | Label text (hidden when omitted). |
91
+ | `error` | `string \| undefined` | — | Error text shown when invalid. |
92
+ | `autoError` | `boolean` | `true` | Auto-resolve the message from the control's `ValidationErrors` when `error` is unset. Explicit `error` always wins; `error=""` counts as unset. Global wording via `provideEgFormErrorMessages`. |
93
+ | `hint` | `string \| undefined` | — | Hint text shown otherwise. |
94
+ | `controlId` | `string \| undefined` | — | Explicit id (first priority for `effectiveId`). |
95
+ | `id` | `string \| undefined` | — | Fallback id (second priority). |
96
+ | `name` | `string \| undefined` | — | Native `name` forwarded to the inner input (`controlName` wins when set, like `eg-form-text-input`). |
97
+ | `placeholder` | `string` | `'Pick a date range'` | Input placeholder. |
98
+ | `readonly` | `boolean` | `false` | Native `readonly` forwarded to the inner input (locks typed text; use `disabled` to lock the calendar too). |
99
+ | `disabled` | `boolean` | `false` | Locks interaction via picker `disabled` (form control stays enabled). |
100
+ | `required` | `boolean` | `false` | Shows a red `*` next to the label. |
101
+ | `min` | `Date \| string \| null` | `null` | Minimum date forwarded to `hlm-date-range-picker`. |
102
+ | `max` | `Date \| string \| null` | `null` | Maximum date forwarded to `hlm-date-range-picker`. |
103
+ | `autoCloseOnEndSelection` | `boolean` | config default | Forwarded to `hlm-date-range-picker` (falls back to `provideHlmDateRangePickerConfig`, default `false`). |
104
+ | `captionLayout` | union | `'label'` | Forwarded to `hlm-date-range-picker` (`'dropdown' \| 'label' \| 'dropdown-months' \| 'dropdown-years'`). |
105
+ | `formatDates` | `(dates: [Date \| null, Date \| null]) => string` | config default | Display format, forwarded to `hlm-date-range-picker` (falls back to `provideHlmDateRangePickerConfig`). |
106
+ | `transformDates` | `(dates: [Date, Date]) => [Date, Date]` | config default | Model transform, forwarded to `hlm-date-range-picker` (falls back to `provideHlmDateRangePickerConfig`). |
107
+ | `parseDate` | `(value: string) => [Date, Date] \| null` | config default | Input parsing, forwarded to `hlm-date-range-input` (falls back to `provideHlmDateRangePickerConfig`). |
108
+ | `formatInputDates` | `(dates: [Date \| null, Date \| null]) => string` | config default | Edit format while focused, forwarded to `hlm-date-range-input` (falls back to `provideHlmDateRangePickerConfig`). |
109
+ | `class` | `ClassValue` | `''` | Extra host classes (base `tw:w-full`). |
110
+ | `labelClass` | `string` | `''` | Extra label classes (base `tw:mb-1`). |
111
+ | `pickerClass` | `string` | `''` | Extra picker-container classes (base `tw:mb-1`). |
112
+ | `inputClass` | `string` | `''` | Extra inner-input classes (e.g. `tw:text-lg` for text size). |
113
+ | `errorClass` | `string` | `''` | Extra error classes (base `tw:mt-0`). |
114
+ | `hintClass` | `string` | `''` | Extra hint classes (base `tw:mt-0`). |
114
115
 
115
116
  | Member | Description |
116
117
  | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -2,15 +2,17 @@ import * as i0 from '@angular/core';
2
2
  import { inject, input, computed, Component } from '@angular/core';
3
3
  import * as i1 from '@angular/forms';
4
4
  import { FormGroupDirective, ReactiveFormsModule, ControlContainer } from '@angular/forms';
5
- import { HlmFormIdGenerator, HlmFormField, HlmError, HlmHint } from '@egose/shadcn-theme-ng/form-field';
5
+ import { injectEgFormErrorMessages, HlmFormIdGenerator, injectEgFormSharedConfig, resolveEgFormError, HlmFormField, HlmError, HlmHint } from '@egose/shadcn-theme-ng/form-field';
6
6
  import { HlmLabel } from '@egose/shadcn-theme-ng/label';
7
7
  import { injectHlmDateRangePickerConfig, HlmDateRangePicker, HlmDateRangeInput } from '@egose/shadcn-theme-ng/date-picker';
8
8
  import { hlm } from '@egose/shadcn-theme-ng/utils';
9
9
 
10
10
  class EgFormDateRangePicker {
11
11
  formGroupDirective = inject(FormGroupDirective);
12
+ _errorMessages = injectEgFormErrorMessages();
12
13
  generatedId = inject(HlmFormIdGenerator).generate('eg-form-date-range-picker');
13
14
  _config = injectHlmDateRangePickerConfig();
15
+ _shared = injectEgFormSharedConfig();
14
16
  // Inputs
15
17
  label = input(undefined, /* @ts-ignore */
16
18
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
@@ -20,6 +22,13 @@ class EgFormDateRangePicker {
20
22
  ...(ngDevMode ? [{ debugName: "controlName" }] : /* istanbul ignore next */ []));
21
23
  error = input(undefined, /* @ts-ignore */
22
24
  ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
25
+ /**
26
+ * Auto-resolve the displayed message from the control's `ValidationErrors`
27
+ * when `error()` is unset. Explicit `error()` always wins. Set to `false`
28
+ * for manual-only messages.
29
+ */
30
+ autoError = input(true, /* @ts-ignore */
31
+ ...(ngDevMode ? [{ debugName: "autoError" }] : /* istanbul ignore next */ []));
23
32
  hint = input(undefined, /* @ts-ignore */
24
33
  ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
25
34
  // HTML attributes
@@ -59,6 +68,32 @@ class EgFormDateRangePicker {
59
68
  ...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
60
69
  hintId = computed(() => `${this.effectiveId()}-hint`, /* @ts-ignore */
61
70
  ...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
71
+ /**
72
+ * Displayed message: explicit `error()` wins; otherwise (when `autoError()`)
73
+ * auto-resolved from the control's `ValidationErrors`. Plain method (not a
74
+ * computed): `ValidationErrors` is not a signal, so the control must be read
75
+ * fresh on every change-detection pass.
76
+ */
77
+ resolvedError() {
78
+ const explicit = this.error();
79
+ if (explicit)
80
+ return explicit;
81
+ if (!this.autoError())
82
+ return undefined;
83
+ const control = this.formGroupDirective.form.get(this.controlName());
84
+ return resolveEgFormError(control?.errors ?? null, this.label(), this._errorMessages);
85
+ }
86
+ /**
87
+ * Whether the error is surfaced: message present + control invalid +
88
+ * touched/dirty/submitted. Gates the visual `<hlm-error>` (and
89
+ * `describedBy()`, where present) so sighted and screen-reader output agree.
90
+ */
91
+ showError() {
92
+ const control = this.formGroupDirective.form.get(this.controlName());
93
+ return (!!this.resolvedError() &&
94
+ !!control?.invalid &&
95
+ (control.dirty || control.touched || this.formGroupDirective.submitted));
96
+ }
62
97
  /**
63
98
  * Bounds projected onto the picker's `Date` model. The cast keeps the
64
99
  * template's generic inference on `Date` while passing runtime values
@@ -69,12 +104,7 @@ class EgFormDateRangePicker {
69
104
  $max = computed(() => this.max(), /* @ts-ignore */
70
105
  ...(ngDevMode ? [{ debugName: "$max" }] : /* istanbul ignore next */ []));
71
106
  describedBy() {
72
- const control = this.formGroupDirective.form.get(this.controlName());
73
- return this.error() && control?.invalid && (control.dirty || control.touched)
74
- ? this.errorId()
75
- : this.hint()
76
- ? this.hintId()
77
- : null;
107
+ return this.showError() ? this.errorId() : this.hint() ? this.hintId() : null;
78
108
  }
79
109
  // Styling
80
110
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
@@ -91,18 +121,18 @@ class EgFormDateRangePicker {
91
121
  // Computed classes (library base < global config < per-instance)
92
122
  $userClass = computed(() => hlm("w-full", this.userClass()), /* @ts-ignore */
93
123
  ...(ngDevMode ? [{ debugName: "$userClass" }] : /* istanbul ignore next */ []));
94
- $labelClass = computed(() => hlm("mb-1", this._config.labelClass, this.labelClass()), /* @ts-ignore */
124
+ $labelClass = computed(() => hlm("mb-1", this._shared.labelClass, this._config.labelClass, this.labelClass()), /* @ts-ignore */
95
125
  ...(ngDevMode ? [{ debugName: "$labelClass" }] : /* istanbul ignore next */ []));
96
- $pickerClass = computed(() => hlm("mb-1", this._config.pickerClass, this.pickerClass()), /* @ts-ignore */
126
+ $pickerClass = computed(() => hlm("mb-1", this._shared.pickerClass, this._config.pickerClass, this.pickerClass()), /* @ts-ignore */
97
127
  ...(ngDevMode ? [{ debugName: "$pickerClass" }] : /* istanbul ignore next */ []));
98
- $inputClass = computed(() => hlm(this._config.inputClass, this.inputClass()), /* @ts-ignore */
128
+ $inputClass = computed(() => hlm(this._shared.inputClass, this._config.inputClass, this.inputClass()), /* @ts-ignore */
99
129
  ...(ngDevMode ? [{ debugName: "$inputClass" }] : /* istanbul ignore next */ []));
100
- $errorClass = computed(() => hlm("mt-0", this._config.errorClass, this.errorClass()), /* @ts-ignore */
130
+ $errorClass = computed(() => hlm("mt-0", this._shared.errorClass, this._config.errorClass, this.errorClass()), /* @ts-ignore */
101
131
  ...(ngDevMode ? [{ debugName: "$errorClass" }] : /* istanbul ignore next */ []));
102
- $hintClass = computed(() => hlm("mt-0", this._config.hintClass, this.hintClass()), /* @ts-ignore */
132
+ $hintClass = computed(() => hlm("mt-0", this._shared.hintClass, this._config.hintClass, this.hintClass()), /* @ts-ignore */
103
133
  ...(ngDevMode ? [{ debugName: "$hintClass" }] : /* istanbul ignore next */ []));
104
134
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgFormDateRangePicker, deps: [], target: i0.ɵɵFactoryTarget.Component });
105
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgFormDateRangePicker, isStandalone: true, selector: "eg-form-date-range-picker", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, controlId: { classPropertyName: "controlId", publicName: "controlId", isSignal: true, isRequired: false, transformFunction: null }, controlName: { classPropertyName: "controlName", publicName: "controlName", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", 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 }, autoCloseOnEndSelection: { classPropertyName: "autoCloseOnEndSelection", publicName: "autoCloseOnEndSelection", isSignal: true, isRequired: false, transformFunction: null }, captionLayout: { classPropertyName: "captionLayout", publicName: "captionLayout", isSignal: true, isRequired: false, transformFunction: null }, formatDates: { classPropertyName: "formatDates", publicName: "formatDates", isSignal: true, isRequired: false, transformFunction: null }, transformDates: { classPropertyName: "transformDates", publicName: "transformDates", isSignal: true, isRequired: false, transformFunction: null }, parseDate: { classPropertyName: "parseDate", publicName: "parseDate", isSignal: true, isRequired: false, transformFunction: null }, formatInputDates: { classPropertyName: "formatInputDates", publicName: "formatInputDates", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, labelClass: { classPropertyName: "labelClass", publicName: "labelClass", isSignal: true, isRequired: false, transformFunction: null }, pickerClass: { classPropertyName: "pickerClass", publicName: "pickerClass", isSignal: true, isRequired: false, transformFunction: null }, inputClass: { classPropertyName: "inputClass", publicName: "inputClass", isSignal: true, isRequired: false, transformFunction: null }, errorClass: { classPropertyName: "errorClass", publicName: "errorClass", isSignal: true, isRequired: false, transformFunction: null }, hintClass: { classPropertyName: "hintClass", publicName: "hintClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "$userClass()" } }, providers: [{ provide: ControlContainer, useExisting: FormGroupDirective }], ngImport: i0, template: "\n @let lbl = label();\n @let cnm = controlName();\n @let err = error();\n @let hnt = hint();\n @let rqrd = required();\n\n <hlm-form-field>\n @if (lbl) {\n <label hlmLabel [for]=\"effectiveId()\" [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <hlm-date-range-picker\n [min]=\"$min()\"\n [max]=\"$max()\"\n [disabled]=\"disabled()\"\n [autoCloseOnEndSelection]=\"autoCloseOnEndSelection()\"\n [captionLayout]=\"captionLayout()\"\n [formatDates]=\"formatDates()\"\n [transformDates]=\"transformDates()\"\n [formControlName]=\"cnm\"\n [class]=\"$pickerClass()\"\n >\n <hlm-date-range-input\n [inputId]=\"effectiveId()\"\n [name]=\"cnm || name()\"\n [readonly]=\"readonly()\"\n [ariaLabel]=\"lbl\"\n [ariaDescribedby]=\"describedBy()\"\n [placeholder]=\"placeholder()\"\n [parseDate]=\"parseDate()\"\n [formatInputDates]=\"formatInputDates()\"\n [inputClass]=\"$inputClass()\"\n />\n </hlm-date-range-picker>\n\n @if (err) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ err }}\n </hlm-error>\n }\n\n @if (hnt) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hnt }}\n </hlm-hint>\n }\n </hlm-form-field>\n ", isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: HlmFormField, selector: "hlm-form-field", inputs: ["class"] }, { kind: "directive", type: HlmError, selector: "hlm-error", inputs: ["class"] }, { kind: "directive", type: HlmHint, selector: "hlm-hint", inputs: ["class"] }, { kind: "directive", type: HlmLabel, selector: "[hlmLabel]", inputs: ["class"] }, { kind: "component", type: HlmDateRangePicker, selector: "hlm-date-range-picker", inputs: ["captionLayout", "min", "max", "disabled", "date", "autoCloseOnEndSelection", "formatDates", "transformDates"], outputs: ["dateChange"] }, { kind: "component", type: HlmDateRangeInput, selector: "hlm-date-range-input", inputs: ["name", "readonly", "ariaLabel", "ariaDescribedby", "class", "inputClass", "parseDate", "formatInputDates"] }] });
135
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgFormDateRangePicker, isStandalone: true, selector: "eg-form-date-range-picker", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, controlId: { classPropertyName: "controlId", publicName: "controlId", isSignal: true, isRequired: false, transformFunction: null }, controlName: { classPropertyName: "controlName", publicName: "controlName", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, autoError: { classPropertyName: "autoError", publicName: "autoError", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", 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 }, autoCloseOnEndSelection: { classPropertyName: "autoCloseOnEndSelection", publicName: "autoCloseOnEndSelection", isSignal: true, isRequired: false, transformFunction: null }, captionLayout: { classPropertyName: "captionLayout", publicName: "captionLayout", isSignal: true, isRequired: false, transformFunction: null }, formatDates: { classPropertyName: "formatDates", publicName: "formatDates", isSignal: true, isRequired: false, transformFunction: null }, transformDates: { classPropertyName: "transformDates", publicName: "transformDates", isSignal: true, isRequired: false, transformFunction: null }, parseDate: { classPropertyName: "parseDate", publicName: "parseDate", isSignal: true, isRequired: false, transformFunction: null }, formatInputDates: { classPropertyName: "formatInputDates", publicName: "formatInputDates", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, labelClass: { classPropertyName: "labelClass", publicName: "labelClass", isSignal: true, isRequired: false, transformFunction: null }, pickerClass: { classPropertyName: "pickerClass", publicName: "pickerClass", isSignal: true, isRequired: false, transformFunction: null }, inputClass: { classPropertyName: "inputClass", publicName: "inputClass", isSignal: true, isRequired: false, transformFunction: null }, errorClass: { classPropertyName: "errorClass", publicName: "errorClass", isSignal: true, isRequired: false, transformFunction: null }, hintClass: { classPropertyName: "hintClass", publicName: "hintClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "$userClass()" } }, providers: [{ provide: ControlContainer, useExisting: FormGroupDirective }], ngImport: i0, template: "\n @let lbl = label();\n @let cnm = controlName();\n @let hnt = hint();\n @let rqrd = required();\n\n <hlm-form-field>\n @if (lbl) {\n <label hlmLabel [for]=\"effectiveId()\" [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <hlm-date-range-picker\n [min]=\"$min()\"\n [max]=\"$max()\"\n [disabled]=\"disabled()\"\n [autoCloseOnEndSelection]=\"autoCloseOnEndSelection()\"\n [captionLayout]=\"captionLayout()\"\n [formatDates]=\"formatDates()\"\n [transformDates]=\"transformDates()\"\n [formControlName]=\"cnm\"\n [class]=\"$pickerClass()\"\n >\n <hlm-date-range-input\n [inputId]=\"effectiveId()\"\n [name]=\"cnm || name()\"\n [readonly]=\"readonly()\"\n [ariaLabel]=\"lbl\"\n [ariaDescribedby]=\"describedBy()\"\n [placeholder]=\"placeholder()\"\n [parseDate]=\"parseDate()\"\n [formatInputDates]=\"formatInputDates()\"\n [inputClass]=\"$inputClass()\"\n />\n </hlm-date-range-picker>\n\n @if (showError()) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ resolvedError() }}\n </hlm-error>\n }\n\n @if (hnt) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hnt }}\n </hlm-hint>\n }\n </hlm-form-field>\n ", isInline: true, dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: HlmFormField, selector: "hlm-form-field", inputs: ["class"] }, { kind: "directive", type: HlmError, selector: "hlm-error", inputs: ["class"] }, { kind: "directive", type: HlmHint, selector: "hlm-hint", inputs: ["class"] }, { kind: "directive", type: HlmLabel, selector: "[hlmLabel]", inputs: ["class"] }, { kind: "component", type: HlmDateRangePicker, selector: "hlm-date-range-picker", inputs: ["captionLayout", "min", "max", "disabled", "date", "autoCloseOnEndSelection", "formatDates", "transformDates"], outputs: ["dateChange"] }, { kind: "component", type: HlmDateRangeInput, selector: "hlm-date-range-input", inputs: ["name", "readonly", "ariaLabel", "ariaDescribedby", "class", "inputClass", "parseDate", "formatInputDates"] }] });
106
136
  }
107
137
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgFormDateRangePicker, decorators: [{
108
138
  type: Component,
@@ -114,9 +144,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
114
144
  },
115
145
  imports: [ReactiveFormsModule, HlmFormField, HlmError, HlmHint, HlmLabel, HlmDateRangePicker, HlmDateRangeInput],
116
146
  providers: [{ provide: ControlContainer, useExisting: FormGroupDirective }],
117
- template: "\n @let lbl = label();\n @let cnm = controlName();\n @let err = error();\n @let hnt = hint();\n @let rqrd = required();\n\n <hlm-form-field>\n @if (lbl) {\n <label hlmLabel [for]=\"effectiveId()\" [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <hlm-date-range-picker\n [min]=\"$min()\"\n [max]=\"$max()\"\n [disabled]=\"disabled()\"\n [autoCloseOnEndSelection]=\"autoCloseOnEndSelection()\"\n [captionLayout]=\"captionLayout()\"\n [formatDates]=\"formatDates()\"\n [transformDates]=\"transformDates()\"\n [formControlName]=\"cnm\"\n [class]=\"$pickerClass()\"\n >\n <hlm-date-range-input\n [inputId]=\"effectiveId()\"\n [name]=\"cnm || name()\"\n [readonly]=\"readonly()\"\n [ariaLabel]=\"lbl\"\n [ariaDescribedby]=\"describedBy()\"\n [placeholder]=\"placeholder()\"\n [parseDate]=\"parseDate()\"\n [formatInputDates]=\"formatInputDates()\"\n [inputClass]=\"$inputClass()\"\n />\n </hlm-date-range-picker>\n\n @if (err) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ err }}\n </hlm-error>\n }\n\n @if (hnt) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hnt }}\n </hlm-hint>\n }\n </hlm-form-field>\n ",
147
+ template: "\n @let lbl = label();\n @let cnm = controlName();\n @let hnt = hint();\n @let rqrd = required();\n\n <hlm-form-field>\n @if (lbl) {\n <label hlmLabel [for]=\"effectiveId()\" [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n }\n\n <hlm-date-range-picker\n [min]=\"$min()\"\n [max]=\"$max()\"\n [disabled]=\"disabled()\"\n [autoCloseOnEndSelection]=\"autoCloseOnEndSelection()\"\n [captionLayout]=\"captionLayout()\"\n [formatDates]=\"formatDates()\"\n [transformDates]=\"transformDates()\"\n [formControlName]=\"cnm\"\n [class]=\"$pickerClass()\"\n >\n <hlm-date-range-input\n [inputId]=\"effectiveId()\"\n [name]=\"cnm || name()\"\n [readonly]=\"readonly()\"\n [ariaLabel]=\"lbl\"\n [ariaDescribedby]=\"describedBy()\"\n [placeholder]=\"placeholder()\"\n [parseDate]=\"parseDate()\"\n [formatInputDates]=\"formatInputDates()\"\n [inputClass]=\"$inputClass()\"\n />\n </hlm-date-range-picker>\n\n @if (showError()) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ resolvedError() }}\n </hlm-error>\n }\n\n @if (hnt) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hnt }}\n </hlm-hint>\n }\n </hlm-form-field>\n ",
118
148
  }]
119
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], controlId: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlId", required: false }] }], controlName: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlName", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], autoCloseOnEndSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoCloseOnEndSelection", required: false }] }], captionLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionLayout", required: false }] }], formatDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatDates", required: false }] }], transformDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "transformDates", required: false }] }], parseDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "parseDate", required: false }] }], formatInputDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatInputDates", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], labelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelClass", required: false }] }], pickerClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "pickerClass", required: false }] }], inputClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputClass", required: false }] }], errorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorClass", required: false }] }], hintClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintClass", required: false }] }] } });
149
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], controlId: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlId", required: false }] }], controlName: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlName", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], autoError: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoError", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], autoCloseOnEndSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoCloseOnEndSelection", required: false }] }], captionLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "captionLayout", required: false }] }], formatDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatDates", required: false }] }], transformDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "transformDates", required: false }] }], parseDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "parseDate", required: false }] }], formatInputDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatInputDates", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], labelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelClass", required: false }] }], pickerClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "pickerClass", required: false }] }], inputClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputClass", required: false }] }], errorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorClass", required: false }] }], hintClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintClass", required: false }] }] } });
120
150
 
121
151
  /*
122
152
  * Public API Surface of form-date-range-picker
@@ -4,12 +4,20 @@ import { ClassValue } from 'clsx';
4
4
  type DateRangeCaptionLayout = 'dropdown' | 'label' | 'dropdown-months' | 'dropdown-years';
5
5
  declare class EgFormDateRangePicker {
6
6
  private readonly formGroupDirective;
7
+ private readonly _errorMessages;
7
8
  private readonly generatedId;
8
9
  private readonly _config;
10
+ private readonly _shared;
9
11
  label: _angular_core.InputSignal<string | undefined>;
10
12
  controlId: _angular_core.InputSignal<string | undefined>;
11
13
  controlName: _angular_core.InputSignal<string>;
12
14
  error: _angular_core.InputSignal<string | undefined>;
15
+ /**
16
+ * Auto-resolve the displayed message from the control's `ValidationErrors`
17
+ * when `error()` is unset. Explicit `error()` always wins. Set to `false`
18
+ * for manual-only messages.
19
+ */
20
+ autoError: _angular_core.InputSignal<boolean>;
13
21
  hint: _angular_core.InputSignal<string | undefined>;
14
22
  id: _angular_core.InputSignal<string | undefined>;
15
23
  name: _angular_core.InputSignal<string | undefined>;
@@ -28,6 +36,19 @@ declare class EgFormDateRangePicker {
28
36
  readonly effectiveId: _angular_core.Signal<string>;
29
37
  readonly errorId: _angular_core.Signal<string>;
30
38
  readonly hintId: _angular_core.Signal<string>;
39
+ /**
40
+ * Displayed message: explicit `error()` wins; otherwise (when `autoError()`)
41
+ * auto-resolved from the control's `ValidationErrors`. Plain method (not a
42
+ * computed): `ValidationErrors` is not a signal, so the control must be read
43
+ * fresh on every change-detection pass.
44
+ */
45
+ protected resolvedError(): string | undefined;
46
+ /**
47
+ * Whether the error is surfaced: message present + control invalid +
48
+ * touched/dirty/submitted. Gates the visual `<hlm-error>` (and
49
+ * `describedBy()`, where present) so sighted and screen-reader output agree.
50
+ */
51
+ protected showError(): boolean;
31
52
  /**
32
53
  * Bounds projected onto the picker's `Date` model. The cast keeps the
33
54
  * template's generic inference on `Date` while passing runtime values
@@ -49,7 +70,7 @@ declare class EgFormDateRangePicker {
49
70
  $errorClass: _angular_core.Signal<string>;
50
71
  $hintClass: _angular_core.Signal<string>;
51
72
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<EgFormDateRangePicker, never>;
52
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgFormDateRangePicker, "eg-form-date-range-picker", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "controlId": { "alias": "controlId"; "required": false; "isSignal": true; }; "controlName": { "alias": "controlName"; "required": false; "isSignal": true; }; "error": { "alias": "error"; "required": false; "isSignal": true; }; "hint": { "alias": "hint"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "min": { "alias": "min"; "required": false; "isSignal": true; }; "max": { "alias": "max"; "required": false; "isSignal": true; }; "autoCloseOnEndSelection": { "alias": "autoCloseOnEndSelection"; "required": false; "isSignal": true; }; "captionLayout": { "alias": "captionLayout"; "required": false; "isSignal": true; }; "formatDates": { "alias": "formatDates"; "required": false; "isSignal": true; }; "transformDates": { "alias": "transformDates"; "required": false; "isSignal": true; }; "parseDate": { "alias": "parseDate"; "required": false; "isSignal": true; }; "formatInputDates": { "alias": "formatInputDates"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "labelClass": { "alias": "labelClass"; "required": false; "isSignal": true; }; "pickerClass": { "alias": "pickerClass"; "required": false; "isSignal": true; }; "inputClass": { "alias": "inputClass"; "required": false; "isSignal": true; }; "errorClass": { "alias": "errorClass"; "required": false; "isSignal": true; }; "hintClass": { "alias": "hintClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
73
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgFormDateRangePicker, "eg-form-date-range-picker", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "controlId": { "alias": "controlId"; "required": false; "isSignal": true; }; "controlName": { "alias": "controlName"; "required": false; "isSignal": true; }; "error": { "alias": "error"; "required": false; "isSignal": true; }; "autoError": { "alias": "autoError"; "required": false; "isSignal": true; }; "hint": { "alias": "hint"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "min": { "alias": "min"; "required": false; "isSignal": true; }; "max": { "alias": "max"; "required": false; "isSignal": true; }; "autoCloseOnEndSelection": { "alias": "autoCloseOnEndSelection"; "required": false; "isSignal": true; }; "captionLayout": { "alias": "captionLayout"; "required": false; "isSignal": true; }; "formatDates": { "alias": "formatDates"; "required": false; "isSignal": true; }; "transformDates": { "alias": "transformDates"; "required": false; "isSignal": true; }; "parseDate": { "alias": "parseDate"; "required": false; "isSignal": true; }; "formatInputDates": { "alias": "formatInputDates"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "labelClass": { "alias": "labelClass"; "required": false; "isSignal": true; }; "pickerClass": { "alias": "pickerClass"; "required": false; "isSignal": true; }; "inputClass": { "alias": "inputClass"; "required": false; "isSignal": true; }; "errorClass": { "alias": "errorClass"; "required": false; "isSignal": true; }; "hintClass": { "alias": "hintClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
53
74
  }
54
75
 
55
76
  export { EgFormDateRangePicker };
@@ -338,6 +338,15 @@ export class CheckFieldComponent {
338
338
  }
339
339
  ```
340
340
 
341
+ ## Automatic error messages (`eg-form-*` wrappers)
342
+
343
+ The `eg-form-*` wrappers auto-resolve their displayed message from the control's `ValidationErrors` when no explicit `error` is set:
344
+
345
+ - Resolution order: explicit `error()` wins; otherwise (when `autoError()` is `true`, the default) the message is resolved from a custom dictionary entry, then the built-in defaults (`required`, `requiredTrue`, `email`, `minlength`, `maxlength`, `min`, `max`, `pattern`), then a generic "`label` is invalid" fallback. `error=""` is treated as unset.
346
+ - Set `autoError="false"` on a wrapper for manual-only messages.
347
+ - Global wording / custom validator keys (e.g. `usernameTaken`) and i18n are configured app-wide via `provideEgFormErrorMessages({...})` (merged over `DEFAULT_EG_FORM_ERROR_MESSAGES`, read with `injectEgFormErrorMessages()`); per-call overrides go through `resolveEgFormError(errors, label, messages)`.
348
+ - _When_ a message surfaces: wrappers render `<hlm-error>` (and point `aria-describedby` at it) only when the control is invalid + touched/dirty/submitted — otherwise the hint shows.
349
+
341
350
  ## Accessibility notes
342
351
 
343
352
  - The wrapper itself adds no label — always include a `<label hlmLabel>` (or `aria-label` on the control) so the field has an accessible name.
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, computed, Directive, contentChild, contentChildren, effect, ChangeDetectionStrategy, Component, inject, APP_ID, Injectable, NgModule } from '@angular/core';
2
+ import { input, computed, Directive, contentChild, contentChildren, inject, signal, effect, ChangeDetectionStrategy, Component, InjectionToken, APP_ID, Injectable, NgModule } from '@angular/core';
3
3
  import { hlm } from '@egose/shadcn-theme-ng/utils';
4
- import { FormGroupDirective, ControlContainer } from '@angular/forms';
4
+ import { FormGroupDirective, NgForm, ControlContainer } from '@angular/forms';
5
5
  import * as i1 from '@spartan-ng/brain/field';
6
6
  import { BrnFieldControl, BrnField } from '@spartan-ng/brain/field';
7
7
 
@@ -30,10 +30,26 @@ class HlmFormField {
30
30
  ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
31
31
  errorChildren = contentChildren(HlmError, /* @ts-ignore */
32
32
  ...(ngDevMode ? [{ debugName: "errorChildren" }] : /* istanbul ignore next */ []));
33
+ _formGroupDirective = inject(FormGroupDirective, { optional: true });
34
+ _ngForm = inject(NgForm, { optional: true });
35
+ /**
36
+ * Tracks parent submit state. `FormGroupDirective.submitted` / `NgForm.submitted`
37
+ * are plain booleans (not signals), so mirror them into a signal via `ngDoCheck`
38
+ * to keep `_hasDisplayedMessage` reactive under OnPush.
39
+ */
40
+ _submitted = signal(false, /* @ts-ignore */
41
+ ...(ngDevMode ? [{ debugName: "_submitted" }] : /* istanbul ignore next */ []));
42
+ ngDoCheck() {
43
+ const submitted = !!this._formGroupDirective?.submitted || !!this._ngForm?.submitted;
44
+ if (this._submitted() !== submitted)
45
+ this._submitted.set(submitted);
46
+ }
33
47
  _hasDisplayedMessage = computed(() => {
34
- const errors = this.control()?.errors();
48
+ const ctrl = this.control();
49
+ const errors = ctrl?.errors();
35
50
  const hasErrors = !!errors && Object.keys(errors).length > 0;
36
- return this.errorChildren() && this.errorChildren().length > 0 && hasErrors ? 'error' : 'hint';
51
+ const interacted = !!ctrl?.touched() || !!ctrl?.dirty() || this._submitted();
52
+ return this.errorChildren() && this.errorChildren().length > 0 && hasErrors && interacted ? 'error' : 'hint';
37
53
  }, /* @ts-ignore */
38
54
  ...(ngDevMode ? [{ debugName: "_hasDisplayedMessage" }] : /* istanbul ignore next */ []));
39
55
  constructor() {
@@ -99,6 +115,65 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
99
115
  }]
100
116
  }], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }] } });
101
117
 
118
+ /** Fallback label used when the field has no `label()` set. */
119
+ const EG_FORM_DEFAULT_ERROR_LABEL = 'This field';
120
+ /** Canonical priority order for resolving the first displayed message. */
121
+ const ERROR_KEY_PRIORITY = ['required', 'requiredTrue', 'email', 'minlength', 'maxlength', 'min', 'max', 'pattern'];
122
+ /**
123
+ * Default English messages for Angular's built-in validator error keys.
124
+ * App-wide wording/i18n is customized via {@link provideEgFormErrorMessages}.
125
+ */
126
+ const DEFAULT_EG_FORM_ERROR_MESSAGES = {
127
+ required: (_, label) => `${label} is required`,
128
+ requiredTrue: (_, label) => `${label} is required`,
129
+ email: () => 'Please enter a valid email address',
130
+ minlength: (params, label) => `${label} must be at least ${params?.['requiredLength']} characters`,
131
+ maxlength: (params, label) => `${label} must be at most ${params?.['requiredLength']} characters`,
132
+ min: (params, label) => `${label} must be at least ${params?.['min']}`,
133
+ max: (params, label) => `${label} must be at most ${params?.['max']}`,
134
+ pattern: (_, label) => `${label} is invalid`,
135
+ };
136
+ const EgFormErrorMessagesToken = new InjectionToken('EgFormErrorMessages');
137
+ /**
138
+ * Overrides/adds error messages app-wide (e.g. custom validator keys or i18n).
139
+ * Merged over {@link DEFAULT_EG_FORM_ERROR_MESSAGES} at injection time.
140
+ *
141
+ * ```ts
142
+ * provideEgFormErrorMessages({ usernameTaken: 'This username is already taken' })
143
+ * ```
144
+ */
145
+ function provideEgFormErrorMessages(messages) {
146
+ return { provide: EgFormErrorMessagesToken, useValue: { ...messages } };
147
+ }
148
+ /** Merged custom + default messages (defaults win only when not overridden). */
149
+ function injectEgFormErrorMessages() {
150
+ return { ...DEFAULT_EG_FORM_ERROR_MESSAGES, ...(inject(EgFormErrorMessagesToken, { optional: true }) ?? {}) };
151
+ }
152
+ /**
153
+ * Resolves the first displayable message for a `ValidationErrors` object.
154
+ * Returns `undefined` when there are no errors. Unknown keys fall back to a
155
+ * generic `${label} is invalid` message (or a custom dictionary entry).
156
+ */
157
+ function resolveEgFormError(errors, label, messages = DEFAULT_EG_FORM_ERROR_MESSAGES) {
158
+ if (!errors)
159
+ return undefined;
160
+ const keys = Object.keys(errors);
161
+ if (keys.length === 0)
162
+ return undefined;
163
+ const resolvedLabel = label || EG_FORM_DEFAULT_ERROR_LABEL;
164
+ const ordered = [
165
+ ...ERROR_KEY_PRIORITY.filter((key) => key in errors),
166
+ ...keys.filter((key) => !ERROR_KEY_PRIORITY.includes(key)),
167
+ ];
168
+ const key = ordered[0];
169
+ const entry = messages[key];
170
+ if (typeof entry === 'function')
171
+ return entry((errors[key] ?? {}), resolvedLabel);
172
+ if (typeof entry === 'string')
173
+ return entry;
174
+ return `${resolvedLabel} is invalid`;
175
+ }
176
+
102
177
  class HlmFormIdGenerator {
103
178
  appId = inject(APP_ID);
104
179
  nextId = 0;
@@ -113,6 +188,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
113
188
  args: [{ providedIn: 'root' }]
114
189
  }] });
115
190
 
191
+ const EgFormSharedConfigToken = new InjectionToken('EgFormSharedConfig');
192
+ /**
193
+ * Sets shared label / error / hint / control classes app-wide, e.g.:
194
+ *
195
+ * ```ts
196
+ * provideEgFormSharedConfig({ labelClass: 'tw:font-semibold', errorClass: 'tw:text-red-600' })
197
+ * provideEgFormSharedConfig({ inputClass: 'tw:border-dashed', selectClass: 'tw:rounded-none' })
198
+ * ```
199
+ */
200
+ function provideEgFormSharedConfig(config) {
201
+ return { provide: EgFormSharedConfigToken, useValue: { ...config } };
202
+ }
203
+ /** Merged shared config (empty object when not provided). */
204
+ function injectEgFormSharedConfig() {
205
+ return inject(EgFormSharedConfigToken, { optional: true }) ?? {};
206
+ }
207
+
116
208
  /*
117
209
  * Public API Surface of form-field
118
210
  */
@@ -134,5 +226,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
134
226
  * Generated bundle index. Do not edit.
135
227
  */
136
228
 
137
- export { HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint };
229
+ export { DEFAULT_EG_FORM_ERROR_MESSAGES, EG_FORM_DEFAULT_ERROR_LABEL, HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint, injectEgFormErrorMessages, injectEgFormSharedConfig, provideEgFormErrorMessages, provideEgFormSharedConfig, resolveEgFormError };
138
230
 
@@ -1,7 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
+ import { DoCheck, ValueProvider } from '@angular/core';
2
3
  import { ClassValue } from 'clsx';
3
4
  import * as i1 from '@spartan-ng/brain/field';
4
5
  import { BrnFieldControl } from '@spartan-ng/brain/field';
6
+ import { ValidationErrors } from '@angular/forms';
5
7
 
6
8
  declare class HlmError {
7
9
  readonly userClass: i0.InputSignal<ClassValue>;
@@ -10,11 +12,20 @@ declare class HlmError {
10
12
  static ɵdir: i0.ɵɵDirectiveDeclaration<HlmError, "hlm-error", never, { "userClass": { "alias": "class"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
11
13
  }
12
14
 
13
- declare class HlmFormField {
15
+ declare class HlmFormField implements DoCheck {
14
16
  readonly userClass: i0.InputSignal<ClassValue>;
15
17
  protected readonly _computedClass: i0.Signal<string>;
16
18
  readonly control: i0.Signal<BrnFieldControl | undefined>;
17
19
  readonly errorChildren: i0.Signal<readonly HlmError[]>;
20
+ private readonly _formGroupDirective;
21
+ private readonly _ngForm;
22
+ /**
23
+ * Tracks parent submit state. `FormGroupDirective.submitted` / `NgForm.submitted`
24
+ * are plain booleans (not signals), so mirror them into a signal via `ngDoCheck`
25
+ * to keep `_hasDisplayedMessage` reactive under OnPush.
26
+ */
27
+ protected readonly _submitted: i0.WritableSignal<boolean>;
28
+ ngDoCheck(): void;
18
29
  protected readonly _hasDisplayedMessage: i0.Signal<"error" | "hint">;
19
30
  constructor();
20
31
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmFormField, never>;
@@ -28,6 +39,42 @@ declare class HlmHint {
28
39
  static ɵdir: i0.ɵɵDirectiveDeclaration<HlmHint, "hlm-hint", never, { "userClass": { "alias": "class"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
29
40
  }
30
41
 
42
+ /**
43
+ * Factory for a single error message. Receives the validator's error params
44
+ * (e.g. `{ requiredLength, actualLength }` for `minlength`) and the field label.
45
+ */
46
+ type EgFormErrorMessageFn = (params: Record<string, unknown>, label: string) => string;
47
+ /**
48
+ * Dictionary of `ValidationErrors` key → message or message factory.
49
+ * Custom validator keys (e.g. `usernameTaken`) are covered by adding entries
50
+ * here, either globally via {@link provideEgFormErrorMessages} or per call.
51
+ */
52
+ type EgFormErrorMessages = Record<string, string | EgFormErrorMessageFn>;
53
+ /** Fallback label used when the field has no `label()` set. */
54
+ declare const EG_FORM_DEFAULT_ERROR_LABEL = "This field";
55
+ /**
56
+ * Default English messages for Angular's built-in validator error keys.
57
+ * App-wide wording/i18n is customized via {@link provideEgFormErrorMessages}.
58
+ */
59
+ declare const DEFAULT_EG_FORM_ERROR_MESSAGES: EgFormErrorMessages;
60
+ /**
61
+ * Overrides/adds error messages app-wide (e.g. custom validator keys or i18n).
62
+ * Merged over {@link DEFAULT_EG_FORM_ERROR_MESSAGES} at injection time.
63
+ *
64
+ * ```ts
65
+ * provideEgFormErrorMessages({ usernameTaken: 'This username is already taken' })
66
+ * ```
67
+ */
68
+ declare function provideEgFormErrorMessages(messages: EgFormErrorMessages): ValueProvider;
69
+ /** Merged custom + default messages (defaults win only when not overridden). */
70
+ declare function injectEgFormErrorMessages(): EgFormErrorMessages;
71
+ /**
72
+ * Resolves the first displayable message for a `ValidationErrors` object.
73
+ * Returns `undefined` when there are no errors. Unknown keys fall back to a
74
+ * generic `${label} is invalid` message (or a custom dictionary entry).
75
+ */
76
+ declare function resolveEgFormError(errors: ValidationErrors | null | undefined, label?: string | null, messages?: EgFormErrorMessages): string | undefined;
77
+
31
78
  declare class HlmFormIdGenerator {
32
79
  private readonly appId;
33
80
  private nextId;
@@ -36,6 +83,52 @@ declare class HlmFormIdGenerator {
36
83
  static ɵprov: i0.ɵɵInjectableDeclaration<HlmFormIdGenerator>;
37
84
  }
38
85
 
86
+ /**
87
+ * Global styling defaults applied to every `eg-form-*` wrapper that follows
88
+ * the shared label / control / error / hint skeleton.
89
+ *
90
+ * `labelClass` / `errorClass` / `hintClass` apply to every wrapper.
91
+ * Each control slot only applies to wrappers that declare the matching slot
92
+ * (e.g. `inputClass` → text-input, phone-input, autocomplete, date-pickers;
93
+ * `selectClass` → select, native-select; `pickerClass` → date-pickers).
94
+ *
95
+ * Each slot is merged under the matching per-component global config and the
96
+ * per-instance `*Class` input:
97
+ *
98
+ * library base < shared global < component global < per-instance
99
+ *
100
+ * `eg-form-toggle` picks up only the `labelClass` / `errorClass` / `hintClass`
101
+ * slots (it keeps its own CVA / exclusive error-hint behavior and local
102
+ * `toggleClass`).
103
+ */
104
+ interface EgFormSharedConfig {
105
+ labelClass?: string;
106
+ errorClass?: string;
107
+ hintClass?: string;
108
+ inputClass?: string;
109
+ textareaClass?: string;
110
+ selectClass?: string;
111
+ controlClass?: string;
112
+ chipsClass?: string;
113
+ checkboxClass?: string;
114
+ switchClass?: string;
115
+ sliderClass?: string;
116
+ groupClass?: string;
117
+ otpClass?: string;
118
+ pickerClass?: string;
119
+ }
120
+ /**
121
+ * Sets shared label / error / hint / control classes app-wide, e.g.:
122
+ *
123
+ * ```ts
124
+ * provideEgFormSharedConfig({ labelClass: 'tw:font-semibold', errorClass: 'tw:text-red-600' })
125
+ * provideEgFormSharedConfig({ inputClass: 'tw:border-dashed', selectClass: 'tw:rounded-none' })
126
+ * ```
127
+ */
128
+ declare function provideEgFormSharedConfig(config: Partial<EgFormSharedConfig>): ValueProvider;
129
+ /** Merged shared config (empty object when not provided). */
130
+ declare function injectEgFormSharedConfig(): EgFormSharedConfig;
131
+
39
132
  declare const HlmFormFieldImports: (typeof HlmError | typeof HlmFormField | typeof HlmHint)[];
40
133
  declare class HlmFormFieldModule {
41
134
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmFormFieldModule, never>;
@@ -43,4 +136,5 @@ declare class HlmFormFieldModule {
43
136
  static ɵinj: i0.ɵɵInjectorDeclaration<HlmFormFieldModule>;
44
137
  }
45
138
 
46
- export { HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint };
139
+ export { DEFAULT_EG_FORM_ERROR_MESSAGES, EG_FORM_DEFAULT_ERROR_LABEL, HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint, injectEgFormErrorMessages, injectEgFormSharedConfig, provideEgFormErrorMessages, provideEgFormSharedConfig, resolveEgFormError };
140
+ export type { EgFormErrorMessageFn, EgFormErrorMessages, EgFormSharedConfig };