ywana-core8 0.2.150 → 0.2.151

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.
@@ -0,0 +1,1392 @@
1
+ # Contrato de componentes — ywana-core8
2
+
3
+ > Este documento describe el **contrato de uso público** (props, callbacks, ejemplos) de los
4
+ > componentes exportados por `ywana-core8`. Está pensado para que lo consuman **agentes de IA
5
+ > de otras aplicaciones** que importan esta librería, de forma que puedan generar código
6
+ > correcto sin tener que leer el código fuente del paquete.
7
+ >
8
+ > Todos los componentes se importan desde el paquete raíz:
9
+ > ```jsx
10
+ > import { NombreComponente } from 'ywana-core8'
11
+ > ```
12
+ > No uses rutas internas (`ywana-core8/html/...`, `ywana-core8/widgets/...`, etc.) — la única
13
+ > API soportada es el export raíz.
14
+
15
+ ## Convenciones generales
16
+
17
+ - **Callbacks con id primero**: la mayoría de callbacks siguen el patrón `onChange(id, value, ...)`
18
+ en vez de `onChange(event)`. Revisa la firma exacta en cada componente antes de asumir nada.
19
+ - **Controlado vs no controlado**: cuando un componente indica "Controlado" por una prop (p.ej.
20
+ `value`, `selected`, `checkedFiles`), ese componente **no** gestiona el valor internamente — si
21
+ no actualizas la prop en tu `onChange`, la UI no cambia. Cuando indica "No controlado", el
22
+ componente mantiene su propio estado y solo notifica cambios vía callback.
23
+ - **Iconos**: la prop `icon` (y variantes `menuIcon`, `emptyIcon`, etc.) siempre espera el nombre
24
+ de un [Material Icon](https://fonts.google.com/icons) en formato `snake_case` (ej. `delete`,
25
+ `arrow_back`, `folder_open`), nunca un componente ni una URL.
26
+ - **`id` obligatorio en inputs**: `CheckBox`, `TextField` (y alias), `RadioButton`, `DropDown`,
27
+ `DateRange` requieren `id` — sin él, algunos emiten `console.warn` y otros fallan al asociar
28
+ label/input.
29
+ - **i18n**: varios componentes (`TextField`, `Text`, `Site`/`SiteProvider`) usan internamente un
30
+ `TranslationContext`; si tu app no lo provee, funcionan igual pero sin traducir literales
31
+ especiales.
32
+ - **CSS por variables**: varios widgets (p.ej. `FileExplorer`, densidades de grid) se personalizan
33
+ sobreescribiendo custom properties CSS en vez de props — revisa la doc específica del
34
+ componente si necesitas cambiar tamaños/colores.
35
+ - **Componentes "V2" o "2"**: cuando existen dos versiones (`TextField`/`TextField2`,
36
+ `Header`/`Header2`, `Tree`/`Tree2`, `DataTable`/`DataTable2`/`DataTable3`, `upload`/`upload2`),
37
+ la versión sin sufijo (o la de número más alto) es la recomendada; la otra se mantiene solo por
38
+ compatibilidad hacia atrás y no debe usarse en código nuevo salvo migración.
39
+
40
+ ---
41
+
42
+ ## Índice
43
+
44
+ 1. [HTML — Inputs y controles](#1-html--inputs-y-controles)
45
+ 2. [HTML — Layout, visualización y datos](#2-html--layout-visualización-y-datos)
46
+ 3. [Site — Shell de aplicación](#3-site--shell-de-aplicación)
47
+ 4. [Widgets — Componentes de negocio genéricos](#4-widgets--componentes-de-negocio-genéricos)
48
+
49
+ ---
50
+
51
+ ## 1. HTML — Inputs y controles
52
+
53
+ ### `Button`
54
+ Import: `import { Button } from 'ywana-core8'`
55
+ Story: [src/html/Button.stories.jsx](src/html/Button.stories.jsx)
56
+
57
+ Propósito: Botón accesible con soporte para iconos, estados de carga y múltiples variantes visuales.
58
+
59
+ Props:
60
+ | Prop | Tipo | Default | Requerido | Descripción |
61
+ | --- | --- | --- | --- | --- |
62
+ | id | string | — | No | Identificador único del botón |
63
+ | label | string \| React.Element | — | Sí (o icon o ariaLabel) | Texto o componente del botón |
64
+ | icon | string | — | No | Nombre del icono Material Icon a mostrar |
65
+ | rightIcon | string | — | No | Icono a mostrar en el lado derecho |
66
+ | action | function | — | No | Alias para onClick (callback) |
67
+ | disabled | boolean | false | No | Desactiva el botón |
68
+ | loading | boolean | false | No | Estado de carga (muestra spinner) |
69
+ | outlined | boolean | false | No | Estilo con borde (sin relleno) |
70
+ | raised | boolean | false | No | Estilo elevado (sombra) |
71
+ | size | 'small' \| 'normal' \| 'large' | 'normal' | No | Tamaño del botón |
72
+ | type | 'button' \| 'submit' \| 'reset' | 'button' | No | Tipo HTML del botón |
73
+ | className | string | — | No | Clases CSS adicionales |
74
+ | ariaLabel | string | — | No | Etiqueta ARIA para accesibilidad |
75
+ | tooltip | string | — | No | Texto de tooltip |
76
+ | form | string | — | No | ID del formulario asociado |
77
+
78
+ Callbacks: `onClick(event)`, `action(event)`, `onFocus(event)`, `onBlur(event)`, `onKeyDown(event)`
79
+
80
+ Controlado/No controlado: No controlado (loading/hover internos, responde a callbacks).
81
+
82
+ Ejemplo mínimo:
83
+ ```jsx
84
+ <Button label="Click me" onClick={() => handleClick()} />
85
+ ```
86
+
87
+ Notas: Requiere al menos una de `label`, `icon` o `ariaLabel`.
88
+
89
+ ---
90
+
91
+ ### `ActionButton`
92
+ Import: `import { ActionButton } from 'ywana-core8'`
93
+ Story: [src/html/Button.stories.jsx](src/html/Button.stories.jsx)
94
+
95
+ Propósito: Botón que cambia entre múltiples estados predefinidos y ejecuta acciones asociadas a cada estado.
96
+
97
+ Props:
98
+ | Prop | Tipo | Default | Requerido | Descripción |
99
+ | --- | --- | --- | --- | --- |
100
+ | states | object | — | Sí | Mapa de nombre de estado a `{label, icon, action, autoexec}` |
101
+ | state | string | — | Sí | Estado actual del botón |
102
+ | className | string | — | No | Clases CSS adicionales |
103
+ | disabled | boolean | false | No | Desactiva el botón |
104
+ | onStateChange | function | — | No | Callback cuando cambia el estado |
105
+
106
+ Callbacks: `onStateChange(newState)`
107
+
108
+ Controlado/No controlado: Controlado por `state`.
109
+
110
+ Ejemplo mínimo:
111
+ ```jsx
112
+ <ActionButton
113
+ state="idle"
114
+ states={{ idle: { label: 'Start' }, loading: { label: 'Loading...', autoexec: true } }}
115
+ onStateChange={(state) => setCurrentState(state)}
116
+ />
117
+ ```
118
+
119
+ Notas: `autoexec: true` en la configuración de un estado ejecuta su `action` automáticamente al entrar en ese estado.
120
+
121
+ ---
122
+
123
+ ### `CheckBox`
124
+ Import: `import { CheckBox } from 'ywana-core8'`
125
+ Story: [src/html/Checkbox.stories.jsx](src/html/Checkbox.stories.jsx)
126
+
127
+ Propósito: Casilla de verificación accesible con soporte para estados indeterminado, error y lectura.
128
+
129
+ Props:
130
+ | Prop | Tipo | Default | Requerido | Descripción |
131
+ | --- | --- | --- | --- | --- |
132
+ | id | string | — | **Sí** | Identificador único |
133
+ | label | string | — | No | Etiqueta de texto |
134
+ | value | boolean | false | No | Estado checado actual |
135
+ | readOnly | boolean | false | No | Solo lectura |
136
+ | disabled | boolean | false | No | Deshabilitado |
137
+ | indeterminate | boolean | false | No | Estado indeterminado (símbolo -) |
138
+ | error | boolean \| string | false | No | Estado error; si es string, muestra el mensaje |
139
+ | required | boolean | false | No | Campo requerido |
140
+ | className | string | — | No | Clases CSS adicionales |
141
+ | ariaLabel | string | — | No | Etiqueta ARIA |
142
+
143
+ Callbacks: `onChange(id, checked)`
144
+
145
+ Controlado/No controlado: Controlado por `value`.
146
+
147
+ Ejemplo mínimo:
148
+ ```jsx
149
+ <CheckBox id="agree" label="I agree" value={isChecked} onChange={(id, checked) => setIsChecked(checked)} />
150
+ ```
151
+
152
+ Notas: `indeterminate` es independiente de `value` (no se deriva de él).
153
+
154
+ ---
155
+
156
+ ### `RadioButton`
157
+ Import: `import { RadioButton } from 'ywana-core8'`
158
+ Story: [src/html/Radio.stories.jsx](src/html/Radio.stories.jsx)
159
+
160
+ Propósito: Botón de opción individual con validación y estados de error/lectura.
161
+
162
+ Props:
163
+ | Prop | Tipo | Default | Requerido | Descripción |
164
+ | --- | --- | --- | --- | --- |
165
+ | id | string | — | **Sí** | Identificador único |
166
+ | name | string | — | **Sí** | Atributo `name` para agrupar |
167
+ | label | string | — | No | Etiqueta de texto |
168
+ | value | string \| number | id | No | Valor del radio |
169
+ | checked | boolean | false | No | Si está seleccionado |
170
+ | disabled | boolean | false | No | Deshabilitado |
171
+ | readOnly | boolean | false | No | Solo lectura |
172
+ | error | boolean \| string | false | No | Estado error |
173
+ | required | boolean | false | No | Campo requerido |
174
+
175
+ Callbacks: `onChange(id, value, event)`
176
+
177
+ Controlado/No controlado: Controlado por `checked`.
178
+
179
+ Ejemplo mínimo:
180
+ ```jsx
181
+ <RadioButton id="opt1" name="group" label="Option 1" checked={selected === 'opt1'} onChange={(id, val) => setSelected(val)} />
182
+ ```
183
+
184
+ Notas: Usa `RadioGroup` en vez de instancias sueltas cuando sea posible; simplifica el manejo de `name`.
185
+
186
+ ---
187
+
188
+ ### `RadioGroup`
189
+ Import: `import { RadioGroup } from 'ywana-core8'`
190
+ Story: [src/html/Radio.stories.jsx](src/html/Radio.stories.jsx)
191
+
192
+ Propósito: Grupo de botones de opción con gestión centralizada y sincronización.
193
+
194
+ Props:
195
+ | Prop | Tipo | Default | Requerido | Descripción |
196
+ | --- | --- | --- | --- | --- |
197
+ | id | string | — | No | Identificador único del grupo |
198
+ | label | string | — | No | Etiqueta del grupo |
199
+ | value | string \| number | — | No | Valor seleccionado |
200
+ | options | array | [] | No | `[{id, label, value, disabled, ariaLabel}]` |
201
+ | disabled | boolean | false | No | Deshabilita el grupo |
202
+ | readOnly | boolean | false | No | Solo lectura |
203
+ | error | boolean \| string | false | No | Estado error |
204
+ | required | boolean | false | No | Campo requerido |
205
+
206
+ Callbacks: `onChange(groupId, value, event)`
207
+
208
+ Controlado/No controlado: Controlado por `value`.
209
+
210
+ Ejemplo mínimo:
211
+ ```jsx
212
+ <RadioGroup label="Choose one" value={selected} options={[{ label: 'A' }, { label: 'B' }]} onChange={(id, val) => setSelected(val)} />
213
+ ```
214
+
215
+ Notas: Genera automáticamente `id`/`name` internos para cada `RadioButton` si no se proporcionan.
216
+
217
+ ---
218
+
219
+ ### `Switch`
220
+ Import: `import { Switch } from 'ywana-core8'`
221
+ Story: [src/html/Switch.stories.jsx](src/html/Switch.stories.jsx)
222
+
223
+ Propósito: Control deslizante on/off con soporte para colores personalizados y tamaños.
224
+
225
+ Props:
226
+ | Prop | Tipo | Default | Requerido | Descripción |
227
+ | --- | --- | --- | --- | --- |
228
+ | id | string | — | No | Identificador único |
229
+ | label | string | — | No | Etiqueta de texto |
230
+ | checked | boolean | false | No | Estado actual |
231
+ | disabled | boolean | false | No | Deshabilitado |
232
+ | readOnly | boolean | false | No | Solo lectura |
233
+ | error | boolean \| string | false | No | Estado error |
234
+ | required | boolean | false | No | Campo requerido |
235
+ | size | 'small' \| 'normal' \| 'large' | 'normal' | No | Tamaño |
236
+ | onColor | string | '#007bff' | No | Color cuando está ON |
237
+ | offColor | string | '#ccc' | No | Color cuando está OFF |
238
+
239
+ Callbacks: `onChange(id, checked)`
240
+
241
+ Controlado/No controlado: Controlado por `checked`.
242
+
243
+ Ejemplo mínimo:
244
+ ```jsx
245
+ <Switch id="toggle" label="Enabled" checked={isOn} onChange={(id, val) => setIsOn(val)} />
246
+ ```
247
+
248
+ Notas: Acepta Espacio/Enter por teclado.
249
+
250
+ ---
251
+
252
+ ### `TextField`
253
+ Import: `import { TextField } from 'ywana-core8'`
254
+ Story: [src/html/TextField.stories.jsx](src/html/TextField.stories.jsx)
255
+
256
+ Propósito: Campo de entrada de texto universal con soporte para múltiples tipos, validación y debounce.
257
+
258
+ Props:
259
+ | Prop | Tipo | Default | Requerido | Descripción |
260
+ | --- | --- | --- | --- | --- |
261
+ | id | string | — | **Sí** | Identificador único |
262
+ | type | string | 'text' | No | Tipo HTML: text, email, password, number, tel, url, search, date, time, etc. |
263
+ | label | string \| React.Element | — | No | Etiqueta |
264
+ | labelPosition | 'top' \| 'left' | 'top' | No | Posición de la etiqueta |
265
+ | placeholder | string | — | No | Placeholder |
266
+ | value | string \| number | — | No | Valor actual |
267
+ | outlined | boolean | false | No | Estilo con borde |
268
+ | readOnly / disabled / required | boolean | false | No | Estados estándar |
269
+ | canClear | boolean | true | No | Muestra botón limpiar |
270
+ | showPasswordToggle | boolean | true | No | Toggle mostrar/ocultar (solo type="password") |
271
+ | autoComplete | string | 'off' | No | Atributo HTML autocomplete |
272
+ | error | string | — | No | Mensaje de error |
273
+ | helperText | string | — | No | Texto de ayuda |
274
+ | maxLength / minLength / pattern / step / min / max | — | — | No | Restricciones estándar de input |
275
+ | rows | number | 3 | No | Filas (solo `TextArea`) |
276
+ | validation | function | — | No | `(value) => boolean \| {valid, message}` |
277
+ | debounceMs | number | 0 | No | Debounce de `onChange` en ms |
278
+
279
+ Callbacks: `onChange(id, value, event)`, `onEnter(id, value, event)`, `onKeyDown(id, key, event)`, `onValidation(id, isValid, errorMessage)`, `onFocus(event)`, `onBlur(event)`
280
+
281
+ Controlado/No controlado: Controlado por `value`.
282
+
283
+ Ejemplo mínimo:
284
+ ```jsx
285
+ <TextField id="email" type="email" placeholder="user@example.com" value={email} onChange={(id, val) => setEmail(val)} />
286
+ ```
287
+
288
+ Notas: La validación se evalúa tras la primera interacción del usuario (touched), no en el primer render.
289
+
290
+ ---
291
+
292
+ ### `TextArea`
293
+ Import: `import { TextArea } from 'ywana-core8'`
294
+ Story: [src/html/TextField.stories.jsx](src/html/TextField.stories.jsx)
295
+
296
+ Propósito: Alias de `TextField` con `type="textarea"` para texto multilínea. Mismas props/callbacks que `TextField`, más `rows`.
297
+
298
+ Ejemplo mínimo:
299
+ ```jsx
300
+ <TextArea id="message" placeholder="Enter message" value={text} onChange={(id, val) => setText(val)} rows={5} />
301
+ ```
302
+
303
+ ---
304
+
305
+ ### `PasswordField`
306
+ Import: `import { PasswordField } from 'ywana-core8'`
307
+ Story: [src/html/TextField.stories.jsx](src/html/TextField.stories.jsx)
308
+
309
+ Propósito: Alias de `TextField` con `type="password"` y toggle mostrar/ocultar. Mismas props/callbacks que `TextField`.
310
+
311
+ Ejemplo mínimo:
312
+ ```jsx
313
+ <PasswordField id="pwd" value={password} onChange={(id, val) => setPassword(val)} showPasswordToggle />
314
+ ```
315
+
316
+ ---
317
+
318
+ ### `DropDown`
319
+ Import: `import { DropDown } from 'ywana-core8'`
320
+ Story: [src/html/TextField.stories.jsx](src/html/TextField.stories.jsx)
321
+
322
+ Propósito: Selector desplegable con soporte para búsqueda, selección múltiple, agrupación y renderizado personalizado.
323
+
324
+ Props:
325
+ | Prop | Tipo | Default | Requerido | Descripción |
326
+ | --- | --- | --- | --- | --- |
327
+ | id | string | — | **Sí** | Identificador único |
328
+ | options | array | [] | No | `[{value, label, disabled, ...}]` |
329
+ | value | string \| number \| array | — | No | Seleccionado (array si `multiple`) |
330
+ | placeholder / label | string | — | No | Textos |
331
+ | outlined / disabled / readOnly / required | boolean | false | No | Estados estándar |
332
+ | searchable | boolean | false | No | Búsqueda en opciones |
333
+ | clearable | boolean | false | No | Botón limpiar |
334
+ | multiple | boolean | false | No | Selección múltiple |
335
+ | groupBy | string \| function | — | No | Agrupar opciones |
336
+ | filterFunction | function | — | No | Filtro custom |
337
+ | renderOption / renderValue | function | — | No | Render personalizado |
338
+ | position | 'top' \| 'bottom' | 'bottom' | No | Posición del menú |
339
+ | menuPortal | boolean | false | No | Renderiza el menú en un portal |
340
+ | predictive / verbose / editable | boolean | — | No | Legacy: alias de `searchable` / mostrar label / entrada libre |
341
+
342
+ Callbacks: `onChange(id, value, option)`, `onOpen()`, `onClose()`, `onSearch(searchTerm)`, `onBlur(event)`
343
+
344
+ Controlado/No controlado: Controlado por `value`.
345
+
346
+ Ejemplo mínimo:
347
+ ```jsx
348
+ <DropDown id="select" options={[{ value: 1, label: 'A' }, { value: 2, label: 'B' }]} value={selected} onChange={(id, val) => setSelected(val)} />
349
+ ```
350
+
351
+ ---
352
+
353
+ ### `DateRange`
354
+ Import: `import { DateRange } from 'ywana-core8'`
355
+ Story: [src/html/TextField.stories.jsx](src/html/TextField.stories.jsx)
356
+
357
+ Propósito: Selector de rango de fechas con validación de rango y restricciones min/max.
358
+
359
+ Props:
360
+ | Prop | Tipo | Default | Requerido | Descripción |
361
+ | --- | --- | --- | --- | --- |
362
+ | id | string | — | **Sí** | Identificador único |
363
+ | value | `{from, to}` (ISO strings) | — | No | Rango actual |
364
+ | minDate / maxDate | string (ISO) | — | No | Límites permitidos |
365
+ | required | boolean | false | No | Ambas fechas requeridas |
366
+ | canClear | boolean | true | No | Botón limpiar |
367
+
368
+ Callbacks: `onChange(id, {from, to})`, `onValidation(id, isValid, errorMessage)`
369
+
370
+ Controlado/No controlado: Controlado por `value`.
371
+
372
+ Ejemplo mínimo:
373
+ ```jsx
374
+ <DateRange id="dateRange" value={{ from: '2024-01-01', to: '2024-12-31' }} onChange={(id, range) => setRange(range)} />
375
+ ```
376
+
377
+ Notas: Valida `from < to` y límites `minDate`/`maxDate`. No es error si falta una fecha salvo `required`.
378
+
379
+ ---
380
+
381
+ ### `TokenField`
382
+ Import: `import { TokenField } from 'ywana-core8'`
383
+ Story: [src/html/TokenField.stories.jsx](src/html/TokenField.stories.jsx)
384
+
385
+ Propósito: Campo para entrada de múltiples tokens/etiquetas con validación, separadores y opciones desplegables.
386
+
387
+ Props:
388
+ | Prop | Tipo | Default | Requerido | Descripción |
389
+ | --- | --- | --- | --- | --- |
390
+ | tokens | array | [] | No | Tokens actuales (controlado) |
391
+ | options | array | — | No | Opciones predefinidas para autocompletar |
392
+ | maxTokens / minTokens | number | — / 0 | No | Límites |
393
+ | allowDuplicates | boolean | true | No | Permite tokens duplicados |
394
+ | validateToken | function | — | No | `(token, tokens) => {isValid, error}` |
395
+ | tokenSeparators | array | `[',', ';', '\n']` | No | Caracteres separadores |
396
+ | searchable / sortable / clearable | boolean | false | No | Comportamiento del dropdown |
397
+ | placeholder | string | 'Add token...' | No | Placeholder |
398
+
399
+ Callbacks: `onChange(id, tokens)`, `onTokenAdd(token, index)`, `onTokenRemove(token, index)`, `onValidationError(error, token)`, `onClear()`, `onFocus(event)`, `onBlur(event)`
400
+
401
+ Controlado/No controlado: Controlado por `tokens`.
402
+
403
+ Ejemplo mínimo:
404
+ ```jsx
405
+ <TokenField id="tags" tokens={myTags} onChange={(id, tokens) => setMyTags(tokens)} placeholder="Add tags..." />
406
+ ```
407
+
408
+ Notas: Backspace en input vacío elimina el último token; pegar texto con separadores crea varios tokens de golpe.
409
+
410
+ ---
411
+
412
+ ### `MultiSelector`
413
+ Import: `import { MultiSelector } from 'ywana-core8'`
414
+ Story: [src/html/Selector.stories.jsx](src/html/Selector.stories.jsx)
415
+
416
+ Propósito: Selector visual de múltiples opciones (renderiza `ToggleButton` por opción) con búsqueda y límites de selección.
417
+
418
+ Props:
419
+ | Prop | Tipo | Default | Requerido | Descripción |
420
+ | --- | --- | --- | --- | --- |
421
+ | options | array | [] | No | `[{value, label, selected, disabled}]` |
422
+ | maxSelections / minSelections | number | — / 0 | No | Límites de selección |
423
+ | searchable | boolean | false | No | Búsqueda de opciones |
424
+ | allowClear | boolean | false | No | Botón "Clear All" |
425
+ | loading / empty | boolean | false | No | Estados especiales |
426
+ | emptyMessage | string | 'No options available' | No | Mensaje vacío |
427
+
428
+ Callbacks: `onChange(selectedValues)`, `onClear()`
429
+
430
+ Controlado/No controlado: Semi-controlado — la selección inicial viene de `options[].selected`, pero el toggle se gestiona internamente; `onChange` reporta cada cambio.
431
+
432
+ Ejemplo mínimo:
433
+ ```jsx
434
+ <MultiSelector options={[{ value: 'a', label: 'Option A' }, { value: 'b', label: 'Option B', selected: true }]} onChange={(selections) => console.log(selections)} />
435
+ ```
436
+
437
+ ---
438
+
439
+ ### `ToggleButton`
440
+ Import: `import { ToggleButton } from 'ywana-core8'`
441
+ Story: [src/html/Selector.stories.jsx](src/html/Selector.stories.jsx)
442
+
443
+ Propósito: Botón seleccionable on/off, normalmente usado dentro de `MultiSelector`.
444
+
445
+ Props: `label`, `value`, `selected` (boolean, controlado), `disabled`, `icon`, `variant`, `size`, `tooltip`.
446
+
447
+ Callbacks: `onToggle(value)`
448
+
449
+ Ejemplo mínimo:
450
+ ```jsx
451
+ <ToggleButton label="Option" value="opt1" selected={isSelected} onToggle={(val) => handleToggle(val)} />
452
+ ```
453
+
454
+ ---
455
+
456
+ ### `Slider`
457
+ Import: `import { Slider } from 'ywana-core8'`
458
+ Story: [src/html/slider.stories.jsx](src/html/slider.stories.jsx)
459
+
460
+ Propósito: Control deslizante de rango numérico.
461
+
462
+ Props: `min` (0), `max` (100), `step` (1), `value` (0, controlado), `showValue` (true), `disabled`.
463
+
464
+ Callbacks: `onChange(newValue)`
465
+
466
+ Ejemplo mínimo:
467
+ ```jsx
468
+ <Slider min={0} max={100} value={volume} onChange={(val) => setVolume(val)} />
469
+ ```
470
+
471
+ Notas: No requiere `id`. Incluye un `<input type="range">` oculto para accesibilidad.
472
+
473
+ ---
474
+
475
+ ### `ColorField`
476
+ Import: `import { ColorField } from 'ywana-core8'`
477
+ Story: [src/html/Color.stories.jsx](src/html/Color.stories.jsx)
478
+
479
+ Propósito: Selector de color con soporte para múltiples formatos y colores predefinidos.
480
+
481
+ Props:
482
+ | Prop | Tipo | Default | Requerido | Descripción |
483
+ | --- | --- | --- | --- | --- |
484
+ | value | string | — | No | Color actual (hex/rgb/hsl) |
485
+ | format | 'hex' \| 'rgb' \| 'hsl' | 'hex' | No | Formato de salida |
486
+ | shape | 'rectangle' \| 'circle' | 'rectangle' | No | Forma del picker |
487
+ | allowTransparent | boolean | false | No | Permite `'transparent'` |
488
+ | presetColors | array | [] | No | Colores predefinidos seleccionables |
489
+ | clearable | boolean | false | No | Botón limpiar |
490
+
491
+ Callbacks: `onChange(id, color)`, `onFocus(event)`, `onBlur(event)`, `onClear()`
492
+
493
+ Controlado/No controlado: Controlado por `value`.
494
+
495
+ Ejemplo mínimo:
496
+ ```jsx
497
+ <ColorField id="bgColor" value="#ff0000" onChange={(id, color) => setColor(color)} />
498
+ ```
499
+
500
+ ---
501
+
502
+ ### `Form`
503
+ Import: `import { Form } from 'ywana-core8'`
504
+ Story: [src/html/Form.stories.jsx](src/html/Form.stories.jsx)
505
+
506
+ Propósito: Contenedor de formulario que gestiona múltiples campos hijos, validación agregada y grid de columnas.
507
+
508
+ Props:
509
+ | Prop | Tipo | Default | Requerido | Descripción |
510
+ | --- | --- | --- | --- | --- |
511
+ | id / title | string | — | No | Identificador y título |
512
+ | columns | number | 1 | No | Columnas del grid |
513
+ | children | ReactNode | — | **Sí** | Campos (deben tener `id`) |
514
+ | outlined / disabled / loading | boolean | false | No | Estados aplicados a los hijos |
515
+ | autoComplete | 'on' \| 'off' | 'on' | No | Atributo HTML |
516
+ | noValidate | boolean | false | No | Desactiva validación HTML5 |
517
+
518
+ Callbacks: `onChange(formData, isValid)`, `onSubmit(formData, event)` (async-safe), `onReset(event)`, `onValidationChange(isValid, fields)`
519
+
520
+ Controlado/No controlado: Los campos hijos se "elevan" al `Form`, que centraliza el estado.
521
+
522
+ Ejemplo mínimo:
523
+ ```jsx
524
+ <Form columns={2} onSubmit={(data) => console.log('Submit:', data)}>
525
+ <TextField id="name" label="Name" value="" />
526
+ <TextField id="email" label="Email" value="" />
527
+ </Form>
528
+ ```
529
+
530
+ Notas: Cada hijo debe declarar `id` para que `Form` pueda componer `formData`.
531
+
532
+ ---
533
+
534
+ ## 2. HTML — Layout, visualización y datos
535
+
536
+ ### `Accordion`
537
+ Import: `import { Accordion } from 'ywana-core8'`
538
+ Story: [src/html/Accordion.stories.jsx](src/html/Accordion.stories.jsx)
539
+
540
+ Propósito: Secciones colapsables, con soporte para múltiples abiertas a la vez y checkboxes opcionales por sección.
541
+
542
+ Props: `sections` (array `{id, open, checked, icon, title, subtitle, disabled, children}`, requerido), `disabled`, `allowMultiple` (true), `animated` (true), `loading`, `skeleton`, `skeletonCount` (4), `emptyMessage`/`emptyIcon`.
543
+
544
+ Callbacks: `onToggle(index, isOpen, section)`, `onCheck(index, isChecked, sectionId, section)`, `onSectionChange(action, index, value, section)`
545
+
546
+ Controlado/No controlado: No controlado (abre/cierra internamente).
547
+
548
+ Ejemplo mínimo:
549
+ ```jsx
550
+ <Accordion sections={[{ id: 'sec1', title: 'Sección 1', children: <p>Contenido</p> }]} onToggle={(idx, open) => console.log(idx, open)} />
551
+ ```
552
+
553
+ Notas: Una sección muestra checkbox solo si define `checked` (no `undefined`).
554
+
555
+ ---
556
+
557
+ ### `ActionsCell`
558
+ Import: `import { ActionsCell } from 'ywana-core8'`
559
+ Story: [src/html/ActionsCell.stories.jsx](src/html/ActionsCell.stories.jsx)
560
+
561
+ Propósito: Contenedor de acciones (botones) que detecta overflow automáticamente y colapsa a un menú `⋮` cuando no caben.
562
+
563
+ Props: `actions` (ReactNode, requerido — normalmente botones), `maxWidth` (200), `menuIcon` ('more_horiz'), `menuAlign`, `menuSize`, `gap`.
564
+
565
+ Ejemplo mínimo:
566
+ ```jsx
567
+ <ActionsCell maxWidth={150} actions={<><Button icon="edit" onClick={handleEdit} /><Button icon="delete" onClick={handleDelete} /></>} />
568
+ ```
569
+
570
+ Notas: Extrae `icon`/`title`/`label`/`onClick`/`disabled` de los botones hijos para reconstruirlos como items de menú. Útil en celdas de tabla.
571
+
572
+ ---
573
+
574
+ ### `Header`
575
+ Import: `import { Header } from 'ywana-core8'`
576
+ Story: [src/html/Header.stories.jsx](src/html/Header.stories.jsx)
577
+
578
+ Propósito: Encabezado simple con título, icono opcional y acciones (children). Sin estado propio.
579
+
580
+ Props: `title` (string|ReactNode), `icon`, `iconSrc`, `img` (fondo), `caption`/`prominent`/`dense` (variantes de altura), `primary`/`secondary` (tema), `clickable`, `action`.
581
+
582
+ Callbacks: `action(event)` si `clickable`.
583
+
584
+ Ejemplo mínimo:
585
+ ```jsx
586
+ <Header title="Mi Sección" icon="dashboard" primary><Button label="Acción" /></Header>
587
+ ```
588
+
589
+ ---
590
+
591
+ ### `Header2`
592
+ Import: `import { Header2 } from 'ywana-core8'`
593
+ Story: — (sin fichero .stories.jsx; ver src/html/header2.example.js)
594
+
595
+ Propósito: Versión mejorada de `Header` (misma apariencia) con mejor accesibilidad, `loading`/`disabled` y callback de icono independiente. Preferir sobre `Header` en código nuevo.
596
+
597
+ Props: igual que `Header` más `disabled`, `loading`, `role` ('banner').
598
+
599
+ Callbacks: `onClick(event)`, `onIconClick(event)` (prioridad sobre `action`).
600
+
601
+ Ejemplo mínimo:
602
+ ```jsx
603
+ <Header2 title="Dashboard" icon="dashboard" loading={isLoading} onIconClick={handleIconClick} />
604
+ ```
605
+
606
+ ---
607
+
608
+ ### `Icon`
609
+ Import: `import { Icon } from 'ywana-core8'`
610
+ Story: [src/html/Icon.stories.jsx](src/html/Icon.stories.jsx)
611
+
612
+ Propósito: Wrapper de Material Icons con estados interactivos, tooltip y accesibilidad — primitiva usada en casi todos los demás componentes.
613
+
614
+ Props: `icon` (string, **requerido**), `size` ('small'|'normal'|'large'), `clickable`, `disabled`, `action`, `eventPropagation` (false — por defecto detiene la propagación del click), `tooltip` (`{text, top, left}`), `ariaLabel`.
615
+
616
+ Callbacks: `action(event)` si `clickable`.
617
+
618
+ Ejemplo mínimo:
619
+ ```jsx
620
+ <Icon icon="delete" clickable action={handleDelete} tooltip={{ text: 'Eliminar' }} />
621
+ ```
622
+
623
+ Notas: Soporta Enter/Espacio por teclado cuando `clickable`. Con `eventPropagation={false}` (default) el click no burbujea — importante si el icono vive dentro de un elemento con su propio `onClick`.
624
+
625
+ ---
626
+
627
+ ### `IconButtonGroup`
628
+ Import: `import { IconButtonGroup } from 'ywana-core8'`
629
+ Story: [src/html/IconButtonGroup.stories.jsx](src/html/IconButtonGroup.stories.jsx)
630
+
631
+ Propósito: Grupo segmentado de botones de icono con un único valor activo (toggles de vista/tamaño).
632
+
633
+ Props: `value` (requerido), `onChange` (**requerido**), `items` (`[{value, icon, tooltip?}]`).
634
+
635
+ Callbacks: `onChange(nextValue)`
636
+
637
+ Controlado/No controlado: **Controlado** por `value`.
638
+
639
+ Ejemplo mínimo:
640
+ ```jsx
641
+ <IconButtonGroup value="grid" onChange={setViewMode} items={[{ value: 'list', icon: 'list' }, { value: 'grid', icon: 'grid_view' }]} />
642
+ ```
643
+
644
+ ---
645
+
646
+ ### `List`
647
+ Import: `import { List } from 'ywana-core8'`
648
+ Story: [src/html/List.stories.jsx](src/html/List.stories.jsx)
649
+
650
+ Propósito: Lista avanzada con búsqueda, ordenamiento, selección (simple/múltiple), agrupamiento y estados vacío/cargando.
651
+
652
+ Props: `items` (`[{id, line1, line2}]`), `selected` (id o array), `groupBy`, `groupRenderer`, `loading`/`empty`/`emptyMessage`/`emptyIcon`, `searchable`/`searchPlaceholder`/`searchBy` (`['line1','line2']`), `sortable`/`sortBy`/`sortDirection`, `multiSelect`, `dense`, `disabled`, `maxHeight`.
653
+
654
+ Callbacks: `onSelect(id, event)`, `onMultiSelect(selectedIds)`, `onSort(config)`
655
+
656
+ Controlado/No controlado: `selected` es controlado; búsqueda/orden son internos.
657
+
658
+ Ejemplo mínimo:
659
+ ```jsx
660
+ <List items={[{ id: 1, line1: 'Item 1' }]} selected={selectedId} onSelect={setSelectedId} searchable />
661
+ ```
662
+
663
+ ---
664
+
665
+ ### `MenuIcon`, `MenuButton`, `MenuBar`, `Menu`, `MenuItem`, `MenuSeparator`
666
+ Import: `import { MenuIcon, MenuButton, MenuBar, Menu, MenuItem, MenuSeparator } from 'ywana-core8'`
667
+ Story: [src/html/Menu.stories.jsx](src/html/Menu.stories.jsx)
668
+
669
+ Propósito: Familia de componentes de menú desplegable.
670
+ - `MenuIcon`: icono clickeable que abre un menú vertical (`position`: bottom-left/right, top-left/right, left/right/top/bottom; `menuSize`).
671
+ - `MenuButton`: botón con label que abre el mismo tipo de menú (`showArrow`, `outlined`, `raised`).
672
+ - `MenuBar`: contenedor horizontal para agrupar varios `MenuButton` (estilo barra de menú tipo app de escritorio).
673
+ - `Menu`: contenedor base de bajo nivel (rara vez se usa directamente).
674
+ - `MenuItem`: opción individual — `label` (**requerido**), `icon`, `meta` (ReactNode, p.ej. atajo de teclado), `disabled`, `onSelect()`.
675
+ - `MenuSeparator`: línea divisoria, sin props.
676
+
677
+ Callbacks: `MenuItem.onSelect()` (cierra el menú padre automáticamente al seleccionar).
678
+
679
+ Ejemplo mínimo:
680
+ ```jsx
681
+ <MenuIcon icon="more_vert" position="bottom-right">
682
+ <MenuItem icon="edit" label="Editar" onSelect={handleEdit} />
683
+ <MenuSeparator />
684
+ <MenuItem icon="delete" label="Eliminar" onSelect={handleDelete} />
685
+ </MenuIcon>
686
+ ```
687
+
688
+ Notas: Los hijos de `MenuIcon`/`MenuButton`/`Menu` deben ser `MenuItem`/`MenuSeparator`; los de `MenuBar` deben ser `MenuButton`.
689
+
690
+ ---
691
+
692
+ ### `CircularProgress`, `LinearProgress`, `RadialProgress`, `StepProgress`, `MultiProgress`
693
+ Import: `import { CircularProgress, LinearProgress, RadialProgress, StepProgress, MultiProgress } from 'ywana-core8'`
694
+ Story: [src/html/Progress.stories.jsx](src/html/Progress.stories.jsx)
695
+
696
+ Propósito: Familia de indicadores de progreso, todos **controlados** por `value`/`currentStep`/`items` (sin estado interno de progreso).
697
+
698
+ - `CircularProgress` / `RadialProgress`: progreso circular SVG. Props comunes: `value`, `max` (100), `size`, `thickness`, `variant` ('determinate'|'indeterminate', solo en `CircularProgress`), `showValue`, `showLabel`, `label`, `icon`, `animated`, `formatValue(value, max)`.
699
+ - `LinearProgress`: barra horizontal. Añade `buffer` (pre-buffering), `striped`, `rounded`, `estimatedTime`, `speed`.
700
+ - `StepProgress`: progreso multi-paso tipo wizard. `steps` (`[{id, label, description?, icon?, error?}]`, **requerido**), `currentStep` (0), `variant` ('horizontal'|'vertical'), `allowClickNavigation`.
701
+ - `MultiProgress`: varias `LinearProgress` con etiqueta. `items` (`[{id, label, value, max, color?, formatValue?}]`, **requerido**).
702
+
703
+ Callbacks: `onComplete()` (cuando `value >= max`, en Circular/Linear), `onStepClick(stepIndex, step)` (StepProgress, solo si `allowClickNavigation`).
704
+
705
+ Ejemplo mínimo:
706
+ ```jsx
707
+ <CircularProgress value={65} max={100} variant="determinate" showValue />
708
+ <StepProgress steps={[{ label: 'Paso 1' }, { label: 'Paso 2' }]} currentStep={1} />
709
+ ```
710
+
711
+ ---
712
+
713
+ ### `Property`
714
+ Import: `import { Property } from 'ywana-core8'`
715
+ Story: [src/html/Property.stories.jsx](src/html/Property.stories.jsx)
716
+
717
+ Propósito: Fila "etiqueta + valor/input" para formularios inline tipo ficha de detalle, con validación integrada.
718
+
719
+ Props: `id`/`label` (**requeridos**), `value`, `initial` (fallback), `editable` (false = solo lectura), `type` ('text' etc.), `multiline`/`rows`, `options` (para modo select), `validateValue(val) => {isValid, error}`, `layout` ('horizontal'|'vertical'), `nameWidth` ('50%'), `copyable`, `clearable` (true).
720
+
721
+ Callbacks: `onChange(id, newValue)`, `onFocus(event)`, `onBlur(event)`, `onValidationError(error, value)`
722
+
723
+ Controlado/No controlado: Controlado por `value`.
724
+
725
+ Ejemplo mínimo:
726
+ ```jsx
727
+ <Property id="email" label="Email" value={email} editable onChange={(id, val) => setEmail(val)} type="email" required />
728
+ ```
729
+
730
+ ---
731
+
732
+ ### `Section`
733
+ Import: `import { Section } from 'ywana-core8'`
734
+ Story: [src/html/Section.stories.jsx](src/html/Section.stories.jsx)
735
+
736
+ Propósito: Sección colapsable con `Header` integrado (título + icono) y contenido plegable.
737
+
738
+ Props: `title`, `icon`, `open` (estado inicial, false), `canCollapse` (true), `actions` (ReactNode extra en el header).
739
+
740
+ Controlado/No controlado: No controlado (gestiona `open` internamente tras el montaje).
741
+
742
+ Ejemplo mínimo:
743
+ ```jsx
744
+ <Section title="Detalles" icon="info" open={false}><p>Contenido de la sección</p></Section>
745
+ ```
746
+
747
+ ---
748
+
749
+ ### `Tabs`, `Tab`, `Stack`
750
+ Import: `import { Tabs, Tab, Stack } from 'ywana-core8'`
751
+ Story: [src/html/Tab.stories.jsx](src/html/Tab.stories.jsx)
752
+
753
+ Propósito: Sistema de pestañas.
754
+ - `Tabs`: contenedor — `selected` (**controlado**, id o índice), `onChange(idOrIndex)` (**requerido para que funcione**), `orientation` ('horizontal'|'vertical'), `variant` ('standard'|'scrollable'|'fullWidth'), `centered`, `persistent`/`persistKey` (guarda selección en localStorage), `beforeChange(idOrIndex, currentSelected) => canChange` (hook cancelable, puede ser async).
755
+ - `Tab`: pestaña individual — `label` (**requerido**), `id`, `icon`, `badge`, `disabled`, `closeable`, `onClose(tabId)`, `tooltip`.
756
+ - `Stack`: renderiza solo el hijo en el índice `selected` (**controlado**) — útil para wizards fuera de `Tabs`.
757
+
758
+ Ejemplo mínimo:
759
+ ```jsx
760
+ <Tabs selected={selectedTab} onChange={setSelectedTab}>
761
+ <Tab id="tab1" label="Tab 1" icon="home">Contenido 1</Tab>
762
+ <Tab id="tab2" label="Tab 2" icon="settings">Contenido 2</Tab>
763
+ </Tabs>
764
+ ```
765
+
766
+ ---
767
+
768
+ ### `Text`
769
+ Import: `import { Text, TEXTFORMATS } from 'ywana-core8'`
770
+ Story: [src/html/Text.stories.jsx](src/html/Text.stories.jsx)
771
+
772
+ Propósito: Texto con formateo automático (números, fechas, moneda, etc.) e i18n vía `Intl`.
773
+
774
+ Props: `format` (uno de `TEXTFORMATS`: HTML, STRING, NUMERIC, CURRENCY, PERCENTAGE, DATE, TIME, DATETIME, EMAIL, PHONE, URL, CAPITALIZE, UPPERCASE, LOWERCASE, TRUNCATE — default HTML), `children` (valor a formatear), `locale`, `currency` ('USD'), `dateStyle`/`timeStyle`, `truncate`/`maxLength`, `fallback`, `loading`/`skeleton`, `copyable`, `as` ('span' por defecto).
775
+
776
+ Ejemplo mínimo:
777
+ ```jsx
778
+ <Text format={TEXTFORMATS.CURRENCY} currency="EUR">{1234.56}</Text>
779
+ ```
780
+
781
+ Notas: Usa `Intl.NumberFormat`/`Intl.DateTimeFormat` — pasa el valor crudo (number/Date), no ya formateado.
782
+
783
+ ---
784
+
785
+ ### `Thumbnail`
786
+ Import: `import { Thumbnail } from 'ywana-core8'`
787
+ Story: [src/html/Thumbnail.stories.jsx](src/html/Thumbnail.stories.jsx)
788
+
789
+ Propósito: Imagen miniatura con estados carga/error, placeholder, overlay y badge.
790
+
791
+ Props: `src`, `alt`, `title`, `loading` ('lazy'|'eager'), `placeholder`/`fallback` (ReactNode), `objectFit` ('cover' por defecto), `size` ('small'|'medium'|'large'|'xlarge'), `shape` ('square'|'circle'|'rounded'), `bordered`/`shadow`, `clickable`, `overlay`, `badge`.
792
+
793
+ Callbacks: `onClick(event)`, `onLoad(event)`, `onError(event)`
794
+
795
+ Ejemplo mínimo:
796
+ ```jsx
797
+ <Thumbnail src="/image.jpg" size="large" shape="circle" badge="New" clickable />
798
+ ```
799
+
800
+ ---
801
+
802
+ ### `Tooltip`
803
+ Import: `import { Tooltip } from 'ywana-core8'`
804
+ Story: [src/html/Tooltip.stories.jsx](src/html/Tooltip.stories.jsx)
805
+
806
+ Propósito: Envuelve contenido para mostrar un tooltip posicionado en hover.
807
+
808
+ Props: `text` (string|ReactNode), `top`/`left` (posición CSS, defaults `'1rem'`).
809
+
810
+ Ejemplo mínimo:
811
+ ```jsx
812
+ <Tooltip text="Haz click aquí"><Button label="Acción" /></Tooltip>
813
+ ```
814
+
815
+ Notas: Muchos otros componentes (Icon, Chip, IconButtonGroup...) ya aceptan una prop `tooltip={{text}}` propia — no hace falta envolverlos con este componente en esos casos.
816
+
817
+ ---
818
+
819
+ ### `Tree`, `TreeNode`, `TreeItem`
820
+ Import: `import { Tree, TreeNode, TreeItem } from 'ywana-core8'`
821
+ Story: [src/html/Tree.stories.jsx](src/html/Tree.stories.jsx)
822
+
823
+ Propósito: Árbol jerárquico declarativo (vía JSX anidado) con búsqueda, multi-selección y drag&drop.
824
+
825
+ Props (`Tree`): `searchable`/`searchPlaceholder`/`searchBy` (`['label']`), `multiSelect`, `onMultiSelect(selectedIds)`, `showExpandIcon` (control global expandir/colapsar todo), `loading`/`empty`/`emptyMessage`/`emptyIcon`.
826
+
827
+ Props (`TreeNode`, rama con hijos): `id`/`label` (**requeridos**), `icon` ('folder'), `open` (estado inicial), `expandable` (true), `disabled`, `draggable`, `badge`, `actions`, `onSelect(id)`, `onDragStart(id, event)`, `onDrop(draggedId, targetId, event)`.
828
+
829
+ Props (`TreeItem`, hoja): `id`/`label` (**requeridos**), `icon` ('description'), `selected`, `checked`, `onSelect(id)`, `onCheck(id, checked)`.
830
+
831
+ Ejemplo mínimo:
832
+ ```jsx
833
+ <Tree searchable showExpandIcon>
834
+ <TreeNode id="folder1" label="Carpeta 1" icon="folder">
835
+ <TreeItem id="file1" label="Archivo 1" onSelect={handleSelect} />
836
+ </TreeNode>
837
+ </Tree>
838
+ ```
839
+
840
+ Notas: Click en la fila de un `TreeNode` (fuera del chevron) selecciona y expande/colapsa a la vez. La multi-selección aplica solo a `TreeItem` con `multiSelect` activo en el `Tree` padre.
841
+
842
+ ---
843
+
844
+ ### `Tree2`, `TreeNode2`, `TreeItem2`
845
+ Import: `import { Tree2, TreeNode2, TreeItem2 } from 'ywana-core8'`
846
+ Story: [src/html/tree2.stories.jsx](src/html/tree2.stories.jsx)
847
+
848
+ Propósito: Variante de árbol con **lazy loading** async por nodo (para árboles muy grandes/remotos), con estados de error y vacío por nodo.
849
+
850
+ Props (`TreeNode2`): `id`/`label` (**requeridos**), `icon` ('folder'), `loadOnExpand` (activa carga diferida), `onExpand(nodeId) => Promise<childrenPropsArray>`, `onRefresh(nodeId) => Promise<...>`, `loading`/`error`/`empty`, `contextMenu` (`[{label, icon, onClick}]`).
851
+
852
+ Props (`TreeItem2`): `id`/`label` (**requeridos**), `icon` ('description'), `onSelect(itemId)`, `selected`, `contextMenu`.
853
+
854
+ Ejemplo mínimo:
855
+ ```jsx
856
+ <Tree2>
857
+ <TreeNode2
858
+ id="folder1" label="Documentos" icon="folder" loadOnExpand
859
+ onExpand={async (nodeId) => {
860
+ const children = await fetchFolderContents(nodeId)
861
+ return children.map(c => ({ id: c.id, label: c.name, icon: c.isFolder ? 'folder' : 'description', isLeaf: !c.isFolder, loadOnExpand: c.isFolder }))
862
+ }}
863
+ />
864
+ </Tree2>
865
+ ```
866
+
867
+ Notas: `onExpand`/`onRefresh` deben devolver un array de props listo para renderizar (`isLeaf: true` renderiza automáticamente como `TreeItem2`).
868
+
869
+ ---
870
+
871
+ ### `DataTable` (deprecated)
872
+ Import: `import { DataTable } from 'ywana-core8'`
873
+ Story: [src/html/Table.stories.jsx](src/html/Table.stories.jsx)
874
+
875
+ ⚠️ **Deprecated** — usar `DataTable2` (uso general) o `DataTable3` (>1000 filas). Se mantiene solo por compatibilidad.
876
+
877
+ Props mínimas: `columns` (formato legacy), `rows` (**requeridos**), `editable`, `multisort`, `filterable`.
878
+
879
+ Callbacks: `onRowSelection(row, event)`, `onSort(draggedRow, droppedRow)`, `onCheckAll(ids, value)`, `onClearFilters()`
880
+
881
+ ---
882
+
883
+ ### `DataTable2`
884
+ Import: `import { DataTable2 } from 'ywana-core8'`
885
+ Story: [src/html/Table2.stories.jsx](src/html/Table2.stories.jsx)
886
+
887
+ Propósito: Tabla avanzada recomendada por defecto — ordenamiento, filtrado, selección, resize de columnas, sidebar de herramientas, exportación, agrupación y temas.
888
+
889
+ Props principales:
890
+ | Prop | Tipo | Default | Descripción |
891
+ | --- | --- | --- | --- |
892
+ | `columns` | array | [] | `{id, label, sortable?, filterable?, width?, minWidth?, maxWidth?, render?(value, row), className?}` |
893
+ | `rows` | array | [] | Cada fila necesita `id`; `info: () => ReactNode` para contenido expandible |
894
+ | `sortDir` | object | — | Controlado: `{ [columnId]: 1 \| -1 }` |
895
+ | `selectedRows` | array | [] | Ids seleccionados (controlado) |
896
+ | `filters` | object | {} | Estado de filtros (controlado) |
897
+ | `showSelectAll` / `showRowNumbers` | boolean | false | Extras de columna |
898
+ | `editable` | boolean | false | Edición inline de celdas |
899
+ | `theme` | 'default'\|'dark'\|'minimal'\|'readable' | 'default' | Tema visual |
900
+ | `density` | 'compact'\|'normal'\|'comfortable' | 'normal' | Densidad de filas |
901
+ | `stickyHeader` / `resizable` | boolean | false / true | Comportamiento de header/columnas |
902
+ | `showSidebar` / `exportable` | boolean | false | Panel lateral / exportar datos |
903
+ | `groupBy` | string \| function | — | Agrupar filas |
904
+
905
+ Callbacks: `onSort(columnId, dir)`, `onSelect(rowId, event)` (y alias `onRowSelection(row, event)`), `onCheckAll(rowIds, value)`, `onCellEdit(rowId, columnId, value)`, `onCellClick(event, colId, rowId, value)`, `onRowDoubleClick(row)`, `onClearFilters()`, `onColumnResize(colId, width)`, `onExport(data, format)`, `onGroupByChange(value)`
906
+
907
+ Controlado/No controlado: `selectedRows`, `sortDir`, `filters` son controlados si se pasan; si no, estado interno.
908
+
909
+ Ejemplo mínimo:
910
+ ```jsx
911
+ <DataTable2
912
+ columns={[{ id: 'name', label: 'Nombre', sortable: true }, { id: 'email', label: 'Email', filterable: true }]}
913
+ rows={users}
914
+ selectedRows={selectedRows}
915
+ onSelect={(id) => setSelectedRows([id])}
916
+ />
917
+ ```
918
+
919
+ Notas: hay documentación extendida en `src/html/table2.md`.
920
+
921
+ ---
922
+
923
+ ### `DataTable3`
924
+ Import: `import { DataTable3 } from 'ywana-core8'`
925
+ Story: [src/html/Table3.stories.jsx](src/html/Table3.stories.jsx)
926
+
927
+ Propósito: Igual que `DataTable2` pero con **virtualización** — usar solo si el dataset supera ~1000 filas.
928
+
929
+ Props relevantes además de las compartidas con `DataTable2`: `height`/`maxHeight` (recomendado fijar altura), `rowHeight` (44), `overscan` (8), `virtualize` (true), `virtualizeThreshold` (200), `showColumnsPanel`.
930
+
931
+ Callbacks: mismos patrones que `DataTable2` (`onSort`, `onSelect`, `onCellClick`, `onCellDoubleClick`, `onColumnResize`, `onExport`).
932
+
933
+ Ejemplo mínimo:
934
+ ```jsx
935
+ <DataTable3 columns={columns} rows={largeDataset} selectedRows={selected} onSelect={setSelected} height="600px" virtualize exportable />
936
+ ```
937
+
938
+ ---
939
+
940
+ ## 3. Site — Shell de aplicación
941
+
942
+ Estos componentes forman el **layout raíz** típico de una app construida con `ywana-core8`:
943
+ `SiteProvider` (contexto global) → `Site` (shell visual) → `Page` (una por sección) → `View`/`Dialog` dentro de cada página según necesidad.
944
+
945
+ ### `SiteProvider`
946
+ Import: `import { SiteProvider } from 'ywana-core8'`
947
+ Story: [src/site/site.stories.jsx](src/site/site.stories.jsx)
948
+
949
+ Propósito: Provider de contexto raíz — navegación, diálogos, notificaciones, logs e i18n. Debe envolver toda la app.
950
+
951
+ Props: `children` (**requerido**), `siteLang` (**requerido**), `siteDictionary` (**requerido**, `{key: {lang: value}}`).
952
+
953
+ Contexto expuesto (via `SiteContext`): navegación (`page`, `goto(id)`, `goBack()`, `gotoNewTab(pageId, params)`, `readPageParams(pageId)`, `direction`), i18n (`lang`, `setLang`, `translate(key)`), menú (`sideNav`, `showNav`, `setShowNav`), diálogos (`dialog`, `openDialog(content)`, `closeDialog()`, `promptDialog`, `openPromptDialog`/`closePromptDialog`), preview (`openPreview(content)`/`closePreview()`), notificaciones (`notify({title, body, type, duration, onRemoval})`), info/breadcrumb, consola de logs (`writeLog`, `clearLog`), y legacy `confirm()`/`prompt()`.
954
+
955
+ Ejemplo mínimo:
956
+ ```jsx
957
+ <SiteProvider siteLang="es" siteDictionary={dict}>
958
+ <Site title="Mi App">{/* Pages */}</Site>
959
+ </SiteProvider>
960
+ ```
961
+
962
+ Notas: Requiere `react-toastify` para notificaciones. Integra `TranslationProvider` internamente.
963
+
964
+ ---
965
+
966
+ ### `Site`
967
+ Import: `import { Site } from 'ywana-core8'`
968
+ Story: [src/site/site.stories.jsx](src/site/site.stories.jsx)
969
+
970
+ Propósito: Shell visual completo: header, menú lateral navegable (construido a partir de los `Page` hijos), área de contenido, preview lateral, consola y footer.
971
+
972
+ Props: `children` (`Page[]`, **requerido**), `icon` ('equalizer'), `iconSrc` (logo, prevalece sobre `icon`), `title`, `toolbar`, `footer`, `init` (id de página inicial), `min` (menú colapsado al inicio), `lang`/`dictionary`.
973
+
974
+ Controlado/No controlado: No controlado (navegación vía hash URL).
975
+
976
+ Ejemplo mínimo:
977
+ ```jsx
978
+ <Site title="Mi App" icon="widgets" init="HOME">
979
+ <Page id="HOME" title="Inicio" icon="home"><HomePage /></Page>
980
+ <Page id="ABOUT" title="Acerca de" icon="info"><AboutPage /></Page>
981
+ </Site>
982
+ ```
983
+
984
+ Notas: Cada `Page` hijo directo aparece automáticamente en el menú lateral usando su `title`/`icon`.
985
+
986
+ ---
987
+
988
+ ### `SiteContext`
989
+ Import: `import { SiteContext } from 'ywana-core8'`
990
+ Story: [src/site/site.stories.jsx](src/site/site.stories.jsx)
991
+
992
+ Propósito: React Context consumido con `useContext(SiteContext)` para acceder a navegación/diálogos/notificaciones desde cualquier componente descendiente de `SiteProvider`.
993
+
994
+ Ejemplo mínimo:
995
+ ```jsx
996
+ const site = useContext(SiteContext)
997
+ site.goto('DETAIL')
998
+ site.notify({ title: 'Guardado', type: 'success' })
999
+ ```
1000
+
1001
+ ---
1002
+
1003
+ ### `Page`, `PageContext`, `PageProvider`
1004
+ Import: `import { Page, PageContext, PageProvider } from 'ywana-core8'`
1005
+ Story: [src/site/site.stories.jsx](src/site/site.stories.jsx)
1006
+
1007
+ Propósito: `Page` renderiza una sección de `Site`; solo una está visible a la vez (controlada por hash URL). Cada `Page` trae su propio contexto local (`PageContext`) para estado aislado a esa página.
1008
+
1009
+ Props (`Page`): `id` (**requerido**, usado en el hash), `children` (**requerido**), `layout` ('simple'), `context` (estado inicial de `PageContext`), `title`/`icon` (usados por `Site` para el menú, opcionales pero recomendados).
1010
+
1011
+ `PageContext`: `[pageCtx, setPageCtx] = useContext(PageContext)` — estado local a la página actual.
1012
+
1013
+ `PageProvider`: wrapper manual de `PageContext` (`Page` ya lo incluye; rara vez se usa suelto).
1014
+
1015
+ Ejemplo mínimo:
1016
+ ```jsx
1017
+ <Page id="HOME" title="Inicio" icon="home">
1018
+ <h1>Bienvenido</h1>
1019
+ </Page>
1020
+ ```
1021
+
1022
+ Notas: `Page` debe ser hijo directo de `Site`.
1023
+
1024
+ ---
1025
+
1026
+ ### `Dialog`
1027
+ Import: `import { Dialog } from 'ywana-core8'`
1028
+ Story: [src/site/site.stories.jsx](src/site/site.stories.jsx)
1029
+
1030
+ Propósito: Modal renderizado sobre overlay. **No se monta directamente** — se abre/cierra vía `SiteContext.openDialog()`/`closeDialog()`.
1031
+
1032
+ Props: `title` ('Dialog'), `icon`, `children` (**requerido**), `actions` (footer con botones), `toolbar`, `className` ('prompt' para una segunda capa modal sobre otro diálogo), `onClose`, `overlayCanClose` (true — click fuera cierra).
1033
+
1034
+ Ejemplo mínimo:
1035
+ ```jsx
1036
+ const site = useContext(SiteContext)
1037
+ site.openDialog(
1038
+ <Dialog title="¿Confirmar?" actions={<Button onClick={() => site.closeDialog()}>Aceptar</Button>}>
1039
+ <p>¿Estás seguro?</p>
1040
+ </Dialog>
1041
+ )
1042
+ ```
1043
+
1044
+ ---
1045
+
1046
+ ### `View`, `TabbedView`
1047
+ Import: `import { View, TabbedView } from 'ywana-core8'`
1048
+ Story: [src/site/site.stories.jsx](src/site/site.stories.jsx)
1049
+
1050
+ Propósito: Sección visual reutilizable con header colapsable, toolbar, menú e info en footer — usable dentro de una `Page` o en cualquier otro lugar. `TabbedView` es la variante que además renderiza tabs en el toolbar (cada hijo con prop `label` es una pestaña).
1051
+
1052
+ Props (`View`): `title`, `icon`, `toolbar`, `menu` (esquina superior derecha), `info` (footer), `children` (**requerido**), `canCollapse` (false), `onClose`.
1053
+
1054
+ Props (`TabbedView`): `title`, `selected` (índice, controlado externamente), `children` (**requerido**, cada uno con `label`).
1055
+
1056
+ Ejemplo mínimo:
1057
+ ```jsx
1058
+ <View title="Configuración" icon="settings" canCollapse>
1059
+ <p>Contenido aquí</p>
1060
+ </View>
1061
+
1062
+ <TabbedView title="Análisis" selected={tabIndex}>
1063
+ <div label="Resumen">…</div>
1064
+ <div label="Detalles">…</div>
1065
+ </TabbedView>
1066
+ ```
1067
+
1068
+ ---
1069
+
1070
+ ### `PageLink`
1071
+ Import: `import { PageLink } from 'ywana-core8'`
1072
+ Story: [src/site/site.stories.jsx](src/site/site.stories.jsx)
1073
+
1074
+ Propósito: `<a>` declarativo que navega entre páginas usando `SiteContext` (alternativa a llamar `site.goto()` manualmente).
1075
+
1076
+ Props: `page` (id destino, **requerido**), `children` (**requerido**), `className`, `style`.
1077
+
1078
+ Ejemplo mínimo:
1079
+ ```jsx
1080
+ <PageLink page="DETAIL">Ver detalle</PageLink>
1081
+ ```
1082
+
1083
+ ---
1084
+
1085
+ ### `useHashPage`
1086
+ Import: `import { useHashPage } from 'ywana-core8'`
1087
+ Story: — (sin fichero .stories.jsx; hook interno de src/site/site.js)
1088
+
1089
+ Propósito: Hook de navegación por hash URL con historial y dirección de transición (usado internamente por `SiteProvider`; rara vez se llama directamente).
1090
+
1091
+ Firma: `useHashPage(defaultPage = 'home')` → `{ page, goto(id, dir?), goBack(), history, direction }`.
1092
+
1093
+ ---
1094
+
1095
+ ### `Workspace`
1096
+ Import: `import { Workspace } from 'ywana-core8'`
1097
+ Story: — (sin fichero .stories.jsx)
1098
+
1099
+ Propósito: Layout experimental (WIP) con áreas colapsables (menú/header/nav/main/aside) y transiciones responsive. **No recomendado para producción.**
1100
+
1101
+ ---
1102
+
1103
+ ## 4. Widgets — Componentes de negocio genéricos
1104
+
1105
+ ### `LoginBox`
1106
+ Import: `import { LoginBox } from 'ywana-core8'`
1107
+ Story: [src/widgets/login/Login.stories.jsx](src/widgets/login/Login.stories.jsx)
1108
+
1109
+ Propósito: Formulario de login (usuario + contraseña) con estados de carga y mensaje de error.
1110
+
1111
+ Props: `userLabel`/`passwordLabel`/`loginLabel` (textos), `userValue`/`passwordValue` (iniciales), `message` (error/info), `loading`, `disabled`, `children` (contenido extra, p.ej. link "¿Olvidaste tu contraseña?").
1112
+
1113
+ Callbacks: `onOK(user, password)`
1114
+
1115
+ Controlado/No controlado: No controlado (estado interno de los campos).
1116
+
1117
+ Ejemplo mínimo:
1118
+ ```jsx
1119
+ <LoginBox onOK={(user, pwd) => authenticate(user, pwd)} message={error} loading={isLoading} />
1120
+ ```
1121
+
1122
+ Notas: Solo valida que ambos campos no estén vacíos; la validación real de credenciales es responsabilidad del `onOK`.
1123
+
1124
+ ---
1125
+
1126
+ ### `ResetPasswordBox`
1127
+ Import: `import { ResetPasswordBox } from 'ywana-core8'`
1128
+ Story: [src/widgets/login/Login.stories.jsx](src/widgets/login/Login.stories.jsx)
1129
+
1130
+ Propósito: Formulario de cambio/reset de contraseña con validación de fortaleza integrada.
1131
+
1132
+ Props: `userRequired` (true), `oldPwdRequired` (false), `validator(password) => [boolean, errorMessage]` (custom), `showStrength` (true), `submitLabel` ('OK'), `loading`/`disabled`.
1133
+
1134
+ Callbacks: `onOK({ user?, oldPassword?, password1, password2 })`
1135
+
1136
+ Ejemplo mínimo:
1137
+ ```jsx
1138
+ <ResetPasswordBox userRequired={false} oldPwdRequired onOK={(form) => changePassword(form.password1)} />
1139
+ ```
1140
+
1141
+ Notas: El módulo `login/validations.js` expone además utilidades sueltas (`validatePassword`, `getPasswordStrength`, `createPasswordValidator`, etc.) reutilizables fuera del componente. `login/context.js`/`login/dialogs.js`/`login/actions.js` aportan un contexto y diálogos de soporte (recuperar contraseña, desbloquear usuario) pensados para integrarse con un backend propio — revisa el código si necesitas personalizarlos.
1142
+
1143
+ ---
1144
+
1145
+ ### `WaitScreen`
1146
+ Import: `import { WaitScreen } from 'ywana-core8'`
1147
+ Story: — (sin fichero .stories.jsx)
1148
+
1149
+ Propósito: Pantalla de espera con spinner centrado. Sin props.
1150
+
1151
+ Ejemplo mínimo:
1152
+ ```jsx
1153
+ <WaitScreen />
1154
+ ```
1155
+
1156
+ ---
1157
+
1158
+ ### `Viewer`
1159
+ Import: `import { Viewer } from 'ywana-core8'`
1160
+ Story: [src/widgets/viewer/Viewer.stories.jsx](src/widgets/viewer/Viewer.stories.jsx)
1161
+
1162
+ Propósito: Visor universal de ficheros (imagen, vídeo, PDF, audio) con detección automática de tipo y navegación entre varios ficheros.
1163
+
1164
+ Props: modo un solo fichero — `src`/`title` (requeridos en ese modo); modo múltiple — `files` (`[{id, title, src, filename, mimeType, thumbnail, info}]`), `initialFileId`, `showThumbnails`. Comunes: `filename`, `mimeType`, `info` (panel lateral), `actions` (array de componentes), `tools` (toolbar de controles), `onClose` (**requerido**).
1165
+
1166
+ Callbacks: `onClose()`, `onFileChange(file, index)`
1167
+
1168
+ Ejemplo mínimo:
1169
+ ```jsx
1170
+ <Viewer src="https://example.com/image.jpg" title="Sample" onClose={() => setOpen(false)} tools />
1171
+ ```
1172
+
1173
+ Notas: Requiere red para cargar los ficheros; detecta tipo por `mimeType`/extensión.
1174
+
1175
+ ---
1176
+
1177
+ ### `Kanban`
1178
+ Import: `import { Kanban } from 'ywana-core8'`
1179
+ Story: [src/widgets/kanban/Kanban.stories.jsx](src/widgets/kanban/Kanban.stories.jsx)
1180
+
1181
+ Propósito: Tablero kanban con columnas, swimlanes y drag&drop (usa `@dnd-kit`).
1182
+
1183
+ Props: `children` (**requerido** — `KanbanColumn`/`KanbanSwimlane`/`KanbanCard`, importados aparte), `enableDragDrop` (false), `animated` (true), `loading`.
1184
+
1185
+ Callbacks: `onCardMove(cardId, fromColumn, toColumn, fromSwimlane, toSwimlane, { fromIndex, toIndex, overId, overType })`, `onColumnEmptyAreaClick`
1186
+
1187
+ Ejemplo mínimo:
1188
+ ```jsx
1189
+ <Kanban enableDragDrop onCardMove={(id, from, to) => moveCard(id, from, to)}>
1190
+ <KanbanColumn id="todo" title="To Do">
1191
+ <KanbanCard id="card1">Task 1</KanbanCard>
1192
+ </KanbanColumn>
1193
+ </Kanban>
1194
+ ```
1195
+
1196
+ ---
1197
+
1198
+ ### `Avatar`
1199
+ Import: `import { Avatar } from 'ywana-core8'`
1200
+ Story: [src/widgets/avatar/Avatar.stories.jsx](src/widgets/avatar/Avatar.stories.jsx)
1201
+
1202
+ Propósito: Avatar con imagen o iniciales (color determinista por nombre), badge de verificado.
1203
+
1204
+ Props: `src`, `name` (para iniciales/color; sin nombre muestra "?"), `verified`, `size` ('small'|'medium'|'large'|'xlarge'), `clickable`, `action(id)`.
1205
+
1206
+ Ejemplo mínimo:
1207
+ ```jsx
1208
+ <Avatar src={user.avatar} name={user.name} verified={user.isVerified} size="large" />
1209
+ ```
1210
+
1211
+ Notas: Si `src` falla al cargar, cae automáticamente a iniciales.
1212
+
1213
+ ---
1214
+
1215
+ ### `Calendar`
1216
+ Import: `import { Calendar } from 'ywana-core8'`
1217
+ Story: [src/widgets/calendar/Calendar.stories.jsx](src/widgets/calendar/Calendar.stories.jsx)
1218
+
1219
+ Propósito: Calendario multi-vista (año/mes/semana/día) con eventos y navegación.
1220
+
1221
+ Props: `events` (array), `view` ('year'|'month'|'week'|'day'), `position` (fecha, semi-controlado), `availableViews` (`['year','month']`), `hideNavigation`, `enableDragDrop`.
1222
+
1223
+ Callbacks: `onChange(position, range)`, `onViewChange(newView)`, `onEventClick(event)`, `onCellDoubleClick(date)`
1224
+
1225
+ Ejemplo mínimo:
1226
+ ```jsx
1227
+ <Calendar events={events} view="month" onChange={(pos) => setPosition(pos)} />
1228
+ ```
1229
+
1230
+ ---
1231
+
1232
+ ### `DateRangePicker`
1233
+ Import: `import { DateRangePicker } from 'ywana-core8'`
1234
+ Story: [src/widgets/calendar/DateRangePicker/DateRangePicker.stories.jsx](src/widgets/calendar/DateRangePicker/DateRangePicker.stories.jsx)
1235
+
1236
+ Propósito: Selector de rango de fechas horizontal con zoom/pan, minimap y selección por arrastre (usa `moment.js`).
1237
+
1238
+ Props: `startDate`/`endDate` (rango total, **requeridos**), `selectedStart`/`selectedEnd` (rango seleccionado, controlado), `unit` ('day'|'week'|'month'|'year'), `weekStartsOnMonday` (true), `height` (160), `showMinimap` (true), `markers` (`[{date, label, color, backgroundColor}]`), `showUnitSelector` (true), `persistent`/`persistKey` (guarda en localStorage).
1239
+
1240
+ Callbacks: `onRangeChange(start, end)` (objetos Moment), `onUnitChange(unit)`
1241
+
1242
+ Ejemplo mínimo:
1243
+ ```jsx
1244
+ <DateRangePicker startDate="2024-01-01" endDate="2024-12-31" selectedStart="2024-06-01" selectedEnd="2024-06-30" onRangeChange={(start, end) => console.log(start, end)} />
1245
+ ```
1246
+
1247
+ ---
1248
+
1249
+ ### `Planner`
1250
+ Import: `import { Planner } from 'ywana-core8'`
1251
+ Story: — (sin fichero .stories.jsx)
1252
+
1253
+ Propósito: Planificador con carriles (lanes/recursos) y eventos en un grid de fechas (usa `moment.js`).
1254
+
1255
+ Props: `title`, `events`, `lanes` (recursos), `navigation` (true), `focusEvent` (id a enfocar), `config` (`{range: 'week'|'month'|'year', from, to}`).
1256
+
1257
+ Callbacks: `onSelectCell(lane, date)`, `onChange(config)`
1258
+
1259
+ Ejemplo mínimo:
1260
+ ```jsx
1261
+ <Planner title="Roadmap" lanes={teams} events={tasks} onSelectCell={(lane, date) => addTask(lane, date)} />
1262
+ ```
1263
+
1264
+ ---
1265
+
1266
+ ### `UploadV2`, `UploadButtonV2`, `UploadDialogV2`, `UploadDropZone`
1267
+ Import: `import { UploadV2, UploadButtonV2, UploadDialogV2, UploadDropZone } from 'ywana-core8'`
1268
+ Story: [src/widgets/upload2/Upload2.stories.jsx](src/widgets/upload2/Upload2.stories.jsx)
1269
+
1270
+ Propósito: Familia moderna de subida de ficheros (basada en Axios/Fetch). Preferir sobre el paquete `upload` legacy (deprecated, basado en ResumableJS).
1271
+
1272
+ - `UploadV2`: componente completo (drop-zone + lista/grid + progreso). Props: `url` (**requerido**), `uploadService` (custom, por defecto axios), `headers`, `accept`, `maxSize`, `maxFiles`, `multiple` (true), `autoUpload` (true), `simultaneousUploads` (3), `view` ('list'|'grid'). Callbacks: `onFileSuccess(file)`, `onFileError(file, error)`, `onAllComplete(stats)`.
1273
+ - `UploadButtonV2`: botón que solo abre el selector de ficheros (no sube nada). Props: `accept`, `multiple`, `label` ('Upload Files'), `icon`. Callback: `onFilesSelected(files)`.
1274
+ - `UploadDialogV2`: `UploadV2` envuelto en `Dialog`. Añade `open` (controlado), `onClose` (**requerido**), `title`, `additionalActions`.
1275
+ - `UploadDropZone`: solo el área de drag&drop con validación (sin lista/progreso). Props: `accept`, `maxSize`, `maxFiles`. Callbacks: `onFilesSelected(files)`, `onValidationError(errors)`.
1276
+
1277
+ Ejemplo mínimo:
1278
+ ```jsx
1279
+ <UploadV2 url="/api/upload" accept="image/*" maxSize={5242880} onFileSuccess={(f) => console.log(f.name)} />
1280
+ ```
1281
+
1282
+ ---
1283
+
1284
+ ### `EmptyMessage`
1285
+ Import: `import { EmptyMessage } from 'ywana-core8'`
1286
+ Story: — (sin fichero .stories.jsx)
1287
+
1288
+ Propósito: Mensaje de estado vacío (icono + texto centrados), usado por defecto dentro de listas/tablas/grids de esta librería cuando no hay datos.
1289
+
1290
+ Props: `icon` (**requerido**), `text` (**requerido**).
1291
+
1292
+ Ejemplo mínimo:
1293
+ ```jsx
1294
+ <EmptyMessage icon="inbox" text="No messages yet" />
1295
+ ```
1296
+
1297
+ ---
1298
+
1299
+ ### `ImageViewer`
1300
+ Import: `import { ImageViewer } from 'ywana-core8'`
1301
+ Story: — (sin fichero .stories.jsx)
1302
+
1303
+ Propósito: Visor de imagen con zoom/rotación/pan controlado imperativamente vía `ref`.
1304
+
1305
+ Props: `image` (**requerido**), `ref`.
1306
+
1307
+ Métodos del ref: `zoomIn()`, `zoomOut()`, `resetZoom()`, `rotateLeft()`, `rotateRight()`, `flipHorizontal()`, `flipVertical()`, `resetTransform()`.
1308
+
1309
+ Ejemplo mínimo:
1310
+ ```jsx
1311
+ const imgRef = useRef()
1312
+ <ImageViewer ref={imgRef} image="https://example.com/image.jpg" />
1313
+ // imgRef.current.zoomIn()
1314
+ ```
1315
+
1316
+ ---
1317
+
1318
+ ### `QueryBuilder`, `QuerySerializer`
1319
+ Import: `import { QueryBuilder, QuerySerializer } from 'ywana-core8'`
1320
+ Story: [src/widgets/querybuilder/QueryBuilder.stories.jsx](src/widgets/querybuilder/QueryBuilder.stories.jsx)
1321
+
1322
+ Propósito: Constructor visual de queries (cláusulas + operadores + valores según tipo de campo) y utilidad de (de)serialización a filtros planos.
1323
+
1324
+ Props (`QueryBuilder`): `fields` (`[{id, label, type, operators}]`, **requerido**), `descriptor` (`{logic, clauses}`, controlado), `onChange(descriptor)`.
1325
+
1326
+ `QuerySerializer` (no es un componente, es un objeto utilitario): `toFilters(descriptor)`, `fromFilters(filters)`, `empty()`.
1327
+
1328
+ Ejemplo mínimo:
1329
+ ```jsx
1330
+ <QueryBuilder
1331
+ fields={[{ id: 'name', label: 'Name', type: 'text' }]}
1332
+ descriptor={{ logic: 'AND', clauses: [] }}
1333
+ onChange={(q) => setQuery(q)}
1334
+ />
1335
+ const filters = QuerySerializer.toFilters(descriptor) // { name: 'John' }
1336
+ ```
1337
+
1338
+ Notas: Los operadores disponibles dependen del `type` declarado en cada field.
1339
+
1340
+ ---
1341
+
1342
+ ### `AsyncImage`, `useInView`
1343
+ Import: `import { AsyncImage, useInView } from 'ywana-core8'`
1344
+ Story: [src/widgets/asyncimage/AsyncImage.stories.jsx](src/widgets/asyncimage/AsyncImage.stories.jsx)
1345
+
1346
+ Propósito: `AsyncImage` es una `<img>` con estados carga/error y cancelación automática (AbortController) al cambiar `src` o desmontar. `useInView` es un hook de `IntersectionObserver` para lazy-loading, pensado para combinarse con `AsyncImage`.
1347
+
1348
+ Props (`AsyncImage`): `src` (**requerido**), `alt`, `defaultStatus` ('loaded'|'error'), `onStatusChange(status)`, `loadingSlot`/`errorSlot` (ReactNode custom).
1349
+
1350
+ `useInView(options = {})` → `{ ref, inView }` (options soporta `rootMargin` de `IntersectionObserverInit`).
1351
+
1352
+ Ejemplo mínimo:
1353
+ ```jsx
1354
+ const { ref, inView } = useInView({ rootMargin: '200px' })
1355
+ return <div ref={ref}>{inView && <AsyncImage src={url} onStatusChange={setStatus} />}</div>
1356
+ ```
1357
+
1358
+ ---
1359
+
1360
+ ### `Chat`
1361
+ Import: `import { Chat } from 'ywana-core8'`
1362
+ Story: [src/widgets/chat/chat.stories.jsx](src/widgets/chat/chat.stories.jsx)
1363
+
1364
+ Propósito: Chat genérico (lista de mensajes + input), sin backend integrado.
1365
+
1366
+ Props: `messages` (`[{user, userName, text, date}]`, controlado), `user` (id del usuario actual, **requerido**), `readOnly` (false — oculta el input), `emptyMessage`, `formatDate(isoString)`, `theme`, `messageBg`.
1367
+
1368
+ Callbacks: `onSend({user, text, date})`
1369
+
1370
+ Ejemplo mínimo:
1371
+ ```jsx
1372
+ <Chat messages={chatMessages} user={currentUserId} onSend={(msg) => sendMessage(msg)} />
1373
+ ```
1374
+
1375
+ Notas: Detecta mensajes propios comparando `message.user === user`. Ctrl/Cmd+Enter envía.
1376
+
1377
+ ---
1378
+
1379
+ ### `FileExplorer`
1380
+ Import: `import { FileExplorer } from 'ywana-core8'`
1381
+ Story: [src/widgets/explorer/Explorer.stories.jsx](src/widgets/explorer/Explorer.stories.jsx)
1382
+
1383
+ Propósito: Explorador de ficheros completo (árbol de carpetas, vistas grid/tabla, búsqueda, selección múltiple/marcado con barra de acciones, drag&drop, paneles de edición lateral de fichero y de carpeta, agrupación, temas).
1384
+
1385
+ Props destacadas: `files`, `folders`, `columns`, `onSelectFile`/`onOpenFile`/`onDeleteFile`/`onRenameFile`, `multiSelect`, `checkable`/`checkedFiles`/`onCheckFiles`/`checkedActions`, `searchBy`/`globalSearch`, `theme`, `groupBy`/`groupMode`, `editorComponent`/`folderEditorComponent`, `sidebarWidth`/`collapsible`/`menuActions`, `defaultFolderEditorCollapsed`.
1386
+
1387
+ Ejemplo mínimo:
1388
+ ```jsx
1389
+ <FileExplorer files={fileList} folders={folderTree} defaultFolder="images" onSelectFile={(f) => openFile(f)} />
1390
+ ```
1391
+
1392
+ **Documentación completa**: [src/widgets/explorer/Explorer.doc.md](src/widgets/explorer/Explorer.doc.md) — modelo de datos, las 50+ props, layout, recetas y errores frecuentes. Consulta ese fichero antes de integrar `FileExplorer` en profundidad; esta entrada es solo un resumen.