@egose/shadcn-theme-ng-tw 0.1.0 → 0.3.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 (165) hide show
  1. package/README.md +2 -2
  2. package/accordion/README.md +405 -2
  3. package/alert/README.md +372 -2
  4. package/alert/fesm2022/alert.mjs +1 -1
  5. package/alert-dialog/README.md +471 -5
  6. package/aspect-ratio/README.md +272 -5
  7. package/autocomplete/README.md +502 -2
  8. package/autocomplete/fesm2022/autocomplete.mjs +1 -1
  9. package/avatar/README.md +357 -5
  10. package/badge/README.md +318 -2
  11. package/basic-alert/README.md +353 -2
  12. package/breadcrumb/README.md +406 -5
  13. package/button/README.md +482 -2
  14. package/button/fesm2022/button.mjs +85 -107
  15. package/button/types/button.d.ts +5 -8
  16. package/button-group/README.md +318 -5
  17. package/button-group/fesm2022/button-group.mjs +1 -1
  18. package/calendar/README.md +357 -2
  19. package/card/README.md +331 -5
  20. package/carousel/README.md +333 -5
  21. package/carousel/fesm2022/carousel.mjs +4 -1
  22. package/checkbox/README.md +320 -2
  23. package/checkbox/fesm2022/checkbox.mjs +6 -7
  24. package/checkbox/types/checkbox.d.ts +1 -1
  25. package/collapsible/README.md +332 -5
  26. package/combobox/README.md +507 -5
  27. package/combobox/fesm2022/combobox.mjs +5 -2
  28. package/command/README.md +435 -5
  29. package/confirmation-dialog/README.md +301 -2
  30. package/context-menu/README.md +366 -5
  31. package/date-picker/README.md +469 -2
  32. package/date-picker/fesm2022/date-picker.mjs +150 -32
  33. package/date-picker/types/date-picker.d.ts +102 -9
  34. package/dialog/README.md +448 -2
  35. package/drawer/README.md +395 -5
  36. package/dropdown-menu/README.md +417 -5
  37. package/empty/README.md +329 -5
  38. package/field/README.md +385 -5
  39. package/form-autocomplete/README.md +177 -0
  40. package/form-autocomplete/fesm2022/form-autocomplete.mjs +211 -0
  41. package/form-autocomplete/package.json +24 -0
  42. package/form-autocomplete/types/form-autocomplete.d.ts +61 -0
  43. package/form-checkbox/README.md +322 -2
  44. package/form-checkbox/fesm2022/form-checkbox.mjs +22 -9
  45. package/form-checkbox/types/form-checkbox.d.ts +23 -4
  46. package/form-combobox/README.md +202 -0
  47. package/form-combobox/fesm2022/form-combobox.mjs +273 -0
  48. package/form-combobox/package.json +24 -0
  49. package/form-combobox/types/form-combobox.d.ts +73 -0
  50. package/form-date-picker/README.md +348 -2
  51. package/form-date-picker/fesm2022/form-date-picker.mjs +60 -16
  52. package/form-date-picker/types/form-date-picker.d.ts +17 -1
  53. package/form-date-picker-multi/README.md +191 -0
  54. package/form-date-picker-multi/fesm2022/form-date-picker-multi.mjs +239 -0
  55. package/form-date-picker-multi/package.json +24 -0
  56. package/form-date-picker-multi/types/form-date-picker-multi.d.ts +58 -0
  57. package/form-date-range-picker/README.md +253 -0
  58. package/form-date-range-picker/fesm2022/form-date-range-picker.mjs +236 -0
  59. package/form-date-range-picker/package.json +24 -0
  60. package/form-date-range-picker/types/form-date-range-picker.d.ts +55 -0
  61. package/form-field/README.md +356 -2
  62. package/form-field-simple/README.md +340 -2
  63. package/form-input-otp/README.md +194 -0
  64. package/form-input-otp/fesm2022/form-input-otp.mjs +194 -0
  65. package/form-input-otp/package.json +24 -0
  66. package/form-input-otp/types/form-input-otp.d.ts +58 -0
  67. package/form-month-year-picker/README.md +188 -0
  68. package/form-month-year-picker/fesm2022/form-month-year-picker.mjs +229 -0
  69. package/form-month-year-picker/package.json +24 -0
  70. package/form-month-year-picker/types/form-month-year-picker.d.ts +53 -0
  71. package/form-native-select/README.md +205 -0
  72. package/form-native-select/fesm2022/form-native-select.mjs +187 -0
  73. package/form-native-select/package.json +24 -0
  74. package/form-native-select/types/form-native-select.d.ts +57 -0
  75. package/form-phone-input/README.md +188 -0
  76. package/form-phone-input/fesm2022/form-phone-input.mjs +203 -0
  77. package/form-phone-input/package.json +24 -0
  78. package/form-phone-input/types/form-phone-input.d.ts +58 -0
  79. package/form-radio-group/README.md +198 -0
  80. package/form-radio-group/fesm2022/form-radio-group.mjs +211 -0
  81. package/form-radio-group/package.json +24 -0
  82. package/form-radio-group/types/form-radio-group.d.ts +63 -0
  83. package/form-searchable-multiselect/README.md +371 -2
  84. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +19 -10
  85. package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +18 -1
  86. package/form-select/README.md +360 -2
  87. package/form-select/fesm2022/form-select.mjs +24 -15
  88. package/form-select/types/form-select.d.ts +18 -1
  89. package/form-slider/README.md +182 -0
  90. package/form-slider/fesm2022/form-slider.mjs +186 -0
  91. package/form-slider/package.json +24 -0
  92. package/form-slider/types/form-slider.d.ts +60 -0
  93. package/form-switch/README.md +173 -0
  94. package/form-switch/fesm2022/form-switch.mjs +178 -0
  95. package/form-switch/package.json +24 -0
  96. package/form-switch/types/form-switch.d.ts +50 -0
  97. package/form-text-input/README.md +381 -2
  98. package/form-text-input/fesm2022/form-text-input.mjs +19 -10
  99. package/form-text-input/types/form-text-input.d.ts +18 -1
  100. package/form-textarea/README.md +357 -2
  101. package/form-textarea/fesm2022/form-textarea.mjs +19 -10
  102. package/form-textarea/types/form-textarea.d.ts +18 -1
  103. package/form-toggle/README.md +186 -0
  104. package/form-toggle/fesm2022/form-toggle.mjs +241 -0
  105. package/form-toggle/package.json +24 -0
  106. package/form-toggle/types/form-toggle.d.ts +82 -0
  107. package/form-toggle-group/README.md +176 -0
  108. package/form-toggle-group/fesm2022/form-toggle-group.mjs +216 -0
  109. package/form-toggle-group/package.json +24 -0
  110. package/form-toggle-group/types/form-toggle-group.d.ts +65 -0
  111. package/hover-card/README.md +256 -5
  112. package/icon/README.md +239 -2
  113. package/input/README.md +269 -2
  114. package/input-group/README.md +335 -5
  115. package/input-group/fesm2022/input-group.mjs +23 -13
  116. package/input-group/types/input-group.d.ts +4 -1
  117. package/input-otp/README.md +375 -5
  118. package/item/README.md +385 -5
  119. package/item/fesm2022/item.mjs +3 -3
  120. package/kbd/README.md +291 -5
  121. package/label/README.md +272 -2
  122. package/layout-simple/README.md +193 -2
  123. package/layout-simple/fesm2022/layout-simple.mjs +877 -409
  124. package/layout-simple/types/layout-simple.d.ts +174 -137
  125. package/menu/README.md +417 -2
  126. package/menubar/README.md +343 -5
  127. package/native-select/README.md +323 -5
  128. package/native-select/fesm2022/native-select.mjs +18 -6
  129. package/native-select/types/native-select.d.ts +7 -2
  130. package/navigation-menu/README.md +369 -5
  131. package/package.json +57 -1
  132. package/pagination/README.md +388 -5
  133. package/phone-input/README.md +114 -0
  134. package/phone-input/fesm2022/phone-input.mjs +191 -0
  135. package/phone-input/package.json +24 -0
  136. package/phone-input/types/phone-input.d.ts +67 -0
  137. package/popover/README.md +331 -2
  138. package/progress/README.md +311 -5
  139. package/radio-group/README.md +364 -2
  140. package/radio-group/fesm2022/radio-group.mjs +5 -1
  141. package/resizable/README.md +269 -5
  142. package/scroll-area/README.md +233 -5
  143. package/searchable-multiselect/README.md +323 -2
  144. package/select/README.md +437 -2
  145. package/separator/README.md +222 -2
  146. package/sheet/README.md +311 -2
  147. package/sheet/fesm2022/sheet.mjs +1 -1
  148. package/sidebar/README.md +457 -5
  149. package/skeleton/README.md +217 -5
  150. package/slider/README.md +273 -5
  151. package/slider/fesm2022/slider.mjs +17 -13
  152. package/sonner/README.md +346 -2
  153. package/spinner/README.md +284 -2
  154. package/switch/README.md +310 -2
  155. package/switch/fesm2022/switch.mjs +7 -5
  156. package/switch/types/switch.d.ts +2 -1
  157. package/table/README.md +423 -5
  158. package/tabs/README.md +411 -2
  159. package/tabs/fesm2022/tabs.mjs +12 -2
  160. package/textarea/README.md +282 -5
  161. package/toggle/README.md +270 -5
  162. package/toggle-group/README.md +340 -5
  163. package/tooltip/README.md +269 -2
  164. package/typography/README.md +271 -5
  165. package/utils/README.md +303 -2
package/input/README.md CHANGED
@@ -1,3 +1,270 @@
1
- # Input Subpath
1
+ # Input (`@egose/shadcn-theme-ng/input`)
2
2
 
3
- This project ships only as `@egose/shadcn-theme-ng/input` or `@egose/shadcn-theme-ng-tw/input`. See the [package README](../../README.md) for installation, compatibility, Tailwind variant, test, and release guidance. Do not publish this project directory independently.
3
+ `HlmInput` is a thin styling directive that gives any native `<input>` (or `<textarea>`) the shadcn/ui _Input_ look — the Angular counterpart of shadcn/ui's `<Input />`. Behavior comes from spartan-ng's `BrnInput` (plus field `aria-describedby` propagation); this directive adds the theme classes, `data-slot="input"`, invalid-state rings, and disabled/file styles.
4
+
5
+ > **Ships as:** `@egose/shadcn-theme-ng/input` (plain Tailwind) and `@egose/shadcn-theme-ng-tw/input` (`tw:`-prefixed variant). See the [package README](../../README.md) for installation, peer dependencies, and the Tailwind-variant contract. 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
+ Peer dependencies (Angular, `@angular/forms`, CDK, `@spartan-ng/brain`, `rxjs`) are documented in the [package README](../../README.md#peer-dependencies).
18
+
19
+ ```ts
20
+ import { HlmInputImports } from '@egose/shadcn-theme-ng/input';
21
+ // tw variant:
22
+ // import { HlmInputImports } from '@egose/shadcn-theme-ng-tw/input';
23
+ ```
24
+
25
+ ## Imports
26
+
27
+ ```ts
28
+ // Standalone component — spread the imports array:
29
+ import { HlmInputImports } from '@egose/shadcn-theme-ng/input';
30
+
31
+ @Component({
32
+ standalone: true,
33
+ imports: [ReactiveFormsModule, HlmInputImports],
34
+ template: `<input hlmInput formControlName="email" type="email" />`,
35
+ })
36
+ export class MyComp {}
37
+ ```
38
+
39
+ ```ts
40
+ // NgModule-based — import the module:
41
+ import { HlmInputModule } from '@egose/shadcn-theme-ng/input';
42
+
43
+ @NgModule({ imports: [HlmInputModule] })
44
+ export class MyModule {}
45
+ ```
46
+
47
+ Exported from `src/public-api.ts`: `HlmInput`, plus `HlmInputImports` and `HlmInputModule`. Import the directive class directly (`import { HlmInput } from '…'`) when you only need the one symbol.
48
+
49
+ ## Anatomy / Structure
50
+
51
+ ```html
52
+ <label for="email">Email</label> <input hlmInput id="email" type="email" placeholder="you@example.com" />
53
+ ```
54
+
55
+ The directive matches `input[hlmInput]` usage in practice (selector `[hlmInput]`), sets `data-slot="input"`, binds `[attr.aria-describedby]` from its own input, and composes `BrnInput` (`id`, `forceInvalid`) + `BrnFieldControlDescribedBy` via `hostDirectives`. It is also the class hook reused by `textarea[hlmInput]` (see `form-textarea`) and by `input[hlmInputGroupInput]`.
56
+
57
+ ## API reference
58
+
59
+ ### `[hlmInput]` — `HlmInput`
60
+
61
+ | Input | Type | Default | Description |
62
+ | ----------------- | ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
63
+ | `ariaDescribedby` | `string \| null` | `null` | Bound to `aria-describedby`. Combine with `<hlm-error>`/`<hlm-hint>` ids (the form wrappers do this automatically). |
64
+ | `id` | (via `BrnInput`) | — | Forwarded to the `BrnInput` host directive. |
65
+ | `forceInvalid` | (via `BrnInput`) | — | Forwarded to the `BrnInput` host directive; forces the `data-matches-spartan-invalid` error styles. |
66
+
67
+ No outputs. Visual states are attribute-driven: `data-matches-spartan-invalid=true` switches the border/ring to destructive; `disabled` applies `pointer-events-none`, `cursor-not-allowed`, `opacity-50`. Base geometry: `h-9`, `rounded-md`, `text-base` (`md:text-sm`), full width.
68
+
69
+ ## Examples
70
+
71
+ ### 1. Basic text + reactive form
72
+
73
+ ```ts
74
+ import { Component } from '@angular/core';
75
+ import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
76
+ import { HlmInputImports } from '@egose/shadcn-theme-ng/input';
77
+
78
+ @Component({
79
+ standalone: true,
80
+ imports: [ReactiveFormsModule, HlmInputImports],
81
+ template: `
82
+ <form [formGroup]="form">
83
+ <label for="username">Username</label>
84
+ <input hlmInput id="username" formControlName="username" placeholder="jane_doe" />
85
+ </form>
86
+ `,
87
+ })
88
+ export class BasicExample {
89
+ readonly form = new FormGroup({
90
+ username: new FormControl<string>('', { nonNullable: true }),
91
+ });
92
+ }
93
+ ```
94
+
95
+ ### 2. Template-driven with `ngModel`
96
+
97
+ ```ts
98
+ import { Component } from '@angular/core';
99
+ import { FormsModule } from '@angular/forms';
100
+ import { HlmInput } from '@egose/shadcn-theme-ng/input';
101
+
102
+ @Component({
103
+ standalone: true,
104
+ imports: [FormsModule, HlmInput],
105
+ template: `
106
+ <label for="nickname">Nickname</label>
107
+ <input hlmInput id="nickname" name="nickname" [(ngModel)]="nickname" placeholder="janey" />
108
+ <p>Hello, {{ nickname || 'stranger' }}!</p>
109
+ `,
110
+ })
111
+ export class NgModelExample {
112
+ nickname = '';
113
+ }
114
+ ```
115
+
116
+ ### 3. Types: email, password, number, search, file
117
+
118
+ ```ts
119
+ import { Component } from '@angular/core';
120
+ import { HlmInputImports } from '@egose/shadcn-theme-ng/input';
121
+
122
+ @Component({
123
+ standalone: true,
124
+ imports: [HlmInputImports],
125
+ template: `
126
+ <div class="tw:grid tw:gap-3">
127
+ <input hlmInput type="email" placeholder="Email" aria-label="Email" autocomplete="email" />
128
+ <input hlmInput type="password" placeholder="Password" aria-label="Password" autocomplete="current-password" />
129
+ <input hlmInput type="number" placeholder="0" aria-label="Amount" min="0" />
130
+ <input hlmInput type="search" placeholder="Search…" aria-label="Search" />
131
+ <input hlmInput type="file" aria-label="Upload avatar" />
132
+ </div>
133
+ `,
134
+ })
135
+ export class TypesExample {}
136
+ ```
137
+
138
+ > File inputs inherit the directive's `file:` styles (sized button text, transparent track) — no extra markup needed.
139
+
140
+ ### 4. Disabled + invalid states
141
+
142
+ ```ts
143
+ import { Component, signal } from '@angular/core';
144
+ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
145
+ import { HlmInput } from '@egose/shadcn-theme-ng/input';
146
+
147
+ @Component({
148
+ standalone: true,
149
+ imports: [ReactiveFormsModule, HlmInput],
150
+ template: `
151
+ <form [formGroup]="form">
152
+ <input hlmInput formControlName="email" type="email" placeholder="you@example.com" aria-label="Email" />
153
+ @if (form.controls.email.invalid && form.controls.email.touched) {
154
+ <p class="tw:text-sm tw:text-red-600">Enter a valid email.</p>
155
+ }
156
+ <input hlmInput placeholder="Disabled field" aria-label="Disabled field" [disabled]="true" />
157
+ <!-- force the error ring regardless of touch state -->
158
+ <input hlmInput placeholder="Forced invalid" aria-label="Forced invalid" [forceInvalid]="true" />
159
+ </form>
160
+ `,
161
+ })
162
+ export class StatesExample {
163
+ readonly form = new FormGroup({
164
+ email: new FormControl<string>('', { nonNullable: true, validators: [Validators.email] }),
165
+ });
166
+ }
167
+ ```
168
+
169
+ ### 5. Manual `aria-describedby` with hint + error (no form wrapper)
170
+
171
+ ```ts
172
+ import { Component } from '@angular/core';
173
+ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
174
+ import { HlmInput } from '@egose/shadcn-theme-ng/input';
175
+ import { HlmError, HlmHint } from '@egose/shadcn-theme-ng/form-field';
176
+
177
+ @Component({
178
+ standalone: true,
179
+ imports: [ReactiveFormsModule, HlmInput, HlmError, HlmHint],
180
+ template: `
181
+ <form [formGroup]="form">
182
+ <label for="handle">Handle</label>
183
+ <input
184
+ hlmInput
185
+ id="handle"
186
+ formControlName="handle"
187
+ placeholder="@jane"
188
+ [ariaDescribedby]="describedBy()"
189
+ />
190
+ @if (showError()) {
191
+ <hlm-error id="handle-error">Use 3+ lowercase letters.</hlm-error>
192
+ } @else {
193
+ <hlm-hint id="handle-hint">Your public @name.</hlm-hint>
194
+ }
195
+ </form>
196
+ `,
197
+ })
198
+ export class DescribedByExample {
199
+ readonly form = new FormGroup({
200
+ handle: new FormControl<string>('', {
201
+ nonNullable: true,
202
+ validators: [Validators.minLength(3), Validators.pattern(/^[a-z]+$/)],
203
+ }),
204
+ });
205
+
206
+ showError(): boolean {
207
+ const c = this.form.controls.handle;
208
+ return c.invalid && (c.dirty || c.touched);
209
+ }
210
+
211
+ describedBy(): string {
212
+ return this.showError() ? 'handle-error' : 'handle-hint';
213
+ }
214
+ }
215
+ ```
216
+
217
+ ### 6. Composition: search row with button (input + button)
218
+
219
+ ```ts
220
+ import { Component, signal } from '@angular/core';
221
+ import { HlmInput } from '@egose/shadcn-theme-ng/input';
222
+ import { HlmButton } from '@egose/shadcn-theme-ng/button';
223
+
224
+ @Component({
225
+ standalone: true,
226
+ imports: [HlmInput, HlmButton],
227
+ template: `
228
+ <div class="tw:flex tw:gap-2">
229
+ <input
230
+ hlmInput
231
+ aria-label="Search docs"
232
+ placeholder="Search docs…"
233
+ [value]="query()"
234
+ (input)="query.set($any($event.target).value)"
235
+ (keydown.enter)="search()"
236
+ />
237
+ <button hlmBtn type="button" (click)="search()">Search</button>
238
+ </div>
239
+ @if (searched()) {
240
+ <p role="status" class="tw:text-sm">Searching for “{{ query() }}”…</p>
241
+ }
242
+ `,
243
+ })
244
+ export class SearchRowExample {
245
+ readonly query = signal('');
246
+ readonly searched = signal(false);
247
+
248
+ search() {
249
+ this.searched.set(true);
250
+ }
251
+ }
252
+ ```
253
+
254
+ ## Accessibility notes
255
+
256
+ - Always pair the input with a `<label for>` (or `aria-label`/`aria-labelledby` when the design is label-less). The directive does not generate labels.
257
+ - Wire `ariaDescribedby` to hint/error ids so screen readers announce help and validation together with the field. The `eg-form-*` wrappers automate this — copy their pattern when hand-rolling.
258
+ - Invalid styling is visual only until you expose the message text (e.g. `<hlm-error>` or `role="alert"`); use `forceInvalid` sparingly and only alongside a message.
259
+ - Keep native semantics: correct `type`, `autocomplete`, `required`, and `disabled` (not `aria-disabled` + click-guard) so AT and password managers behave.
260
+
261
+ ## Theming / CSS variables
262
+
263
+ No component-specific CSS variables; colors/spacing come from the shared theme tokens (`--input`, `--ring`, `--destructive`, `--radius`, …). Taller fields: add `tw:h-11`; monospace/code looks: add `tw:font-mono`.
264
+
265
+ ## Related subpaths
266
+
267
+ - `@egose/shadcn-theme-ng/form-text-input` — labeled + validated reactive-forms wrapper (prefer for forms).
268
+ - `@egose/shadcn-theme-ng/form-textarea` — multiline sibling reusing the same class hook.
269
+ - `@egose/shadcn-theme-ng/input-group` — joined prefix/input/suffix compositions.
270
+ - `@egose/shadcn-theme-ng/label` — `HlmLabel` for the `<label>` side.
@@ -1,11 +1,341 @@
1
- # InputGroup
1
+ # Input Group (`@egose/shadcn-theme-ng/input-group`)
2
2
 
3
- This project was generated using [Angular CLI](https://github.com/angular/angular-cli).
3
+ The input group joins an input (or textarea) with prefixes, suffixes, and inline action buttons into a single bordered control — the shadcn/ui _InputGroup_ equivalent (currency prefix + amount + Apply, URL slug builder, message composer with block add-ons). The outer directive owns the border/focus ring; the inner pieces are borderless so the group reads as one field.
4
4
 
5
- ## Building
5
+ > **Ships as:** `@egose/shadcn-theme-ng/input-group` (plain Tailwind) and `@egose/shadcn-theme-ng-tw/input-group` (`tw:`-prefixed variant). See the [package README](../../README.md) for installation, peer dependencies, and the Tailwind-variant contract. Do not publish this project directory independently.
6
6
 
7
- To build the library, run:
7
+ ## Installation
8
8
 
9
9
  ```bash
10
- ng build input-group
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
+ Peer dependencies (Angular, CDK, `@spartan-ng/brain`, `rxjs`) are documented in the [package README](../../README.md#peer-dependencies).
18
+
19
+ ```ts
20
+ import { HlmInputGroupImports } from '@egose/shadcn-theme-ng/input-group';
21
+ // tw variant:
22
+ // import { HlmInputGroupImports } from '@egose/shadcn-theme-ng-tw/input-group';
23
+ ```
24
+
25
+ ## Imports
26
+
27
+ ```ts
28
+ // Standalone component — spread the imports array:
29
+ import { HlmInputGroupImports } from '@egose/shadcn-theme-ng/input-group';
30
+
31
+ @Component({
32
+ standalone: true,
33
+ imports: [HlmInputGroupImports],
34
+ template: `
35
+ <div hlmInputGroup>
36
+ <span hlmInputGroupText>$</span>
37
+ <input hlmInputGroupInput placeholder="0.00" aria-label="Amount in dollars" />
38
+ </div>
39
+ `,
40
+ })
41
+ export class MyComp {}
42
+ ```
43
+
44
+ ```ts
45
+ // NgModule-based — import the module:
46
+ import { HlmInputGroupModule } from '@egose/shadcn-theme-ng/input-group';
47
+
48
+ @NgModule({ imports: [HlmInputGroupModule] })
49
+ export class MyModule {}
50
+ ```
51
+
52
+ Exported from `src/public-api.ts`: `HlmInputGroup`, `HlmInputGroupAddon`, `HlmInputGroupButton`, `HlmInputGroupInput`, `HlmInputGroupText`, `HlmInputGroupTextarea`, plus `HlmInputGroupImports` and `HlmInputGroupModule`.
53
+
54
+ ## Anatomy / Structure
55
+
56
+ ```html
57
+ <div hlmInputGroup>
58
+ <!-- inline prefix -->
59
+ <span hlmInputGroupText>$</span>
60
+
61
+ <!-- the actual control (borderless, flex-1) -->
62
+ <input hlmInputGroupInput placeholder="Monthly budget" aria-label="Monthly budget" />
63
+
64
+ <!-- inline action -->
65
+ <button hlmInputGroupButton type="button">Apply</button>
66
+ </div>
67
+
68
+ <!-- block layout for composers -->
69
+ <div hlmInputGroup>
70
+ <div hlmInputGroupAddon align="block-start">Message to #general</div>
71
+ <textarea hlmInputGroupTextarea rows="3" aria-label="Message"></textarea>
72
+ <div hlmInputGroupAddon align="block-end">…</div>
73
+ </div>
11
74
  ```
75
+
76
+ Real selectors: `[hlmInputGroup]` / `hlm-input-group` (container, `role="group"`); `input[hlmInputGroupInput]` (input, `hostDirectives: [HlmInput]`, `data-slot="input-group-control"`); `textarea[hlmInputGroupTextarea]` (`hostDirectives: [HlmTextarea]`, same control slot); `[hlmInputGroupAddon]` / `hlm-input-group-addon` (alignable addon, `align` = `inline-start` | `inline-end` | `block-start` | `block-end`, default `inline-start`); `[hlmInputGroupText]` / `hlm-input-group-text` (plain inline text); `button[hlmInputGroupButton]` (action button, ghost variant preset, `hostDirectives: [HlmBtn]`).
77
+
78
+ ## API reference
79
+
80
+ ### `[hlmInputGroup]` — `HlmInputGroup`
81
+
82
+ Container. No inputs/outputs. `role="group"`, `data-slot="input-group"`. Carries the border, `h-9` height, shadow, and the `:focus-visible` / invalid ring logic that reacts to the inner `[data-slot=input-group-control]` and disabled state (`group-data-[disabled=true]/input-group` dims addons).
83
+
84
+ ### `input[hlmInputGroupInput]` — `HlmInputGroupInput`
85
+
86
+ Borderless input (`hostDirectives: [HlmInput]`). No inputs of its own — all native input attributes and `HlmInput` inputs (`ariaDescribedby`, `id`, `forceInvalid`) apply directly on the element.
87
+
88
+ ### `textarea[hlmInputGroupTextarea]` — `HlmInputGroupTextarea`
89
+
90
+ Borderless textarea (`hostDirectives: [HlmTextarea]`, `resize-none`). No inputs of its own; native textarea attributes apply directly.
91
+
92
+ ### `[hlmInputGroupAddon]` — `HlmInputGroupAddon`
93
+
94
+ | Input | Type | Default | Description |
95
+ | ------- | ---------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------- |
96
+ | `align` | `'inline-start' \| 'inline-end' \| 'block-start' \| 'block-end'` | `'inline-start'` | Placement: inline prefixes/suffixes vs full-width block rows (reflected as `data-align`). |
97
+
98
+ Inline addons order themselves first/last automatically; block addons stack the group vertically (`flex-col`).
99
+
100
+ ### `[hlmInputGroupText]` — `HlmInputGroupText`
101
+
102
+ Static inline text (currency symbols, units, URL fragments). No inputs/outputs.
103
+
104
+ ### `button[hlmInputGroupButton]` — `HlmInputGroupButton`
105
+
106
+ | Input | Type | Default | Description |
107
+ | --------- | ---------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------- |
108
+ | `size` | `'xs' \| 'sm' \| 'icon-xs' \| 'icon-sm'` | `'xs'` | Button density; `icon-*` variants are square for glyph-only buttons (reflected as `data-size`). |
109
+ | `variant` | (via `HlmBtn`) | `'ghost'` | Forwarded to `HlmBtn`; the group presets the brain button config to `ghost`. |
110
+ | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Native button type (bound as `[type]`). |
111
+
112
+ ## Examples
113
+
114
+ ### 1. Currency prefix + action button
115
+
116
+ ```ts
117
+ import { Component, signal } from '@angular/core';
118
+ import { HlmInputGroupImports } from '@egose/shadcn-theme-ng/input-group';
119
+
120
+ @Component({
121
+ standalone: true,
122
+ imports: [HlmInputGroupImports],
123
+ template: `
124
+ <div hlmInputGroup>
125
+ <span hlmInputGroupText>$</span>
126
+ <input hlmInputGroupInput type="number" placeholder="Monthly budget" aria-label="Monthly budget" />
127
+ <button hlmInputGroupButton type="button" (click)="apply()">Apply</button>
128
+ </div>
129
+ @if (message()) {
130
+ <p role="status" class="tw:mt-2 tw:text-sm">{{ message() }}</p>
131
+ }
132
+ `,
133
+ })
134
+ export class CurrencyExample {
135
+ readonly message = signal<string | null>(null);
136
+
137
+ apply() {
138
+ this.message.set('Budget applied.');
139
+ }
140
+ }
141
+ ```
142
+
143
+ ### 2. URL slug builder (prefix + input + suffix)
144
+
145
+ ```ts
146
+ import { Component } from '@angular/core';
147
+ import { FormsModule } from '@angular/forms';
148
+ import { HlmInputGroupImports } from '@egose/shadcn-theme-ng/input-group';
149
+
150
+ @Component({
151
+ standalone: true,
152
+ imports: [FormsModule, HlmInputGroupImports],
153
+ template: `
154
+ <div hlmInputGroup>
155
+ <span hlmInputGroupText>https://egose.dev/</span>
156
+ <input
157
+ hlmInputGroupInput
158
+ [(ngModel)]="slug"
159
+ name="slug"
160
+ placeholder="campaign-spring"
161
+ aria-label="Campaign slug"
162
+ />
163
+ <span hlmInputGroupText>.html</span>
164
+ </div>
165
+ <p class="tw:mt-2 tw:text-sm tw:text-slate-500">Preview: https://egose.dev/{{ slug || '…' }}.html</p>
166
+ `,
167
+ })
168
+ export class SlugExample {
169
+ slug = 'angular-standard';
170
+ }
171
+ ```
172
+
173
+ ### 3. Search with icon addon + keyboard shortcut hint
174
+
175
+ ```ts
176
+ import { Component } from '@angular/core';
177
+ import { NgIcon, provideIcons } from '@ng-icons/core';
178
+ import { lucideSearch } from '@ng-icons/lucide';
179
+ import { HlmIcon } from '@egose/shadcn-theme-ng/icon';
180
+ import { HlmInputGroupImports } from '@egose/shadcn-theme-ng/input-group';
181
+ import { HlmKbd } from '@egose/shadcn-theme-ng/kbd';
182
+
183
+ @Component({
184
+ standalone: true,
185
+ imports: [NgIcon, HlmIcon, HlmInputGroupImports, HlmKbd],
186
+ providers: [provideIcons({ lucideSearch })],
187
+ template: `
188
+ <div hlmInputGroup>
189
+ <span hlmInputGroupText><ng-icon hlm name="lucideSearch" size="sm" /></span>
190
+ <input hlmInputGroupInput placeholder="Search docs…" aria-label="Search docs" />
191
+ <div hlmInputGroupAddon align="inline-end"><kbd hlmKbd>⌘K</kbd></div>
192
+ </div>
193
+ `,
194
+ })
195
+ export class SearchExample {}
196
+ ```
197
+
198
+ ### 4. Icon-only buttons (`icon-xs` / `icon-sm`)
199
+
200
+ ```ts
201
+ import { Component, signal } from '@angular/core';
202
+ import { NgIcon, provideIcons } from '@ng-icons/core';
203
+ import { lucideCopy, lucideCheck } from '@ng-icons/lucide';
204
+ import { HlmIcon } from '@egose/shadcn-theme-ng/icon';
205
+ import { HlmInputGroupImports } from '@egose/shadcn-theme-ng/input-group';
206
+
207
+ @Component({
208
+ standalone: true,
209
+ imports: [NgIcon, HlmIcon, HlmInputGroupImports],
210
+ providers: [provideIcons({ lucideCopy, lucideCheck })],
211
+ template: `
212
+ <div hlmInputGroup>
213
+ <input hlmInputGroupInput [value]="token()" readonly aria-label="API token" />
214
+ <button
215
+ hlmInputGroupButton
216
+ size="icon-xs"
217
+ type="button"
218
+ (click)="copy()"
219
+ [attr.aria-label]="copied() ? 'Copied' : 'Copy token'"
220
+ >
221
+ <ng-icon hlm [name]="copied() ? 'lucideCheck' : 'lucideCopy'" size="sm" />
222
+ </button>
223
+ </div>
224
+ `,
225
+ })
226
+ export class CopyExample {
227
+ readonly token = signal('sk-live-abc123');
228
+ readonly copied = signal(false);
229
+
230
+ async copy() {
231
+ await navigator.clipboard.writeText(this.token());
232
+ this.copied.set(true);
233
+ setTimeout(() => this.copied.set(false), 1500);
234
+ }
235
+ }
236
+ ```
237
+
238
+ ### 5. Block composer (textarea + block addons)
239
+
240
+ ```ts
241
+ import { Component, signal } from '@angular/core';
242
+ import { FormsModule } from '@angular/forms';
243
+ import { HlmInputGroupImports } from '@egose/shadcn-theme-ng/input-group';
244
+
245
+ @Component({
246
+ standalone: true,
247
+ imports: [FormsModule, HlmInputGroupImports],
248
+ template: `
249
+ <div hlmInputGroup>
250
+ <div hlmInputGroupAddon align="block-start">Message to #design-review</div>
251
+ <textarea
252
+ hlmInputGroupTextarea
253
+ rows="4"
254
+ [(ngModel)]="draft"
255
+ name="draft"
256
+ aria-label="Message to the design-review channel"
257
+ placeholder="Describe the behavior you want to change…"
258
+ ></textarea>
259
+ <div hlmInputGroupAddon align="block-end" class="tw:flex tw:items-center tw:justify-between">
260
+ <span role="status">{{ status() }}</span>
261
+ <button hlmInputGroupButton type="button" (click)="send()">Send</button>
262
+ </div>
263
+ </div>
264
+ `,
265
+ })
266
+ export class ComposerExample {
267
+ draft = '';
268
+ readonly status = signal('Draft autosaved just now');
269
+
270
+ send() {
271
+ this.status.set('Message sent just now.');
272
+ this.draft = '';
273
+ }
274
+ }
275
+ ```
276
+
277
+ ### 6. Reactive form + validation (forceInvalid + error text)
278
+
279
+ ```ts
280
+ import { Component } from '@angular/core';
281
+ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
282
+ import { HlmInputGroupImports } from '@egose/shadcn-theme-ng/input-group';
283
+ import { HlmError } from '@egose/shadcn-theme-ng/form-field';
284
+
285
+ @Component({
286
+ standalone: true,
287
+ imports: [ReactiveFormsModule, HlmInputGroupImports, HlmError],
288
+ template: `
289
+ <form [formGroup]="form" (ngSubmit)="submit()">
290
+ <div hlmInputGroup>
291
+ <span hlmInputGroupText>€</span>
292
+ <input
293
+ hlmInputGroupInput
294
+ type="number"
295
+ formControlName="price"
296
+ placeholder="0.00"
297
+ aria-label="Price in euros"
298
+ aria-describedby="price-error"
299
+ [forceInvalid]="showError()"
300
+ />
301
+ </div>
302
+ @if (showError()) {
303
+ <hlm-error id="price-error">Enter a price greater than 0.</hlm-error>
304
+ }
305
+ <button type="submit">Save</button>
306
+ </form>
307
+ `,
308
+ })
309
+ export class ValidationExample {
310
+ readonly form = new FormGroup({
311
+ price: new FormControl<number | null>(null, { validators: [Validators.required, Validators.min(0.01)] }),
312
+ });
313
+
314
+ showError(): boolean {
315
+ const c = this.form.controls.price;
316
+ return c.invalid && (c.dirty || c.touched);
317
+ }
318
+
319
+ submit() {
320
+ if (this.form.invalid) this.form.markAllAsTouched();
321
+ }
322
+ }
323
+ ```
324
+
325
+ ## Accessibility notes
326
+
327
+ - The container has `role="group"` — give the inner control an `aria-label` (or visible `<label>`) since add-on text is not automatically associated.
328
+ - Add-on text (`$`, `https://…`) is visual context; repeat the essential unit in the input's accessible name (`aria-label="Monthly budget in dollars"`), not just the placeholder.
329
+ - Icon-only `hlmInputGroupButton`s need an `aria-label` (`Copy token`, `Send`).
330
+ - Validation messages live outside the group — link them with `aria-describedby` + `forceInvalid` as in example 6 so the error ring and announcement stay in sync.
331
+
332
+ ## Theming / CSS variables
333
+
334
+ No component-specific CSS variables; borders, rings, and muted text come from the shared tokens (`--input`, `--ring`, `--muted-foreground`, `--destructive`, `--radius`). Width/height adapt to content — constrain with `tw:max-w-*` on the group.
335
+
336
+ ## Related subpaths
337
+
338
+ - `@egose/shadcn-theme-ng/input` — standalone `HlmInput` (what the group input extends).
339
+ - `@egose/shadcn-theme-ng/textarea` — `HlmTextarea` (what the group textarea extends).
340
+ - `@egose/shadcn-theme-ng/button` — `HlmBtn` variants behind the group button.
341
+ - `@egose/shadcn-theme-ng/kbd` — shortcut hints inside addons.