@egose/shadcn-theme-ng 0.6.0 → 0.6.2

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 (50) hide show
  1. package/README.md +119 -1
  2. package/autocomplete/fesm2022/autocomplete.mjs +14 -4
  3. package/autocomplete/types/autocomplete.d.ts +4 -1
  4. package/checkbox/README.md +6 -2
  5. package/checkbox/fesm2022/checkbox.mjs +11 -7
  6. package/checkbox/types/checkbox.d.ts +6 -4
  7. package/combobox/fesm2022/combobox.mjs +13 -7
  8. package/combobox/types/combobox.d.ts +8 -2
  9. package/data-table/README.md +11 -1
  10. package/data-table/fesm2022/data-table.mjs +64 -18
  11. package/data-table/types/data-table.d.ts +25 -6
  12. package/date-picker/README.md +41 -9
  13. package/date-picker/fesm2022/date-picker.mjs +265 -59
  14. package/date-picker/types/date-picker.d.ts +58 -27
  15. package/form-autocomplete/README.md +4 -2
  16. package/form-autocomplete/fesm2022/form-autocomplete.mjs +19 -6
  17. package/form-autocomplete/types/form-autocomplete.d.ts +9 -3
  18. package/form-combobox/README.md +4 -2
  19. package/form-combobox/fesm2022/form-combobox.mjs +23 -6
  20. package/form-combobox/types/form-combobox.d.ts +10 -3
  21. package/form-input-otp/README.md +7 -5
  22. package/form-input-otp/fesm2022/form-input-otp.mjs +20 -8
  23. package/form-input-otp/types/form-input-otp.d.ts +9 -3
  24. package/form-searchable-multiselect/README.md +15 -1
  25. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +15 -3
  26. package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +9 -1
  27. package/form-slider/README.md +3 -1
  28. package/form-slider/fesm2022/form-slider.mjs +19 -6
  29. package/form-slider/types/form-slider.d.ts +9 -3
  30. package/form-toggle/README.md +22 -18
  31. package/form-toggle/fesm2022/form-toggle.mjs +22 -9
  32. package/form-toggle/types/form-toggle.d.ts +6 -2
  33. package/input-otp/README.md +33 -9
  34. package/input-otp/fesm2022/input-otp.mjs +127 -11
  35. package/input-otp/types/input-otp.d.ts +24 -4
  36. package/package.json +1 -1
  37. package/pagination/README.md +21 -12
  38. package/pagination/fesm2022/pagination.mjs +73 -65
  39. package/pagination/types/pagination.d.ts +21 -8
  40. package/phone-input/README.md +16 -0
  41. package/phone-input/fesm2022/phone-input.mjs +41 -3
  42. package/phone-input/types/phone-input.d.ts +5 -2
  43. package/searchable-multiselect/README.md +62 -40
  44. package/searchable-multiselect/fesm2022/searchable-multiselect.mjs +59 -32
  45. package/searchable-multiselect/types/searchable-multiselect.d.ts +23 -6
  46. package/slider/fesm2022/slider.mjs +9 -5
  47. package/slider/types/slider.d.ts +6 -2
  48. package/stepper/README.md +39 -19
  49. package/stepper/fesm2022/stepper.mjs +64 -12
  50. package/stepper/types/stepper.d.ts +12 -1
@@ -53,6 +53,8 @@ class EgFormToggle {
53
53
  ...(ngDevMode ? [{ debugName: "autoError" }] : /* istanbul ignore next */ []));
54
54
  hint = input(undefined, /* @ts-ignore */
55
55
  ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
56
+ /** Consumer-owned description IDs, merged with the displayed error or hint on the button. */
57
+ ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
56
58
  id = input(undefined, /* @ts-ignore */
57
59
  ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
58
60
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
@@ -78,12 +80,14 @@ class EgFormToggle {
78
80
  ...(ngDevMode ? [{ debugName: "_status" }] : /* istanbul ignore next */ []));
79
81
  _submitted = signal(false, /* @ts-ignore */
80
82
  ...(ngDevMode ? [{ debugName: "_submitted" }] : /* istanbul ignore next */ []));
83
+ _interacted = signal(false, /* @ts-ignore */
84
+ ...(ngDevMode ? [{ debugName: "_interacted" }] : /* istanbul ignore next */ []));
81
85
  /**
82
86
  * Displayed message: explicit `error()` wins; otherwise (when `autoError()`)
83
87
  * auto-resolved from the control's `ValidationErrors`. Plain method (not a
84
88
  * computed): `ValidationErrors` is not a signal, so the control must be read
85
89
  * fresh on every change-detection pass (`showError()` re-evaluates with it
86
- * via the `_status`/`_submitted` signals).
90
+ * via the `_status`/`_submitted`/`_interacted` signals).
87
91
  */
88
92
  resolvedError() {
89
93
  const explicit = this.error();
@@ -96,20 +100,26 @@ class EgFormToggle {
96
100
  showError = computed(() => {
97
101
  this._status();
98
102
  this._submitted();
103
+ this._interacted();
99
104
  const control = this._controlDir?.control;
100
- return (!!this.resolvedError() &&
101
- !!control?.errors &&
102
- (control.touched || control.dirty || !!this._formGroupDirective?.submitted));
105
+ return !!this.resolvedError() && !!control?.errors && (this._interacted() || this._submitted());
103
106
  }, /* @ts-ignore */
104
107
  ...(ngDevMode ? [{ debugName: "showError" }] : /* istanbul ignore next */ []));
105
108
  showHint = computed(() => !this.showError() && !!this.hint(), /* @ts-ignore */
106
109
  ...(ngDevMode ? [{ debugName: "showHint" }] : /* istanbul ignore next */ []));
110
+ describedBy() {
111
+ const ids = [this.ariaDescribedBy(), this.showError() ? this.errorId() : this.showHint() ? this.hintId() : null];
112
+ return [...new Set(ids.filter(Boolean).join(' ').split(/\s+/).filter(Boolean))].join(' ') || null;
113
+ }
107
114
  _onChange;
108
115
  _onTouched;
109
116
  ngDoCheck() {
110
117
  const submitted = !!this._formGroupDirective?.submitted;
111
118
  if (this._submitted() !== submitted)
112
119
  this._submitted.set(submitted);
120
+ // Keep silent interaction updates (emitEvent: false) visible on the next check too.
121
+ const control = this._controlDir?.control;
122
+ this._interacted.set(!!control && (control.touched || control.dirty));
113
123
  }
114
124
  ngAfterViewInit() {
115
125
  // Resolved here (not earlier): this component IS the value accessor for
@@ -119,8 +129,11 @@ class EgFormToggle {
119
129
  this._controlDir = this._injector.get(FormControlName, null, { self: true, optional: true }) ?? null;
120
130
  const control = this._controlDir?.control;
121
131
  this._status.set(control?.status ?? null);
122
- control?.statusChanges.pipe(takeUntilDestroyed(this._destroyRef)).subscribe((status) => {
123
- this._status.set(status);
132
+ this._interacted.set(!!control && (control.touched || control.dirty));
133
+ // Touch/dirty/reset events must invalidate feedback even when status stays INVALID.
134
+ control?.events.pipe(takeUntilDestroyed(this._destroyRef)).subscribe(() => {
135
+ this._status.set(control.status);
136
+ this._interacted.set(control.touched || control.dirty);
124
137
  });
125
138
  }
126
139
  _handleStateChange(state) {
@@ -167,7 +180,7 @@ class EgFormToggle {
167
180
  $hintClass = computed(() => hlm("mt-0", this._shared.hintClass, this._config.hintClass, this.hintClass()), /* @ts-ignore */
168
181
  ...(ngDevMode ? [{ debugName: "$hintClass" }] : /* istanbul ignore next */ []));
169
182
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgFormToggle, deps: [], target: i0.ɵɵFactoryTarget.Component });
170
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgFormToggle, isStandalone: true, selector: "eg-form-toggle", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, controlId: { classPropertyName: "controlId", publicName: "controlId", 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 }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, toggleClass: { classPropertyName: "toggleClass", publicName: "toggleClass", isSignal: true, isRequired: false, transformFunction: null }, labelClass: { classPropertyName: "labelClass", publicName: "labelClass", 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: [EG_FORM_TOGGLE_VALUE_ACCESSOR, provideIcons({ lucideCheck })], ngImport: i0, template: "\n @let lbl = label();\n @let rqrd = required();\n\n <div>\n <div class=\"flex items-center gap-1\">\n <button\n hlmToggle\n type=\"button\"\n [id]=\"effectiveId()\"\n [state]=\"_state() ? 'on' : 'off'\"\n (stateChange)=\"_handleStateChange($event)\"\n [disabled]=\"_disabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$toggleClass()\"\n >\n @if (_state()) {\n <ng-icon name=\"lucideCheck\" />\n }\n </button>\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 </div>\n\n @if (showError()) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ resolvedError() }}\n </hlm-error>\n } @else if (showHint()) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hint() }}\n </hlm-hint>\n }\n </div>\n ", isInline: true, dependencies: [{ kind: "directive", type: HlmToggle, selector: "button[hlmToggle]", inputs: ["variant", "size"] }, { 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: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
183
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgFormToggle, isStandalone: true, selector: "eg-form-toggle", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, controlId: { classPropertyName: "controlId", publicName: "controlId", 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 }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", 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 }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, toggleClass: { classPropertyName: "toggleClass", publicName: "toggleClass", isSignal: true, isRequired: false, transformFunction: null }, labelClass: { classPropertyName: "labelClass", publicName: "labelClass", 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: [EG_FORM_TOGGLE_VALUE_ACCESSOR, provideIcons({ lucideCheck })], ngImport: i0, template: "\n @let lbl = label();\n @let rqrd = required();\n\n <div>\n <div class=\"flex items-center gap-1\">\n <button\n hlmToggle\n type=\"button\"\n [id]=\"effectiveId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? 'true' : null\"\n [state]=\"_state() ? 'on' : 'off'\"\n (stateChange)=\"_handleStateChange($event)\"\n [disabled]=\"_disabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$toggleClass()\"\n >\n @if (_state()) {\n <ng-icon name=\"lucideCheck\" />\n }\n </button>\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 </div>\n\n @if (showError()) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ resolvedError() }}\n </hlm-error>\n } @else if (showHint()) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hint() }}\n </hlm-hint>\n }\n </div>\n ", isInline: true, dependencies: [{ kind: "directive", type: HlmToggle, selector: "button[hlmToggle]", inputs: ["variant", "size"] }, { 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: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
171
184
  }
172
185
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgFormToggle, decorators: [{
173
186
  type: Component,
@@ -180,9 +193,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
180
193
  changeDetection: ChangeDetectionStrategy.OnPush,
181
194
  imports: [HlmToggle, HlmError, HlmHint, HlmLabel, NgIcon],
182
195
  providers: [EG_FORM_TOGGLE_VALUE_ACCESSOR, provideIcons({ lucideCheck })],
183
- template: "\n @let lbl = label();\n @let rqrd = required();\n\n <div>\n <div class=\"flex items-center gap-1\">\n <button\n hlmToggle\n type=\"button\"\n [id]=\"effectiveId()\"\n [state]=\"_state() ? 'on' : 'off'\"\n (stateChange)=\"_handleStateChange($event)\"\n [disabled]=\"_disabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$toggleClass()\"\n >\n @if (_state()) {\n <ng-icon name=\"lucideCheck\" />\n }\n </button>\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 </div>\n\n @if (showError()) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ resolvedError() }}\n </hlm-error>\n } @else if (showHint()) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hint() }}\n </hlm-hint>\n }\n </div>\n ",
196
+ template: "\n @let lbl = label();\n @let rqrd = required();\n\n <div>\n <div class=\"flex items-center gap-1\">\n <button\n hlmToggle\n type=\"button\"\n [id]=\"effectiveId()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-invalid]=\"showError() ? 'true' : null\"\n [state]=\"_state() ? 'on' : 'off'\"\n (stateChange)=\"_handleStateChange($event)\"\n [disabled]=\"_disabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$toggleClass()\"\n >\n @if (_state()) {\n <ng-icon name=\"lucideCheck\" />\n }\n </button>\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 </div>\n\n @if (showError()) {\n <hlm-error [id]=\"errorId()\" [class]=\"$errorClass()\">\n {{ resolvedError() }}\n </hlm-error>\n } @else if (showHint()) {\n <hlm-hint [id]=\"hintId()\" [class]=\"$hintClass()\">\n {{ hint() }}\n </hlm-hint>\n }\n </div>\n ",
184
197
  }]
185
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], controlId: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlId", 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 }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], toggleClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleClass", required: false }] }], labelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelClass", required: false }] }], errorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorClass", required: false }] }], hintClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintClass", required: false }] }] } });
198
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], controlId: [{ type: i0.Input, args: [{ isSignal: true, alias: "controlId", 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 }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], toggleClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleClass", required: false }] }], labelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelClass", required: false }] }], errorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorClass", required: false }] }], hintClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintClass", required: false }] }] } });
186
199
 
187
200
  /*
188
201
  * Public API Surface of form-toggle
@@ -36,6 +36,8 @@ declare class EgFormToggle implements ControlValueAccessor, AfterViewInit, DoChe
36
36
  */
37
37
  autoError: _angular_core.InputSignal<boolean>;
38
38
  hint: _angular_core.InputSignal<string | undefined>;
39
+ /** Consumer-owned description IDs, merged with the displayed error or hint on the button. */
40
+ ariaDescribedBy: _angular_core.InputSignal<string | null>;
39
41
  id: _angular_core.InputSignal<string | undefined>;
40
42
  disabled: _angular_core.InputSignalWithTransform<boolean, BooleanInput>;
41
43
  required: _angular_core.InputSignal<boolean>;
@@ -49,16 +51,18 @@ declare class EgFormToggle implements ControlValueAccessor, AfterViewInit, DoChe
49
51
  protected readonly _disabled: _angular_core.Signal<boolean>;
50
52
  private readonly _status;
51
53
  private readonly _submitted;
54
+ private readonly _interacted;
52
55
  /**
53
56
  * Displayed message: explicit `error()` wins; otherwise (when `autoError()`)
54
57
  * auto-resolved from the control's `ValidationErrors`. Plain method (not a
55
58
  * computed): `ValidationErrors` is not a signal, so the control must be read
56
59
  * fresh on every change-detection pass (`showError()` re-evaluates with it
57
- * via the `_status`/`_submitted` signals).
60
+ * via the `_status`/`_submitted`/`_interacted` signals).
58
61
  */
59
62
  protected resolvedError(): string | undefined;
60
63
  protected readonly showError: _angular_core.Signal<boolean>;
61
64
  protected readonly showHint: _angular_core.Signal<boolean>;
65
+ protected describedBy(): string | null;
62
66
  protected _onChange?: ChangeFn<boolean>;
63
67
  protected _onTouched?: TouchFn;
64
68
  ngDoCheck(): void;
@@ -80,7 +84,7 @@ declare class EgFormToggle implements ControlValueAccessor, AfterViewInit, DoChe
80
84
  $errorClass: _angular_core.Signal<string>;
81
85
  $hintClass: _angular_core.Signal<string>;
82
86
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<EgFormToggle, never>;
83
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgFormToggle, "eg-form-toggle", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "controlId": { "alias": "controlId"; "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; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "toggleClass": { "alias": "toggleClass"; "required": false; "isSignal": true; }; "labelClass": { "alias": "labelClass"; "required": false; "isSignal": true; }; "errorClass": { "alias": "errorClass"; "required": false; "isSignal": true; }; "hintClass": { "alias": "hintClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
87
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgFormToggle, "eg-form-toggle", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "controlId": { "alias": "controlId"; "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; }; "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "toggleClass": { "alias": "toggleClass"; "required": false; "isSignal": true; }; "labelClass": { "alias": "labelClass"; "required": false; "isSignal": true; }; "errorClass": { "alias": "errorClass"; "required": false; "isSignal": true; }; "hintClass": { "alias": "hintClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
84
88
  }
85
89
 
86
90
  interface EgFormToggleConfig {
@@ -61,7 +61,7 @@ import { HlmInputOtpModule } from '@egose/shadcn-theme-ng/input-otp';
61
61
  export class MyModule {}
62
62
  ```
63
63
 
64
- Exported from `src/public-api.ts`: `HlmInputOtp`, `HlmInputOtpGroup`, `HlmInputOtpSeparator`, `HlmInputOtpSlot`, `HlmInputOtpFakeCaret`, plus `HlmInputOtpImports` and `HlmInputOtpModule`.
64
+ Exported from `src/public-api.ts`: `HlmInputOtp`, `HlmInputOtpControl`, `HlmInputOtpGroup`, `HlmInputOtpSeparator`, `HlmInputOtpSlot`, `HlmInputOtpFakeCaret`, plus `HlmInputOtpImports` and `HlmInputOtpModule`.
65
65
 
66
66
  ## Anatomy / Structure
67
67
 
@@ -88,6 +88,32 @@ Brain inputs on `brn-input-otp` (from `@spartan-ng/brain/input-otp`): `length` (
88
88
 
89
89
  ## API reference
90
90
 
91
+ ### `hlm-input-otp` — `HlmInputOtpControl`
92
+
93
+ Use this component for native-input accessibility forwarding. It extends `BrnInputOtp` with
94
+ a declarative input template and retains its inputs, outputs, CVA, paste and focus behavior.
95
+ `HlmInputOtpImports` / `HlmInputOtpModule` include it; a separate brain import is unnecessary.
96
+ Compose it with `HlmInputOtpSlot` (the raw brain slot's private injection token is not supported).
97
+
98
+ - `[aria-describedby]` (`string | null`, default `null`) reaches the native input and merges
99
+ with enclosing Spartan field description IDs, deduplicated. Bind only IDs of mounted elements.
100
+ - `required` (boolean-coerced, default `false`) exposes native `aria-required`. Add Angular
101
+ validators separately; native `aria-invalid` follows the bound form control.
102
+ - `inputId` connects a normal `<label for>` to the one focusable native input.
103
+
104
+ ```html
105
+ <label for="verification">Verification code</label>
106
+ <hlm-input-otp [length]="4" inputId="verification" aria-describedby="verification-help" required>
107
+ <div hlmInputOtpGroup>
108
+ <hlm-input-otp-slot index="0" />
109
+ <hlm-input-otp-slot index="1" />
110
+ <hlm-input-otp-slot index="2" />
111
+ <hlm-input-otp-slot index="3" />
112
+ </div>
113
+ </hlm-input-otp>
114
+ <p id="verification-help">Enter the four-character code.</p>
115
+ ```
116
+
91
117
  ### `brn-input-otp[hlmInputOtp]` — `HlmInputOtp`
92
118
 
93
119
  Pure styling directive (`data-slot="input-otp"`, flex row, dims on `has-disabled`). No inputs/outputs — bind `length`, `value`, `disabled`, `inputId`, … on the `brn-input-otp` element itself.
@@ -102,7 +128,7 @@ Slot cluster wrapper (`data-slot="input-otp-group"`). No inputs/outputs. Use one
102
128
  | ------- | -------------------------------------------------- | -------------------------------------------------------- |
103
129
  | `index` | `number` (**required**, `numberAttribute` coerced) | Zero-based position of the character this slot displays. |
104
130
 
105
- Renders `<brn-input-otp-slot [index]>` internally; active slot gets the `ring` highlight, invalid state the destructive border.
131
+ Under `brn-input-otp`, renders `<brn-input-otp-slot [index]>`; under `hlm-input-otp`, reads the adapter's public context and renders the same character/caret and state attributes declaratively. Active slots get the `ring` highlight.
106
132
 
107
133
  ### `hlm-input-otp-separator` — `HlmInputOtpSeparator`
108
134
 
@@ -267,17 +293,15 @@ export class LengthsExample {}
267
293
 
268
294
  ```ts
269
295
  import { Component, signal } from '@angular/core';
270
- import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
271
296
  import { HlmInputOtpImports } from '@egose/shadcn-theme-ng/input-otp';
272
297
  import { HlmError } from '@egose/shadcn-theme-ng/form-field';
273
298
 
274
299
  @Component({
275
300
  standalone: true,
276
- imports: [BrnInputOtp, HlmInputOtpImports, HlmError],
301
+ imports: [HlmInputOtpImports, HlmError],
277
302
  template: `
278
303
  <label for="otp-verify">Verification code</label>
279
- <brn-input-otp
280
- hlmInputOtp
304
+ <hlm-input-otp
281
305
  [length]="6"
282
306
  inputId="otp-verify"
283
307
  inputMode="numeric"
@@ -286,7 +310,7 @@ import { HlmError } from '@egose/shadcn-theme-ng/form-field';
286
310
  [value]="code()"
287
311
  (valueChange)="code.set($event); error.set(null)"
288
312
  (completed)="verify()"
289
- aria-describedby="otp-error"
313
+ [aria-describedby]="error() ? 'otp-error' : null"
290
314
  >
291
315
  <div hlmInputOtpGroup>
292
316
  <hlm-input-otp-slot index="0" />
@@ -299,7 +323,7 @@ import { HlmError } from '@egose/shadcn-theme-ng/form-field';
299
323
  <hlm-input-otp-slot index="4" />
300
324
  <hlm-input-otp-slot index="5" />
301
325
  </div>
302
- </brn-input-otp>
326
+ </hlm-input-otp>
303
327
  @if (error()) {
304
328
  <hlm-error id="otp-error">{{ error() }}</hlm-error>
305
329
  }
@@ -366,7 +390,7 @@ export class PasteExample {
366
390
 
367
391
  - Always render a visible `<label>` bound via `inputId` — the hidden native input is what AT focuses; without the label the purpose is unclear.
368
392
  - `inputMode="numeric"` (digits) vs `text` (alphanumeric) selects the right mobile keyboard; `inputAutocomplete="one-time-code"` lets browsers/OS autofill SMS codes.
369
- - Announce failures with a text error (`<hlm-error>` + `aria-describedby`), never by slot color alone.
393
+ - Associate failures with text (`<hlm-error>` + `hlm-input-otp`'s `[aria-describedby]`), never by slot color alone. The legacy brain component has no native description input. `HlmError` is not a live region; browser DOM/focus tests do not establish screen-reader announcements.
370
394
  - Keep `length` aligned with what the backend expects and validate `minLength(length)` on the form control so incomplete codes cannot submit.
371
395
 
372
396
  ## Theming / CSS variables
@@ -1,9 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Directive, ChangeDetectionStrategy, Component, input, numberAttribute, NgModule } from '@angular/core';
2
+ import { Directive, input, booleanAttribute, forwardRef, ChangeDetectionStrategy, Component, inject, computed, numberAttribute, NgModule } from '@angular/core';
3
3
  import { classes } from '@egose/shadcn-theme-ng/utils';
4
+ import { NG_VALUE_ACCESSOR } from '@angular/forms';
5
+ import * as i1 from '@spartan-ng/brain/field';
6
+ import { BrnFieldControlDescribedBy, provideBrnLabelable, BrnFieldControl } from '@spartan-ng/brain/field';
7
+ import { BrnInputOtp, BrnInputOtpSlot } from '@spartan-ng/brain/input-otp';
4
8
  import { NgIcon, provideIcons } from '@ng-icons/core';
5
9
  import { lucideMinus } from '@ng-icons/lucide';
6
- import { BrnInputOtpSlot } from '@spartan-ng/brain/input-otp';
7
10
 
8
11
  class HlmInputOtp {
9
12
  constructor() {
@@ -20,6 +23,83 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
20
23
  }]
21
24
  }], ctorParameters: () => [] });
22
25
 
26
+ /**
27
+ * BrnInputOtp's CVA and editing behavior with declarative native-input ARIA forwarding.
28
+ * Use with HlmInputOtpSlot; the legacy brn-input-otp skin remains available.
29
+ */
30
+ class HlmInputOtpControl extends BrnInputOtp {
31
+ /** Native input description IDs, merged with enclosing Spartan field descriptions. */
32
+ ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
33
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
34
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpControl, deps: null, target: i0.ɵɵFactoryTarget.Component });
35
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: HlmInputOtpControl, isStandalone: true, selector: "hlm-input-otp", inputs: { ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
36
+ { provide: BrnInputOtp, useExisting: forwardRef(() => HlmInputOtpControl) },
37
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => HlmInputOtpControl), multi: true },
38
+ provideBrnLabelable(HlmInputOtpControl),
39
+ ], usesInheritance: true, hostDirectives: [{ directive: i1.BrnFieldControl }, { directive: HlmInputOtp }], ngImport: i0, template: `
40
+ <ng-content />
41
+ <div [style]="containerStyles()">
42
+ <input
43
+ #otpInput
44
+ data-slot="input-otp"
45
+ [id]="inputId()"
46
+ [class]="inputClass()"
47
+ [autocomplete]="inputAutocomplete()"
48
+ [style]="inputStyles()"
49
+ [disabled]="_disabled()"
50
+ [inputMode]="inputMode()"
51
+ [value]="value() ?? ''"
52
+ brnFieldControlDescribedBy
53
+ [aria-describedby]="ariaDescribedBy()"
54
+ [attr.aria-required]="required() || null"
55
+ [attr.aria-invalid]="invalid?.() ? 'true' : null"
56
+ (input)="onInputChange($event)"
57
+ (paste)="onPaste($event)"
58
+ (focus)="_focused.set(true)"
59
+ (blur)="onBlur()"
60
+ />
61
+ </div>
62
+ `, isInline: true, dependencies: [{ kind: "directive", type: BrnFieldControlDescribedBy, selector: "[brnFieldControlDescribedBy]", inputs: ["aria-describedby"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
63
+ }
64
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpControl, decorators: [{
65
+ type: Component,
66
+ args: [{
67
+ selector: 'hlm-input-otp',
68
+ changeDetection: ChangeDetectionStrategy.OnPush,
69
+ imports: [BrnFieldControlDescribedBy],
70
+ hostDirectives: [BrnFieldControl, HlmInputOtp],
71
+ providers: [
72
+ { provide: BrnInputOtp, useExisting: forwardRef(() => HlmInputOtpControl) },
73
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => HlmInputOtpControl), multi: true },
74
+ provideBrnLabelable(HlmInputOtpControl),
75
+ ],
76
+ template: `
77
+ <ng-content />
78
+ <div [style]="containerStyles()">
79
+ <input
80
+ #otpInput
81
+ data-slot="input-otp"
82
+ [id]="inputId()"
83
+ [class]="inputClass()"
84
+ [autocomplete]="inputAutocomplete()"
85
+ [style]="inputStyles()"
86
+ [disabled]="_disabled()"
87
+ [inputMode]="inputMode()"
88
+ [value]="value() ?? ''"
89
+ brnFieldControlDescribedBy
90
+ [aria-describedby]="ariaDescribedBy()"
91
+ [attr.aria-required]="required() || null"
92
+ [attr.aria-invalid]="invalid?.() ? 'true' : null"
93
+ (input)="onInputChange($event)"
94
+ (paste)="onPaste($event)"
95
+ (focus)="_focused.set(true)"
96
+ (blur)="onBlur()"
97
+ />
98
+ </div>
99
+ `,
100
+ }]
101
+ }], propDecorators: { ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }] } });
102
+
23
103
  class HlmInputOtpFakeCaret {
24
104
  constructor() {
25
105
  classes(() => "pointer-events-none absolute inset-0 flex items-center justify-center");
@@ -74,16 +154,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
74
154
  }], ctorParameters: () => [] });
75
155
 
76
156
  class HlmInputOtpSlot {
157
+ _control = inject(HlmInputOtpControl, { optional: true });
158
+ _slot = computed(() => this._control?.context()[this.index()], /* @ts-ignore */
159
+ ...(ngDevMode ? [{ debugName: "_slot" }] : /* istanbul ignore next */ []));
77
160
  /** The index of the slot to render the char or a fake caret */
78
161
  index = input.required({ ...(ngDevMode ? { debugName: "index" } : /* istanbul ignore next */ {}), transform: numberAttribute });
79
162
  constructor() {
80
- classes(() => "dark:bg-input/30 border-input has-[brn-input-otp-slot[data-active=\"true\"]]:border-ring has-[brn-input-otp-slot[data-active=\"true\"]]:ring-ring/50 has-[brn-input-otp-slot[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:ring-destructive/20 dark:has-[brn-input-otp-slot[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:ring-destructive/40 data-[matches-spartan-invalid=true]:border-destructive has-[brn-input-otp-slot[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:border-destructive size-9 border-y border-e text-sm shadow-xs transition-all outline-none first:rounded-s-md first:border-s last:rounded-e-md has-[brn-input-otp-slot[data-active=\"true\"]]:ring-3 relative flex items-center justify-center has-[brn-input-otp-slot[data-active=\"true\"]]:z-10");
163
+ classes(() => "dark:bg-input/30 border-input has-[[data-active=\"true\"]]:border-ring has-[[data-active=\"true\"]]:ring-ring/50 has-[[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:ring-destructive/20 dark:has-[[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:ring-destructive/40 data-[matches-spartan-invalid=true]:border-destructive has-[[data-active=\"true\"]]:data-[matches-spartan-invalid=true]:border-destructive size-9 border-y border-e text-sm shadow-xs transition-all outline-none first:rounded-s-md first:border-s last:rounded-e-md has-[[data-active=\"true\"]]:ring-3 relative flex items-center justify-center has-[[data-active=\"true\"]]:z-10");
81
164
  }
82
165
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpSlot, deps: [], target: i0.ɵɵFactoryTarget.Component });
83
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: HlmInputOtpSlot, isStandalone: true, selector: "hlm-input-otp-slot", inputs: { index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "data-slot": "input-otp-slot" } }, ngImport: i0, template: `
84
- <brn-input-otp-slot [index]="index()">
85
- <hlm-input-otp-fake-caret />
86
- </brn-input-otp-slot>
166
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmInputOtpSlot, isStandalone: true, selector: "hlm-input-otp-slot", inputs: { index: { classPropertyName: "index", publicName: "index", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "data-slot": "input-otp-slot" } }, ngImport: i0, template: `
167
+ @if (_control) {
168
+ <span
169
+ [attr.data-active]="_slot()?.isActive"
170
+ [attr.data-invalid]="_control.invalid?.() ? 'true' : null"
171
+ [attr.data-touched]="_control.touched?.() ? 'true' : null"
172
+ [attr.data-dirty]="_control.dirty?.() ? 'true' : null"
173
+ [attr.data-matches-spartan-invalid]="_control.spartanInvalid?.() ? 'true' : null"
174
+ >
175
+ {{ _slot()?.char }}
176
+ @if (_slot()?.hasFakeCaret) {
177
+ <hlm-input-otp-fake-caret />
178
+ }
179
+ </span>
180
+ } @else {
181
+ <brn-input-otp-slot [index]="index()">
182
+ <hlm-input-otp-fake-caret />
183
+ </brn-input-otp-slot>
184
+ }
87
185
  `, isInline: true, dependencies: [{ kind: "component", type: BrnInputOtpSlot, selector: "brn-input-otp-slot", inputs: ["index", "forceInvalid"] }, { kind: "component", type: HlmInputOtpFakeCaret, selector: "hlm-input-otp-fake-caret" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
88
186
  }
89
187
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpSlot, decorators: [{
@@ -94,9 +192,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
94
192
  changeDetection: ChangeDetectionStrategy.OnPush,
95
193
  host: { 'data-slot': 'input-otp-slot' },
96
194
  template: `
97
- <brn-input-otp-slot [index]="index()">
98
- <hlm-input-otp-fake-caret />
99
- </brn-input-otp-slot>
195
+ @if (_control) {
196
+ <span
197
+ [attr.data-active]="_slot()?.isActive"
198
+ [attr.data-invalid]="_control.invalid?.() ? 'true' : null"
199
+ [attr.data-touched]="_control.touched?.() ? 'true' : null"
200
+ [attr.data-dirty]="_control.dirty?.() ? 'true' : null"
201
+ [attr.data-matches-spartan-invalid]="_control.spartanInvalid?.() ? 'true' : null"
202
+ >
203
+ {{ _slot()?.char }}
204
+ @if (_slot()?.hasFakeCaret) {
205
+ <hlm-input-otp-fake-caret />
206
+ }
207
+ </span>
208
+ } @else {
209
+ <brn-input-otp-slot [index]="index()">
210
+ <hlm-input-otp-fake-caret />
211
+ </brn-input-otp-slot>
212
+ }
100
213
  `,
101
214
  }]
102
215
  }], ctorParameters: () => [], propDecorators: { index: [{ type: i0.Input, args: [{ isSignal: true, alias: "index", required: true }] }] } });
@@ -106,6 +219,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
106
219
  */
107
220
  const HlmInputOtpImports = [
108
221
  HlmInputOtp,
222
+ HlmInputOtpControl,
109
223
  HlmInputOtpGroup,
110
224
  HlmInputOtpSeparator,
111
225
  HlmInputOtpSlot,
@@ -114,10 +228,12 @@ const HlmInputOtpImports = [
114
228
  class HlmInputOtpModule {
115
229
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
116
230
  static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.1.3", ngImport: i0, type: HlmInputOtpModule, imports: [HlmInputOtp,
231
+ HlmInputOtpControl,
117
232
  HlmInputOtpGroup,
118
233
  HlmInputOtpSeparator,
119
234
  HlmInputOtpSlot,
120
235
  HlmInputOtpFakeCaret], exports: [HlmInputOtp,
236
+ HlmInputOtpControl,
121
237
  HlmInputOtpGroup,
122
238
  HlmInputOtpSeparator,
123
239
  HlmInputOtpSlot,
@@ -137,5 +253,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
137
253
  * Generated bundle index. Do not edit.
138
254
  */
139
255
 
140
- export { HlmInputOtp, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
256
+ export { HlmInputOtp, HlmInputOtpControl, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
141
257
 
@@ -1,5 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { NumberInput } from '@angular/cdk/coercion';
2
+ import { BooleanInput, NumberInput } from '@angular/cdk/coercion';
3
+ import { BrnInputOtp } from '@spartan-ng/brain/input-otp';
4
+ import * as i1 from '@spartan-ng/brain/field';
3
5
 
4
6
  declare class HlmInputOtp {
5
7
  constructor();
@@ -7,6 +9,18 @@ declare class HlmInputOtp {
7
9
  static ɵdir: i0.ɵɵDirectiveDeclaration<HlmInputOtp, "brn-input-otp[hlmInputOtp], brn-input-otp[hlm]", never, {}, {}, never, never, true, never>;
8
10
  }
9
11
 
12
+ /**
13
+ * BrnInputOtp's CVA and editing behavior with declarative native-input ARIA forwarding.
14
+ * Use with HlmInputOtpSlot; the legacy brn-input-otp skin remains available.
15
+ */
16
+ declare class HlmInputOtpControl extends BrnInputOtp {
17
+ /** Native input description IDs, merged with enclosing Spartan field descriptions. */
18
+ readonly ariaDescribedBy: i0.InputSignal<string | null>;
19
+ readonly required: i0.InputSignalWithTransform<boolean, BooleanInput>;
20
+ static ɵfac: i0.ɵɵFactoryDeclaration<HlmInputOtpControl, never>;
21
+ static ɵcmp: i0.ɵɵComponentDeclaration<HlmInputOtpControl, "hlm-input-otp", never, { "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, [{ directive: typeof i1.BrnFieldControl; inputs: {}; outputs: {}; }, { directive: typeof HlmInputOtp; inputs: {}; outputs: {}; }]>;
22
+ }
23
+
10
24
  declare class HlmInputOtpFakeCaret {
11
25
  constructor();
12
26
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmInputOtpFakeCaret, never>;
@@ -26,6 +40,12 @@ declare class HlmInputOtpSeparator {
26
40
  }
27
41
 
28
42
  declare class HlmInputOtpSlot {
43
+ protected readonly _control: HlmInputOtpControl | null;
44
+ protected readonly _slot: i0.Signal<{
45
+ char: string | null;
46
+ isActive: boolean;
47
+ hasFakeCaret: boolean;
48
+ } | undefined>;
29
49
  /** The index of the slot to render the char or a fake caret */
30
50
  readonly index: i0.InputSignalWithTransform<number, NumberInput>;
31
51
  constructor();
@@ -33,11 +53,11 @@ declare class HlmInputOtpSlot {
33
53
  static ɵcmp: i0.ɵɵComponentDeclaration<HlmInputOtpSlot, "hlm-input-otp-slot", never, { "index": { "alias": "index"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
34
54
  }
35
55
 
36
- declare const HlmInputOtpImports: readonly [typeof HlmInputOtp, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret];
56
+ declare const HlmInputOtpImports: readonly [typeof HlmInputOtp, typeof HlmInputOtpControl, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret];
37
57
  declare class HlmInputOtpModule {
38
58
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmInputOtpModule, never>;
39
- static ɵmod: i0.ɵɵNgModuleDeclaration<HlmInputOtpModule, never, [typeof HlmInputOtp, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret], [typeof HlmInputOtp, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret]>;
59
+ static ɵmod: i0.ɵɵNgModuleDeclaration<HlmInputOtpModule, never, [typeof HlmInputOtp, typeof HlmInputOtpControl, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret], [typeof HlmInputOtp, typeof HlmInputOtpControl, typeof HlmInputOtpGroup, typeof HlmInputOtpSeparator, typeof HlmInputOtpSlot, typeof HlmInputOtpFakeCaret]>;
40
60
  static ɵinj: i0.ɵɵInjectorDeclaration<HlmInputOtpModule>;
41
61
  }
42
62
 
43
- export { HlmInputOtp, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
63
+ export { HlmInputOtp, HlmInputOtpControl, HlmInputOtpFakeCaret, HlmInputOtpGroup, HlmInputOtpImports, HlmInputOtpModule, HlmInputOtpSeparator, HlmInputOtpSlot };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@egose/shadcn-theme-ng",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Angular shadcn UI theme",
5
5
  "author": "Junmin Ahn",
6
6
  "bundles": [
@@ -144,12 +144,21 @@ For the `tw:` build, swap the specifier to `@egose/shadcn-theme-ng-tw/pagination
144
144
  | `currentPage` | **model (required)** | `number` | Two-way: `[(currentPage)]`. Clicks set it; out-of-range values are auto-corrected via `outOfBoundCorrection` |
145
145
  | `itemsPerPage` | **model (required)** | `number` | Two-way: `[(itemsPerPage)]`, bound to the page-size `hlm-select` |
146
146
  | `totalItems` | input (required) | `number` (number coercion) | Server or client total; `0`/negative collapses to a single empty page |
147
- | `maxSize` | input | `number`, default `7` (number coercion) | Max page links in the window (ellipsis logic from `ngx-pagination`) |
147
+ | `maxSize` | input | `number`, default `7` (number coercion) | Window entries including ellipses; floored and bounded to 1–100; invalid/non-positive uses 7 |
148
148
  | `showEdges` | input | `boolean`, default `true` (boolean coercion) | Shows prev/next edge buttons (hidden on first/last page respectively) |
149
149
  | `pageSizes` | input | `number[]`, default `[10, 20, 50, 100]` | Page-size menu; the current size is merged in + sorted if missing |
150
150
 
151
151
  Layout: `flex justify-between` row — left count summary (`<b>total</b> total items | <b>pages</b> pages`), center pager, right `hlm-select` page-size picker. Previous/next are plain click handlers (`goToPrevious/goToNext`); page links set `currentPage` directly without routing.
152
152
 
153
+ #### Normalization and numeric limits (both numbered pagers)
154
+
155
+ - Page count is `ceil(totalItems / itemsPerPage)`, at least 1 and at most `Number.MAX_SAFE_INTEGER`. Finite positive fractional totals/sizes retain that division behavior, including sizes below 1. Overflowing positive division saturates at the safe-integer page limit.
156
+ - Empty, negative or non-finite totals use one page. Non-positive or non-finite sizes (`0`, negative, `NaN`, `Infinity`) also use one page, **without rewriting `itemsPerPage`**. Supply a finite positive size to resume paging. The models are numeric; template number attributes such as `totalItems` and `maxSize` are coerced by Angular.
157
+ - Finite `currentPage` values are floored and clamped to `[1, lastPage]`; non-finite values become 1. Changes to totals, size or the external page are reconciled by an effect, emitting `currentPageChange` once per correction. Valid models emit nothing. Bind `[(currentPage)]` to receive corrections.
158
+ - All derived signals, including the page list, are side-effect-free. Rendering uses the corrected page immediately. Previous/next disappear at their bounds, and direct client-state navigation handlers are also clamped. Empty results show active page 1 and neither edge.
159
+ - `maxSize` / helper `paginationRange` is a window-entry budget, including ellipses. Finite positive values are floored and clamped to **1–100**; non-finite/non-positive values fall back to **7**. Ranges below 5 use a contiguous window containing the active page; larger ranges retain the first/last/ellipsis layout. Work and allocation are proportional to `min(pageCount, normalizedRange)`, never to an unbounded total or range. The array-returning helper API is unchanged.
160
+ - The summary displays the supplied total; normalization controls page count/navigation and does not repair the application's data. For asynchronous server loading, keep the last known total until the new count arrives if a temporary zero should not reset the page.
161
+
153
162
  ### `HlmNumberedPaginationQueryParams` (`hlm-numbered-pagination-query-params`)
154
163
 
155
164
  Same models/inputs as `HlmNumberedPagination` **plus**:
@@ -160,11 +169,13 @@ Same models/inputs as `HlmNumberedPagination` **plus**:
160
169
 
161
170
  Every link (prev/numbered/next) navigates via `RouterLink` with `[queryParams]="{ page }"` and `queryParamsHandling="merge"`. The active page renders with `link === undefined` (no navigation) + `isActive`.
162
171
 
172
+ The component **does not subscribe to the route or navigate on correction/size changes**. The parent reads `?page=` and supplies `currentPage`; link clicks navigate, then the parent's route subscription updates that model. Corrections emit `currentPageChange` but leave the URL unchanged. To canonicalize a corrected page in the URL, handle that output in the parent with `router.navigate([], { relativeTo: route, queryParams: { page }, queryParamsHandling: 'merge', replaceUrl: true })`. Keep unrelated parameters when doing so. Page-size URL persistence is also parent-owned.
173
+
163
174
  ### Helpers
164
175
 
165
176
  | Symbol | Signature | Notes |
166
177
  | ------------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
167
- | `outOfBoundCorrection(totalItems, itemsPerPage, currentPage)` | `(number, number, number) => number` | Clamps to `[1, totalPages]` (from `ngx-pagination`) |
178
+ | `outOfBoundCorrection(totalItems, itemsPerPage, currentPage)` | `(number, number, number) => number` | Pure floor/clamp using the normalization policy above; empty/invalid paging returns 1 |
168
179
  | `createPageArray(currentPage, itemsPerPage, totalItems, paginationRange)` | `(number, number, number, number) => (number \| '...')[]` | Window with `'...'` gaps; powers both numbered pagers |
169
180
  | `Page` | `type Page = number \| '...'` | Not exported from public-api (internal to the pager file) — redeclare locally if you type custom windows |
170
181
 
@@ -246,7 +257,8 @@ export class PagerClientComponent {
246
257
  ### 3. Router `?page=` pager (server-driven)
247
258
 
248
259
  ```ts
249
- import { Component, signal, effect } from '@angular/core';
260
+ import { Component, signal } from '@angular/core';
261
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
250
262
  import { ActivatedRoute } from '@angular/router';
251
263
  import { HlmPaginationImports } from '@egose/shadcn-theme-ng/pagination';
252
264
 
@@ -261,7 +273,7 @@ import { HlmPaginationImports } from '@egose/shadcn-theme-ng/pagination';
261
273
  [totalItems]="total()"
262
274
  link="."
263
275
  />
264
- <p class="tw:text-sm tw:text-muted-foreground">URL holds ?page={{ page }}.</p>
276
+ <p class="tw:text-sm tw:text-muted-foreground">Displayed page: {{ page() }}.</p>
265
277
  `,
266
278
  })
267
279
  export class PagerQueryComponent {
@@ -270,13 +282,10 @@ export class PagerQueryComponent {
270
282
  readonly total = signal(320);
271
283
 
272
284
  constructor(route: ActivatedRoute) {
273
- effect(() => {
274
- // keep ?page= as the source of truth on back/forward + deep links
275
- const sub = route.queryParamMap.subscribe((params) => {
276
- const p = Number(params.get('page') ?? 1);
277
- this.page.set(Number.isFinite(p) && p >= 1 ? Math.floor(p) : 1);
278
- });
279
- return () => sub.unsubscribe();
285
+ // Read deep links and back/forward changes; the pager corrects model bounds.
286
+ // Corrections here update the displayed page, not the URL.
287
+ route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((params) => {
288
+ this.page.set(Number(params.get('page') ?? 1));
280
289
  });
281
290
  }
282
291
  }
@@ -330,7 +339,7 @@ import { HlmPaginationImports, createPageArray } from '@egose/shadcn-theme-ng/pa
330
339
  template: `
331
340
  <nav hlmPagination>
332
341
  <ul hlmPaginationContent>
333
- @for (p of window(); track p) {
342
+ @for (p of window(); track $index) {
334
343
  <li hlmPaginationItem>
335
344
  @if (p === '...') {
336
345
  <hlm-pagination-ellipsis />