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.
Files changed (206) hide show
  1. package/dist/exports/api/index.d.mts +3 -3
  2. package/dist/exports/api/index.mjs +34 -9
  3. package/dist/exports/api/workflows/index.d.mts +2 -2
  4. package/dist/exports/api/workflows/index.mjs +2 -2
  5. package/dist/exports/{auth-By0xx0MI.d.mts → auth-BCOIpGDO.d.mts} +325 -214
  6. package/dist/exports/cli/index.d.mts +105 -106
  7. package/dist/exports/cli/index.mjs +87 -13
  8. package/dist/exports/{constants-BePPc_yF.mjs → constants-BicCnEiJ.mjs} +7 -1
  9. package/dist/exports/{index-Bpo5QE7k.d.mts → index-CiuTp085.d.mts} +580 -614
  10. package/dist/exports/{openapi-crG3j4xx.mjs → openapi-uisUTLq7.mjs} +45 -102
  11. package/dist/exports/{send-email-Bgcdjy-e.mjs → send-email-CXxlkfFL.mjs} +1 -1
  12. package/dist/exports/{server-BlNxgHUc.mjs → server-CA4aI0U6.mjs} +6 -2
  13. package/dist/exports/storybook/index.d.ts +40 -0
  14. package/dist/exports/storybook/index.js +43 -0
  15. package/dist/exports/tests/api.d.mts +2 -2
  16. package/dist/exports/tests/api.mjs +3 -3
  17. package/dist/exports/vite/index.d.mts +19 -3
  18. package/dist/exports/vite/index.mjs +82 -6
  19. package/dist/exports/web/index.d.ts +2247 -700
  20. package/dist/exports/web/index.js +2445 -754
  21. package/dist/exports/web/routes.d.mts +41 -43
  22. package/dist/exports/web/ssr.d.mts +2 -2
  23. package/package.json +48 -31
  24. package/src/storybook/.storybook/main.ts +119 -0
  25. package/src/storybook/.storybook/manager.ts +75 -0
  26. package/src/storybook/.storybook/preview.tsx +76 -0
  27. package/src/storybook/components/ColorGrid.tsx +324 -0
  28. package/src/storybook/components/IconGallery.tsx +268 -0
  29. package/src/storybook/components/SpacingScale.tsx +236 -0
  30. package/src/storybook/components/TypographyScale.tsx +277 -0
  31. package/src/storybook/components/index.ts +3 -0
  32. package/src/storybook/docs/Introduction.mdx +29 -0
  33. package/src/storybook/docs/foundations/Colors.mdx +27 -0
  34. package/src/storybook/docs/foundations/Icons.mdx +26 -0
  35. package/src/storybook/docs/foundations/Spacing.mdx +23 -0
  36. package/src/storybook/docs/foundations/Typography.mdx +39 -0
  37. package/src/storybook/index.ts +147 -0
  38. package/src/storybook/shims/void-elements.js +18 -0
  39. package/src/web/client/auth-paths.ts +148 -0
  40. package/src/web/client/auth.ts +218 -0
  41. package/src/web/client/i18n.d.ts +9 -0
  42. package/src/web/client/i18n.ts +127 -0
  43. package/src/web/client/react-router.ts +84 -0
  44. package/src/web/hooks/index.ts +1 -0
  45. package/src/web/hooks/use-mobile.ts +19 -0
  46. package/src/web/index.ts +42 -0
  47. package/src/web/lib/colors.ts +220 -0
  48. package/src/web/lib/utils.ts +52 -0
  49. package/src/web/routes/auth/_layout.tsx +5 -11
  50. package/src/web/routes.ts +101 -0
  51. package/src/web/server/context.ts +14 -0
  52. package/src/web/server/render.ts +11 -0
  53. package/src/web/ssr.ts +17 -0
  54. package/src/web/ui/accordion.stories.tsx +183 -0
  55. package/src/web/ui/accordion.tsx +134 -0
  56. package/src/web/ui/alert-dialog.stories.tsx +215 -0
  57. package/src/web/ui/alert-dialog.tsx +328 -0
  58. package/src/web/ui/alert.stories.tsx +224 -0
  59. package/src/web/ui/alert.tsx +136 -0
  60. package/src/web/ui/animated-check.stories.tsx +200 -0
  61. package/src/web/ui/animated-check.tsx +98 -0
  62. package/src/web/ui/aspect-ratio.stories.tsx +204 -0
  63. package/src/web/ui/aspect-ratio.tsx +36 -0
  64. package/src/web/ui/auth/__mocks__/auth-context.tsx +273 -0
  65. package/src/web/ui/auth/auth-card.stories.tsx +295 -0
  66. package/src/web/ui/auth/auth-card.tsx +124 -0
  67. package/src/web/ui/auth/auth-layout.stories.tsx +330 -0
  68. package/src/web/ui/auth/auth-layout.tsx +269 -0
  69. package/src/web/ui/auth/captcha-widget.stories.tsx +200 -0
  70. package/src/web/ui/auth/captcha-widget.tsx +197 -0
  71. package/src/web/ui/auth/context.tsx +87 -0
  72. package/src/web/ui/auth/email-input.stories.tsx +167 -0
  73. package/src/web/ui/auth/email-input.tsx +84 -0
  74. package/src/web/ui/auth/forms/email-otp-form.tsx +377 -0
  75. package/src/web/ui/auth/forms/forgot-password-form.tsx +240 -0
  76. package/src/web/ui/auth/forms/index.ts +40 -0
  77. package/src/web/ui/auth/forms/login-form.tsx +486 -0
  78. package/src/web/ui/auth/forms/phone-otp-form.tsx +388 -0
  79. package/src/web/ui/auth/forms/reset-password-form.tsx +331 -0
  80. package/src/web/ui/auth/forms/signup-form.tsx +499 -0
  81. package/src/web/ui/auth/forms/two-factor-form.tsx +410 -0
  82. package/src/web/ui/auth/forms/verify-email-form.tsx +373 -0
  83. package/src/web/ui/auth/hooks.ts +266 -0
  84. package/src/web/ui/auth/index.ts +31 -0
  85. package/src/web/ui/auth/magic-link-form.tsx +191 -0
  86. package/src/web/ui/auth/otp-input.stories.tsx +199 -0
  87. package/src/web/ui/auth/otp-input.tsx +157 -0
  88. package/src/web/ui/auth/passkey-button.stories.tsx +115 -0
  89. package/src/web/ui/auth/passkey-button.tsx +113 -0
  90. package/src/web/ui/auth/passkey-register-button.stories.tsx +109 -0
  91. package/src/web/ui/auth/passkey-register-button.tsx +106 -0
  92. package/src/web/ui/auth/password-input.stories.tsx +287 -0
  93. package/src/web/ui/auth/password-input.tsx +306 -0
  94. package/src/web/ui/auth/phone-input.stories.tsx +160 -0
  95. package/src/web/ui/auth/phone-input.tsx +89 -0
  96. package/src/web/ui/auth/social-buttons.stories.tsx +217 -0
  97. package/src/web/ui/auth/social-buttons.tsx +196 -0
  98. package/src/web/ui/auth/sso-button.stories.tsx +160 -0
  99. package/src/web/ui/auth/sso-button.tsx +108 -0
  100. package/src/web/ui/auth/trust-signals.stories.tsx +208 -0
  101. package/src/web/ui/auth/trust-signals.tsx +260 -0
  102. package/src/web/ui/avatar.stories.tsx +302 -0
  103. package/src/web/ui/avatar.tsx +189 -0
  104. package/src/web/ui/badge.stories.tsx +225 -0
  105. package/src/web/ui/badge.tsx +66 -0
  106. package/src/web/ui/brand.stories.tsx +266 -0
  107. package/src/web/ui/brand.tsx +162 -0
  108. package/src/web/ui/breadcrumb.stories.tsx +271 -0
  109. package/src/web/ui/breadcrumb.tsx +214 -0
  110. package/src/web/ui/button-group.stories.tsx +251 -0
  111. package/src/web/ui/button-group.tsx +87 -0
  112. package/src/web/ui/button.stories.tsx +264 -0
  113. package/src/web/ui/button.tsx +120 -0
  114. package/src/web/ui/calendar.stories.tsx +235 -0
  115. package/src/web/ui/calendar.tsx +221 -0
  116. package/src/web/ui/card.stories.tsx +262 -0
  117. package/src/web/ui/card.tsx +199 -0
  118. package/src/web/ui/carousel.stories.tsx +244 -0
  119. package/src/web/ui/carousel.tsx +241 -0
  120. package/src/web/ui/chart.stories.tsx +833 -0
  121. package/src/web/ui/chart.tsx +390 -0
  122. package/src/web/ui/checkbox.stories.tsx +208 -0
  123. package/src/web/ui/checkbox.tsx +39 -0
  124. package/src/web/ui/collapsible.stories.tsx +239 -0
  125. package/src/web/ui/collapsible.tsx +21 -0
  126. package/src/web/ui/command.stories.tsx +291 -0
  127. package/src/web/ui/command.tsx +189 -0
  128. package/src/web/ui/context-menu.stories.tsx +255 -0
  129. package/src/web/ui/context-menu.tsx +263 -0
  130. package/src/web/ui/dialog.stories.tsx +263 -0
  131. package/src/web/ui/dialog.tsx +273 -0
  132. package/src/web/ui/drawer.stories.tsx +299 -0
  133. package/src/web/ui/drawer.tsx +130 -0
  134. package/src/web/ui/dropdown-menu.stories.tsx +320 -0
  135. package/src/web/ui/dropdown-menu.tsx +263 -0
  136. package/src/web/ui/empty.stories.tsx +204 -0
  137. package/src/web/ui/empty.tsx +101 -0
  138. package/src/web/ui/error-page.stories.tsx +181 -0
  139. package/src/web/ui/error-page.tsx +136 -0
  140. package/src/web/ui/field.stories.tsx +364 -0
  141. package/src/web/ui/field.tsx +252 -0
  142. package/src/web/ui/globals.css +3 -0
  143. package/src/web/ui/hover-card.stories.tsx +261 -0
  144. package/src/web/ui/hover-card.tsx +50 -0
  145. package/src/web/ui/index.ts +55 -0
  146. package/src/web/ui/input-group.stories.tsx +290 -0
  147. package/src/web/ui/input-group.tsx +157 -0
  148. package/src/web/ui/input-otp.stories.tsx +235 -0
  149. package/src/web/ui/input-otp.tsx +86 -0
  150. package/src/web/ui/input.stories.tsx +205 -0
  151. package/src/web/ui/input.tsx +30 -0
  152. package/src/web/ui/item.stories.tsx +348 -0
  153. package/src/web/ui/item.tsx +200 -0
  154. package/src/web/ui/kbd.stories.tsx +236 -0
  155. package/src/web/ui/kbd.tsx +27 -0
  156. package/src/web/ui/label.stories.tsx +180 -0
  157. package/src/web/ui/label.tsx +32 -0
  158. package/src/web/ui/locales/en/ui.json +13 -30
  159. package/src/web/ui/locales/zh-CN/ui.json +17 -34
  160. package/src/web/ui/locales/zh-TW/ui.json +17 -34
  161. package/src/web/ui/menubar.stories.tsx +385 -0
  162. package/src/web/ui/menubar.tsx +273 -0
  163. package/src/web/ui/navigation-menu.stories.tsx +361 -0
  164. package/src/web/ui/navigation-menu.tsx +168 -0
  165. package/src/web/ui/pagination.stories.tsx +312 -0
  166. package/src/web/ui/pagination.tsx +221 -0
  167. package/src/web/ui/popover.stories.tsx +315 -0
  168. package/src/web/ui/popover.tsx +89 -0
  169. package/src/web/ui/progress.stories.tsx +239 -0
  170. package/src/web/ui/progress.tsx +128 -0
  171. package/src/web/ui/radio-group.stories.tsx +315 -0
  172. package/src/web/ui/radio-group.tsx +38 -0
  173. package/src/web/ui/resizable.stories.tsx +304 -0
  174. package/src/web/ui/resizable.tsx +53 -0
  175. package/src/web/ui/scroll-area.stories.tsx +276 -0
  176. package/src/web/ui/scroll-area.tsx +53 -0
  177. package/src/web/ui/select.stories.tsx +314 -0
  178. package/src/web/ui/select.tsx +312 -0
  179. package/src/web/ui/separator.stories.tsx +246 -0
  180. package/src/web/ui/separator.tsx +36 -0
  181. package/src/web/ui/sheet.stories.tsx +417 -0
  182. package/src/web/ui/sheet.tsx +133 -0
  183. package/src/web/ui/sidebar.stories.tsx +402 -0
  184. package/src/web/ui/sidebar.tsx +723 -0
  185. package/src/web/ui/skeleton.stories.tsx +235 -0
  186. package/src/web/ui/skeleton.tsx +26 -0
  187. package/src/web/ui/slider.stories.tsx +294 -0
  188. package/src/web/ui/slider.tsx +75 -0
  189. package/src/web/ui/sonner.stories.tsx +279 -0
  190. package/src/web/ui/sonner.tsx +46 -0
  191. package/src/web/ui/spinner.stories.tsx +204 -0
  192. package/src/web/ui/spinner.tsx +28 -0
  193. package/src/web/ui/switch.stories.tsx +285 -0
  194. package/src/web/ui/switch.tsx +43 -0
  195. package/src/web/ui/table.stories.tsx +362 -0
  196. package/src/web/ui/table.tsx +115 -0
  197. package/src/web/ui/tabs.stories.tsx +401 -0
  198. package/src/web/ui/tabs.tsx +136 -0
  199. package/src/web/ui/textarea.stories.tsx +272 -0
  200. package/src/web/ui/textarea.tsx +28 -0
  201. package/src/web/ui/toggle-group.stories.tsx +334 -0
  202. package/src/web/ui/toggle-group.tsx +88 -0
  203. package/src/web/ui/toggle.stories.tsx +264 -0
  204. package/src/web/ui/toggle.tsx +57 -0
  205. package/src/web/ui/tooltip.stories.tsx +342 -0
  206. package/src/web/ui/tooltip.tsx +116 -0
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+ import { cn } from "#src/web/lib/utils.ts";
5
+
6
+ function Table({ className, ...props }: React.ComponentProps<"table">) {
7
+ return (
8
+ <div
9
+ data-slot="table-container"
10
+ className="relative w-full overflow-x-auto"
11
+ >
12
+ <table
13
+ data-slot="table"
14
+ className={cn("w-full caption-bottom text-sm", className)}
15
+ {...props}
16
+ />
17
+ </div>
18
+ );
19
+ }
20
+
21
+ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
22
+ return (
23
+ <thead
24
+ data-slot="table-header"
25
+ className={cn("[&_tr]:border-b", className)}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
32
+ return (
33
+ <tbody
34
+ data-slot="table-body"
35
+ className={cn("[&_tr:last-child]:border-0", className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
42
+ return (
43
+ <tfoot
44
+ data-slot="table-footer"
45
+ className={cn(
46
+ "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
47
+ className,
48
+ )}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
55
+ return (
56
+ <tr
57
+ data-slot="table-row"
58
+ className={cn(
59
+ "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
60
+ className,
61
+ )}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
68
+ return (
69
+ <th
70
+ data-slot="table-head"
71
+ className={cn(
72
+ "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0",
73
+ className,
74
+ )}
75
+ {...props}
76
+ />
77
+ );
78
+ }
79
+
80
+ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
81
+ return (
82
+ <td
83
+ data-slot="table-cell"
84
+ className={cn(
85
+ "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
86
+ className,
87
+ )}
88
+ {...props}
89
+ />
90
+ );
91
+ }
92
+
93
+ function TableCaption({
94
+ className,
95
+ ...props
96
+ }: React.ComponentProps<"caption">) {
97
+ return (
98
+ <caption
99
+ data-slot="table-caption"
100
+ className={cn("text-muted-foreground mt-4 text-sm", className)}
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ export {
107
+ Table,
108
+ TableHeader,
109
+ TableBody,
110
+ TableFooter,
111
+ TableHead,
112
+ TableRow,
113
+ TableCell,
114
+ TableCaption,
115
+ };
@@ -0,0 +1,401 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import {
3
+ Button,
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ Input,
10
+ Label,
11
+ Tabs,
12
+ TabsContent,
13
+ TabsList,
14
+ TabsTrigger,
15
+ } from "appos/web";
16
+ import { BellIcon, CreditCardIcon, SettingsIcon, UserIcon } from "lucide-react";
17
+ import { expect, userEvent, within } from "storybook/test";
18
+
19
+ const meta = {
20
+ title: "Base/Tabs",
21
+ component: Tabs,
22
+ tags: ["autodocs"],
23
+ parameters: {
24
+ layout: "centered",
25
+ docs: {
26
+ description: {
27
+ component:
28
+ "A tabs component for organizing content into switchable panels. Supports horizontal and vertical orientations with default and line variants.",
29
+ },
30
+ },
31
+ },
32
+ } satisfies Meta<typeof Tabs>;
33
+
34
+ export default meta;
35
+ type Story = StoryObj<typeof meta>;
36
+
37
+ export const Default: Story = {
38
+ render: () => (
39
+ <Tabs defaultValue="account" className="w-100">
40
+ <TabsList>
41
+ <TabsTrigger value="account">Account</TabsTrigger>
42
+ <TabsTrigger value="password">Password</TabsTrigger>
43
+ </TabsList>
44
+ <TabsContent value="account">
45
+ <Card>
46
+ <CardHeader>
47
+ <CardTitle>Account</CardTitle>
48
+ <CardDescription>
49
+ Make changes to your account here.
50
+ </CardDescription>
51
+ </CardHeader>
52
+ <CardContent className="space-y-2">
53
+ <div className="space-y-1">
54
+ <Label htmlFor="name">Name</Label>
55
+ <Input id="name" defaultValue="John Doe" />
56
+ </div>
57
+ <div className="space-y-1">
58
+ <Label htmlFor="username">Username</Label>
59
+ <Input id="username" defaultValue="@johndoe" />
60
+ </div>
61
+ </CardContent>
62
+ </Card>
63
+ </TabsContent>
64
+ <TabsContent value="password">
65
+ <Card>
66
+ <CardHeader>
67
+ <CardTitle>Password</CardTitle>
68
+ <CardDescription>Change your password here.</CardDescription>
69
+ </CardHeader>
70
+ <CardContent className="space-y-2">
71
+ <div className="space-y-1">
72
+ <Label htmlFor="current">Current password</Label>
73
+ <Input id="current" type="password" />
74
+ </div>
75
+ <div className="space-y-1">
76
+ <Label htmlFor="new">New password</Label>
77
+ <Input id="new" type="password" />
78
+ </div>
79
+ </CardContent>
80
+ </Card>
81
+ </TabsContent>
82
+ </Tabs>
83
+ ),
84
+ parameters: {
85
+ docs: {
86
+ description: {
87
+ story: "Default tabs with card content.",
88
+ },
89
+ },
90
+ },
91
+ play: async ({ canvasElement }) => {
92
+ const canvas = within(canvasElement);
93
+ const passwordTab = canvas.getByRole("tab", { name: /password/i });
94
+ await userEvent.click(passwordTab);
95
+ await expect(canvas.getByText("Current password")).toBeInTheDocument();
96
+ },
97
+ };
98
+
99
+ export const LineVariant: Story = {
100
+ render: () => (
101
+ <Tabs defaultValue="overview" className="w-100">
102
+ <TabsList variant="line">
103
+ <TabsTrigger value="overview">Overview</TabsTrigger>
104
+ <TabsTrigger value="analytics">Analytics</TabsTrigger>
105
+ <TabsTrigger value="reports">Reports</TabsTrigger>
106
+ </TabsList>
107
+ <TabsContent value="overview" className="pt-4">
108
+ <p className="text-sm text-muted-foreground">
109
+ Overview content goes here.
110
+ </p>
111
+ </TabsContent>
112
+ <TabsContent value="analytics" className="pt-4">
113
+ <p className="text-sm text-muted-foreground">
114
+ Analytics content goes here.
115
+ </p>
116
+ </TabsContent>
117
+ <TabsContent value="reports" className="pt-4">
118
+ <p className="text-sm text-muted-foreground">
119
+ Reports content goes here.
120
+ </p>
121
+ </TabsContent>
122
+ </Tabs>
123
+ ),
124
+ parameters: {
125
+ docs: {
126
+ description: {
127
+ story: "Tabs with line variant styling.",
128
+ },
129
+ },
130
+ },
131
+ };
132
+
133
+ export const WithIcons: Story = {
134
+ render: () => (
135
+ <Tabs defaultValue="profile" className="w-100">
136
+ <TabsList>
137
+ <TabsTrigger value="profile">
138
+ <UserIcon />
139
+ Profile
140
+ </TabsTrigger>
141
+ <TabsTrigger value="notifications">
142
+ <BellIcon />
143
+ Notifications
144
+ </TabsTrigger>
145
+ <TabsTrigger value="settings">
146
+ <SettingsIcon />
147
+ Settings
148
+ </TabsTrigger>
149
+ </TabsList>
150
+ <TabsContent value="profile" className="pt-4">
151
+ <p className="text-sm text-muted-foreground">
152
+ Manage your profile settings.
153
+ </p>
154
+ </TabsContent>
155
+ <TabsContent value="notifications" className="pt-4">
156
+ <p className="text-sm text-muted-foreground">
157
+ Configure notification preferences.
158
+ </p>
159
+ </TabsContent>
160
+ <TabsContent value="settings" className="pt-4">
161
+ <p className="text-sm text-muted-foreground">
162
+ Adjust general settings.
163
+ </p>
164
+ </TabsContent>
165
+ </Tabs>
166
+ ),
167
+ parameters: {
168
+ docs: {
169
+ description: {
170
+ story: "Tabs with icons in triggers.",
171
+ },
172
+ },
173
+ },
174
+ };
175
+
176
+ export const Vertical: Story = {
177
+ render: () => (
178
+ <Tabs defaultValue="general" orientation="vertical" className="w-125">
179
+ <TabsList className="h-fit">
180
+ <TabsTrigger value="general">General</TabsTrigger>
181
+ <TabsTrigger value="security">Security</TabsTrigger>
182
+ <TabsTrigger value="billing">Billing</TabsTrigger>
183
+ <TabsTrigger value="team">Team</TabsTrigger>
184
+ </TabsList>
185
+ <TabsContent value="general" className="flex-1">
186
+ <Card>
187
+ <CardHeader>
188
+ <CardTitle>General Settings</CardTitle>
189
+ <CardDescription>Manage your general settings.</CardDescription>
190
+ </CardHeader>
191
+ <CardContent>
192
+ <div className="space-y-2">
193
+ <Label htmlFor="org-name">Organization Name</Label>
194
+ <Input id="org-name" defaultValue="Acme Inc" />
195
+ </div>
196
+ </CardContent>
197
+ </Card>
198
+ </TabsContent>
199
+ <TabsContent value="security" className="flex-1">
200
+ <Card>
201
+ <CardHeader>
202
+ <CardTitle>Security</CardTitle>
203
+ <CardDescription>Configure security options.</CardDescription>
204
+ </CardHeader>
205
+ </Card>
206
+ </TabsContent>
207
+ <TabsContent value="billing" className="flex-1">
208
+ <Card>
209
+ <CardHeader>
210
+ <CardTitle>Billing</CardTitle>
211
+ <CardDescription>Manage billing and payments.</CardDescription>
212
+ </CardHeader>
213
+ </Card>
214
+ </TabsContent>
215
+ <TabsContent value="team" className="flex-1">
216
+ <Card>
217
+ <CardHeader>
218
+ <CardTitle>Team</CardTitle>
219
+ <CardDescription>Manage team members.</CardDescription>
220
+ </CardHeader>
221
+ </Card>
222
+ </TabsContent>
223
+ </Tabs>
224
+ ),
225
+ parameters: {
226
+ docs: {
227
+ description: {
228
+ story: "Vertical tabs for settings pages.",
229
+ },
230
+ },
231
+ },
232
+ };
233
+
234
+ export const VerticalLine: Story = {
235
+ render: () => (
236
+ <Tabs defaultValue="account" orientation="vertical" className="w-125">
237
+ <TabsList variant="line" className="h-fit">
238
+ <TabsTrigger value="account">
239
+ <UserIcon />
240
+ Account
241
+ </TabsTrigger>
242
+ <TabsTrigger value="billing">
243
+ <CreditCardIcon />
244
+ Billing
245
+ </TabsTrigger>
246
+ <TabsTrigger value="notifications">
247
+ <BellIcon />
248
+ Notifications
249
+ </TabsTrigger>
250
+ <TabsTrigger value="settings">
251
+ <SettingsIcon />
252
+ Settings
253
+ </TabsTrigger>
254
+ </TabsList>
255
+ <TabsContent value="account" className="flex-1 pl-4">
256
+ <h3 className="text-lg font-medium">Account</h3>
257
+ <p className="text-sm text-muted-foreground mt-2">
258
+ Update your account information.
259
+ </p>
260
+ </TabsContent>
261
+ <TabsContent value="billing" className="flex-1 pl-4">
262
+ <h3 className="text-lg font-medium">Billing</h3>
263
+ <p className="text-sm text-muted-foreground mt-2">
264
+ Manage your billing and subscription.
265
+ </p>
266
+ </TabsContent>
267
+ <TabsContent value="notifications" className="flex-1 pl-4">
268
+ <h3 className="text-lg font-medium">Notifications</h3>
269
+ <p className="text-sm text-muted-foreground mt-2">
270
+ Configure your notification preferences.
271
+ </p>
272
+ </TabsContent>
273
+ <TabsContent value="settings" className="flex-1 pl-4">
274
+ <h3 className="text-lg font-medium">Settings</h3>
275
+ <p className="text-sm text-muted-foreground mt-2">
276
+ Adjust your general settings.
277
+ </p>
278
+ </TabsContent>
279
+ </Tabs>
280
+ ),
281
+ parameters: {
282
+ docs: {
283
+ description: {
284
+ story: "Vertical tabs with line variant and icons.",
285
+ },
286
+ },
287
+ },
288
+ };
289
+
290
+ export const Disabled: Story = {
291
+ render: () => (
292
+ <Tabs defaultValue="active" className="w-100">
293
+ <TabsList>
294
+ <TabsTrigger value="active">Active</TabsTrigger>
295
+ <TabsTrigger value="disabled" disabled>
296
+ Disabled
297
+ </TabsTrigger>
298
+ <TabsTrigger value="other">Other</TabsTrigger>
299
+ </TabsList>
300
+ <TabsContent value="active" className="pt-4">
301
+ <p className="text-sm text-muted-foreground">Active tab content.</p>
302
+ </TabsContent>
303
+ <TabsContent value="other" className="pt-4">
304
+ <p className="text-sm text-muted-foreground">Other tab content.</p>
305
+ </TabsContent>
306
+ </Tabs>
307
+ ),
308
+ parameters: {
309
+ docs: {
310
+ description: {
311
+ story: "Tabs with a disabled trigger.",
312
+ },
313
+ },
314
+ },
315
+ };
316
+
317
+ export const FullWidth: Story = {
318
+ render: () => (
319
+ <Tabs defaultValue="tab1" className="w-100">
320
+ <TabsList className="w-full">
321
+ <TabsTrigger value="tab1" className="flex-1">
322
+ Tab 1
323
+ </TabsTrigger>
324
+ <TabsTrigger value="tab2" className="flex-1">
325
+ Tab 2
326
+ </TabsTrigger>
327
+ <TabsTrigger value="tab3" className="flex-1">
328
+ Tab 3
329
+ </TabsTrigger>
330
+ </TabsList>
331
+ <TabsContent value="tab1" className="pt-4">
332
+ Content for Tab 1
333
+ </TabsContent>
334
+ <TabsContent value="tab2" className="pt-4">
335
+ Content for Tab 2
336
+ </TabsContent>
337
+ <TabsContent value="tab3" className="pt-4">
338
+ Content for Tab 3
339
+ </TabsContent>
340
+ </Tabs>
341
+ ),
342
+ parameters: {
343
+ docs: {
344
+ description: {
345
+ story: "Full-width tabs with equal tab widths.",
346
+ },
347
+ },
348
+ },
349
+ };
350
+
351
+ export const ProductTabs: Story = {
352
+ render: () => (
353
+ <Tabs defaultValue="description" className="w-125">
354
+ <TabsList variant="line">
355
+ <TabsTrigger value="description">Description</TabsTrigger>
356
+ <TabsTrigger value="specifications">Specifications</TabsTrigger>
357
+ <TabsTrigger value="reviews">Reviews (24)</TabsTrigger>
358
+ </TabsList>
359
+ <TabsContent value="description" className="pt-4">
360
+ <p className="text-sm text-muted-foreground">
361
+ This is a premium product with exceptional quality and craftsmanship.
362
+ Made from sustainable materials and designed to last a lifetime.
363
+ </p>
364
+ </TabsContent>
365
+ <TabsContent value="specifications" className="pt-4">
366
+ <ul className="text-sm space-y-2">
367
+ <li className="flex justify-between">
368
+ <span className="text-muted-foreground">Dimensions</span>
369
+ <span>10 x 5 x 2 inches</span>
370
+ </li>
371
+ <li className="flex justify-between">
372
+ <span className="text-muted-foreground">Weight</span>
373
+ <span>1.5 lbs</span>
374
+ </li>
375
+ <li className="flex justify-between">
376
+ <span className="text-muted-foreground">Material</span>
377
+ <span>Recycled aluminum</span>
378
+ </li>
379
+ </ul>
380
+ </TabsContent>
381
+ <TabsContent value="reviews" className="pt-4">
382
+ <div className="space-y-4">
383
+ <div className="flex items-center gap-2">
384
+ <span className="text-2xl font-bold">4.8</span>
385
+ <span className="text-sm text-muted-foreground">out of 5</span>
386
+ </div>
387
+ <Button variant="outline" size="sm">
388
+ Write a Review
389
+ </Button>
390
+ </div>
391
+ </TabsContent>
392
+ </Tabs>
393
+ ),
394
+ parameters: {
395
+ docs: {
396
+ description: {
397
+ story: "Product detail tabs with line variant.",
398
+ },
399
+ },
400
+ },
401
+ };
@@ -0,0 +1,136 @@
1
+ "use client";
2
+
3
+ import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "#src/web/lib/utils.ts";
6
+
7
+ /**
8
+ * A set of layered content panels displayed one at a time.
9
+ *
10
+ * Organizes content into distinct views that users can switch between.
11
+ * Supports horizontal and vertical orientations.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <Tabs defaultValue="account">
16
+ * <TabsList>
17
+ * <TabsTrigger value="account">Account</TabsTrigger>
18
+ * <TabsTrigger value="settings">Settings</TabsTrigger>
19
+ * </TabsList>
20
+ * <TabsContent value="account">Account content</TabsContent>
21
+ * <TabsContent value="settings">Settings content</TabsContent>
22
+ * </Tabs>
23
+ * ```
24
+ */
25
+ function Tabs({
26
+ className,
27
+ orientation = "horizontal",
28
+ ...props
29
+ }: TabsPrimitive.Root.Props) {
30
+ return (
31
+ <TabsPrimitive.Root
32
+ data-slot="tabs"
33
+ data-orientation={orientation}
34
+ className={cn(
35
+ "gap-2 group/tabs flex data-[orientation=horizontal]:flex-col",
36
+ className,
37
+ )}
38
+ {...props}
39
+ />
40
+ );
41
+ }
42
+
43
+ /** TabsList variant styles for different visual appearances. */
44
+ const tabsListVariants = cva(
45
+ "rounded-lg p-[3px] group-data-horizontal/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
46
+ {
47
+ variants: {
48
+ variant: {
49
+ default: "bg-muted",
50
+ line: "gap-1 bg-transparent",
51
+ },
52
+ },
53
+ defaultVariants: {
54
+ variant: "default",
55
+ },
56
+ },
57
+ );
58
+
59
+ /**
60
+ * The container for tab triggers.
61
+ *
62
+ * Supports default (pill-style) and line variants. Arranges triggers
63
+ * horizontally or vertically based on parent orientation.
64
+ *
65
+ * @example
66
+ * ```tsx
67
+ * <TabsList variant="line">
68
+ * <TabsTrigger value="tab1">Tab 1</TabsTrigger>
69
+ * </TabsList>
70
+ * ```
71
+ */
72
+ function TabsList({
73
+ className,
74
+ variant = "default",
75
+ ...props
76
+ }: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) {
77
+ return (
78
+ <TabsPrimitive.List
79
+ data-slot="tabs-list"
80
+ data-variant={variant}
81
+ className={cn(tabsListVariants({ variant }), className)}
82
+ {...props}
83
+ />
84
+ );
85
+ }
86
+
87
+ /**
88
+ * A button that activates its associated tab content panel.
89
+ *
90
+ * Displays active state styling and supports keyboard navigation.
91
+ *
92
+ * @example
93
+ * ```tsx
94
+ * <TabsTrigger value="settings">Settings</TabsTrigger>
95
+ * ```
96
+ */
97
+ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
98
+ return (
99
+ <TabsPrimitive.Tab
100
+ data-slot="tabs-trigger"
101
+ className={cn(
102
+ "gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
103
+ "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
104
+ "data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground",
105
+ "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
106
+ className,
107
+ )}
108
+ {...props}
109
+ />
110
+ );
111
+ }
112
+
113
+ /**
114
+ * The content panel associated with a tab trigger.
115
+ *
116
+ * Only visible when its corresponding trigger is active. Hidden panels
117
+ * are removed from the accessibility tree.
118
+ *
119
+ * @example
120
+ * ```tsx
121
+ * <TabsContent value="settings">
122
+ * <p>Settings panel content.</p>
123
+ * </TabsContent>
124
+ * ```
125
+ */
126
+ function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) {
127
+ return (
128
+ <TabsPrimitive.Panel
129
+ data-slot="tabs-content"
130
+ className={cn("text-sm flex-1 outline-none", className)}
131
+ {...props}
132
+ />
133
+ );
134
+ }
135
+
136
+ export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };