@boxcustodia/library 2.0.0-alpha.12 → 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.
Files changed (174) hide show
  1. package/dist/index.cjs.js +1 -138
  2. package/dist/index.d.ts +1087 -720
  3. package/dist/index.es.js +7011 -56097
  4. package/dist/theme.css +1 -1
  5. package/package.json +34 -26
  6. package/src/__doc__/Examples.tsx +1 -1
  7. package/src/__doc__/Intro.mdx +3 -3
  8. package/src/__doc__/Tabs.mdx +112 -0
  9. package/src/__doc__/V2.mdx +1246 -0
  10. package/src/components/accordion/accordion.stories.tsx +143 -0
  11. package/src/components/accordion/accordion.tsx +135 -0
  12. package/src/components/accordion/index.ts +1 -0
  13. package/src/components/alert/alert.stories.tsx +24 -4
  14. package/src/components/alert/alert.tsx +17 -9
  15. package/src/components/alert-dialog/alert-dialog.stories.tsx +24 -0
  16. package/src/components/alert-dialog/alert-dialog.test.tsx +1 -1
  17. package/src/components/alert-dialog/alert-dialog.tsx +58 -10
  18. package/src/components/auto-complete/auto-complete.stories.tsx +616 -200
  19. package/src/components/auto-complete/auto-complete.tsx +420 -68
  20. package/src/components/auto-complete/index.ts +0 -1
  21. package/src/components/avatar/avatar.stories.tsx +162 -21
  22. package/src/components/avatar/avatar.tsx +79 -20
  23. package/src/components/button/button.stories.tsx +219 -294
  24. package/src/components/button/button.test.tsx +10 -17
  25. package/src/components/button/button.tsx +78 -19
  26. package/src/components/button/components/base-button.tsx +30 -53
  27. package/src/components/button/index.ts +0 -1
  28. package/src/components/calendar/calendar.stories.tsx +1 -1
  29. package/src/components/calendar/calendar.tsx +4 -4
  30. package/src/components/card/card.stories.tsx +141 -69
  31. package/src/components/card/card.tsx +155 -54
  32. package/src/components/center/center.stories.tsx +22 -39
  33. package/src/components/checkbox/checkbox.stories.tsx +25 -5
  34. package/src/components/checkbox/checkbox.tsx +76 -15
  35. package/src/components/checkbox-group/checkbox-group.stories.tsx +116 -28
  36. package/src/components/checkbox-group/checkbox-group.tsx +84 -3
  37. package/src/components/combobox/combobox.stories.tsx +33 -23
  38. package/src/components/combobox/combobox.tsx +99 -77
  39. package/src/components/date-picker/date-input.stories.tsx +14 -6
  40. package/src/components/date-picker/date-input.tsx +2 -2
  41. package/src/components/date-picker/date-picker.model.ts +13 -4
  42. package/src/components/date-picker/date-picker.stories.tsx +38 -12
  43. package/src/components/date-picker/date-picker.tsx +28 -14
  44. package/src/components/dialog/dialog.stories.tsx +18 -0
  45. package/src/components/dialog/dialog.test.tsx +1 -1
  46. package/src/components/dialog/dialog.tsx +51 -20
  47. package/src/components/divider/divider.stories.tsx +126 -51
  48. package/src/components/divider/divider.tsx +16 -16
  49. package/src/components/dropzone/dropzone.stories.tsx +71 -90
  50. package/src/components/dropzone/dropzone.tsx +383 -105
  51. package/src/components/dropzone/index.ts +0 -1
  52. package/src/components/empty/empty.stories.tsx +165 -0
  53. package/src/components/empty/empty.tsx +156 -0
  54. package/src/components/empty/index.ts +1 -0
  55. package/src/components/field/field.stories.tsx +227 -4
  56. package/src/components/field/field.tsx +77 -42
  57. package/src/components/form/form.stories.tsx +320 -197
  58. package/src/components/form/form.tsx +3 -23
  59. package/src/components/index.ts +2 -6
  60. package/src/components/input/input.stories.tsx +5 -5
  61. package/src/components/input/input.tsx +4 -4
  62. package/src/components/kbd/kbd.stories.tsx +1 -0
  63. package/src/components/label/label.stories.tsx +16 -0
  64. package/src/components/label/label.tsx +13 -2
  65. package/src/components/loader/loader.stories.tsx +7 -5
  66. package/src/components/loader/loader.tsx +8 -3
  67. package/src/components/menu/menu-primitives.tsx +207 -196
  68. package/src/components/menu/menu.stories.tsx +276 -146
  69. package/src/components/menu/menu.tsx +146 -54
  70. package/src/components/number-input/number-input.stories.tsx +27 -4
  71. package/src/components/number-input/number-input.test.tsx +2 -2
  72. package/src/components/number-input/number-input.tsx +31 -33
  73. package/src/components/otp/index.ts +1 -0
  74. package/src/components/otp/otp.stories.tsx +209 -0
  75. package/src/components/otp/otp.tsx +100 -0
  76. package/src/components/pagination/index.ts +1 -0
  77. package/src/components/pagination/pagination.model.ts +2 -0
  78. package/src/components/pagination/pagination.stories.tsx +154 -59
  79. package/src/components/pagination/pagination.test.tsx +122 -57
  80. package/src/components/pagination/pagination.tsx +575 -77
  81. package/src/components/password/password.stories.tsx +18 -3
  82. package/src/components/password/password.tsx +29 -9
  83. package/src/components/popover/popover.stories.tsx +26 -5
  84. package/src/components/popover/popover.tsx +15 -23
  85. package/src/components/progress/progress.stories.tsx +1 -0
  86. package/src/components/radio-group/index.ts +1 -0
  87. package/src/components/radio-group/radio-group.stories.tsx +251 -0
  88. package/src/components/radio-group/radio-group.tsx +212 -0
  89. package/src/components/scroll-area/scroll-area.stories.tsx +1 -0
  90. package/src/components/select/select.stories.tsx +118 -19
  91. package/src/components/select/select.tsx +67 -62
  92. package/src/components/skeleton/skeleton.stories.tsx +1 -0
  93. package/src/components/stack/stack.stories.tsx +179 -89
  94. package/src/components/stack/stack.tsx +2 -2
  95. package/src/components/stepper/index.ts +1 -1
  96. package/src/components/stepper/stepper.stories.tsx +767 -83
  97. package/src/components/stepper/stepper.test.tsx +18 -18
  98. package/src/components/stepper/stepper.tsx +554 -0
  99. package/src/components/switch/switch.stories.tsx +15 -1
  100. package/src/components/switch/switch.tsx +17 -4
  101. package/src/components/table/index.ts +0 -2
  102. package/src/components/table/table.stories.tsx +131 -18
  103. package/src/components/table/table.test.tsx +1 -1
  104. package/src/components/table/table.tsx +183 -77
  105. package/src/components/tabs/tabs.stories.tsx +373 -155
  106. package/src/components/tabs/tabs.test.tsx +12 -12
  107. package/src/components/tabs/tabs.tsx +72 -149
  108. package/src/components/tag/index.ts +0 -1
  109. package/src/components/tag/tag.stories.tsx +155 -120
  110. package/src/components/tag/tag.tsx +47 -95
  111. package/src/components/textarea/textarea.stories.tsx +8 -22
  112. package/src/components/textarea/textarea.tsx +17 -79
  113. package/src/components/timeline/timeline.stories.tsx +323 -42
  114. package/src/components/timeline/timeline.tsx +359 -132
  115. package/src/components/toast/toast.stories.tsx +1 -0
  116. package/src/components/tooltip/tooltip.tsx +11 -9
  117. package/src/components/tree/index.ts +0 -1
  118. package/src/components/tree/tree.stories.tsx +365 -408
  119. package/src/components/tree/tree.test.tsx +163 -0
  120. package/src/components/tree/tree.tsx +212 -36
  121. package/src/hooks/useAsync/__doc__/useAsync.stories.tsx +5 -5
  122. package/src/hooks/useClipboard/__doc__/useClipboard.stories.tsx +1 -3
  123. package/src/hooks/useDebounceCallback/__doc__/useDebouncedCallback.stories.tsx +6 -6
  124. package/src/hooks/useDocumentTitle/__doc__/useDocumentTitle.stories.tsx +1 -1
  125. package/src/hooks/useEventListener/__test__/useEventListener.test.tsx +1 -1
  126. package/src/hooks/useLocalStorage/__doc__/useLocalStorage.stories.tsx +1 -1
  127. package/src/hooks/usePagination/usePagination.tsx +36 -24
  128. package/src/styles/theme.css +1 -1
  129. package/src/utils/form.tsx +67 -37
  130. package/src/utils/index.ts +1 -1
  131. package/src/__doc__/Migration.mdx +0 -475
  132. package/src/components/auto-complete/auto-complete-primitives.tsx +0 -155
  133. package/src/components/background-image/background-image.stories.tsx +0 -21
  134. package/src/components/background-image/background-image.test.tsx +0 -29
  135. package/src/components/background-image/background-image.tsx +0 -23
  136. package/src/components/background-image/index.ts +0 -1
  137. package/src/components/button/button.variants.ts +0 -44
  138. package/src/components/button/components/loader-overlay.tsx +0 -21
  139. package/src/components/button/components/loading-icon.tsx +0 -47
  140. package/src/components/dropzone/upload-primitives.tsx +0 -310
  141. package/src/components/dropzone/use-dropzone.ts +0 -122
  142. package/src/components/empty-state/empty-state.stories.tsx +0 -56
  143. package/src/components/empty-state/empty-state.tsx +0 -39
  144. package/src/components/empty-state/index.ts +0 -1
  145. package/src/components/heading/heading.stories.tsx +0 -74
  146. package/src/components/heading/heading.tsx +0 -28
  147. package/src/components/heading/heading.variants.ts +0 -27
  148. package/src/components/heading/index.ts +0 -1
  149. package/src/components/kbd/kbd.variants.ts +0 -26
  150. package/src/components/menu/util/render-menu-item.tsx +0 -54
  151. package/src/components/multi-select/hooks/use-multi-select.ts +0 -66
  152. package/src/components/multi-select/index.ts +0 -1
  153. package/src/components/multi-select/multi-select.stories.tsx +0 -294
  154. package/src/components/multi-select/multi-select.tsx +0 -300
  155. package/src/components/multi-select/multi-select.variants.ts +0 -22
  156. package/src/components/pagination/components/pagination-option.tsx +0 -27
  157. package/src/components/show/index.ts +0 -1
  158. package/src/components/show/show.stories.tsx +0 -197
  159. package/src/components/show/show.test.tsx +0 -41
  160. package/src/components/show/show.tsx +0 -16
  161. package/src/components/stepper/Stepper.tsx +0 -190
  162. package/src/components/stepper/context/stepper-context.tsx +0 -11
  163. package/src/components/table/table-primitives.tsx +0 -122
  164. package/src/components/table/table.model.ts +0 -20
  165. package/src/components/table-pagination/index.ts +0 -2
  166. package/src/components/table-pagination/table-pagination.model.ts +0 -2
  167. package/src/components/table-pagination/table-pagination.stories.tsx +0 -23
  168. package/src/components/table-pagination/table-pagination.test.tsx +0 -32
  169. package/src/components/table-pagination/table-pagination.tsx +0 -108
  170. package/src/components/tabs/context/tabs-context.tsx +0 -14
  171. package/src/components/tag/tag.variants.ts +0 -31
  172. package/src/components/timeline/timeline-status.ts +0 -5
  173. package/src/components/tree/hooks/use-controllable-tree-state.ts +0 -80
  174. package/src/components/tree/tree-primitives.tsx +0 -126
@@ -1,22 +1,31 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react-vite";
2
- import { cva, VariantProps } from "class-variance-authority";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { Flame, SearchIcon } from "lucide-react";
4
- import { ComponentProps } from "react";
5
- import { BaseButton, Button, Heading } from "../../components";
4
+ import { type ComponentProps } from "react";
5
+ import { BaseButton, Button, buttonVariants } from "../../components";
6
6
  import { cn } from "../../lib";
7
- import { ThemeProvider } from "../../providers";
8
7
 
9
8
  /**
10
- * Componente principal para ejecutar acciones.
9
+ * Primary action trigger built on the [Base UI Button](https://base-ui.com/react/components/button) primitive.
11
10
  *
12
- * Extendiende al componente `BaseButton` agregándole variantes
11
+ * **Loading** pass an async function to `onClick` and loading state is managed automatically.
12
+ * Control it externally with the `loading` prop. The spinner overlays content while preserving
13
+ * the button's original dimensions so layout never shifts.
14
+ *
15
+ * **Custom elements** — use the `render` prop to swap the underlying element.
16
+ * For links, apply `buttonVariants` directly to a plain `<a>` — Base UI always sets
17
+ * `role="button"` which overrides the semantic link role on `<a>` elements rendered via `render`.
18
+ *
19
+ * **Extension** — use `BaseButton` to build fully custom variants without inheriting any styles.
13
20
  */
14
21
  const meta: Meta<typeof Button> = {
15
- title: "Data entry/Button",
22
+ title: "Components/Button",
16
23
  component: Button,
17
24
  args: {
18
- children: "Click me!",
25
+ children: "Click me",
19
26
  },
27
+ tags: ["beta"],
28
+ parameters: { layout: "centered" },
20
29
  };
21
30
 
22
31
  export default meta;
@@ -24,161 +33,176 @@ type Story = StoryObj<typeof Button>;
24
33
 
25
34
  export const Default: Story = {};
26
35
 
27
- export const Outline: Story = {
28
- args: {
29
- variant: "outline",
30
- },
31
- };
32
-
33
- export const Secondary: Story = {
34
- args: {
35
- variant: "secondary",
36
- },
37
- };
38
-
39
- export const Ghost: Story = {
40
- args: {
41
- variant: "ghost",
42
- },
43
- };
44
-
45
- export const Link: Story = {
46
- args: {
47
- variant: "link",
48
- },
36
+ export const Variants: Story = {
37
+ render: (args) => (
38
+ <div className="flex flex-wrap items-center gap-3">
39
+ {(
40
+ [
41
+ "default",
42
+ "outline",
43
+ "secondary",
44
+ "ghost",
45
+ "link",
46
+ "error",
47
+ "success",
48
+ "warning",
49
+ ] as const
50
+ ).map((variant) => (
51
+ <Button key={variant} {...args} variant={variant}>
52
+ {variant}
53
+ </Button>
54
+ ))}
55
+ </div>
56
+ ),
49
57
  };
50
58
 
51
- export const Error: Story = {
52
- args: {
53
- variant: "error",
54
- },
55
- };
59
+ export const Sizes: Story = {
60
+ render: () => {
61
+ const textSizes = ["xs", "sm", "default", "lg"] as const;
62
+ const iconSizes = ["icon-xs", "icon-sm", "icon", "icon-lg"] as const;
56
63
 
57
- export const Success: Story = {
58
- args: {
59
- variant: "success",
64
+ return (
65
+ <div className="space-y-6">
66
+ <div className="flex flex-wrap items-end gap-4">
67
+ {textSizes.map((size) => (
68
+ <div key={size} className="flex flex-col items-center gap-2">
69
+ <p className="text-xs text-muted-foreground">{size}</p>
70
+ <Button size={size}>Click me</Button>
71
+ </div>
72
+ ))}
73
+ </div>
74
+ <div className="flex flex-wrap items-end gap-4">
75
+ {iconSizes.map((size) => (
76
+ <div key={size} className="flex flex-col items-center gap-2">
77
+ <p className="text-xs text-muted-foreground">{size}</p>
78
+ <Button size={size}>
79
+ <Flame />
80
+ </Button>
81
+ </div>
82
+ ))}
83
+ </div>
84
+ </div>
85
+ );
60
86
  },
61
87
  };
62
88
 
63
89
  /**
64
- * Se puede agregar un ícono al botón con la prop `icon`.
90
+ * Pass any icon element to the `icon` prop. It renders before the label by default
91
+ * (`inline-start`). Add `data-icon="inline-end"` to the icon element to move it after
92
+ * the label. Use an icon-only size (`icon`, `icon-sm`, etc.) for square icon buttons.
65
93
  */
66
- export const Icon: Story = {
67
- args: {
68
- icon: <SearchIcon />,
69
- },
70
- };
94
+ export const WithIcon: Story = {
95
+ render: (args) => (
96
+ <div className="flex flex-wrap items-center gap-4">
97
+ <Button {...args} icon={<SearchIcon />}>
98
+ Search
99
+ </Button>
100
+ <Button {...args} icon={<SearchIcon data-icon="inline-end" />}>
101
+ Search
102
+ </Button>
103
+ <Button icon={<SearchIcon />} size="icon" />
104
+ </div>
105
+ ),
106
+ parameters: {
107
+ docs: {
108
+ source: {
109
+ code: `<Button icon={<SearchIcon />}>Search</Button>
71
110
 
72
- /**
73
- * Se puede cambiar la posición del ícono con la prop `iconPosition`.
74
- *
75
- * Los valores posibles son `start` y `end`.
76
- */
77
- export const IconPosition: Story = {
78
- args: {
79
- icon: <SearchIcon />,
80
- iconPosition: "end",
111
+ <Button icon={<SearchIcon data-icon="inline-end" />}>Search</Button>
112
+
113
+ <Button icon={<SearchIcon />} size="icon" />`,
114
+ },
115
+ },
81
116
  },
82
117
  };
83
118
 
84
119
  /**
85
- * El button recibe la propieda `loading` que permite controlar el estado de carga del botón.
86
- * Se puede cambiar la forma en la que se muestra el loader con la prop `loaderReplace`.
87
- * Si `loaderReplace` es `true`, el loader reemplaza todo el contenido manteniendo el ancho original del botón.
88
- * Por defecto, el loader se agrega al contenido existente o reemplaza al ícono en caso de que exista.
120
+ * When `onClick` returns a `Promise`, loading activates automatically and stops when
121
+ * the Promise settles no external state needed. Pass `loading` directly to control it
122
+ * from outside (e.g., from a form submission handler or mutation hook).
123
+ *
124
+ * While loading, the button is non-interactive (`pointer-events-none`, `aria-busy`)
125
+ * and the spinner overlays content without changing the button's dimensions.
89
126
  */
90
127
  export const Loading: Story = {
91
128
  render: () => {
92
- const sleep = (): Promise<void> => {
93
- return new Promise((resolve) => setTimeout(resolve, 2000));
94
- };
129
+ const sleep = (): Promise<void> =>
130
+ new Promise((resolve) => setTimeout(resolve, 2000));
95
131
 
96
132
  return (
97
- <div className="space-y-8">
98
- {/* Modo Append (default) */}
99
- <section>
100
- <Heading as="h2" className="mb-4">
101
- Modo Append (default)
102
- </Heading>
103
- <p className="text-sm text-muted-foreground mb-4">
104
- En este modo, el loader se agrega al contenido existente. Si hay un
105
- ícono, el loader lo reemplaza.
133
+ <div className="space-y-10">
134
+ <section className="space-y-3">
135
+ <h2 className="text-sm font-semibold">Auto — async onClick</h2>
136
+ <p className="text-sm text-muted-foreground">
137
+ Click any button. Loading starts when the Promise begins and stops
138
+ when it resolves.
106
139
  </p>
107
- <div className="flex flex-wrap items-center gap-4">
108
- <Button onClick={sleep}>Por izquierda</Button>
109
- <Button onClick={sleep} iconPosition="end">
110
- Por derecha
111
- </Button>
140
+ <div className="flex flex-wrap items-center gap-3">
141
+ <Button onClick={sleep}>Save</Button>
112
142
  <Button onClick={sleep} icon={<SearchIcon />}>
113
- Ícono inicio
114
- </Button>
115
- <Button onClick={sleep} icon={<SearchIcon />} iconPosition="end">
116
- Ícono final
143
+ Search
117
144
  </Button>
118
145
  <Button onClick={sleep} icon={<SearchIcon />} size="icon" />
119
146
  </div>
120
147
  </section>
121
148
 
122
- {/* Modo Replace */}
123
- <section>
124
- <Heading as="h2" className="mb-4">
125
- Modo Replace (loaderReplace = true)
126
- </Heading>
127
- <p className="text-sm text-muted-foreground mb-4">
128
- En este modo, el loader reemplaza todo el contenido manteniendo el
129
- ancho original del botón.
149
+ <section className="space-y-3">
150
+ <h2 className="text-sm font-semibold">Controlled</h2>
151
+ <p className="text-sm text-muted-foreground">
152
+ Pass the <code>loading</code> prop directly to control state
153
+ externally.
130
154
  </p>
131
- <div className="flex flex-wrap items-center gap-4">
132
- <Button onClick={sleep} loaderReplace>
133
- Solo texto
155
+ <div className="flex flex-wrap items-center gap-3">
156
+ <Button loading>Save</Button>
157
+ <Button loading icon={<SearchIcon />}>
158
+ Search
134
159
  </Button>
135
- <Button onClick={sleep} loaderReplace icon={<SearchIcon />}>
136
- Ícono inicio
137
- </Button>
138
- <Button
139
- onClick={sleep}
140
- loaderReplace
141
- icon={<SearchIcon />}
142
- iconPosition="end"
143
- >
144
- Ícono final
145
- </Button>
146
- <Button
147
- onClick={sleep}
148
- loaderReplace
149
- icon={<SearchIcon />}
150
- size="icon"
151
- />
160
+ <Button loading icon={<SearchIcon />} size="icon" />
152
161
  </div>
153
162
  </section>
154
163
 
155
- {/* Variantes */}
156
- <section>
157
- <Heading as="h2" className="mb-4">
158
- Variantes
159
- </Heading>
160
- <p className="text-sm text-muted-foreground mb-4">
161
- El loader funciona con todas las variantes de botón.
162
- </p>
163
- <div className="flex flex-wrap items-center gap-4">
164
- <Button onClick={sleep} variant="outline" icon={<SearchIcon />}>
165
- Outline
166
- </Button>
167
- <Button onClick={sleep} variant="secondary" icon={<SearchIcon />}>
168
- Secondary
169
- </Button>
170
- <Button onClick={sleep} variant="ghost" icon={<SearchIcon />}>
171
- Ghost
172
- </Button>
173
- <Button onClick={sleep} variant="link" icon={<SearchIcon />}>
174
- Link
175
- </Button>
176
- <Button onClick={sleep} variant="error" icon={<SearchIcon />}>
177
- Error
178
- </Button>
179
- <Button onClick={sleep} variant="success" icon={<SearchIcon />}>
180
- Success
181
- </Button>
164
+ <section className="space-y-3">
165
+ <h2 className="text-sm font-semibold">All sizes</h2>
166
+ <div className="flex flex-wrap items-end gap-4">
167
+ {(["xs", "sm", "default", "lg"] as const).map((size) => (
168
+ <div key={size} className="flex flex-col items-center gap-2">
169
+ <p className="text-xs text-muted-foreground">{size}</p>
170
+ <Button loading size={size}>
171
+ Save
172
+ </Button>
173
+ </div>
174
+ ))}
175
+ {(["icon-xs", "icon-sm", "icon", "icon-lg"] as const).map(
176
+ (size) => (
177
+ <div key={size} className="flex flex-col items-center gap-2">
178
+ <p className="text-xs text-muted-foreground">{size}</p>
179
+ <Button loading size={size}>
180
+ <SearchIcon />
181
+ </Button>
182
+ </div>
183
+ ),
184
+ )}
185
+ </div>
186
+ </section>
187
+
188
+ <section className="space-y-3">
189
+ <h2 className="text-sm font-semibold">All variants</h2>
190
+ <div className="flex flex-wrap items-center gap-3">
191
+ {(
192
+ [
193
+ "default",
194
+ "outline",
195
+ "secondary",
196
+ "ghost",
197
+ "error",
198
+ "success",
199
+ "warning",
200
+ ] as const
201
+ ).map((variant) => (
202
+ <Button key={variant} onClick={sleep} variant={variant}>
203
+ {variant}
204
+ </Button>
205
+ ))}
182
206
  </div>
183
207
  </section>
184
208
  </div>
@@ -187,83 +211,55 @@ export const Loading: Story = {
187
211
  };
188
212
 
189
213
  /**
190
- * Al setear `asChild` a `true`, puedes renderizar cualquier elemento con los estilos, props y referencias del `Button`.
214
+ * Use the `render` prop to render the button as any element while keeping all styles
215
+ * and behavior. For navigation links, use `buttonVariants` on a plain `<a>` tag instead —
216
+ * Base UI always applies `role="button"` which overrides the semantic `<a>` role.
191
217
  *
192
- * En este caso, se usa un elemento `a` que hereda los estilos, props y referencias del `Button`.
193
- * Esto es útil para aplicar los mismos estilos y garantizar consistencia en la interfaz de usuario de su aplicación.
194
- * Se puede verificar abriendo la consola del navegador.
195
- */
196
- export const AsChild: Story = {
197
- args: {
198
- children: <a href="#">Click me</a>,
199
- variant: "error",
200
- asChild: true,
201
- },
202
- };
203
-
204
- /**
205
- * El componente recibe todas las propiedades de `button`, por lo que se puede customizar los estilos, accesibilidad, referencias, eventos, etc
206
- */
207
- export const Custom: Story = {
208
- args: {
209
- className: "bg-amber-500 hover:bg-amber-600",
210
- },
211
- };
212
-
213
- /**
214
- * Estos son los tamanos disponibles para el `Button`.
218
+ * ```tsx
219
+ * // Render as a different element
220
+ * <Button render={<div />}>Custom element</Button>
221
+ *
222
+ * // Link styled as a button — correct pattern for navigation
223
+ * <a href="/dashboard" className={buttonVariants({ variant: "outline" })}>
224
+ * Go to dashboard
225
+ * </a>
226
+ * ```
215
227
  */
216
- export const Sizes: Story = {
217
- render: () => {
218
- const sizes = ["default", "sm", "lg", "icon"] as const;
219
-
220
- return (
221
- <>
222
- {sizes.map((size) => (
223
- <div key={size} className="mb-1">
224
- <p className="font-semibold">{`size: ${size}`}</p>
225
- <Button size={size} key={size}>
226
- {size === "icon" ? <Flame /> : "Click me"}
227
- </Button>
228
- </div>
229
- ))}
230
- </>
231
- );
232
- },
233
- };
234
-
235
- export const Shapes: Story = {
236
- render: () => {
237
- const shapes = ["rounded", "square", "circle"] as const;
228
+ export const Render: Story = {
229
+ render: (args) => (
230
+ <div className="flex flex-wrap items-center gap-4">
231
+ <Button {...args} render={<div />}>
232
+ Rendered as div
233
+ </Button>
234
+ <a href="#" className={buttonVariants({ variant: "outline" })}>
235
+ Link as button
236
+ </a>
237
+ </div>
238
+ ),
239
+ parameters: {
240
+ docs: {
241
+ source: {
242
+ code: `<Button render={<div />}>Rendered as div</Button>
238
243
 
239
- return (
240
- <div className="flex flex-wrap items-center gap-4">
241
- {shapes.map((shape) => (
242
- <div key={shape} className="flex flex-col gap-2">
243
- <p className="font-semibold capitalize">{shape}</p>
244
- <Button shape={shape} key={shape} size="icon" icon={<Flame />} />
245
- <Button shape={shape} key={shape}>
246
- {shape}
247
- </Button>
248
- </div>
249
- ))}
250
- </div>
251
- );
244
+ <a href="/dashboard" className={buttonVariants({ variant: "outline" })}>
245
+ Link as button
246
+ </a>`,
247
+ },
248
+ },
252
249
  },
253
250
  };
254
251
 
255
- const buttonVariants = cva(
252
+ const customButtonVariants = cva(
256
253
  [
257
- "flex items-center justify-center gap-2 px-4 py-2",
258
- "text-sm font-medium transition-all",
259
- "rounded ring-offset-background",
260
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
254
+ "inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2",
255
+ "text-sm font-medium transition-all outline-none select-none",
256
+ "focus-visible:ring-3 focus-visible:ring-ring/50",
261
257
  "disabled:pointer-events-none disabled:opacity-50",
262
258
  ],
263
259
  {
264
260
  variants: {
265
261
  variant: {
266
- white: "bg-white text-primary border",
262
+ white: "border border-border bg-white text-primary",
267
263
  black: "bg-black text-white",
268
264
  },
269
265
  },
@@ -274,123 +270,52 @@ const buttonVariants = cva(
274
270
  );
275
271
 
276
272
  type BaseButtonProps = ComponentProps<typeof BaseButton>;
277
- interface Props extends BaseButtonProps, VariantProps<typeof buttonVariants> {}
273
+ interface CustomButtonProps
274
+ extends BaseButtonProps,
275
+ VariantProps<typeof customButtonVariants> {}
278
276
 
279
- const CustomButton = ({ variant, ...props }: Props) => {
280
- return <BaseButton className={cn(buttonVariants({ variant }))} {...props} />;
281
- };
277
+ const CustomButton = ({ variant, className, ...props }: CustomButtonProps) => (
278
+ <BaseButton
279
+ className={cn(customButtonVariants({ variant }), className)}
280
+ {...props}
281
+ />
282
+ );
282
283
 
283
284
  /**
284
- * #### Crea tus propias variantes
285
- * En caso de que necesites customizar demasiado a tu componente, lo mejor es crear tus propias variantes.
286
- * Para esto se puede importar el componente `BaseButton` el cuál no incluye estilos por defecto y se puede extender para crear variantes personalizadas.
287
- *
288
- * A continuación, se definen las variantes de botón que puedes extender o modificar a tus necesidades.
285
+ * `BaseButton` is an unstyled wrapper around the Base UI primitive. Use it to build
286
+ * fully custom button variants without inheriting any library styles.
287
+ * It supports all native button props plus `loading` and the `render` prop.
289
288
  *
290
289
  * ```tsx
291
- * import { ComponentProps } from 'react';
290
+ * import { type ComponentProps } from 'react';
292
291
  * import { cva, type VariantProps } from 'class-variance-authority';
293
292
  * import { BaseButton, cn } from '@boxcustodia/library';
294
293
  *
295
- * export const buttonVariants = cva(
296
- * [
297
- * 'flex items-center justify-center gap-2',
298
- * 'text-sm font-medium transition-all',
299
- * 'rounded-md ring-offset-background',
300
- * 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
301
- * 'disabled:pointer-events-none disabled:opacity-50',
302
- * 'hover:brightness-105',
303
- * ],
294
+ * const myVariants = cva(
295
+ * 'inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all outline-none disabled:pointer-events-none disabled:opacity-50',
304
296
  * {
305
297
  * variants: {
306
298
  * variant: {
307
- * default: "bg-primary text-primary-foreground",
308
- * error: "bg-error text-error-foreground",
309
- * success: "bg-success text-success-foreground",
310
- * outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
311
- * secondary: "bg-secondary text-secondary-foreground hover:brightness-100 hover:bg-secondary/80",
312
- * ghost: "hover:bg-accent hover:brightness-100 hover:text-accent-foreground",
313
- * link: "text-primary underline-offset-4 hover:underline",
314
- * white: 'bg-white text-primary', <-- Nueva variante
315
- * black: 'bg-black text-white', <-- Nueva variante
316
- * },
317
- * size: {
318
- * default: "h-10 px-4 py-2",
319
- * sm: "h-9 rounded-md px-3",
320
- * lg: "h-11 rounded-md px-8",
321
- * icon: "w-10 aspect-square",
299
+ * white: 'border border-border bg-white text-primary',
300
+ * black: 'bg-black text-white',
322
301
  * },
323
302
  * },
324
- * defaultVariants: {
325
- * variant: 'default',
326
- * size: 'default',
327
- * },
303
+ * defaultVariants: { variant: 'white' },
328
304
  * },
329
305
  * );
330
306
  *
331
- * type BaseButtonProps = ComponentProps<typeof BaseButton>;
332
- * typer Props = BaseButtonProps, VariantProps<typeof buttonVariants>
333
- *
334
- * export const Button = ({ className, variant, size, ...props }: Props) => {
335
- * return (
336
- * <BaseButton
337
- * {...props}
338
- * className={cn(buttonVariants({ variant, size }), className)}
339
- * />
340
- * );
341
- * };
307
+ * type Props = ComponentProps<typeof BaseButton> & VariantProps<typeof myVariants>;
342
308
  *
309
+ * export const CustomButton = ({ variant, className, ...props }: Props) => (
310
+ * <BaseButton className={cn(myVariants({ variant }), className)} {...props} />
311
+ * );
343
312
  * ```
344
- *
345
313
  */
346
-
347
314
  export const CustomVariants: Story = {
348
315
  render: () => (
349
- <div className="space-y-4">
350
- <CustomButton variant="black">Default Button</CustomButton>
351
- <CustomButton variant="white">Error Button</CustomButton>
316
+ <div className="flex flex-wrap items-center gap-4">
317
+ <CustomButton variant="white">White button</CustomButton>
318
+ <CustomButton variant="black">Black button</CustomButton>
352
319
  </div>
353
320
  ),
354
321
  };
355
-
356
- /**
357
- * ThemeProvider permite customizar de manera global las propiedades del componente
358
- *
359
- * Ejemplo:
360
- * ```tsx
361
- * <ThemeProvider
362
- * theme={{
363
- * Button: {
364
- * className: "bg-red-500",
365
- * icon: <SearchIcon />,
366
- * iconPosition: "end",
367
- * loaderReplace: true,
368
- * },
369
- * }}
370
- * >
371
- * <Button>Default Button</Button>
372
- * </ThemeProvider>
373
- * ```
374
- */
375
- export const Theme: Story = {
376
- render: () => {
377
- const sleep = (): Promise<void> => {
378
- return new Promise((resolve) => setTimeout(resolve, 2000));
379
- };
380
-
381
- return (
382
- <ThemeProvider
383
- theme={{
384
- Button: {
385
- className: "bg-red-500",
386
- icon: <SearchIcon />,
387
- iconPosition: "end",
388
- loaderReplace: true,
389
- },
390
- }}
391
- >
392
- <Button onClick={sleep}>Default Button</Button>
393
- </ThemeProvider>
394
- );
395
- },
396
- };
@@ -18,21 +18,14 @@ describe("Button component", () => {
18
18
  expect(button).toHaveTextContent("Click me");
19
19
  });
20
20
 
21
- it("should render as child correctly", () => {
22
- render(
23
- <Button asChild>
24
- <a href="#">Click me</a>
25
- </Button>,
26
- );
27
-
28
- // No debe renderizarse como un button
29
- const button = screen.queryByRole("button");
30
- expect(button).not.toBeInTheDocument();
31
-
32
- // Debe renderizarse como un link
33
- const link = screen.getByRole("link");
34
- expect(link).toBeInTheDocument();
35
- expect(link).toHaveTextContent("Click me");
21
+ it("should render as custom element via render prop", () => {
22
+ render(<Button render={<a href="#">Click me</a>}>Click me</Button>);
23
+
24
+ // Base UI always applies role="button" even on custom elements
25
+ const button = screen.getByRole("button");
26
+ expect(button).toBeInTheDocument();
27
+ expect(button).toHaveTextContent("Click me");
28
+ expect(button.tagName.toLowerCase()).toBe("a");
36
29
  });
37
30
 
38
31
  it("should accept className prop", () => {
@@ -51,8 +44,8 @@ describe("Button component", () => {
51
44
  expect(ref.current).toBeInstanceOf(HTMLButtonElement);
52
45
  });
53
46
 
54
- it("should show loader", () => {
47
+ it("should set data-loading when loading", () => {
55
48
  render(<Button loading />);
56
- expect(screen.getByTestId("btn-loader")).toBeInTheDocument();
49
+ expect(screen.getByRole("button")).toHaveAttribute("data-loading");
57
50
  });
58
51
  });