@regardio/react 0.5.7 → 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.
Files changed (180) hide show
  1. package/dist/background-slideshow/index.d.mts +36 -0
  2. package/dist/background-slideshow/index.mjs +110 -0
  3. package/dist/blurry-gradient/index.d.mts +17 -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 +40 -0
  9. package/dist/carousel/index.mjs +141 -0
  10. package/dist/checkbox/index.d.mts +37 -0
  11. package/dist/checkbox/index.mjs +70 -0
  12. package/dist/checkbox-group/index.d.mts +17 -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 +6 -0
  16. package/dist/countdown/index.mjs +58 -0
  17. package/dist/field/index.d.mts +66 -0
  18. package/dist/field/index.mjs +115 -0
  19. package/dist/fieldset/index.d.mts +33 -0
  20. package/dist/fieldset/index.mjs +61 -0
  21. package/dist/form/index.d.mts +22 -0
  22. package/dist/form/index.mjs +31 -0
  23. package/dist/generic-error/{index.d.ts → index.d.mts} +22 -18
  24. package/dist/generic-error/index.mjs +57 -0
  25. package/dist/grid/index.d.mts +1197 -0
  26. package/dist/grid/index.mjs +221 -0
  27. package/dist/heading/index.d.mts +31 -0
  28. package/dist/heading/index.mjs +29 -0
  29. package/dist/highlight/index.d.mts +18 -0
  30. package/dist/highlight/index.mjs +35 -0
  31. package/dist/hooks/{use-current-route-data.d.ts → use-current-route-data.d.mts} +3 -2
  32. package/dist/hooks/use-current-route-data.mjs +20 -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 +21 -0
  35. package/dist/hooks/{use-matches-data.d.ts → use-matches-data.d.mts} +3 -2
  36. package/dist/hooks/use-matches-data.mjs +21 -0
  37. package/dist/hooks/{use-media-query.d.ts → use-media-query.d.mts} +3 -2
  38. package/dist/hooks/use-media-query.mjs +26 -0
  39. package/dist/hooks/use-mobile.d.mts +4 -0
  40. package/dist/hooks/use-mobile.mjs +20 -0
  41. package/dist/hooks/use-nonce.d.mts +8 -0
  42. package/dist/hooks/use-nonce.mjs +13 -0
  43. package/dist/hooks/{use-orientation.d.ts → use-orientation.d.mts} +3 -2
  44. package/dist/hooks/use-orientation.mjs +30 -0
  45. package/dist/hooks/use-user.d.mts +55 -0
  46. package/dist/hooks/use-user.mjs +39 -0
  47. package/dist/icon-button/index.d.mts +29 -0
  48. package/dist/icon-button/index.mjs +36 -0
  49. package/dist/if/index.d.mts +15 -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-Bm-tWhsb.d.mts +30 -0
  54. package/dist/index-YT2CkvL6.d.mts +36 -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 +73 -0
  59. package/dist/link/index.mjs +129 -0
  60. package/dist/list/index.d.mts +71 -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 +24 -0
  65. package/dist/password-input/index.mjs +92 -0
  66. package/dist/picture/{index.d.ts → index.d.mts} +21 -20
  67. package/dist/picture/index.mjs +3 -0
  68. package/dist/picture-DkX3W5zl.mjs +69 -0
  69. package/dist/protected-email/{index.d.ts → index.d.mts} +14 -8
  70. package/dist/protected-email/index.mjs +37 -0
  71. package/dist/radio/index.d.mts +37 -0
  72. package/dist/radio/index.mjs +72 -0
  73. package/dist/radio-group/index.d.mts +17 -0
  74. package/dist/radio-group/index.mjs +29 -0
  75. package/dist/slider/index.d.mts +85 -0
  76. package/dist/slider/index.mjs +133 -0
  77. package/dist/switch/index.d.mts +38 -0
  78. package/dist/switch/index.mjs +87 -0
  79. package/dist/text/index.d.mts +26 -0
  80. package/dist/text/index.mjs +32 -0
  81. package/dist/text-CPlUND-Z.mjs +58 -0
  82. package/dist/toggle/index.d.mts +59 -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} +4 -3
  87. package/dist/utils/text/index.mjs +3 -0
  88. package/package.json +5 -117
  89. package/src/button/button.stories.tsx +161 -0
  90. package/src/button/button.test.tsx +73 -0
  91. package/src/button/button.tsx +112 -0
  92. package/src/button/index.ts +2 -0
  93. package/src/carousel/carousel-next.tsx +2 -2
  94. package/src/carousel/carousel-previous.tsx +2 -2
  95. package/src/checkbox/checkbox.stories.tsx +118 -0
  96. package/src/checkbox/checkbox.tsx +91 -0
  97. package/src/checkbox/index.ts +2 -0
  98. package/src/checkbox-group/checkbox-group.tsx +40 -0
  99. package/src/checkbox-group/index.ts +2 -0
  100. package/src/field/field.stories.tsx +105 -0
  101. package/src/field/field.test.tsx +61 -0
  102. package/src/field/field.tsx +165 -0
  103. package/src/field/index.ts +12 -0
  104. package/src/fieldset/fieldset.stories.tsx +204 -0
  105. package/src/fieldset/fieldset.test.tsx +63 -0
  106. package/src/fieldset/fieldset.tsx +75 -0
  107. package/src/fieldset/index.ts +7 -0
  108. package/src/form/form.stories.tsx +230 -0
  109. package/src/form/form.test.tsx +68 -0
  110. package/src/form/form.tsx +38 -0
  111. package/src/form/index.ts +2 -0
  112. package/src/icon-button/icon-button.stories.tsx +128 -7
  113. package/src/icon-button/icon-button.test.tsx +152 -0
  114. package/src/icon-button/icon-button.tsx +43 -9
  115. package/src/input/index.ts +2 -0
  116. package/src/input/input.stories.tsx +151 -0
  117. package/src/input/input.test.tsx +65 -0
  118. package/src/input/input.tsx +113 -0
  119. package/src/password-input/index.ts +1 -1
  120. package/src/password-input/password-input.tsx +104 -27
  121. package/src/radio/index.ts +2 -0
  122. package/src/radio/radio.tsx +92 -0
  123. package/src/radio-group/index.ts +2 -0
  124. package/src/radio-group/radio-group.tsx +36 -0
  125. package/src/slider/index.ts +18 -0
  126. package/src/slider/slider.tsx +179 -0
  127. package/src/switch/index.ts +2 -0
  128. package/src/switch/switch.stories.tsx +118 -0
  129. package/src/switch/switch.tsx +101 -0
  130. package/src/toggle/index.ts +2 -0
  131. package/src/toggle/toggle.stories.tsx +232 -0
  132. package/src/toggle/toggle.test.tsx +149 -0
  133. package/src/toggle/toggle.tsx +88 -0
  134. package/dist/background-slideshow/index.d.ts +0 -24
  135. package/dist/background-slideshow/index.js +0 -165
  136. package/dist/blurry-gradient/index.d.ts +0 -16
  137. package/dist/blurry-gradient/index.js +0 -128
  138. package/dist/carousel/index.d.ts +0 -36
  139. package/dist/carousel/index.js +0 -171
  140. package/dist/countdown/index.d.ts +0 -5
  141. package/dist/countdown/index.js +0 -73
  142. package/dist/generic-error/index.js +0 -47
  143. package/dist/grid/index.d.ts +0 -1196
  144. package/dist/grid/index.js +0 -239
  145. package/dist/heading/index.d.ts +0 -24
  146. package/dist/heading/index.js +0 -99
  147. package/dist/highlight/index.d.ts +0 -13
  148. package/dist/highlight/index.js +0 -59
  149. package/dist/hooks/use-current-route-data.js +0 -16
  150. package/dist/hooks/use-focus-search.js +0 -19
  151. package/dist/hooks/use-matches-data.js +0 -15
  152. package/dist/hooks/use-media-query.js +0 -20
  153. package/dist/hooks/use-mobile.d.ts +0 -3
  154. package/dist/hooks/use-mobile.js +0 -19
  155. package/dist/hooks/use-nonce.d.ts +0 -7
  156. package/dist/hooks/use-nonce.js +0 -8
  157. package/dist/hooks/use-orientation.js +0 -29
  158. package/dist/hooks/use-user.d.ts +0 -50
  159. package/dist/hooks/use-user.js +0 -25
  160. package/dist/icon-button/index.d.ts +0 -9
  161. package/dist/icon-button/index.js +0 -17
  162. package/dist/if/index.d.ts +0 -10
  163. package/dist/if/index.js +0 -24
  164. package/dist/iframe/index.d.ts +0 -10
  165. package/dist/iframe/index.js +0 -17
  166. package/dist/link/index.d.ts +0 -55
  167. package/dist/link/index.js +0 -195
  168. package/dist/list/index.d.ts +0 -69
  169. package/dist/list/index.js +0 -65
  170. package/dist/markdown-container/index.d.ts +0 -22
  171. package/dist/markdown-container/index.js +0 -128
  172. package/dist/password-input/index.d.ts +0 -11
  173. package/dist/password-input/index.js +0 -46
  174. package/dist/picture/index.js +0 -68
  175. package/dist/protected-email/index.js +0 -30
  176. package/dist/text/index.d.ts +0 -20
  177. package/dist/text/index.js +0 -38
  178. package/dist/utils/author/index.d.ts +0 -3
  179. package/dist/utils/author/index.js +0 -33
  180. package/dist/utils/text/index.js +0 -73
@@ -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,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 = {
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
+ };
@@ -0,0 +1,2 @@
1
+ export type { FormProps, FormVariant } from './form';
2
+ export { Form } from './form';
@@ -2,6 +2,26 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { IconButton } from './icon-button';
3
3
 
4
4
  const meta: Meta<typeof IconButton> = {
5
+ argTypes: {
6
+ disabled: {
7
+ control: 'boolean',
8
+ description: 'Disable the button',
9
+ },
10
+ size: {
11
+ control: 'select',
12
+ description: 'Icon button size',
13
+ options: ['sm', 'md', 'lg'],
14
+ },
15
+ title: {
16
+ control: 'text',
17
+ description: 'Title for tooltip and accessibility',
18
+ },
19
+ variant: {
20
+ control: 'select',
21
+ description: 'Button variant',
22
+ options: ['primary', 'secondary', 'outline', 'ghost', 'destructive'],
23
+ },
24
+ },
5
25
  component: IconButton,
6
26
  parameters: {
7
27
  layout: 'centered',
@@ -11,7 +31,7 @@ const meta: Meta<typeof IconButton> = {
11
31
  };
12
32
 
13
33
  export default meta;
14
- type Story = StoryObj<typeof IconButton>;
34
+ type Story = StoryObj<typeof meta>;
15
35
 
16
36
  const PlusIcon = () => (
17
37
  <svg
@@ -61,30 +81,131 @@ const CloseIcon = () => (
61
81
  </svg>
62
82
  );
63
83
 
84
+ const SettingsIcon = () => (
85
+ <svg
86
+ fill="none"
87
+ height="24"
88
+ stroke="currentColor"
89
+ strokeWidth="2"
90
+ viewBox="0 0 24 24"
91
+ width="24"
92
+ >
93
+ <circle
94
+ cx="12"
95
+ cy="12"
96
+ r="3"
97
+ />
98
+ <path d="m12 1v6m0 6v6m4.22-13.22 4.24 4.24M1.54 8.96l4.24 4.24m12.44 0 4.24 4.24M1.54 15.04l4.24-4.24" />
99
+ </svg>
100
+ );
101
+
64
102
  export const Default: Story = {
65
103
  args: {
66
104
  icon: <PlusIcon />,
105
+ title: 'Add',
67
106
  },
68
107
  };
69
108
 
70
109
  export const Close: Story = {
71
110
  args: {
72
111
  icon: <CloseIcon />,
112
+ title: 'Close',
113
+ variant: 'ghost',
114
+ },
115
+ };
116
+
117
+ export const Settings: Story = {
118
+ args: {
119
+ icon: <SettingsIcon />,
120
+ title: 'Settings',
121
+ variant: 'secondary',
122
+ },
123
+ };
124
+
125
+ export const Small: Story = {
126
+ args: {
127
+ icon: <PlusIcon />,
128
+ size: 'sm',
129
+ title: 'Add',
130
+ },
131
+ };
132
+
133
+ export const Large: Story = {
134
+ args: {
135
+ icon: <PlusIcon />,
136
+ size: 'lg',
137
+ title: 'Add',
73
138
  },
74
139
  };
75
140
 
76
- export const WithCustomClass: Story = {
141
+ export const Disabled: Story = {
77
142
  args: {
78
- className: 'p-2 bg-gray-100 rounded hover:bg-gray-200',
143
+ disabled: true,
79
144
  icon: <PlusIcon />,
145
+ title: 'Add',
146
+ },
147
+ };
148
+
149
+ export const WithAriaLabel: Story = {
150
+ args: {
151
+ 'aria-label': 'Close dialog',
152
+ icon: <CloseIcon />,
80
153
  },
81
154
  };
82
155
 
83
- export const AllIcons: Story = {
156
+ export const AllVariants: Story = {
84
157
  render: () => (
85
- <div style={{ display: 'flex', gap: '16px' }}>
86
- <IconButton icon={<PlusIcon />} />
87
- <IconButton icon={<CloseIcon />} />
158
+ <div className="flex gap-4">
159
+ <IconButton
160
+ icon={<PlusIcon />}
161
+ title="Add"
162
+ variant="primary"
163
+ />
164
+ <IconButton
165
+ icon={<SettingsIcon />}
166
+ title="Settings"
167
+ variant="secondary"
168
+ />
169
+ <IconButton
170
+ icon={<CloseIcon />}
171
+ title="Close"
172
+ variant="ghost"
173
+ />
174
+ <IconButton
175
+ icon={<PlusIcon />}
176
+ title="Delete"
177
+ variant="destructive"
178
+ />
88
179
  </div>
89
180
  ),
90
181
  };
182
+
183
+ export const AllSizes: Story = {
184
+ render: () => (
185
+ <div className="flex items-center gap-4">
186
+ <IconButton
187
+ icon={<PlusIcon />}
188
+ size="sm"
189
+ title="Small"
190
+ />
191
+ <IconButton
192
+ icon={<PlusIcon />}
193
+ size="md"
194
+ title="Medium"
195
+ />
196
+ <IconButton
197
+ icon={<PlusIcon />}
198
+ size="lg"
199
+ title="Large"
200
+ />
201
+ </div>
202
+ ),
203
+ };
204
+
205
+ export const Interactive: Story = {
206
+ args: {
207
+ icon: <PlusIcon />,
208
+ onClick: () => alert('Icon button clicked!'),
209
+ title: 'Add item',
210
+ },
211
+ };