notionsoft-ui 1.0.29 → 1.0.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notionsoft-ui",
3
- "version": "1.0.29",
3
+ "version": "1.0.32",
4
4
  "description": "A React UI component installer (shadcn-style). Installs components directly into your project.",
5
5
  "bin": {
6
6
  "notionsoft-ui": "./cli/index.cjs"
@@ -0,0 +1,3 @@
1
+ import MultiTabInput from "./multi-tab-input";
2
+
3
+ export default MultiTabInput;
@@ -0,0 +1,190 @@
1
+ import React, { type ReactElement, useState, useMemo } from "react";
2
+ import { cn } from "../../utils/cn";
3
+ import AnimatedItem from "../animated-item";
4
+ import type { TabState } from "../tab/tab";
5
+ import Input, { NastranInputSize } from "../input/input";
6
+ import { OptionalTabs, Tab } from "../tab/tab";
7
+
8
+ export interface MultiTabInputProps
9
+ extends React.InputHTMLAttributes<HTMLInputElement> {
10
+ children:
11
+ | ReactElement<typeof Tab>
12
+ | ReactElement<typeof Tab>[]
13
+ | ReactElement<typeof OptionalTabs>;
14
+ tabData: Record<string, string>;
15
+ errorData?: Map<string, string>;
16
+ onTabChanged?: (key: string, data: string, optional?: boolean) => void;
17
+ onChanged: (value: string, name: string) => void;
18
+ placeholder?: string;
19
+ label?: string;
20
+ name: string;
21
+ classNames?: {
22
+ tabsDivClassName?: string;
23
+ rootDivClassName?: string;
24
+ };
25
+ measurement?: NastranInputSize;
26
+ }
27
+
28
+ const MultiTabInput = React.forwardRef<HTMLInputElement, MultiTabInputProps>(
29
+ (props, ref) => {
30
+ const {
31
+ className,
32
+ name,
33
+ classNames,
34
+ children,
35
+ tabData,
36
+ errorData,
37
+ onTabChanged,
38
+ onChanged,
39
+ placeholder,
40
+ label,
41
+ measurement,
42
+ ...rest
43
+ } = props;
44
+ const { tabsDivClassName, rootDivClassName } = classNames || {};
45
+ // Separate mandatory and optional tabs (memoized)
46
+ const { mandatoryTabs, optionalTabs } = useMemo(() => {
47
+ const mandatory: React.ReactElement<any>[] = [];
48
+ const optional: React.ReactElement<any>[] = [];
49
+
50
+ React.Children.forEach(children, (child) => {
51
+ if (!React.isValidElement(child)) return;
52
+
53
+ // Type-safe element access
54
+ const element = child as React.ReactElement<any>;
55
+ const typeName = (element.type as any)?.displayName;
56
+
57
+ if (typeName === "OptionalTabs") {
58
+ React.Children.forEach(element.props.children, (c) => {
59
+ if (React.isValidElement(c))
60
+ optional.push(c as React.ReactElement<any>);
61
+ });
62
+ } else if (typeName === "Tab") {
63
+ mandatory.push(element);
64
+ }
65
+ });
66
+
67
+ return { mandatoryTabs: mandatory, optionalTabs: optional };
68
+ }, [children]);
69
+
70
+ // Initialize state
71
+ const [tabState, setTabState] = useState({
72
+ active: mandatoryTabs[0]?.props.children || "",
73
+ mandatory: mandatoryTabs[0]?.props.children || "",
74
+ optional: optionalTabs[0]?.props.children || "",
75
+ });
76
+
77
+ const selectionName = `${name}_selections`;
78
+
79
+ const handleTabChange = (tabName: string, optional = false) => {
80
+ setTabState((prev) => ({
81
+ ...prev,
82
+ active: tabName,
83
+ [optional ? "optional" : "mandatory"]: tabName,
84
+ }));
85
+ onTabChanged?.(selectionName, tabName, optional);
86
+ };
87
+
88
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
89
+ onChanged(e.target.value, e.target.name);
90
+ };
91
+
92
+ const generateUniqueName = (base: string, key: string) => `${base}_${key}`;
93
+
94
+ const getTabState = (tabName: string, optional = false): TabState => {
95
+ if (tabName === tabState.active) return "active";
96
+ if (optional)
97
+ return tabName === tabState.optional ? "selected" : "unselected";
98
+ return tabName === tabState.mandatory ? "selected" : "unselected";
99
+ };
100
+
101
+ const renderTabs = (tabs: React.ReactElement<any>[], optional = false) =>
102
+ tabs.map((tab, idx) => {
103
+ const tabName = tab.props.children;
104
+ const state: TabState = getTabState(tabName, optional);
105
+
106
+ return React.cloneElement(tab, {
107
+ key: `${optional ? "opt" : "mand"}-${idx}`,
108
+ state,
109
+ optional,
110
+ onClick: () => handleTabChange(tabName, optional),
111
+ className: tab.props.className,
112
+ });
113
+ });
114
+
115
+ const activeTabName = generateUniqueName(name, tabState.active);
116
+ const selectTabValue = tabData[activeTabName] || "";
117
+ const errorMessages = errorData?.get(activeTabName)
118
+ ? [errorData.get(activeTabName)!]
119
+ : [];
120
+
121
+ const direction =
122
+ activeTabName.endsWith("farsi") || activeTabName.endsWith("pashto")
123
+ ? "rtl"
124
+ : "ltr";
125
+ return (
126
+ <div className={cn("flex flex-col select-none", rootDivClassName)}>
127
+ <div className="flex flex-col-reverse sm:flex-row sm:justify-between sm:items-end gap-4">
128
+ {label && (
129
+ <h1 className="font-semibold relative top-1 rtl:text-lg ltr:text-[13px] text-start">
130
+ {label}
131
+ </h1>
132
+ )}
133
+
134
+ <div
135
+ className={cn(
136
+ "flex flex-wrap gap-2 items-center",
137
+ tabsDivClassName
138
+ )}
139
+ >
140
+ {renderTabs(mandatoryTabs)}
141
+ {optionalTabs.length > 0 && (
142
+ <>
143
+ <span className="bg-primary/30 sm:w-px w-full h-px sm:my-0 sm:mx-2 sm:h-4" />
144
+ {renderTabs(optionalTabs, true)}
145
+ </>
146
+ )}
147
+ </div>
148
+ </div>
149
+
150
+ <Input
151
+ measurement={measurement}
152
+ dir={direction}
153
+ {...rest}
154
+ ref={ref}
155
+ name={activeTabName}
156
+ value={selectTabValue}
157
+ placeholder={placeholder}
158
+ onChange={handleInputChange}
159
+ className={cn(
160
+ `mt-2 ${errorMessages.length > 0 && "border-red-400 border-b!"}`,
161
+ className
162
+ )}
163
+ />
164
+
165
+ {errorMessages.map((error: string, index) => (
166
+ <AnimatedItem
167
+ key={index}
168
+ springProps={{
169
+ from: { opacity: 0, transform: "translateY(-8px)" },
170
+ to: {
171
+ opacity: 1,
172
+ transform: "translateY(0px)",
173
+ delay: index * 100,
174
+ },
175
+ config: { mass: 1, tension: 210, friction: 20 },
176
+ delay: index * 100,
177
+ }}
178
+ intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
179
+ >
180
+ <h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-[11px]">
181
+ {error}
182
+ </h1>
183
+ </AnimatedItem>
184
+ ))}
185
+ </div>
186
+ );
187
+ }
188
+ );
189
+
190
+ export default MultiTabInput;
@@ -0,0 +1,290 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+
4
+ import { OptionalTabs, Tab } from "../tab/tab";
5
+ import MultiTabInput from "./multi-tab-input";
6
+
7
+ const meta: Meta<typeof MultiTabInput> = {
8
+ title: "Form/MultiTabInput",
9
+ component: MultiTabInput,
10
+ parameters: {
11
+ layout: "centered",
12
+ docs: {
13
+ description: {
14
+ component:
15
+ "An input component with tabbed sections for managing multiple related text inputs. Supports mandatory and optional tabs with automatic RTL/LTR text direction detection.",
16
+ },
17
+ },
18
+ },
19
+ argTypes: {
20
+ onChanged: { action: "changed" },
21
+ onTabChanged: { action: "tab changed" },
22
+ },
23
+ };
24
+
25
+ export default meta;
26
+
27
+ type Story = StoryObj<typeof MultiTabInput>;
28
+
29
+ export const Default: Story = {
30
+ render: (args) => {
31
+ const [tabData, setTabData] = useState<Record<string, string>>({
32
+ title_english: "Sample product title",
33
+ title_spanish: "Título de producto de ejemplo",
34
+ title_french: "",
35
+ });
36
+
37
+ const [errors] = useState<Map<string, string>>(
38
+ new Map([["title_french", "French title is required"]])
39
+ );
40
+
41
+ return (
42
+ <div className="w-[400px]">
43
+ <MultiTabInput
44
+ {...args}
45
+ name="title"
46
+ label="Product Title"
47
+ placeholder="Enter title..."
48
+ tabData={tabData}
49
+ errorData={errors}
50
+ onChanged={(value, name) =>
51
+ setTabData((prev) => ({ ...prev, [name]: value }))
52
+ }
53
+ >
54
+ <Tab>english</Tab>
55
+ <Tab>spanish</Tab>
56
+
57
+ <OptionalTabs>
58
+ <Tab>french</Tab>
59
+ </OptionalTabs>
60
+ </MultiTabInput>
61
+ </div>
62
+ );
63
+ },
64
+ };
65
+
66
+ export const WithoutOptionalTabs: Story = {
67
+ render: () => {
68
+ const [tabData, setTabData] = useState<Record<string, string>>({
69
+ username_english: "john_doe",
70
+ username_german: "johndoe_de",
71
+ });
72
+
73
+ return (
74
+ <div className="w-[400px]">
75
+ <MultiTabInput
76
+ name="username"
77
+ label="Username"
78
+ placeholder="Enter username"
79
+ tabData={tabData}
80
+ onChanged={(value, name) =>
81
+ setTabData((prev) => ({ ...prev, [name]: value }))
82
+ }
83
+ >
84
+ <Tab>english</Tab>
85
+ <Tab>german</Tab>
86
+ </MultiTabInput>
87
+ </div>
88
+ );
89
+ },
90
+ };
91
+
92
+ export const WithRTLTabs: Story = {
93
+ render: () => {
94
+ const [tabData, setTabData] = useState<Record<string, string>>({
95
+ name_english: "International Company",
96
+ name_arabic: "شركة دولية",
97
+ name_farsi: "شرکت بین‌المللی",
98
+ });
99
+
100
+ const [errors] = useState<Map<string, string>>(
101
+ new Map([
102
+ ["name_arabic", "Arabic name is required"],
103
+ ["name_farsi", "نام فارسی الزامی است"],
104
+ ])
105
+ );
106
+
107
+ return (
108
+ <div className="w-[400px]">
109
+ <MultiTabInput
110
+ name="name"
111
+ label="Company Name"
112
+ placeholder="Enter company name"
113
+ tabData={tabData}
114
+ errorData={errors}
115
+ onChanged={(value, name) =>
116
+ setTabData((prev) => ({ ...prev, [name]: value }))
117
+ }
118
+ >
119
+ <Tab>english</Tab>
120
+
121
+ <OptionalTabs>
122
+ <Tab>arabic</Tab>
123
+ <Tab>farsi</Tab>
124
+ <Tab>pashto</Tab>
125
+ </OptionalTabs>
126
+ </MultiTabInput>
127
+ </div>
128
+ );
129
+ },
130
+ };
131
+
132
+ export const WithMultipleOptionalTabs: Story = {
133
+ render: () => {
134
+ const [tabData, setTabData] = useState<Record<string, string>>({
135
+ sku_basic: "SKU-001",
136
+ sku_internal: "INT-98432",
137
+ sku_vendor: "VND-7781",
138
+ sku_seo: "premium-product-sku",
139
+ });
140
+
141
+ return (
142
+ <div className="w-[400px]">
143
+ <MultiTabInput
144
+ name="sku"
145
+ label="Product SKU"
146
+ placeholder="Enter SKU"
147
+ tabData={tabData}
148
+ onChanged={(value, name) =>
149
+ setTabData((prev) => ({ ...prev, [name]: value }))
150
+ }
151
+ classNames={{
152
+ tabsDivClassName: "bg-gray-50 p-2 rounded-lg",
153
+ rootDivClassName: "p-4 border rounded-lg",
154
+ }}
155
+ >
156
+ <Tab>basic</Tab>
157
+ <Tab>internal</Tab>
158
+
159
+ <OptionalTabs>
160
+ <Tab>vendor</Tab>
161
+ <Tab>seo</Tab>
162
+ </OptionalTabs>
163
+ </MultiTabInput>
164
+ </div>
165
+ );
166
+ },
167
+ };
168
+
169
+ export const WithErrorStates: Story = {
170
+ render: () => {
171
+ const [tabData, setTabData] = useState<Record<string, string>>({
172
+ email_english: "test@",
173
+ email_spanish: "correo@",
174
+ email_french: "email@",
175
+ });
176
+
177
+ const [errors] = useState<Map<string, string>>(
178
+ new Map([
179
+ ["email_english", "Invalid email address"],
180
+ ["email_spanish", "Correo electrónico inválido"],
181
+ ["email_french", "Adresse e-mail invalide"],
182
+ ])
183
+ );
184
+
185
+ return (
186
+ <div className="w-[400px]">
187
+ <MultiTabInput
188
+ name="email"
189
+ label="Contact Email"
190
+ placeholder="Enter email"
191
+ tabData={tabData}
192
+ errorData={errors}
193
+ onChanged={(value, name) =>
194
+ setTabData((prev) => ({ ...prev, [name]: value }))
195
+ }
196
+ >
197
+ <Tab>english</Tab>
198
+ <Tab>spanish</Tab>
199
+
200
+ <OptionalTabs>
201
+ <Tab>french</Tab>
202
+ </OptionalTabs>
203
+ </MultiTabInput>
204
+ </div>
205
+ );
206
+ },
207
+ };
208
+
209
+ export const DisabledState: Story = {
210
+ render: () => {
211
+ const [tabData] = useState<Record<string, string>>({
212
+ code_english: "READ_ONLY_001",
213
+ code_spanish: "SOLO_LECTURA_001",
214
+ });
215
+
216
+ return (
217
+ <div className="w-[400px]">
218
+ <MultiTabInput
219
+ name="code"
220
+ label="Reference Code"
221
+ placeholder="Read only"
222
+ tabData={tabData}
223
+ disabled
224
+ onChanged={() => {}}
225
+ >
226
+ <Tab>english</Tab>
227
+ <Tab>spanish</Tab>
228
+ </MultiTabInput>
229
+ </div>
230
+ );
231
+ },
232
+ };
233
+
234
+ export const InFormContext: Story = {
235
+ render: () => {
236
+ const [formData, setFormData] = useState({
237
+ price: "",
238
+ });
239
+
240
+ const [tabData, setTabData] = useState<Record<string, string>>({
241
+ title_english: "",
242
+ title_spanish: "",
243
+ });
244
+
245
+ const handleSubmit = (e: React.FormEvent) => {
246
+ e.preventDefault();
247
+ console.log({
248
+ ...formData,
249
+ ...tabData,
250
+ });
251
+ };
252
+
253
+ return (
254
+ <form onSubmit={handleSubmit} className="w-[400px] space-y-4">
255
+ <div>
256
+ <label className="block text-sm font-medium mb-1">Price</label>
257
+ <input
258
+ type="number"
259
+ className="w-full px-3 py-2 border rounded-md"
260
+ value={formData.price}
261
+ onChange={(e) =>
262
+ setFormData((prev) => ({ ...prev, price: e.target.value }))
263
+ }
264
+ placeholder="Enter price"
265
+ />
266
+ </div>
267
+
268
+ <MultiTabInput
269
+ name="title"
270
+ label="Product Title"
271
+ placeholder="Enter title"
272
+ tabData={tabData}
273
+ onChanged={(value, name) =>
274
+ setTabData((prev) => ({ ...prev, [name]: value }))
275
+ }
276
+ >
277
+ <Tab>english</Tab>
278
+ <Tab>spanish</Tab>
279
+ </MultiTabInput>
280
+
281
+ <button
282
+ type="submit"
283
+ className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
284
+ >
285
+ Save Product
286
+ </button>
287
+ </form>
288
+ );
289
+ },
290
+ };
@@ -0,0 +1,3 @@
1
+ import MultiTabTextarea from "./multi-tab-textarea";
2
+
3
+ export default MultiTabTextarea;
@@ -0,0 +1,184 @@
1
+ import React, { type ReactElement, useState, useMemo } from "react";
2
+ import { cn } from "../../utils/cn";
3
+ import Textarea from "../textarea";
4
+ import AnimatedItem from "../animated-item";
5
+ import type { OptionalTabs, Tab, TabState } from "../tab/tab";
6
+
7
+ export interface MultiTabTextareaProps
8
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
9
+ children:
10
+ | ReactElement<typeof Tab>
11
+ | ReactElement<typeof Tab>[]
12
+ | ReactElement<typeof OptionalTabs>;
13
+ tabData: Record<string, string>;
14
+ errorData?: Map<string, string>;
15
+ onTabChanged?: (key: string, data: string, optional?: boolean) => void;
16
+ onChanged: (value: string, name: string) => void;
17
+ placeholder?: string;
18
+ label?: string;
19
+ name: string;
20
+ classNames?: {
21
+ tabsDivClassName?: string;
22
+ rootDivClassName?: string;
23
+ };
24
+ }
25
+
26
+ const MultiTabTextarea = React.forwardRef<
27
+ HTMLTextAreaElement,
28
+ MultiTabTextareaProps
29
+ >((props, ref) => {
30
+ const {
31
+ className,
32
+ name,
33
+ classNames,
34
+ children,
35
+ tabData,
36
+ errorData,
37
+ onTabChanged,
38
+ onChanged,
39
+ placeholder,
40
+ label,
41
+ ...rest
42
+ } = props;
43
+ const { tabsDivClassName, rootDivClassName } = classNames || {};
44
+ // Separate mandatory and optional tabs (memoized)
45
+ const { mandatoryTabs, optionalTabs } = useMemo(() => {
46
+ const mandatory: React.ReactElement<any>[] = [];
47
+ const optional: React.ReactElement<any>[] = [];
48
+
49
+ React.Children.forEach(children, (child) => {
50
+ if (!React.isValidElement(child)) return;
51
+
52
+ // Type-safe element access
53
+ const element = child as React.ReactElement<any>;
54
+ const typeName = (element.type as any)?.displayName;
55
+
56
+ if (typeName === "OptionalTabs") {
57
+ React.Children.forEach(element.props.children, (c) => {
58
+ if (React.isValidElement(c))
59
+ optional.push(c as React.ReactElement<any>);
60
+ });
61
+ } else if (typeName === "Tab") {
62
+ mandatory.push(element);
63
+ }
64
+ });
65
+
66
+ return { mandatoryTabs: mandatory, optionalTabs: optional };
67
+ }, [children]);
68
+
69
+ // Initialize state
70
+ const [tabState, setTabState] = useState({
71
+ active: mandatoryTabs[0]?.props.children || "",
72
+ mandatory: mandatoryTabs[0]?.props.children || "",
73
+ optional: optionalTabs[0]?.props.children || "",
74
+ });
75
+
76
+ const selectionName = `${name}_selections`;
77
+
78
+ const handleTabChange = (tabName: string, optional = false) => {
79
+ setTabState((prev) => ({
80
+ ...prev,
81
+ active: tabName,
82
+ [optional ? "optional" : "mandatory"]: tabName,
83
+ }));
84
+ onTabChanged?.(selectionName, tabName, optional);
85
+ };
86
+
87
+ const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
88
+ onChanged(e.target.value, e.target.name);
89
+ };
90
+
91
+ const generateUniqueName = (base: string, key: string) => `${base}_${key}`;
92
+
93
+ const getTabState = (tabName: string, optional = false): TabState => {
94
+ if (tabName === tabState.active) return "active";
95
+ if (optional)
96
+ return tabName === tabState.optional ? "selected" : "unselected";
97
+ return tabName === tabState.mandatory ? "selected" : "unselected";
98
+ };
99
+
100
+ const renderTabs = (tabs: React.ReactElement<any>[], optional = false) =>
101
+ tabs.map((tab, idx) => {
102
+ const tabName = tab.props.children;
103
+ const state: TabState = getTabState(tabName, optional);
104
+
105
+ return React.cloneElement(tab, {
106
+ key: `${optional ? "opt" : "mand"}-${idx}`,
107
+ state,
108
+ optional,
109
+ onClick: () => handleTabChange(tabName, optional),
110
+ className: tab.props.className,
111
+ });
112
+ });
113
+
114
+ const activeTabName = generateUniqueName(name, tabState.active);
115
+ const selectTabValue = tabData[activeTabName] || "";
116
+ const errorMessages = errorData?.get(activeTabName)
117
+ ? [errorData.get(activeTabName)!]
118
+ : [];
119
+
120
+ const direction =
121
+ activeTabName.endsWith("farsi") || activeTabName.endsWith("pashto")
122
+ ? "rtl"
123
+ : "ltr";
124
+ return (
125
+ <div className={cn("flex flex-col select-none", rootDivClassName)}>
126
+ <div className="flex flex-col-reverse sm:flex-row sm:justify-between sm:items-end gap-4">
127
+ {label && (
128
+ <h1 className="font-semibold relative top-1 rtl:text-lg ltr:text-[13px] text-start">
129
+ {label}
130
+ </h1>
131
+ )}
132
+
133
+ <div
134
+ className={cn("flex flex-wrap gap-2 items-center", tabsDivClassName)}
135
+ >
136
+ {renderTabs(mandatoryTabs)}
137
+ {optionalTabs.length > 0 && (
138
+ <>
139
+ <span className="bg-primary/30 sm:w-px w-full h-px sm:my-0 sm:mx-2 sm:h-4" />
140
+ {renderTabs(optionalTabs, true)}
141
+ </>
142
+ )}
143
+ </div>
144
+ </div>
145
+
146
+ <Textarea
147
+ dir={direction}
148
+ {...rest}
149
+ ref={ref}
150
+ name={activeTabName}
151
+ value={selectTabValue}
152
+ placeholder={placeholder}
153
+ onChange={handleInputChange}
154
+ className={cn(
155
+ `mt-2 ${errorMessages.length > 0 ? "border-red-400 border-b!" : ""}`,
156
+ className
157
+ )}
158
+ />
159
+
160
+ {errorMessages.map((error: string, index) => (
161
+ <AnimatedItem
162
+ key={index}
163
+ springProps={{
164
+ from: { opacity: 0, transform: "translateY(-8px)" },
165
+ to: {
166
+ opacity: 1,
167
+ transform: "translateY(0px)",
168
+ delay: index * 100,
169
+ },
170
+ config: { mass: 1, tension: 210, friction: 20 },
171
+ delay: index * 100,
172
+ }}
173
+ intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
174
+ >
175
+ <h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-[11px]">
176
+ {error}
177
+ </h1>
178
+ </AnimatedItem>
179
+ ))}
180
+ </div>
181
+ );
182
+ });
183
+
184
+ export default MultiTabTextarea;
@@ -0,0 +1,340 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+
4
+ import { OptionalTabs, Tab } from "../tab/tab";
5
+ import MultiTabTextarea from "../multi-tab-textarea";
6
+
7
+ const meta: Meta<typeof MultiTabTextarea> = {
8
+ title: "Form/MultiTabTextarea",
9
+ component: MultiTabTextarea,
10
+ parameters: {
11
+ layout: "centered",
12
+ docs: {
13
+ description: {
14
+ component:
15
+ "A textarea component with tabbed sections for managing multiple related text inputs. Supports mandatory and optional tabs with automatic RTL/LTR text direction detection.",
16
+ },
17
+ },
18
+ },
19
+ argTypes: {
20
+ onChanged: { action: "changed" },
21
+ onTabChanged: { action: "tab changed" },
22
+ },
23
+ };
24
+
25
+ export default meta;
26
+
27
+ type Story = StoryObj<typeof MultiTabTextarea>;
28
+
29
+ export const Default: Story = {
30
+ render: (args) => {
31
+ const [tabData, setTabData] = useState<Record<string, string>>({
32
+ description_en: "This is a sample product description.",
33
+ description_es: "Esta es una descripción de producto de muestra.",
34
+ description_fr: "",
35
+ });
36
+
37
+ const [errors, setErrors] = useState<Map<string, string>>(
38
+ new Map([["description_fr", "French description is required"]])
39
+ );
40
+
41
+ const handleChange = (value: string, name: string) => {
42
+ setTabData((prev) => ({
43
+ ...prev,
44
+ [name]: value,
45
+ }));
46
+ };
47
+
48
+ const handleTabChanged = (
49
+ key: string,
50
+ value: string,
51
+ optional?: boolean
52
+ ) => {
53
+ console.log("Tab changed:", { key, value, optional });
54
+ };
55
+
56
+ return (
57
+ <div className="w-[500px]">
58
+ <MultiTabTextarea
59
+ {...args}
60
+ name="description"
61
+ label="Product Description"
62
+ placeholder="Enter your description here..."
63
+ tabData={tabData}
64
+ errorData={errors}
65
+ onChanged={handleChange}
66
+ onTabChanged={handleTabChanged}
67
+ >
68
+ {/* Mandatory tabs */}
69
+ <Tab>english</Tab>
70
+ <Tab>spanish</Tab>
71
+
72
+ {/* Optional tabs */}
73
+ <OptionalTabs>
74
+ <Tab>french</Tab>
75
+ </OptionalTabs>
76
+ </MultiTabTextarea>
77
+ </div>
78
+ );
79
+ },
80
+ };
81
+
82
+ export const WithoutOptionalTabs: Story = {
83
+ render: () => {
84
+ const [tabData, setTabData] = useState<Record<string, string>>({
85
+ bio_en: "I'm a software developer with 5 years of experience.",
86
+ bio_de: "Ich bin Softwareentwickler mit 5 Jahren Erfahrung.",
87
+ });
88
+
89
+ return (
90
+ <div className="w-[500px]">
91
+ <MultiTabTextarea
92
+ name="bio"
93
+ label="Biography"
94
+ placeholder="Tell us about yourself..."
95
+ tabData={tabData}
96
+ onChanged={(value, name) =>
97
+ setTabData((prev) => ({ ...prev, [name]: value }))
98
+ }
99
+ >
100
+ <Tab>english</Tab>
101
+ <Tab>german</Tab>
102
+ </MultiTabTextarea>
103
+ </div>
104
+ );
105
+ },
106
+ };
107
+
108
+ export const WithRTLTabs: Story = {
109
+ render: () => {
110
+ const [tabData, setTabData] = useState<Record<string, string>>({
111
+ content_en: "Welcome to our international platform.",
112
+ content_ar: "مرحبًا بكم في منصتنا الدولية.",
113
+ content_fa: "به پلتفرم بین‌المللی ما خوش آمدید.",
114
+ });
115
+
116
+ const [errors, setErrors] = useState<Map<string, string>>(
117
+ new Map([
118
+ ["content_ar", "Arabic content must be at least 20 characters"],
119
+ ["content_fa", "فارسی باید حداقل 20 کاراکتر باشد"],
120
+ ])
121
+ );
122
+
123
+ return (
124
+ <div className="w-[500px]">
125
+ <MultiTabTextarea
126
+ name="content"
127
+ label="Content"
128
+ placeholder="Type your content here..."
129
+ tabData={tabData}
130
+ errorData={errors}
131
+ onChanged={(value, name) =>
132
+ setTabData((prev) => ({ ...prev, [name]: value }))
133
+ }
134
+ >
135
+ <Tab>english</Tab>
136
+ <OptionalTabs>
137
+ <Tab>arabic</Tab>
138
+ <Tab>farsi</Tab>
139
+ <Tab>pashto</Tab>
140
+ </OptionalTabs>
141
+ </MultiTabTextarea>
142
+ </div>
143
+ );
144
+ },
145
+ };
146
+
147
+ export const WithMultipleOptionalTabs: Story = {
148
+ render: () => {
149
+ const [tabData, setTabData] = useState<Record<string, string>>({
150
+ product_details_basic:
151
+ "A premium quality product with excellent durability.",
152
+ product_details_advanced:
153
+ "Made from 100% recycled materials. Water-resistant up to 50m.",
154
+ product_details_technical:
155
+ "Material: Polycarbonate\nWeight: 150g\nDimensions: 10x5x2cm",
156
+ product_details_seo:
157
+ "Premium durable product for everyday use. Best value for money.",
158
+ });
159
+
160
+ return (
161
+ <div className="w-[500px]">
162
+ <MultiTabTextarea
163
+ name="product_details"
164
+ label="Product Details"
165
+ placeholder="Enter product information..."
166
+ tabData={tabData}
167
+ onChanged={(value, name) =>
168
+ setTabData((prev) => ({ ...prev, [name]: value }))
169
+ }
170
+ classNames={{
171
+ tabsDivClassName: "bg-gray-50 p-2 rounded-lg",
172
+ rootDivClassName: "p-4 border rounded-lg",
173
+ }}
174
+ >
175
+ <Tab>basic</Tab>
176
+ <Tab>advanced</Tab>
177
+ <OptionalTabs>
178
+ <Tab>technical</Tab>
179
+ <Tab>seo</Tab>
180
+ </OptionalTabs>
181
+ </MultiTabTextarea>
182
+ </div>
183
+ );
184
+ },
185
+ };
186
+
187
+ export const WithErrorStates: Story = {
188
+ render: () => {
189
+ const [tabData, setTabData] = useState<Record<string, string>>({
190
+ review_en: "This product is",
191
+ review_es: "Este producto es",
192
+ review_fr: "Ce produit est",
193
+ });
194
+
195
+ const [errors, setErrors] = useState<Map<string, string>>(
196
+ new Map([
197
+ ["review_en", "Review must be at least 20 characters"],
198
+ ["review_es", "La reseña debe tener al menos 20 caracteres"],
199
+ ["review_fr", "L'avis doit comporter au moins 20 caractères"],
200
+ ])
201
+ );
202
+
203
+ return (
204
+ <div className="w-[500px]">
205
+ <MultiTabTextarea
206
+ name="review"
207
+ label="Product Review"
208
+ placeholder="Write your review here..."
209
+ tabData={tabData}
210
+ errorData={errors}
211
+ onChanged={(value, name) =>
212
+ setTabData((prev) => ({ ...prev, [name]: value }))
213
+ }
214
+ >
215
+ <Tab>english</Tab>
216
+ <Tab>spanish</Tab>
217
+ <OptionalTabs>
218
+ <Tab>french</Tab>
219
+ </OptionalTabs>
220
+ </MultiTabTextarea>
221
+ </div>
222
+ );
223
+ },
224
+ };
225
+
226
+ export const DisabledState: Story = {
227
+ render: () => {
228
+ const [tabData] = useState<Record<string, string>>({
229
+ template_en:
230
+ "This is a read-only template.\nYou cannot edit this content.",
231
+ template_es:
232
+ "Esta es una plantilla de solo lectura.\nNo puedes editar este contenido.",
233
+ });
234
+
235
+ return (
236
+ <div className="w-[500px]">
237
+ <MultiTabTextarea
238
+ name="template"
239
+ label="Email Template"
240
+ placeholder="Cannot edit - this is read-only"
241
+ tabData={tabData}
242
+ disabled
243
+ onChanged={() => {}}
244
+ >
245
+ <Tab>english</Tab>
246
+ <Tab>spanish</Tab>
247
+ </MultiTabTextarea>
248
+ </div>
249
+ );
250
+ },
251
+ };
252
+
253
+ export const CustomPlaceholders: Story = {
254
+ render: () => {
255
+ const [tabData, setTabData] = useState<Record<string, string>>({
256
+ instructions_en: "",
257
+ instructions_ar: "",
258
+ instructions_fa: "",
259
+ });
260
+
261
+ return (
262
+ <div className="w-[500px]">
263
+ <MultiTabTextarea
264
+ name="instructions"
265
+ label="Instructions"
266
+ placeholder="Default placeholder..."
267
+ tabData={tabData}
268
+ onChanged={(value, name) =>
269
+ setTabData((prev) => ({ ...prev, [name]: value }))
270
+ }
271
+ >
272
+ <Tab>english</Tab>
273
+ <Tab>arabic</Tab>
274
+ <OptionalTabs>
275
+ <Tab>farsi</Tab>
276
+ </OptionalTabs>
277
+ </MultiTabTextarea>
278
+ </div>
279
+ );
280
+ },
281
+ };
282
+
283
+ export const InFormContext: Story = {
284
+ render: () => {
285
+ const [formData, setFormData] = useState({
286
+ name: "",
287
+ description: "",
288
+ });
289
+
290
+ const [tabData, setTabData] = useState<Record<string, string>>({
291
+ description_en: "",
292
+ description_es: "",
293
+ });
294
+
295
+ const handleSubmit = (e: React.FormEvent) => {
296
+ e.preventDefault();
297
+ console.log({
298
+ ...formData,
299
+ ...tabData,
300
+ });
301
+ };
302
+
303
+ return (
304
+ <form onSubmit={handleSubmit} className="w-[500px] space-y-4">
305
+ <div>
306
+ <label className="block text-sm font-medium mb-1">Product Name</label>
307
+ <input
308
+ type="text"
309
+ className="w-full px-3 py-2 border rounded-md"
310
+ value={formData.name}
311
+ onChange={(e) =>
312
+ setFormData((prev) => ({ ...prev, name: e.target.value }))
313
+ }
314
+ placeholder="Enter product name"
315
+ />
316
+ </div>
317
+
318
+ <MultiTabTextarea
319
+ name="description"
320
+ label="Product Description"
321
+ placeholder="Describe your product..."
322
+ tabData={tabData}
323
+ onChanged={(value, name) =>
324
+ setTabData((prev) => ({ ...prev, [name]: value }))
325
+ }
326
+ >
327
+ <Tab>english</Tab>
328
+ <Tab>spanish</Tab>
329
+ </MultiTabTextarea>
330
+
331
+ <button
332
+ type="submit"
333
+ className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
334
+ >
335
+ Save Product
336
+ </button>
337
+ </form>
338
+ );
339
+ },
340
+ };
@@ -383,6 +383,7 @@ const PhoneInput: React.FC<PhoneInputProps> = ({
383
383
  )}
384
384
  {...rest}
385
385
  disabled={readOnly}
386
+ dir="ltr"
386
387
  />
387
388
  </div>
388
389
 
@@ -0,0 +1,3 @@
1
+ import Tab from "./tab";
2
+
3
+ export default Tab;
@@ -0,0 +1,51 @@
1
+ import { cn } from "../../utils/cn";
2
+ import { Asterisk, CircleDot } from "lucide-react";
3
+
4
+ export type TabState = "active" | "selected" | "unselected";
5
+
6
+ interface TabProps {
7
+ children: React.ReactNode;
8
+ className?: string;
9
+ onClick?: () => void;
10
+ translation?: string;
11
+
12
+ state?: TabState; // <-- now strongly typed
13
+ optional?: boolean;
14
+ }
15
+ export function Tab({
16
+ children,
17
+ className,
18
+ onClick,
19
+ translation,
20
+ state = "unselected",
21
+ optional = false,
22
+ }: TabProps) {
23
+ // Icon for this tab
24
+ const Icon = optional ? CircleDot : Asterisk;
25
+
26
+ const baseClass = cn(
27
+ "capitalize transition px-3 py-1.5 ltr:text-xs rtl:text-[13px] sm:rtl:text-sm rtl:font-semibold px-2 rounded cursor-pointer shadow-md dark:shadow-none dark:border dark:hover:opacity-80 shadow-primary/20 hover:shadow-sm flex items-center gap-1",
28
+
29
+ state === "active"
30
+ ? "text-primary-foreground bg-tertiary"
31
+ : state === "selected"
32
+ ? "bg-primary/50 text-primary-foreground/90"
33
+ : "bg-primary/10 text-primary/50",
34
+
35
+ className
36
+ );
37
+
38
+ return (
39
+ <div onClick={onClick} className={baseClass}>
40
+ <Icon size={14} className="opacity-70 size-3 text-current" />
41
+ {translation ?? children}
42
+ </div>
43
+ );
44
+ }
45
+
46
+ Tab.displayName = "Tab";
47
+
48
+ export function OptionalTabs({ children }: { children: React.ReactNode }) {
49
+ return <>{children}</>;
50
+ }
51
+ OptionalTabs.displayName = "OptionalTabs";
@@ -14,7 +14,7 @@ export interface TextareaProps
14
14
  };
15
15
  }
16
16
 
17
- export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
17
+ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
18
18
  (
19
19
  {
20
20
  className,