@egose/shadcn-theme-ng 0.1.0 → 0.2.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 (87) hide show
  1. package/README.md +1 -1
  2. package/accordion/README.md +405 -2
  3. package/alert/README.md +372 -2
  4. package/alert-dialog/README.md +471 -5
  5. package/aspect-ratio/README.md +272 -5
  6. package/autocomplete/README.md +502 -2
  7. package/avatar/README.md +357 -5
  8. package/badge/README.md +318 -2
  9. package/basic-alert/README.md +353 -2
  10. package/breadcrumb/README.md +406 -5
  11. package/button/README.md +482 -2
  12. package/button/fesm2022/button.mjs +85 -107
  13. package/button/types/button.d.ts +5 -8
  14. package/button-group/README.md +318 -5
  15. package/calendar/README.md +357 -2
  16. package/card/README.md +331 -5
  17. package/carousel/README.md +333 -5
  18. package/carousel/fesm2022/carousel.mjs +4 -1
  19. package/checkbox/README.md +320 -2
  20. package/collapsible/README.md +332 -5
  21. package/combobox/README.md +507 -5
  22. package/combobox/fesm2022/combobox.mjs +4 -1
  23. package/command/README.md +435 -5
  24. package/confirmation-dialog/README.md +301 -2
  25. package/context-menu/README.md +366 -5
  26. package/date-picker/README.md +465 -2
  27. package/date-picker/fesm2022/date-picker.mjs +2 -2
  28. package/dialog/README.md +448 -2
  29. package/drawer/README.md +395 -5
  30. package/dropdown-menu/README.md +417 -5
  31. package/empty/README.md +329 -5
  32. package/field/README.md +385 -5
  33. package/form-checkbox/README.md +312 -2
  34. package/form-date-picker/README.md +322 -2
  35. package/form-field/README.md +356 -2
  36. package/form-field-simple/README.md +340 -2
  37. package/form-searchable-multiselect/README.md +361 -2
  38. package/form-select/README.md +350 -2
  39. package/form-text-input/README.md +371 -2
  40. package/form-textarea/README.md +347 -2
  41. package/hover-card/README.md +256 -5
  42. package/icon/README.md +239 -2
  43. package/input/README.md +269 -2
  44. package/input-group/README.md +335 -5
  45. package/input-group/fesm2022/input-group.mjs +3 -3
  46. package/input-otp/README.md +375 -5
  47. package/item/README.md +385 -5
  48. package/item/fesm2022/item.mjs +3 -3
  49. package/kbd/README.md +291 -5
  50. package/label/README.md +272 -2
  51. package/layout-simple/README.md +193 -2
  52. package/layout-simple/fesm2022/layout-simple.mjs +472 -236
  53. package/layout-simple/types/layout-simple.d.ts +174 -137
  54. package/menu/README.md +417 -2
  55. package/menubar/README.md +343 -5
  56. package/native-select/README.md +323 -5
  57. package/navigation-menu/README.md +369 -5
  58. package/package.json +1 -1
  59. package/pagination/README.md +388 -5
  60. package/popover/README.md +331 -2
  61. package/progress/README.md +311 -5
  62. package/radio-group/README.md +364 -2
  63. package/radio-group/fesm2022/radio-group.mjs +5 -1
  64. package/radio-group/types/radio-group.d.ts +1 -1
  65. package/resizable/README.md +269 -5
  66. package/scroll-area/README.md +233 -5
  67. package/searchable-multiselect/README.md +323 -2
  68. package/select/README.md +437 -2
  69. package/separator/README.md +222 -2
  70. package/sheet/README.md +311 -2
  71. package/sidebar/README.md +457 -5
  72. package/skeleton/README.md +217 -5
  73. package/slider/README.md +273 -5
  74. package/slider/fesm2022/slider.mjs +3 -3
  75. package/sonner/README.md +346 -2
  76. package/spinner/README.md +284 -2
  77. package/switch/README.md +310 -2
  78. package/table/README.md +423 -5
  79. package/tabs/README.md +411 -2
  80. package/tabs/fesm2022/tabs.mjs +2 -2
  81. package/textarea/README.md +282 -5
  82. package/toggle/README.md +270 -5
  83. package/toggle-group/README.md +340 -5
  84. package/tooltip/README.md +269 -2
  85. package/typography/README.md +271 -5
  86. package/typography/types/typography.d.ts +8 -8
  87. package/utils/README.md +303 -2
package/icon/README.md CHANGED
@@ -1,3 +1,240 @@
1
- # Icon Subpath
1
+ # Icon (`@egose/shadcn-theme-ng/icon`)
2
2
 
3
- This project ships only as `@egose/shadcn-theme-ng/icon` or `@egose/shadcn-theme-ng-tw/icon`. See the [package README](../../README.md) for installation, compatibility, Tailwind variant, test, and release guidance. Do not publish this project directory independently.
3
+ `HlmIcon` is a thin directive that sizes `@ng-icons/core` icons consistently with the shadcn theme. It applies to `ng-icon` elements (`selector: 'ng-icon[hlm]'`) and maps t-shirt sizes (`xs`–`xl`) to pixel values via the `--ng-icon__size` CSS variable — the shadcn/ui equivalent of the `size-*` icon convention. A global default size can be set once with `provideHlmIconConfig`.
4
+
5
+ > **Ships as:** `@egose/shadcn-theme-ng/icon` (plain Tailwind) and `@egose/shadcn-theme-ng-tw/icon` (`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, `@ng-icons/core`, …) are documented in the [package README](../../README.md#peer-dependencies). You must also register the icons you use with `provideIcons` from `@ng-icons/core` (see examples).
18
+
19
+ ```ts
20
+ import { HlmIconImports } from '@egose/shadcn-theme-ng/icon';
21
+ // tw variant:
22
+ // import { HlmIconImports } from '@egose/shadcn-theme-ng-tw/icon';
23
+ ```
24
+
25
+ ## Imports
26
+
27
+ ```ts
28
+ // Standalone component — spread the imports array:
29
+ import { HlmIconImports } from '@egose/shadcn-theme-ng/icon';
30
+
31
+ @Component({
32
+ standalone: true,
33
+ imports: [NgIcon, ...HlmIconImports], // or just [NgIcon, HlmIcon]
34
+ providers: [provideIcons({ lucidePlus })],
35
+ template: `<ng-icon hlm name="lucidePlus" size="sm" />`,
36
+ })
37
+ export class MyComp {}
38
+ ```
39
+
40
+ ```ts
41
+ // NgModule-based — import the module:
42
+ import { HlmIconModule } from '@egose/shadcn-theme-ng/icon';
43
+
44
+ @NgModule({ imports: [HlmIconModule] })
45
+ export class MyModule {}
46
+ ```
47
+
48
+ Exported from `src/public-api.ts`: `HlmIcon`, `IconSize`, `HlmIconConfig`, `provideHlmIconConfig`, `injectHlmIconConfig`, plus `HlmIconImports` and `HlmIconModule`.
49
+
50
+ > Note: `HlmIcon` decorates `ng-icon` — you still import `NgIcon` itself from `@ng-icons/core` and register glyphs with `provideIcons`.
51
+
52
+ ## Anatomy / Structure
53
+
54
+ ```html
55
+ <!-- hlm attribute activates the sizing directive on any ng-icon -->
56
+ <ng-icon hlm name="lucidePlus" size="sm" />
57
+ ```
58
+
59
+ The directive sets `[style.--ng-icon__size]` from the `size` input; named sizes resolve to pixels, anything else passes through verbatim as CSS.
60
+
61
+ ## API reference
62
+
63
+ ### `ng-icon[hlm]` — `HlmIcon`
64
+
65
+ | Input | Type | Default | Description |
66
+ | ------ | ---------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
67
+ | `size` | `IconSize` | config default (`base`) | `xs` → `12px`, `sm` → `16px`, `base` → `24px`, `lg` → `32px`, `xl` → `48px`, `none` → `none`, or any custom CSS size string (e.g. `'20px'`, `'1.5rem'`). |
68
+
69
+ No outputs. `IconSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | 'none' | (string & {})`.
70
+
71
+ ### Config — `HlmIconConfig` / `provideHlmIconConfig` / `injectHlmIconConfig`
72
+
73
+ | API | Signature | Description |
74
+ | ------------------------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------- |
75
+ | `HlmIconConfig` | `{ size: IconSize }` | Global icon config shape (default `{ size: 'base' }`). |
76
+ | `provideHlmIconConfig(config: Partial<HlmIconConfig>)` | returns `ValueProvider` | Provide once (root or feature) to change the default `size` for every `ng-icon[hlm]`. |
77
+ | `injectHlmIconConfig()` | returns `HlmIconConfig` | Reads the ambient config (falls back to defaults). |
78
+
79
+ ## Examples
80
+
81
+ ### 1. Basic icon with registered glyph
82
+
83
+ ```ts
84
+ import { Component } from '@angular/core';
85
+ import { NgIcon, provideIcons } from '@ng-icons/core';
86
+ import { lucidePlus } from '@ng-icons/lucide';
87
+ import { HlmIcon } from '@egose/shadcn-theme-ng/icon';
88
+
89
+ @Component({
90
+ standalone: true,
91
+ imports: [NgIcon, HlmIcon],
92
+ providers: [provideIcons({ lucidePlus })],
93
+ template: `<ng-icon hlm name="lucidePlus" />`,
94
+ })
95
+ export class BasicExample {}
96
+ ```
97
+
98
+ ### 2. All named sizes
99
+
100
+ ```ts
101
+ import { Component } from '@angular/core';
102
+ import { NgIcon, provideIcons } from '@ng-icons/core';
103
+ import { lucideSearch } from '@ng-icons/lucide';
104
+ import { HlmIconImports } from '@egose/shadcn-theme-ng/icon';
105
+
106
+ @Component({
107
+ standalone: true,
108
+ imports: [NgIcon, HlmIconImports],
109
+ providers: [provideIcons({ lucideSearch })],
110
+ template: `
111
+ <div class="tw:flex tw:items-center tw:gap-4">
112
+ <ng-icon hlm name="lucideSearch" size="xs" />
113
+ <ng-icon hlm name="lucideSearch" size="sm" />
114
+ <ng-icon hlm name="lucideSearch" size="base" />
115
+ <ng-icon hlm name="lucideSearch" size="lg" />
116
+ <ng-icon hlm name="lucideSearch" size="xl" />
117
+ </div>
118
+ `,
119
+ })
120
+ export class SizesExample {}
121
+ ```
122
+
123
+ ### 3. Custom CSS sizes (any string passes through)
124
+
125
+ ```ts
126
+ import { Component } from '@angular/core';
127
+ import { NgIcon, provideIcons } from '@ng-icons/core';
128
+ import { lucideBell } from '@ng-icons/lucide';
129
+ import { HlmIcon } from '@egose/shadcn-theme-ng/icon';
130
+
131
+ @Component({
132
+ standalone: true,
133
+ imports: [NgIcon, HlmIcon],
134
+ providers: [provideIcons({ lucideBell })],
135
+ template: `
136
+ <div class="tw:flex tw:items-center tw:gap-4">
137
+ <ng-icon hlm name="lucideBell" size="20px" />
138
+ <ng-icon hlm name="lucideBell" size="1.5rem" />
139
+ <ng-icon hlm name="lucideBell" size="none" />
140
+ </div>
141
+ `,
142
+ })
143
+ export class CustomSizeExample {}
144
+ ```
145
+
146
+ ### 4. Global default via `provideHlmIconConfig`
147
+
148
+ ```ts
149
+ import { Component } from '@angular/core';
150
+ import { NgIcon, provideIcons } from '@ng-icons/core';
151
+ import { lucideCheck, lucideX } from '@ng-icons/lucide';
152
+ import { HlmIcon, provideHlmIconConfig } from '@egose/shadcn-theme-ng/icon';
153
+
154
+ @Component({
155
+ standalone: true,
156
+ imports: [NgIcon, HlmIcon],
157
+ providers: [provideIcons({ lucideCheck, lucideX }), provideHlmIconConfig({ size: 'sm' })],
158
+ template: `
159
+ <!-- both inherit size="sm" unless overridden -->
160
+ <ng-icon hlm name="lucideCheck" />
161
+ <ng-icon hlm name="lucideX" size="lg" />
162
+ `,
163
+ })
164
+ export class ConfigExample {}
165
+ ```
166
+
167
+ ### 5. Icons inside buttons and inputs (composition)
168
+
169
+ ```ts
170
+ import { Component } from '@angular/core';
171
+ import { NgIcon, provideIcons } from '@ng-icons/core';
172
+ import { lucideSearch, lucidePlus } from '@ng-icons/lucide';
173
+ import { HlmIcon } from '@egose/shadcn-theme-ng/icon';
174
+ import { HlmButton } from '@egose/shadcn-theme-ng/button';
175
+ import { HlmInputGroupImports } from '@egose/shadcn-theme-ng/input-group';
176
+
177
+ @Component({
178
+ standalone: true,
179
+ imports: [NgIcon, HlmIcon, HlmButton, HlmInputGroupImports],
180
+ providers: [provideIcons({ lucideSearch, lucidePlus })],
181
+ template: `
182
+ <button hlmBtn type="button"><ng-icon hlm name="lucidePlus" size="sm" /> New item</button>
183
+
184
+ <div hlmInputGroup>
185
+ <span hlmInputGroupText><ng-icon hlm name="lucideSearch" size="sm" /></span>
186
+ <input hlmInputGroupInput placeholder="Search…" aria-label="Search" />
187
+ </div>
188
+ `,
189
+ })
190
+ export class CompositionExample {}
191
+ ```
192
+
193
+ > The input-group addon styles (`[&>ng-icon…]` hooks) assume icons sized through this directive — prefer `size="sm"` or smaller inside addons/buttons so text and glyph align.
194
+
195
+ ### 6. Dynamic icon + size with signals
196
+
197
+ ```ts
198
+ import { Component, signal } from '@angular/core';
199
+ import { NgIcon, provideIcons } from '@ng-icons/core';
200
+ import { lucideLoaderCircle, lucideCheck } from '@ng-icons/lucide';
201
+ import { HlmIcon, type IconSize } from '@egose/shadcn-theme-ng/icon';
202
+
203
+ @Component({
204
+ standalone: true,
205
+ imports: [NgIcon, HlmIcon],
206
+ providers: [provideIcons({ lucideLoaderCircle, lucideCheck })],
207
+ template: `
208
+ <p>
209
+ <ng-icon hlm [name]="saving() ? 'lucideLoaderCircle' : 'lucideCheck'" [size]="iconSize()" />
210
+ {{ saving() ? 'Saving…' : 'Saved' }}
211
+ </p>
212
+ <button type="button" (click)="toggleSize()">Toggle size</button>
213
+ `,
214
+ })
215
+ export class DynamicExample {
216
+ readonly saving = signal(true);
217
+ readonly iconSize = signal<IconSize>('sm');
218
+
219
+ toggleSize() {
220
+ this.iconSize.update((s) => (s === 'sm' ? 'lg' : 'sm'));
221
+ }
222
+ }
223
+ ```
224
+
225
+ ## Accessibility notes
226
+
227
+ - `ng-icon` renders decorative SVG — screen readers ignore it by default. When an icon is the _only_ content of a button/link, put the accessible name on the control (`aria-label="Search"`), not on the icon.
228
+ - Never convey status by icon alone (e.g. a lone red `x` for errors); pair it with text or an `role="status"` message.
229
+ - Icon buttons need a visible focus indicator and at least a 24px (ideally 44px) hit area — use the button size variants rather than shrinking the control to the glyph.
230
+
231
+ ## Theming / CSS variables
232
+
233
+ Sizing flows through the `--ng-icon__size` CSS variable set by the directive; color inherits `currentColor`, so icons follow surrounding text/foreground tokens automatically. No component-specific theme variables.
234
+
235
+ ## Related subpaths
236
+
237
+ - `@egose/shadcn-theme-ng/button` — icon buttons and icon+label composition.
238
+ - `@egose/shadcn-theme-ng/input-group` — addon slots with `ng-icon` styling hooks.
239
+ - `@egose/shadcn-theme-ng/badge` — common icon+text pill composition.
240
+ - `@egose/shadcn-theme-ng/spinner` — animated loading indicator alternative.
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.