@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.
- 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 +5 -117
- 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/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/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,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
|
+
};
|
|
@@ -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
|
|
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
|
|
141
|
+
export const Disabled: Story = {
|
|
77
142
|
args: {
|
|
78
|
-
|
|
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
|
|
156
|
+
export const AllVariants: Story = {
|
|
84
157
|
render: () => (
|
|
85
|
-
<div
|
|
86
|
-
<IconButton
|
|
87
|
-
|
|
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
|
+
};
|