@regardio/react 0.5.5 → 0.6.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/background-slideshow/index.d.mts +36 -0
- package/dist/background-slideshow/index.mjs +110 -0
- package/dist/blurry-gradient/index.d.mts +17 -0
- package/dist/blurry-gradient/index.mjs +93 -0
- package/dist/button/index.d.mts +2 -0
- package/dist/button/index.mjs +3 -0
- package/dist/button-BiSQpBbc.mjs +129 -0
- package/dist/carousel/index.d.mts +40 -0
- package/dist/carousel/index.mjs +141 -0
- package/dist/checkbox/index.d.mts +37 -0
- package/dist/checkbox/index.mjs +70 -0
- package/dist/checkbox-group/index.d.mts +17 -0
- package/dist/checkbox-group/index.mjs +29 -0
- package/dist/chunk-BTpB_u-K.mjs +18 -0
- package/dist/countdown/index.d.mts +6 -0
- package/dist/countdown/index.mjs +58 -0
- package/dist/field/index.d.mts +66 -0
- package/dist/field/index.mjs +115 -0
- package/dist/fieldset/index.d.mts +33 -0
- package/dist/fieldset/index.mjs +61 -0
- package/dist/form/index.d.mts +22 -0
- package/dist/form/index.mjs +31 -0
- package/dist/generic-error/{index.d.ts → index.d.mts} +22 -18
- package/dist/generic-error/index.mjs +57 -0
- package/dist/grid/index.d.mts +1197 -0
- package/dist/grid/index.mjs +221 -0
- package/dist/heading/index.d.mts +31 -0
- package/dist/heading/index.mjs +29 -0
- package/dist/highlight/index.d.mts +18 -0
- package/dist/highlight/index.mjs +35 -0
- package/dist/hooks/{use-current-route-data.d.ts → use-current-route-data.d.mts} +3 -2
- package/dist/hooks/use-current-route-data.mjs +20 -0
- package/dist/hooks/{use-focus-search.d.ts → use-focus-search.d.mts} +4 -3
- package/dist/hooks/use-focus-search.mjs +21 -0
- package/dist/hooks/{use-matches-data.d.ts → use-matches-data.d.mts} +3 -2
- package/dist/hooks/use-matches-data.mjs +21 -0
- package/dist/hooks/{use-media-query.d.ts → use-media-query.d.mts} +3 -2
- package/dist/hooks/use-media-query.mjs +26 -0
- package/dist/hooks/use-mobile.d.mts +4 -0
- package/dist/hooks/use-mobile.mjs +20 -0
- package/dist/hooks/use-nonce.d.mts +8 -0
- package/dist/hooks/use-nonce.mjs +13 -0
- package/dist/hooks/{use-orientation.d.ts → use-orientation.d.mts} +3 -2
- package/dist/hooks/use-orientation.mjs +30 -0
- package/dist/hooks/use-user.d.mts +55 -0
- package/dist/hooks/use-user.mjs +39 -0
- package/dist/icon-button/index.d.mts +29 -0
- package/dist/icon-button/index.mjs +36 -0
- package/dist/if/index.d.mts +15 -0
- package/dist/if/index.mjs +21 -0
- package/dist/iframe/index.d.mts +11 -0
- package/dist/iframe/index.mjs +15 -0
- package/dist/index-Bm-tWhsb.d.mts +30 -0
- package/dist/index-YT2CkvL6.d.mts +36 -0
- package/dist/input/index.d.mts +2 -0
- package/dist/input/index.mjs +3 -0
- package/dist/input-CtR6aRVi.mjs +73 -0
- package/dist/link/index.d.mts +73 -0
- package/dist/link/index.mjs +129 -0
- package/dist/list/index.d.mts +71 -0
- package/dist/list/index.mjs +54 -0
- package/dist/markdown-container/index.d.mts +23 -0
- package/dist/markdown-container/index.mjs +71 -0
- package/dist/password-input/index.d.mts +24 -0
- package/dist/password-input/index.mjs +92 -0
- package/dist/picture/{index.d.ts → index.d.mts} +21 -20
- package/dist/picture/index.mjs +3 -0
- package/dist/picture-DkX3W5zl.mjs +69 -0
- package/dist/protected-email/{index.d.ts → index.d.mts} +14 -8
- package/dist/protected-email/index.mjs +37 -0
- package/dist/radio/index.d.mts +37 -0
- package/dist/radio/index.mjs +72 -0
- package/dist/radio-group/index.d.mts +17 -0
- package/dist/radio-group/index.mjs +29 -0
- package/dist/slider/index.d.mts +85 -0
- package/dist/slider/index.mjs +133 -0
- package/dist/switch/index.d.mts +38 -0
- package/dist/switch/index.mjs +87 -0
- package/dist/text/index.d.mts +26 -0
- package/dist/text/index.mjs +32 -0
- package/dist/text-CPlUND-Z.mjs +58 -0
- package/dist/toggle/index.d.mts +59 -0
- package/dist/toggle/index.mjs +82 -0
- package/dist/utils/author/index.d.mts +4 -0
- package/dist/utils/author/index.mjs +26 -0
- package/dist/utils/text/{index.d.ts → index.d.mts} +4 -3
- package/dist/utils/text/index.mjs +3 -0
- package/package.json +17 -129
- package/src/button/button.stories.tsx +161 -0
- package/src/button/button.test.tsx +73 -0
- package/src/button/button.tsx +112 -0
- package/src/button/index.ts +2 -0
- package/src/carousel/carousel-next.tsx +2 -2
- package/src/carousel/carousel-previous.tsx +2 -2
- package/src/checkbox/checkbox.stories.tsx +118 -0
- package/src/checkbox/checkbox.tsx +91 -0
- package/src/checkbox/index.ts +2 -0
- package/src/checkbox-group/checkbox-group.tsx +40 -0
- package/src/checkbox-group/index.ts +2 -0
- package/src/field/field.stories.tsx +105 -0
- package/src/field/field.test.tsx +61 -0
- package/src/field/field.tsx +165 -0
- package/src/field/index.ts +12 -0
- package/src/fieldset/fieldset.stories.tsx +204 -0
- package/src/fieldset/fieldset.test.tsx +63 -0
- package/src/fieldset/fieldset.tsx +75 -0
- package/src/fieldset/index.ts +7 -0
- package/src/form/form.stories.tsx +230 -0
- package/src/form/form.test.tsx +68 -0
- package/src/form/form.tsx +38 -0
- package/src/form/index.ts +2 -0
- package/src/icon-button/icon-button.stories.tsx +128 -7
- package/src/icon-button/icon-button.test.tsx +152 -0
- package/src/icon-button/icon-button.tsx +43 -9
- package/src/input/index.ts +2 -0
- package/src/input/input.stories.tsx +151 -0
- package/src/input/input.test.tsx +65 -0
- package/src/input/input.tsx +113 -0
- package/src/link/link.test.tsx +169 -0
- package/src/password-input/index.ts +1 -1
- package/src/password-input/password-input.tsx +104 -27
- package/src/radio/index.ts +2 -0
- package/src/radio/radio.tsx +92 -0
- package/src/radio-group/index.ts +2 -0
- package/src/radio-group/radio-group.tsx +36 -0
- package/src/slider/index.ts +18 -0
- package/src/slider/slider.tsx +179 -0
- package/src/switch/index.ts +2 -0
- package/src/switch/switch.stories.tsx +118 -0
- package/src/switch/switch.tsx +101 -0
- package/src/toggle/index.ts +2 -0
- package/src/toggle/toggle.stories.tsx +232 -0
- package/src/toggle/toggle.test.tsx +149 -0
- package/src/toggle/toggle.tsx +88 -0
- package/src/utils/text/text.test.tsx +110 -0
- package/dist/background-slideshow/index.d.ts +0 -24
- package/dist/background-slideshow/index.js +0 -165
- package/dist/blurry-gradient/index.d.ts +0 -16
- package/dist/blurry-gradient/index.js +0 -128
- package/dist/carousel/index.d.ts +0 -36
- package/dist/carousel/index.js +0 -171
- package/dist/countdown/index.d.ts +0 -5
- package/dist/countdown/index.js +0 -73
- package/dist/generic-error/index.js +0 -47
- package/dist/grid/index.d.ts +0 -1196
- package/dist/grid/index.js +0 -239
- package/dist/heading/index.d.ts +0 -24
- package/dist/heading/index.js +0 -99
- package/dist/highlight/index.d.ts +0 -13
- package/dist/highlight/index.js +0 -59
- package/dist/hooks/use-current-route-data.js +0 -16
- package/dist/hooks/use-focus-search.js +0 -19
- package/dist/hooks/use-matches-data.js +0 -15
- package/dist/hooks/use-media-query.js +0 -20
- package/dist/hooks/use-mobile.d.ts +0 -3
- package/dist/hooks/use-mobile.js +0 -19
- package/dist/hooks/use-nonce.d.ts +0 -7
- package/dist/hooks/use-nonce.js +0 -8
- package/dist/hooks/use-orientation.js +0 -29
- package/dist/hooks/use-user.d.ts +0 -50
- package/dist/hooks/use-user.js +0 -25
- package/dist/icon-button/index.d.ts +0 -9
- package/dist/icon-button/index.js +0 -17
- package/dist/if/index.d.ts +0 -10
- package/dist/if/index.js +0 -24
- package/dist/iframe/index.d.ts +0 -10
- package/dist/iframe/index.js +0 -17
- package/dist/link/index.d.ts +0 -55
- package/dist/link/index.js +0 -195
- package/dist/list/index.d.ts +0 -69
- package/dist/list/index.js +0 -65
- package/dist/markdown-container/index.d.ts +0 -22
- package/dist/markdown-container/index.js +0 -128
- package/dist/password-input/index.d.ts +0 -11
- package/dist/password-input/index.js +0 -46
- package/dist/picture/index.js +0 -68
- package/dist/protected-email/index.js +0 -30
- package/dist/text/index.d.ts +0 -20
- package/dist/text/index.js +0 -38
- package/dist/utils/author/index.d.ts +0 -3
- package/dist/utils/author/index.js +0 -33
- package/dist/utils/text/index.js +0 -73
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Button } from '../button';
|
|
3
|
+
import { Fieldset } from './fieldset';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
argTypes: {
|
|
7
|
+
variant: {
|
|
8
|
+
control: 'select',
|
|
9
|
+
description: 'Fieldset variant',
|
|
10
|
+
options: ['default', 'compact'],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
component: Fieldset.Root,
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: 'centered',
|
|
16
|
+
},
|
|
17
|
+
tags: ['autodocs'],
|
|
18
|
+
title: 'Components/Fieldset',
|
|
19
|
+
} satisfies Meta<typeof Fieldset.Root>;
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
render: () => (
|
|
26
|
+
<Fieldset.Root>
|
|
27
|
+
<Fieldset.Legend>Personal Information</Fieldset.Legend>
|
|
28
|
+
<div className="space-y-4">
|
|
29
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
30
|
+
First Name
|
|
31
|
+
<input
|
|
32
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md mt-1"
|
|
33
|
+
placeholder="Enter first name"
|
|
34
|
+
type="text"
|
|
35
|
+
/>
|
|
36
|
+
</label>
|
|
37
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
38
|
+
Last Name
|
|
39
|
+
<input
|
|
40
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md mt-1"
|
|
41
|
+
placeholder="Enter last name"
|
|
42
|
+
type="text"
|
|
43
|
+
/>
|
|
44
|
+
</label>
|
|
45
|
+
</div>
|
|
46
|
+
</Fieldset.Root>
|
|
47
|
+
),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const Compact: Story = {
|
|
51
|
+
render: () => (
|
|
52
|
+
<Fieldset.Root variant="compact">
|
|
53
|
+
<Fieldset.Legend>Contact Details</Fieldset.Legend>
|
|
54
|
+
<div className="space-y-2">
|
|
55
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
56
|
+
Email
|
|
57
|
+
<input
|
|
58
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md mt-1"
|
|
59
|
+
placeholder="Enter email"
|
|
60
|
+
type="email"
|
|
61
|
+
/>
|
|
62
|
+
</label>
|
|
63
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
64
|
+
Phone
|
|
65
|
+
<input
|
|
66
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md mt-1"
|
|
67
|
+
placeholder="Enter phone number"
|
|
68
|
+
type="tel"
|
|
69
|
+
/>
|
|
70
|
+
</label>
|
|
71
|
+
</div>
|
|
72
|
+
</Fieldset.Root>
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const SmallLegend: Story = {
|
|
77
|
+
render: () => (
|
|
78
|
+
<Fieldset.Root>
|
|
79
|
+
<Fieldset.Legend size="small">Settings</Fieldset.Legend>
|
|
80
|
+
<div className="space-y-4">
|
|
81
|
+
<label className="flex items-center">
|
|
82
|
+
<input
|
|
83
|
+
className="mr-2"
|
|
84
|
+
type="checkbox"
|
|
85
|
+
/>
|
|
86
|
+
<span className="text-sm">Enable notifications</span>
|
|
87
|
+
</label>
|
|
88
|
+
<label className="flex items-center">
|
|
89
|
+
<input
|
|
90
|
+
className="mr-2"
|
|
91
|
+
type="checkbox"
|
|
92
|
+
/>
|
|
93
|
+
<span className="text-sm">Allow marketing emails</span>
|
|
94
|
+
</label>
|
|
95
|
+
</div>
|
|
96
|
+
</Fieldset.Root>
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const WithCustomClass: Story = {
|
|
101
|
+
render: () => (
|
|
102
|
+
<Fieldset.Root className="bg-blue-50 border-blue-200">
|
|
103
|
+
<Fieldset.Legend className="text-blue-900">Blue Theme</Fieldset.Legend>
|
|
104
|
+
<div className="space-y-4">
|
|
105
|
+
<input
|
|
106
|
+
className="w-full px-3 py-2 border border-blue-300 rounded-md bg-white"
|
|
107
|
+
placeholder="Custom styled input"
|
|
108
|
+
type="text"
|
|
109
|
+
/>
|
|
110
|
+
<input
|
|
111
|
+
className="w-full px-3 py-2 border border-blue-300 rounded-md bg-white"
|
|
112
|
+
placeholder="Another input"
|
|
113
|
+
type="text"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
</Fieldset.Root>
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const NestedFieldsets: Story = {
|
|
121
|
+
render: () => (
|
|
122
|
+
<Fieldset.Root>
|
|
123
|
+
<Fieldset.Legend>Account Settings</Fieldset.Legend>
|
|
124
|
+
<div className="space-y-6">
|
|
125
|
+
<Fieldset.Root className="border-gray-300">
|
|
126
|
+
<Fieldset.Legend size="small">Profile Information</Fieldset.Legend>
|
|
127
|
+
<div className="space-y-3">
|
|
128
|
+
<input
|
|
129
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
|
130
|
+
placeholder="Username"
|
|
131
|
+
type="text"
|
|
132
|
+
/>
|
|
133
|
+
<input
|
|
134
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
|
135
|
+
placeholder="Email"
|
|
136
|
+
type="email"
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
</Fieldset.Root>
|
|
140
|
+
<Fieldset.Root className="border-gray-300">
|
|
141
|
+
<Fieldset.Legend size="small">Privacy Settings</Fieldset.Legend>
|
|
142
|
+
<div className="space-y-2">
|
|
143
|
+
<label className="flex items-center">
|
|
144
|
+
<input
|
|
145
|
+
className="mr-2"
|
|
146
|
+
type="checkbox"
|
|
147
|
+
/>
|
|
148
|
+
<span className="text-sm">Public profile</span>
|
|
149
|
+
</label>
|
|
150
|
+
<label className="flex items-center">
|
|
151
|
+
<input
|
|
152
|
+
className="mr-2"
|
|
153
|
+
type="checkbox"
|
|
154
|
+
/>
|
|
155
|
+
<span className="text-sm">Show email</span>
|
|
156
|
+
</label>
|
|
157
|
+
</div>
|
|
158
|
+
</Fieldset.Root>
|
|
159
|
+
</div>
|
|
160
|
+
</Fieldset.Root>
|
|
161
|
+
),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const FormExample: Story = {
|
|
165
|
+
render: () => (
|
|
166
|
+
<form className="space-y-6">
|
|
167
|
+
<Fieldset.Root>
|
|
168
|
+
<Fieldset.Legend>User Registration</Fieldset.Legend>
|
|
169
|
+
<div className="space-y-4">
|
|
170
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
171
|
+
Full Name
|
|
172
|
+
<input
|
|
173
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md mt-1"
|
|
174
|
+
placeholder="Enter your full name"
|
|
175
|
+
type="text"
|
|
176
|
+
/>
|
|
177
|
+
</label>
|
|
178
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
179
|
+
Email Address
|
|
180
|
+
<input
|
|
181
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md mt-1"
|
|
182
|
+
placeholder="Enter your email"
|
|
183
|
+
type="email"
|
|
184
|
+
/>
|
|
185
|
+
</label>
|
|
186
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
187
|
+
Password
|
|
188
|
+
<input
|
|
189
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md mt-1"
|
|
190
|
+
placeholder="Enter password"
|
|
191
|
+
type="password"
|
|
192
|
+
/>
|
|
193
|
+
</label>
|
|
194
|
+
</div>
|
|
195
|
+
</Fieldset.Root>
|
|
196
|
+
<Button
|
|
197
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
198
|
+
type="submit"
|
|
199
|
+
>
|
|
200
|
+
Register
|
|
201
|
+
</Button>
|
|
202
|
+
</form>
|
|
203
|
+
),
|
|
204
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { Fieldset } from './fieldset';
|
|
4
|
+
|
|
5
|
+
describe('Fieldset', () => {
|
|
6
|
+
it('renders FieldsetRoot with legend', () => {
|
|
7
|
+
render(
|
|
8
|
+
<Fieldset.Root>
|
|
9
|
+
<Fieldset.Legend>Test Legend</Fieldset.Legend>
|
|
10
|
+
<div>Fieldset content</div>
|
|
11
|
+
</Fieldset.Root>,
|
|
12
|
+
);
|
|
13
|
+
expect(screen.getByText('Test Legend')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('applies variant styles to FieldsetRoot', () => {
|
|
17
|
+
render(
|
|
18
|
+
<Fieldset.Root variant="compact">
|
|
19
|
+
<Fieldset.Legend>Compact Legend</Fieldset.Legend>
|
|
20
|
+
</Fieldset.Root>,
|
|
21
|
+
);
|
|
22
|
+
const fieldsetRoot = screen.getByText('Compact Legend').parentElement;
|
|
23
|
+
expect(fieldsetRoot).toHaveClass('space-y-2');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('applies size styles to FieldsetLegend', () => {
|
|
27
|
+
render(
|
|
28
|
+
<Fieldset.Root>
|
|
29
|
+
<Fieldset.Legend size="small">Small Legend</Fieldset.Legend>
|
|
30
|
+
</Fieldset.Root>,
|
|
31
|
+
);
|
|
32
|
+
expect(screen.getByText('Small Legend')).toHaveClass('text-base', 'font-medium');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('applies custom className to FieldsetRoot', () => {
|
|
36
|
+
render(
|
|
37
|
+
<Fieldset.Root className="custom-fieldset">
|
|
38
|
+
<Fieldset.Legend>Custom Legend</Fieldset.Legend>
|
|
39
|
+
</Fieldset.Root>,
|
|
40
|
+
);
|
|
41
|
+
const fieldsetRoot = screen.getByText('Custom Legend').parentElement;
|
|
42
|
+
expect(fieldsetRoot).toHaveClass('custom-fieldset');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('applies custom className to FieldsetLegend', () => {
|
|
46
|
+
render(
|
|
47
|
+
<Fieldset.Root>
|
|
48
|
+
<Fieldset.Legend className="custom-legend">Legend</Fieldset.Legend>
|
|
49
|
+
</Fieldset.Root>,
|
|
50
|
+
);
|
|
51
|
+
expect(screen.getByText('Legend')).toHaveClass('custom-legend');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders children content', () => {
|
|
55
|
+
render(
|
|
56
|
+
<Fieldset.Root>
|
|
57
|
+
<Fieldset.Legend>Legend</Fieldset.Legend>
|
|
58
|
+
<p data-testid="fieldset-content">Fieldset content</p>
|
|
59
|
+
</Fieldset.Root>,
|
|
60
|
+
);
|
|
61
|
+
expect(screen.getByTestId('fieldset-content')).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Fieldset as BaseUIFieldset } from '@base-ui/react/fieldset';
|
|
2
|
+
import { tv } from '@regardio/tailwind/utils';
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
const fieldsetRootVariants = {
|
|
6
|
+
compact: ['space-y-2'],
|
|
7
|
+
default: ['space-y-4'],
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
const fieldsetRoot = tv({
|
|
11
|
+
base: ['border', 'border-gray-200', 'rounded-lg', 'p-4'],
|
|
12
|
+
defaultVariants: {
|
|
13
|
+
variant: 'default',
|
|
14
|
+
},
|
|
15
|
+
variants: {
|
|
16
|
+
variant: fieldsetRootVariants,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const fieldsetLegend = tv({
|
|
21
|
+
base: ['text-lg', 'font-semibold', 'text-gray-900', 'mb-2'],
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
size: 'default',
|
|
24
|
+
},
|
|
25
|
+
variants: {
|
|
26
|
+
size: {
|
|
27
|
+
default: [],
|
|
28
|
+
small: ['text-base', 'font-medium', 'text-gray-900', 'mb-1'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export type FieldsetRootVariant = keyof typeof fieldsetRootVariants;
|
|
34
|
+
export type FieldsetLegendSize = 'default' | 'small';
|
|
35
|
+
|
|
36
|
+
export interface FieldsetRootProps
|
|
37
|
+
extends Omit<ComponentProps<typeof BaseUIFieldset.Root>, 'className'> {
|
|
38
|
+
className?: string;
|
|
39
|
+
variant?: FieldsetRootVariant;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FieldsetLegendProps
|
|
43
|
+
extends Omit<ComponentProps<typeof BaseUIFieldset.Legend>, 'className'> {
|
|
44
|
+
className?: string;
|
|
45
|
+
size?: FieldsetLegendSize;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const FieldsetRoot = ({ className, variant, ...props }: FieldsetRootProps) => {
|
|
49
|
+
return (
|
|
50
|
+
<BaseUIFieldset.Root
|
|
51
|
+
className={fieldsetRoot({
|
|
52
|
+
className,
|
|
53
|
+
variant,
|
|
54
|
+
})}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const FieldsetLegend = ({ className, size, ...props }: FieldsetLegendProps) => {
|
|
61
|
+
return (
|
|
62
|
+
<BaseUIFieldset.Legend
|
|
63
|
+
className={fieldsetLegend({
|
|
64
|
+
className,
|
|
65
|
+
size,
|
|
66
|
+
})}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Fieldset = {
|
|
73
|
+
Legend: FieldsetLegend,
|
|
74
|
+
Root: FieldsetRoot,
|
|
75
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Button } from '../button';
|
|
3
|
+
import { Field } from '../field';
|
|
4
|
+
import { Input } from '../input';
|
|
5
|
+
import { Form } from './form';
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
argTypes: {
|
|
9
|
+
variant: {
|
|
10
|
+
control: 'select',
|
|
11
|
+
description: 'Form variant',
|
|
12
|
+
options: ['default', 'compact', 'inline'],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
component: Form,
|
|
16
|
+
parameters: {
|
|
17
|
+
layout: 'centered',
|
|
18
|
+
},
|
|
19
|
+
tags: ['autodocs'],
|
|
20
|
+
title: 'Components/Form',
|
|
21
|
+
} satisfies Meta<typeof Form>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof meta>;
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<Form>
|
|
29
|
+
<Field.Root>
|
|
30
|
+
<Field.Label>Email</Field.Label>
|
|
31
|
+
<Input
|
|
32
|
+
placeholder="Enter your email"
|
|
33
|
+
type="email"
|
|
34
|
+
/>
|
|
35
|
+
<Field.Description>We'll never share your email.</Field.Description>
|
|
36
|
+
</Field.Root>
|
|
37
|
+
<Field.Root>
|
|
38
|
+
<Field.Label>Password</Field.Label>
|
|
39
|
+
<Input
|
|
40
|
+
placeholder="Enter your password"
|
|
41
|
+
type="password"
|
|
42
|
+
/>
|
|
43
|
+
</Field.Root>
|
|
44
|
+
<Button type="submit">Sign In</Button>
|
|
45
|
+
</Form>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Compact: Story = {
|
|
50
|
+
render: () => (
|
|
51
|
+
<Form variant="compact">
|
|
52
|
+
<Field.Root>
|
|
53
|
+
<Field.Label>Username</Field.Label>
|
|
54
|
+
<Input placeholder="Enter username" />
|
|
55
|
+
</Field.Root>
|
|
56
|
+
<Field.Root>
|
|
57
|
+
<Field.Label>Email</Field.Label>
|
|
58
|
+
<Input
|
|
59
|
+
placeholder="Enter email"
|
|
60
|
+
type="email"
|
|
61
|
+
/>
|
|
62
|
+
</Field.Root>
|
|
63
|
+
<Button type="submit">Register</Button>
|
|
64
|
+
</Form>
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const Inline: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<Form variant="inline">
|
|
71
|
+
<Field.Root>
|
|
72
|
+
<Field.Label>Search</Field.Label>
|
|
73
|
+
<Input placeholder="Search..." />
|
|
74
|
+
</Field.Root>
|
|
75
|
+
<Button type="submit">Search</Button>
|
|
76
|
+
</Form>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const WithValidation: Story = {
|
|
81
|
+
render: () => (
|
|
82
|
+
<Form>
|
|
83
|
+
<Field.Root>
|
|
84
|
+
<Field.Label variant="error">Email</Field.Label>
|
|
85
|
+
<Input
|
|
86
|
+
placeholder="Enter your email"
|
|
87
|
+
type="email"
|
|
88
|
+
variant="error"
|
|
89
|
+
/>
|
|
90
|
+
<Field.Error>Please enter a valid email address</Field.Error>
|
|
91
|
+
</Field.Root>
|
|
92
|
+
<Field.Root>
|
|
93
|
+
<Field.Label>Password</Field.Label>
|
|
94
|
+
<Input
|
|
95
|
+
placeholder="Enter your password"
|
|
96
|
+
type="password"
|
|
97
|
+
/>
|
|
98
|
+
<Field.Description>Password must be at least 8 characters</Field.Description>
|
|
99
|
+
</Field.Root>
|
|
100
|
+
<Button type="submit">Sign In</Button>
|
|
101
|
+
</Form>
|
|
102
|
+
),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const RegistrationForm: Story = {
|
|
106
|
+
render: () => (
|
|
107
|
+
<Form>
|
|
108
|
+
<div className="space-y-6">
|
|
109
|
+
<Field.Root>
|
|
110
|
+
<Field.Label>Full Name</Field.Label>
|
|
111
|
+
<Input placeholder="Enter your full name" />
|
|
112
|
+
</Field.Root>
|
|
113
|
+
<Field.Root>
|
|
114
|
+
<Field.Label>Email Address</Field.Label>
|
|
115
|
+
<Input
|
|
116
|
+
placeholder="Enter your email"
|
|
117
|
+
type="email"
|
|
118
|
+
/>
|
|
119
|
+
</Field.Root>
|
|
120
|
+
<Field.Root>
|
|
121
|
+
<Field.Label>Password</Field.Label>
|
|
122
|
+
<Input
|
|
123
|
+
placeholder="Create a password"
|
|
124
|
+
type="password"
|
|
125
|
+
/>
|
|
126
|
+
</Field.Root>
|
|
127
|
+
<Field.Root>
|
|
128
|
+
<Field.Label>Confirm Password</Field.Label>
|
|
129
|
+
<Input
|
|
130
|
+
placeholder="Confirm your password"
|
|
131
|
+
type="password"
|
|
132
|
+
/>
|
|
133
|
+
</Field.Root>
|
|
134
|
+
<Button
|
|
135
|
+
className="w-full"
|
|
136
|
+
type="submit"
|
|
137
|
+
>
|
|
138
|
+
Submit Form
|
|
139
|
+
</Button>
|
|
140
|
+
</div>
|
|
141
|
+
</Form>
|
|
142
|
+
),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const ContactForm: Story = {
|
|
146
|
+
render: () => (
|
|
147
|
+
<Form>
|
|
148
|
+
<div className="space-y-6">
|
|
149
|
+
<Field.Root>
|
|
150
|
+
<Field.Label>Name</Field.Label>
|
|
151
|
+
<Input placeholder="Your name" />
|
|
152
|
+
</Field.Root>
|
|
153
|
+
<Field.Root>
|
|
154
|
+
<Field.Label>Email</Field.Label>
|
|
155
|
+
<Input
|
|
156
|
+
placeholder="Your email"
|
|
157
|
+
type="email"
|
|
158
|
+
/>
|
|
159
|
+
</Field.Root>
|
|
160
|
+
<Field.Root>
|
|
161
|
+
<Field.Label>Subject</Field.Label>
|
|
162
|
+
<Input placeholder="Message subject" />
|
|
163
|
+
</Field.Root>
|
|
164
|
+
<Field.Root>
|
|
165
|
+
<Field.Label>Message</Field.Label>
|
|
166
|
+
<textarea
|
|
167
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
|
168
|
+
placeholder="Your message"
|
|
169
|
+
rows={4}
|
|
170
|
+
/>
|
|
171
|
+
</Field.Root>
|
|
172
|
+
<Button type="submit">Send Message</Button>
|
|
173
|
+
</div>
|
|
174
|
+
</Form>
|
|
175
|
+
),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const SearchForm: Story = {
|
|
179
|
+
render: () => (
|
|
180
|
+
<Form
|
|
181
|
+
className="max-w-md mx-auto"
|
|
182
|
+
variant="inline"
|
|
183
|
+
>
|
|
184
|
+
<Field.Root className="flex-1">
|
|
185
|
+
<Field.Label className="sr-only">Search</Field.Label>
|
|
186
|
+
<Input placeholder="Search products..." />
|
|
187
|
+
</Field.Root>
|
|
188
|
+
<Button type="submit">Search</Button>
|
|
189
|
+
</Form>
|
|
190
|
+
),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const WithCustomClass: Story = {
|
|
194
|
+
render: () => (
|
|
195
|
+
<Form className="bg-gray-50 p-6 rounded-lg shadow-md">
|
|
196
|
+
<Field.Root>
|
|
197
|
+
<Field.Label className="text-blue-600">Custom Field</Field.Label>
|
|
198
|
+
<Input
|
|
199
|
+
className="bg-white border-blue-300"
|
|
200
|
+
placeholder="Custom styled input"
|
|
201
|
+
/>
|
|
202
|
+
</Field.Root>
|
|
203
|
+
<Button
|
|
204
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
205
|
+
type="submit"
|
|
206
|
+
>
|
|
207
|
+
Register
|
|
208
|
+
</Button>
|
|
209
|
+
</Form>
|
|
210
|
+
),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const Interactive: Story = {
|
|
214
|
+
render: () => {
|
|
215
|
+
const handleSubmit = (event: React.FormEvent) => {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
alert('Form submitted!');
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<Form onSubmit={handleSubmit}>
|
|
222
|
+
<Field.Root>
|
|
223
|
+
<Field.Label>Interactive Field</Field.Label>
|
|
224
|
+
<Input placeholder="Type something..." />
|
|
225
|
+
</Field.Root>
|
|
226
|
+
<Button type="submit">Submit Form</Button>
|
|
227
|
+
</Form>
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { Form } from './form';
|
|
4
|
+
|
|
5
|
+
describe('Form', () => {
|
|
6
|
+
it('renders form with children', () => {
|
|
7
|
+
render(
|
|
8
|
+
<Form>
|
|
9
|
+
<div>Form content</div>
|
|
10
|
+
</Form>,
|
|
11
|
+
);
|
|
12
|
+
expect(screen.getByText('Form content')).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('applies variant styles', () => {
|
|
16
|
+
render(
|
|
17
|
+
<Form variant="compact">
|
|
18
|
+
<div>Compact form</div>
|
|
19
|
+
</Form>,
|
|
20
|
+
);
|
|
21
|
+
const formElement = screen.getByText('Compact form').parentElement;
|
|
22
|
+
expect(formElement).toHaveClass('space-y-4');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('applies inline variant styles', () => {
|
|
26
|
+
render(
|
|
27
|
+
<Form variant="inline">
|
|
28
|
+
<div>Inline form</div>
|
|
29
|
+
</Form>,
|
|
30
|
+
);
|
|
31
|
+
const formElement = screen.getByText('Inline form').parentElement;
|
|
32
|
+
expect(formElement).toHaveClass('flex', 'flex-wrap', 'gap-4');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('applies custom className', () => {
|
|
36
|
+
render(
|
|
37
|
+
<Form className="custom-form">
|
|
38
|
+
<div>Custom form</div>
|
|
39
|
+
</Form>,
|
|
40
|
+
);
|
|
41
|
+
const formElement = screen.getByText('Custom form').parentElement;
|
|
42
|
+
expect(formElement).toHaveClass('custom-form');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('passes through other props', () => {
|
|
46
|
+
render(
|
|
47
|
+
<Form
|
|
48
|
+
data-testid="test-form"
|
|
49
|
+
method="post"
|
|
50
|
+
>
|
|
51
|
+
<div>Test form</div>
|
|
52
|
+
</Form>,
|
|
53
|
+
);
|
|
54
|
+
const formElement = screen.getByTestId('test-form');
|
|
55
|
+
expect(formElement).toHaveAttribute('method', 'post');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('handles onSubmit', () => {
|
|
59
|
+
const handleSubmit = vi.fn();
|
|
60
|
+
render(
|
|
61
|
+
<Form onSubmit={handleSubmit}>
|
|
62
|
+
<div>Submit form</div>
|
|
63
|
+
</Form>,
|
|
64
|
+
);
|
|
65
|
+
const formElement = screen.getByText('Submit form').parentElement;
|
|
66
|
+
expect(formElement).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Form as BaseUIForm } from '@base-ui/react/form';
|
|
2
|
+
import { tv } from '@regardio/tailwind/utils';
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
const formVariants = {
|
|
6
|
+
compact: ['space-y-4'],
|
|
7
|
+
default: ['space-y-6'],
|
|
8
|
+
inline: ['flex', 'flex-wrap', 'gap-4', 'items-end'],
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
const form = tv({
|
|
12
|
+
base: [],
|
|
13
|
+
defaultVariants: {
|
|
14
|
+
variant: 'default',
|
|
15
|
+
},
|
|
16
|
+
variants: {
|
|
17
|
+
variant: formVariants,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type FormVariant = keyof typeof formVariants;
|
|
22
|
+
|
|
23
|
+
export interface FormProps extends Omit<ComponentProps<typeof BaseUIForm>, 'className'> {
|
|
24
|
+
className?: string;
|
|
25
|
+
variant?: FormVariant;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Form = ({ className, variant, ...props }: FormProps) => {
|
|
29
|
+
return (
|
|
30
|
+
<BaseUIForm
|
|
31
|
+
className={form({
|
|
32
|
+
className,
|
|
33
|
+
variant,
|
|
34
|
+
})}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
};
|