@regardio/react 0.5.7 → 0.7.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 (214) hide show
  1. package/dist/background-slideshow/index.d.mts +34 -0
  2. package/dist/background-slideshow/index.mjs +110 -0
  3. package/dist/blurry-gradient/index.d.mts +16 -0
  4. package/dist/blurry-gradient/index.mjs +93 -0
  5. package/dist/button/index.d.mts +2 -0
  6. package/dist/button/index.mjs +3 -0
  7. package/dist/button-BiSQpBbc.mjs +129 -0
  8. package/dist/carousel/index.d.mts +74 -0
  9. package/dist/carousel/index.mjs +136 -0
  10. package/dist/checkbox/index.d.mts +28 -0
  11. package/dist/checkbox/index.mjs +70 -0
  12. package/dist/checkbox-group/index.d.mts +16 -0
  13. package/dist/checkbox-group/index.mjs +29 -0
  14. package/dist/chunk-BTpB_u-K.mjs +18 -0
  15. package/dist/countdown/index.d.mts +4 -0
  16. package/dist/countdown/index.mjs +58 -0
  17. package/dist/field/index.d.mts +68 -0
  18. package/dist/field/index.mjs +115 -0
  19. package/dist/fieldset/index.d.mts +34 -0
  20. package/dist/fieldset/index.mjs +61 -0
  21. package/dist/form/index.d.mts +21 -0
  22. package/dist/form/index.mjs +31 -0
  23. package/dist/generic-error/index.d.mts +44 -0
  24. package/dist/generic-error/index.mjs +57 -0
  25. package/dist/grid/index.d.mts +101 -0
  26. package/dist/grid/index.mjs +219 -0
  27. package/dist/heading/index.d.mts +27 -0
  28. package/dist/heading/index.mjs +28 -0
  29. package/dist/highlight/index.d.mts +17 -0
  30. package/dist/highlight/index.mjs +35 -0
  31. package/dist/hooks/use-current-route-data.d.mts +8 -0
  32. package/dist/hooks/use-current-route-data.mjs +22 -0
  33. package/dist/hooks/{use-focus-search.d.ts → use-focus-search.d.mts} +4 -3
  34. package/dist/hooks/use-focus-search.mjs +23 -0
  35. package/dist/hooks/use-matches-data.d.mts +10 -0
  36. package/dist/hooks/use-matches-data.mjs +23 -0
  37. package/dist/hooks/use-media-query.d.mts +9 -0
  38. package/dist/hooks/use-media-query.mjs +28 -0
  39. package/dist/hooks/use-mobile.d.mts +4 -0
  40. package/dist/hooks/use-mobile.mjs +22 -0
  41. package/dist/hooks/use-nonce.d.mts +6 -0
  42. package/dist/hooks/use-nonce.mjs +13 -0
  43. package/dist/hooks/use-orientation.d.mts +12 -0
  44. package/dist/hooks/use-orientation.mjs +32 -0
  45. package/dist/hooks/use-user.d.mts +53 -0
  46. package/dist/hooks/use-user.mjs +39 -0
  47. package/dist/icon-button/index.d.mts +28 -0
  48. package/dist/icon-button/index.mjs +36 -0
  49. package/dist/if/index.d.mts +13 -0
  50. package/dist/if/index.mjs +21 -0
  51. package/dist/iframe/index.d.mts +11 -0
  52. package/dist/iframe/index.mjs +15 -0
  53. package/dist/index-Bj5_XfEC.d.mts +29 -0
  54. package/dist/index-C_evL5vG.d.mts +35 -0
  55. package/dist/input/index.d.mts +2 -0
  56. package/dist/input/index.mjs +3 -0
  57. package/dist/input-CtR6aRVi.mjs +73 -0
  58. package/dist/link/index.d.mts +71 -0
  59. package/dist/link/index.mjs +129 -0
  60. package/dist/list/index.d.mts +72 -0
  61. package/dist/list/index.mjs +54 -0
  62. package/dist/markdown-container/index.d.mts +23 -0
  63. package/dist/markdown-container/index.mjs +71 -0
  64. package/dist/password-input/index.d.mts +23 -0
  65. package/dist/password-input/index.mjs +92 -0
  66. package/dist/picture/index.d.mts +39 -0
  67. package/dist/picture/index.mjs +3 -0
  68. package/dist/picture-DkX3W5zl.mjs +69 -0
  69. package/dist/protected-email/index.d.mts +24 -0
  70. package/dist/protected-email/index.mjs +37 -0
  71. package/dist/radio/index.d.mts +28 -0
  72. package/dist/radio/index.mjs +72 -0
  73. package/dist/radio-group/index.d.mts +16 -0
  74. package/dist/radio-group/index.mjs +29 -0
  75. package/dist/slider/index.d.mts +63 -0
  76. package/dist/slider/index.mjs +133 -0
  77. package/dist/switch/index.d.mts +29 -0
  78. package/dist/switch/index.mjs +87 -0
  79. package/dist/text/index.d.mts +25 -0
  80. package/dist/text/index.mjs +32 -0
  81. package/dist/text-EQC4zJbE.mjs +52 -0
  82. package/dist/toggle/index.d.mts +25 -0
  83. package/dist/toggle/index.mjs +82 -0
  84. package/dist/utils/author/index.d.mts +4 -0
  85. package/dist/utils/author/index.mjs +26 -0
  86. package/dist/utils/text/{index.d.ts → index.d.mts} +9 -8
  87. package/dist/utils/text/index.mjs +3 -0
  88. package/package.json +131 -83
  89. package/src/background-slideshow/background-slideshow.tsx +1 -2
  90. package/src/blurry-gradient/blurry-gradient.tsx +1 -1
  91. package/src/button/button.stories.tsx +161 -0
  92. package/src/button/button.test.tsx +73 -0
  93. package/src/button/button.tsx +118 -0
  94. package/src/button/index.ts +2 -0
  95. package/src/carousel/carousel-content.tsx +17 -14
  96. package/src/carousel/carousel-item.tsx +18 -18
  97. package/src/carousel/carousel-next.tsx +22 -17
  98. package/src/carousel/carousel-previous.tsx +22 -17
  99. package/src/carousel/carousel-root.tsx +91 -86
  100. package/src/checkbox/checkbox.stories.tsx +118 -0
  101. package/src/checkbox/checkbox.tsx +102 -0
  102. package/src/checkbox/index.ts +2 -0
  103. package/src/checkbox-group/checkbox-group.tsx +40 -0
  104. package/src/checkbox-group/index.ts +2 -0
  105. package/src/countdown/countdown.tsx +1 -1
  106. package/src/field/field.stories.tsx +105 -0
  107. package/src/field/field.test.tsx +61 -0
  108. package/src/field/field.tsx +186 -0
  109. package/src/field/index.ts +12 -0
  110. package/src/fieldset/fieldset.stories.tsx +204 -0
  111. package/src/fieldset/fieldset.test.tsx +63 -0
  112. package/src/fieldset/fieldset.tsx +86 -0
  113. package/src/fieldset/index.ts +7 -0
  114. package/src/form/form.stories.tsx +230 -0
  115. package/src/form/form.test.tsx +68 -0
  116. package/src/form/form.tsx +38 -0
  117. package/src/form/index.ts +2 -0
  118. package/src/generic-error/generic-error.tsx +2 -3
  119. package/src/grid/grid-item.tsx +77 -36
  120. package/src/grid/grid-root.tsx +49 -22
  121. package/src/heading/heading.tsx +7 -3
  122. package/src/highlight/highlight.tsx +1 -1
  123. package/src/hooks/use-current-route-data.ts +4 -2
  124. package/src/hooks/use-focus-search.ts +3 -1
  125. package/src/hooks/use-matches-data.ts +2 -0
  126. package/src/hooks/use-media-query.ts +2 -0
  127. package/src/hooks/use-mobile.ts +3 -1
  128. package/src/hooks/use-nonce.ts +3 -3
  129. package/src/hooks/use-orientation.ts +2 -0
  130. package/src/hooks/use-user.tsx +3 -2
  131. package/src/icon-button/icon-button.stories.tsx +128 -7
  132. package/src/icon-button/icon-button.test.tsx +152 -0
  133. package/src/icon-button/icon-button.tsx +43 -9
  134. package/src/if/if.tsx +3 -1
  135. package/src/input/index.ts +2 -0
  136. package/src/input/input.stories.tsx +151 -0
  137. package/src/input/input.test.tsx +65 -0
  138. package/src/input/input.tsx +113 -0
  139. package/src/link/link.tsx +4 -3
  140. package/src/list/list-item.tsx +10 -13
  141. package/src/list/list-root-context.ts +3 -3
  142. package/src/list/list-root.tsx +10 -13
  143. package/src/password-input/index.ts +1 -1
  144. package/src/password-input/password-input.tsx +104 -27
  145. package/src/protected-email/protected-email.tsx +6 -1
  146. package/src/radio/index.ts +2 -0
  147. package/src/radio/radio.tsx +103 -0
  148. package/src/radio-group/index.ts +2 -0
  149. package/src/radio-group/radio-group.tsx +40 -0
  150. package/src/slider/index.ts +18 -0
  151. package/src/slider/slider.tsx +201 -0
  152. package/src/switch/index.ts +2 -0
  153. package/src/switch/switch.stories.tsx +118 -0
  154. package/src/switch/switch.tsx +112 -0
  155. package/src/text/text.tsx +6 -1
  156. package/src/toggle/index.ts +2 -0
  157. package/src/toggle/toggle.stories.tsx +232 -0
  158. package/src/toggle/toggle.test.tsx +149 -0
  159. package/src/toggle/toggle.tsx +88 -0
  160. package/src/utils/text/text.tsx +8 -16
  161. package/dist/background-slideshow/index.d.ts +0 -24
  162. package/dist/background-slideshow/index.js +0 -165
  163. package/dist/blurry-gradient/index.d.ts +0 -16
  164. package/dist/blurry-gradient/index.js +0 -128
  165. package/dist/carousel/index.d.ts +0 -36
  166. package/dist/carousel/index.js +0 -171
  167. package/dist/countdown/index.d.ts +0 -5
  168. package/dist/countdown/index.js +0 -73
  169. package/dist/generic-error/index.d.ts +0 -43
  170. package/dist/generic-error/index.js +0 -47
  171. package/dist/grid/index.d.ts +0 -1196
  172. package/dist/grid/index.js +0 -239
  173. package/dist/heading/index.d.ts +0 -24
  174. package/dist/heading/index.js +0 -99
  175. package/dist/highlight/index.d.ts +0 -13
  176. package/dist/highlight/index.js +0 -59
  177. package/dist/hooks/use-current-route-data.d.ts +0 -7
  178. package/dist/hooks/use-current-route-data.js +0 -16
  179. package/dist/hooks/use-focus-search.js +0 -19
  180. package/dist/hooks/use-matches-data.d.ts +0 -9
  181. package/dist/hooks/use-matches-data.js +0 -15
  182. package/dist/hooks/use-media-query.d.ts +0 -8
  183. package/dist/hooks/use-media-query.js +0 -20
  184. package/dist/hooks/use-mobile.d.ts +0 -3
  185. package/dist/hooks/use-mobile.js +0 -19
  186. package/dist/hooks/use-nonce.d.ts +0 -7
  187. package/dist/hooks/use-nonce.js +0 -8
  188. package/dist/hooks/use-orientation.d.ts +0 -11
  189. package/dist/hooks/use-orientation.js +0 -29
  190. package/dist/hooks/use-user.d.ts +0 -50
  191. package/dist/hooks/use-user.js +0 -25
  192. package/dist/icon-button/index.d.ts +0 -9
  193. package/dist/icon-button/index.js +0 -17
  194. package/dist/if/index.d.ts +0 -10
  195. package/dist/if/index.js +0 -24
  196. package/dist/iframe/index.d.ts +0 -10
  197. package/dist/iframe/index.js +0 -17
  198. package/dist/link/index.d.ts +0 -55
  199. package/dist/link/index.js +0 -195
  200. package/dist/list/index.d.ts +0 -69
  201. package/dist/list/index.js +0 -65
  202. package/dist/markdown-container/index.d.ts +0 -22
  203. package/dist/markdown-container/index.js +0 -128
  204. package/dist/password-input/index.d.ts +0 -11
  205. package/dist/password-input/index.js +0 -46
  206. package/dist/picture/index.d.ts +0 -38
  207. package/dist/picture/index.js +0 -68
  208. package/dist/protected-email/index.d.ts +0 -20
  209. package/dist/protected-email/index.js +0 -30
  210. package/dist/text/index.d.ts +0 -20
  211. package/dist/text/index.js +0 -38
  212. package/dist/utils/author/index.d.ts +0 -3
  213. package/dist/utils/author/index.js +0 -33
  214. 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: Meta<typeof Fieldset.Root> = {
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,86 @@
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 = ({
49
+ className,
50
+ variant,
51
+ ...props
52
+ }: FieldsetRootProps): React.JSX.Element => {
53
+ return (
54
+ <BaseUIFieldset.Root
55
+ className={fieldsetRoot({
56
+ className,
57
+ variant,
58
+ })}
59
+ {...props}
60
+ />
61
+ );
62
+ };
63
+
64
+ export const FieldsetLegend = ({
65
+ className,
66
+ size,
67
+ ...props
68
+ }: FieldsetLegendProps): React.JSX.Element => {
69
+ return (
70
+ <BaseUIFieldset.Legend
71
+ className={fieldsetLegend({
72
+ className,
73
+ size,
74
+ })}
75
+ {...props}
76
+ />
77
+ );
78
+ };
79
+
80
+ export const Fieldset: {
81
+ Legend: typeof FieldsetLegend;
82
+ Root: typeof FieldsetRoot;
83
+ } = {
84
+ Legend: FieldsetLegend,
85
+ Root: FieldsetRoot,
86
+ };
@@ -0,0 +1,7 @@
1
+ export type {
2
+ FieldsetLegendProps,
3
+ FieldsetLegendSize,
4
+ FieldsetRootProps,
5
+ FieldsetRootVariant,
6
+ } from './fieldset';
7
+ export { Fieldset } from './fieldset';
@@ -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: Meta<typeof Form> = {
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): React.JSX.Element => {
29
+ return (
30
+ <BaseUIForm
31
+ className={form({
32
+ className,
33
+ variant,
34
+ })}
35
+ {...props}
36
+ />
37
+ );
38
+ };
@@ -0,0 +1,2 @@
1
+ export type { FormProps, FormVariant } from './form';
2
+ export { Form } from './form';