@adamosuiteservices/ui 2.11.15 → 2.11.17
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.
- package/dist/colors.css +1 -1
- package/dist/styles.css +1 -1
- package/dist/themes.css +1 -1
- package/docs/AI-GUIDE.md +321 -321
- package/docs/components/layout/sidebar.md +399 -399
- package/docs/components/layout/toaster.md +436 -436
- package/docs/components/ui/accordion-rounded.md +584 -584
- package/docs/components/ui/accordion.md +269 -269
- package/docs/components/ui/button-group.md +984 -984
- package/docs/components/ui/button.md +1137 -1137
- package/docs/components/ui/calendar.md +1159 -1159
- package/docs/components/ui/card.md +1455 -1455
- package/docs/components/ui/checkbox.md +292 -292
- package/docs/components/ui/collapsible.md +323 -323
- package/docs/components/ui/command.md +454 -454
- package/docs/components/ui/context-menu.md +540 -540
- package/docs/components/ui/dialog.md +628 -628
- package/docs/components/ui/dropdown-menu.md +709 -709
- package/docs/components/ui/field.md +706 -706
- package/docs/components/ui/hover-card.md +446 -446
- package/docs/components/ui/input.md +362 -362
- package/docs/components/ui/kbd.md +434 -434
- package/docs/components/ui/label.md +359 -359
- package/docs/components/ui/pagination.md +650 -650
- package/docs/components/ui/popover.md +536 -536
- package/docs/components/ui/progress.md +182 -182
- package/docs/components/ui/radio-group.md +311 -311
- package/docs/components/ui/select.md +352 -352
- package/docs/components/ui/separator.md +214 -214
- package/docs/components/ui/sheet.md +142 -142
- package/docs/components/ui/skeleton.md +140 -140
- package/docs/components/ui/slider.md +341 -341
- package/docs/components/ui/spinner.md +170 -170
- package/docs/components/ui/switch.md +408 -408
- package/docs/components/ui/tabs-underline.md +106 -106
- package/docs/components/ui/tabs.md +122 -122
- package/docs/components/ui/textarea.md +243 -243
- package/docs/components/ui/toggle.md +237 -237
- package/docs/components/ui/tooltip.md +317 -317
- package/docs/components/ui/typography.md +280 -280
- package/package.json +1 -1
|
@@ -1,292 +1,292 @@
|
|
|
1
|
-
# Checkbox
|
|
2
|
-
|
|
3
|
-
Checkbox basado en Radix UI con 3 estados (checked, unchecked, indeterminate), validación con `aria-invalid`, focus ring, y estado disabled.
|
|
4
|
-
|
|
5
|
-
## Importación
|
|
6
|
-
|
|
7
|
-
```tsx
|
|
8
|
-
import { Checkbox } from "@adamosuiteservices/ui/checkbox";
|
|
9
|
-
import { Label } from "@adamosuiteservices/ui/label";
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Anatomía
|
|
13
|
-
|
|
14
|
-
```tsx
|
|
15
|
-
// Componente único, sin subcomponentes
|
|
16
|
-
<Checkbox />
|
|
17
|
-
|
|
18
|
-
// Con label (patrón recomendado)
|
|
19
|
-
<div className="flex items-center gap-3">
|
|
20
|
-
<Checkbox id="terms" />
|
|
21
|
-
<Label htmlFor="terms">Accept terms</Label>
|
|
22
|
-
</div>
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Props Principales
|
|
26
|
-
|
|
27
|
-
| Prop | Tipo | Default | Descripción |
|
|
28
|
-
| ----------------- | ----------------------------------------------- | ------- | -------------------------------------------- |
|
|
29
|
-
| `checked` | `boolean \| "indeterminate"` | - | Estado controlado (true/false/indeterminate) |
|
|
30
|
-
| `defaultChecked` | `boolean` | `false` | Estado inicial no controlado |
|
|
31
|
-
| `onCheckedChange` | `(checked: boolean \| "indeterminate") => void` | - | Callback al cambiar estado |
|
|
32
|
-
| `disabled` | `boolean` | `false` | Desactiva interacción |
|
|
33
|
-
| `required` | `boolean` | `false` | Campo requerido (HTML5) |
|
|
34
|
-
| `name` | `string` | - | Nombre para formularios |
|
|
35
|
-
| `value` | `string` | `"on"` | Valor para formularios |
|
|
36
|
-
| `aria-invalid` | `boolean` | `false` | Muestra estado error (border rojo + ring) |
|
|
37
|
-
| `className` | `string` | - | Clases CSS adicionales |
|
|
38
|
-
|
|
39
|
-
## Estados del Checkbox
|
|
40
|
-
|
|
41
|
-
### 1. Unchecked (Default)
|
|
42
|
-
|
|
43
|
-
```tsx
|
|
44
|
-
<Checkbox />
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### 2. Checked
|
|
48
|
-
|
|
49
|
-
```tsx
|
|
50
|
-
// No controlado
|
|
51
|
-
<Checkbox defaultChecked />;
|
|
52
|
-
|
|
53
|
-
// Controlado
|
|
54
|
-
const [checked, setChecked] = useState(true);
|
|
55
|
-
<Checkbox checked={checked} onCheckedChange={setChecked} />;
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### 3. Indeterminate
|
|
59
|
-
|
|
60
|
-
Estado especial para "select all" cuando algunos items están seleccionados.
|
|
61
|
-
|
|
62
|
-
```tsx
|
|
63
|
-
<Checkbox checked="indeterminate" />;
|
|
64
|
-
|
|
65
|
-
// Ejemplo práctico
|
|
66
|
-
const allChecked = items.every(Boolean);
|
|
67
|
-
const someChecked = items.some(Boolean) && !allChecked;
|
|
68
|
-
|
|
69
|
-
<Checkbox
|
|
70
|
-
checked={allChecked ? true : someChecked ? "indeterminate" : false}
|
|
71
|
-
onCheckedChange={(checked) => setItems(items.map(() => checked === true))}
|
|
72
|
-
/>;
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
**Visual**: Muestra un icono de guión (`MinusIcon`) en lugar de check.
|
|
76
|
-
|
|
77
|
-
### 4. Disabled
|
|
78
|
-
|
|
79
|
-
```tsx
|
|
80
|
-
<Checkbox disabled />
|
|
81
|
-
<Checkbox disabled defaultChecked />
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**Estilos**: `opacity-50` + `cursor-not-allowed`
|
|
85
|
-
|
|
86
|
-
### 5. Invalid (Error State)
|
|
87
|
-
|
|
88
|
-
```tsx
|
|
89
|
-
<Checkbox aria-invalid />
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**Estilos**: Border rojo (`border-destructive`) + ring rojo (`ring-destructive/20`)
|
|
93
|
-
|
|
94
|
-
## Patrones de Uso
|
|
95
|
-
|
|
96
|
-
### Con Label Simple
|
|
97
|
-
|
|
98
|
-
```tsx
|
|
99
|
-
<div className="flex items-center gap-3">
|
|
100
|
-
<Checkbox id="notifications" />
|
|
101
|
-
<Label htmlFor="notifications">Enable notifications</Label>
|
|
102
|
-
</div>
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### Con Label y Descripción
|
|
106
|
-
|
|
107
|
-
```tsx
|
|
108
|
-
<div className="flex items-start gap-3">
|
|
109
|
-
<Checkbox id="marketing" />
|
|
110
|
-
<div className="grid gap-1">
|
|
111
|
-
<Label htmlFor="marketing">Marketing emails</Label>
|
|
112
|
-
<p className="text-sm text-muted-foreground">
|
|
113
|
-
Receive updates about new products
|
|
114
|
-
</p>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### Lista de Opciones
|
|
120
|
-
|
|
121
|
-
```tsx
|
|
122
|
-
const options = ["Email", "SMS", "Push"];
|
|
123
|
-
|
|
124
|
-
<div className="space-y-4">
|
|
125
|
-
{options.map((option) => (
|
|
126
|
-
<div key={option} className="flex items-center gap-3">
|
|
127
|
-
<Checkbox id={option.toLowerCase()} />
|
|
128
|
-
<Label htmlFor={option.toLowerCase()}>{option} notifications</Label>
|
|
129
|
-
</div>
|
|
130
|
-
))}
|
|
131
|
-
</div>;
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### Select All con Indeterminate
|
|
135
|
-
|
|
136
|
-
```tsx
|
|
137
|
-
const [items, setItems] = useState([false, true, false]);
|
|
138
|
-
const allChecked = items.every(Boolean);
|
|
139
|
-
const isIndeterminate = items.some(Boolean) && !allChecked;
|
|
140
|
-
|
|
141
|
-
<div className="space-y-4">
|
|
142
|
-
{/* Parent checkbox */}
|
|
143
|
-
<div className="flex items-center gap-3">
|
|
144
|
-
<Checkbox
|
|
145
|
-
checked={allChecked ? true : isIndeterminate ? "indeterminate" : false}
|
|
146
|
-
onCheckedChange={(checked) => setItems(items.map(() => checked === true))}
|
|
147
|
-
/>
|
|
148
|
-
<Label className="font-medium">Select all</Label>
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
{/* Child checkboxes */}
|
|
152
|
-
<div className="ml-6 space-y-3">
|
|
153
|
-
{items.map((checked, i) => (
|
|
154
|
-
<div key={i} className="flex items-center gap-3">
|
|
155
|
-
<Checkbox
|
|
156
|
-
checked={checked}
|
|
157
|
-
onCheckedChange={(newChecked) => {
|
|
158
|
-
const newItems = [...items];
|
|
159
|
-
newItems[i] = newChecked as boolean;
|
|
160
|
-
setItems(newItems);
|
|
161
|
-
}}
|
|
162
|
-
/>
|
|
163
|
-
<Label>Item {i + 1}</Label>
|
|
164
|
-
</div>
|
|
165
|
-
))}
|
|
166
|
-
</div>
|
|
167
|
-
</div>;
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Card/Button Style
|
|
171
|
-
|
|
172
|
-
Para checkboxes clickeables como botones.
|
|
173
|
-
|
|
174
|
-
```tsx
|
|
175
|
-
<Label className="flex items-start gap-3 rounded-lg border p-4 cursor-pointer hover:bg-accent/50 has-aria-checked:border-primary has-aria-checked:bg-primary/5 transition-colors">
|
|
176
|
-
<Checkbox id="card-option" />
|
|
177
|
-
<div className="grid gap-1.5">
|
|
178
|
-
<p className="text-sm font-medium">Enable feature</p>
|
|
179
|
-
<p className="text-sm text-muted-foreground">Description</p>
|
|
180
|
-
</div>
|
|
181
|
-
</Label>
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
**Nota**: `has-aria-checked` detecta cuando el checkbox está checked para estilos.
|
|
185
|
-
|
|
186
|
-
### Formulario de Preferencias
|
|
187
|
-
|
|
188
|
-
```tsx
|
|
189
|
-
function PreferencesForm() {
|
|
190
|
-
const [prefs, setPrefs] = useState({
|
|
191
|
-
marketing: false,
|
|
192
|
-
analytics: true,
|
|
193
|
-
social: false,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return (
|
|
197
|
-
<form className="space-y-4">
|
|
198
|
-
{Object.entries(prefs).map(([key, value]) => (
|
|
199
|
-
<div key={key} className="flex items-center gap-3">
|
|
200
|
-
<Checkbox
|
|
201
|
-
id={key}
|
|
202
|
-
checked={value}
|
|
203
|
-
onCheckedChange={(checked) =>
|
|
204
|
-
setPrefs((prev) => ({ ...prev, [key]: checked as boolean }))
|
|
205
|
-
}
|
|
206
|
-
/>
|
|
207
|
-
<Label htmlFor={key}>{key} preferences</Label>
|
|
208
|
-
</div>
|
|
209
|
-
))}
|
|
210
|
-
<Button type="submit">Save</Button>
|
|
211
|
-
</form>
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
### Terms & Conditions
|
|
217
|
-
|
|
218
|
-
```tsx
|
|
219
|
-
const [agreed, setAgreed] = useState(false);
|
|
220
|
-
|
|
221
|
-
<div className="flex items-start gap-3">
|
|
222
|
-
<Checkbox
|
|
223
|
-
id="terms"
|
|
224
|
-
checked={agreed}
|
|
225
|
-
onCheckedChange={(checked) => setAgreed(checked as boolean)}
|
|
226
|
-
aria-invalid={!agreed}
|
|
227
|
-
/>
|
|
228
|
-
<Label htmlFor="terms" className="text-sm">
|
|
229
|
-
I agree to the{" "}
|
|
230
|
-
<a href="/terms" className="underline">terms and conditions</a>
|
|
231
|
-
</Label>
|
|
232
|
-
</div>
|
|
233
|
-
<Button disabled={!agreed}>Continue</Button>
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
## Estilos Internos (Data Attributes)
|
|
237
|
-
|
|
238
|
-
### Estados Visuales
|
|
239
|
-
|
|
240
|
-
- **Checked**: `data-[state=checked]` → `bg-primary`, `text-primary-foreground`, `border-primary`
|
|
241
|
-
- **Unchecked**: Default → `border-input`, `bg-transparent` (dark: `bg-input/30`)
|
|
242
|
-
- **Indeterminate**: `data-[state=indeterminate]` → Muestra `MinusIcon` en lugar de `CheckIcon`
|
|
243
|
-
- **Focus**: `focus-visible:ring-[3px]`, `focus-visible:ring-ring/50`, `focus-visible:border-ring`
|
|
244
|
-
- **Invalid**: `aria-invalid:border-destructive`, `aria-invalid:ring-destructive/20`
|
|
245
|
-
|
|
246
|
-
### Iconos
|
|
247
|
-
|
|
248
|
-
- **CheckIcon**: Se muestra cuando `state=checked` (oculto con `group-data-[state=indeterminate]:hidden`)
|
|
249
|
-
- **MinusIcon**: Se muestra cuando `state=indeterminate` (oculto con `group-data-[state=checked]:hidden`)
|
|
250
|
-
- Tamaño: `size-3.5`, color blanco (`text-white`) para check, primary para minus
|
|
251
|
-
|
|
252
|
-
## Casos de Uso Comunes
|
|
253
|
-
|
|
254
|
-
**Términos y condiciones**: Single checkbox requerido para continuar
|
|
255
|
-
**Preferencias de notificación**: Múltiples checkboxes independientes
|
|
256
|
-
**Select all**: Checkbox parent con estado indeterminate
|
|
257
|
-
**Filtros**: Checkboxes para filtrar listas/tablas
|
|
258
|
-
**Permisos**: Grant/revoke permissions con checkboxes
|
|
259
|
-
**Multi-select lists**: Selección múltiple en tablas/listas
|
|
260
|
-
|
|
261
|
-
## Accesibilidad
|
|
262
|
-
|
|
263
|
-
- ✅ **Navegación teclado**: Space para toggle, Tab para navegar
|
|
264
|
-
- ✅ **ARIA**: `role="checkbox"`, `aria-checked="true|false|mixed"` (mixed = indeterminate)
|
|
265
|
-
- ✅ **Labels**: Siempre usar `<Label htmlFor="id">` para clickear label
|
|
266
|
-
- ✅ **Focus visible**: Ring azul en focus con teclado
|
|
267
|
-
- ✅ **Screen readers**: Anuncia estado checked/unchecked/indeterminate
|
|
268
|
-
- ✅ **Peer class**: Permite estilos CSS basados en estado para labels
|
|
269
|
-
|
|
270
|
-
## Notas de Implementación
|
|
271
|
-
|
|
272
|
-
- **Basado en Radix UI**: `@radix-ui/react-checkbox`
|
|
273
|
-
- **Tamaño**: `size-5` (20px × 20px), no tiene variantes de tamaño
|
|
274
|
-
- **Border radius**: `rounded-[6px]` fijo
|
|
275
|
-
- **Shadow**: `shadow-xs` sutil
|
|
276
|
-
- **Transition**: `transition-shadow` solo en shadow (no en background para performance)
|
|
277
|
-
- **Peer utility**: Tiene clase `peer` para estilos con `peer-*:` en hermanos
|
|
278
|
-
- **Data slots**: `data-slot="checkbox"` y `data-slot="checkbox-indicator"`
|
|
279
|
-
- **Form integration**: Soporta `name` y `value` para formularios nativos
|
|
280
|
-
- **RTL ready**: Funciona correctamente en right-to-left
|
|
281
|
-
|
|
282
|
-
## Troubleshooting
|
|
283
|
-
|
|
284
|
-
**Label no clickeable**: Asegúrate de usar `id` en Checkbox y `htmlFor` en Label
|
|
285
|
-
**Estado no cambia**: Verifica que uses `onCheckedChange` en modo controlado
|
|
286
|
-
**Indeterminate no se ve**: El valor debe ser string `"indeterminate"`, no boolean
|
|
287
|
-
**Focus ring no aparece**: Verifica que no haya `outline-none` sin `focus-visible:ring`
|
|
288
|
-
|
|
289
|
-
## Referencias
|
|
290
|
-
|
|
291
|
-
- **Radix UI Checkbox**: https://www.radix-ui.com/primitives/docs/components/checkbox
|
|
292
|
-
- **ARIA Checkbox**: https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/
|
|
1
|
+
# Checkbox
|
|
2
|
+
|
|
3
|
+
Checkbox basado en Radix UI con 3 estados (checked, unchecked, indeterminate), validación con `aria-invalid`, focus ring, y estado disabled.
|
|
4
|
+
|
|
5
|
+
## Importación
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { Checkbox } from "@adamosuiteservices/ui/checkbox";
|
|
9
|
+
import { Label } from "@adamosuiteservices/ui/label";
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Anatomía
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
// Componente único, sin subcomponentes
|
|
16
|
+
<Checkbox />
|
|
17
|
+
|
|
18
|
+
// Con label (patrón recomendado)
|
|
19
|
+
<div className="flex items-center gap-3">
|
|
20
|
+
<Checkbox id="terms" />
|
|
21
|
+
<Label htmlFor="terms">Accept terms</Label>
|
|
22
|
+
</div>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Props Principales
|
|
26
|
+
|
|
27
|
+
| Prop | Tipo | Default | Descripción |
|
|
28
|
+
| ----------------- | ----------------------------------------------- | ------- | -------------------------------------------- |
|
|
29
|
+
| `checked` | `boolean \| "indeterminate"` | - | Estado controlado (true/false/indeterminate) |
|
|
30
|
+
| `defaultChecked` | `boolean` | `false` | Estado inicial no controlado |
|
|
31
|
+
| `onCheckedChange` | `(checked: boolean \| "indeterminate") => void` | - | Callback al cambiar estado |
|
|
32
|
+
| `disabled` | `boolean` | `false` | Desactiva interacción |
|
|
33
|
+
| `required` | `boolean` | `false` | Campo requerido (HTML5) |
|
|
34
|
+
| `name` | `string` | - | Nombre para formularios |
|
|
35
|
+
| `value` | `string` | `"on"` | Valor para formularios |
|
|
36
|
+
| `aria-invalid` | `boolean` | `false` | Muestra estado error (border rojo + ring) |
|
|
37
|
+
| `className` | `string` | - | Clases CSS adicionales |
|
|
38
|
+
|
|
39
|
+
## Estados del Checkbox
|
|
40
|
+
|
|
41
|
+
### 1. Unchecked (Default)
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
<Checkbox />
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Checked
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// No controlado
|
|
51
|
+
<Checkbox defaultChecked />;
|
|
52
|
+
|
|
53
|
+
// Controlado
|
|
54
|
+
const [checked, setChecked] = useState(true);
|
|
55
|
+
<Checkbox checked={checked} onCheckedChange={setChecked} />;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Indeterminate
|
|
59
|
+
|
|
60
|
+
Estado especial para "select all" cuando algunos items están seleccionados.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<Checkbox checked="indeterminate" />;
|
|
64
|
+
|
|
65
|
+
// Ejemplo práctico
|
|
66
|
+
const allChecked = items.every(Boolean);
|
|
67
|
+
const someChecked = items.some(Boolean) && !allChecked;
|
|
68
|
+
|
|
69
|
+
<Checkbox
|
|
70
|
+
checked={allChecked ? true : someChecked ? "indeterminate" : false}
|
|
71
|
+
onCheckedChange={(checked) => setItems(items.map(() => checked === true))}
|
|
72
|
+
/>;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Visual**: Muestra un icono de guión (`MinusIcon`) en lugar de check.
|
|
76
|
+
|
|
77
|
+
### 4. Disabled
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
<Checkbox disabled />
|
|
81
|
+
<Checkbox disabled defaultChecked />
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Estilos**: `opacity-50` + `cursor-not-allowed`
|
|
85
|
+
|
|
86
|
+
### 5. Invalid (Error State)
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
<Checkbox aria-invalid />
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Estilos**: Border rojo (`border-destructive`) + ring rojo (`ring-destructive/20`)
|
|
93
|
+
|
|
94
|
+
## Patrones de Uso
|
|
95
|
+
|
|
96
|
+
### Con Label Simple
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<div className="flex items-center gap-3">
|
|
100
|
+
<Checkbox id="notifications" />
|
|
101
|
+
<Label htmlFor="notifications">Enable notifications</Label>
|
|
102
|
+
</div>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Con Label y Descripción
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
<div className="flex items-start gap-3">
|
|
109
|
+
<Checkbox id="marketing" />
|
|
110
|
+
<div className="grid gap-1">
|
|
111
|
+
<Label htmlFor="marketing">Marketing emails</Label>
|
|
112
|
+
<p className="text-sm text-muted-foreground">
|
|
113
|
+
Receive updates about new products
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Lista de Opciones
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
const options = ["Email", "SMS", "Push"];
|
|
123
|
+
|
|
124
|
+
<div className="space-y-4">
|
|
125
|
+
{options.map((option) => (
|
|
126
|
+
<div key={option} className="flex items-center gap-3">
|
|
127
|
+
<Checkbox id={option.toLowerCase()} />
|
|
128
|
+
<Label htmlFor={option.toLowerCase()}>{option} notifications</Label>
|
|
129
|
+
</div>
|
|
130
|
+
))}
|
|
131
|
+
</div>;
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Select All con Indeterminate
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
const [items, setItems] = useState([false, true, false]);
|
|
138
|
+
const allChecked = items.every(Boolean);
|
|
139
|
+
const isIndeterminate = items.some(Boolean) && !allChecked;
|
|
140
|
+
|
|
141
|
+
<div className="space-y-4">
|
|
142
|
+
{/* Parent checkbox */}
|
|
143
|
+
<div className="flex items-center gap-3">
|
|
144
|
+
<Checkbox
|
|
145
|
+
checked={allChecked ? true : isIndeterminate ? "indeterminate" : false}
|
|
146
|
+
onCheckedChange={(checked) => setItems(items.map(() => checked === true))}
|
|
147
|
+
/>
|
|
148
|
+
<Label className="font-medium">Select all</Label>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Child checkboxes */}
|
|
152
|
+
<div className="ml-6 space-y-3">
|
|
153
|
+
{items.map((checked, i) => (
|
|
154
|
+
<div key={i} className="flex items-center gap-3">
|
|
155
|
+
<Checkbox
|
|
156
|
+
checked={checked}
|
|
157
|
+
onCheckedChange={(newChecked) => {
|
|
158
|
+
const newItems = [...items];
|
|
159
|
+
newItems[i] = newChecked as boolean;
|
|
160
|
+
setItems(newItems);
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
<Label>Item {i + 1}</Label>
|
|
164
|
+
</div>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
</div>;
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Card/Button Style
|
|
171
|
+
|
|
172
|
+
Para checkboxes clickeables como botones.
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
<Label className="flex items-start gap-3 rounded-lg border p-4 cursor-pointer hover:bg-accent/50 has-aria-checked:border-primary has-aria-checked:bg-primary/5 transition-colors">
|
|
176
|
+
<Checkbox id="card-option" />
|
|
177
|
+
<div className="grid gap-1.5">
|
|
178
|
+
<p className="text-sm font-medium">Enable feature</p>
|
|
179
|
+
<p className="text-sm text-muted-foreground">Description</p>
|
|
180
|
+
</div>
|
|
181
|
+
</Label>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Nota**: `has-aria-checked` detecta cuando el checkbox está checked para estilos.
|
|
185
|
+
|
|
186
|
+
### Formulario de Preferencias
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
function PreferencesForm() {
|
|
190
|
+
const [prefs, setPrefs] = useState({
|
|
191
|
+
marketing: false,
|
|
192
|
+
analytics: true,
|
|
193
|
+
social: false,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<form className="space-y-4">
|
|
198
|
+
{Object.entries(prefs).map(([key, value]) => (
|
|
199
|
+
<div key={key} className="flex items-center gap-3">
|
|
200
|
+
<Checkbox
|
|
201
|
+
id={key}
|
|
202
|
+
checked={value}
|
|
203
|
+
onCheckedChange={(checked) =>
|
|
204
|
+
setPrefs((prev) => ({ ...prev, [key]: checked as boolean }))
|
|
205
|
+
}
|
|
206
|
+
/>
|
|
207
|
+
<Label htmlFor={key}>{key} preferences</Label>
|
|
208
|
+
</div>
|
|
209
|
+
))}
|
|
210
|
+
<Button type="submit">Save</Button>
|
|
211
|
+
</form>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Terms & Conditions
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
const [agreed, setAgreed] = useState(false);
|
|
220
|
+
|
|
221
|
+
<div className="flex items-start gap-3">
|
|
222
|
+
<Checkbox
|
|
223
|
+
id="terms"
|
|
224
|
+
checked={agreed}
|
|
225
|
+
onCheckedChange={(checked) => setAgreed(checked as boolean)}
|
|
226
|
+
aria-invalid={!agreed}
|
|
227
|
+
/>
|
|
228
|
+
<Label htmlFor="terms" className="text-sm">
|
|
229
|
+
I agree to the{" "}
|
|
230
|
+
<a href="/terms" className="underline">terms and conditions</a>
|
|
231
|
+
</Label>
|
|
232
|
+
</div>
|
|
233
|
+
<Button disabled={!agreed}>Continue</Button>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Estilos Internos (Data Attributes)
|
|
237
|
+
|
|
238
|
+
### Estados Visuales
|
|
239
|
+
|
|
240
|
+
- **Checked**: `data-[state=checked]` → `bg-primary`, `text-primary-foreground`, `border-primary`
|
|
241
|
+
- **Unchecked**: Default → `border-input`, `bg-transparent` (dark: `bg-input/30`)
|
|
242
|
+
- **Indeterminate**: `data-[state=indeterminate]` → Muestra `MinusIcon` en lugar de `CheckIcon`
|
|
243
|
+
- **Focus**: `focus-visible:ring-[3px]`, `focus-visible:ring-ring/50`, `focus-visible:border-ring`
|
|
244
|
+
- **Invalid**: `aria-invalid:border-destructive`, `aria-invalid:ring-destructive/20`
|
|
245
|
+
|
|
246
|
+
### Iconos
|
|
247
|
+
|
|
248
|
+
- **CheckIcon**: Se muestra cuando `state=checked` (oculto con `group-data-[state=indeterminate]:hidden`)
|
|
249
|
+
- **MinusIcon**: Se muestra cuando `state=indeterminate` (oculto con `group-data-[state=checked]:hidden`)
|
|
250
|
+
- Tamaño: `size-3.5`, color blanco (`text-white`) para check, primary para minus
|
|
251
|
+
|
|
252
|
+
## Casos de Uso Comunes
|
|
253
|
+
|
|
254
|
+
**Términos y condiciones**: Single checkbox requerido para continuar
|
|
255
|
+
**Preferencias de notificación**: Múltiples checkboxes independientes
|
|
256
|
+
**Select all**: Checkbox parent con estado indeterminate
|
|
257
|
+
**Filtros**: Checkboxes para filtrar listas/tablas
|
|
258
|
+
**Permisos**: Grant/revoke permissions con checkboxes
|
|
259
|
+
**Multi-select lists**: Selección múltiple en tablas/listas
|
|
260
|
+
|
|
261
|
+
## Accesibilidad
|
|
262
|
+
|
|
263
|
+
- ✅ **Navegación teclado**: Space para toggle, Tab para navegar
|
|
264
|
+
- ✅ **ARIA**: `role="checkbox"`, `aria-checked="true|false|mixed"` (mixed = indeterminate)
|
|
265
|
+
- ✅ **Labels**: Siempre usar `<Label htmlFor="id">` para clickear label
|
|
266
|
+
- ✅ **Focus visible**: Ring azul en focus con teclado
|
|
267
|
+
- ✅ **Screen readers**: Anuncia estado checked/unchecked/indeterminate
|
|
268
|
+
- ✅ **Peer class**: Permite estilos CSS basados en estado para labels
|
|
269
|
+
|
|
270
|
+
## Notas de Implementación
|
|
271
|
+
|
|
272
|
+
- **Basado en Radix UI**: `@radix-ui/react-checkbox`
|
|
273
|
+
- **Tamaño**: `size-5` (20px × 20px), no tiene variantes de tamaño
|
|
274
|
+
- **Border radius**: `rounded-[6px]` fijo
|
|
275
|
+
- **Shadow**: `shadow-xs` sutil
|
|
276
|
+
- **Transition**: `transition-shadow` solo en shadow (no en background para performance)
|
|
277
|
+
- **Peer utility**: Tiene clase `peer` para estilos con `peer-*:` en hermanos
|
|
278
|
+
- **Data slots**: `data-slot="checkbox"` y `data-slot="checkbox-indicator"`
|
|
279
|
+
- **Form integration**: Soporta `name` y `value` para formularios nativos
|
|
280
|
+
- **RTL ready**: Funciona correctamente en right-to-left
|
|
281
|
+
|
|
282
|
+
## Troubleshooting
|
|
283
|
+
|
|
284
|
+
**Label no clickeable**: Asegúrate de usar `id` en Checkbox y `htmlFor` en Label
|
|
285
|
+
**Estado no cambia**: Verifica que uses `onCheckedChange` en modo controlado
|
|
286
|
+
**Indeterminate no se ve**: El valor debe ser string `"indeterminate"`, no boolean
|
|
287
|
+
**Focus ring no aparece**: Verifica que no haya `outline-none` sin `focus-visible:ring`
|
|
288
|
+
|
|
289
|
+
## Referencias
|
|
290
|
+
|
|
291
|
+
- **Radix UI Checkbox**: https://www.radix-ui.com/primitives/docs/components/checkbox
|
|
292
|
+
- **ARIA Checkbox**: https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/
|