@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
@@ -1,11 +1,329 @@
1
- # NativeSelect
1
+ # Native Select (`@egose/shadcn-theme-ng/native-select`)
2
2
 
3
- This project was generated using [Angular CLI](https://github.com/angular/angular-cli).
3
+ Zero-dependency HTML `<select>` with shadcn styling (shadcn/ui `native-select` equivalent). No overlay, no CDK menu, no listbox emulation — a real `<select>` inside a positioning wrapper with a chevron icon, wired as an Angular `ControlValueAccessor` and a spartan-ng `BrnFieldControl`/`BrnLabelable` so it participates in `formControl`/`ngModel`, `hlm-label` association, and field error states.
4
4
 
5
- ## Building
5
+ Ships as `@egose/shadcn-theme-ng/native-select` and `@egose/shadcn-theme-ng-tw/native-select` (tw: variant). See the [package README](../../README.md) for installation, peer dependencies, Tailwind setup, and testing. 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 native-select
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 are inherited from the package root (see [package README](../../README.md)). This subpath itself declares `@angular/common`, `@angular/core`, `@spartan-ng/brain` as peers plus a `tslib` runtime dependency; at runtime it also uses `@ng-icons/lucide` (`lucideChevronDown`), `@spartan-ng/brain/field`, and `@spartan-ng/brain/forms` types. No extra install step is needed beyond the package install above.
18
+
19
+ ## Imports
20
+
21
+ Real exported symbols (from `src/public-api.ts`):
22
+
23
+ ```ts
24
+ import {
25
+ HlmNativeSelect, // component: hlm-native-select (CVA)
26
+ HlmNativeSelectOption, // directive: option[hlmNativeSelectOption]
27
+ HlmNativeSelectOptGroup, // directive: optgroup[hlmNativeSelectOptGroup]
28
+ HLM_NATIVE_SELECT_VALUE_ACCESSOR, // NG_VALUE_ACCESSOR provider const
29
+ HlmNativeSelectImports, // readonly [HlmNativeSelect, HlmNativeSelectOption, HlmNativeSelectOptGroup]
30
+ HlmNativeSelectModule, // NgModule wrapping HlmNativeSelectImports
31
+ } from '@egose/shadcn-theme-ng/native-select';
32
+ ```
33
+
34
+ Standalone usage:
35
+
36
+ ```ts
37
+ import { Component } from '@angular/core';
38
+ import { HlmNativeSelectImports } from '@egose/shadcn-theme-ng/native-select';
39
+
40
+ @Component({
41
+ selector: 'app-demo',
42
+ standalone: true,
43
+ imports: [...HlmNativeSelectImports],
44
+ template: `
45
+ <hlm-native-select>
46
+ <option hlmNativeSelectOption value="apple">Apple</option>
47
+ </hlm-native-select>
48
+ `,
49
+ })
50
+ export class DemoComponent {}
51
+ ```
52
+
53
+ NgModule usage:
54
+
55
+ ```ts
56
+ import { NgModule } from '@angular/core';
57
+ import { HlmNativeSelectModule } from '@egose/shadcn-theme-ng/native-select';
58
+
59
+ @NgModule({ imports: [HlmNativeSelectModule] })
60
+ export class DemoModule {}
61
+ ```
62
+
63
+ For the `tw:` build, swap the specifier to `@egose/shadcn-theme-ng-tw/native-select`. Symbol names are identical.
64
+
65
+ ## Anatomy / Structure
66
+
67
+ ```html
68
+ <hlm-native-select>
69
+ <option hlmNativeSelectOption value="">Choose a fruit…</option>
70
+ <optgroup hlmNativeSelectOptGroup label="Citrus">
71
+ <option hlmNativeSelectOption value="orange">Orange</option>
72
+ <option hlmNativeSelectOption value="lemon">Lemon</option>
73
+ </optgroup>
74
+ <option hlmNativeSelectOption value="apple">Apple</option>
75
+ </hlm-native-select>
76
+
77
+ <!-- sizes + classes -->
78
+ <hlm-native-select size="sm" selectClass="tw:w-48" selectIconClass="tw:text-primary">
79
+ <option hlmNativeSelectOption value="s">Small</option>
80
+ </hlm-native-select>
81
+ ```
82
+
83
+ Rendered DOM: the host (`data-slot="native-select-wrapper"`, `group/native-select relative w-fit`) projects your options into an inner native `<select data-slot="native-select">` plus an absolutely-positioned `lucideChevronDown` icon (`data-slot="native-select-icon"`). The inner select carries `data-size`, `data-invalid`, `data-dirty`, `data-touched`, and `data-matches-spartan-invalid` attributes for styling hooks.
84
+
85
+ | Class | Selector | Role |
86
+ | ------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------ |
87
+ | `HlmNativeSelect` | `hlm-native-select` | Wrapper + CVA + chevron |
88
+ | `HlmNativeSelectOption` | `option[hlmNativeSelectOption]` | Styled `<option>` (`bg-[Canvas] text-[CanvasText]` so dropdown lists stay native-readable) |
89
+ | `HlmNativeSelectOptGroup` | `optgroup[hlmNativeSelectOptGroup]` | Styled `<optgroup>` (same Canvas treatment) |
90
+
91
+ ## API reference
92
+
93
+ ### `HlmNativeSelect` (`hlm-native-select`, implements `ControlValueAccessor`)
94
+
95
+ | Member | Kind | Type / Default | Notes |
96
+ | ------------------------------------------------ | ------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------- |
97
+ | `selectId` | input | `string`, auto `hlm-native-select-${n}` | Applied as the inner `<select>`'s `id`; also exposed as `labelableId` for `hlm-label` association |
98
+ | `selectClass` | input | `ClassValue`, `''` | Merged over the inner select classes |
99
+ | `selectIconClass` | input | `ClassValue`, `''` | Merged over the chevron icon classes |
100
+ | `size` | input | `'sm' \| 'default'`, `'default'` | Reflected as `data-size` on host + select; `sm` → `h-8` instead of `h-9` |
101
+ | `disabled` | input | `boolean`, `false` (boolean coercion) | Initial disabled; reactive forms override via `setDisabledState` into `_disabled` linked signal |
102
+ | `forceInvalid` | input | `boolean`, `false` (boolean coercion) | Forces `data-matches-spartan-invalid` styling |
103
+ | `ariaInvalidOverride` (alias `aria-invalid`) | input | `boolean \| undefined`, `undefined` | Manual `aria-invalid`; defaults to the parent `BrnFieldControl.invalid` state |
104
+ | `valueInput` (alias `value`) | input | `string \| undefined \| null`, `''` | One-way value; mirrored into the `value` linked signal |
105
+ | `value` | linked signal | `string \| undefined \| null` | Current value; bound to the select's `[value]` |
106
+ | `valueChange` | output | `string \| undefined \| null` | Emitted on native `change` alongside the CVA callback |
107
+ | `labelableId` | property | = `selectId` | `BrnLabelable` contract for label association |
108
+ | `writeValue(v)` | CVA method | | Sets `value` |
109
+ | `registerOnChange(fn)` / `registerOnTouched(fn)` | CVA methods | | Stored as `_onChange` / `_onTouched`; invoked on `change` / `change`+`blur` |
110
+ | `setDisabledState(isDisabled)` | CVA method | | Sets `_disabled` |
111
+
112
+ Host directives: `BrnFieldControl` (provides `invalid`/`touched`/`dirty`/`spartanInvalid` read by the template). Providers: `NG_VALUE_ACCESSOR` (`HLM_NATIVE_SELECT_VALUE_ACCESSOR`), `provideIcons({ lucideChevronDown })`, `provideBrnLabelable(HlmNativeSelect)`.
113
+
114
+ ### `HlmNativeSelectOption` / `HlmNativeSelectOptGroup`
115
+
116
+ No inputs/outputs/methods. Pure styling directives setting `data-slot` (`native-select-option` / `native-select-optgroup`).
117
+
118
+ ## Examples
119
+
120
+ ### 1. Basic uncontrolled select
121
+
122
+ ```ts
123
+ import { Component } from '@angular/core';
124
+ import { HlmNativeSelectImports } from '@egose/shadcn-theme-ng/native-select';
125
+
126
+ @Component({
127
+ selector: 'app-native-basic',
128
+ standalone: true,
129
+ imports: [...HlmNativeSelectImports],
130
+ template: `
131
+ <hlm-native-select (valueChange)="onChange($event)">
132
+ <option hlmNativeSelectOption value="">Choose a fruit…</option>
133
+ <option hlmNativeSelectOption value="apple">Apple</option>
134
+ <option hlmNativeSelectOption value="banana">Banana</option>
135
+ </hlm-native-select>
136
+ <p class="tw:text-sm tw:text-muted-foreground">Picked: {{ picked ?? 'nothing' }}</p>
137
+ `,
138
+ })
139
+ export class NativeBasicComponent {
140
+ picked: string | null | undefined;
141
+
142
+ onChange(value: string | null | undefined): void {
143
+ this.picked = value;
144
+ }
145
+ }
11
146
  ```
147
+
148
+ ### 2. Sizes and optgroups
149
+
150
+ ```ts
151
+ import { Component } from '@angular/core';
152
+ import { HlmNativeSelectImports } from '@egose/shadcn-theme-ng/native-select';
153
+
154
+ @Component({
155
+ selector: 'app-native-groups',
156
+ standalone: true,
157
+ imports: [...HlmNativeSelectImports],
158
+ template: `
159
+ <div class="tw:flex tw:flex-col tw:gap-4 tw:max-w-xs">
160
+ <hlm-native-select size="sm">
161
+ <option hlmNativeSelectOption value="s">Small</option>
162
+ <option hlmNativeSelectOption value="m">Medium</option>
163
+ </hlm-native-select>
164
+
165
+ <hlm-native-select size="default">
166
+ <option hlmNativeSelectOption value="">Choose a citrus…</option>
167
+ <optgroup hlmNativeSelectOptGroup label="Citrus">
168
+ <option hlmNativeSelectOption value="orange">Orange</option>
169
+ <option hlmNativeSelectOption value="lemon">Lemon</option>
170
+ </optgroup>
171
+ <optgroup hlmNativeSelectOptGroup label="Berries">
172
+ <option hlmNativeSelectOption value="strawberry">Strawberry</option>
173
+ <option hlmNativeSelectOption value="blueberry">Blueberry</option>
174
+ </optgroup>
175
+ </hlm-native-select>
176
+ </div>
177
+ `,
178
+ })
179
+ export class NativeGroupsComponent {}
180
+ ```
181
+
182
+ ### 3. Reactive form + label
183
+
184
+ ```ts
185
+ import { Component } from '@angular/core';
186
+ import { ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';
187
+ import { HlmNativeSelectImports } from '@egose/shadcn-theme-ng/native-select';
188
+ import { HlmLabelImports } from '@egose/shadcn-theme-ng/label';
189
+
190
+ @Component({
191
+ selector: 'app-native-reactive',
192
+ standalone: true,
193
+ imports: [ReactiveFormsModule, ...HlmNativeSelectImports, ...HlmLabelImports],
194
+ template: `
195
+ <form [formGroup]="form" class="tw:grid tw:gap-1.5 tw:max-w-xs">
196
+ <label hlmLabel for="country">Country</label>
197
+ <hlm-native-select selectId="country" formControlName="country">
198
+ <option hlmNativeSelectOption value="">Select…</option>
199
+ <option hlmNativeSelectOption value="de">Germany</option>
200
+ <option hlmNativeSelectOption value="fr">France</option>
201
+ <option hlmNativeSelectOption value="jp">Japan</option>
202
+ </hlm-native-select>
203
+ @if (form.controls.country.touched && form.controls.country.invalid) {
204
+ <p class="tw:text-sm tw:text-destructive">Country is required.</p>
205
+ }
206
+ <button type="button" (click)="disable()">Toggle disabled</button>
207
+ </form>
208
+ `,
209
+ })
210
+ export class NativeReactiveComponent {
211
+ readonly form = new FormGroup({
212
+ country: new FormControl('', { validators: [Validators.required], nonNullable: true }),
213
+ });
214
+
215
+ disable(): void {
216
+ const c = this.form.controls.country;
217
+ c.disabled ? c.enable() : c.disable();
218
+ }
219
+ }
220
+ ```
221
+
222
+ ### 4. Template-driven (`ngModel`) + two-way `value`
223
+
224
+ ```ts
225
+ import { Component } from '@angular/core';
226
+ import { FormsModule } from '@angular/forms';
227
+ import { HlmNativeSelectImports } from '@egose/shadcn-theme-ng/native-select';
228
+
229
+ @Component({
230
+ selector: 'app-native-ngmodel',
231
+ standalone: true,
232
+ imports: [FormsModule, ...HlmNativeSelectImports],
233
+ template: `
234
+ <hlm-native-select [(ngModel)]="flavor">
235
+ <option hlmNativeSelectOption value="vanilla">Vanilla</option>
236
+ <option hlmNativeSelectOption value="chocolate">Chocolate</option>
237
+ <option hlmNativeSelectOption value="mint">Mint</option>
238
+ </hlm-native-select>
239
+ <p class="tw:text-sm">Flavor: {{ flavor }}</p>
240
+
241
+ <!-- one-way value input + valueChange output (no forms): -->
242
+ <hlm-native-select [value]="flavor" (valueChange)="flavor = $event ?? ''">
243
+ <option hlmNativeSelectOption value="vanilla">Vanilla</option>
244
+ <option hlmNativeSelectOption value="chocolate">Chocolate</option>
245
+ </hlm-native-select>
246
+ `,
247
+ })
248
+ export class NativeNgModelComponent {
249
+ flavor = 'vanilla';
250
+ }
251
+ ```
252
+
253
+ ### 5. Async options, invalid, and disabled states
254
+
255
+ ```ts
256
+ import { Component, signal, OnInit } from '@angular/core';
257
+ import { HlmNativeSelectImports } from '@egose/shadcn-theme-ng/native-select';
258
+
259
+ @Component({
260
+ selector: 'app-native-async',
261
+ standalone: true,
262
+ imports: [...HlmNativeSelectImports],
263
+ template: `
264
+ <div class="tw:grid tw:gap-4 tw:max-w-xs">
265
+ <hlm-native-select [value]="selected()" (valueChange)="selected.set($event ?? '')">
266
+ <option hlmNativeSelectOption value="">Loading…</option>
267
+ @for (c of cities(); track c) {
268
+ <option hlmNativeSelectOption [value]="c">{{ c }}</option>
269
+ }
270
+ </hlm-native-select>
271
+
272
+ <hlm-native-select [forceInvalid]="true" value="bad">
273
+ <option hlmNativeSelectOption value="bad">Forced invalid</option>
274
+ <option hlmNativeSelectOption value="good">Good</option>
275
+ </hlm-native-select>
276
+
277
+ <hlm-native-select [disabled]="true" value="locked">
278
+ <option hlmNativeSelectOption value="locked">Disabled</option>
279
+ </hlm-native-select>
280
+ </div>
281
+ `,
282
+ })
283
+ export class NativeAsyncComponent implements OnInit {
284
+ readonly cities = signal<string[]>([]);
285
+ readonly selected = signal('');
286
+
287
+ async ngOnInit(): Promise<void> {
288
+ await new Promise((r) => setTimeout(r, 300));
289
+ this.cities.set(['Berlin', 'Paris', 'Tokyo']);
290
+ }
291
+ }
292
+ ```
293
+
294
+ ### 6. Custom widths and icon tint
295
+
296
+ ```ts
297
+ import { Component } from '@angular/core';
298
+ import { HlmNativeSelectImports } from '@egose/shadcn-theme-ng/native-select';
299
+
300
+ @Component({
301
+ selector: 'app-native-styled',
302
+ standalone: true,
303
+ imports: [...HlmNativeSelectImports],
304
+ template: `
305
+ <hlm-native-select selectClass="tw:w-56" selectIconClass="tw:text-primary" size="sm">
306
+ <option hlmNativeSelectOption value="a">Compact tinted chevron</option>
307
+ <option hlmNativeSelectOption value="b">Option B</option>
308
+ </hlm-native-select>
309
+ `,
310
+ })
311
+ export class NativeStyledComponent {}
312
+ ```
313
+
314
+ ## Accessibility notes
315
+
316
+ - A native `<select>` gives you listbox semantics, keyboard support (arrows/typeahead), and mobile pickers for free — prefer it over a custom dropdown when the option list is simple.
317
+ - Associate with `<label hlmLabel for="…">` matching `selectId` (or nest contextually); invalid state flows to `aria-invalid` automatically via `BrnFieldControl` unless overridden with `aria-invalid`.
318
+ - Keep the first `<option>` a real placeholder with `value=""` when a choice is required, and validate accordingly.
319
+ - `forceInvalid` is visual + `data-*` styling; still set form errors so screen readers announce the problem.
320
+
321
+ ## Theming / CSS variables
322
+
323
+ No theming inputs. The select uses `border-input`, `ring-ring`, `bg-muted`-adjacent tokens with dark-mode (`dark:bg-input/30`) and invalid (`border-destructive ring-destructive`) treatments. Extend via `selectClass` / `selectIconClass` (merged after base classes).
324
+
325
+ ## Related subpaths
326
+
327
+ - `@egose/shadcn-theme-ng/select` — custom overlay select for searchable/rich options; use native-select when a plain `<select>` suffices.
328
+ - `@egose/shadcn-theme-ng/label` — label association via `selectId`/`labelableId`.
329
+ - `@egose/shadcn-theme-ng/form-field` — field wrapper with error display that reads the same `BrnFieldControl` state.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { forwardRef, inject, input, computed, booleanAttribute, linkedSignal, output, ChangeDetectionStrategy, Component, Directive, NgModule } from '@angular/core';
2
+ import { forwardRef, inject, input, computed, booleanAttribute, signal, linkedSignal, output, ChangeDetectionStrategy, Component, Directive, NgModule } from '@angular/core';
3
3
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
4
  import { NgIcon, provideIcons } from '@ng-icons/core';
5
5
  import { lucideChevronDown } from '@ng-icons/lucide';
@@ -17,9 +17,15 @@ class HlmNativeSelect {
17
17
  static _id = 0;
18
18
  selectId = input(`hlm-native-select-${HlmNativeSelect._id++}`, /* @ts-ignore */
19
19
  ...(ngDevMode ? [{ debugName: "selectId" }] : /* istanbul ignore next */ []));
20
+ /** Native `name` attribute forwarded to the inner `<select>`. */
21
+ name = input(undefined, /* @ts-ignore */
22
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
23
+ /** ID of the element describing the select (error/hint) for assistive tech. */
24
+ ariaDescribedby = input(null, /* @ts-ignore */
25
+ ...(ngDevMode ? [{ debugName: "ariaDescribedby" }] : /* istanbul ignore next */ []));
20
26
  selectClass = input('', /* @ts-ignore */
21
27
  ...(ngDevMode ? [{ debugName: "selectClass" }] : /* istanbul ignore next */ []));
22
- _computedSelectClass = computed(() => hlm('tw:border-input tw:placeholder:text-muted-foreground tw:selection:bg-primary tw:selection:text-primary-foreground tw:dark:bg-input/30 tw:dark:hover:bg-input/50 tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:data-[matches-spartan-invalid=true]:ring-destructive/20 tw:dark:data-[matches-spartan-invalid=true]:ring-destructive/40 tw:data-[matches-spartan-invalid=true]:border-destructive tw:dark:data-[matches-spartan-invalid=true]:border-destructive/50 tw:h-9 tw:w-full tw:min-w-0 tw:appearance-none tw:rounded-md tw:border tw:bg-transparent tw:py-1 tw:ps-2.5 tw:pe-8 tw:text-sm tw:shadow-xs tw:transition-[color,box-shadow] tw:select-none tw:focus-visible:ring-3 tw:data-[matches-spartan-invalid=true]:ring-3 tw:data-[size=sm]:h-8 tw:outline-none tw:disabled:pointer-events-none tw:disabled:cursor-not-allowed', this.selectClass()), /* @ts-ignore */
28
+ _computedSelectClass = computed(() => hlm('tw:border-input tw:placeholder:text-muted-foreground tw:selection:bg-primary tw:selection:text-primary-foreground tw:dark:bg-input/30 tw:dark:hover:bg-input/50 tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:data-[matches-spartan-invalid=true]:ring-destructive/20 tw:dark:data-[matches-spartan-invalid=true]:ring-destructive/40 tw:data-[matches-spartan-invalid=true]:border-destructive tw:dark:data-[matches-spartan-invalid=true]:border-destructive/50 tw:h-9 tw:w-full tw:min-w-0 tw:appearance-none tw:rounded-md tw:border tw:bg-transparent tw:py-1 tw:ps-2.5 tw:pe-8 tw:text-sm tw:text-foreground tw:shadow-xs tw:transition-[color,box-shadow] tw:select-none tw:focus-visible:ring-3 tw:data-[matches-spartan-invalid=true]:ring-3 tw:data-[size=sm]:h-8 tw:outline-none tw:disabled:pointer-events-none tw:disabled:cursor-not-allowed tw:[color-scheme:light] tw:dark:[color-scheme:dark]', this.selectClass()), /* @ts-ignore */
23
29
  ...(ngDevMode ? [{ debugName: "_computedSelectClass" }] : /* istanbul ignore next */ []));
24
30
  selectIconClass = input('', /* @ts-ignore */
25
31
  ...(ngDevMode ? [{ debugName: "selectIconClass" }] : /* istanbul ignore next */ []));
@@ -28,7 +34,9 @@ class HlmNativeSelect {
28
34
  size = input('default', /* @ts-ignore */
29
35
  ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
30
36
  disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
31
- _disabled = linkedSignal(this.disabled, /* @ts-ignore */
37
+ _formDisabled = signal(false, /* @ts-ignore */
38
+ ...(ngDevMode ? [{ debugName: "_formDisabled" }] : /* istanbul ignore next */ []));
39
+ _disabled = computed(() => this.disabled() || this._formDisabled(), /* @ts-ignore */
32
40
  ...(ngDevMode ? [{ debugName: "_disabled" }] : /* istanbul ignore next */ []));
33
41
  /** Whether to force the input into an invalid state. */
34
42
  forceInvalid = input(false, { ...(ngDevMode ? { debugName: "forceInvalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
@@ -73,10 +81,10 @@ class HlmNativeSelect {
73
81
  this._onTouched = fn;
74
82
  }
75
83
  setDisabledState(isDisabled) {
76
- this._disabled.set(isDisabled);
84
+ this._formDisabled.set(isDisabled);
77
85
  }
78
86
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmNativeSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
79
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: HlmNativeSelect, isStandalone: true, selector: "hlm-native-select", inputs: { selectId: { classPropertyName: "selectId", publicName: "selectId", isSignal: true, isRequired: false, transformFunction: null }, selectClass: { classPropertyName: "selectClass", publicName: "selectClass", isSignal: true, isRequired: false, transformFunction: null }, selectIconClass: { classPropertyName: "selectIconClass", publicName: "selectIconClass", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, forceInvalid: { classPropertyName: "forceInvalid", publicName: "forceInvalid", isSignal: true, isRequired: false, transformFunction: null }, ariaInvalidOverride: { classPropertyName: "ariaInvalidOverride", publicName: "aria-invalid", isSignal: true, isRequired: false, transformFunction: null }, valueInput: { classPropertyName: "valueInput", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { attributes: { "data-slot": "native-select-wrapper" }, properties: { "attr.data-size": "size()" } }, providers: [
87
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: HlmNativeSelect, isStandalone: true, selector: "hlm-native-select", inputs: { selectId: { classPropertyName: "selectId", publicName: "selectId", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "ariaDescribedby", isSignal: true, isRequired: false, transformFunction: null }, selectClass: { classPropertyName: "selectClass", publicName: "selectClass", isSignal: true, isRequired: false, transformFunction: null }, selectIconClass: { classPropertyName: "selectIconClass", publicName: "selectIconClass", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, forceInvalid: { classPropertyName: "forceInvalid", publicName: "forceInvalid", isSignal: true, isRequired: false, transformFunction: null }, ariaInvalidOverride: { classPropertyName: "ariaInvalidOverride", publicName: "aria-invalid", isSignal: true, isRequired: false, transformFunction: null }, valueInput: { classPropertyName: "valueInput", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { attributes: { "data-slot": "native-select-wrapper" }, properties: { "attr.data-size": "size()" } }, providers: [
80
88
  HLM_NATIVE_SELECT_VALUE_ACCESSOR,
81
89
  provideIcons({ lucideChevronDown }),
82
90
  provideBrnLabelable(HlmNativeSelect),
@@ -84,6 +92,8 @@ class HlmNativeSelect {
84
92
  <select
85
93
  data-slot="native-select"
86
94
  [id]="selectId()"
95
+ [attr.name]="name()"
96
+ [attr.aria-describedby]="ariaDescribedby()"
87
97
  [class]="_computedSelectClass()"
88
98
  [attr.data-size]="size()"
89
99
  [attr.aria-invalid]="_ariaInvalid() ? 'true' : null"
@@ -127,6 +137,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
127
137
  <select
128
138
  data-slot="native-select"
129
139
  [id]="selectId()"
140
+ [attr.name]="name()"
141
+ [attr.aria-describedby]="ariaDescribedby()"
130
142
  [class]="_computedSelectClass()"
131
143
  [attr.data-size]="size()"
132
144
  [attr.aria-invalid]="_ariaInvalid() ? 'true' : null"
@@ -150,7 +162,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
150
162
  />
151
163
  `,
152
164
  }]
153
- }], ctorParameters: () => [], propDecorators: { selectId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectId", required: false }] }], selectClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectClass", required: false }] }], selectIconClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectIconClass", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], forceInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceInvalid", required: false }] }], ariaInvalidOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-invalid", required: false }] }], valueInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
165
+ }], ctorParameters: () => [], propDecorators: { selectId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectId", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedby", required: false }] }], selectClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectClass", required: false }] }], selectIconClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectIconClass", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], forceInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceInvalid", required: false }] }], ariaInvalidOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-invalid", required: false }] }], valueInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
154
166
 
155
167
  class HlmNativeSelectOptGroup {
156
168
  constructor() {
@@ -14,13 +14,18 @@ declare class HlmNativeSelect implements ControlValueAccessor {
14
14
  private readonly _fieldControl;
15
15
  private static _id;
16
16
  readonly selectId: _angular_core.InputSignal<string>;
17
+ /** Native `name` attribute forwarded to the inner `<select>`. */
18
+ readonly name: _angular_core.InputSignal<string | undefined>;
19
+ /** ID of the element describing the select (error/hint) for assistive tech. */
20
+ readonly ariaDescribedby: _angular_core.InputSignal<string | null>;
17
21
  readonly selectClass: _angular_core.InputSignal<ClassValue>;
18
22
  protected readonly _computedSelectClass: _angular_core.Signal<string>;
19
23
  readonly selectIconClass: _angular_core.InputSignal<ClassValue>;
20
24
  protected readonly _computedSelectIconClass: _angular_core.Signal<string>;
21
25
  readonly size: _angular_core.InputSignal<"sm" | "default">;
22
26
  readonly disabled: _angular_core.InputSignalWithTransform<boolean, BooleanInput>;
23
- protected readonly _disabled: _angular_core.WritableSignal<boolean>;
27
+ private readonly _formDisabled;
28
+ protected readonly _disabled: _angular_core.Signal<boolean>;
24
29
  /** Whether to force the input into an invalid state. */
25
30
  readonly forceInvalid: _angular_core.InputSignalWithTransform<boolean, BooleanInput>;
26
31
  /** Manual override for aria-invalid. When not set, auto-detects from the parent autocomplete error state. */
@@ -45,7 +50,7 @@ declare class HlmNativeSelect implements ControlValueAccessor {
45
50
  registerOnTouched(fn: TouchFn): void;
46
51
  setDisabledState(isDisabled: boolean): void;
47
52
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<HlmNativeSelect, never>;
48
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<HlmNativeSelect, "hlm-native-select", never, { "selectId": { "alias": "selectId"; "required": false; "isSignal": true; }; "selectClass": { "alias": "selectClass"; "required": false; "isSignal": true; }; "selectIconClass": { "alias": "selectIconClass"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "forceInvalid": { "alias": "forceInvalid"; "required": false; "isSignal": true; }; "ariaInvalidOverride": { "alias": "aria-invalid"; "required": false; "isSignal": true; }; "valueInput": { "alias": "value"; "required": false; "isSignal": true; }; }, { "valueChange": "valueChange"; }, never, ["*"], true, [{ directive: typeof i1.BrnFieldControl; inputs: {}; outputs: {}; }]>;
53
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<HlmNativeSelect, "hlm-native-select", never, { "selectId": { "alias": "selectId"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "ariaDescribedby": { "alias": "ariaDescribedby"; "required": false; "isSignal": true; }; "selectClass": { "alias": "selectClass"; "required": false; "isSignal": true; }; "selectIconClass": { "alias": "selectIconClass"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "forceInvalid": { "alias": "forceInvalid"; "required": false; "isSignal": true; }; "ariaInvalidOverride": { "alias": "aria-invalid"; "required": false; "isSignal": true; }; "valueInput": { "alias": "value"; "required": false; "isSignal": true; }; }, { "valueChange": "valueChange"; }, never, ["*"], true, [{ directive: typeof i1.BrnFieldControl; inputs: {}; outputs: {}; }]>;
49
54
  }
50
55
 
51
56
  declare class HlmNativeSelectOptGroup {