sonance-brand-mcp 1.1.4 → 1.2.2
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/assets/BRAND_GUIDELINES.md +0 -8
- package/dist/assets/components/accordion.stories.tsx +310 -0
- package/dist/assets/components/accordion.tsx +56 -30
- package/dist/assets/components/alert.stories.tsx +199 -0
- package/dist/assets/components/autocomplete.stories.tsx +307 -0
- package/dist/assets/components/autocomplete.tsx +28 -4
- package/dist/assets/components/avatar.stories.tsx +175 -0
- package/dist/assets/components/badge.stories.tsx +258 -0
- package/dist/assets/components/breadcrumbs.stories.tsx +175 -0
- package/dist/assets/components/button.stories.tsx +362 -0
- package/dist/assets/components/button.tsx +48 -3
- package/dist/assets/components/calendar.stories.tsx +247 -0
- package/dist/assets/components/card.stories.tsx +275 -0
- package/dist/assets/components/card.tsx +26 -1
- package/dist/assets/components/checkbox-group.stories.tsx +281 -0
- package/dist/assets/components/checkbox.stories.tsx +160 -0
- package/dist/assets/components/checkbox.tsx +32 -4
- package/dist/assets/components/code.stories.tsx +265 -0
- package/dist/assets/components/date-input.stories.tsx +278 -0
- package/dist/assets/components/date-input.tsx +24 -2
- package/dist/assets/components/date-picker.stories.tsx +337 -0
- package/dist/assets/components/date-picker.tsx +28 -4
- package/dist/assets/components/date-range-picker.stories.tsx +340 -0
- package/dist/assets/components/dialog.stories.tsx +285 -0
- package/dist/assets/components/divider.stories.tsx +176 -0
- package/dist/assets/components/drawer.stories.tsx +216 -0
- package/dist/assets/components/dropdown.stories.tsx +342 -0
- package/dist/assets/components/dropdown.tsx +55 -10
- package/dist/assets/components/form.stories.tsx +372 -0
- package/dist/assets/components/image.stories.tsx +348 -0
- package/dist/assets/components/input-otp.stories.tsx +336 -0
- package/dist/assets/components/input-otp.tsx +24 -2
- package/dist/assets/components/input.stories.tsx +223 -0
- package/dist/assets/components/input.tsx +27 -2
- package/dist/assets/components/kbd.stories.tsx +272 -0
- package/dist/assets/components/link.stories.tsx +199 -0
- package/dist/assets/components/link.tsx +50 -1
- package/dist/assets/components/listbox.stories.tsx +287 -0
- package/dist/assets/components/listbox.tsx +30 -7
- package/dist/assets/components/navbar.stories.tsx +218 -0
- package/dist/assets/components/number-input.stories.tsx +295 -0
- package/dist/assets/components/number-input.tsx +30 -8
- package/dist/assets/components/pagination.stories.tsx +280 -0
- package/dist/assets/components/pagination.tsx +45 -21
- package/dist/assets/components/popover.stories.tsx +219 -0
- package/dist/assets/components/progress.stories.tsx +153 -0
- package/dist/assets/components/radio-group.stories.tsx +187 -0
- package/dist/assets/components/radio-group.tsx +30 -6
- package/dist/assets/components/range-calendar.stories.tsx +334 -0
- package/dist/assets/components/scroll-shadow.stories.tsx +335 -0
- package/dist/assets/components/select.stories.tsx +192 -0
- package/dist/assets/components/select.tsx +54 -7
- package/dist/assets/components/skeleton.stories.tsx +166 -0
- package/dist/assets/components/slider.stories.tsx +145 -0
- package/dist/assets/components/slider.tsx +43 -8
- package/dist/assets/components/spacer.stories.tsx +216 -0
- package/dist/assets/components/spinner.stories.tsx +149 -0
- package/dist/assets/components/switch.stories.tsx +170 -0
- package/dist/assets/components/switch.tsx +29 -4
- package/dist/assets/components/table.stories.tsx +322 -0
- package/dist/assets/components/tabs.stories.tsx +306 -0
- package/dist/assets/components/tabs.tsx +25 -4
- package/dist/assets/components/textarea.stories.tsx +103 -0
- package/dist/assets/components/textarea.tsx +27 -3
- package/dist/assets/components/theme-toggle.stories.tsx +248 -0
- package/dist/assets/components/time-input.stories.tsx +365 -0
- package/dist/assets/components/time-input.tsx +25 -3
- package/dist/assets/components/toast.stories.tsx +195 -0
- package/dist/assets/components/tooltip.stories.tsx +226 -0
- package/dist/assets/components/user.stories.tsx +274 -0
- package/dist/assets/logo-manifest.json +0 -18
- package/dist/index.js +2142 -85
- package/package.json +1 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { ComponentProps } from 'react';
|
|
3
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent, type TabsTriggerState } from './tabs';
|
|
4
|
+
import { Card, CardContent } from './card';
|
|
5
|
+
|
|
6
|
+
type TabsStoryProps = ComponentProps<typeof Tabs> & {
|
|
7
|
+
state?: TabsTriggerState;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const meta: Meta<TabsStoryProps> = {
|
|
11
|
+
title: 'Components/Navigation/Tabs',
|
|
12
|
+
component: Tabs,
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component: 'A tabbed interface component for organizing content into selectable sections.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
argTypes: {
|
|
22
|
+
state: {
|
|
23
|
+
control: 'select',
|
|
24
|
+
options: ['default', 'hover', 'focus', 'active', 'disabled'],
|
|
25
|
+
description: 'Visual state for TabsTrigger documentation',
|
|
26
|
+
table: {
|
|
27
|
+
defaultValue: { summary: 'default' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<TabsStoryProps>;
|
|
35
|
+
|
|
36
|
+
// Default
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
render: () => (
|
|
39
|
+
<Tabs defaultValue="tab1" className="w-[400px]">
|
|
40
|
+
<TabsList>
|
|
41
|
+
<TabsTrigger value="tab1">Account</TabsTrigger>
|
|
42
|
+
<TabsTrigger value="tab2">Password</TabsTrigger>
|
|
43
|
+
<TabsTrigger value="tab3">Settings</TabsTrigger>
|
|
44
|
+
</TabsList>
|
|
45
|
+
<TabsContent value="tab1">
|
|
46
|
+
<p className="text-foreground-secondary">
|
|
47
|
+
Manage your account settings and preferences here.
|
|
48
|
+
</p>
|
|
49
|
+
</TabsContent>
|
|
50
|
+
<TabsContent value="tab2">
|
|
51
|
+
<p className="text-foreground-secondary">
|
|
52
|
+
Change your password and security settings.
|
|
53
|
+
</p>
|
|
54
|
+
</TabsContent>
|
|
55
|
+
<TabsContent value="tab3">
|
|
56
|
+
<p className="text-foreground-secondary">
|
|
57
|
+
Configure application settings and preferences.
|
|
58
|
+
</p>
|
|
59
|
+
</TabsContent>
|
|
60
|
+
</Tabs>
|
|
61
|
+
),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// With Cards
|
|
65
|
+
export const WithCards: Story = {
|
|
66
|
+
render: () => (
|
|
67
|
+
<Tabs defaultValue="overview" className="w-[500px]">
|
|
68
|
+
<TabsList>
|
|
69
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
70
|
+
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
|
71
|
+
<TabsTrigger value="reports">Reports</TabsTrigger>
|
|
72
|
+
</TabsList>
|
|
73
|
+
<TabsContent value="overview">
|
|
74
|
+
<Card>
|
|
75
|
+
<CardContent className="pt-6">
|
|
76
|
+
<h4 className="text-lg font-medium text-foreground mb-2">Overview</h4>
|
|
77
|
+
<p className="text-foreground-secondary">
|
|
78
|
+
Get a high-level view of your account activity and recent changes.
|
|
79
|
+
</p>
|
|
80
|
+
</CardContent>
|
|
81
|
+
</Card>
|
|
82
|
+
</TabsContent>
|
|
83
|
+
<TabsContent value="analytics">
|
|
84
|
+
<Card>
|
|
85
|
+
<CardContent className="pt-6">
|
|
86
|
+
<h4 className="text-lg font-medium text-foreground mb-2">Analytics</h4>
|
|
87
|
+
<p className="text-foreground-secondary">
|
|
88
|
+
View detailed analytics and performance metrics.
|
|
89
|
+
</p>
|
|
90
|
+
</CardContent>
|
|
91
|
+
</Card>
|
|
92
|
+
</TabsContent>
|
|
93
|
+
<TabsContent value="reports">
|
|
94
|
+
<Card>
|
|
95
|
+
<CardContent className="pt-6">
|
|
96
|
+
<h4 className="text-lg font-medium text-foreground mb-2">Reports</h4>
|
|
97
|
+
<p className="text-foreground-secondary">
|
|
98
|
+
Generate and download reports for your data.
|
|
99
|
+
</p>
|
|
100
|
+
</CardContent>
|
|
101
|
+
</Card>
|
|
102
|
+
</TabsContent>
|
|
103
|
+
</Tabs>
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// With Disabled Tab
|
|
108
|
+
export const WithDisabledTab: Story = {
|
|
109
|
+
render: () => (
|
|
110
|
+
<Tabs defaultValue="tab1" className="w-[400px]">
|
|
111
|
+
<TabsList>
|
|
112
|
+
<TabsTrigger value="tab1">Available</TabsTrigger>
|
|
113
|
+
<TabsTrigger value="tab2" disabled>
|
|
114
|
+
Coming Soon
|
|
115
|
+
</TabsTrigger>
|
|
116
|
+
<TabsTrigger value="tab3">Active</TabsTrigger>
|
|
117
|
+
</TabsList>
|
|
118
|
+
<TabsContent value="tab1">
|
|
119
|
+
<p className="text-foreground-secondary">This feature is available now.</p>
|
|
120
|
+
</TabsContent>
|
|
121
|
+
<TabsContent value="tab2">
|
|
122
|
+
<p className="text-foreground-secondary">This content is coming soon.</p>
|
|
123
|
+
</TabsContent>
|
|
124
|
+
<TabsContent value="tab3">
|
|
125
|
+
<p className="text-foreground-secondary">This feature is active.</p>
|
|
126
|
+
</TabsContent>
|
|
127
|
+
</Tabs>
|
|
128
|
+
),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Many Tabs
|
|
132
|
+
export const ManyTabs: Story = {
|
|
133
|
+
render: () => (
|
|
134
|
+
<Tabs defaultValue="home" className="w-[600px]">
|
|
135
|
+
<TabsList>
|
|
136
|
+
<TabsTrigger value="home">Home</TabsTrigger>
|
|
137
|
+
<TabsTrigger value="products">Products</TabsTrigger>
|
|
138
|
+
<TabsTrigger value="services">Services</TabsTrigger>
|
|
139
|
+
<TabsTrigger value="about">About</TabsTrigger>
|
|
140
|
+
<TabsTrigger value="contact">Contact</TabsTrigger>
|
|
141
|
+
</TabsList>
|
|
142
|
+
<TabsContent value="home">
|
|
143
|
+
<p className="text-foreground-secondary">Welcome to our home page.</p>
|
|
144
|
+
</TabsContent>
|
|
145
|
+
<TabsContent value="products">
|
|
146
|
+
<p className="text-foreground-secondary">Browse our product catalog.</p>
|
|
147
|
+
</TabsContent>
|
|
148
|
+
<TabsContent value="services">
|
|
149
|
+
<p className="text-foreground-secondary">Explore our services.</p>
|
|
150
|
+
</TabsContent>
|
|
151
|
+
<TabsContent value="about">
|
|
152
|
+
<p className="text-foreground-secondary">Learn more about us.</p>
|
|
153
|
+
</TabsContent>
|
|
154
|
+
<TabsContent value="contact">
|
|
155
|
+
<p className="text-foreground-secondary">Get in touch with us.</p>
|
|
156
|
+
</TabsContent>
|
|
157
|
+
</Tabs>
|
|
158
|
+
),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Settings Panel Example
|
|
162
|
+
export const SettingsPanel: Story = {
|
|
163
|
+
render: () => (
|
|
164
|
+
<Tabs defaultValue="profile" className="w-[500px]">
|
|
165
|
+
<TabsList>
|
|
166
|
+
<TabsTrigger value="profile">Profile</TabsTrigger>
|
|
167
|
+
<TabsTrigger value="notifications">Notifications</TabsTrigger>
|
|
168
|
+
<TabsTrigger value="security">Security</TabsTrigger>
|
|
169
|
+
</TabsList>
|
|
170
|
+
<TabsContent value="profile">
|
|
171
|
+
<div className="space-y-4">
|
|
172
|
+
<div>
|
|
173
|
+
<h4 className="font-medium text-foreground">Profile Information</h4>
|
|
174
|
+
<p className="text-sm text-foreground-secondary mt-1">
|
|
175
|
+
Update your personal information and profile picture.
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
178
|
+
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-divider">
|
|
179
|
+
<div>
|
|
180
|
+
<span className="text-xs text-foreground-muted">Name</span>
|
|
181
|
+
<p className="text-foreground">John Doe</p>
|
|
182
|
+
</div>
|
|
183
|
+
<div>
|
|
184
|
+
<span className="text-xs text-foreground-muted">Email</span>
|
|
185
|
+
<p className="text-foreground">john@example.com</p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</TabsContent>
|
|
190
|
+
<TabsContent value="notifications">
|
|
191
|
+
<div className="space-y-4">
|
|
192
|
+
<div>
|
|
193
|
+
<h4 className="font-medium text-foreground">Notification Preferences</h4>
|
|
194
|
+
<p className="text-sm text-foreground-secondary mt-1">
|
|
195
|
+
Choose how you want to be notified about updates.
|
|
196
|
+
</p>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</TabsContent>
|
|
200
|
+
<TabsContent value="security">
|
|
201
|
+
<div className="space-y-4">
|
|
202
|
+
<div>
|
|
203
|
+
<h4 className="font-medium text-foreground">Security Settings</h4>
|
|
204
|
+
<p className="text-sm text-foreground-secondary mt-1">
|
|
205
|
+
Manage your password and two-factor authentication.
|
|
206
|
+
</p>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</TabsContent>
|
|
210
|
+
</Tabs>
|
|
211
|
+
),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// State Matrix - Visual documentation of all TabsTrigger states
|
|
215
|
+
export const StateMatrix: Story = {
|
|
216
|
+
render: () => {
|
|
217
|
+
const states: TabsTriggerState[] = ['default', 'hover', 'focus', 'active', 'disabled'];
|
|
218
|
+
return (
|
|
219
|
+
<div className="space-y-6">
|
|
220
|
+
<h3 className="text-sm font-medium text-foreground-muted">TabsTrigger States</h3>
|
|
221
|
+
<div className="flex flex-wrap gap-4">
|
|
222
|
+
{states.map((state) => (
|
|
223
|
+
<div key={state} className="flex flex-col items-center gap-2">
|
|
224
|
+
<span className="text-xs font-medium text-foreground-muted uppercase">{state}</span>
|
|
225
|
+
<Tabs defaultValue="demo">
|
|
226
|
+
<TabsList>
|
|
227
|
+
<TabsTrigger value="demo" state={state}>
|
|
228
|
+
{state}
|
|
229
|
+
</TabsTrigger>
|
|
230
|
+
</TabsList>
|
|
231
|
+
</Tabs>
|
|
232
|
+
</div>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Responsive Matrix - Mobile, Tablet, Desktop
|
|
241
|
+
export const ResponsiveMatrix: Story = {
|
|
242
|
+
render: () => (
|
|
243
|
+
<div className="space-y-8">
|
|
244
|
+
{/* Mobile */}
|
|
245
|
+
<div>
|
|
246
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
|
|
247
|
+
<div className="w-[375px] border border-dashed border-border p-4">
|
|
248
|
+
<Tabs defaultValue="tab1" className="w-full">
|
|
249
|
+
<TabsList className="w-full">
|
|
250
|
+
<TabsTrigger value="tab1" className="flex-1">Tab 1</TabsTrigger>
|
|
251
|
+
<TabsTrigger value="tab2" className="flex-1">Tab 2</TabsTrigger>
|
|
252
|
+
</TabsList>
|
|
253
|
+
<TabsContent value="tab1">
|
|
254
|
+
<p className="text-foreground-secondary text-sm">Mobile tab content.</p>
|
|
255
|
+
</TabsContent>
|
|
256
|
+
<TabsContent value="tab2">
|
|
257
|
+
<p className="text-foreground-secondary text-sm">Second tab content.</p>
|
|
258
|
+
</TabsContent>
|
|
259
|
+
</Tabs>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
{/* Tablet */}
|
|
263
|
+
<div>
|
|
264
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
|
|
265
|
+
<div className="w-[768px] border border-dashed border-border p-4">
|
|
266
|
+
<Tabs defaultValue="overview" className="w-full">
|
|
267
|
+
<TabsList>
|
|
268
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
269
|
+
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
|
270
|
+
<TabsTrigger value="reports">Reports</TabsTrigger>
|
|
271
|
+
</TabsList>
|
|
272
|
+
<TabsContent value="overview">
|
|
273
|
+
<Card>
|
|
274
|
+
<CardContent className="pt-6">
|
|
275
|
+
<p className="text-foreground-secondary">Overview content for tablet view.</p>
|
|
276
|
+
</CardContent>
|
|
277
|
+
</Card>
|
|
278
|
+
</TabsContent>
|
|
279
|
+
</Tabs>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
{/* Desktop */}
|
|
283
|
+
<div>
|
|
284
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
|
|
285
|
+
<div className="w-[1280px] border border-dashed border-border p-4">
|
|
286
|
+
<Tabs defaultValue="home" className="w-full">
|
|
287
|
+
<TabsList>
|
|
288
|
+
<TabsTrigger value="home">Home</TabsTrigger>
|
|
289
|
+
<TabsTrigger value="products">Products</TabsTrigger>
|
|
290
|
+
<TabsTrigger value="services">Services</TabsTrigger>
|
|
291
|
+
<TabsTrigger value="about">About</TabsTrigger>
|
|
292
|
+
<TabsTrigger value="contact">Contact</TabsTrigger>
|
|
293
|
+
</TabsList>
|
|
294
|
+
<TabsContent value="home">
|
|
295
|
+
<Card>
|
|
296
|
+
<CardContent className="pt-6">
|
|
297
|
+
<p className="text-foreground-secondary">Full-width desktop tab content with multiple tabs available.</p>
|
|
298
|
+
</CardContent>
|
|
299
|
+
</Card>
|
|
300
|
+
</TabsContent>
|
|
301
|
+
</Tabs>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
),
|
|
306
|
+
};
|
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
import { createContext, useContext, useState } from "react";
|
|
4
4
|
import { cn } from "@/lib/utils";
|
|
5
5
|
|
|
6
|
+
export type TabsTriggerState = "default" | "hover" | "focus" | "active" | "disabled";
|
|
7
|
+
|
|
8
|
+
// State styles for Storybook/Figma visualization
|
|
9
|
+
const getStateStyles = (state?: TabsTriggerState, isActive?: boolean) => {
|
|
10
|
+
if (!state || state === "default") return "";
|
|
11
|
+
|
|
12
|
+
const stateMap: Record<string, string> = {
|
|
13
|
+
hover: "text-foreground",
|
|
14
|
+
focus: "ring-2 ring-border-focus",
|
|
15
|
+
active: "text-foreground",
|
|
16
|
+
disabled: "opacity-50 cursor-not-allowed",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
return stateMap[state] || "";
|
|
20
|
+
};
|
|
21
|
+
|
|
6
22
|
interface TabsContextValue {
|
|
7
23
|
value: string;
|
|
8
24
|
onChange: (value: string) => void;
|
|
@@ -64,6 +80,8 @@ interface TabsTriggerProps {
|
|
|
64
80
|
disabled?: boolean;
|
|
65
81
|
className?: string;
|
|
66
82
|
children: React.ReactNode;
|
|
83
|
+
/** Visual state for Storybook/Figma documentation */
|
|
84
|
+
state?: TabsTriggerState;
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
export function TabsTrigger({
|
|
@@ -71,26 +89,29 @@ export function TabsTrigger({
|
|
|
71
89
|
disabled = false,
|
|
72
90
|
className,
|
|
73
91
|
children,
|
|
92
|
+
state,
|
|
74
93
|
}: TabsTriggerProps) {
|
|
75
94
|
const context = useContext(TabsContext);
|
|
76
95
|
if (!context) throw new Error("TabsTrigger must be used within Tabs");
|
|
77
96
|
|
|
78
|
-
const isActive = context.value === value;
|
|
97
|
+
const isActive = context.value === value || state === "active";
|
|
98
|
+
const isDisabled = disabled || state === "disabled";
|
|
79
99
|
|
|
80
100
|
return (
|
|
81
101
|
<button
|
|
82
102
|
role="tab"
|
|
83
103
|
type="button"
|
|
84
104
|
aria-selected={isActive}
|
|
85
|
-
disabled={
|
|
86
|
-
onClick={() => !
|
|
105
|
+
disabled={isDisabled}
|
|
106
|
+
onClick={() => !isDisabled && context.onChange(value)}
|
|
87
107
|
className={cn(
|
|
88
108
|
"relative px-4 py-2.5 text-sm font-medium transition-colors",
|
|
89
109
|
"focus:outline-none focus-visible:ring-2 focus-visible:ring-border-focus",
|
|
90
110
|
isActive
|
|
91
111
|
? "text-foreground"
|
|
92
112
|
: "text-foreground-muted hover:text-foreground",
|
|
93
|
-
|
|
113
|
+
isDisabled && "cursor-not-allowed opacity-50",
|
|
114
|
+
getStateStyles(state, isActive),
|
|
94
115
|
className
|
|
95
116
|
)}
|
|
96
117
|
>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
|
|
2
|
+
import { Textarea } from './textarea';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Textarea> = {
|
|
5
|
+
title: 'Components/Forms/Textarea',
|
|
6
|
+
component: Textarea,
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: 'A multi-line text input field with optional label and error state.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
label: { control: 'text' },
|
|
17
|
+
error: { control: 'text' },
|
|
18
|
+
placeholder: { control: 'text' },
|
|
19
|
+
disabled: { control: 'boolean' },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
placeholder: 'Enter your message...',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const WithLabel: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
label: 'Message',
|
|
35
|
+
placeholder: 'Type your message here...',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const WithError: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
label: 'Description',
|
|
42
|
+
error: 'Description is required',
|
|
43
|
+
defaultValue: '',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Disabled: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
label: 'Notes',
|
|
50
|
+
placeholder: 'Cannot edit...',
|
|
51
|
+
disabled: true,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const AllStates: Story = {
|
|
56
|
+
render: () => (
|
|
57
|
+
<div className="space-y-6 w-96">
|
|
58
|
+
<Textarea placeholder="Default textarea" />
|
|
59
|
+
<Textarea label="With Label" placeholder="Enter text..." />
|
|
60
|
+
<Textarea label="With Error" error="This field is required" />
|
|
61
|
+
<Textarea label="Disabled" placeholder="Cannot edit..." disabled />
|
|
62
|
+
<Textarea label="With Content" defaultValue="This textarea has pre-filled content that can be edited by the user." />
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Responsive Matrix - Mobile, Tablet, Desktop
|
|
68
|
+
export const ResponsiveMatrix: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<div className="space-y-8">
|
|
71
|
+
{/* Mobile */}
|
|
72
|
+
<div>
|
|
73
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Mobile (375px)</h4>
|
|
74
|
+
<div className="w-[375px] border border-dashed border-border p-4 space-y-4">
|
|
75
|
+
<Textarea label="Message" placeholder="Enter your message..." />
|
|
76
|
+
<Textarea label="Feedback" placeholder="Share your thoughts..." rows={4} />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
{/* Tablet */}
|
|
80
|
+
<div>
|
|
81
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Tablet (768px)</h4>
|
|
82
|
+
<div className="w-[768px] border border-dashed border-border p-4">
|
|
83
|
+
<div className="grid grid-cols-2 gap-4">
|
|
84
|
+
<Textarea label="Description" placeholder="Enter description..." />
|
|
85
|
+
<Textarea label="Notes" placeholder="Additional notes..." />
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
{/* Desktop */}
|
|
90
|
+
<div>
|
|
91
|
+
<h4 className="text-xs uppercase text-foreground-muted mb-2">Desktop (1280px)</h4>
|
|
92
|
+
<div className="w-[1280px] border border-dashed border-border p-4">
|
|
93
|
+
<Textarea
|
|
94
|
+
label="Project Brief"
|
|
95
|
+
placeholder="Describe your project requirements..."
|
|
96
|
+
rows={6}
|
|
97
|
+
className="w-full"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
),
|
|
103
|
+
};
|
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { cn } from "@/lib/utils";
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
|
|
4
|
+
export type TextareaState = "default" | "hover" | "focus" | "error" | "disabled";
|
|
5
|
+
|
|
4
6
|
interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
5
7
|
label?: string;
|
|
6
8
|
error?: string;
|
|
9
|
+
/** Visual state for Storybook/Figma documentation */
|
|
10
|
+
state?: TextareaState;
|
|
7
11
|
}
|
|
8
12
|
|
|
13
|
+
// State styles for Storybook/Figma visualization
|
|
14
|
+
const getStateStyles = (state?: TextareaState) => {
|
|
15
|
+
if (!state || state === "default") return "";
|
|
16
|
+
|
|
17
|
+
const stateMap: Record<string, string> = {
|
|
18
|
+
hover: "border-border-hover",
|
|
19
|
+
focus: "border-input-focus",
|
|
20
|
+
error: "border-error",
|
|
21
|
+
disabled: "opacity-50 cursor-not-allowed bg-secondary",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return stateMap[state] || "";
|
|
25
|
+
};
|
|
26
|
+
|
|
9
27
|
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
10
|
-
({ className, label, error, ...props }, ref) => {
|
|
28
|
+
({ className, label, error, state, disabled, ...props }, ref) => {
|
|
29
|
+
const isDisabled = disabled || state === "disabled";
|
|
30
|
+
const hasError = error || state === "error";
|
|
31
|
+
|
|
11
32
|
return (
|
|
12
33
|
<div className="w-full">
|
|
13
34
|
{label && (
|
|
@@ -17,13 +38,16 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
17
38
|
)}
|
|
18
39
|
<textarea
|
|
19
40
|
ref={ref}
|
|
41
|
+
disabled={isDisabled}
|
|
20
42
|
className={cn(
|
|
21
43
|
"min-h-[120px] w-full resize-y border border-input-border bg-input px-4 py-3",
|
|
22
44
|
"text-foreground placeholder:text-input-placeholder",
|
|
23
45
|
"transition-colors duration-200",
|
|
46
|
+
"hover:border-border-hover",
|
|
24
47
|
"focus:border-input-focus focus:outline-none",
|
|
25
|
-
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
26
|
-
|
|
48
|
+
"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-secondary",
|
|
49
|
+
hasError && "border-error",
|
|
50
|
+
getStateStyles(state),
|
|
27
51
|
className
|
|
28
52
|
)}
|
|
29
53
|
{...props}
|