@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
|
@@ -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,
|
|
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
|
-
*
|
|
9
|
+
* Primary action trigger built on the [Base UI Button](https://base-ui.com/react/components/button) primitive.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
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: "
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
*
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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
|
-
|
|
94
|
-
};
|
|
129
|
+
const sleep = (): Promise<void> =>
|
|
130
|
+
new Promise((resolve) => setTimeout(resolve, 2000));
|
|
95
131
|
|
|
96
132
|
return (
|
|
97
|
-
<div className="space-y-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
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-
|
|
108
|
-
<Button onClick={sleep}>
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
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-
|
|
132
|
-
<Button
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
217
|
-
render: () =>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
273
|
+
interface CustomButtonProps
|
|
274
|
+
extends BaseButtonProps,
|
|
275
|
+
VariantProps<typeof customButtonVariants> {}
|
|
278
276
|
|
|
279
|
-
const CustomButton = ({ variant, ...props }:
|
|
280
|
-
|
|
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
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
308
|
-
*
|
|
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
|
|
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="
|
|
350
|
-
<CustomButton variant="
|
|
351
|
-
<CustomButton variant="
|
|
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
|
|
22
|
-
render(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
47
|
+
it("should set data-loading when loading", () => {
|
|
55
48
|
render(<Button loading />);
|
|
56
|
-
expect(screen.
|
|
49
|
+
expect(screen.getByRole("button")).toHaveAttribute("data-loading");
|
|
57
50
|
});
|
|
58
51
|
});
|