@egose/shadcn-theme-ng-tw 0.2.0 → 0.4.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 (97) hide show
  1. package/README.md +1 -1
  2. package/alert/fesm2022/alert.mjs +1 -1
  3. package/autocomplete/fesm2022/autocomplete.mjs +1 -1
  4. package/button/fesm2022/button.mjs +3 -3
  5. package/checkbox/fesm2022/checkbox.mjs +6 -7
  6. package/checkbox/types/checkbox.d.ts +1 -1
  7. package/combobox/fesm2022/combobox.mjs +1 -1
  8. package/date-picker/README.md +15 -11
  9. package/date-picker/fesm2022/date-picker.mjs +148 -30
  10. package/date-picker/types/date-picker.d.ts +102 -9
  11. package/form-autocomplete/README.md +178 -0
  12. package/form-autocomplete/fesm2022/form-autocomplete.mjs +243 -0
  13. package/form-autocomplete/package.json +24 -0
  14. package/form-autocomplete/types/form-autocomplete.d.ts +81 -0
  15. package/form-checkbox/README.md +29 -18
  16. package/form-checkbox/fesm2022/form-checkbox.mjs +62 -22
  17. package/form-checkbox/types/form-checkbox.d.ts +43 -4
  18. package/form-combobox/README.md +203 -0
  19. package/form-combobox/fesm2022/form-combobox.mjs +305 -0
  20. package/form-combobox/package.json +24 -0
  21. package/form-combobox/types/form-combobox.d.ts +93 -0
  22. package/form-date-picker/README.md +49 -22
  23. package/form-date-picker/fesm2022/form-date-picker.mjs +100 -29
  24. package/form-date-picker/types/form-date-picker.d.ts +37 -1
  25. package/form-date-picker-multi/README.md +192 -0
  26. package/form-date-picker-multi/fesm2022/form-date-picker-multi.mjs +266 -0
  27. package/form-date-picker-multi/package.json +24 -0
  28. package/form-date-picker-multi/types/form-date-picker-multi.d.ts +78 -0
  29. package/form-date-range-picker/README.md +254 -0
  30. package/form-date-range-picker/fesm2022/form-date-range-picker.mjs +263 -0
  31. package/form-date-range-picker/package.json +24 -0
  32. package/form-date-range-picker/types/form-date-range-picker.d.ts +75 -0
  33. package/form-field/README.md +9 -0
  34. package/form-field/fesm2022/form-field.mjs +80 -5
  35. package/form-field/types/form-field.d.ts +50 -2
  36. package/form-field-simple/fesm2022/form-field-simple.mjs +15 -1
  37. package/form-field-simple/types/form-field-simple.d.ts +9 -2
  38. package/form-input-otp/README.md +195 -0
  39. package/form-input-otp/fesm2022/form-input-otp.mjs +226 -0
  40. package/form-input-otp/package.json +24 -0
  41. package/form-input-otp/types/form-input-otp.d.ts +78 -0
  42. package/form-month-year-picker/README.md +189 -0
  43. package/form-month-year-picker/fesm2022/form-month-year-picker.mjs +256 -0
  44. package/form-month-year-picker/package.json +24 -0
  45. package/form-month-year-picker/types/form-month-year-picker.d.ts +73 -0
  46. package/form-native-select/README.md +206 -0
  47. package/form-native-select/fesm2022/form-native-select.mjs +226 -0
  48. package/form-native-select/package.json +24 -0
  49. package/form-native-select/types/form-native-select.d.ts +77 -0
  50. package/form-phone-input/README.md +189 -0
  51. package/form-phone-input/fesm2022/form-phone-input.mjs +230 -0
  52. package/form-phone-input/package.json +24 -0
  53. package/form-phone-input/types/form-phone-input.d.ts +78 -0
  54. package/form-radio-group/README.md +199 -0
  55. package/form-radio-group/fesm2022/form-radio-group.mjs +238 -0
  56. package/form-radio-group/package.json +24 -0
  57. package/form-radio-group/types/form-radio-group.d.ts +83 -0
  58. package/form-searchable-multiselect/README.md +28 -17
  59. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +60 -24
  60. package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +39 -2
  61. package/form-select/README.md +30 -19
  62. package/form-select/fesm2022/form-select.mjs +65 -29
  63. package/form-select/types/form-select.d.ts +39 -2
  64. package/form-slider/README.md +183 -0
  65. package/form-slider/fesm2022/form-slider.mjs +218 -0
  66. package/form-slider/package.json +24 -0
  67. package/form-slider/types/form-slider.d.ts +80 -0
  68. package/form-switch/README.md +174 -0
  69. package/form-switch/fesm2022/form-switch.mjs +205 -0
  70. package/form-switch/package.json +24 -0
  71. package/form-switch/types/form-switch.d.ts +70 -0
  72. package/form-text-input/README.md +37 -26
  73. package/form-text-input/fesm2022/form-text-input.mjs +60 -24
  74. package/form-text-input/types/form-text-input.d.ts +39 -2
  75. package/form-textarea/README.md +33 -22
  76. package/form-textarea/fesm2022/form-textarea.mjs +60 -24
  77. package/form-textarea/types/form-textarea.d.ts +39 -2
  78. package/form-toggle/README.md +187 -0
  79. package/form-toggle/fesm2022/form-toggle.mjs +275 -0
  80. package/form-toggle/package.json +24 -0
  81. package/form-toggle/types/form-toggle.d.ts +100 -0
  82. package/form-toggle-group/README.md +177 -0
  83. package/form-toggle-group/fesm2022/form-toggle-group.mjs +243 -0
  84. package/form-toggle-group/package.json +24 -0
  85. package/form-toggle-group/types/form-toggle-group.d.ts +85 -0
  86. package/input-group/fesm2022/input-group.mjs +20 -10
  87. package/input-group/types/input-group.d.ts +4 -1
  88. package/native-select/fesm2022/native-select.mjs +18 -6
  89. package/native-select/types/native-select.d.ts +7 -2
  90. package/package.json +57 -1
  91. package/phone-input/README.md +114 -0
  92. package/phone-input/fesm2022/phone-input.mjs +191 -0
  93. package/phone-input/package.json +24 -0
  94. package/phone-input/types/phone-input.d.ts +67 -0
  95. package/sheet/fesm2022/sheet.mjs +1 -1
  96. package/switch/fesm2022/switch.mjs +7 -5
  97. package/switch/types/switch.d.ts +2 -1
@@ -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-tw/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;
@@ -134,5 +209,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
134
209
  * Generated bundle index. Do not edit.
135
210
  */
136
211
 
137
- export { HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint };
212
+ export { DEFAULT_EG_FORM_ERROR_MESSAGES, EG_FORM_DEFAULT_ERROR_LABEL, HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint, injectEgFormErrorMessages, provideEgFormErrorMessages, resolveEgFormError };
138
213
 
@@ -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;
@@ -43,4 +90,5 @@ declare class HlmFormFieldModule {
43
90
  static ɵinj: i0.ɵɵInjectorDeclaration<HlmFormFieldModule>;
44
91
  }
45
92
 
46
- export { HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint };
93
+ export { DEFAULT_EG_FORM_ERROR_MESSAGES, EG_FORM_DEFAULT_ERROR_LABEL, HlmError, HlmFormField, HlmFormFieldImports, HlmFormFieldModule, HlmFormIdGenerator, HlmHint, injectEgFormErrorMessages, provideEgFormErrorMessages, resolveEgFormError };
94
+ export type { EgFormErrorMessageFn, EgFormErrorMessages };
@@ -14,6 +14,13 @@ class EgFormField {
14
14
  form = this.formGroupDirective.form;
15
15
  statusSignal = signal(null, /* @ts-ignore */
16
16
  ...(ngDevMode ? [{ debugName: "statusSignal" }] : /* istanbul ignore next */ []));
17
+ /**
18
+ * Mirrors `FormGroupDirective.submitted` (plain boolean) into a signal so
19
+ * `hasError` stays reactive under OnPush and errors appear after submit
20
+ * even when the control was never touched/dirty.
21
+ */
22
+ submittedSignal = signal(false, /* @ts-ignore */
23
+ ...(ngDevMode ? [{ debugName: "submittedSignal" }] : /* istanbul ignore next */ []));
17
24
  sub = new Subscription();
18
25
  ngAfterContentInit() {
19
26
  const ctrlDir = this.control();
@@ -24,6 +31,11 @@ class EgFormField {
24
31
  this.statusSignal.set(ctrlDir.control.status);
25
32
  }
26
33
  }
34
+ ngDoCheck() {
35
+ const submitted = !!this.formGroupDirective?.submitted;
36
+ if (this.submittedSignal() !== submitted)
37
+ this.submittedSignal.set(submitted);
38
+ }
27
39
  ngOnDestroy() {
28
40
  this.sub.unsubscribe();
29
41
  }
@@ -32,7 +44,9 @@ class EgFormField {
32
44
  if (!ctrlDir)
33
45
  return false;
34
46
  this.statusSignal();
35
- return !!ctrlDir.control.errors && (ctrlDir.control.touched || ctrlDir.control.dirty);
47
+ this.submittedSignal();
48
+ return (!!ctrlDir.control.errors &&
49
+ (ctrlDir.control.touched || ctrlDir.control.dirty || !!this.formGroupDirective?.submitted));
36
50
  }, /* @ts-ignore */
37
51
  ...(ngDevMode ? [{ debugName: "hasError" }] : /* istanbul ignore next */ []));
38
52
  firstErrorKey = computed(() => {
@@ -1,17 +1,24 @@
1
1
  import * as _angular_core from '@angular/core';
2
- import { AfterContentInit, OnDestroy } from '@angular/core';
2
+ import { AfterContentInit, DoCheck, OnDestroy } from '@angular/core';
3
3
  import { FormControlName, FormGroup } from '@angular/forms';
4
4
  import { ClassValue } from 'clsx';
5
5
 
6
- declare class EgFormField implements AfterContentInit, OnDestroy {
6
+ declare class EgFormField implements AfterContentInit, DoCheck, OnDestroy {
7
7
  readonly userClass: _angular_core.InputSignal<ClassValue>;
8
8
  protected readonly _computedClass: _angular_core.Signal<string>;
9
9
  readonly control: _angular_core.Signal<FormControlName | undefined>;
10
10
  private readonly formGroupDirective;
11
11
  readonly form: FormGroup;
12
12
  private statusSignal;
13
+ /**
14
+ * Mirrors `FormGroupDirective.submitted` (plain boolean) into a signal so
15
+ * `hasError` stays reactive under OnPush and errors appear after submit
16
+ * even when the control was never touched/dirty.
17
+ */
18
+ private submittedSignal;
13
19
  private sub;
14
20
  ngAfterContentInit(): void;
21
+ ngDoCheck(): void;
15
22
  ngOnDestroy(): void;
16
23
  readonly hasError: _angular_core.Signal<boolean>;
17
24
  readonly firstErrorKey: _angular_core.Signal<string | null>;
@@ -0,0 +1,195 @@
1
+ # Form Input OTP (`@egose/shadcn-theme-ng/form-input-otp`)
2
+
3
+ A ready-made reactive-form one-time-code field: label + `brn-input-otp` slots + validation error/hint display in one tag. The model is a plain `string`.
4
+
5
+ The Angular implementation is a standalone wrapper: it renders `HlmFormField` / `HlmError` / `HlmHint` from `@egose/shadcn-theme-ng/form-field`, `HlmLabel`, and `BrnInputOtp` + `HlmInputOtp*` parts from `@egose/shadcn-theme-ng/input-otp` and `@spartan-ng/brain/input-otp`. The OTP input is bound with `[formControlName]="controlName()"` and renders one `hlm-input-otp-slot` per `length` unit in a single group. It must live inside a `FormGroupDirective` (`[formGroup]` parent); ids come from `HlmFormIdGenerator` unless overridden.
6
+
7
+ > **Ships as:** `@egose/shadcn-theme-ng/form-input-otp` and `@egose/shadcn-theme-ng-tw/form-input-otp`
8
+ > (the `tw:`-prefixed Tailwind variant). Both expose the identical TypeScript surface; only the
9
+ > emitted Tailwind class strings differ. See the [package README](../../README.md) for install
10
+ > steps, peer dependencies, and Tailwind setup. Do not publish this project directory independently.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ # Plain Tailwind (no prefix)
16
+ npm install @egose/shadcn-theme-ng
17
+
18
+ # tw:-prefixed Tailwind variant
19
+ npm install @egose/shadcn-theme-ng-tw
20
+ ```
21
+
22
+ Peer dependencies (Angular, `@spartan-ng/brain`, `rxjs`, …) are documented in the
23
+ [package README](../../README.md#peer-dependencies). This subpath additionally relies at runtime on
24
+ `@egose/shadcn-theme-ng/input-otp`, `@egose/shadcn-theme-ng/form-field`, and
25
+ `@egose/shadcn-theme-ng/label`.
26
+
27
+ ## Imports
28
+
29
+ Exported from the subpath root (`projects/form-input-otp/src/public-api.ts`):
30
+
31
+ ```ts
32
+ import { EgFormInputOtp, provideEgFormInputOtpConfig } from '@egose/shadcn-theme-ng/form-input-otp';
33
+ // tw variant: replace with '@egose/shadcn-theme-ng-tw/form-input-otp'
34
+ ```
35
+
36
+ Standalone usage:
37
+
38
+ ```ts
39
+ import { Component } from '@angular/core';
40
+ import { ReactiveFormsModule } from '@angular/forms';
41
+ import { EgFormInputOtp } from '@egose/shadcn-theme-ng/form-input-otp';
42
+
43
+ @Component({
44
+ selector: 'app-demo',
45
+ standalone: true,
46
+ imports: [ReactiveFormsModule, EgFormInputOtp],
47
+ template: `...`,
48
+ })
49
+ export class DemoComponent {}
50
+ ```
51
+
52
+ NgModule-based consumer: add `EgFormInputOtp` (and `ReactiveFormsModule`) to the module's
53
+ `imports` — it is a standalone component, not a module.
54
+
55
+ `ControlValueAccessor` behavior: `eg-form-input-otp` itself is not a `ControlValueAccessor`; the
56
+ inner `brn-input-otp` is (bound via `formControlName`), so `formControlName`/`formGroup` handling,
57
+ `Validators`, and `disabled` state from the control all flow through the parent form.
58
+
59
+ ## Anatomy / Structure
60
+
61
+ ```html
62
+ <form [formGroup]="form">
63
+ <eg-form-input-otp
64
+ controlName="code"
65
+ label="Verification code"
66
+ [length]="6"
67
+ [required]="true"
68
+ error="Enter the 6-digit code."
69
+ />
70
+ </form>
71
+ ```
72
+
73
+ Selector (from source): `eg-form-input-otp` (standalone component, host `tw:w-full`).
74
+
75
+ ## API reference
76
+
77
+ ### `EgFormInputOtp` — `eg-form-input-otp`
78
+
79
+ | Input | Type | Default | Description |
80
+ | ------------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
81
+ | `controlName` | `string` | `''` | `formControlName` key inside the parent `FormGroup`. **Required.** |
82
+ | `label` | `string \| undefined` | — | Label text (hidden when omitted). |
83
+ | `error` | `string \| undefined` | — | Error text shown when invalid. |
84
+ | `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`. |
85
+ | `hint` | `string \| undefined` | — | Hint text shown otherwise. |
86
+ | `controlId` | `string \| undefined` | — | Explicit id (first priority for `effectiveId`). |
87
+ | `id` | `string \| undefined` | — | Fallback id (second priority). |
88
+ | `length` | `number` | `6` | Slot count; one `hlm-input-otp-slot` is rendered per unit. |
89
+ | `disabled` | `boolean` | `false` | Locks interaction (form control stays enabled). |
90
+ | `required` | `boolean` | `false` | Shows a red `*` next to the label. |
91
+ | `class` | `ClassValue` | `''` | Extra host classes (base `tw:w-full`). |
92
+ | `labelClass` | `string` | `''` | Extra label classes (base `tw:mb-1`). |
93
+ | `otpClass` | `string` | `''` | Extra OTP container classes. |
94
+ | `errorClass` | `string` | `''` | Extra error classes (base `tw:mt-0`). |
95
+ | `hintClass` | `string` | `''` | Extra hint classes (base `tw:mt-0`). |
96
+
97
+ | Member | Description |
98
+ | -------------------- | -------------------------------------------------------------------------------- |
99
+ | `effectiveId` | `controlId() \|\| id() \|\| generated` — wired to OTP `inputId` and label `for`. |
100
+ | `errorId` / `hintId` | `effectiveId + '-error' / '-hint'`. |
101
+ | `slotIndexes` | `0..length-1` — drives slot rendering. |
102
+
103
+ Requires a `[formGroup]` ancestor (injects `FormGroupDirective`, provides `ControlContainer → FormGroupDirective`).
104
+
105
+ ## Examples
106
+
107
+ ### 1. Basic verification code
108
+
109
+ ```ts
110
+ import { Component, inject } from '@angular/core';
111
+ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
112
+ import { EgFormInputOtp } from '@egose/shadcn-theme-ng/form-input-otp';
113
+
114
+ @Component({
115
+ selector: 'app-verify',
116
+ standalone: true,
117
+ imports: [ReactiveFormsModule, EgFormInputOtp],
118
+ template: `
119
+ <form [formGroup]="form" (ngSubmit)="submit()">
120
+ <eg-form-input-otp
121
+ controlName="code"
122
+ label="Verification code"
123
+ [length]="6"
124
+ [required]="true"
125
+ error="Enter the 6-digit code."
126
+ />
127
+ <button type="submit" [disabled]="form.invalid">Verify</button>
128
+ </form>
129
+ `,
130
+ })
131
+ export class VerifyComponent {
132
+ private readonly fb = inject(FormBuilder);
133
+ readonly form = this.fb.group({ code: ['', [Validators.required, Validators.minLength(6)]] });
134
+ submit() {
135
+ console.log(this.form.value.code);
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### 2. Short PIN with hint
141
+
142
+ ```ts
143
+ import { Component, inject } from '@angular/core';
144
+ import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
145
+ import { EgFormInputOtp } from '@egose/shadcn-theme-ng/form-input-otp';
146
+
147
+ @Component({
148
+ selector: 'app-pin',
149
+ standalone: true,
150
+ imports: [ReactiveFormsModule, EgFormInputOtp],
151
+ template: `
152
+ <form [formGroup]="form">
153
+ <eg-form-input-otp
154
+ controlName="pin"
155
+ label="PIN"
156
+ [length]="4"
157
+ hint="Check your authenticator app."
158
+ error="PIN is required."
159
+ />
160
+ </form>
161
+ `,
162
+ })
163
+ export class PinComponent {
164
+ private readonly fb = inject(FormBuilder);
165
+ readonly form = this.fb.group({ pin: [''] });
166
+ }
167
+ ```
168
+
169
+ ## Accessibility notes
170
+
171
+ - Label `for` ↔ OTP `inputId` wiring is automatic via `effectiveId` — always pass a `label`.
172
+ - `BrnInputOtp` exposes no `aria-describedby`, so error/hint ids render without an input-level link.
173
+ - The required `*` is visual; pair with `Validators.required` (plus `minLength`) so assistive tech and validation agree.
174
+
175
+ ## Theming / CSS variables
176
+
177
+ No component-specific CSS variables. Style per-instance via `class` / `labelClass` / `otpClass` / `errorClass` / `hintClass` (precedence: library base < global config < per-instance).
178
+
179
+ Global defaults per styling slot via the wrapper config:
180
+
181
+ ```ts
182
+ import { provideEgFormInputOtpConfig } from '@egose/shadcn-theme-ng/form-input-otp';
183
+
184
+ await bootstrapApplication(App, {
185
+ providers: [provideEgFormInputOtpConfig({ labelClass: 'tw:font-medium' })],
186
+ });
187
+ ```
188
+
189
+ The host keeps `tw:w-full`; constrain width with `class` or a wrapping container.
190
+
191
+ ## Related subpaths
192
+
193
+ - `@egose/shadcn-theme-ng/input-otp` — raw `brn-input-otp` + slot/group/separator parts for custom layouts.
194
+ - `@egose/shadcn-theme-ng/form-field` — `hlm-form-field` / `hlm-error` / `hlm-hint` used internally.
195
+ - `@egose/shadcn-theme-ng/form-field-simple` — alternative minimal wrapper when you compose controls by hand.