@boxcustodia/library 2.0.0-alpha.13 → 2.0.0-alpha.14
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/index.cjs.js +1 -138
- package/dist/index.d.ts +1083 -715
- package/dist/index.es.js +7077 -56175
- package/dist/theme.css +1 -1
- package/package.json +34 -26
- package/src/__doc__/Examples.tsx +1 -1
- package/src/__doc__/Intro.mdx +3 -3
- package/src/__doc__/Tabs.mdx +112 -0
- package/src/__doc__/V2.mdx +1246 -0
- package/src/components/accordion/accordion.stories.tsx +143 -0
- package/src/components/accordion/accordion.tsx +135 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/alert/alert.stories.tsx +24 -4
- package/src/components/alert/alert.tsx +17 -9
- package/src/components/alert-dialog/alert-dialog.stories.tsx +24 -0
- package/src/components/alert-dialog/alert-dialog.test.tsx +1 -1
- package/src/components/alert-dialog/alert-dialog.tsx +58 -10
- package/src/components/auto-complete/auto-complete.stories.tsx +616 -200
- package/src/components/auto-complete/auto-complete.tsx +420 -68
- package/src/components/auto-complete/index.ts +0 -1
- package/src/components/avatar/avatar.stories.tsx +162 -21
- package/src/components/avatar/avatar.tsx +79 -20
- package/src/components/button/button.stories.tsx +219 -294
- package/src/components/button/button.test.tsx +10 -17
- package/src/components/button/button.tsx +78 -19
- package/src/components/button/components/base-button.tsx +30 -53
- package/src/components/button/index.ts +0 -1
- package/src/components/calendar/calendar.stories.tsx +1 -1
- package/src/components/calendar/calendar.tsx +4 -4
- package/src/components/card/card.stories.tsx +141 -69
- package/src/components/card/card.tsx +155 -54
- package/src/components/center/center.stories.tsx +22 -39
- package/src/components/checkbox/checkbox.stories.tsx +25 -5
- package/src/components/checkbox/checkbox.tsx +76 -15
- package/src/components/checkbox-group/checkbox-group.stories.tsx +116 -28
- package/src/components/checkbox-group/checkbox-group.tsx +84 -3
- package/src/components/combobox/combobox.stories.tsx +33 -23
- package/src/components/combobox/combobox.tsx +119 -103
- package/src/components/date-picker/date-input.stories.tsx +14 -6
- package/src/components/date-picker/date-input.tsx +2 -2
- package/src/components/date-picker/date-picker.model.ts +13 -4
- package/src/components/date-picker/date-picker.stories.tsx +38 -12
- package/src/components/date-picker/date-picker.tsx +28 -14
- package/src/components/dialog/dialog.stories.tsx +18 -0
- package/src/components/dialog/dialog.test.tsx +1 -1
- package/src/components/dialog/dialog.tsx +51 -20
- package/src/components/divider/divider.stories.tsx +6 -0
- package/src/components/dropzone/dropzone.stories.tsx +71 -90
- package/src/components/dropzone/dropzone.tsx +383 -105
- package/src/components/dropzone/index.ts +0 -1
- package/src/components/empty/empty.stories.tsx +165 -0
- package/src/components/empty/empty.tsx +156 -0
- package/src/components/empty/index.ts +1 -0
- package/src/components/field/field.stories.tsx +226 -3
- package/src/components/field/field.tsx +77 -42
- package/src/components/form/form.stories.tsx +320 -197
- package/src/components/form/form.tsx +3 -23
- package/src/components/index.ts +2 -6
- package/src/components/input/input.stories.tsx +5 -5
- package/src/components/input/input.tsx +4 -4
- package/src/components/kbd/kbd.stories.tsx +1 -0
- package/src/components/label/label.stories.tsx +16 -0
- package/src/components/label/label.tsx +13 -2
- package/src/components/loader/loader.stories.tsx +7 -5
- package/src/components/loader/loader.tsx +8 -3
- package/src/components/menu/menu-primitives.tsx +207 -196
- package/src/components/menu/menu.stories.tsx +276 -146
- package/src/components/menu/menu.tsx +146 -54
- package/src/components/number-input/number-input.stories.tsx +27 -4
- package/src/components/number-input/number-input.test.tsx +2 -2
- package/src/components/number-input/number-input.tsx +25 -29
- package/src/components/otp/index.ts +1 -0
- package/src/components/otp/otp.stories.tsx +209 -0
- package/src/components/otp/otp.tsx +100 -0
- package/src/components/pagination/index.ts +1 -0
- package/src/components/pagination/pagination.model.ts +2 -0
- package/src/components/pagination/pagination.stories.tsx +154 -59
- package/src/components/pagination/pagination.test.tsx +122 -57
- package/src/components/pagination/pagination.tsx +575 -77
- package/src/components/password/password.stories.tsx +18 -3
- package/src/components/password/password.tsx +26 -10
- package/src/components/popover/popover.stories.tsx +26 -5
- package/src/components/popover/popover.tsx +15 -23
- package/src/components/progress/progress.stories.tsx +1 -0
- package/src/components/radio-group/index.ts +1 -0
- package/src/components/radio-group/radio-group.stories.tsx +251 -0
- package/src/components/radio-group/radio-group.tsx +212 -0
- package/src/components/scroll-area/scroll-area.stories.tsx +1 -0
- package/src/components/select/select.stories.tsx +118 -19
- package/src/components/select/select.tsx +67 -62
- package/src/components/skeleton/skeleton.stories.tsx +1 -0
- package/src/components/stack/stack.stories.tsx +179 -89
- package/src/components/stack/stack.tsx +2 -2
- package/src/components/stepper/index.ts +1 -1
- package/src/components/stepper/stepper.stories.tsx +767 -83
- package/src/components/stepper/stepper.test.tsx +18 -18
- package/src/components/stepper/stepper.tsx +554 -0
- package/src/components/switch/switch.stories.tsx +15 -1
- package/src/components/switch/switch.tsx +17 -4
- package/src/components/table/index.ts +0 -2
- package/src/components/table/table.stories.tsx +131 -18
- package/src/components/table/table.test.tsx +1 -1
- package/src/components/table/table.tsx +183 -77
- package/src/components/tabs/tabs.stories.tsx +373 -155
- package/src/components/tabs/tabs.test.tsx +12 -12
- package/src/components/tabs/tabs.tsx +72 -149
- package/src/components/tag/index.ts +0 -1
- package/src/components/tag/tag.stories.tsx +155 -120
- package/src/components/tag/tag.tsx +47 -95
- package/src/components/textarea/textarea.stories.tsx +8 -22
- package/src/components/textarea/textarea.tsx +17 -79
- package/src/components/timeline/timeline.stories.tsx +323 -42
- package/src/components/timeline/timeline.tsx +359 -132
- package/src/components/toast/toast.stories.tsx +1 -0
- package/src/components/tooltip/tooltip.tsx +11 -9
- package/src/components/tree/index.ts +0 -1
- package/src/components/tree/tree.stories.tsx +365 -408
- package/src/components/tree/tree.test.tsx +163 -0
- package/src/components/tree/tree.tsx +212 -36
- package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +5 -5
- package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +1 -3
- package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +6 -6
- package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +1 -1
- package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +1 -1
- package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +1 -1
- package/src/hooks/usePagination/usePagination.tsx +36 -24
- package/src/styles/theme.css +1 -1
- package/src/utils/form.tsx +67 -37
- package/src/utils/index.ts +1 -1
- package/src/__doc__/Migration.mdx +0 -451
- package/src/components/auto-complete/auto-complete-primitives.tsx +0 -155
- package/src/components/background-image/background-image.stories.tsx +0 -21
- package/src/components/background-image/background-image.test.tsx +0 -29
- package/src/components/background-image/background-image.tsx +0 -23
- package/src/components/background-image/index.ts +0 -1
- package/src/components/button/button.variants.ts +0 -44
- package/src/components/button/components/loader-overlay.tsx +0 -21
- package/src/components/button/components/loading-icon.tsx +0 -47
- package/src/components/dropzone/upload-primitives.tsx +0 -310
- package/src/components/dropzone/use-dropzone.ts +0 -122
- package/src/components/empty-state/empty-state.stories.tsx +0 -56
- package/src/components/empty-state/empty-state.tsx +0 -39
- package/src/components/empty-state/index.ts +0 -1
- package/src/components/heading/heading.stories.tsx +0 -74
- package/src/components/heading/heading.tsx +0 -28
- package/src/components/heading/heading.variants.ts +0 -27
- package/src/components/heading/index.ts +0 -1
- package/src/components/kbd/kbd.variants.ts +0 -26
- package/src/components/menu/util/render-menu-item.tsx +0 -54
- package/src/components/multi-select/hooks/use-multi-select.ts +0 -66
- package/src/components/multi-select/index.ts +0 -1
- package/src/components/multi-select/multi-select.stories.tsx +0 -294
- package/src/components/multi-select/multi-select.tsx +0 -300
- package/src/components/multi-select/multi-select.variants.ts +0 -22
- package/src/components/pagination/components/pagination-option.tsx +0 -27
- package/src/components/show/index.ts +0 -1
- package/src/components/show/show.stories.tsx +0 -197
- package/src/components/show/show.test.tsx +0 -41
- package/src/components/show/show.tsx +0 -16
- package/src/components/stepper/Stepper.tsx +0 -190
- package/src/components/stepper/context/stepper-context.tsx +0 -11
- package/src/components/table/table-primitives.tsx +0 -122
- package/src/components/table/table.model.ts +0 -20
- package/src/components/table-pagination/index.ts +0 -2
- package/src/components/table-pagination/table-pagination.model.ts +0 -2
- package/src/components/table-pagination/table-pagination.stories.tsx +0 -23
- package/src/components/table-pagination/table-pagination.test.tsx +0 -32
- package/src/components/table-pagination/table-pagination.tsx +0 -108
- package/src/components/tabs/context/tabs-context.tsx +0 -14
- package/src/components/tag/tag.variants.ts +0 -31
- package/src/components/timeline/timeline-status.ts +0 -5
- package/src/components/tree/hooks/use-controllable-tree-state.ts +0 -80
- package/src/components/tree/tree-primitives.tsx +0 -126
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Documentation/Guía de Migración v1 → v2" />
|
|
4
|
+
|
|
5
|
+
# Migración v1 → v2
|
|
6
|
+
|
|
7
|
+
Si venís usando `@boxcustodia/library@1.x`, esta guía te lleva paso a paso a `2.x`. Aplicá los cambios en orden y deberías quedar funcional.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Instalación
|
|
12
|
+
|
|
13
|
+
v2 todavía está en alpha. Instalá con el tag `@next`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i @boxcustodia/library@next
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
La librería trae todo lo que necesita para funcionar. No tenés que instalar peers manualmente.
|
|
20
|
+
|
|
21
|
+
### Instalá el MCP de v2
|
|
22
|
+
|
|
23
|
+
El MCP (Model Context Protocol) le da a Claude acceso directo a la documentación de los componentes de v2: props, variantes, ejemplos y patrones. Sin él, Claude inventa props y genera código que no funciona contra esta librería. Ver la página **Documentation/MCP** para detalles (incluye la instrucción opcional para `CLAUDE.md`).
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx mcp-add --type http \
|
|
27
|
+
--url "https://v2--69de51f0642746d263d5c46f.chromatic.com/mcp" \
|
|
28
|
+
--client-id "cdf3737dff9d485485968e50b63fd8b4" \
|
|
29
|
+
--scope global \
|
|
30
|
+
--clients "claude code"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Se instala una vez por máquina (global). El instalador pregunta el server name — respondé `boxcustodia-library`; los otros prompts quedan vacíos. La primera consulta abre el browser para autenticar contra Chromatic (pedile acceso a Franco si no lo tenés).
|
|
34
|
+
|
|
35
|
+
### Actualizá el import del CSS
|
|
36
|
+
|
|
37
|
+
v2 expone un subpath estable para el CSS. Cambiá el `@import` en tu `index.css`:
|
|
38
|
+
|
|
39
|
+
```diff filename="index.css"
|
|
40
|
+
@import "tailwindcss";
|
|
41
|
+
- @import "@boxcustodia/library/dist/index.css";
|
|
42
|
+
+ @import "@boxcustodia/library/styles";
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Migración de Radix UI a Base UI
|
|
48
|
+
|
|
49
|
+
v2 reemplaza **Radix UI** por **[Base UI](https://base-ui.com)** como capa de primitivos. Este cambio trae dos breaking changes globales que aplican a todos los componentes.
|
|
50
|
+
|
|
51
|
+
### 1. `asChild` → prop `render`
|
|
52
|
+
|
|
53
|
+
Base UI no usa el patrón `asChild`. Para renderizar un componente propio en lugar del elemento por defecto, usá la prop `render`:
|
|
54
|
+
|
|
55
|
+
```diff
|
|
56
|
+
- <DialogTrigger asChild>
|
|
57
|
+
- <Button>Abrir</Button>
|
|
58
|
+
- </DialogTrigger>
|
|
59
|
+
+ <DialogTrigger render={<Button>Abrir</Button>} />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. `onChange` → `onValueChange`
|
|
63
|
+
|
|
64
|
+
Base UI renombra los callbacks de control. El prop `onChange` desaparece — reemplazalo por `onValueChange`. Los demás siguen igual pero ahora reciben `eventDetails` como segundo argumento:
|
|
65
|
+
|
|
66
|
+
```diff
|
|
67
|
+
- <Select value={value} onChange={(v) => setValue(v)} />
|
|
68
|
+
+ <Select value={value} onValueChange={(v) => setValue(v)} />
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Firmas completas de Base UI:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
onValueChange: (value, eventDetails) => void
|
|
75
|
+
onOpenChange: (open, eventDetails) => void
|
|
76
|
+
onPressedChange: (pressed, eventDetails) => void
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Componentes eliminados
|
|
82
|
+
|
|
83
|
+
Los siguientes componentes fueron removidos en v2:
|
|
84
|
+
|
|
85
|
+
- `BackgroundImage`
|
|
86
|
+
- `Heading`
|
|
87
|
+
- `MultiSelect`
|
|
88
|
+
- `Show`
|
|
89
|
+
- `TablePagination`
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Componentes nuevos
|
|
94
|
+
|
|
95
|
+
- `Accordion`
|
|
96
|
+
- `Alert`
|
|
97
|
+
- `CheckboxGroup`
|
|
98
|
+
- `DateInput`
|
|
99
|
+
- `OTP`
|
|
100
|
+
- `Progress`
|
|
101
|
+
- `RadioGroup`
|
|
102
|
+
- `ScrollArea`
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Subpaths públicos disponibles
|
|
107
|
+
|
|
108
|
+
- `@boxcustodia/library` — componentes, hooks, utilidades.
|
|
109
|
+
- `@boxcustodia/library/styles` — CSS principal (theme + variantes + estilos de componentes).
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Nuevo patrón de composición y estilado
|
|
114
|
+
|
|
115
|
+
v2 cambia la forma de extender componentes. Estos dos cambios aplican a **todos los composites** de la librería (Autocomplete, Combobox, Select, Dialog, AlertDialog, etc.), no a un componente puntual.
|
|
116
|
+
|
|
117
|
+
### 1. Se eliminan los `xProps` pass-through
|
|
118
|
+
|
|
119
|
+
En v1, los composites exponían props pass-through tipo `triggerProps`, `contentProps`, `inputProps`, `popupProps`, `itemProps`, etc. para reenviar atributos al primitivo correspondiente.
|
|
120
|
+
|
|
121
|
+
```diff
|
|
122
|
+
- <Dialog
|
|
123
|
+
- triggerProps={{ className: "..." }}
|
|
124
|
+
- contentProps={{ onEscapeKeyDown: handler, className: "..." }}
|
|
125
|
+
- titleProps={{ className: "..." }}
|
|
126
|
+
- >
|
|
127
|
+
- Hola
|
|
128
|
+
- </Dialog>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Por qué se elimina:**
|
|
132
|
+
|
|
133
|
+
- **Abstracción que filtra.** Para usar `contentProps` el consumer tiene que conocer el primitivo subyacente, qué props acepta y qué se le omitió. Es decir, ya pagó el costo cognitivo del primitivo — el composite dejó de aportar valor.
|
|
134
|
+
- **Falso intermedio.** Si necesitás tocar 3+ pass-through props, ya estás reescribiendo la composición a mano — solo que con peor ergonomía que usando los primitivos directamente.
|
|
135
|
+
- **Mantenimiento.** Cada cambio en un primitivo se propagaba al tipo del composite y multiplicaba la matriz de casos a probar.
|
|
136
|
+
- **Duplicación de API.** Cada composite terminaba clonando la API de N primitivos prop por prop.
|
|
137
|
+
|
|
138
|
+
**Qué hacer en su lugar:**
|
|
139
|
+
|
|
140
|
+
- Para **cambios de estilo** → usá la nueva prop `classNames` (ver abajo).
|
|
141
|
+
- Para **cambios de comportamiento, estructura o props no triviales** → usá los primitivos directamente. Cada componente exporta sus primitivos (`DialogTrigger`, `DialogContent`, `AutocompletePopup`, etc.) y la composición es la API extendida real.
|
|
142
|
+
|
|
143
|
+
```diff
|
|
144
|
+
+ // Estructura custom: usá primitivos directamente
|
|
145
|
+
+ <DialogRoot>
|
|
146
|
+
+ <DialogTrigger render={<Button variant="outline" />}>Abrir</DialogTrigger>
|
|
147
|
+
+ <DialogPortal>
|
|
148
|
+
+ <DialogBackdrop />
|
|
149
|
+
+ <DialogPopup onEscapeKeyDown={handler}>
|
|
150
|
+
+ <DialogTitle>Hola</DialogTitle>
|
|
151
|
+
+ </DialogPopup>
|
|
152
|
+
+ </DialogPortal>
|
|
153
|
+
+ </DialogRoot>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 2. Nuevo escape hatch de estilado: `classNames`
|
|
157
|
+
|
|
158
|
+
Para el 80% de los casos de customización (solo CSS) v2 introduce una prop `classNames` namespaceada por slot. Reemplaza a las props sueltas `containerClassName`, `inputClassName`, `listClassName`, `itemClassName`, etc. que existían en v1.
|
|
159
|
+
|
|
160
|
+
```diff
|
|
161
|
+
- <Autocomplete
|
|
162
|
+
- containerClassName="w-72"
|
|
163
|
+
- inputClassName="h-12"
|
|
164
|
+
- listClassName="max-h-60"
|
|
165
|
+
- itemClassName="py-2"
|
|
166
|
+
- emptyClassName="italic"
|
|
167
|
+
- />
|
|
168
|
+
+ <Autocomplete
|
|
169
|
+
+ className="w-72 h-12"
|
|
170
|
+
+ classNames={{
|
|
171
|
+
+ popup: "max-h-60",
|
|
172
|
+
+ item: "py-2",
|
|
173
|
+
+ empty: "italic",
|
|
174
|
+
+ }}
|
|
175
|
+
+ />
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Convención:**
|
|
179
|
+
|
|
180
|
+
- `className` → estilá el elemento principal del composite (el campo visible, el trigger, etc.).
|
|
181
|
+
- `classNames` → objeto con un slot por cada parte interna estilable.
|
|
182
|
+
|
|
183
|
+
**Por qué este patrón y no props sueltas:**
|
|
184
|
+
|
|
185
|
+
- **API plana y predecible.** Un solo prop adicional (`classNames`) en lugar de N props con el sufijo `ClassName`.
|
|
186
|
+
- **Consistencia entre componentes.** Mismo shape en `Autocomplete`, `Combobox`, `Select`, `Dialog`, etc. — aprendés una vez.
|
|
187
|
+
- **Autocompletado.** TypeScript expone los slots disponibles al destructurar el objeto.
|
|
188
|
+
- **Extensible.** Agregar un slot nuevo no rompe nada.
|
|
189
|
+
- **No es escape hatch de comportamiento.** `classNames` solo toca estilo — para comportamiento, primitivos.
|
|
190
|
+
|
|
191
|
+
Los slots disponibles están documentados en la página de cada componente.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Componentes
|
|
196
|
+
|
|
197
|
+
Breaking changes específicos por componente.
|
|
198
|
+
|
|
199
|
+
### 1. Dialog
|
|
200
|
+
|
|
201
|
+
`xProps` eliminadas — usá `className` y `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
202
|
+
|
|
203
|
+
Slots de `classNames` disponibles: `backdrop`, `viewport`, `header`, `title`, `description`, `footer`, `closeButton`.
|
|
204
|
+
|
|
205
|
+
**`DialogContent` → `DialogPopup`** (rename del primitive — Base UI usa `Popup` como naming). Si componés con primitivos, actualizá:
|
|
206
|
+
|
|
207
|
+
```diff
|
|
208
|
+
- import { DialogRoot, DialogTrigger, DialogContent } from "@boxcustodia/library";
|
|
209
|
+
+ import { DialogRoot, DialogTrigger, DialogPopup } from "@boxcustodia/library";
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**`trigger: ReactNode → ReactElement`.** v1 aceptaba cualquier `ReactNode`; v2 exige un `ReactElement` porque Base UI usa `render={trigger}` y necesita un componente para clonar props:
|
|
213
|
+
|
|
214
|
+
```diff
|
|
215
|
+
- <Dialog trigger="Open">…</Dialog>
|
|
216
|
+
+ <Dialog trigger={<Button>Open</Button>}>…</Dialog>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 2. AlertDialog
|
|
220
|
+
|
|
221
|
+
Migrado al nuevo patrón — se eliminaron los `xProps` pass-through (ver sección [Nuevo patrón de composición y estilado](#nuevo-patrón-de-composición-y-estilado)). Para estilo, usá `className` y `classNames`. Para cambios estructurales (layout custom, slots extra), usá los primitivos.
|
|
222
|
+
|
|
223
|
+
Slots de `classNames` disponibles: `backdrop`, `header`, `title`, `description`, `footer`, `closeButton`, `actionButton`.
|
|
224
|
+
|
|
225
|
+
### 3. Autocomplete
|
|
226
|
+
|
|
227
|
+
**Renombrado.** `AutoComplete` → `Autocomplete` (la `C` mayúscula del medio queda en minúscula). Actualizá los imports:
|
|
228
|
+
|
|
229
|
+
```diff
|
|
230
|
+
- import { AutoComplete } from "@boxcustodia/library";
|
|
231
|
+
+ import { Autocomplete } from "@boxcustodia/library";
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Migrado al nuevo patrón — se eliminaron los `xProps` pass-through (ver sección [Nuevo patrón de composición y estilado](#nuevo-patrón-de-composición-y-estilado)). Para estilo, usá `className` y `classNames`. Para cambios estructurales (layout custom, slots extra, búsqueda async, grupos), usá los primitivos.
|
|
235
|
+
|
|
236
|
+
Slots de `classNames` disponibles: `popup`, `list`, `item`, `empty`, `status`.
|
|
237
|
+
|
|
238
|
+
**`onSelect` eliminado.** La prop `onSelect: (value: string, item: AutoCompleteItem) => any` ya no existe. Usá `onValueChange` para controlar la selección:
|
|
239
|
+
|
|
240
|
+
```diff
|
|
241
|
+
- <Autocomplete onSelect={(value, item) => handleSelect(value, item)} />
|
|
242
|
+
+ <Autocomplete onValueChange={(item) => handleSelect(item)} />
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`onValueChange` recibe el item completo (o `null` cuando se limpia el campo).
|
|
246
|
+
|
|
247
|
+
### 4. Avatar
|
|
248
|
+
|
|
249
|
+
Migrado al nuevo patrón — se eliminaron los `xProps` pass-through (ver sección [Nuevo patrón de composición y estilado](#nuevo-patrón-de-composición-y-estilado)). Para estilo, usá `className` y `classNames`. Para cambios estructurales (layout custom, slots extra), usá los primitivos.
|
|
250
|
+
|
|
251
|
+
Slots de `classNames` disponibles: `image`, `fallback`, `badge`.
|
|
252
|
+
|
|
253
|
+
**`variant` eliminada.** v1 controlaba la forma con `variant: "circle" | "square"`. v2 no acepta variante — todos los avatares son circulares por defecto. Para esquinas cuadradas, override con `className`:
|
|
254
|
+
|
|
255
|
+
```diff
|
|
256
|
+
- <Avatar variant="square" src={src} alt={alt} />
|
|
257
|
+
+ <Avatar className="rounded-xl" src={src} alt={alt} />
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 5. Button
|
|
261
|
+
|
|
262
|
+
**`asChild` eliminado.** Base UI no usa `asChild` (ver sección [Migración de Radix UI a Base UI](#migración-de-radix-ui-a-base-ui)). Para renderizar un elemento custom, usá la prop `render`:
|
|
263
|
+
|
|
264
|
+
```diff
|
|
265
|
+
- <Button asChild><a href="/foo">Ir</a></Button>
|
|
266
|
+
+ <Button render={<a href="/foo">Ir</a>} />
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Si lo que querés es aplicar los estilos del botón a otro componente, importá `buttonVariants` y componé las clases:
|
|
270
|
+
|
|
271
|
+
```tsx
|
|
272
|
+
import { buttonVariants } from "@boxcustodia/library";
|
|
273
|
+
|
|
274
|
+
<a href="/foo" className={buttonVariants({ variant: "outline" })}>
|
|
275
|
+
Ir
|
|
276
|
+
</a>;
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**`showLoader` eliminado.** El loader se activa automáticamente cuando `onClick` devuelve una `Promise`. Para deshabilitarlo, pasá un `onClick` sincrónico:
|
|
280
|
+
|
|
281
|
+
```diff
|
|
282
|
+
- <Button showLoader={false} onClick={async () => await save()}>Guardar</Button>
|
|
283
|
+
+ <Button onClick={() => { void save(); }}>Guardar</Button>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**`iconPosition` eliminado.** La posición del ícono ahora se controla con el atributo `data-icon` en el elemento del ícono:
|
|
287
|
+
|
|
288
|
+
```diff
|
|
289
|
+
- <Button icon={<TrashIcon />} iconPosition="inline-end">Borrar</Button>
|
|
290
|
+
+ <Button icon={<TrashIcon data-icon="inline-end" />}>Borrar</Button>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Valores aceptados: `inline-start` (default) o `inline-end`.
|
|
294
|
+
|
|
295
|
+
**`shape` eliminado.** Para esquinas custom, override con `className="rounded-..."`.
|
|
296
|
+
|
|
297
|
+
**`loaderReplace` y `append` eliminados.** El loader siempre **reemplaza** el contenido (efecto antes conocido como `loaderReplace`). El efecto `append` (loader al lado del contenido) ya no se soporta.
|
|
298
|
+
|
|
299
|
+
### 6. Checkbox
|
|
300
|
+
|
|
301
|
+
`xProps` eliminadas — usá `className` y `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
302
|
+
|
|
303
|
+
**`onChange` → `onCheckedChange`** (convención Base UI para binario):
|
|
304
|
+
|
|
305
|
+
```diff
|
|
306
|
+
- <Checkbox checked={value} onChange={setValue} />
|
|
307
|
+
+ <Checkbox checked={value} onCheckedChange={setValue} />
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**`size` eliminada.** Customizá el tamaño con `className` o estilos:
|
|
311
|
+
|
|
312
|
+
```diff
|
|
313
|
+
- <Checkbox size="lg" />
|
|
314
|
+
+ <Checkbox className="size-5" />
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### 7. Calendar
|
|
318
|
+
|
|
319
|
+
`Calendar` en v2 es un wrapper fino sobre [`react-day-picker`](https://daypicker.dev) (`DayPicker`). El componente adopta su API nativa, lo que implica varios breaking changes respecto a v1.
|
|
320
|
+
|
|
321
|
+
**Props de selección renombradas.** El par `value` / `onChange` se reemplaza por `selected` / `onSelect`:
|
|
322
|
+
|
|
323
|
+
```diff
|
|
324
|
+
- <Calendar mode="single" value={date} onChange={setDate} />
|
|
325
|
+
+ <Calendar mode="single" selected={date} onSelect={setDate} />
|
|
326
|
+
|
|
327
|
+
- <Calendar mode="range" value={range} onChange={setRange} />
|
|
328
|
+
+ <Calendar mode="range" selected={range} onSelect={setRange} />
|
|
329
|
+
|
|
330
|
+
- <Calendar mode="multiple" value={dates} onChange={setDates} />
|
|
331
|
+
+ <Calendar mode="multiple" selected={dates} onSelect={setDates} />
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Shape de `DateRange` cambió.** v1 usaba `{ start, end }`; DayPicker usa `{ from, to }`. Actualizá los datos al construir y leer:
|
|
335
|
+
|
|
336
|
+
```diff
|
|
337
|
+
- interface DateRange {
|
|
338
|
+
- start: Date | null;
|
|
339
|
+
- end: Date | null;
|
|
340
|
+
- }
|
|
341
|
+
+ interface DateRange {
|
|
342
|
+
+ from?: Date;
|
|
343
|
+
+ to?: Date;
|
|
344
|
+
+ }
|
|
345
|
+
|
|
346
|
+
- <Calendar mode="range" selected={{ start, end }} />
|
|
347
|
+
+ <Calendar mode="range" selected={{ from: start, to: end }} />
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Tipos de `selected` por modo (vienen de `react-day-picker`):
|
|
351
|
+
|
|
352
|
+
- `single` → `Date | undefined`
|
|
353
|
+
- `range` → `{ from?: Date; to?: Date } | undefined`
|
|
354
|
+
- `multiple` → `Date[] | undefined`
|
|
355
|
+
|
|
356
|
+
**Props `view`, `initialView` y `onChangeView` eliminadas.** v1 navegaba entre la grilla de días, la selección de mes y la selección de año con un patrón propio (`view: "month" | "year" | "decade"`). v2 usa el `captionLayout` de `DayPicker` (default `"dropdown"`) — los meses y años se navegan con un dropdown en la cabecera, no con cambios de vista.
|
|
357
|
+
|
|
358
|
+
```diff
|
|
359
|
+
- <Calendar view="month" onChangeView={setView} initialView="day" />
|
|
360
|
+
+ <Calendar captionLayout="dropdown" />
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Valores aceptados: `"label" | "dropdown" | "dropdown-months" | "dropdown-years"`.
|
|
364
|
+
|
|
365
|
+
**Prop `initialDate` eliminada.** Reemplazada por `defaultMonth` (prop nativa de DayPicker) — controla el mes que se muestra al montar el calendario.
|
|
366
|
+
|
|
367
|
+
```diff
|
|
368
|
+
- <Calendar initialDate={new Date(2025, 0, 1)} />
|
|
369
|
+
+ <Calendar defaultMonth={new Date(2025, 0, 1)} />
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**Prop `disabled` con semántica más amplia.** v1 solo aceptaba una función `(date: Date) => boolean`. v2 acepta el `Matcher` de DayPicker: función, `Date`, array de `Date`, rangos `{ from, to }`, días de semana `{ dayOfWeek: [0, 6] }`, etc. La forma de función sigue funcionando, pero ahora hay más opciones declarativas (ver [matchers de DayPicker](https://daypicker.dev/docs/selection-modes#disabled-dates)).
|
|
373
|
+
|
|
374
|
+
**xProps eliminadas.** Las props pass-through del composite (`goBackProps`, `goNextProps`, `changeViewProps`, `monthViewProps`) ya no existen (ver sección [Nuevo patrón de composición y estilado](#nuevo-patrón-de-composición-y-estilado)). Para estilar, usá `className` (al root del calendario) y `classNames` (objeto namespaceado con los slots de `DayPicker`: `day`, `weekday`, `month`, `nav`, etc. — ver [docs de DayPicker](https://daypicker.dev/docs/styling#class-names)).
|
|
375
|
+
|
|
376
|
+
**Sub-exports eliminados.** v1 exponía `MonthView`, `YearView`, `DecadeView`, `CalendarNavigation`, `useCalendar`, `useCalendarNavigation` y `CalendarContext` para composición custom. v2 los elimina — toda la composición interna queda en `DayPicker`. Si necesitabas algo de eso, usá los slots de `components` de DayPicker (`Day`, `MonthCaption`, etc.).
|
|
377
|
+
|
|
378
|
+
### 8. Combobox
|
|
379
|
+
|
|
380
|
+
`xProps` eliminadas — usá `className` y `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
381
|
+
|
|
382
|
+
Slots de `classNames` disponibles: `input`, `chips`, `chipsInput`, `popup`, `list`, `item`, `empty`.
|
|
383
|
+
|
|
384
|
+
**Props renombradas:**
|
|
385
|
+
|
|
386
|
+
- `valueKey: string` → `getId: (item) => string`
|
|
387
|
+
- `labelKey: string` → `getLabel: (item) => string`
|
|
388
|
+
- `renderOption` → `renderItem`
|
|
389
|
+
- `emptyMessage` → `emptyText`
|
|
390
|
+
- `searchPlaceholder` → `placeholder`
|
|
391
|
+
|
|
392
|
+
**Props eliminadas:**
|
|
393
|
+
|
|
394
|
+
- `searchProps` — en modo `multiple` el input de búsqueda vive en el popup, no en el trigger.
|
|
395
|
+
- `commandProps` — no tiene equivalente; el composite ya no envuelve `cmdk`. Para layouts custom, usá los primitives (`ComboboxRoot`, `ComboboxInput`, `ComboboxPopup`, etc.).
|
|
396
|
+
|
|
397
|
+
Los string keys pasan a ser funciones tipadas contra el genérico `TItem`. Sirven para campos anidados o valores derivados. Para shapes comunes (`{ id, name }`, `{ label, value }`, primitivos) los defaults los resuelven sin pasar nada.
|
|
398
|
+
|
|
399
|
+
`getId` reemplaza al `getValue` que existió en alphas previas — el nombre se eligió para reflejar el uso real (id para form value + React key) y evitar la colisión visual con la prop `value` del Combobox.
|
|
400
|
+
|
|
401
|
+
**`onChange` → `onValueChange` (cambio semántico).** Antes `value` / `onChange` trabajaban con el **key** (`string`). Ahora `value` / `onValueChange` trabajan con el **item completo** (o `null` al limpiar). Actualizá el estado del consumer:
|
|
402
|
+
|
|
403
|
+
```diff
|
|
404
|
+
- const [selectedId, setSelectedId] = useState<string>("");
|
|
405
|
+
+ const [selected, setSelected] = useState<Fruit | null>(null);
|
|
406
|
+
|
|
407
|
+
- <Combobox
|
|
408
|
+
- items={fruits}
|
|
409
|
+
- valueKey="id"
|
|
410
|
+
- labelKey="name"
|
|
411
|
+
- renderOption={(item) => item.name}
|
|
412
|
+
- emptyMessage="Sin resultados"
|
|
413
|
+
- value={selectedId}
|
|
414
|
+
- onChange={setSelectedId}
|
|
415
|
+
- />
|
|
416
|
+
+ <Combobox
|
|
417
|
+
+ items={fruits}
|
|
418
|
+
+ // getId / getLabel se pueden omitir si fruits es { id, name } — defaults los resuelven
|
|
419
|
+
+ renderItem={(item) => item.name}
|
|
420
|
+
+ emptyText="Sin resultados"
|
|
421
|
+
+ value={selected}
|
|
422
|
+
+ onValueChange={setSelected}
|
|
423
|
+
+ />
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### 9. Select
|
|
427
|
+
|
|
428
|
+
`xProps` eliminadas — usá `className` (estiliza el trigger) y `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
429
|
+
|
|
430
|
+
Slots de `classNames` disponibles: `popup`, `item`.
|
|
431
|
+
|
|
432
|
+
**Props renombradas:**
|
|
433
|
+
|
|
434
|
+
- `valueKey: string` → `getId: (item) => string`
|
|
435
|
+
- `labelKey: string` → `getLabel: (item) => string`
|
|
436
|
+
- `renderOption` → `renderItem`
|
|
437
|
+
- `disabledItem` → `getDisabled` (alineado con la familia `get*`)
|
|
438
|
+
|
|
439
|
+
Para shapes comunes (`{ id, name }`, `{ label, value }`, primitivos) los defaults resuelven todo — `getLabel`/`getId` solo hacen falta para shapes custom.
|
|
440
|
+
|
|
441
|
+
**`onChange` → `onValueChange` (cambio semántico).** Mismo cambio que en Combobox: `value` / `onValueChange` ahora trabajan con el **item completo** (o `null`) en lugar del key. Si guardabas el id como string, ahora guardás el item:
|
|
442
|
+
|
|
443
|
+
```diff
|
|
444
|
+
- const [animalId, setAnimalId] = useState<string>("");
|
|
445
|
+
+ const [animal, setAnimal] = useState<Animal | null>(null);
|
|
446
|
+
|
|
447
|
+
- <Select
|
|
448
|
+
- items={animals}
|
|
449
|
+
- valueKey="id"
|
|
450
|
+
- labelKey="name"
|
|
451
|
+
- value={animalId}
|
|
452
|
+
- onChange={setAnimalId}
|
|
453
|
+
- />
|
|
454
|
+
+ <Select
|
|
455
|
+
+ items={animals}
|
|
456
|
+
+ value={animal}
|
|
457
|
+
+ onValueChange={setAnimal}
|
|
458
|
+
+ />
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### 10. Dropzone
|
|
462
|
+
|
|
463
|
+
Se agrega `classNames` para estilar los slots internos del composite (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
464
|
+
|
|
465
|
+
Slots de `classNames` disponibles: `trigger`, `icon`, `label`, `content`.
|
|
466
|
+
|
|
467
|
+
**`asChild` eliminado de `DropzoneTrigger`.** Estaba marcado deprecated en v1. v2 usa solo la prop `render` (Base UI):
|
|
468
|
+
|
|
469
|
+
```diff
|
|
470
|
+
- <DropzoneTrigger asChild>
|
|
471
|
+
- <Button>Upload</Button>
|
|
472
|
+
- </DropzoneTrigger>
|
|
473
|
+
+ <DropzoneTrigger render={<Button>Upload</Button>} />
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**`onChangeFiles` → `onFilesChange`.** Renombrada para alinearse con la convención Base UI (`onValueChange`, `onOpenChange`, etc.):
|
|
477
|
+
|
|
478
|
+
```diff
|
|
479
|
+
- <Dropzone onChangeFiles={(files) => setFiles(files)} />
|
|
480
|
+
+ <Dropzone onFilesChange={(files) => setFiles(files)} />
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### 11. DatePicker
|
|
484
|
+
|
|
485
|
+
En v1, `DatePicker` era un input tipeable con popover de calendario. En v2 se divide en dos componentes con responsabilidades separadas:
|
|
486
|
+
|
|
487
|
+
- **`DatePicker`** — trigger tipo botón que abre un popover con el calendario. Soporta tres modos vía discriminated union: `single`, `range`, `multiple`.
|
|
488
|
+
- **`DateInput`** — nuevo componente para el caso "input tipeable con auto-máscara `DD/MM/YYYY`" que cubría el `DatePicker` de v1. Solo modo `single`.
|
|
489
|
+
|
|
490
|
+
Si en v1 usabas `DatePicker` para escribir la fecha a mano, migrá a `DateInput`:
|
|
491
|
+
|
|
492
|
+
```diff
|
|
493
|
+
- <DatePicker value={date} onChange={setDate} />
|
|
494
|
+
+ <DateInput value={date} onValueChange={setDate} />
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**`onChange` → `onValueChange`** (ver [Migración de Radix UI a Base UI](#migración-de-radix-ui-a-base-ui)).
|
|
498
|
+
|
|
499
|
+
**Shape de `DateRange` cambió** — el modo `range` ahora trabaja con el shape de DayPicker `{ from, to }` (ver [Calendar](#6-calendar)). En el `DatePicker` el shape sigue siendo `{ start, end }` para no romper consumers, pero internamente se mapea a `{ from, to }` al pasar al `Calendar`.
|
|
500
|
+
|
|
501
|
+
**Footer por defecto eliminado.** v1 renderizaba siempre un footer con botones `Borrar` / `Hoy` (controlable con `hideFooter`). v2 no renderiza ningún footer salvo que pases `renderFooter` explícito. Si querés el comportamiento anterior, implementalo via `renderFooter`:
|
|
502
|
+
|
|
503
|
+
```diff
|
|
504
|
+
- <DatePicker value={date} onChange={setDate} hideFooter />
|
|
505
|
+
+ <DatePicker mode="single" value={date} onValueChange={setDate} />
|
|
506
|
+
|
|
507
|
+
- <DatePicker value={date} onChange={setDate} />
|
|
508
|
+
+ <DatePicker
|
|
509
|
+
+ mode="single"
|
|
510
|
+
+ value={date}
|
|
511
|
+
+ onValueChange={setDate}
|
|
512
|
+
+ renderFooter={({ clear, selectToday }) => (
|
|
513
|
+
+ <div className="flex justify-between">
|
|
514
|
+
+ <Button variant="ghost" onClick={clear}>Borrar</Button>
|
|
515
|
+
+ <Button variant="ghost" onClick={selectToday}>Hoy</Button>
|
|
516
|
+
+ </div>
|
|
517
|
+
+ )}
|
|
518
|
+
+ />
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**`renderFooter` ahora recibe un discriminated union por `mode`.** `selectToday` solo existe en modo `single` — narrow `mode` antes de usarlo:
|
|
522
|
+
|
|
523
|
+
```tsx
|
|
524
|
+
renderFooter={(props) => {
|
|
525
|
+
if (props.mode !== "single") return null;
|
|
526
|
+
return <Button onClick={props.selectToday}>Hoy</Button>;
|
|
527
|
+
}}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
**Navegación del calendario interno cambió.** Las props `view`, `initialView`, `onChangeView` e `initialDate` del Calendar de v1 ya no existen — el popover navega meses y años con un dropdown en la cabecera (ver [Calendar](#6-calendar) para detalles).
|
|
531
|
+
|
|
532
|
+
`xProps` eliminadas — usá `className` (estiliza el trigger) y `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
533
|
+
|
|
534
|
+
Slots de `classNames` disponibles: `popup`, `calendar`, `footer`.
|
|
535
|
+
|
|
536
|
+
### 12. Form
|
|
537
|
+
|
|
538
|
+
**Cambio grande.** v1 era un wrapper sobre Radix Slot + `react-hook-form` + `zod`. v2 usa directamente la API nativa del navegador (Base UI Form): el form es un `<form>` real con validación nativa (`required`, `pattern`, `minLength`, etc.) y `FormData` parseada al submit. No hay schema, no hay context de RHF, no hay re-renders por keystroke.
|
|
539
|
+
|
|
540
|
+
**`form` eliminado.** v1 esperaba el `UseFormReturn` de `react-hook-form` (que a su vez recibía un schema de zod). v2 no necesita ningún objeto externo: el estado vive en el DOM.
|
|
541
|
+
|
|
542
|
+
```diff
|
|
543
|
+
- const form = useForm(MySchema, { defaultValues });
|
|
544
|
+
- <Form form={form} onSubmit={(data) => save(data)}>
|
|
545
|
+
+ <Form onFormSubmit={(data) => save(data)}>
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**`onSubmit` → `onFormSubmit`** (convención Base UI). La firma cambia: ya no recibe el shape tipado del schema sino un `Record<string, unknown>` parseado desde `FormData`.
|
|
549
|
+
|
|
550
|
+
```diff
|
|
551
|
+
- <Form onSubmit={(data: MySchema) => save(data)} />
|
|
552
|
+
+ <Form onFormSubmit={(data, eventDetails) => save(data)} />
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
`preventDefault()` lo llama Base UI automáticamente cuando definís `onFormSubmit`.
|
|
556
|
+
|
|
557
|
+
**Validación.** En v2 cada campo vive dentro de un `Field` con `name`, `error` (matchers `valueMissing`, `typeMismatch`, `patternMismatch`, etc.) y opcionalmente `validate`. Los errores de servidor entran por `Form.errors` (objeto `{ [name]: string }`) y se limpian automáticamente al editar el campo.
|
|
558
|
+
|
|
559
|
+
```tsx
|
|
560
|
+
<Form errors={serverErrors} onFormSubmit={save}>
|
|
561
|
+
<Field
|
|
562
|
+
name="email"
|
|
563
|
+
label="Email"
|
|
564
|
+
error={[
|
|
565
|
+
{ message: "Email is required.", match: "valueMissing" },
|
|
566
|
+
{ message: "Enter a valid email.", match: "typeMismatch" },
|
|
567
|
+
]}
|
|
568
|
+
>
|
|
569
|
+
<Input type="email" required />
|
|
570
|
+
</Field>
|
|
571
|
+
</Form>
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Validación con schema (zod) sin RHF.** Si querés mantener un schema como source of truth pero no necesitás reactividad entre campos, parseá los `formValues` con el helper `parseFormValues(schema, values)`. Devuelve `{ data, errors }` listos para alimentar el prop `errors` del Form:
|
|
575
|
+
|
|
576
|
+
```tsx
|
|
577
|
+
import {
|
|
578
|
+
Form,
|
|
579
|
+
Field,
|
|
580
|
+
Input,
|
|
581
|
+
Button,
|
|
582
|
+
parseFormValues,
|
|
583
|
+
} from "@boxcustodia/library";
|
|
584
|
+
import { useState } from "react";
|
|
585
|
+
import { z } from "zod";
|
|
586
|
+
|
|
587
|
+
const schema = z.object({
|
|
588
|
+
name: z.string().min(1, "Name is required."),
|
|
589
|
+
age: z.coerce.number().positive("Age must be positive."),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
function Page() {
|
|
593
|
+
const [errors, setErrors] = useState({});
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<Form
|
|
597
|
+
errors={errors}
|
|
598
|
+
onFormSubmit={(values) => {
|
|
599
|
+
const { data, errors } = parseFormValues(schema, values);
|
|
600
|
+
setErrors(errors);
|
|
601
|
+
if (data) save(data);
|
|
602
|
+
}}
|
|
603
|
+
>
|
|
604
|
+
<Field name="name" label="Name">
|
|
605
|
+
<Input />
|
|
606
|
+
</Field>
|
|
607
|
+
<Field name="age" label="Age">
|
|
608
|
+
<Input />
|
|
609
|
+
</Field>
|
|
610
|
+
<Button type="submit">Submit</Button>
|
|
611
|
+
</Form>
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
`data` es `null` cuando alguna validación falla — los mensajes ya viajaron a `errors` y se limpian automáticamente al editar cada campo.
|
|
617
|
+
|
|
618
|
+
**Escape hatch: `HookForm` + `HookField`.** Para formularios con reactividad real (watchers, dependent fields, wizards multi-step), la librería sigue exponiendo wrappers sobre `react-hook-form`:
|
|
619
|
+
|
|
620
|
+
```tsx
|
|
621
|
+
import { HookForm, HookField, useHookForm } from "@boxcustodia/library";
|
|
622
|
+
|
|
623
|
+
const form = useHookForm(MySchema, { defaultValues });
|
|
624
|
+
|
|
625
|
+
<HookForm form={form} onFormSubmit={(data) => save(data)}>
|
|
626
|
+
<HookField
|
|
627
|
+
name="email"
|
|
628
|
+
label="Email"
|
|
629
|
+
render={(field) => <Input {...field} type="email" />}
|
|
630
|
+
/>
|
|
631
|
+
</HookForm>;
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
`HookField` envuelve `Controller` + `Field` — invalid y error se manejan solos.
|
|
635
|
+
|
|
636
|
+
**Cuándo usar cada API:**
|
|
637
|
+
|
|
638
|
+
- **Validación HTML simple** → `Form` + `Field` con matchers (`valueMissing`, `typeMismatch`, etc.).
|
|
639
|
+
- **Schema (zod) sin reactividad** → `Form` + `parseFormValues(schema, values)` dentro de `onFormSubmit`.
|
|
640
|
+
- **Watch / dependent fields / multi-step** → `HookForm` + `HookField` + `useHookForm`.
|
|
641
|
+
|
|
642
|
+
### 13. Field
|
|
643
|
+
|
|
644
|
+
**Reemplaza a `FormField`.** En v1 había un componente `FormField` que envolvía `Controller` de `react-hook-form` y renderizaba label + control + error automáticamente. Ese componente se eliminó. `Field` es el sucesor conceptual pero con una API distinta: no asume RHF, se integra opcionalmente con el `Form` nativo (Base UI), y usa el pattern de `children` en vez de un `render` callback.
|
|
645
|
+
|
|
646
|
+
```diff
|
|
647
|
+
- import { FormField } from "@boxcustodia/library";
|
|
648
|
+
- <FormField
|
|
649
|
+
- name="email"
|
|
650
|
+
- label="Email"
|
|
651
|
+
- render={(field) => <Input {...field} type="email" />}
|
|
652
|
+
- />
|
|
653
|
+
+ import { Field, Input } from "@boxcustodia/library";
|
|
654
|
+
+ <Field name="email" label="Email">
|
|
655
|
+
+ <Input type="email" required />
|
|
656
|
+
+ </Field>
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Para mantener el flujo con `react-hook-form` + `zod`, usá `HookField` (ver [Form](#12-form)).
|
|
660
|
+
|
|
661
|
+
**Integración con `Form`.** Cuando un `Field` con `name` vive dentro de un `Form`, los errores que llegan por el prop `errors` del Form se rutean automáticamente al Field por su `name` y se limpian solos cuando el usuario edita el campo. Ningún cableado manual:
|
|
662
|
+
|
|
663
|
+
```tsx
|
|
664
|
+
<Form errors={{ email: "Already taken." }}>
|
|
665
|
+
<Field name="email" label="Email">
|
|
666
|
+
<Input />
|
|
667
|
+
</Field>
|
|
668
|
+
</Form>
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**`error` cambia de tipo.** En v1, el error de `FormField` venía del `fieldState` de `react-hook-form` y se renderizaba en un `FormMessage` interno (no era una prop). En v2, `Field.error` es una prop tipada `FieldErrorProp` con tres formas:
|
|
672
|
+
|
|
673
|
+
- `string` → siempre renderiza una vez que el field es inválido.
|
|
674
|
+
- `{ message, match }` → renderiza sólo cuando la clave del `ValidityState` coincide (`valueMissing`, `typeMismatch`, `tooShort`, etc.).
|
|
675
|
+
- `FieldErrorItem[]` → múltiples mensajes, cada uno con su propio `match`.
|
|
676
|
+
|
|
677
|
+
```tsx
|
|
678
|
+
<Field
|
|
679
|
+
name="email"
|
|
680
|
+
error={[
|
|
681
|
+
{ message: "Email is required.", match: "valueMissing" },
|
|
682
|
+
{ message: "Enter a valid email.", match: "typeMismatch" },
|
|
683
|
+
]}
|
|
684
|
+
>
|
|
685
|
+
<Input type="email" required />
|
|
686
|
+
</Field>
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
`xProps` eliminadas — usá `className` (root) y `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
690
|
+
|
|
691
|
+
Slots de `classNames` disponibles: `label`, `description`, `error`.
|
|
692
|
+
|
|
693
|
+
### 14. Input
|
|
694
|
+
|
|
695
|
+
**`onChange` → `onValueChange`** (convención Base UI para controles con `value: string`). El signature se mantiene `(value, event)` — primer argumento el string parseado, segundo el `ChangeEvent` nativo:
|
|
696
|
+
|
|
697
|
+
```diff
|
|
698
|
+
- <Input value={value} onChange={(v) => setValue(v)} />
|
|
699
|
+
+ <Input value={value} onValueChange={(v) => setValue(v)} />
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
Sin otros breaking changes.
|
|
703
|
+
|
|
704
|
+
### 15. Empty
|
|
705
|
+
|
|
706
|
+
**Renombrado.** `EmptyState` → `Empty`. Actualizá los imports:
|
|
707
|
+
|
|
708
|
+
```diff
|
|
709
|
+
- import { EmptyState } from "@boxcustodia/library";
|
|
710
|
+
+ import { Empty } from "@boxcustodia/library";
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
**Ícono por defecto cambió.** `FolderSearch` → `Inbox`. Si necesitás el anterior, pasalo explícito:
|
|
714
|
+
|
|
715
|
+
```diff
|
|
716
|
+
- <Empty />
|
|
717
|
+
+ <Empty icon={<FolderSearch />} />
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Para estilar partes internas, usá `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
721
|
+
|
|
722
|
+
Slots de `classNames` disponibles: `header`, `media`, `title`, `description`, `content`.
|
|
723
|
+
|
|
724
|
+
### 16. Kbd
|
|
725
|
+
|
|
726
|
+
**Variantes eliminadas.** Las props `size` (`default | sm | lg`) y `variant` (`default | error | outline | secondary`) ya no existen. v2 expone un único estilo. Para tamaños o colores custom, override con `className`:
|
|
727
|
+
|
|
728
|
+
```diff
|
|
729
|
+
- <Kbd variant="error" size="lg">⌘</Kbd>
|
|
730
|
+
+ <Kbd className="bg-error text-error-foreground text-lg px-4">⌘</Kbd>
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
También se eliminó el archivo `kbd.variants.ts` (`KbdVariants`) — no formaba parte del API público, pero si lo importabas directo del subpath ya no existe.
|
|
734
|
+
|
|
735
|
+
### 17. Loader
|
|
736
|
+
|
|
737
|
+
**`containerClassName` eliminada.** Se reemplaza por `classNames.container` (slot del wrapper externo). `className` sigue estilando el ícono — sin cambio de semántica:
|
|
738
|
+
|
|
739
|
+
```diff
|
|
740
|
+
- <Loader containerClassName="rounded-full bg-card p-2" />
|
|
741
|
+
+ <Loader classNames={{ container: "rounded-full bg-card p-2" }} />
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
La prop `center` se mantiene igual.
|
|
745
|
+
|
|
746
|
+
### 18. Menu
|
|
747
|
+
|
|
748
|
+
`contentProps` eliminada — usá `className` y `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)). Para customizaciones de comportamiento sobre el popup (`onEscapeKeyDown`, `onPointerDownOutside`, etc.), componé con los primitives exportados: `MenuRoot`, `MenuTrigger`, `MenuPopup`.
|
|
749
|
+
|
|
750
|
+
Slot de `classNames` disponible: `popup`.
|
|
751
|
+
|
|
752
|
+
**`shortcut` ahora es display-only.** En v1, cada item podía declarar `shortcut`, `onShortcut` y `shortcutOptions` — `MenuShortcut` registraba un keyboard handler real y disparaba el `onShortcut` cuando se presionaba la combinación. En v2, `shortcut` es un string que **solo se renderiza** dentro del item; no se captura ningún teclado. Si dependías del handler, reemplazalo con un listener propio (`useEffect` + `keydown`, o un hook de shortcuts):
|
|
753
|
+
|
|
754
|
+
```diff
|
|
755
|
+
- items={[
|
|
756
|
+
- {
|
|
757
|
+
- type: "item",
|
|
758
|
+
- label: "Save",
|
|
759
|
+
- shortcut: "Ctrl+S",
|
|
760
|
+
- onShortcut: handleSave,
|
|
761
|
+
- },
|
|
762
|
+
- ]}
|
|
763
|
+
+ items={[
|
|
764
|
+
+ {
|
|
765
|
+
+ type: "item",
|
|
766
|
+
+ label: "Save",
|
|
767
|
+
+ shortcut: "Ctrl+S",
|
|
768
|
+
+ onSelect: handleSave,
|
|
769
|
+
+ },
|
|
770
|
+
+ ]}
|
|
771
|
+
+ // y registrá el atajo de teclado vos mismo en un useEffect
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
**`trigger` ahora exige un `ReactElement`.** v1 aceptaba cualquier `ReactNode` (strings, fragments, `null`); v2 requiere un elemento React real porque Base UI usa `render={trigger}` para clonar props sobre el componente:
|
|
775
|
+
|
|
776
|
+
```diff
|
|
777
|
+
- <Menu trigger="Open" items={...} />
|
|
778
|
+
+ <Menu trigger={<Button>Open</Button>} items={...} />
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### 19. NumberInput
|
|
782
|
+
|
|
783
|
+
**`onChange` → `onValueChange`** (convención Base UI). El callback recibe `number | null` — `null` cuando el campo está vacío o contiene un valor no parseable:
|
|
784
|
+
|
|
785
|
+
```diff
|
|
786
|
+
- <NumberInput value={value} onChange={(v) => setValue(v)} />
|
|
787
|
+
+ <NumberInput value={value} onValueChange={(v) => v !== null && setValue(v)} />
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### 20. Password
|
|
791
|
+
|
|
792
|
+
**`containerClassName` eliminada.** v1 dejaba estilar el wrapper externo via `containerClassName`. v2 lo absorbe en `className` y suma el slot `classNames.toggle` para el botón del ojo. La arquitectura interna cambió — el wrapper ahora carga todo el styling visual (borde, alto, focus ring) y el input pasa a ser transparente adentro — pero la API desde afuera se simplifica: `className` estila el field visible como hacía v1.
|
|
793
|
+
|
|
794
|
+
```diff
|
|
795
|
+
- <Password containerClassName="my-2" className="border-primary" />
|
|
796
|
+
+ <Password className="my-2 border-primary" />
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**`onChange` → `onValueChange`** (convención Base UI, alineado con Input):
|
|
800
|
+
|
|
801
|
+
```diff
|
|
802
|
+
- <Password value={pwd} onChange={(v) => setPwd(v)} />
|
|
803
|
+
+ <Password value={pwd} onValueChange={(v) => setPwd(v)} />
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
Slots de `classNames`: `input` (texto interno, font / color del texto tipeado), `toggle` (botón del ojo).
|
|
807
|
+
|
|
808
|
+
### 21. Popover
|
|
809
|
+
|
|
810
|
+
`xProps` eliminadas — usá `className` y `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)). `triggerClassName` también se eliminó: pasá `className` al elemento dentro del `trigger`.
|
|
811
|
+
|
|
812
|
+
Slot de `classNames` disponible: `viewport`.
|
|
813
|
+
|
|
814
|
+
**`offset` → `sideOffset`.** v1 tenía una sola prop `offset` para el gap eje-side. v2 la separa: `sideOffset` (gap perpendicular al trigger en el eje del side) y `alignOffset` (desplazamiento paralelo en el eje del align). Si sólo usabas `offset`, mové el valor a `sideOffset`:
|
|
815
|
+
|
|
816
|
+
```diff
|
|
817
|
+
- <Popover offset={8}>…</Popover>
|
|
818
|
+
+ <Popover sideOffset={8}>…</Popover>
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
**`trigger: ReactNode → ReactElement`.** v1 aceptaba cualquier `ReactNode` (strings, fragments, `null`); v2 requiere un `ReactElement` porque Base UI usa `render={trigger}` y necesita un componente para clonar props.
|
|
822
|
+
|
|
823
|
+
```diff
|
|
824
|
+
- <Popover trigger="Open">…</Popover>
|
|
825
|
+
+ <Popover trigger={<Button>Open</Button>}>…</Popover>
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
**`triggerAsChild` eliminada.** Base UI no usa el patrón `asChild` (ver [Migración de Radix UI a Base UI](#migración-de-radix-ui-a-base-ui)). Si necesitás un render custom sobre el trigger, componé con el primitive y la prop `render`:
|
|
829
|
+
|
|
830
|
+
```diff
|
|
831
|
+
- <Popover trigger={<Button>Open</Button>} triggerAsChild={false} />
|
|
832
|
+
+ <PopoverRoot>
|
|
833
|
+
+ <PopoverTrigger render={<Button>Open</Button>} />
|
|
834
|
+
+ <PopoverPopup>…</PopoverPopup>
|
|
835
|
+
+ </PopoverRoot>
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
### 22. Skeleton
|
|
839
|
+
|
|
840
|
+
**Drop de toda la API custom.** v1 exponía `width`, `height`, `isCircle` y `isLoading`. v2 sólo recibe `className` (más cualquier prop nativa de `div`).
|
|
841
|
+
|
|
842
|
+
```diff
|
|
843
|
+
- <Skeleton width="15ch" height="1rem" isCircle isLoading={loading} />
|
|
844
|
+
+ {loading && <Skeleton className="h-4 w-[15ch] rounded-full" />}
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### 23. Stepper
|
|
848
|
+
|
|
849
|
+
`xProps` eliminadas — usá `className` y `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
850
|
+
|
|
851
|
+
Slots de `classNames` disponibles: `nav`, `item`, `trigger`, `indicator`, `title`, `description`, `separator`, `panel`, `content`.
|
|
852
|
+
|
|
853
|
+
**`current` → `value`** y **`onChange` → `onValueChange`** (convención Base UI):
|
|
854
|
+
|
|
855
|
+
```diff
|
|
856
|
+
- <Stepper items={steps} current={step} onChange={(i) => setStep(i)} />
|
|
857
|
+
+ <Stepper items={steps} value={step} onValueChange={(i) => setStep(i)} />
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
**Pasaron a ser 1-indexed.** v1 numeraba los pasos desde `0` (`current={0}` = primer paso). v2 numera desde `1` (`value={1}` = primer paso). Actualizá los valores de estado:
|
|
861
|
+
|
|
862
|
+
```diff
|
|
863
|
+
- const [step, setStep] = useState(0); // primer paso en v1
|
|
864
|
+
+ const [step, setStep] = useState(1); // primer paso en v2
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
`defaultValue` también es 1-indexed.
|
|
868
|
+
|
|
869
|
+
### 24. Switch
|
|
870
|
+
|
|
871
|
+
`xProps` eliminadas — usá `className` y `classNames` (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
872
|
+
|
|
873
|
+
Slots de `classNames` disponibles: `wrapper`, `thumb`, `label`.
|
|
874
|
+
|
|
875
|
+
**`onChange` → `onCheckedChange`** (convención Base UI para controles binarios). El callback ahora recibe `eventDetails` como segundo argumento:
|
|
876
|
+
|
|
877
|
+
```diff
|
|
878
|
+
- <Switch checked={on} onChange={(v) => setOn(v)} />
|
|
879
|
+
+ <Switch checked={on} onCheckedChange={(v) => setOn(v)} />
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
**`containerClassName` y `containerProps` eliminadas.** El wrapper externo ahora se estila vía `classNames.wrapper`:
|
|
883
|
+
|
|
884
|
+
```diff
|
|
885
|
+
- <Switch containerClassName="gap-4" />
|
|
886
|
+
+ <Switch classNames={{ wrapper: "gap-4" }} />
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### 25. Table
|
|
890
|
+
|
|
891
|
+
**xProps eliminadas.** v1 exponía pass-through completo (`theadProps`, `tbodyProps`, `trProps`, `thProps`, `tdProps`, `containerProps`) y un set paralelo de `*ClassName` (`theadClassName`, `tbodyClassName`, `trClassName`, `thClassName`, `tdClassName`, `containerClassName`). v2 los reemplaza por un único `classNames` namespaceado (ver [patrón global](#nuevo-patrón-de-composición-y-estilado)).
|
|
892
|
+
|
|
893
|
+
```diff
|
|
894
|
+
- <Table
|
|
895
|
+
- theadClassName="bg-muted/50"
|
|
896
|
+
- thClassName="uppercase"
|
|
897
|
+
- trClassName="odd:bg-muted/20"
|
|
898
|
+
- containerClassName="max-h-[500px]"
|
|
899
|
+
- />
|
|
900
|
+
+ <Table
|
|
901
|
+
+ classNames={{
|
|
902
|
+
+ thead: "bg-muted/50",
|
|
903
|
+
+ th: "uppercase",
|
|
904
|
+
+ tr: "odd:bg-muted/20",
|
|
905
|
+
+ container: "max-h-[500px]",
|
|
906
|
+
+ }}
|
|
907
|
+
+ />
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
Slots de `classNames` disponibles: `container`, `thead`, `tbody`, `headerRow`, `tr`, `th`, `td`. **`tr` aplica sólo a filas del body**; para la fila del header usá `headerRow`. v1 aplicaba `trClassName` a las dos por bug — si dependías de eso, dividí en `headerRow` + `tr` con la misma clase.
|
|
911
|
+
|
|
912
|
+
Para comportamientos que `classNames` no cubre (handlers en `<tr>`, refs, custom `tfoot`, layout no estándar), usá los primitives exportados: `TableRoot`, `TableHeader`, `TableBody`, `TableRow`, `TableHead`, `TableCell`, `TableFooter`, `TableCaption`.
|
|
913
|
+
|
|
914
|
+
**`onDoubleClick` → `onRowDoubleClick`.** v1 usaba `onDoubleClick` (colisionaba con el handler nativo de React sobre `<table>`). v2 lo renombra para alinear con `onRowClick`:
|
|
915
|
+
|
|
916
|
+
```diff
|
|
917
|
+
- <Table onDoubleClick={(row) => openDetail(row)} />
|
|
918
|
+
+ <Table onRowDoubleClick={(row) => openDetail(row)} />
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### 26. Tabs
|
|
922
|
+
|
|
923
|
+
**Componentes renombrados.** Los hijos de `<Tabs>` adoptan el prefijo `Tabs*` (consistente con el root):
|
|
924
|
+
|
|
925
|
+
```diff
|
|
926
|
+
- import { Tabs, TabList, TabTrigger, TabContent } from "@boxcustodia/library";
|
|
927
|
+
+ import { Tabs, TabsList, TabsTab, TabsPanel } from "@boxcustodia/library";
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
- `TabList` → `TabsList`
|
|
931
|
+
- `TabTrigger` → `TabsTab` (también exportado como `TabsTrigger`)
|
|
932
|
+
- `TabContent` → `TabsPanel` (también exportado como `TabsContent`)
|
|
933
|
+
|
|
934
|
+
**`variant` se mudó de `<Tabs>` a `<TabsList>`.** El indicator vive dentro del list — la prop pasa donde corresponde:
|
|
935
|
+
|
|
936
|
+
```diff
|
|
937
|
+
- <Tabs variant="background">
|
|
938
|
+
- <TabList>…</TabList>
|
|
939
|
+
- </Tabs>
|
|
940
|
+
+ <Tabs>
|
|
941
|
+
+ <TabsList variant="background">…</TabsList>
|
|
942
|
+
+ </Tabs>
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
**Valores de `variant`:** `"default"` → `"underline"` (default actual). `"background"` se mantiene.
|
|
946
|
+
|
|
947
|
+
**`onChange` → `onValueChange`** (convención Base UI):
|
|
948
|
+
|
|
949
|
+
```diff
|
|
950
|
+
- <Tabs value={tab} onChange={(v) => setTab(v)} />
|
|
951
|
+
+ <Tabs value={tab} onValueChange={(v) => setTab(v)} />
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
### 27. Tag
|
|
955
|
+
|
|
956
|
+
**Variantes renombradas:**
|
|
957
|
+
|
|
958
|
+
- `primary` → `default`
|
|
959
|
+
- `borderless` → `ghost`
|
|
960
|
+
|
|
961
|
+
```diff
|
|
962
|
+
- <Tag variant="primary">…</Tag>
|
|
963
|
+
+ <Tag variant="default">…</Tag>
|
|
964
|
+
|
|
965
|
+
- <Tag variant="borderless">…</Tag>
|
|
966
|
+
+ <Tag variant="ghost">…</Tag>
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
**Variante `success` eliminada.** Si la usabas, override con `className`:
|
|
970
|
+
|
|
971
|
+
```diff
|
|
972
|
+
- <Tag variant="success">Done</Tag>
|
|
973
|
+
+ <Tag className="bg-success/10 text-success">Done</Tag>
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
**`icon` eliminado.** v1 tenía slot dedicado para ícono. v2 acepta el ícono como children, posicionado vía atributo `data-icon`:
|
|
977
|
+
|
|
978
|
+
```diff
|
|
979
|
+
- <Tag icon={<Check />}>Approved</Tag>
|
|
980
|
+
+ <Tag><Check data-icon="inline-start" /> Approved</Tag>
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
Valores aceptados: `inline-start` (antes del texto) o `inline-end` (después).
|
|
984
|
+
|
|
985
|
+
**`rounded` eliminado.** v2 siempre renderea pill (rounded total). Para otros radios usá `className`:
|
|
986
|
+
|
|
987
|
+
```diff
|
|
988
|
+
- <Tag rounded="square">…</Tag>
|
|
989
|
+
+ <Tag className="rounded-md">…</Tag>
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
**`color` eliminado.** v2 no acepta color libre — usá una variant o override con `className`:
|
|
993
|
+
|
|
994
|
+
```diff
|
|
995
|
+
- <Tag color="purple">…</Tag>
|
|
996
|
+
+ <Tag className="bg-purple-500 text-white">…</Tag>
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
**`closable` y `onClose` eliminados.** v2 no incluye botón de cerrar. Componé el patrón vos:
|
|
1000
|
+
|
|
1001
|
+
```diff
|
|
1002
|
+
- <Tag closable onClose={handleRemove}>Item</Tag>
|
|
1003
|
+
+ <Tag>
|
|
1004
|
+
+ Item
|
|
1005
|
+
+ <button data-icon="inline-end" onClick={handleRemove}>
|
|
1006
|
+
+ <XIcon />
|
|
1007
|
+
+ </button>
|
|
1008
|
+
+ </Tag>
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### 28. Textarea
|
|
1012
|
+
|
|
1013
|
+
**`onChange` → `onValueChange`** (convención Base UI, alineado con Input). El callback ahora recibe `(value: string, event)` en lugar del event nativo. Si dependías del signature nativo, usá `onValueChange` con el segundo arg:
|
|
1014
|
+
|
|
1015
|
+
```diff
|
|
1016
|
+
- <Textarea value={text} onChange={(e) => setText(e.target.value)} />
|
|
1017
|
+
+ <Textarea value={text} onValueChange={(value) => setText(value)} />
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
### 29. Timeline
|
|
1021
|
+
|
|
1022
|
+
**xProps eliminadas.** v1 exponía `itemsProps` (HTMLProps por item) y `containerProps` (HTMLProps del root). Ambas removidas — para comportamiento custom usá los primitives (`TimelineRoot`, `TimelineItem`, `TimelineHeader`, `TimelineIndicator`, `TimelineSeparator`, `TimelineTitle`, `TimelineDate`, `TimelineContent`).
|
|
1023
|
+
|
|
1024
|
+
**Props `className*` consolidadas en `classNames`.** El root se estila con `className`; el resto con el objeto namespaceado:
|
|
1025
|
+
|
|
1026
|
+
```diff
|
|
1027
|
+
- <Timeline
|
|
1028
|
+
- classNameContainer="max-w-md"
|
|
1029
|
+
- classNameItem="ms-10"
|
|
1030
|
+
- classNameItemDot="bg-primary"
|
|
1031
|
+
- classNameItemContent="text-xs"
|
|
1032
|
+
- />
|
|
1033
|
+
+ <Timeline
|
|
1034
|
+
+ className="max-w-md"
|
|
1035
|
+
+ classNames={{
|
|
1036
|
+
+ item: "ms-10",
|
|
1037
|
+
+ indicator: "bg-primary",
|
|
1038
|
+
+ content: "text-xs",
|
|
1039
|
+
+ }}
|
|
1040
|
+
+ />
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
Slots de `classNames` disponibles: `item`, `header`, `title`, `date`, `content`, `indicator`, `separator`.
|
|
1044
|
+
|
|
1045
|
+
**Shape de `items` cambió.** v1: `{ content?, icon?, status?, color? }`. v2: `{ title?, content?, date?, indicator? }`.
|
|
1046
|
+
|
|
1047
|
+
- `icon` → `indicator`
|
|
1048
|
+
- `status` (`done | pending | error`) eliminado. Usá `defaultValue` / `value` en el root para marcar el step activo; los pasos previos quedan completados automáticamente.
|
|
1049
|
+
- `color` eliminado. Override visual con `classNames.indicator`.
|
|
1050
|
+
|
|
1051
|
+
```diff
|
|
1052
|
+
- <Timeline
|
|
1053
|
+
- items={[
|
|
1054
|
+
- { content: "Step 1", icon: <Check />, status: "done" },
|
|
1055
|
+
- { content: "Step 2", icon: <Clock />, status: "pending", color: "amber" },
|
|
1056
|
+
- ]}
|
|
1057
|
+
- />
|
|
1058
|
+
+ <Timeline
|
|
1059
|
+
+ value={1}
|
|
1060
|
+
+ items={[
|
|
1061
|
+
+ { title: "Step 1", content: "...", indicator: <Check /> },
|
|
1062
|
+
+ { title: "Step 2", content: "...", indicator: <Clock /> },
|
|
1063
|
+
+ ]}
|
|
1064
|
+
+ />
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
### 30. Toast
|
|
1068
|
+
|
|
1069
|
+
**`<Toaster />` eliminado.** El render layer ahora vive dentro de `<LibraryProvider>` (un único provider que configura toda la infraestructura de la lib — toasts, tooltips, etc.). Reemplazá el wrapper en la raíz de tu app:
|
|
1070
|
+
|
|
1071
|
+
```diff
|
|
1072
|
+
- import { Toaster } from "@boxcustodia/library";
|
|
1073
|
+
-
|
|
1074
|
+
- export function App() {
|
|
1075
|
+
- return (
|
|
1076
|
+
- <>
|
|
1077
|
+
- <Toaster />
|
|
1078
|
+
- <Router />
|
|
1079
|
+
- </>
|
|
1080
|
+
- );
|
|
1081
|
+
- }
|
|
1082
|
+
+ import { LibraryProvider } from "@boxcustodia/library";
|
|
1083
|
+
+
|
|
1084
|
+
+ export function App() {
|
|
1085
|
+
+ return (
|
|
1086
|
+
+ <LibraryProvider>
|
|
1087
|
+
+ <Router />
|
|
1088
|
+
+ </LibraryProvider>
|
|
1089
|
+
+ );
|
|
1090
|
+
+ }
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
Si necesitás un provider de Toast aislado (tests, microfrontends), el `<ToastProvider>` sigue exportado.
|
|
1094
|
+
|
|
1095
|
+
**`type` → `variant`** (convención del resto de la lib):
|
|
1096
|
+
|
|
1097
|
+
```diff
|
|
1098
|
+
- toast({ type: "info", title: "Saved" });
|
|
1099
|
+
+ toast({ variant: "info", title: "Saved" });
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
**`type: "loading"` eliminado.** Para estados async usá `toast.promise()`:
|
|
1103
|
+
|
|
1104
|
+
```diff
|
|
1105
|
+
- toast({ type: "loading", description: "Guardando..." });
|
|
1106
|
+
+ toast.promise(saveData(), {
|
|
1107
|
+
+ loading: { description: "Guardando..." },
|
|
1108
|
+
+ success: () => ({ variant: "success", description: "Guardado!" }),
|
|
1109
|
+
+ error: (e) => ({ variant: "error", description: e.message }),
|
|
1110
|
+
+ });
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
**`duration` → `timeout`** (Base UI naming):
|
|
1114
|
+
|
|
1115
|
+
```diff
|
|
1116
|
+
- toast({ description: "Saved", duration: 5000 });
|
|
1117
|
+
+ toast({ description: "Saved", timeout: 5000 });
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
**`useToastStore` eliminado.** El store Zustand interno se reemplaza por la API imperativa `toast()` (funciona desde cualquier lado) o por `useToastManager()` (cuando necesitás acceder al manager como hook):
|
|
1121
|
+
|
|
1122
|
+
```diff
|
|
1123
|
+
- import { useToastStore } from "@boxcustodia/library";
|
|
1124
|
+
- const addToast = useToastStore((s) => s.addToast);
|
|
1125
|
+
- addToast({ description: "Saved" });
|
|
1126
|
+
+ import { toast } from "@boxcustodia/library";
|
|
1127
|
+
+ toast({ description: "Saved" });
|
|
1128
|
+
```
|
|
1129
|
+
|
|
1130
|
+
### 31. Tooltip
|
|
1131
|
+
|
|
1132
|
+
**`label` → `content`** (rename — alinea con el resto de los composites: la prop que recibe el contenido del popup se llama `content`):
|
|
1133
|
+
|
|
1134
|
+
```diff
|
|
1135
|
+
- <Tooltip label="Save changes"><Button>Save</Button></Tooltip>
|
|
1136
|
+
+ <Tooltip content="Save changes"><Button>Save</Button></Tooltip>
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
**`asChild` eliminada.** Base UI no usa el patrón `asChild` (ver [Migración de Radix UI a Base UI](#migración-de-radix-ui-a-base-ui)). Pasá el trigger directo como children — el composite lo monta sobre `<TooltipTrigger render={...}>` automáticamente:
|
|
1140
|
+
|
|
1141
|
+
```diff
|
|
1142
|
+
- <Tooltip label="Hint" asChild>
|
|
1143
|
+
- <Button>Hover me</Button>
|
|
1144
|
+
- </Tooltip>
|
|
1145
|
+
+ <Tooltip content="Hint">
|
|
1146
|
+
+ <Button>Hover me</Button>
|
|
1147
|
+
+ </Tooltip>
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
**`children` tightened.** v1 aceptaba cualquier `object`. v2 exige un `ReactElement` único porque Base UI clona el elemento via `render={children}`. Strings, fragments, arrays o `null` no funcionan más:
|
|
1151
|
+
|
|
1152
|
+
```diff
|
|
1153
|
+
- <Tooltip label="Hint">Save</Tooltip>
|
|
1154
|
+
+ <Tooltip content="Hint"><span>Save</span></Tooltip>
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
**`arrowClassName` → `classNames.arrow`** (consolidación al patrón global de slots):
|
|
1158
|
+
|
|
1159
|
+
```diff
|
|
1160
|
+
- <Tooltip label="Hint" arrowClassName="text-primary">…</Tooltip>
|
|
1161
|
+
+ <Tooltip content="Hint" classNames={{ arrow: "text-primary" }}>…</Tooltip>
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
### 32. Tree
|
|
1165
|
+
|
|
1166
|
+
**Reescrito de raíz.** v1 estaba construido sobre `react-aria-components` y aceptaba un array recursivo `items: TreeNode[]` con `{ id, title, children }` que el componente paseaba solo. v2 está construido sobre [`@headless-tree/core`](https://headless-tree.lukasbach.com) — el consumer crea una **instancia** de tree (vía `useTree`) y se la pasa al componente. No hay equivalencia 1-a-1 entre las dos APIs: cambia el modelo mental, el shape de los datos, los hooks, el patrón de drag-and-drop y la composición.
|
|
1167
|
+
|
|
1168
|
+
```diff
|
|
1169
|
+
- import { Tree } from "@boxcustodia/library";
|
|
1170
|
+
-
|
|
1171
|
+
- <Tree
|
|
1172
|
+
- items={[
|
|
1173
|
+
- { id: 1, title: "Folder", children: [
|
|
1174
|
+
- { id: 2, title: "File", children: [] },
|
|
1175
|
+
- ] },
|
|
1176
|
+
- ]}
|
|
1177
|
+
- />
|
|
1178
|
+
|
|
1179
|
+
+ import { useTree } from "@headless-tree/core";
|
|
1180
|
+
+ import { TreeRoot, TreeItem } from "@boxcustodia/library";
|
|
1181
|
+
+
|
|
1182
|
+
+ const tree = useTree<MyData>({ /* dataLoader, features, ... */ });
|
|
1183
|
+
+
|
|
1184
|
+
+ <TreeRoot tree={tree}>
|
|
1185
|
+
+ {tree.getItems().map((item) => (
|
|
1186
|
+
+ <TreeItem key={item.getId()} item={item} />
|
|
1187
|
+
+ ))}
|
|
1188
|
+
+ </TreeRoot>
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
### 33. Pagination
|
|
1192
|
+
|
|
1193
|
+
**Reescrito como composite + primitivos.** La API es mayormente nueva; el layout por defecto del composite ahora coincide con el viejo `TablePagination` (size selector + "X – Y de Z" + prev / next), no con el layout numerado de v1. El layout numerado se reconstruye componiendo primitivos.
|
|
1194
|
+
|
|
1195
|
+
**`TablePagination` eliminado.** Era un componente aparte en v1; en v2 quedó absorbido en el composite `Pagination`. Cambiá el import — el shape de props también cambia (ver más abajo):
|
|
1196
|
+
|
|
1197
|
+
```diff
|
|
1198
|
+
- import { TablePagination } from "@boxcustodia/library";
|
|
1199
|
+
+ import { Pagination } from "@boxcustodia/library";
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
**Props renombradas / cambiadas en `Pagination`:**
|
|
1203
|
+
|
|
1204
|
+
- `initialCurrentPage` → `defaultCurrentPage`
|
|
1205
|
+
- `onChange` → `onCurrentPageChange` + `onPageSizeChange` (separadas)
|
|
1206
|
+
- `optionProps` y `containerProps` eliminadas — para customizar items, usá los primitives (`PaginationLink`, `PaginationPrevious`, `PaginationNext`, etc.)
|
|
1207
|
+
- `TABLE_PAGE_SIZES` eliminada — usá `PAGINATION_SIZES`
|
|
1208
|
+
|
|
1209
|
+
```diff
|
|
1210
|
+
- <Pagination
|
|
1211
|
+
- totalItems={100}
|
|
1212
|
+
- pageSize={10}
|
|
1213
|
+
- initialCurrentPage={1}
|
|
1214
|
+
- onChange={(page) => setPage(page)}
|
|
1215
|
+
- />
|
|
1216
|
+
+ <Pagination
|
|
1217
|
+
+ totalItems={100}
|
|
1218
|
+
+ defaultPageSize={10}
|
|
1219
|
+
+ defaultCurrentPage={1}
|
|
1220
|
+
+ onCurrentPageChange={(page) => setPage(page)}
|
|
1221
|
+
+ onPageSizeChange={(size) => setSize(size)}
|
|
1222
|
+
+ />
|
|
1223
|
+
```
|
|
1224
|
+
|
|
1225
|
+
**Layout numerado** — reconstruí con primitivos:
|
|
1226
|
+
|
|
1227
|
+
```tsx
|
|
1228
|
+
import {
|
|
1229
|
+
PaginationRoot,
|
|
1230
|
+
PaginationFirst,
|
|
1231
|
+
PaginationPrevious,
|
|
1232
|
+
PaginationPages,
|
|
1233
|
+
PaginationNext,
|
|
1234
|
+
PaginationLast,
|
|
1235
|
+
} from "@boxcustodia/library";
|
|
1236
|
+
|
|
1237
|
+
<PaginationRoot totalItems={100} defaultPageSize={10}>
|
|
1238
|
+
<PaginationFirst />
|
|
1239
|
+
<PaginationPrevious />
|
|
1240
|
+
<PaginationPages />
|
|
1241
|
+
<PaginationNext />
|
|
1242
|
+
<PaginationLast />
|
|
1243
|
+
</PaginationRoot>;
|
|
1244
|
+
```
|
|
1245
|
+
|
|
1246
|
+
**Cambió mucho.** Si tu uso es no-trivial, lee la story `Components/Pagination` en Storybook — ahí están todos los modos (composite default, layout numerado, modo URL con `getPageHref` / `linkComponent`, overrides de textos via `texts`).
|