appos 0.4.3-0 → 0.5.1-0
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/exports/api/index.d.mts +3 -3
- package/dist/exports/api/index.mjs +34 -9
- package/dist/exports/api/workflows/index.d.mts +2 -2
- package/dist/exports/api/workflows/index.mjs +2 -2
- package/dist/exports/{auth-By0xx0MI.d.mts → auth-BCOIpGDO.d.mts} +325 -214
- package/dist/exports/cli/index.d.mts +105 -106
- package/dist/exports/cli/index.mjs +87 -13
- package/dist/exports/{constants-BePPc_yF.mjs → constants-BicCnEiJ.mjs} +7 -1
- package/dist/exports/{index-Bpo5QE7k.d.mts → index-CiuTp085.d.mts} +580 -614
- package/dist/exports/{openapi-crG3j4xx.mjs → openapi-uisUTLq7.mjs} +45 -102
- package/dist/exports/{send-email-Bgcdjy-e.mjs → send-email-CXxlkfFL.mjs} +1 -1
- package/dist/exports/{server-BlNxgHUc.mjs → server-CA4aI0U6.mjs} +6 -2
- package/dist/exports/storybook/index.d.ts +40 -0
- package/dist/exports/storybook/index.js +43 -0
- package/dist/exports/tests/api.d.mts +2 -2
- package/dist/exports/tests/api.mjs +3 -3
- package/dist/exports/vite/index.d.mts +19 -3
- package/dist/exports/vite/index.mjs +82 -6
- package/dist/exports/web/index.d.ts +2247 -700
- package/dist/exports/web/index.js +2445 -754
- package/dist/exports/web/routes.d.mts +41 -43
- package/dist/exports/web/ssr.d.mts +2 -2
- package/package.json +48 -31
- package/src/storybook/.storybook/main.ts +119 -0
- package/src/storybook/.storybook/manager.ts +75 -0
- package/src/storybook/.storybook/preview.tsx +76 -0
- package/src/storybook/components/ColorGrid.tsx +324 -0
- package/src/storybook/components/IconGallery.tsx +268 -0
- package/src/storybook/components/SpacingScale.tsx +236 -0
- package/src/storybook/components/TypographyScale.tsx +277 -0
- package/src/storybook/components/index.ts +3 -0
- package/src/storybook/docs/Introduction.mdx +29 -0
- package/src/storybook/docs/foundations/Colors.mdx +27 -0
- package/src/storybook/docs/foundations/Icons.mdx +26 -0
- package/src/storybook/docs/foundations/Spacing.mdx +23 -0
- package/src/storybook/docs/foundations/Typography.mdx +39 -0
- package/src/storybook/index.ts +147 -0
- package/src/storybook/shims/void-elements.js +18 -0
- package/src/web/client/auth-paths.ts +148 -0
- package/src/web/client/auth.ts +218 -0
- package/src/web/client/i18n.d.ts +9 -0
- package/src/web/client/i18n.ts +127 -0
- package/src/web/client/react-router.ts +84 -0
- package/src/web/hooks/index.ts +1 -0
- package/src/web/hooks/use-mobile.ts +19 -0
- package/src/web/index.ts +42 -0
- package/src/web/lib/colors.ts +220 -0
- package/src/web/lib/utils.ts +52 -0
- package/src/web/routes/auth/_layout.tsx +5 -11
- package/src/web/routes.ts +101 -0
- package/src/web/server/context.ts +14 -0
- package/src/web/server/render.ts +11 -0
- package/src/web/ssr.ts +17 -0
- package/src/web/ui/accordion.stories.tsx +183 -0
- package/src/web/ui/accordion.tsx +134 -0
- package/src/web/ui/alert-dialog.stories.tsx +215 -0
- package/src/web/ui/alert-dialog.tsx +328 -0
- package/src/web/ui/alert.stories.tsx +224 -0
- package/src/web/ui/alert.tsx +136 -0
- package/src/web/ui/animated-check.stories.tsx +200 -0
- package/src/web/ui/animated-check.tsx +98 -0
- package/src/web/ui/aspect-ratio.stories.tsx +204 -0
- package/src/web/ui/aspect-ratio.tsx +36 -0
- package/src/web/ui/auth/__mocks__/auth-context.tsx +273 -0
- package/src/web/ui/auth/auth-card.stories.tsx +295 -0
- package/src/web/ui/auth/auth-card.tsx +124 -0
- package/src/web/ui/auth/auth-layout.stories.tsx +330 -0
- package/src/web/ui/auth/auth-layout.tsx +269 -0
- package/src/web/ui/auth/captcha-widget.stories.tsx +200 -0
- package/src/web/ui/auth/captcha-widget.tsx +197 -0
- package/src/web/ui/auth/context.tsx +87 -0
- package/src/web/ui/auth/email-input.stories.tsx +167 -0
- package/src/web/ui/auth/email-input.tsx +84 -0
- package/src/web/ui/auth/forms/email-otp-form.tsx +377 -0
- package/src/web/ui/auth/forms/forgot-password-form.tsx +240 -0
- package/src/web/ui/auth/forms/index.ts +40 -0
- package/src/web/ui/auth/forms/login-form.tsx +486 -0
- package/src/web/ui/auth/forms/phone-otp-form.tsx +388 -0
- package/src/web/ui/auth/forms/reset-password-form.tsx +331 -0
- package/src/web/ui/auth/forms/signup-form.tsx +499 -0
- package/src/web/ui/auth/forms/two-factor-form.tsx +410 -0
- package/src/web/ui/auth/forms/verify-email-form.tsx +373 -0
- package/src/web/ui/auth/hooks.ts +266 -0
- package/src/web/ui/auth/index.ts +31 -0
- package/src/web/ui/auth/magic-link-form.tsx +191 -0
- package/src/web/ui/auth/otp-input.stories.tsx +199 -0
- package/src/web/ui/auth/otp-input.tsx +157 -0
- package/src/web/ui/auth/passkey-button.stories.tsx +115 -0
- package/src/web/ui/auth/passkey-button.tsx +113 -0
- package/src/web/ui/auth/passkey-register-button.stories.tsx +109 -0
- package/src/web/ui/auth/passkey-register-button.tsx +106 -0
- package/src/web/ui/auth/password-input.stories.tsx +287 -0
- package/src/web/ui/auth/password-input.tsx +306 -0
- package/src/web/ui/auth/phone-input.stories.tsx +160 -0
- package/src/web/ui/auth/phone-input.tsx +89 -0
- package/src/web/ui/auth/social-buttons.stories.tsx +217 -0
- package/src/web/ui/auth/social-buttons.tsx +196 -0
- package/src/web/ui/auth/sso-button.stories.tsx +160 -0
- package/src/web/ui/auth/sso-button.tsx +108 -0
- package/src/web/ui/auth/trust-signals.stories.tsx +208 -0
- package/src/web/ui/auth/trust-signals.tsx +260 -0
- package/src/web/ui/avatar.stories.tsx +302 -0
- package/src/web/ui/avatar.tsx +189 -0
- package/src/web/ui/badge.stories.tsx +225 -0
- package/src/web/ui/badge.tsx +66 -0
- package/src/web/ui/brand.stories.tsx +266 -0
- package/src/web/ui/brand.tsx +162 -0
- package/src/web/ui/breadcrumb.stories.tsx +271 -0
- package/src/web/ui/breadcrumb.tsx +214 -0
- package/src/web/ui/button-group.stories.tsx +251 -0
- package/src/web/ui/button-group.tsx +87 -0
- package/src/web/ui/button.stories.tsx +264 -0
- package/src/web/ui/button.tsx +120 -0
- package/src/web/ui/calendar.stories.tsx +235 -0
- package/src/web/ui/calendar.tsx +221 -0
- package/src/web/ui/card.stories.tsx +262 -0
- package/src/web/ui/card.tsx +199 -0
- package/src/web/ui/carousel.stories.tsx +244 -0
- package/src/web/ui/carousel.tsx +241 -0
- package/src/web/ui/chart.stories.tsx +833 -0
- package/src/web/ui/chart.tsx +390 -0
- package/src/web/ui/checkbox.stories.tsx +208 -0
- package/src/web/ui/checkbox.tsx +39 -0
- package/src/web/ui/collapsible.stories.tsx +239 -0
- package/src/web/ui/collapsible.tsx +21 -0
- package/src/web/ui/command.stories.tsx +291 -0
- package/src/web/ui/command.tsx +189 -0
- package/src/web/ui/context-menu.stories.tsx +255 -0
- package/src/web/ui/context-menu.tsx +263 -0
- package/src/web/ui/dialog.stories.tsx +263 -0
- package/src/web/ui/dialog.tsx +273 -0
- package/src/web/ui/drawer.stories.tsx +299 -0
- package/src/web/ui/drawer.tsx +130 -0
- package/src/web/ui/dropdown-menu.stories.tsx +320 -0
- package/src/web/ui/dropdown-menu.tsx +263 -0
- package/src/web/ui/empty.stories.tsx +204 -0
- package/src/web/ui/empty.tsx +101 -0
- package/src/web/ui/error-page.stories.tsx +181 -0
- package/src/web/ui/error-page.tsx +136 -0
- package/src/web/ui/field.stories.tsx +364 -0
- package/src/web/ui/field.tsx +252 -0
- package/src/web/ui/globals.css +3 -0
- package/src/web/ui/hover-card.stories.tsx +261 -0
- package/src/web/ui/hover-card.tsx +50 -0
- package/src/web/ui/index.ts +55 -0
- package/src/web/ui/input-group.stories.tsx +290 -0
- package/src/web/ui/input-group.tsx +157 -0
- package/src/web/ui/input-otp.stories.tsx +235 -0
- package/src/web/ui/input-otp.tsx +86 -0
- package/src/web/ui/input.stories.tsx +205 -0
- package/src/web/ui/input.tsx +30 -0
- package/src/web/ui/item.stories.tsx +348 -0
- package/src/web/ui/item.tsx +200 -0
- package/src/web/ui/kbd.stories.tsx +236 -0
- package/src/web/ui/kbd.tsx +27 -0
- package/src/web/ui/label.stories.tsx +180 -0
- package/src/web/ui/label.tsx +32 -0
- package/src/web/ui/locales/en/ui.json +13 -30
- package/src/web/ui/locales/zh-CN/ui.json +17 -34
- package/src/web/ui/locales/zh-TW/ui.json +17 -34
- package/src/web/ui/menubar.stories.tsx +385 -0
- package/src/web/ui/menubar.tsx +273 -0
- package/src/web/ui/navigation-menu.stories.tsx +361 -0
- package/src/web/ui/navigation-menu.tsx +168 -0
- package/src/web/ui/pagination.stories.tsx +312 -0
- package/src/web/ui/pagination.tsx +221 -0
- package/src/web/ui/popover.stories.tsx +315 -0
- package/src/web/ui/popover.tsx +89 -0
- package/src/web/ui/progress.stories.tsx +239 -0
- package/src/web/ui/progress.tsx +128 -0
- package/src/web/ui/radio-group.stories.tsx +315 -0
- package/src/web/ui/radio-group.tsx +38 -0
- package/src/web/ui/resizable.stories.tsx +304 -0
- package/src/web/ui/resizable.tsx +53 -0
- package/src/web/ui/scroll-area.stories.tsx +276 -0
- package/src/web/ui/scroll-area.tsx +53 -0
- package/src/web/ui/select.stories.tsx +314 -0
- package/src/web/ui/select.tsx +312 -0
- package/src/web/ui/separator.stories.tsx +246 -0
- package/src/web/ui/separator.tsx +36 -0
- package/src/web/ui/sheet.stories.tsx +417 -0
- package/src/web/ui/sheet.tsx +133 -0
- package/src/web/ui/sidebar.stories.tsx +402 -0
- package/src/web/ui/sidebar.tsx +723 -0
- package/src/web/ui/skeleton.stories.tsx +235 -0
- package/src/web/ui/skeleton.tsx +26 -0
- package/src/web/ui/slider.stories.tsx +294 -0
- package/src/web/ui/slider.tsx +75 -0
- package/src/web/ui/sonner.stories.tsx +279 -0
- package/src/web/ui/sonner.tsx +46 -0
- package/src/web/ui/spinner.stories.tsx +204 -0
- package/src/web/ui/spinner.tsx +28 -0
- package/src/web/ui/switch.stories.tsx +285 -0
- package/src/web/ui/switch.tsx +43 -0
- package/src/web/ui/table.stories.tsx +362 -0
- package/src/web/ui/table.tsx +115 -0
- package/src/web/ui/tabs.stories.tsx +401 -0
- package/src/web/ui/tabs.tsx +136 -0
- package/src/web/ui/textarea.stories.tsx +272 -0
- package/src/web/ui/textarea.tsx +28 -0
- package/src/web/ui/toggle-group.stories.tsx +334 -0
- package/src/web/ui/toggle-group.tsx +88 -0
- package/src/web/ui/toggle.stories.tsx +264 -0
- package/src/web/ui/toggle.tsx +57 -0
- package/src/web/ui/tooltip.stories.tsx +342 -0
- package/src/web/ui/tooltip.tsx +116 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import {
|
|
3
|
+
IconBold,
|
|
4
|
+
IconChevronDown,
|
|
5
|
+
IconCopy,
|
|
6
|
+
IconItalic,
|
|
7
|
+
IconUnderline,
|
|
8
|
+
} from "@tabler/icons-react";
|
|
9
|
+
import { orientations } from "#src/web/lib/utils.ts";
|
|
10
|
+
import { Button } from "#src/web/ui/button.tsx";
|
|
11
|
+
import {
|
|
12
|
+
ButtonGroup,
|
|
13
|
+
ButtonGroupSeparator,
|
|
14
|
+
ButtonGroupText,
|
|
15
|
+
} from "#src/web/ui/button-group.tsx";
|
|
16
|
+
import {
|
|
17
|
+
DropdownMenu,
|
|
18
|
+
DropdownMenuContent,
|
|
19
|
+
DropdownMenuItem,
|
|
20
|
+
DropdownMenuTrigger,
|
|
21
|
+
} from "#src/web/ui/dropdown-menu.tsx";
|
|
22
|
+
import { Input } from "#src/web/ui/input.tsx";
|
|
23
|
+
|
|
24
|
+
const meta = {
|
|
25
|
+
title: "Base/ButtonGroup",
|
|
26
|
+
component: ButtonGroup,
|
|
27
|
+
tags: ["autodocs"],
|
|
28
|
+
parameters: {
|
|
29
|
+
layout: "centered",
|
|
30
|
+
docs: {
|
|
31
|
+
description: {
|
|
32
|
+
component:
|
|
33
|
+
"Groups related buttons together with shared borders and styling. Supports horizontal and vertical orientations, separators, and text labels.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
argTypes: {
|
|
38
|
+
orientation: {
|
|
39
|
+
control: "select",
|
|
40
|
+
options: orientations,
|
|
41
|
+
description: "The orientation of the button group",
|
|
42
|
+
table: { defaultValue: { summary: "horizontal" } },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
} satisfies Meta<typeof ButtonGroup>;
|
|
46
|
+
|
|
47
|
+
export default meta;
|
|
48
|
+
type Story = StoryObj<typeof meta>;
|
|
49
|
+
|
|
50
|
+
export const Default: Story = {
|
|
51
|
+
render: () => (
|
|
52
|
+
<ButtonGroup>
|
|
53
|
+
<Button variant="outline">Left</Button>
|
|
54
|
+
<Button variant="outline">Center</Button>
|
|
55
|
+
<Button variant="outline">Right</Button>
|
|
56
|
+
</ButtonGroup>
|
|
57
|
+
),
|
|
58
|
+
parameters: {
|
|
59
|
+
docs: {
|
|
60
|
+
description: {
|
|
61
|
+
story:
|
|
62
|
+
"Default horizontal button group. Buttons share borders and have connected corners.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const WithIcons: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<ButtonGroup>
|
|
71
|
+
<Button variant="outline" size="icon" aria-label="Bold">
|
|
72
|
+
<IconBold />
|
|
73
|
+
</Button>
|
|
74
|
+
<Button variant="outline" size="icon" aria-label="Italic">
|
|
75
|
+
<IconItalic />
|
|
76
|
+
</Button>
|
|
77
|
+
<Button variant="outline" size="icon" aria-label="Underline">
|
|
78
|
+
<IconUnderline />
|
|
79
|
+
</Button>
|
|
80
|
+
</ButtonGroup>
|
|
81
|
+
),
|
|
82
|
+
parameters: {
|
|
83
|
+
docs: {
|
|
84
|
+
description: {
|
|
85
|
+
story:
|
|
86
|
+
"Icon button group for toolbar actions like text formatting. Always include aria-labels for accessibility.",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const Vertical: Story = {
|
|
93
|
+
render: () => (
|
|
94
|
+
<ButtonGroup orientation="vertical">
|
|
95
|
+
<Button variant="outline">Top</Button>
|
|
96
|
+
<Button variant="outline">Middle</Button>
|
|
97
|
+
<Button variant="outline">Bottom</Button>
|
|
98
|
+
</ButtonGroup>
|
|
99
|
+
),
|
|
100
|
+
parameters: {
|
|
101
|
+
docs: {
|
|
102
|
+
description: {
|
|
103
|
+
story:
|
|
104
|
+
'Vertical button group for stacked actions. Use orientation="vertical" prop.',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const WithSeparator: Story = {
|
|
111
|
+
render: () => (
|
|
112
|
+
<ButtonGroup>
|
|
113
|
+
<Button variant="outline">Save</Button>
|
|
114
|
+
<ButtonGroupSeparator />
|
|
115
|
+
<Button variant="outline" size="icon" aria-label="More options">
|
|
116
|
+
<IconChevronDown />
|
|
117
|
+
</Button>
|
|
118
|
+
</ButtonGroup>
|
|
119
|
+
),
|
|
120
|
+
parameters: {
|
|
121
|
+
docs: {
|
|
122
|
+
description: {
|
|
123
|
+
story:
|
|
124
|
+
"Button group with a visual separator between primary and secondary actions.",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const WithText: Story = {
|
|
131
|
+
render: () => (
|
|
132
|
+
<ButtonGroup>
|
|
133
|
+
<ButtonGroupText>https://</ButtonGroupText>
|
|
134
|
+
<Input placeholder="example.com" className="rounded-l-none border-l-0" />
|
|
135
|
+
</ButtonGroup>
|
|
136
|
+
),
|
|
137
|
+
parameters: {
|
|
138
|
+
docs: {
|
|
139
|
+
description: {
|
|
140
|
+
story:
|
|
141
|
+
"Button group with a text prefix. Use ButtonGroupText for static labels or prefixes.",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export const SplitButton: Story = {
|
|
148
|
+
render: () => (
|
|
149
|
+
<ButtonGroup>
|
|
150
|
+
<Button>Save</Button>
|
|
151
|
+
<DropdownMenu>
|
|
152
|
+
<DropdownMenuTrigger
|
|
153
|
+
render={<Button size="icon" aria-label="More save options" />}
|
|
154
|
+
>
|
|
155
|
+
<IconChevronDown />
|
|
156
|
+
</DropdownMenuTrigger>
|
|
157
|
+
<DropdownMenuContent align="end">
|
|
158
|
+
<DropdownMenuItem>Save as draft</DropdownMenuItem>
|
|
159
|
+
<DropdownMenuItem>Save and publish</DropdownMenuItem>
|
|
160
|
+
<DropdownMenuItem>Save and exit</DropdownMenuItem>
|
|
161
|
+
</DropdownMenuContent>
|
|
162
|
+
</DropdownMenu>
|
|
163
|
+
</ButtonGroup>
|
|
164
|
+
),
|
|
165
|
+
parameters: {
|
|
166
|
+
docs: {
|
|
167
|
+
description: {
|
|
168
|
+
story:
|
|
169
|
+
"Split button pattern with a primary action and dropdown for additional options.",
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const CopyWithPreview: Story = {
|
|
176
|
+
render: () => (
|
|
177
|
+
<ButtonGroup>
|
|
178
|
+
<ButtonGroupText className="text-muted-foreground font-mono text-xs">
|
|
179
|
+
npm install appos
|
|
180
|
+
</ButtonGroupText>
|
|
181
|
+
<Button variant="outline" size="icon" aria-label="Copy command">
|
|
182
|
+
<IconCopy />
|
|
183
|
+
</Button>
|
|
184
|
+
</ButtonGroup>
|
|
185
|
+
),
|
|
186
|
+
parameters: {
|
|
187
|
+
docs: {
|
|
188
|
+
description: {
|
|
189
|
+
story: "Copy-to-clipboard pattern with a code snippet and copy button.",
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const MixedSizes: Story = {
|
|
196
|
+
render: () => (
|
|
197
|
+
<div className="flex flex-col gap-4">
|
|
198
|
+
<ButtonGroup>
|
|
199
|
+
<Button variant="outline" size="sm">
|
|
200
|
+
Small
|
|
201
|
+
</Button>
|
|
202
|
+
<Button variant="outline" size="sm">
|
|
203
|
+
Group
|
|
204
|
+
</Button>
|
|
205
|
+
</ButtonGroup>
|
|
206
|
+
<ButtonGroup>
|
|
207
|
+
<Button variant="outline">Default</Button>
|
|
208
|
+
<Button variant="outline">Group</Button>
|
|
209
|
+
</ButtonGroup>
|
|
210
|
+
<ButtonGroup>
|
|
211
|
+
<Button variant="outline" size="lg">
|
|
212
|
+
Large
|
|
213
|
+
</Button>
|
|
214
|
+
<Button variant="outline" size="lg">
|
|
215
|
+
Group
|
|
216
|
+
</Button>
|
|
217
|
+
</ButtonGroup>
|
|
218
|
+
</div>
|
|
219
|
+
),
|
|
220
|
+
parameters: {
|
|
221
|
+
docs: {
|
|
222
|
+
description: {
|
|
223
|
+
story:
|
|
224
|
+
"Button groups work with all button sizes. All buttons in a group should use the same size.",
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export const Nested: Story = {
|
|
231
|
+
render: () => (
|
|
232
|
+
<ButtonGroup>
|
|
233
|
+
<ButtonGroup>
|
|
234
|
+
<Button variant="outline">A</Button>
|
|
235
|
+
<Button variant="outline">B</Button>
|
|
236
|
+
</ButtonGroup>
|
|
237
|
+
<ButtonGroup>
|
|
238
|
+
<Button variant="outline">C</Button>
|
|
239
|
+
<Button variant="outline">D</Button>
|
|
240
|
+
</ButtonGroup>
|
|
241
|
+
</ButtonGroup>
|
|
242
|
+
),
|
|
243
|
+
parameters: {
|
|
244
|
+
docs: {
|
|
245
|
+
description: {
|
|
246
|
+
story:
|
|
247
|
+
"Nested button groups for complex toolbar layouts. Inner groups maintain proper spacing.",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import type * as React from "react";
|
|
5
|
+
import { cn } from "#src/web/lib/utils.ts";
|
|
6
|
+
import { Separator } from "#src/web/ui/separator.tsx";
|
|
7
|
+
|
|
8
|
+
const buttonGroupVariants = cva(
|
|
9
|
+
"has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
orientation: {
|
|
13
|
+
horizontal:
|
|
14
|
+
"[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0 [&>[data-slot]]:rounded-r-none",
|
|
15
|
+
vertical:
|
|
16
|
+
"[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! flex-col [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0 [&>[data-slot]]:rounded-b-none",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
defaultVariants: {
|
|
20
|
+
orientation: "horizontal",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function ButtonGroup({
|
|
26
|
+
className,
|
|
27
|
+
orientation,
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
role="group"
|
|
33
|
+
data-slot="button-group"
|
|
34
|
+
data-orientation={orientation}
|
|
35
|
+
className={cn(buttonGroupVariants({ orientation }), className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ButtonGroupText({
|
|
42
|
+
className,
|
|
43
|
+
render,
|
|
44
|
+
...props
|
|
45
|
+
}: useRender.ComponentProps<"div">) {
|
|
46
|
+
return useRender({
|
|
47
|
+
defaultTagName: "div",
|
|
48
|
+
props: mergeProps<"div">(
|
|
49
|
+
{
|
|
50
|
+
className: cn(
|
|
51
|
+
"bg-muted gap-2 rounded-md border px-2.5 text-sm font-medium shadow-xs [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
|
|
52
|
+
className,
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
props,
|
|
56
|
+
),
|
|
57
|
+
render,
|
|
58
|
+
state: {
|
|
59
|
+
slot: "button-group-text",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ButtonGroupSeparator({
|
|
65
|
+
className,
|
|
66
|
+
orientation = "vertical",
|
|
67
|
+
...props
|
|
68
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
69
|
+
return (
|
|
70
|
+
<Separator
|
|
71
|
+
data-slot="button-group-separator"
|
|
72
|
+
orientation={orientation}
|
|
73
|
+
className={cn(
|
|
74
|
+
"bg-input relative self-stretch data-[orientation=horizontal]:mx-px data-[orientation=horizontal]:w-auto data-[orientation=vertical]:my-px data-[orientation=vertical]:h-auto",
|
|
75
|
+
className,
|
|
76
|
+
)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
ButtonGroup,
|
|
84
|
+
ButtonGroupSeparator,
|
|
85
|
+
ButtonGroupText,
|
|
86
|
+
buttonGroupVariants,
|
|
87
|
+
};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import {
|
|
3
|
+
IconChevronDown,
|
|
4
|
+
IconLoader2,
|
|
5
|
+
IconPlus,
|
|
6
|
+
IconTrash,
|
|
7
|
+
} from "@tabler/icons-react";
|
|
8
|
+
import { expect, fn, userEvent, within } from "storybook/test";
|
|
9
|
+
import {
|
|
10
|
+
Button,
|
|
11
|
+
buttonSizeKeys,
|
|
12
|
+
buttonVariantKeys,
|
|
13
|
+
} from "#src/web/ui/button.tsx";
|
|
14
|
+
|
|
15
|
+
const meta = {
|
|
16
|
+
title: "Base/Button",
|
|
17
|
+
component: Button,
|
|
18
|
+
tags: ["autodocs"],
|
|
19
|
+
parameters: {
|
|
20
|
+
layout: "centered",
|
|
21
|
+
docs: {
|
|
22
|
+
description: {
|
|
23
|
+
component:
|
|
24
|
+
"A versatile button component with multiple visual variants and sizes. Supports icons, loading states, and accessible icon-only buttons.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
argTypes: {
|
|
29
|
+
variant: {
|
|
30
|
+
control: "select",
|
|
31
|
+
options: buttonVariantKeys,
|
|
32
|
+
description: "The visual style of the button",
|
|
33
|
+
table: { defaultValue: { summary: "default" } },
|
|
34
|
+
},
|
|
35
|
+
size: {
|
|
36
|
+
control: "select",
|
|
37
|
+
options: buttonSizeKeys,
|
|
38
|
+
description: "The size of the button",
|
|
39
|
+
table: { defaultValue: { summary: "default" } },
|
|
40
|
+
},
|
|
41
|
+
disabled: {
|
|
42
|
+
control: "boolean",
|
|
43
|
+
description: "Whether the button is disabled",
|
|
44
|
+
},
|
|
45
|
+
children: {
|
|
46
|
+
control: "text",
|
|
47
|
+
description: "Button content",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
args: {
|
|
51
|
+
onClick: fn(),
|
|
52
|
+
},
|
|
53
|
+
} satisfies Meta<typeof Button>;
|
|
54
|
+
|
|
55
|
+
export default meta;
|
|
56
|
+
type Story = StoryObj<typeof meta>;
|
|
57
|
+
|
|
58
|
+
export const Default: Story = {
|
|
59
|
+
args: { children: "Button" },
|
|
60
|
+
parameters: {
|
|
61
|
+
docs: {
|
|
62
|
+
description: {
|
|
63
|
+
story:
|
|
64
|
+
"Primary action button. Use for the main call-to-action on a page.",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
play: async ({ canvasElement, args }) => {
|
|
69
|
+
const canvas = within(canvasElement);
|
|
70
|
+
await userEvent.click(canvas.getByRole("button"));
|
|
71
|
+
await expect(args.onClick).toHaveBeenCalled();
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const Secondary: Story = {
|
|
76
|
+
args: { children: "Secondary", variant: "secondary" },
|
|
77
|
+
parameters: {
|
|
78
|
+
docs: {
|
|
79
|
+
description: {
|
|
80
|
+
story:
|
|
81
|
+
"Secondary action. Use alongside primary buttons for less important actions.",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const Outline: Story = {
|
|
88
|
+
args: { children: "Outline", variant: "outline" },
|
|
89
|
+
parameters: {
|
|
90
|
+
docs: {
|
|
91
|
+
description: {
|
|
92
|
+
story:
|
|
93
|
+
"Outlined button with transparent background. Good for secondary actions that need visual distinction.",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const Ghost: Story = {
|
|
100
|
+
args: { children: "Ghost", variant: "ghost" },
|
|
101
|
+
parameters: {
|
|
102
|
+
docs: {
|
|
103
|
+
description: {
|
|
104
|
+
story:
|
|
105
|
+
"Minimal button with no background. Use for tertiary actions or in tight spaces.",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const Destructive: Story = {
|
|
112
|
+
args: { children: "Delete", variant: "destructive" },
|
|
113
|
+
parameters: {
|
|
114
|
+
docs: {
|
|
115
|
+
description: {
|
|
116
|
+
story:
|
|
117
|
+
"Destructive action button. Use for delete, remove, or other irreversible actions.",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const Link: Story = {
|
|
124
|
+
args: { children: "Link", variant: "link" },
|
|
125
|
+
parameters: {
|
|
126
|
+
docs: {
|
|
127
|
+
description: {
|
|
128
|
+
story:
|
|
129
|
+
"Link-styled button with underline on hover. Use for navigation-like actions.",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const AllSizes: Story = {
|
|
136
|
+
render: () => (
|
|
137
|
+
<div className="flex items-end gap-2">
|
|
138
|
+
<Button size="xs">Extra Small</Button>
|
|
139
|
+
<Button size="sm">Small</Button>
|
|
140
|
+
<Button size="default">Default</Button>
|
|
141
|
+
<Button size="lg">Large</Button>
|
|
142
|
+
</div>
|
|
143
|
+
),
|
|
144
|
+
parameters: {
|
|
145
|
+
docs: {
|
|
146
|
+
description: {
|
|
147
|
+
story: "All available text button sizes from extra small to large.",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const IconSizes: Story = {
|
|
154
|
+
render: () => (
|
|
155
|
+
<div className="flex items-end gap-2">
|
|
156
|
+
<Button size="icon-xs" aria-label="Add">
|
|
157
|
+
<IconPlus />
|
|
158
|
+
</Button>
|
|
159
|
+
<Button size="icon-sm" aria-label="Add">
|
|
160
|
+
<IconPlus />
|
|
161
|
+
</Button>
|
|
162
|
+
<Button size="icon" aria-label="Add">
|
|
163
|
+
<IconPlus />
|
|
164
|
+
</Button>
|
|
165
|
+
<Button size="icon-lg" aria-label="Add">
|
|
166
|
+
<IconPlus />
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
),
|
|
170
|
+
parameters: {
|
|
171
|
+
docs: {
|
|
172
|
+
description: {
|
|
173
|
+
story:
|
|
174
|
+
"Icon-only button sizes. Always include aria-label for accessibility.",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const Loading: Story = {
|
|
181
|
+
args: {
|
|
182
|
+
children: (
|
|
183
|
+
<>
|
|
184
|
+
<IconLoader2 className="animate-spin" data-icon="inline-start" />
|
|
185
|
+
Loading...
|
|
186
|
+
</>
|
|
187
|
+
),
|
|
188
|
+
disabled: true,
|
|
189
|
+
},
|
|
190
|
+
parameters: {
|
|
191
|
+
docs: {
|
|
192
|
+
description: {
|
|
193
|
+
story:
|
|
194
|
+
"Loading state with spinner. Disable the button to prevent double-clicks during async operations.",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const IconStart: Story = {
|
|
201
|
+
args: {
|
|
202
|
+
children: (
|
|
203
|
+
<>
|
|
204
|
+
<IconPlus data-icon="inline-start" />
|
|
205
|
+
Create
|
|
206
|
+
</>
|
|
207
|
+
),
|
|
208
|
+
},
|
|
209
|
+
parameters: {
|
|
210
|
+
docs: {
|
|
211
|
+
description: {
|
|
212
|
+
story:
|
|
213
|
+
'Button with icon at the start. Use data-icon="inline-start" for proper spacing.',
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const IconEnd: Story = {
|
|
220
|
+
args: {
|
|
221
|
+
children: (
|
|
222
|
+
<>
|
|
223
|
+
Options
|
|
224
|
+
<IconChevronDown data-icon="inline-end" />
|
|
225
|
+
</>
|
|
226
|
+
),
|
|
227
|
+
},
|
|
228
|
+
parameters: {
|
|
229
|
+
docs: {
|
|
230
|
+
description: {
|
|
231
|
+
story:
|
|
232
|
+
'Button with icon at the end. Use data-icon="inline-end" for proper spacing.',
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const IconOnly: Story = {
|
|
239
|
+
args: {
|
|
240
|
+
children: <IconTrash />,
|
|
241
|
+
size: "icon",
|
|
242
|
+
variant: "destructive",
|
|
243
|
+
"aria-label": "Delete",
|
|
244
|
+
},
|
|
245
|
+
parameters: {
|
|
246
|
+
docs: {
|
|
247
|
+
description: {
|
|
248
|
+
story:
|
|
249
|
+
"Icon-only button. MUST have aria-label for accessibility. Use icon size variants.",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export const Disabled: Story = {
|
|
256
|
+
args: { children: "Disabled", disabled: true },
|
|
257
|
+
parameters: {
|
|
258
|
+
docs: {
|
|
259
|
+
description: {
|
|
260
|
+
story: "Disabled state. Button is non-interactive and visually muted.",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
};
|