@tanstack/cta-framework-react-cra 0.23.1 → 0.24.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 (35) hide show
  1. package/add-ons/clerk/package.json +1 -1
  2. package/add-ons/convex/assets/convex/schema.ts +7 -3
  3. package/add-ons/convex/assets/convex/todos.ts +43 -0
  4. package/add-ons/convex/assets/src/routes/demo.convex.tsx +157 -19
  5. package/add-ons/convex/package.json +2 -1
  6. package/add-ons/db/assets/src/routes/demo.db-chat-api.ts +2 -0
  7. package/add-ons/mcp/small-logo.svg +1 -0
  8. package/add-ons/storybook/assets/_dot_storybook/main.ts +17 -0
  9. package/add-ons/storybook/assets/_dot_storybook/preview.ts +15 -0
  10. package/add-ons/storybook/assets/src/components/storybook/button.stories.ts +67 -0
  11. package/add-ons/storybook/assets/src/components/storybook/button.tsx +50 -0
  12. package/add-ons/storybook/assets/src/components/storybook/dialog.stories.tsx +92 -0
  13. package/add-ons/storybook/assets/src/components/storybook/dialog.tsx +33 -0
  14. package/add-ons/storybook/assets/src/components/storybook/index.ts +14 -0
  15. package/add-ons/storybook/assets/src/components/storybook/input.stories.ts +43 -0
  16. package/add-ons/storybook/assets/src/components/storybook/input.tsx +42 -0
  17. package/add-ons/storybook/assets/src/components/storybook/radio-group.stories.ts +53 -0
  18. package/add-ons/storybook/assets/src/components/storybook/radio-group.tsx +52 -0
  19. package/add-ons/storybook/assets/src/components/storybook/slider.stories.ts +55 -0
  20. package/add-ons/storybook/assets/src/components/storybook/slider.tsx +57 -0
  21. package/add-ons/storybook/assets/src/routes/demo.storybook.tsx +93 -0
  22. package/add-ons/storybook/info.json +16 -0
  23. package/add-ons/storybook/package.json +10 -0
  24. package/add-ons/storybook/small-logo.svg +1 -0
  25. package/package.json +2 -2
  26. package/project/base/package.json +1 -1
  27. package/tests/snapshots/react-cra/cr-js-form-npm.json +1 -1
  28. package/tests/snapshots/react-cra/cr-js-npm.json +1 -1
  29. package/tests/snapshots/react-cra/cr-ts-npm.json +1 -1
  30. package/tests/snapshots/react-cra/cr-ts-start-npm.json +1 -1
  31. package/tests/snapshots/react-cra/cr-ts-start-tanstack-query-npm.json +1 -1
  32. package/tests/snapshots/react-cra/fr-ts-biome-npm.json +1 -1
  33. package/tests/snapshots/react-cra/fr-ts-npm.json +1 -1
  34. package/tests/snapshots/react-cra/fr-ts-tw-npm.json +1 -1
  35. package/add-ons/convex/assets/convex/products.ts +0 -8
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "dependencies": {
3
- "@clerk/clerk-react": "^5.22.13"
3
+ "@clerk/clerk-react": "^5.49.0"
4
4
  }
5
5
  }
@@ -1,5 +1,5 @@
1
- import { defineSchema, defineTable } from "convex/server";
2
- import { v } from "convex/values";
1
+ import { defineSchema, defineTable } from 'convex/server'
2
+ import { v } from 'convex/values'
3
3
 
4
4
  export default defineSchema({
5
5
  products: defineTable({
@@ -7,4 +7,8 @@ export default defineSchema({
7
7
  imageId: v.string(),
8
8
  price: v.number(),
9
9
  }),
10
- });
10
+ todos: defineTable({
11
+ text: v.string(),
12
+ completed: v.boolean(),
13
+ }),
14
+ })
@@ -0,0 +1,43 @@
1
+ import { mutation, query } from './_generated/server'
2
+ import { v } from 'convex/values'
3
+
4
+ export const list = query({
5
+ args: {},
6
+ handler: async (ctx) => {
7
+ return await ctx.db
8
+ .query('todos')
9
+ .withIndex('by_creation_time')
10
+ .order('desc')
11
+ .collect()
12
+ },
13
+ })
14
+
15
+ export const add = mutation({
16
+ args: { text: v.string() },
17
+ handler: async (ctx, args) => {
18
+ return await ctx.db.insert('todos', {
19
+ text: args.text,
20
+ completed: false,
21
+ })
22
+ },
23
+ })
24
+
25
+ export const toggle = mutation({
26
+ args: { id: v.id('todos') },
27
+ handler: async (ctx, args) => {
28
+ const todo = await ctx.db.get(args.id)
29
+ if (!todo) {
30
+ throw new Error('Todo not found')
31
+ }
32
+ return await ctx.db.patch(args.id, {
33
+ completed: !todo.completed,
34
+ })
35
+ },
36
+ })
37
+
38
+ export const remove = mutation({
39
+ args: { id: v.id('todos') },
40
+ handler: async (ctx, args) => {
41
+ return await ctx.db.delete(args.id)
42
+ },
43
+ })
@@ -1,33 +1,171 @@
1
- import { Suspense } from 'react'
1
+ import { useCallback, useState } from 'react'
2
2
  import { createFileRoute } from '@tanstack/react-router'
3
- import { useQuery } from 'convex/react'
3
+ import { useQuery, useMutation } from 'convex/react'
4
+ import { Trash2, Plus, Check, Circle } from 'lucide-react'
4
5
 
5
6
  import { api } from '../../convex/_generated/api'
6
7
 
7
8
  export const Route = createFileRoute('/demo/convex')({
8
- component: App,
9
+ ssr: false,
10
+ component: ConvexTodos,
9
11
  })
10
12
 
11
- function Products() {
12
- const products = useQuery(api.products.get)
13
+ function ConvexTodos() {
14
+ const todos = useQuery(api.todos.list)
15
+ const addTodo = useMutation(api.todos.add)
16
+ const toggleTodo = useMutation(api.todos.toggle)
17
+ const removeTodo = useMutation(api.todos.remove)
13
18
 
14
- return (
15
- <ul>
16
- {(products || []).map((p) => (
17
- <li key={p._id}>
18
- {p.title} - {p.price}
19
- </li>
20
- ))}
21
- </ul>
19
+ const [newTodo, setNewTodo] = useState('')
20
+
21
+ const handleAddTodo = useCallback(async () => {
22
+ if (newTodo.trim()) {
23
+ await addTodo({ text: newTodo.trim() })
24
+ setNewTodo('')
25
+ }
26
+ }, [addTodo, newTodo])
27
+
28
+ const handleToggleTodo = useCallback(
29
+ async (id: string) => {
30
+ await toggleTodo({ id })
31
+ },
32
+ [toggleTodo],
33
+ )
34
+
35
+ const handleRemoveTodo = useCallback(
36
+ async (id: string) => {
37
+ await removeTodo({ id })
38
+ },
39
+ [removeTodo],
22
40
  )
23
- }
24
41
 
25
- function App() {
42
+ const completedCount = todos?.filter((todo) => todo.completed).length || 0
43
+ const totalCount = todos?.length || 0
44
+
26
45
  return (
27
- <div className="p-4">
28
- <Suspense fallback={<div>Loading...</div>}>
29
- <Products />
30
- </Suspense>
46
+ <div
47
+ className="min-h-screen flex items-center justify-center p-4"
48
+ style={{
49
+ background:
50
+ 'linear-gradient(135deg, #667a56 0%, #8fbc8f 25%, #90ee90 50%, #98fb98 75%, #f0fff0 100%)',
51
+ }}
52
+ >
53
+ <div className="w-full max-w-2xl">
54
+ {/* Header Card */}
55
+ <div className="bg-white/95 backdrop-blur-sm rounded-2xl shadow-2xl border border-green-200/50 p-8 mb-6">
56
+ <div className="text-center">
57
+ <h1 className="text-4xl font-bold text-green-800 mb-2">
58
+ Convex Todos
59
+ </h1>
60
+ <p className="text-green-600 text-lg">Powered by real-time sync</p>
61
+ {totalCount > 0 && (
62
+ <div className="mt-4 flex justify-center space-x-6 text-sm">
63
+ <span className="text-green-700 font-medium">
64
+ {completedCount} completed
65
+ </span>
66
+ <span className="text-gray-600">
67
+ {totalCount - completedCount} remaining
68
+ </span>
69
+ </div>
70
+ )}
71
+ </div>
72
+ </div>
73
+
74
+ {/* Add Todo Card */}
75
+ <div className="bg-white/95 backdrop-blur-sm rounded-2xl shadow-xl border border-green-200/50 p-6 mb-6">
76
+ <div className="flex gap-3">
77
+ <input
78
+ type="text"
79
+ value={newTodo}
80
+ onChange={(e) => setNewTodo(e.target.value)}
81
+ onKeyDown={(e) => {
82
+ if (e.key === 'Enter') {
83
+ handleAddTodo()
84
+ }
85
+ }}
86
+ placeholder="What needs to be done?"
87
+ className="flex-1 px-4 py-3 rounded-xl border-2 border-green-200 focus:border-green-400 focus:outline-none text-gray-800 placeholder-gray-500 bg-white/80 transition-colors"
88
+ />
89
+ <button
90
+ onClick={handleAddTodo}
91
+ disabled={!newTodo.trim()}
92
+ className="bg-gradient-to-r from-green-500 to-green-600 hover:from-green-600 hover:to-green-700 disabled:from-gray-300 disabled:to-gray-400 disabled:cursor-not-allowed text-white font-semibold py-3 px-6 rounded-xl transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl"
93
+ >
94
+ <Plus size={20} />
95
+ Add
96
+ </button>
97
+ </div>
98
+ </div>
99
+
100
+ {/* Todos List */}
101
+ <div className="bg-white/95 backdrop-blur-sm rounded-2xl shadow-xl border border-green-200/50 overflow-hidden">
102
+ {!todos ? (
103
+ <div className="p-8 text-center">
104
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-500 mx-auto mb-4"></div>
105
+ <p className="text-green-600">Loading todos...</p>
106
+ </div>
107
+ ) : todos.length === 0 ? (
108
+ <div className="p-12 text-center">
109
+ <Circle size={48} className="text-green-300 mx-auto mb-4" />
110
+ <h3 className="text-xl font-semibold text-green-800 mb-2">
111
+ No todos yet
112
+ </h3>
113
+ <p className="text-green-600">
114
+ Add your first todo above to get started!
115
+ </p>
116
+ </div>
117
+ ) : (
118
+ <div className="divide-y divide-green-100">
119
+ {todos.map((todo, index) => (
120
+ <div
121
+ key={todo._id}
122
+ className={`p-4 flex items-center gap-4 hover:bg-green-50/50 transition-colors ${
123
+ todo.completed ? 'opacity-75' : ''
124
+ }`}
125
+ style={{
126
+ animationDelay: `${index * 50}ms`,
127
+ }}
128
+ >
129
+ <button
130
+ onClick={() => handleToggleTodo(todo._id)}
131
+ className={`flex-shrink-0 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all duration-200 ${
132
+ todo.completed
133
+ ? 'bg-green-500 border-green-500 text-white'
134
+ : 'border-green-300 hover:border-green-400 text-transparent hover:text-green-400'
135
+ }`}
136
+ >
137
+ <Check size={14} />
138
+ </button>
139
+
140
+ <span
141
+ className={`flex-1 text-lg transition-all duration-200 ${
142
+ todo.completed
143
+ ? 'line-through text-gray-500'
144
+ : 'text-gray-800'
145
+ }`}
146
+ >
147
+ {todo.text}
148
+ </span>
149
+
150
+ <button
151
+ onClick={() => handleRemoveTodo(todo._id)}
152
+ className="flex-shrink-0 p-2 text-red-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
153
+ >
154
+ <Trash2 size={18} />
155
+ </button>
156
+ </div>
157
+ ))}
158
+ </div>
159
+ )}
160
+ </div>
161
+
162
+ {/* Footer */}
163
+ <div className="text-center mt-6">
164
+ <p className="text-green-700/80 text-sm">
165
+ Built with Convex • Real-time updates • Always in sync
166
+ </p>
167
+ </div>
168
+ </div>
31
169
  </div>
32
170
  )
33
171
  }
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "dependencies": {
3
3
  "@convex-dev/react-query": "0.0.0-alpha.8",
4
- "convex": "^1.19.2"
4
+ "convex": "^1.27.3",
5
+ "lucide-react": "^0.544.0"
5
6
  }
6
7
  }
@@ -1,4 +1,5 @@
1
1
  import { createFileRoute } from '@tanstack/react-router'
2
+ import { json } from '@tanstack/react-start'
2
3
 
3
4
  import { createCollection, localOnlyCollectionOptions } from '@tanstack/db'
4
5
  import { z } from 'zod'
@@ -72,6 +73,7 @@ export const Route = createFileRoute('/demo/db-chat-api')({
72
73
  return new Response(message.error.message, { status: 400 })
73
74
  }
74
75
  sendMessage(message.data)
76
+ return json(message.data)
75
77
  },
76
78
  },
77
79
  },
@@ -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
+ };