@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
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { InjectionToken, 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, HlmError, HlmHint } from '@egose/shadcn-theme-ng/form-field';
5
+ import { injectEgFormErrorMessages, HlmFormIdGenerator, injectEgFormSharedConfig, resolveEgFormError, HlmError, HlmHint } from '@egose/shadcn-theme-ng/form-field';
6
6
  import { EgFormField } from '@egose/shadcn-theme-ng/form-field-simple';
7
7
  import { HlmLabel } from '@egose/shadcn-theme-ng/label';
8
8
  import { HlmToggleGroup, HlmToggleGroupItem } from '@egose/shadcn-theme-ng/toggle-group';
@@ -18,8 +18,10 @@ function injectEgFormToggleGroupConfig() {
18
18
 
19
19
  class EgFormToggleGroup {
20
20
  formGroupDirective = inject(FormGroupDirective);
21
+ _errorMessages = injectEgFormErrorMessages();
21
22
  generatedId = inject(HlmFormIdGenerator).generate('eg-form-toggle-group');
22
23
  _config = injectEgFormToggleGroupConfig();
24
+ _shared = injectEgFormSharedConfig();
23
25
  label = input(undefined, /* @ts-ignore */
24
26
  ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
25
27
  controlId = input(undefined, /* @ts-ignore */
@@ -28,6 +30,13 @@ class EgFormToggleGroup {
28
30
  ...(ngDevMode ? [{ debugName: "controlName" }] : /* istanbul ignore next */ []));
29
31
  error = input(undefined, /* @ts-ignore */
30
32
  ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
33
+ /**
34
+ * Auto-resolve the displayed message from the control's `ValidationErrors`
35
+ * when `error()` is unset. Explicit `error()` always wins. Set to `false`
36
+ * for manual-only messages.
37
+ */
38
+ autoError = input(true, /* @ts-ignore */
39
+ ...(ngDevMode ? [{ debugName: "autoError" }] : /* istanbul ignore next */ []));
31
40
  hint = input(undefined, /* @ts-ignore */
32
41
  ...(ngDevMode ? [{ debugName: "hint" }] : /* istanbul ignore next */ []));
33
42
  id = input(undefined, /* @ts-ignore */
@@ -50,6 +59,32 @@ class EgFormToggleGroup {
50
59
  ...(ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []));
51
60
  hintId = computed(() => `${this.effectiveId()}-hint`, /* @ts-ignore */
52
61
  ...(ngDevMode ? [{ debugName: "hintId" }] : /* istanbul ignore next */ []));
62
+ /**
63
+ * Displayed message: explicit `error()` wins; otherwise (when `autoError()`)
64
+ * auto-resolved from the control's `ValidationErrors`. Plain method (not a
65
+ * computed): `ValidationErrors` is not a signal, so the control must be read
66
+ * fresh on every change-detection pass.
67
+ */
68
+ resolvedError() {
69
+ const explicit = this.error();
70
+ if (explicit)
71
+ return explicit;
72
+ if (!this.autoError())
73
+ return undefined;
74
+ const control = this.formGroupDirective.form.get(this.controlName());
75
+ return resolveEgFormError(control?.errors ?? null, this.label(), this._errorMessages);
76
+ }
77
+ /**
78
+ * Whether the error is surfaced: message present + control invalid +
79
+ * touched/dirty/submitted. Gates the visual `<hlm-error>` (and
80
+ * `describedBy()`, where present) so sighted and screen-reader output agree.
81
+ */
82
+ showError() {
83
+ const control = this.formGroupDirective.form.get(this.controlName());
84
+ return (!!this.resolvedError() &&
85
+ !!control?.invalid &&
86
+ (control.dirty || control.touched || this.formGroupDirective.submitted));
87
+ }
53
88
  /**
54
89
  * Combined lock for the brain `linkedSignal`-based disabled state: the raw
55
90
  * input write always carries the resolved value, so a same-pass
@@ -59,12 +94,7 @@ class EgFormToggleGroup {
59
94
  return this.disabled() || !!this.formGroupDirective.form.get(this.controlName())?.disabled;
60
95
  }
61
96
  describedBy() {
62
- const control = this.formGroupDirective.form.get(this.controlName());
63
- return this.error() && control?.invalid && (control.dirty || control.touched)
64
- ? this.errorId()
65
- : this.hint()
66
- ? this.hintId()
67
- : null;
97
+ return this.showError() ? this.errorId() : this.hint() ? this.hintId() : null;
68
98
  }
69
99
  // Styling
70
100
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
@@ -79,16 +109,16 @@ class EgFormToggleGroup {
79
109
  // Computed classes (library base < global config < per-instance)
80
110
  $userClass = computed(() => hlm("w-full", this.userClass()), /* @ts-ignore */
81
111
  ...(ngDevMode ? [{ debugName: "$userClass" }] : /* istanbul ignore next */ []));
82
- $labelClass = computed(() => hlm("mb-1", this._config.labelClass, this.labelClass()), /* @ts-ignore */
112
+ $labelClass = computed(() => hlm("mb-1", this._shared.labelClass, this._config.labelClass, this.labelClass()), /* @ts-ignore */
83
113
  ...(ngDevMode ? [{ debugName: "$labelClass" }] : /* istanbul ignore next */ []));
84
- $groupClass = computed(() => hlm(this._config.groupClass, this.groupClass()), /* @ts-ignore */
114
+ $groupClass = computed(() => hlm(this._shared.groupClass, this._config.groupClass, this.groupClass()), /* @ts-ignore */
85
115
  ...(ngDevMode ? [{ debugName: "$groupClass" }] : /* istanbul ignore next */ []));
86
- $errorClass = computed(() => hlm("mt-0", this._config.errorClass, this.errorClass()), /* @ts-ignore */
116
+ $errorClass = computed(() => hlm("mt-0", this._shared.errorClass, this._config.errorClass, this.errorClass()), /* @ts-ignore */
87
117
  ...(ngDevMode ? [{ debugName: "$errorClass" }] : /* istanbul ignore next */ []));
88
- $hintClass = computed(() => hlm("mt-0", this._config.hintClass, this.hintClass()), /* @ts-ignore */
118
+ $hintClass = computed(() => hlm("mt-0", this._shared.hintClass, this._config.hintClass, this.hintClass()), /* @ts-ignore */
89
119
  ...(ngDevMode ? [{ debugName: "$hintClass" }] : /* istanbul ignore next */ []));
90
120
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgFormToggleGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
91
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgFormToggleGroup, isStandalone: true, selector: "eg-form-toggle-group", 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 }, type: { classPropertyName: "type", publicName: "type", 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 }, options: { classPropertyName: "options", publicName: "options", 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 }, groupClass: { classPropertyName: "groupClass", publicName: "groupClass", 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 <eg-form-field>\n @if (lbl) {\n <span hlmLabel [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n\n <hlm-toggle-group\n [formControlName]=\"cnm\"\n [type]=\"type()\"\n [disabled]=\"effectiveDisabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$groupClass()\"\n >\n @for (option of options(); track option.value) {\n <button\n hlmToggleGroupItem\n type=\"button\"\n [value]=\"option.value\"\n [disabled]=\"option.disabled ?? false\"\n [attr.aria-describedby]=\"describedBy()\"\n >\n {{ option.label }}\n </button>\n }\n </hlm-toggle-group>\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 </eg-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: "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: "directive", type: HlmToggleGroup, selector: "[hlmToggleGroup],hlm-toggle-group", inputs: ["variant", "size", "spacing", "orientation"] }, { kind: "directive", type: HlmToggleGroupItem, selector: "button[hlmToggleGroupItem]", inputs: ["variant", "size"] }, { kind: "component", type: EgFormField, selector: "eg-form-field", inputs: ["class"] }] });
121
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgFormToggleGroup, isStandalone: true, selector: "eg-form-toggle-group", 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 }, type: { classPropertyName: "type", publicName: "type", 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 }, options: { classPropertyName: "options", publicName: "options", 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 }, groupClass: { classPropertyName: "groupClass", publicName: "groupClass", 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 <eg-form-field>\n @if (lbl) {\n <span hlmLabel [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n\n <hlm-toggle-group\n [formControlName]=\"cnm\"\n [type]=\"type()\"\n [disabled]=\"effectiveDisabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$groupClass()\"\n >\n @for (option of options(); track option.value) {\n <button\n hlmToggleGroupItem\n type=\"button\"\n [value]=\"option.value\"\n [disabled]=\"option.disabled ?? false\"\n [attr.aria-describedby]=\"describedBy()\"\n >\n {{ option.label }}\n </button>\n }\n </hlm-toggle-group>\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 </eg-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: "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: "directive", type: HlmToggleGroup, selector: "[hlmToggleGroup],hlm-toggle-group", inputs: ["variant", "size", "spacing", "orientation"] }, { kind: "directive", type: HlmToggleGroupItem, selector: "button[hlmToggleGroupItem]", inputs: ["variant", "size"] }, { kind: "component", type: EgFormField, selector: "eg-form-field", inputs: ["class"] }] });
92
122
  }
93
123
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgFormToggleGroup, decorators: [{
94
124
  type: Component,
@@ -100,9 +130,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
100
130
  },
101
131
  imports: [ReactiveFormsModule, HlmError, HlmHint, HlmLabel, HlmToggleGroup, HlmToggleGroupItem, EgFormField],
102
132
  providers: [{ provide: ControlContainer, useExisting: FormGroupDirective }],
103
- template: "\n @let lbl = label();\n @let cnm = controlName();\n @let err = error();\n @let hnt = hint();\n @let rqrd = required();\n\n <eg-form-field>\n @if (lbl) {\n <span hlmLabel [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n\n <hlm-toggle-group\n [formControlName]=\"cnm\"\n [type]=\"type()\"\n [disabled]=\"effectiveDisabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$groupClass()\"\n >\n @for (option of options(); track option.value) {\n <button\n hlmToggleGroupItem\n type=\"button\"\n [value]=\"option.value\"\n [disabled]=\"option.disabled ?? false\"\n [attr.aria-describedby]=\"describedBy()\"\n >\n {{ option.label }}\n </button>\n }\n </hlm-toggle-group>\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 </eg-form-field>\n ",
133
+ template: "\n @let lbl = label();\n @let cnm = controlName();\n @let hnt = hint();\n @let rqrd = required();\n\n <eg-form-field>\n @if (lbl) {\n <span hlmLabel [class]=\"$labelClass()\"\n >{{ lbl }}\n @if (rqrd) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n\n <hlm-toggle-group\n [formControlName]=\"cnm\"\n [type]=\"type()\"\n [disabled]=\"effectiveDisabled()\"\n [variant]=\"variant()\"\n [size]=\"size()\"\n [class]=\"$groupClass()\"\n >\n @for (option of options(); track option.value) {\n <button\n hlmToggleGroupItem\n type=\"button\"\n [value]=\"option.value\"\n [disabled]=\"option.disabled ?? false\"\n [attr.aria-describedby]=\"describedBy()\"\n >\n {{ option.label }}\n </button>\n }\n </hlm-toggle-group>\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 </eg-form-field>\n ",
104
134
  }]
105
- }], 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 }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", 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 }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], labelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelClass", required: false }] }], groupClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupClass", required: false }] }], errorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorClass", required: false }] }], hintClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintClass", required: false }] }] } });
135
+ }], 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 }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", 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 }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], labelClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelClass", required: false }] }], groupClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupClass", required: false }] }], errorClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorClass", required: false }] }], hintClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "hintClass", required: false }] }] } });
106
136
 
107
137
  /*
108
138
  * Public API Surface of form-toggle-group
@@ -9,12 +9,20 @@ interface ToggleGroupOption {
9
9
  }
10
10
  declare class EgFormToggleGroup {
11
11
  private readonly formGroupDirective;
12
+ private readonly _errorMessages;
12
13
  private readonly generatedId;
13
14
  private readonly _config;
15
+ private readonly _shared;
14
16
  label: _angular_core.InputSignal<string | undefined>;
15
17
  controlId: _angular_core.InputSignal<string | undefined>;
16
18
  controlName: _angular_core.InputSignal<string>;
17
19
  error: _angular_core.InputSignal<string | undefined>;
20
+ /**
21
+ * Auto-resolve the displayed message from the control's `ValidationErrors`
22
+ * when `error()` is unset. Explicit `error()` always wins. Set to `false`
23
+ * for manual-only messages.
24
+ */
25
+ autoError: _angular_core.InputSignal<boolean>;
18
26
  hint: _angular_core.InputSignal<string | undefined>;
19
27
  id: _angular_core.InputSignal<string | undefined>;
20
28
  type: _angular_core.InputSignal<"single" | "multiple">;
@@ -26,6 +34,19 @@ declare class EgFormToggleGroup {
26
34
  readonly effectiveId: _angular_core.Signal<string>;
27
35
  readonly errorId: _angular_core.Signal<string>;
28
36
  readonly hintId: _angular_core.Signal<string>;
37
+ /**
38
+ * Displayed message: explicit `error()` wins; otherwise (when `autoError()`)
39
+ * auto-resolved from the control's `ValidationErrors`. Plain method (not a
40
+ * computed): `ValidationErrors` is not a signal, so the control must be read
41
+ * fresh on every change-detection pass.
42
+ */
43
+ protected resolvedError(): string | undefined;
44
+ /**
45
+ * Whether the error is surfaced: message present + control invalid +
46
+ * touched/dirty/submitted. Gates the visual `<hlm-error>` (and
47
+ * `describedBy()`, where present) so sighted and screen-reader output agree.
48
+ */
49
+ protected showError(): boolean;
29
50
  /**
30
51
  * Combined lock for the brain `linkedSignal`-based disabled state: the raw
31
52
  * input write always carries the resolved value, so a same-pass
@@ -44,7 +65,7 @@ declare class EgFormToggleGroup {
44
65
  $errorClass: _angular_core.Signal<string>;
45
66
  $hintClass: _angular_core.Signal<string>;
46
67
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<EgFormToggleGroup, never>;
47
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgFormToggleGroup, "eg-form-toggle-group", 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; }; "type": { "alias": "type"; "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; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "labelClass": { "alias": "labelClass"; "required": false; "isSignal": true; }; "groupClass": { "alias": "groupClass"; "required": false; "isSignal": true; }; "errorClass": { "alias": "errorClass"; "required": false; "isSignal": true; }; "hintClass": { "alias": "hintClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
68
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgFormToggleGroup, "eg-form-toggle-group", 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; }; "type": { "alias": "type"; "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; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "labelClass": { "alias": "labelClass"; "required": false; "isSignal": true; }; "groupClass": { "alias": "groupClass"; "required": false; "isSignal": true; }; "errorClass": { "alias": "errorClass"; "required": false; "isSignal": true; }; "hintClass": { "alias": "hintClass"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
48
69
  }
49
70
 
50
71
  interface EgFormToggleGroupConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@egose/shadcn-theme-ng",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Angular shadcn UI theme",
5
5
  "author": "Junmin Ahn",
6
6
  "bundles": [
@@ -350,6 +350,10 @@
350
350
  "types": "./spinner/types/spinner.d.ts",
351
351
  "default": "./spinner/fesm2022/spinner.mjs"
352
352
  },
353
+ "./stepper": {
354
+ "types": "./stepper/types/stepper.d.ts",
355
+ "default": "./stepper/fesm2022/stepper.mjs"
356
+ },
353
357
  "./switch": {
354
358
  "types": "./switch/types/switch.d.ts",
355
359
  "default": "./switch/fesm2022/switch.mjs"
@@ -0,0 +1,353 @@
1
+ # Stepper (`@egose/shadcn-theme-ng/stepper`)
2
+
3
+ Multi-step workflow navigation, ported from the spartan stepper blocks. This subpath ships seven pieces — `HlmStepper` (root), `HlmStep` (step), `HlmStepHeader` (clickable header with indicator), `HlmStepLabel` (rich label template), `HlmStepContent` (lazy body), `HlmStepperNext` / `HlmStepperPrevious` (navigation buttons) — plus `provideHlmStepperConfig` / `injectHlmStepperConfig` for global defaults. All are thin shadcn-styled components over Angular CDK's `@angular/cdk/stepper` primitives (`CdkStepper`, `CdkStep`, `CdkStepHeader`, `CdkStepLabel`).
4
+
5
+ > **Ships as:** `@egose/shadcn-theme-ng/stepper` and `@egose/shadcn-theme-ng-tw/stepper` (the `tw:`-prefixed Tailwind variant). See the [package README](../../README.md) for install steps, peer dependencies, Tailwind setup, and testing/release guidance. Do not publish this project directory independently.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # Plain Tailwind (no prefix)
11
+ npm install @egose/shadcn-theme-ng
12
+
13
+ # Or the tw:-prefixed variant
14
+ npm install @egose/shadcn-theme-ng-tw
15
+ ```
16
+
17
+ `@angular/cdk` (stepper, portal) arrives via the package peer dependencies. See the [package README](../../README.md) for the full peer-dependency table.
18
+
19
+ ## Imports
20
+
21
+ All public symbols are re-exported from `projects/stepper/src/public-api.ts`:
22
+
23
+ ```ts
24
+ import {
25
+ HlmStep,
26
+ HlmStepContent,
27
+ HlmStepHeader,
28
+ HlmStepLabel,
29
+ HlmStepper,
30
+ HlmStepperImports,
31
+ HlmStepperModule,
32
+ HlmStepperNext,
33
+ HlmStepperPrevious,
34
+ injectHlmStepperConfig,
35
+ provideHlmStepperConfig,
36
+ } from '@egose/shadcn-theme-ng/stepper';
37
+ // tw variant: swap to '@egose/shadcn-theme-ng-tw/stepper'
38
+ ```
39
+
40
+ Standalone-component usage (preferred):
41
+
42
+ ```ts
43
+ import { Component } from '@angular/core';
44
+ import { HlmStepperImports } from '@egose/shadcn-theme-ng/stepper';
45
+
46
+ @Component({
47
+ selector: 'app-demo',
48
+ standalone: true,
49
+ imports: [...HlmStepperImports],
50
+ template: `<!-- stepper markup here -->`,
51
+ })
52
+ export class DemoComponent {}
53
+ ```
54
+
55
+ NgModule usage:
56
+
57
+ ```ts
58
+ import { NgModule } from '@angular/core';
59
+ import { HlmStepperModule } from '@egose/shadcn-theme-ng/stepper';
60
+
61
+ @NgModule({ imports: [HlmStepperModule] })
62
+ export class FeatureModule {}
63
+ ```
64
+
65
+ | Symbol | Kind | Description |
66
+ | ------------------------ | ------------- | ------------------------------------------------------------------------ |
67
+ | `HlmStepper` | Component | Root: `<hlm-stepper>` — extends `CdkStepper`. |
68
+ | `HlmStep` | Component | Step: `<hlm-step label="…">` — extends `CdkStep`, adds `icon` input. |
69
+ | `HlmStepHeader` | Component | Header: `<hlm-step-header>` — extends `CdkStepHeader`, renders indicator. |
70
+ | `HlmStepLabel` | Directive | Rich label: `<ng-template hlmStepLabel>` — extends `CdkStepLabel`. |
71
+ | `HlmStepContent` | Directive | Lazy body: `<ng-template hlmStepContent>` — attached on first selection. |
72
+ | `HlmStepperNext` | Directive | Next button: `button[hlmStepperNext]` — extends `CdkStepperNext`. |
73
+ | `HlmStepperPrevious` | Directive | Back button: `button[hlmStepperPrevious]` — extends `CdkStepperPrevious`. |
74
+ | `HlmStepperImports` | `const` array | All seven, spread into `imports: [...]`. |
75
+ | `HlmStepperModule` | NgModule | Imports + re-exports all seven. |
76
+ | `provideHlmStepperConfig`| Function | Global defaults for animations + indicator mode. |
77
+ | `injectHlmStepperConfig` | Function | Reads the (optionally provided) global config. |
78
+
79
+ ## Anatomy / Structure
80
+
81
+ ```html
82
+ <hlm-stepper>
83
+ <hlm-step label="Step One">
84
+ <p>Content 1</p>
85
+ <button hlmBtn hlmStepperNext>Next</button>
86
+ </hlm-step>
87
+
88
+ <hlm-step label="Step Two">
89
+ <p>Content 2</p>
90
+ <button hlmBtn variant="outline" hlmStepperPrevious>Back</button>
91
+ <button hlmBtn hlmStepperNext>Next</button>
92
+ </hlm-step>
93
+
94
+ <hlm-step label="Step Three">
95
+ <p>Content 3</p>
96
+ <button hlmBtn variant="outline" hlmStepperPrevious>Back</button>
97
+ <button hlmBtn>Finish</button>
98
+ </hlm-step>
99
+ </hlm-stepper>
100
+ ```
101
+
102
+ Rich label via template (takes precedence over the `label` string):
103
+
104
+ ```html
105
+ <hlm-step [stepControl]="form">
106
+ <ng-template hlmStepLabel>Security</ng-template>
107
+ <!-- … -->
108
+ </hlm-step>
109
+ ```
110
+
111
+ Lazy body (attached only when the step is first selected):
112
+
113
+ ```html
114
+ <hlm-step label="Analytics">
115
+ <ng-template hlmStepContent>
116
+ <p>Heavy content…</p>
117
+ </ng-template>
118
+ </hlm-step>
119
+ ```
120
+
121
+ ## API reference
122
+
123
+ ### `HlmStepper` — selector `hlm-stepper` (component, extends `CdkStepper`)
124
+
125
+ | Input | Type | Default | Description |
126
+ | -------------------- | ----------------------------- | ---------- | ------------------------------------------------------------------ |
127
+ | `orientation` | forwarded to `CdkStepper` | — | `'horizontal' \| 'vertical'`. |
128
+ | `linear` | forwarded to `CdkStepper` | — | Current step must be valid (via `stepControl`) before advancing. |
129
+ | `selectedIndex` | forwarded to `CdkStepper` | — | Controlled selected-step index. |
130
+ | `labelPosition` | `'end' \| 'bottom'` | `'end'` | Horizontal layout: label beside vs. below the indicator. |
131
+ | `headerPosition` | `'top' \| 'bottom'` | `'top'` | Horizontal layout: headers above vs. below the content panel. |
132
+ | `indicatorMode` | `'number' \| 'state' \| 'icon'` | from config (`'state'`) | Indicator rendering (see below). |
133
+ | `stepperAriaLabel` | `string \| null` | `'Progress'` | `aria-label` for the tablist (ignored when labelledby is set). |
134
+ | `stepperAriaLabelledby` | `string \| null` | `null` | `aria-labelledby` for the tablist. |
135
+ | `animationsEnabled` | `boolean` | from config (`true`) | Toggle step transitions per instance. |
136
+ | `animationDuration` | `number` (ms) | from config (`300`) | Transition duration per instance. |
137
+
138
+ `next()` is overridden to `markAllAsTouched()` + `updateValueAndValidity()` on the current `stepControl` first, so linear steppers surface validation errors before denying the transition.
139
+
140
+ ### `HlmStep` — selector `hlm-step` (component, extends `CdkStep`)
141
+
142
+ | Input | Type | Default | Description |
143
+ | ------------- | ---------------- | ------- | -------------------------------------------------------- |
144
+ | `label` | forwarded to `CdkStep` | — | Step title (string, or rich template via `hlmStepLabel`). |
145
+ | `icon` | `string \| null` | `null` | Icon name rendered when `indicatorMode="icon"`. |
146
+ | `stepControl` | forwarded to `CdkStep` | — | Form group driving `linear` validation. |
147
+ | `optional` | forwarded to `CdkStep` | — | Shows an "Optional" caption in the header. |
148
+ | `errorMessage`| forwarded to `CdkStep` | — | Header error caption when the step state is `error`. |
149
+ | `hasError` | forwarded to `CdkStep` | — | Forces the error state (pair with `STEPPER_GLOBAL_OPTIONS` `showError`). |
150
+
151
+ ### `HlmStepHeader` — selector `hlm-step-header` (component, extends `CdkStepHeader`)
152
+
153
+ Usually rendered by `HlmStepper` itself; use directly only for custom layouts.
154
+
155
+ | Input | Type | Default | Description |
156
+ | --------------- | ------------------------------------------- | ------- | -------------------------------------------- |
157
+ | `index` | `number` | `0` | Step index (drives the number indicator). |
158
+ | `state` | `StepState` | `'number'` | CDK step state (`number`, `edit`, `done`, `error`, …). |
159
+ | `label` | `HlmStepLabel \| string \| null` | `null` | Resolved label content. |
160
+ | `selected` / `reached` / `active` | `boolean` | `false` | Visual state flags. |
161
+ | `optional` / `disabled` | `boolean` | `false` | Optional caption / disabled styling. |
162
+ | `icon` | `string \| null` | `null` | Icon name for `indicatorMode="icon"`. |
163
+ | `indicatorMode` | `'number' \| 'state' \| 'icon'` | from config (`'state'`) | Indicator rendering. |
164
+ | `labelPosition` | `'end' \| 'bottom'` | `'end'` | Label beside vs. below the indicator. |
165
+ | `errorMessage` | `string` | `''` | Error caption shown when `state === 'error'`.|
166
+
167
+ Indicator modes: `number` always renders `1, 2, 3`; `state` (default) renders a check icon for selected/reached steps and an alert icon for errors; `icon` renders the per-step `icon` name (provide icons via `@ng-icons/core` `provideIcons` in your app).
168
+
169
+ ### `HlmStepperNext` / `HlmStepperPrevious` — `button[hlmStepperNext]` / `button[hlmStepperPrevious]`
170
+
171
+ No inputs/outputs. Thin extensions of `CdkStepperNext` / `CdkStepperPrevious` (sets `type` binding + `touch-action: manipulation`). Pair with `hlmBtn` for styling.
172
+
173
+ ### Global config
174
+
175
+ ```ts
176
+ import { provideHlmStepperConfig } from '@egose/shadcn-theme-ng/stepper';
177
+
178
+ @Component({
179
+ // ...
180
+ providers: [provideHlmStepperConfig({ animationEnabled: true, animationDuration: 300, defaultIndicatorMode: 'state' })],
181
+ })
182
+ export class FeatureComponent {}
183
+ ```
184
+
185
+ Per-instance `animationsEnabled`, `animationDuration`, and `indicatorMode` inputs override the global defaults.
186
+
187
+ ## Examples
188
+
189
+ ### 1. Basic horizontal stepper
190
+
191
+ ```ts
192
+ import { Component } from '@angular/core';
193
+ import { HlmButtonImports } from '@egose/shadcn-theme-ng/button';
194
+ import { HlmStepperImports } from '@egose/shadcn-theme-ng/stepper';
195
+
196
+ @Component({
197
+ selector: 'demo-basic',
198
+ standalone: true,
199
+ imports: [...HlmStepperImports, ...HlmButtonImports],
200
+ template: `
201
+ <hlm-stepper>
202
+ <hlm-step label="Step One">
203
+ <p>Content 1</p>
204
+ <button hlmBtn hlmStepperNext>Next</button>
205
+ </hlm-step>
206
+ <hlm-step label="Step Two">
207
+ <p>Content 2</p>
208
+ <button hlmBtn variant="outline" hlmStepperPrevious>Back</button>
209
+ <button hlmBtn hlmStepperNext>Next</button>
210
+ </hlm-step>
211
+ <hlm-step label="Step Three">
212
+ <p>Content 3</p>
213
+ <button hlmBtn variant="outline" hlmStepperPrevious>Back</button>
214
+ <button hlmBtn>Finish</button>
215
+ </hlm-step>
216
+ </hlm-stepper>
217
+ `,
218
+ })
219
+ export class DemoBasic {}
220
+ ```
221
+
222
+ ### 2. Vertical orientation
223
+
224
+ ```html
225
+ <hlm-stepper orientation="vertical">
226
+ <hlm-step label="Campaign"><!-- … --></hlm-step>
227
+ <hlm-step label="Audience"><!-- … --></hlm-step>
228
+ <hlm-step label="Review"><!-- … --></hlm-step>
229
+ </hlm-stepper>
230
+ ```
231
+
232
+ Use vertical when content is dense or horizontal space is limited.
233
+
234
+ ### 3. Responsive orientation via `BreakpointObserver`
235
+
236
+ ```ts
237
+ import { BreakpointObserver } from '@angular/cdk/layout';
238
+ import { Component, computed, inject } from '@angular/core';
239
+ import { toSignal } from '@angular/core/rxjs-interop';
240
+ import { map } from 'rxjs/operators';
241
+
242
+ @Component({ /* … */ })
243
+ export class DemoResponsive {
244
+ private readonly _breakpoints = inject(BreakpointObserver);
245
+ private readonly _isSmall = toSignal(
246
+ this._breakpoints.observe('(max-width: 767.98px)').pipe(map((s) => s.matches)),
247
+ { initialValue: false },
248
+ );
249
+ protected readonly orientation = computed(() => (this._isSmall() ? 'vertical' : 'horizontal'));
250
+ }
251
+ ```
252
+
253
+ ```html
254
+ <hlm-stepper [orientation]="orientation()"><!-- … --></hlm-stepper>
255
+ ```
256
+
257
+ ### 4. Linear stepper with validation
258
+
259
+ With `linear`, the current step must be valid before advancing — `HlmStepper.next()` marks the step form touched first:
260
+
261
+ ```ts
262
+ import { Component, inject } from '@angular/core';
263
+ import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
264
+
265
+ @Component({ /* …, imports: [ReactiveFormsModule, ...HlmStepperImports, ...HlmButtonImports] */ })
266
+ export class DemoLinear {
267
+ private readonly _fb = inject(FormBuilder);
268
+ protected readonly identity = this._fb.group({ name: ['', Validators.required] });
269
+ }
270
+ ```
271
+
272
+ ```html
273
+ <hlm-stepper [linear]="true">
274
+ <hlm-step [stepControl]="identity" label="Identity">
275
+ <form [formGroup]="identity">
276
+ <input hlmInput formControlName="name" placeholder="Required before continuing" />
277
+ <button hlmBtn hlmStepperNext>Next</button>
278
+ </form>
279
+ </hlm-step>
280
+ <hlm-step label="Review"><!-- … --></hlm-step>
281
+ </hlm-stepper>
282
+ ```
283
+
284
+ ### 5. Error states
285
+
286
+ Enable `showError` through `STEPPER_GLOBAL_OPTIONS` to surface a step's error message in its header:
287
+
288
+ ```ts
289
+ import { STEPPER_GLOBAL_OPTIONS } from '@angular/cdk/stepper';
290
+
291
+ @Component({
292
+ providers: [{ provide: STEPPER_GLOBAL_OPTIONS, useValue: { showError: true } }],
293
+ /* … */
294
+ })
295
+ export class DemoError {}
296
+ ```
297
+
298
+ ```html
299
+ <hlm-step
300
+ [stepControl]="contact"
301
+ [hasError]="contact.invalid && contact.touched"
302
+ errorMessage="Enter a valid work email before continuing."
303
+ label="Contact"
304
+ >
305
+ <!-- … -->
306
+ </hlm-step>
307
+ ```
308
+
309
+ ### 6. Layout controls
310
+
311
+ ```html
312
+ <hlm-stepper labelPosition="bottom" headerPosition="bottom"><!-- … --></hlm-stepper>
313
+ ```
314
+
315
+ `labelPosition` moves horizontal labels below the indicator; `headerPosition="bottom"` moves the header row below the content panel.
316
+
317
+ ### 7. Animation controls
318
+
319
+ ```html
320
+ <hlm-stepper [animationsEnabled]="true" [animationDuration]="300"><!-- … --></hlm-stepper>
321
+ ```
322
+
323
+ Or set global defaults with `provideHlmStepperConfig` and override per instance.
324
+
325
+ ### 8. Indicator modes
326
+
327
+ ```html
328
+ <hlm-stepper indicatorMode="number"><!-- always 1, 2, 3 --></hlm-stepper>
329
+ <hlm-stepper indicatorMode="state"><!-- default: check / alert icons --></hlm-stepper>
330
+ <hlm-stepper indicatorMode="icon">
331
+ <hlm-step label="Profile" icon="lucideUser"><!-- … --></hlm-step>
332
+ </hlm-stepper>
333
+ ```
334
+
335
+ Per-step `icon` names resolve through `@ng-icons/core` — register them with `provideIcons` in your app.
336
+
337
+ ## Accessibility notes
338
+
339
+ - Horizontal headers get `role="tab"` semantics inside a `role="tablist"` (labelled `Progress` by default — override with `stepperAriaLabel` / `stepperAriaLabelledby`); panels get `role="tabpanel"` wired via `aria-labelledby` / `aria-controls`.
340
+ - Vertical headers use `role="button"` with `aria-expanded` / `aria-current="step"`.
341
+ - Keyboard: `HlmStepper` inherits CDK arrow-key navigation (`_onKeydown`). Headers are focusable via roving `tabIndex`.
342
+ - Disabled steps (`linear` + not navigable) get `aria-disabled` and `data-disabled` styling — explain in the label why a step is unavailable.
343
+ - In `linear` mode, failed validation marks fields touched so screen readers announce the errors.
344
+
345
+ ## Theming / CSS variables
346
+
347
+ Headers follow `buttonVariants({ size: 'icon-sm' })` (`default` when selected/reached, `outline` otherwise, `destructive` on error) with `data-disabled` opacity handling. Connectors use `bg-primary` once reached, `bg-border` otherwise. Follows your shadcn theme automatically; extend via `class` (merged through `classes()` on headers) or the layout/indicator inputs.
348
+
349
+ ## Related subpaths
350
+
351
+ - `@egose/shadcn-theme-ng/button` — `buttonVariants` powers the step indicators; `hlmBtn` styles the nav buttons.
352
+ - `@egose/shadcn-theme-ng/input` / `@egose/shadcn-theme-ng/label` / `@egose/shadcn-theme-ng/field` — form controls inside linear steps.
353
+ - `@egose/shadcn-theme-ng/card` — rich content panels.