@adamosuiteservices/ui 1.6.7 → 1.7.8

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.
@@ -1,631 +1,654 @@
1
- # Combobox
2
-
3
- Componente de selección avanzado que combina Popover + Command para búsqueda y selección. Soporta selección simple/múltiple, búsqueda filtrable, opciones deshabilitadas, feedback visual con checkbox o check, placeholder persistente, iconos personalizados, y renderizado completamente personalizable mediante render props.
4
-
5
- ## Importación
6
-
7
- ```tsx
8
- import { Combobox } from "@adamosuiteservices/ui/combobox";
9
- ```
10
-
11
- ## Anatomía
12
-
13
- ```tsx
14
- <Combobox
15
- options={[
16
- { value: "option1", label: "Option 1" },
17
- { value: "option2", label: "Option 2", disabled: true },
18
- ]}
19
- value={selectedValue}
20
- onValueChange={setSelectedValue}
21
- />
22
- ```
23
-
24
- **Componente único**: Internamente usa Popover, Command, Button, Checkbox
25
-
26
- ## Props Principales
27
-
28
- | Prop | Tipo | Default | Descripción |
29
- | ----------------------- | --------------------------------------- | ------------ | ----------------------------------------------------------- |
30
- | `options` | `ComboboxOption[]` | **required** | Array de opciones `{ value, label, disabled? }` |
31
- | `value` | `string \| string[]` | - | Valor controlado (string para single, array para multiple) |
32
- | `onValueChange` | `(value: string \| string[]) => void` | - | Callback al cambiar selección |
33
- | `searchable` | `boolean` | `false` | Habilita input de búsqueda |
34
- | `multiple` | `boolean` | `false` | Permite selección múltiple |
35
- | `selectedFeedback` | `"checkbox" \| "check"` | `"checkbox"` | Tipo de indicador visual |
36
- | `icon` | `ComponentType<{ className?: string }>` | - | Componente de icono opcional para mostrar en el trigger |
37
- | `alwaysShowPlaceholder` | `boolean` | `false` | Mantiene el placeholder visible junto al valor seleccionado |
38
- | `valuePosition` | `"left" \| "right"` | `"right"` | Posición del valor cuando alwaysShowPlaceholder es true |
39
- | `labels` | `ComboboxLabels` | - | Textos personalizables (ver tabla abajo) |
40
- | `classNames` | `ComboboxClassNames` | - | Clases CSS por sección (ver tabla abajo) |
41
- | `renders` | `ComboboxRenderProps` | - | Funciones de renderizado personalizadas (ver tabla abajo) |
42
-
43
- ### ComboboxLabels
44
-
45
- | Prop | Tipo | Default | Descripción |
46
- | ------------------- | --------------------------- | ---------------------- | ----------------------------------------------- |
47
- | `placeholder` | `string` | `"Select options..."` | Texto cuando no hay selección |
48
- | `searchPlaceholder` | `string` | `"Search options..."` | Placeholder del input de búsqueda |
49
- | `noItemsFound` | `string` | `"No options found."` | Mensaje cuando búsqueda no encuentra resultados |
50
- | `multipleSelected` | `(count: number) => string` | `"X options selected"` | Función para texto de múltiples seleccionados |
51
-
52
- ### ComboboxClassNames
53
-
54
- | Prop | Descripción |
55
- | ---------- | -------------------------------------------------------- |
56
- | `trigger` | Button trigger del popover |
57
- | `popover` | Contenedor del popover |
58
- | `command` | Componente Command root |
59
- | `input` | Input de búsqueda |
60
- | `list` | Lista de opciones |
61
- | `empty` | Mensaje de no resultados |
62
- | `group` | Grupo de items |
63
- | `item` | Cada opción individual |
64
- | `checkbox` | Checkbox de selección (si `selectedFeedback="checkbox"`) |
65
- | `check` | Icono de check (si `selectedFeedback="check"`) |
66
-
67
- ### ComboboxRenderProps
68
-
69
- Funciones de renderizado personalizadas para control total sobre la UI. Todas son opcionales y reciben props relevantes.
70
-
71
- | Prop | Parámetros | Descripción |
72
- | ------------------ | ----------------------------------------------------------- | --------------------------------------- |
73
- | `trigger` | `{ open, value, displayText, placeholder, hasValue, icon }` | Renderiza el botón trigger completo |
74
- | `triggerIcon` | `{ open }` | Renderiza el icono del dropdown |
75
- | `placeholder` | `{ text, hasValue }` | Renderiza el texto del placeholder |
76
- | `displayValue` | `{ text, value }` | Renderiza el valor seleccionado |
77
- | `option` | `{ option, isSelected, selectedFeedback }` | Renderiza una opción completa |
78
- | `optionLabel` | `{ option }` | Renderiza solo la etiqueta de la opción |
79
- | `selectedFeedback` | `{ option, isSelected, type }` | Renderiza el indicador de selección |
80
- | `empty` | `{ text }` | Renderiza el estado vacío |
81
- | `searchInput` | `{ placeholder }` | Renderiza el input de búsqueda |
82
-
83
- ## Patrones de Uso
84
-
85
- ### Básico (Sin Búsqueda)
86
-
87
- ```tsx
88
- const frameworks = [
89
- { value: "next", label: "Next.js" },
90
- { value: "remix", label: "Remix" },
91
- { value: "astro", label: "Astro" },
92
- ];
93
-
94
- <Combobox
95
- options={frameworks}
96
- labels={{ placeholder: "Select framework..." }}
97
- />;
98
- ```
99
-
100
- ### Con Icono
101
-
102
- ```tsx
103
- import { CalendarIcon } from "lucide-react";
104
-
105
- <Combobox
106
- searchable
107
- icon={CalendarIcon}
108
- options={frameworks}
109
- labels={{ placeholder: "Select framework..." }}
110
- />;
111
- ```
112
-
113
- ### Placeholder Persistente
114
-
115
- Útil para usar el placeholder como etiqueta que permanece visible.
116
-
117
- ```tsx
118
- <Combobox
119
- searchable
120
- alwaysShowPlaceholder
121
- valuePosition="right"
122
- options={frameworks}
123
- value={value}
124
- onValueChange={setValue}
125
- labels={{ placeholder: "Framework" }}
126
- />
127
- ```
128
-
129
- **Comportamiento**:
130
-
131
- - Sin valor: Muestra solo "Framework"
132
- - Con valor y `valuePosition="right"`: Muestra "Framework" a la izquierda y el valor a la derecha
133
- - Con valor y `valuePosition="left"`: Muestra "Framework" y el valor ambos a la izquierda
134
-
135
- ### Posición del Valor
136
-
137
- Cuando `alwaysShowPlaceholder` es true, puedes controlar dónde aparece el valor seleccionado:
138
-
139
- ```tsx
140
- // Valor a la derecha (default)
141
- <Combobox
142
- alwaysShowPlaceholder
143
- valuePosition="right"
144
- options={frameworks}
145
- />
146
-
147
- // Valor a la izquierda junto al placeholder
148
- <Combobox
149
- alwaysShowPlaceholder
150
- valuePosition="left"
151
- options={frameworks}
152
- />
153
- ```
154
-
155
- **Layouts resultantes**:
156
-
157
- - `valuePosition="right"`: `[Icon] [Placeholder]` ... `[Value] [Chevron]`
158
- - `valuePosition="left"`: `[Icon] [Placeholder] [Value]` ... `[Chevron]`
159
-
160
- ### Icono + Placeholder Persistente
161
-
162
- ```tsx
163
- <Combobox
164
- searchable
165
- alwaysShowPlaceholder
166
- icon={CalendarIcon}
167
- options={frameworks}
168
- value={value}
169
- onValueChange={setValue}
170
- labels={{ placeholder: "Framework" }}
171
- />
172
- ```
173
-
174
- ### Con Búsqueda
175
-
176
- ```tsx
177
- <Combobox
178
- searchable
179
- options={frameworks}
180
- labels={{
181
- placeholder: "Select framework...",
182
- searchPlaceholder: "Search frameworks...",
183
- noItemsFound: "No framework found.",
184
- }}
185
- />
186
- ```
187
-
188
- ### Selección Simple Controlada
189
-
190
- ```tsx
191
- const [value, setValue] = useState("");
192
-
193
- <Combobox
194
- searchable
195
- options={frameworks}
196
- value={value}
197
- onValueChange={(newValue) => setValue(newValue as string)}
198
- labels={{ placeholder: "Select framework..." }}
199
- />;
200
- ```
201
-
202
- ### Selección Múltiple
203
-
204
- ```tsx
205
- const [values, setValues] = useState<string[]>([]);
206
-
207
- <Combobox
208
- searchable
209
- multiple
210
- options={frameworks}
211
- value={values}
212
- onValueChange={(newValues) => setValues(newValues as string[])}
213
- labels={{
214
- placeholder: "Select frameworks...",
215
- multipleSelected: (count) => `${count} frameworks selected`,
216
- }}
217
- />;
218
- ```
219
-
220
- ### Con Feedback de Check
221
-
222
- ```tsx
223
- <Combobox
224
- searchable
225
- options={frameworks}
226
- selectedFeedback="check"
227
- labels={{ placeholder: "Select option..." }}
228
- />
229
- ```
230
-
231
- **Diferencia**: `"checkbox"` muestra checkboxes a la izquierda, `"check"` muestra ícono check a la derecha.
232
-
233
- ### Opciones Deshabilitadas
234
-
235
- ```tsx
236
- const options = [
237
- { value: "us", label: "United States" },
238
- { value: "ca", label: "Canada" },
239
- { value: "mx", label: "Mexico", disabled: true },
240
- ];
241
-
242
- <Combobox
243
- searchable
244
- options={options}
245
- labels={{ placeholder: "Select country..." }}
246
- />;
247
- ```
248
-
249
- ### Labels Personalizadas
250
-
251
- ```tsx
252
- <Combobox
253
- searchable
254
- multiple
255
- options={frameworks}
256
- labels={{
257
- placeholder: "Choose your stack...",
258
- searchPlaceholder: "Type to filter...",
259
- noItemsFound: "No matching technologies found.",
260
- multipleSelected: (count) =>
261
- `${count} ${count === 1 ? "tech" : "techs"} selected`,
262
- }}
263
- />
264
- ```
265
-
266
- ### Styling Personalizado
267
-
268
- ```tsx
269
- <Combobox
270
- searchable
271
- options={frameworks}
272
- classNames={{
273
- trigger: "border-2 border-blue-500 w-64",
274
- popover: "border-blue-200 shadow-lg",
275
- item: "rounded px-3 py-2 hover:bg-blue-50",
276
- check: "text-blue-600",
277
- }}
278
- />
279
- ```
280
-
281
- ### En Formulario
282
-
283
- ```tsx
284
- import { Field, FieldLabel } from "@adamosuiteservices/ui/field";
285
-
286
- function FormExample() {
287
- const [framework, setFramework] = useState("");
288
- const [country, setCountry] = useState("");
289
-
290
- return (
291
- <form className="space-y-4">
292
- <Field>
293
- <FieldLabel>Framework</FieldLabel>
294
- <Combobox
295
- searchable
296
- options={frameworks}
297
- value={framework}
298
- onValueChange={(value) => setFramework(value as string)}
299
- labels={{ placeholder: "Select framework..." }}
300
- />
301
- </Field>
302
-
303
- <Field>
304
- <FieldLabel>Country</FieldLabel>
305
- <Combobox
306
- searchable
307
- options={countries}
308
- value={country}
309
- onValueChange={(value) => setCountry(value as string)}
310
- selectedFeedback="check"
311
- labels={{ placeholder: "Select country..." }}
312
- />
313
- </Field>
314
-
315
- <button type="submit">Submit</button>
316
- </form>
317
- );
318
- }
319
- ```
320
-
321
- ### Multiple con Clear All
322
-
323
- ```tsx
324
- const [values, setValues] = useState<string[]>([]);
325
-
326
- <div className="space-y-2">
327
- <Combobox
328
- searchable
329
- multiple
330
- options={frameworks}
331
- value={values}
332
- onValueChange={(newValues) => setValues(newValues as string[])}
333
- />
334
-
335
- {values.length > 0 && (
336
- <button
337
- onClick={() => setValues([])}
338
- className="text-xs text-blue-600 hover:text-blue-800 underline"
339
- >
340
- Clear all
341
- </button>
342
- )}
343
- </div>;
344
- ```
345
-
346
- ## Renderizado Personalizado
347
-
348
- El prop `renders` permite personalizar completamente la UI usando el patrón render prop. Todas las funciones son opcionales.
349
-
350
- ### Custom Option Label
351
-
352
- ```tsx
353
- <Combobox
354
- searchable
355
- options={frameworks}
356
- value={value}
357
- onValueChange={setValue}
358
- renders={{
359
- optionLabel: ({ option }) => (
360
- <div className="flex items-center gap-2">
361
- <span className="font-semibold text-blue-600">🚀</span>
362
- <span>{option.label}</span>
363
- </div>
364
- ),
365
- }}
366
- />
367
- ```
368
-
369
- ### Custom Display Value
370
-
371
- ```tsx
372
- <Combobox
373
- searchable
374
- options={frameworks}
375
- value={value}
376
- onValueChange={setValue}
377
- renders={{
378
- displayValue: ({ text }) => (
379
- <span className="font-semibold text-blue-600">{text}</span>
380
- ),
381
- }}
382
- />
383
- ```
384
-
385
- ### Custom Trigger Completo
386
-
387
- ```tsx
388
- <Combobox
389
- searchable
390
- options={frameworks}
391
- value={value}
392
- onValueChange={setValue}
393
- renders={{
394
- trigger: ({ open, displayText, placeholder, hasValue }) => (
395
- <button
396
- type="button"
397
- className="flex items-center justify-between w-full px-4 py-2 text-sm bg-linear-to-r from-purple-500 to-pink-500 text-white rounded-lg shadow-md hover:shadow-lg transition-all"
398
- >
399
- <span>{hasValue ? displayText : placeholder}</span>
400
- <span className={`transition-transform ${open ? "rotate-180" : ""}`}>
401
-
402
- </span>
403
- </button>
404
- ),
405
- }}
406
- />
407
- ```
408
-
409
- ### Custom Empty State
410
-
411
- ```tsx
412
- <Combobox
413
- searchable
414
- options={[]}
415
- renders={{
416
- empty: ({ text }) => (
417
- <div className="py-8 text-center">
418
- <div className="text-4xl mb-2">🔍</div>
419
- <p className="text-sm font-medium text-muted-foreground">{text}</p>
420
- <p className="text-xs text-muted-foreground mt-1">
421
- Try adjusting your search criteria
422
- </p>
423
- </div>
424
- ),
425
- }}
426
- />
427
- ```
428
-
429
- ### Custom Selected Feedback
430
-
431
- ```tsx
432
- <Combobox
433
- searchable
434
- multiple
435
- options={frameworks}
436
- value={values}
437
- onValueChange={setValues}
438
- renders={{
439
- selectedFeedback: ({ isSelected }) => (
440
- <span className="text-lg">{isSelected ? "✅" : "⬜"}</span>
441
- ),
442
- }}
443
- />
444
- ```
445
-
446
- ### Custom Placeholder
447
-
448
- ```tsx
449
- <Combobox
450
- searchable
451
- alwaysShowPlaceholder
452
- options={frameworks}
453
- value={value}
454
- onValueChange={setValue}
455
- renders={{
456
- placeholder: ({ text, hasValue }) => (
457
- <span className={hasValue ? "opacity-50 italic" : ""}>{text}</span>
458
- ),
459
- }}
460
- />
461
- ```
462
-
463
- ### Custom Trigger Icon
464
-
465
- ```tsx
466
- <Combobox
467
- searchable
468
- options={frameworks}
469
- value={value}
470
- onValueChange={setValue}
471
- renders={{
472
- triggerIcon: ({ open }) => (
473
- <span className={`transition-transform ${open ? "rotate-90" : ""}`}>
474
-
475
- </span>
476
- ),
477
- }}
478
- />
479
- ```
480
-
481
- ### Custom Search Input
482
-
483
- ```tsx
484
- <Combobox
485
- searchable
486
- options={frameworks}
487
- value={value}
488
- onValueChange={setValue}
489
- renders={{
490
- searchInput: ({ placeholder }) => (
491
- <div className="flex items-center px-3 py-2 border-b">
492
- <span className="mr-2">🔍</span>
493
- <input
494
- type="text"
495
- placeholder={placeholder}
496
- className="flex-1 outline-none text-sm"
497
- />
498
- </div>
499
- ),
500
- }}
501
- />
502
- ```
503
-
504
- ### Combinando Múltiples Renders
505
-
506
- ```tsx
507
- <Combobox
508
- searchable
509
- multiple
510
- options={frameworks}
511
- value={values}
512
- onValueChange={setValues}
513
- labels={{
514
- placeholder: "Select frameworks...",
515
- searchPlaceholder: "Search...",
516
- }}
517
- renders={{
518
- optionLabel: ({ option }) => (
519
- <div className="flex items-center gap-2">
520
- <span>🚀</span>
521
- <span className="font-medium">{option.label}</span>
522
- </div>
523
- ),
524
- displayValue: ({ text }) => (
525
- <span className="font-semibold text-purple-600">{text}</span>
526
- ),
527
- selectedFeedback: ({ isSelected }) => (
528
- <span className="text-lg">{isSelected ? "✅" : "⬜"}</span>
529
- ),
530
- empty: ({ text }) => (
531
- <div className="py-6 text-center">
532
- <div className="text-3xl mb-2">😕</div>
533
- <p className="text-sm text-gray-500">{text}</p>
534
- </div>
535
- ),
536
- }}
537
- />
538
- ```
539
-
540
- ## Modos de Operación
541
-
542
- ### Single Selection
543
-
544
- - `multiple={false}` (default)
545
- - `value` es `string`
546
- - Click en opción cierra el popover automáticamente
547
- - Seleccionar opción ya seleccionada la deselecciona
548
-
549
- ### Multiple Selection
550
-
551
- - `multiple={true}`
552
- - `value` es `string[]`
553
- - Click en opción NO cierra el popover
554
- - Checkboxes permiten toggle de múltiples opciones
555
- - Display muestra contador cuando >1 seleccionado
556
-
557
- ## Casos de Uso Comunes
558
-
559
- **Framework selector**: Selección de tecnología con búsqueda
560
- **Country/City picker**: Selección geográfica con opciones deshabilitadas
561
- **Tags/Categories**: Selección múltiple de categorías
562
- **Settings filters**: Filtros multi-selección en dashboards
563
- **User assignment**: Asignar múltiples usuarios a tarea
564
- **Language selector**: Selección de idioma con búsqueda
565
- **Status selector con icono**: Usar `icon` + `alwaysShowPlaceholder` para selectores etiquetados
566
- **Rich options**: Usar `renders.optionLabel` para mostrar avatares, badges, etc.
567
- **Custom UI**: Usar `renders.trigger` para triggers completamente personalizados
568
-
569
- ## Estados Internos
570
-
571
- ### Display Text
572
-
573
- - **Sin selección**: Muestra `labels.placeholder`
574
- - **Single selection**: Muestra `label` de la opción seleccionada
575
- - **Single selection + alwaysShowPlaceholder**: Muestra `placeholder: label` separados
576
- - **Multiple (1 item)**: Muestra `label` del único item seleccionado
577
- - **Multiple (1 item) + alwaysShowPlaceholder**: Muestra `placeholder: label` separados
578
- - **Multiple (>1 items)**: Muestra resultado de `labels.multipleSelected(count)`
579
- - **Multiple (>1 items) + alwaysShowPlaceholder**: Muestra `placeholder: X items selected` separados
580
-
581
- ### Popover State
582
-
583
- - Controlado internamente con `useState`
584
- - Se cierra automáticamente en single selection
585
- - Permanece abierto en multiple selection
586
-
587
- ## Accesibilidad
588
-
589
- - **ARIA**: Button tiene `role="combobox"` y `aria-expanded`
590
- - ✅ **Navegación teclado**: Arrow keys para navegar opciones, Enter para seleccionar, Escape para cerrar
591
- - **Screen readers**: Anuncia opciones y estado seleccionado
592
- - ✅ **Focus management**: Focus automático en input cuando searchable
593
- - **Disabled options**: No seleccionables, estilo visual diferente
594
-
595
- ## Notas de Implementación
596
-
597
- - **Composición**: Usa Popover + Command + Button + Checkbox internamente
598
- - **Búsqueda**: Command component maneja fuzzy search automáticamente
599
- - **Estado interno**: Soporta modo controlado y no controlado
600
- - **Tipo de value**: Cambia según `multiple` (string vs string[])
601
- - **selectedFeedback**:
602
- - `"checkbox"`: Muestra Checkbox a la izquierda de cada opción
603
- - `"check"`: Muestra CheckIcon a la derecha (agrega `pr-8` automáticamente)
604
- - **Popover alignment**: `align="start"` por defecto para alinear con trigger
605
- - **ChevronDownIcon**: Se muestra en trigger con `opacity-50` (puede sobrescribirse con `renders.triggerIcon`)
606
- - **No animation**: Selección instantánea, sin delays
607
- - **alwaysShowPlaceholder**: Renderiza placeholder y valor como elementos separados con gap
608
- - **valuePosition**: Controla donde aparece el valor cuando `alwaysShowPlaceholder` es true ("left" o "right", default "right")
609
- - **icon**: Se muestra con `opacity-50` por defecto cuando se proporciona el prop `icon`
610
- - **Render props**: Tienen prioridad sobre renderizado por defecto, permiten personalización completa
611
- - **Composición de renders**: Puedes combinar múltiples render props para personalización granular
612
-
613
- ## Troubleshooting
614
-
615
- **Value no cambia**: Verifica que uses `onValueChange` en modo controlado
616
- **Búsqueda no funciona**: Asegúrate de pasar `searchable={true}`
617
- **Multiple selection cierra popover**: Verifica que `multiple={true}` esté activo
618
- **Labels incorrectas en multiple**: Usa función `multipleSelected` para pluralización
619
- **Tipo de value incorrecto**: Single usa `string`, Multiple usa `string[]`
620
- **Checkboxes no aparecen**: Solo aparecen cuando `selectedFeedback="checkbox"` (default)
621
- **Check icon no visible**: Solo aparece cuando `selectedFeedback="check"`
622
- **Icon no se muestra**: Asegúrate de pasar el prop `icon` con un componente válido
623
- **Placeholder desaparece al seleccionar**: Si quieres mantenerlo visible, usa `alwaysShowPlaceholder={true}`
624
- **Render prop no funciona**: Verifica que el render prop retorne un `ReactNode` válido
625
- **Custom trigger pierde funcionalidad**: Cuando usas `renders.trigger`, debes manejar el click manualmente o envolver en `PopoverTrigger`
626
-
627
- ## Referencias
628
-
629
- - **Radix UI Popover**: <https://www.radix-ui.com/primitives/docs/components/popover>
630
- - **Command Component**: Ver documentación de Command
631
- - **ARIA Combobox**: <https://www.w3.org/WAI/ARIA/apg/patterns/combobox/>
1
+ # Combobox
2
+
3
+ Componente de selección avanzado que combina Popover + Command para búsqueda y selección. Soporta selección simple/múltiple, búsqueda filtrable, opciones deshabilitadas, feedback visual con checkbox o check, placeholder persistente, iconos personalizados, y renderizado completamente personalizable mediante render props.
4
+
5
+ ## Importación
6
+
7
+ ```tsx
8
+ import { Combobox } from "@adamosuiteservices/ui/combobox";
9
+ ```
10
+
11
+ ## Anatomía
12
+
13
+ ```tsx
14
+ <Combobox
15
+ options={[
16
+ { value: "option1", label: "Option 1" },
17
+ { value: "option2", label: "Option 2", disabled: true },
18
+ ]}
19
+ value={selectedValue}
20
+ onValueChange={setSelectedValue}
21
+ />
22
+ ```
23
+
24
+ **Componente único**: Internamente usa Popover, Command, Button, Checkbox
25
+
26
+ ## Props Principales
27
+
28
+ | Prop | Tipo | Default | Descripción |
29
+ | ----------------------- | --------------------------------------- | ------------ | ----------------------------------------------------------- |
30
+ | `options` | `ComboboxOption[]` | **required** | Array de opciones `{ value, label, disabled? }` |
31
+ | `value` | `string \| string[]` | - | Valor controlado (string para single, array para multiple) |
32
+ | `onValueChange` | `(value: string \| string[]) => void` | - | Callback al cambiar selección |
33
+ | `searchable` | `boolean` | `false` | Habilita input de búsqueda |
34
+ | `multiple` | `boolean` | `false` | Permite selección múltiple |
35
+ | `selectedFeedback` | `"checkbox" \| "check"` | `"checkbox"` | Tipo de indicador visual |
36
+ | `icon` | `ComponentType<{ className?: string }>` | - | Componente de icono opcional para mostrar en el trigger |
37
+ | `alwaysShowPlaceholder` | `boolean` | `false` | Mantiene el placeholder visible junto al valor seleccionado |
38
+ | `valuePosition` | `"left" \| "right"` | `"right"` | Posición del valor cuando alwaysShowPlaceholder es true |
39
+ | `labels` | `ComboboxLabels` | - | Textos personalizables (ver tabla abajo) |
40
+ | `classNames` | `ComboboxClassNames` | - | Clases CSS por sección (ver tabla abajo) |
41
+ | `renders` | `ComboboxRenderProps` | - | Funciones de renderizado personalizadas (ver tabla abajo) |
42
+
43
+ ### ComboboxLabels
44
+
45
+ | Prop | Tipo | Default | Descripción |
46
+ | ------------------- | --------------------------- | ---------------------- | ----------------------------------------------- |
47
+ | `placeholder` | `string` | `"Select options..."` | Texto cuando no hay selección |
48
+ | `searchPlaceholder` | `string` | `"Search options..."` | Placeholder del input de búsqueda |
49
+ | `noItemsFound` | `string` | `"No options found."` | Mensaje cuando búsqueda no encuentra resultados |
50
+ | `multipleSelected` | `(count: number) => string` | `"X options selected"` | Función para texto de múltiples seleccionados |
51
+
52
+ ### ComboboxClassNames
53
+
54
+ | Prop | Descripción |
55
+ | ---------- | -------------------------------------------------------- |
56
+ | `trigger` | Button trigger del popover |
57
+ | `popover` | Contenedor del popover |
58
+ | `command` | Componente Command root |
59
+ | `input` | Input de búsqueda |
60
+ | `list` | Lista de opciones |
61
+ | `empty` | Mensaje de no resultados |
62
+ | `group` | Grupo de items |
63
+ | `item` | Cada opción individual |
64
+ | `checkbox` | Checkbox de selección (si `selectedFeedback="checkbox"`) |
65
+ | `check` | Icono de check (si `selectedFeedback="check"`) |
66
+
67
+ ### ComboboxRenderProps
68
+
69
+ Funciones de renderizado personalizadas para control total sobre la UI. Todas son opcionales y reciben props relevantes.
70
+
71
+ **Patrón Original Component**: Cada render prop recibe un componente `Original` que renderiza la implementación por defecto, permitiendo envolver o mejorar el renderizado sin reimplementar toda la lógica.
72
+
73
+ | Prop | Parámetros | Descripción |
74
+ | ------------------ | --------------------------------------------------------------------- | --------------------------------------- |
75
+ | `trigger` | `{ Original, open, value, displayText, placeholder, hasValue, icon }` | Renderiza el botón trigger completo |
76
+ | `triggerIcon` | `{ Original, open }` | Renderiza el icono del dropdown |
77
+ | `placeholder` | `{ Original, text, hasValue }` | Renderiza el texto del placeholder |
78
+ | `displayValue` | `{ Original, text, value }` | Renderiza el valor seleccionado |
79
+ | `option` | `{ Original, option, isSelected, selectedFeedback }` | Renderiza una opción completa |
80
+ | `optionLabel` | `{ Original, option }` | Renderiza solo la etiqueta de la opción |
81
+ | `selectedFeedback` | `{ Original, option, isSelected, type }` | Renderiza el indicador de selección |
82
+ | `empty` | `{ Original, text }` | Renderiza el estado vacío |
83
+ | `searchInput` | `{ Original, placeholder }` | Renderiza el input de búsqueda |
84
+
85
+ ## Patrones de Uso
86
+
87
+ ### Básico (Sin Búsqueda)
88
+
89
+ ```tsx
90
+ const frameworks = [
91
+ { value: "next", label: "Next.js" },
92
+ { value: "remix", label: "Remix" },
93
+ { value: "astro", label: "Astro" },
94
+ ];
95
+
96
+ <Combobox
97
+ options={frameworks}
98
+ labels={{ placeholder: "Select framework..." }}
99
+ />;
100
+ ```
101
+
102
+ ### Con Icono
103
+
104
+ ```tsx
105
+ import { CalendarIcon } from "lucide-react";
106
+
107
+ <Combobox
108
+ searchable
109
+ icon={CalendarIcon}
110
+ options={frameworks}
111
+ labels={{ placeholder: "Select framework..." }}
112
+ />;
113
+ ```
114
+
115
+ ### Placeholder Persistente
116
+
117
+ Útil para usar el placeholder como etiqueta que permanece visible.
118
+
119
+ ```tsx
120
+ <Combobox
121
+ searchable
122
+ alwaysShowPlaceholder
123
+ valuePosition="right"
124
+ options={frameworks}
125
+ value={value}
126
+ onValueChange={setValue}
127
+ labels={{ placeholder: "Framework" }}
128
+ />
129
+ ```
130
+
131
+ **Comportamiento**:
132
+
133
+ - Sin valor: Muestra solo "Framework"
134
+ - Con valor y `valuePosition="right"`: Muestra "Framework" a la izquierda y el valor a la derecha
135
+ - Con valor y `valuePosition="left"`: Muestra "Framework" y el valor ambos a la izquierda
136
+
137
+ ### Posición del Valor
138
+
139
+ Cuando `alwaysShowPlaceholder` es true, puedes controlar dónde aparece el valor seleccionado:
140
+
141
+ ```tsx
142
+ // Valor a la derecha (default)
143
+ <Combobox
144
+ alwaysShowPlaceholder
145
+ valuePosition="right"
146
+ options={frameworks}
147
+ />
148
+
149
+ // Valor a la izquierda junto al placeholder
150
+ <Combobox
151
+ alwaysShowPlaceholder
152
+ valuePosition="left"
153
+ options={frameworks}
154
+ />
155
+ ```
156
+
157
+ **Layouts resultantes**:
158
+
159
+ - `valuePosition="right"`: `[Icon] [Placeholder]` ... `[Value] [Chevron]`
160
+ - `valuePosition="left"`: `[Icon] [Placeholder] [Value]` ... `[Chevron]`
161
+
162
+ ### Icono + Placeholder Persistente
163
+
164
+ ```tsx
165
+ <Combobox
166
+ searchable
167
+ alwaysShowPlaceholder
168
+ icon={CalendarIcon}
169
+ options={frameworks}
170
+ value={value}
171
+ onValueChange={setValue}
172
+ labels={{ placeholder: "Framework" }}
173
+ />
174
+ ```
175
+
176
+ ### Con Búsqueda
177
+
178
+ ```tsx
179
+ <Combobox
180
+ searchable
181
+ options={frameworks}
182
+ labels={{
183
+ placeholder: "Select framework...",
184
+ searchPlaceholder: "Search frameworks...",
185
+ noItemsFound: "No framework found.",
186
+ }}
187
+ />
188
+ ```
189
+
190
+ ### Selección Simple Controlada
191
+
192
+ ```tsx
193
+ const [value, setValue] = useState("");
194
+
195
+ <Combobox
196
+ searchable
197
+ options={frameworks}
198
+ value={value}
199
+ onValueChange={(newValue) => setValue(newValue as string)}
200
+ labels={{ placeholder: "Select framework..." }}
201
+ />;
202
+ ```
203
+
204
+ ### Selección Múltiple
205
+
206
+ ```tsx
207
+ const [values, setValues] = useState<string[]>([]);
208
+
209
+ <Combobox
210
+ searchable
211
+ multiple
212
+ options={frameworks}
213
+ value={values}
214
+ onValueChange={(newValues) => setValues(newValues as string[])}
215
+ labels={{
216
+ placeholder: "Select frameworks...",
217
+ multipleSelected: (count) => `${count} frameworks selected`,
218
+ }}
219
+ />;
220
+ ```
221
+
222
+ ### Con Feedback de Check
223
+
224
+ ```tsx
225
+ <Combobox
226
+ searchable
227
+ options={frameworks}
228
+ selectedFeedback="check"
229
+ labels={{ placeholder: "Select option..." }}
230
+ />
231
+ ```
232
+
233
+ **Diferencia**: `"checkbox"` muestra checkboxes a la izquierda, `"check"` muestra ícono check a la derecha.
234
+
235
+ ### Opciones Deshabilitadas
236
+
237
+ ```tsx
238
+ const options = [
239
+ { value: "us", label: "United States" },
240
+ { value: "ca", label: "Canada" },
241
+ { value: "mx", label: "Mexico", disabled: true },
242
+ ];
243
+
244
+ <Combobox
245
+ searchable
246
+ options={options}
247
+ labels={{ placeholder: "Select country..." }}
248
+ />;
249
+ ```
250
+
251
+ ### Labels Personalizadas
252
+
253
+ ```tsx
254
+ <Combobox
255
+ searchable
256
+ multiple
257
+ options={frameworks}
258
+ labels={{
259
+ placeholder: "Choose your stack...",
260
+ searchPlaceholder: "Type to filter...",
261
+ noItemsFound: "No matching technologies found.",
262
+ multipleSelected: (count) =>
263
+ `${count} ${count === 1 ? "tech" : "techs"} selected`,
264
+ }}
265
+ />
266
+ ```
267
+
268
+ ### Styling Personalizado
269
+
270
+ ```tsx
271
+ <Combobox
272
+ searchable
273
+ options={frameworks}
274
+ classNames={{
275
+ trigger: "border-2 border-blue-500 w-64",
276
+ popover: "border-blue-200 shadow-lg",
277
+ item: "rounded px-3 py-2 hover:bg-blue-50",
278
+ check: "text-blue-600",
279
+ }}
280
+ />
281
+ ```
282
+
283
+ ### En Formulario
284
+
285
+ ```tsx
286
+ import { Field, FieldLabel } from "@adamosuiteservices/ui/field";
287
+
288
+ function FormExample() {
289
+ const [framework, setFramework] = useState("");
290
+ const [country, setCountry] = useState("");
291
+
292
+ return (
293
+ <form className="space-y-4">
294
+ <Field>
295
+ <FieldLabel>Framework</FieldLabel>
296
+ <Combobox
297
+ searchable
298
+ options={frameworks}
299
+ value={framework}
300
+ onValueChange={(value) => setFramework(value as string)}
301
+ labels={{ placeholder: "Select framework..." }}
302
+ />
303
+ </Field>
304
+
305
+ <Field>
306
+ <FieldLabel>Country</FieldLabel>
307
+ <Combobox
308
+ searchable
309
+ options={countries}
310
+ value={country}
311
+ onValueChange={(value) => setCountry(value as string)}
312
+ selectedFeedback="check"
313
+ labels={{ placeholder: "Select country..." }}
314
+ />
315
+ </Field>
316
+
317
+ <button type="submit">Submit</button>
318
+ </form>
319
+ );
320
+ }
321
+ ```
322
+
323
+ ### Multiple con Clear All
324
+
325
+ ```tsx
326
+ const [values, setValues] = useState<string[]>([]);
327
+
328
+ <div className="space-y-2">
329
+ <Combobox
330
+ searchable
331
+ multiple
332
+ options={frameworks}
333
+ value={values}
334
+ onValueChange={(newValues) => setValues(newValues as string[])}
335
+ />
336
+
337
+ {values.length > 0 && (
338
+ <button
339
+ onClick={() => setValues([])}
340
+ className="text-xs text-blue-600 hover:text-blue-800 underline"
341
+ >
342
+ Clear all
343
+ </button>
344
+ )}
345
+ </div>;
346
+ ```
347
+
348
+ ## Renderizado Personalizado
349
+
350
+ El prop `renders` permite personalizar completamente la UI usando el patrón render prop. Todas las funciones son opcionales.
351
+
352
+ ### Patrón Original Component
353
+
354
+ Cada render prop recibe un componente `Original` que renderiza la implementación por defecto. Esto permite:
355
+
356
+ - **Envolver**: Agregar contenedores o elementos adicionales
357
+ - **Mejorar**: Añadir badges, iconos o decoraciones junto al original
358
+ - **Condicional**: Usar el original como fallback para ciertos estados
359
+ - **Componer**: Mezclar renderizado custom y default según condiciones
360
+
361
+ ### Envolver el Trigger con Label
362
+
363
+ ```tsx
364
+ <Combobox
365
+ searchable
366
+ options={frameworks}
367
+ value={value}
368
+ onValueChange={setValue}
369
+ renders={{
370
+ trigger: ({ Original }) => (
371
+ <div className="border border-purple-300 p-2 rounded-lg bg-purple-50">
372
+ <label className="text-xs font-medium text-purple-700 mb-1 block">
373
+ Choose your framework:
374
+ </label>
375
+ <Original />
376
+ </div>
377
+ ),
378
+ }}
379
+ />
380
+ ```
381
+
382
+ ### Mejorar Display Value con Badge
383
+
384
+ ```tsx
385
+ <Combobox
386
+ searchable
387
+ options={frameworks}
388
+ value={value}
389
+ onValueChange={setValue}
390
+ renders={{
391
+ displayValue: ({ Original, text }) => (
392
+ <div className="flex items-center gap-2">
393
+ <Original />
394
+ {text && (
395
+ <span className="px-1.5 py-0.5 text-xs bg-green-100 text-green-700 rounded">
396
+ Active
397
+ </span>
398
+ )}
399
+ </div>
400
+ ),
401
+ }}
402
+ />
403
+ ```
404
+
405
+ ### Fallback Condicional
406
+
407
+ ```tsx
408
+ <Combobox
409
+ searchable
410
+ options={frameworks}
411
+ value={value}
412
+ onValueChange={setValue}
413
+ renders={{
414
+ trigger: ({ Original, hasValue, displayText }) => {
415
+ // Custom cuando hay valor
416
+ if (hasValue) {
417
+ return (
418
+ <button className="w-full px-4 py-2 bg-green-600 text-white rounded-md">
419
+ {displayText}
420
+ </button>
421
+ );
422
+ }
423
+
424
+ // Fallback al original cuando está vacío
425
+ return <Original />;
426
+ },
427
+ }}
428
+ />
429
+ ```
430
+
431
+ ### Envolver Options con Styling
432
+
433
+ ```tsx
434
+ <Combobox
435
+ searchable
436
+ options={frameworks}
437
+ value={value}
438
+ onValueChange={setValue}
439
+ selectedFeedback="check"
440
+ renders={{
441
+ option: ({ Original, isSelected }) => (
442
+ <div
443
+ className={`border-l-4 ${
444
+ isSelected ? "border-l-blue-500 bg-blue-50" : "border-l-transparent"
445
+ }`}
446
+ >
447
+ <Original />
448
+ </div>
449
+ ),
450
+ }}
451
+ />
452
+ ```
453
+
454
+ ### Mejorar Empty State con Acción
455
+
456
+ ```tsx
457
+ <Combobox
458
+ searchable
459
+ options={[]}
460
+ renders={{
461
+ empty: ({ Original }) => (
462
+ <div className="space-y-3">
463
+ <Original />
464
+ <div className="px-4 pb-3">
465
+ <button
466
+ onClick={() => console.log("Add new")}
467
+ className="w-full px-3 py-1.5 text-xs text-blue-600 border border-blue-200 rounded hover:bg-blue-50"
468
+ >
469
+ + Create new item
470
+ </button>
471
+ </div>
472
+ </div>
473
+ ),
474
+ }}
475
+ />
476
+ ```
477
+
478
+ ### Custom Option Label (Sin Original)
479
+
480
+ ```tsx
481
+ <Combobox
482
+ searchable
483
+ options={frameworks}
484
+ value={value}
485
+ onValueChange={setValue}
486
+ renders={{
487
+ optionLabel: ({ option }) => (
488
+ <div className="flex items-center gap-2">
489
+ <span className="font-semibold text-blue-600">🚀</span>
490
+ <span>{option.label}</span>
491
+ </div>
492
+ ),
493
+ }}
494
+ />
495
+ ```
496
+
497
+ ### Custom Trigger Completo (Sin Original)
498
+
499
+ ```tsx
500
+ <Combobox
501
+ searchable
502
+ options={frameworks}
503
+ value={value}
504
+ onValueChange={setValue}
505
+ renders={{
506
+ trigger: ({ open, displayText, placeholder, hasValue }) => (
507
+ <button
508
+ type="button"
509
+ className="flex items-center justify-between w-full px-4 py-2 text-sm bg-gradient-to-r from-purple-500 to-pink-500 text-white rounded-lg shadow-md hover:shadow-lg transition-all"
510
+ >
511
+ <span>{hasValue ? displayText : placeholder}</span>
512
+ <span className={`transition-transform ${open ? "rotate-180" : ""}`}>
513
+
514
+ </span>
515
+ </button>
516
+ ),
517
+ }}
518
+ />
519
+ ```
520
+
521
+ ### Combinando Original y Custom
522
+
523
+ ```tsx
524
+ <Combobox
525
+ searchable
526
+ multiple
527
+ options={frameworks}
528
+ value={values}
529
+ onValueChange={setValues}
530
+ renders={{
531
+ // Usa Original para envolver
532
+ trigger: ({ Original }) => (
533
+ <div className="relative">
534
+ <Original />
535
+ {values.length > 0 && (
536
+ <span className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
537
+ {values.length}
538
+ </span>
539
+ )}
540
+ </div>
541
+ ),
542
+ // Custom completo sin Original
543
+ optionLabel: ({ option }) => (
544
+ <div className="flex items-center gap-2">
545
+ <span>🚀</span>
546
+ <span className="font-medium">{option.label}</span>
547
+ </div>
548
+ ),
549
+ // Mejora el Original
550
+ displayValue: ({ Original, text }) => (
551
+ <div className="flex items-center gap-2">
552
+ <Original />
553
+ {text && <span className="text-xs opacity-50">selected</span>}
554
+ </div>
555
+ ),
556
+ }}
557
+ />
558
+ ```
559
+
560
+ ## Modos de Operación
561
+
562
+ ### Single Selection
563
+
564
+ - `multiple={false}` (default)
565
+ - `value` es `string`
566
+ - Click en opción cierra el popover automáticamente
567
+ - Seleccionar opción ya seleccionada la deselecciona
568
+
569
+ ### Multiple Selection
570
+
571
+ - `multiple={true}`
572
+ - `value` es `string[]`
573
+ - Click en opción NO cierra el popover
574
+ - Checkboxes permiten toggle de múltiples opciones
575
+ - Display muestra contador cuando >1 seleccionado
576
+
577
+ ## Casos de Uso Comunes
578
+
579
+ **Framework selector**: Selección de tecnología con búsqueda
580
+ **Country/City picker**: Selección geográfica con opciones deshabilitadas
581
+ **Tags/Categories**: Selección múltiple de categorías
582
+ **Settings filters**: Filtros multi-selección en dashboards
583
+ **User assignment**: Asignar múltiples usuarios a tarea
584
+ **Language selector**: Selección de idioma con búsqueda
585
+ **Status selector con icono**: Usar `icon` + `alwaysShowPlaceholder` para selectores etiquetados
586
+ **Rich options**: Usar `renders.optionLabel` para mostrar avatares, badges, etc.
587
+ **Custom UI**: Usar `renders.trigger` para triggers completamente personalizados
588
+
589
+ ## Estados Internos
590
+
591
+ ### Display Text
592
+
593
+ - **Sin selección**: Muestra `labels.placeholder`
594
+ - **Single selection**: Muestra `label` de la opción seleccionada
595
+ - **Single selection + alwaysShowPlaceholder**: Muestra `placeholder: label` separados
596
+ - **Multiple (1 item)**: Muestra `label` del único item seleccionado
597
+ - **Multiple (1 item) + alwaysShowPlaceholder**: Muestra `placeholder: label` separados
598
+ - **Multiple (>1 items)**: Muestra resultado de `labels.multipleSelected(count)`
599
+ - **Multiple (>1 items) + alwaysShowPlaceholder**: Muestra `placeholder: X items selected` separados
600
+
601
+ ### Popover State
602
+
603
+ - Controlado internamente con `useState`
604
+ - Se cierra automáticamente en single selection
605
+ - Permanece abierto en multiple selection
606
+
607
+ ## Accesibilidad
608
+
609
+ - **ARIA**: Button tiene `role="combobox"` y `aria-expanded`
610
+ - **Navegación teclado**: Arrow keys para navegar opciones, Enter para seleccionar, Escape para cerrar
611
+ - **Screen readers**: Anuncia opciones y estado seleccionado
612
+ - ✅ **Focus management**: Focus automático en input cuando searchable
613
+ - ✅ **Disabled options**: No seleccionables, estilo visual diferente
614
+
615
+ ## Notas de Implementación
616
+
617
+ - **Composición**: Usa Popover + Command + Button + Checkbox internamente
618
+ - **Búsqueda**: Command component maneja fuzzy search automáticamente
619
+ - **Estado interno**: Soporta modo controlado y no controlado
620
+ - **Tipo de value**: Cambia según `multiple` (string vs string[])
621
+ - **selectedFeedback**:
622
+ - `"checkbox"`: Muestra Checkbox a la izquierda de cada opción
623
+ - `"check"`: Muestra CheckIcon a la derecha (agrega `pr-8` automáticamente)
624
+ - **Popover alignment**: `align="start"` por defecto para alinear con trigger
625
+ - **ChevronDownIcon**: Se muestra en trigger con `opacity-50` (puede sobrescribirse con `renders.triggerIcon`)
626
+ - **No animation**: Selección instantánea, sin delays
627
+ - **alwaysShowPlaceholder**: Renderiza placeholder y valor como elementos separados con gap
628
+ - **valuePosition**: Controla donde aparece el valor cuando `alwaysShowPlaceholder` es true ("left" o "right", default "right")
629
+ - **icon**: Se muestra con `opacity-50` por defecto cuando se proporciona el prop `icon`
630
+ - **Render props**: Tienen prioridad sobre renderizado por defecto, permiten personalización completa
631
+ - **Composición de renders**: Puedes combinar múltiples render props para personalización granular
632
+ - **Original component**: Cada render prop recibe un componente `Original` para renderizar la implementación por defecto, permitiendo envolver o mejorar sin reimplementar
633
+
634
+ ## Troubleshooting
635
+
636
+ **Value no cambia**: Verifica que uses `onValueChange` en modo controlado
637
+ **Búsqueda no funciona**: Asegúrate de pasar `searchable={true}`
638
+ **Multiple selection cierra popover**: Verifica que `multiple={true}` esté activo
639
+ **Labels incorrectas en multiple**: Usa función `multipleSelected` para pluralización
640
+ **Tipo de value incorrecto**: Single usa `string`, Multiple usa `string[]`
641
+ **Checkboxes no aparecen**: Solo aparecen cuando `selectedFeedback="checkbox"` (default)
642
+ **Check icon no visible**: Solo aparece cuando `selectedFeedback="check"`
643
+ **Icon no se muestra**: Asegúrate de pasar el prop `icon` con un componente válido
644
+ **Placeholder desaparece al seleccionar**: Si quieres mantenerlo visible, usa `alwaysShowPlaceholder={true}`
645
+ **Render prop no funciona**: Verifica que el render prop retorne un `ReactNode` válido
646
+ **Custom trigger pierde funcionalidad**: Cuando usas `renders.trigger`, debes manejar el click manualmente o envolver en `PopoverTrigger`
647
+ **Original component no renderiza**: Asegúrate de usar `<Original />` como componente JSX, no como función
648
+ **Quiero envolver sin reimplementar**: Usa el componente `Original` que recibe cada render prop para mantener la funcionalidad por defecto
649
+
650
+ ## Referencias
651
+
652
+ - **Radix UI Popover**: <https://www.radix-ui.com/primitives/docs/components/popover>
653
+ - **Command Component**: Ver documentación de Command
654
+ - **ARIA Combobox**: <https://www.w3.org/WAI/ARIA/apg/patterns/combobox/>