@tanstack/cta-framework-react-cra 0.23.2 → 0.24.1
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/add-ons/convex/package.json +1 -1
- package/add-ons/db/package.json +1 -1
- package/add-ons/form/package.json +1 -1
- package/add-ons/mcp/package.json +1 -1
- package/add-ons/mcp/small-logo.svg +1 -0
- package/add-ons/sentry/package.json +1 -1
- package/add-ons/shadcn/package.json +1 -1
- package/add-ons/store/package.json +1 -1
- package/add-ons/storybook/assets/_dot_storybook/main.ts +17 -0
- package/add-ons/storybook/assets/_dot_storybook/preview.ts +15 -0
- package/add-ons/storybook/assets/src/components/storybook/button.stories.ts +67 -0
- package/add-ons/storybook/assets/src/components/storybook/button.tsx +50 -0
- package/add-ons/storybook/assets/src/components/storybook/dialog.stories.tsx +92 -0
- package/add-ons/storybook/assets/src/components/storybook/dialog.tsx +33 -0
- package/add-ons/storybook/assets/src/components/storybook/index.ts +14 -0
- package/add-ons/storybook/assets/src/components/storybook/input.stories.ts +43 -0
- package/add-ons/storybook/assets/src/components/storybook/input.tsx +42 -0
- package/add-ons/storybook/assets/src/components/storybook/radio-group.stories.ts +53 -0
- package/add-ons/storybook/assets/src/components/storybook/radio-group.tsx +52 -0
- package/add-ons/storybook/assets/src/components/storybook/slider.stories.ts +55 -0
- package/add-ons/storybook/assets/src/components/storybook/slider.tsx +57 -0
- package/add-ons/storybook/assets/src/routes/demo.storybook.tsx +93 -0
- package/add-ons/storybook/info.json +16 -0
- package/add-ons/storybook/package.json +10 -0
- package/add-ons/storybook/small-logo.svg +1 -0
- package/add-ons/t3env/package.json +3 -3
- package/add-ons/tRPC/package.json +1 -1
- package/add-ons/table/package.json +1 -1
- package/examples/tanchat/package.json +3 -3
- package/package.json +3 -3
- package/project/base/package.json +4 -4
- package/project/base/src/routes/__root.tsx.ejs +3 -3
- package/tests/snapshots/react-cra/cr-js-form-npm.json +2 -2
- package/tests/snapshots/react-cra/cr-js-npm.json +2 -2
- package/tests/snapshots/react-cra/cr-ts-npm.json +2 -2
- package/tests/snapshots/react-cra/cr-ts-start-npm.json +2 -2
- package/tests/snapshots/react-cra/cr-ts-start-tanstack-query-npm.json +2 -2
- package/tests/snapshots/react-cra/fr-ts-biome-npm.json +2 -2
- package/tests/snapshots/react-cra/fr-ts-npm.json +2 -2
- package/tests/snapshots/react-cra/fr-ts-tw-npm.json +2 -2
- package/toolchains/biome/package.json +1 -1
package/add-ons/db/package.json
CHANGED
package/add-ons/mcp/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ModelContextProtocol</title><path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path><path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z"></path></svg>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StorybookConfig } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
const config: StorybookConfig = {
|
|
4
|
+
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
5
|
+
addons: [],
|
|
6
|
+
framework: {
|
|
7
|
+
name: "@storybook/react-vite",
|
|
8
|
+
options: {},
|
|
9
|
+
},
|
|
10
|
+
async viteFinal(config) {
|
|
11
|
+
const { default: tailwindcss } = await import("@tailwindcss/vite");
|
|
12
|
+
config.plugins = config.plugins || [];
|
|
13
|
+
config.plugins.push(tailwindcss());
|
|
14
|
+
return config;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export default config;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Preview } from "@storybook/react-vite";
|
|
2
|
+
import "../src/styles.css";
|
|
3
|
+
|
|
4
|
+
const preview: Preview = {
|
|
5
|
+
parameters: {
|
|
6
|
+
controls: {
|
|
7
|
+
matchers: {
|
|
8
|
+
color: /(background|color)$/i,
|
|
9
|
+
date: /Date$/i,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default preview;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { fn } from "storybook/test";
|
|
3
|
+
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Form/Button",
|
|
8
|
+
component: Button,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "centered",
|
|
11
|
+
},
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
args: { onClick: fn() },
|
|
14
|
+
} satisfies Meta<typeof Button>;
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
|
|
19
|
+
export const Primary: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
variant: "primary",
|
|
22
|
+
children: "Primary Button",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Secondary: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
variant: "secondary",
|
|
29
|
+
children: "Secondary Button",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Danger: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
variant: "danger",
|
|
36
|
+
children: "Delete Account",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Small: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
size: "small",
|
|
43
|
+
children: "Small Button",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Medium: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
size: "medium",
|
|
50
|
+
children: "Medium Button",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Large: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
size: "large",
|
|
57
|
+
children: "Large Button",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const Disabled: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
variant: "primary",
|
|
64
|
+
children: "Disabled Button",
|
|
65
|
+
disabled: true,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface ButtonProps {
|
|
4
|
+
variant?: "primary" | "secondary" | "danger";
|
|
5
|
+
size?: "small" | "medium" | "large";
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
type?: "button" | "submit" | "reset";
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Button: React.FC<ButtonProps> = ({
|
|
14
|
+
variant = "primary",
|
|
15
|
+
size = "medium",
|
|
16
|
+
children,
|
|
17
|
+
onClick,
|
|
18
|
+
disabled = false,
|
|
19
|
+
type = "button",
|
|
20
|
+
className = "",
|
|
21
|
+
}) => {
|
|
22
|
+
const baseStyles =
|
|
23
|
+
"font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed";
|
|
24
|
+
|
|
25
|
+
const variantStyles = {
|
|
26
|
+
primary:
|
|
27
|
+
"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600",
|
|
28
|
+
secondary:
|
|
29
|
+
"bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600",
|
|
30
|
+
danger:
|
|
31
|
+
"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 dark:bg-red-500 dark:hover:bg-red-600",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const sizeStyles = {
|
|
35
|
+
small: "px-3 py-1.5 text-sm",
|
|
36
|
+
medium: "px-4 py-2 text-base",
|
|
37
|
+
large: "px-6 py-3 text-lg",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<button
|
|
42
|
+
type={type}
|
|
43
|
+
onClick={onClick}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { Dialog } from "./dialog";
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Form/Dialog",
|
|
8
|
+
component: Dialog,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "centered",
|
|
11
|
+
},
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
} satisfies Meta<typeof Dialog>;
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
title: "User Profile",
|
|
21
|
+
children: (
|
|
22
|
+
<div className="space-y-4">
|
|
23
|
+
<p className="text-gray-700 dark:text-gray-300">
|
|
24
|
+
This is a simple dialog component with a title and content area.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const WithFooter: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
title: "Confirm Action",
|
|
34
|
+
children: (
|
|
35
|
+
<div className="space-y-4">
|
|
36
|
+
<p className="text-gray-700 dark:text-gray-300">
|
|
37
|
+
Are you sure you want to proceed with this action?
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
),
|
|
41
|
+
footer: (
|
|
42
|
+
<div className="flex gap-3 justify-end">
|
|
43
|
+
<Button variant="secondary" size="medium">
|
|
44
|
+
Cancel
|
|
45
|
+
</Button>
|
|
46
|
+
<Button variant="primary" size="medium">
|
|
47
|
+
Confirm
|
|
48
|
+
</Button>
|
|
49
|
+
</div>
|
|
50
|
+
),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Form: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
title: "Create Account",
|
|
57
|
+
children: (
|
|
58
|
+
<div className="space-y-4 min-w-80">
|
|
59
|
+
<div className="flex flex-col gap-2">
|
|
60
|
+
<label className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
61
|
+
Email
|
|
62
|
+
</label>
|
|
63
|
+
<input
|
|
64
|
+
type="email"
|
|
65
|
+
placeholder="you@example.com"
|
|
66
|
+
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<div className="flex flex-col gap-2">
|
|
70
|
+
<label className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
71
|
+
Password
|
|
72
|
+
</label>
|
|
73
|
+
<input
|
|
74
|
+
type="password"
|
|
75
|
+
placeholder="••••••••"
|
|
76
|
+
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
),
|
|
81
|
+
footer: (
|
|
82
|
+
<div className="flex gap-3 justify-end">
|
|
83
|
+
<Button variant="secondary" size="medium">
|
|
84
|
+
Cancel
|
|
85
|
+
</Button>
|
|
86
|
+
<Button variant="primary" size="medium">
|
|
87
|
+
Create Account
|
|
88
|
+
</Button>
|
|
89
|
+
</div>
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface DialogProps {
|
|
4
|
+
title: string;
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
footer?: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Dialog: React.FC<DialogProps> = ({
|
|
11
|
+
title,
|
|
12
|
+
children,
|
|
13
|
+
footer,
|
|
14
|
+
className = "",
|
|
15
|
+
}) => {
|
|
16
|
+
return (
|
|
17
|
+
<div
|
|
18
|
+
className={`bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 overflow-hidden ${className}`}
|
|
19
|
+
>
|
|
20
|
+
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
21
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
|
22
|
+
{title}
|
|
23
|
+
</h2>
|
|
24
|
+
</div>
|
|
25
|
+
<div className="px-6 py-6">{children}</div>
|
|
26
|
+
{footer && (
|
|
27
|
+
<div className="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
|
|
28
|
+
{footer}
|
|
29
|
+
</div>
|
|
30
|
+
)}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { Input } from "./input";
|
|
2
|
+
export type { InputProps } from "./input";
|
|
3
|
+
|
|
4
|
+
export { RadioGroup } from "./radio-group";
|
|
5
|
+
export type { RadioGroupProps, RadioOption } from "./radio-group";
|
|
6
|
+
|
|
7
|
+
export { Slider } from "./slider";
|
|
8
|
+
export type { SliderProps } from "./slider";
|
|
9
|
+
|
|
10
|
+
export { Dialog } from "./dialog";
|
|
11
|
+
export type { DialogProps } from "./dialog";
|
|
12
|
+
|
|
13
|
+
export { Button } from "./button";
|
|
14
|
+
export type { ButtonProps } from "./button";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { fn } from "storybook/test";
|
|
3
|
+
|
|
4
|
+
import { Input } from "./input";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Form/Input",
|
|
8
|
+
component: Input,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "centered",
|
|
11
|
+
},
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
args: { onChange: fn() },
|
|
14
|
+
} satisfies Meta<typeof Input>;
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
label: "Email Address",
|
|
22
|
+
id: "email",
|
|
23
|
+
placeholder: "Enter your email",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const Required: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
label: "First Name",
|
|
30
|
+
id: "firstName",
|
|
31
|
+
placeholder: "John",
|
|
32
|
+
required: true,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const WithValue: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
label: "Last Name",
|
|
39
|
+
id: "lastName",
|
|
40
|
+
value: "Doe",
|
|
41
|
+
placeholder: "Enter last name",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface InputProps {
|
|
4
|
+
label: string;
|
|
5
|
+
id: string;
|
|
6
|
+
value?: string;
|
|
7
|
+
onChange?: (value: string) => void;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Input: React.FC<InputProps> = ({
|
|
14
|
+
label,
|
|
15
|
+
id,
|
|
16
|
+
value = "",
|
|
17
|
+
onChange,
|
|
18
|
+
placeholder,
|
|
19
|
+
required = false,
|
|
20
|
+
className = "",
|
|
21
|
+
}) => {
|
|
22
|
+
return (
|
|
23
|
+
<div className={`flex flex-col gap-2 ${className}`}>
|
|
24
|
+
<label
|
|
25
|
+
htmlFor={id}
|
|
26
|
+
className="text-sm font-medium text-gray-700 dark:text-gray-200"
|
|
27
|
+
>
|
|
28
|
+
{label}
|
|
29
|
+
{required && <span className="text-red-500 ml-1">*</span>}
|
|
30
|
+
</label>
|
|
31
|
+
<input
|
|
32
|
+
type="text"
|
|
33
|
+
id={id}
|
|
34
|
+
value={value}
|
|
35
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
36
|
+
placeholder={placeholder}
|
|
37
|
+
required={required}
|
|
38
|
+
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 transition-colors"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { fn } from "storybook/test";
|
|
3
|
+
|
|
4
|
+
import { RadioGroup } from "./radio-group";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Form/RadioGroup",
|
|
8
|
+
component: RadioGroup,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "centered",
|
|
11
|
+
},
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
args: { onChange: fn() },
|
|
14
|
+
} satisfies Meta<typeof RadioGroup>;
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
label: "Employment Type",
|
|
22
|
+
name: "employmentType",
|
|
23
|
+
options: [
|
|
24
|
+
{ value: "full-time", label: "Full Time" },
|
|
25
|
+
{ value: "part-time", label: "Part Time" },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Selected: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
label: "Employment Type",
|
|
33
|
+
name: "employmentType",
|
|
34
|
+
options: [
|
|
35
|
+
{ value: "full-time", label: "Full Time" },
|
|
36
|
+
{ value: "part-time", label: "Part Time" },
|
|
37
|
+
],
|
|
38
|
+
value: "full-time",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const MultipleOptions: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
label: "Subscription Plan",
|
|
45
|
+
name: "plan",
|
|
46
|
+
options: [
|
|
47
|
+
{ value: "basic", label: "Basic" },
|
|
48
|
+
{ value: "pro", label: "Pro" },
|
|
49
|
+
{ value: "enterprise", label: "Enterprise" },
|
|
50
|
+
],
|
|
51
|
+
value: "pro",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface RadioOption {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RadioGroupProps {
|
|
9
|
+
label: string;
|
|
10
|
+
name: string;
|
|
11
|
+
options: RadioOption[];
|
|
12
|
+
value?: string;
|
|
13
|
+
onChange?: (value: string) => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const RadioGroup: React.FC<RadioGroupProps> = ({
|
|
18
|
+
label,
|
|
19
|
+
name,
|
|
20
|
+
options,
|
|
21
|
+
value,
|
|
22
|
+
onChange,
|
|
23
|
+
className = "",
|
|
24
|
+
}) => {
|
|
25
|
+
return (
|
|
26
|
+
<div className={`flex flex-col gap-3 ${className}`}>
|
|
27
|
+
<label className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
28
|
+
{label}
|
|
29
|
+
</label>
|
|
30
|
+
<div className="flex gap-4">
|
|
31
|
+
{options.map((option) => (
|
|
32
|
+
<label
|
|
33
|
+
key={option.value}
|
|
34
|
+
className="flex items-center gap-2 cursor-pointer group"
|
|
35
|
+
>
|
|
36
|
+
<input
|
|
37
|
+
type="radio"
|
|
38
|
+
name={name}
|
|
39
|
+
value={option.value}
|
|
40
|
+
checked={value === option.value}
|
|
41
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
42
|
+
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 cursor-pointer"
|
|
43
|
+
/>
|
|
44
|
+
<span className="text-sm text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100 transition-colors">
|
|
45
|
+
{option.label}
|
|
46
|
+
</span>
|
|
47
|
+
</label>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { fn } from "storybook/test";
|
|
3
|
+
|
|
4
|
+
import { Slider } from "./slider";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Form/Slider",
|
|
8
|
+
component: Slider,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "centered",
|
|
11
|
+
},
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
args: { onChange: fn() },
|
|
14
|
+
} satisfies Meta<typeof Slider>;
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
args: {
|
|
21
|
+
label: "Volume",
|
|
22
|
+
id: "volume",
|
|
23
|
+
value: 50,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const Skill: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
label: "Plays Guitar",
|
|
30
|
+
id: "guitar",
|
|
31
|
+
value: 75,
|
|
32
|
+
min: 0,
|
|
33
|
+
max: 100,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const NoValue: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
label: "Experience Level",
|
|
40
|
+
id: "experience",
|
|
41
|
+
value: 30,
|
|
42
|
+
showValue: false,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const CustomRange: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
label: "Custom Range",
|
|
49
|
+
id: "custom-range",
|
|
50
|
+
value: 5,
|
|
51
|
+
min: 1,
|
|
52
|
+
max: 20,
|
|
53
|
+
step: 1,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface SliderProps {
|
|
4
|
+
label: string;
|
|
5
|
+
id: string;
|
|
6
|
+
value?: number;
|
|
7
|
+
onChange?: (value: number) => void;
|
|
8
|
+
min?: number;
|
|
9
|
+
max?: number;
|
|
10
|
+
step?: number;
|
|
11
|
+
showValue?: boolean;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Slider: React.FC<SliderProps> = ({
|
|
16
|
+
label,
|
|
17
|
+
id,
|
|
18
|
+
value = 0,
|
|
19
|
+
onChange,
|
|
20
|
+
min = 0,
|
|
21
|
+
max = 100,
|
|
22
|
+
step = 1,
|
|
23
|
+
showValue = true,
|
|
24
|
+
className = "",
|
|
25
|
+
}) => {
|
|
26
|
+
return (
|
|
27
|
+
<div className={`flex flex-col gap-2 ${className}`}>
|
|
28
|
+
<div className="flex justify-between items-center">
|
|
29
|
+
<label
|
|
30
|
+
htmlFor={id}
|
|
31
|
+
className="text-sm font-medium text-gray-700 dark:text-gray-200"
|
|
32
|
+
>
|
|
33
|
+
{label}
|
|
34
|
+
</label>
|
|
35
|
+
{showValue && (
|
|
36
|
+
<span className="text-sm font-semibold text-blue-600 dark:text-blue-400 min-w-12 text-right">
|
|
37
|
+
{value}
|
|
38
|
+
</span>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
<input
|
|
42
|
+
type="range"
|
|
43
|
+
id={id}
|
|
44
|
+
value={value}
|
|
45
|
+
onChange={(e) => onChange?.(Number(e.target.value))}
|
|
46
|
+
min={min}
|
|
47
|
+
max={max}
|
|
48
|
+
step={step}
|
|
49
|
+
className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600 dark:accent-blue-500"
|
|
50
|
+
/>
|
|
51
|
+
<div className="flex justify-between text-xs text-gray-500 dark:text-gray-400">
|
|
52
|
+
<span>{min}</span>
|
|
53
|
+
<span>{max}</span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|