notionsoft-ui 1.0.27 → 1.0.30

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.27",
3
+ "version": "1.0.30",
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,196 @@
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 Tab from "../tab/tab";
7
+
8
+ // OptionalTabs wrapper
9
+ export function OptionalTabs({ children }: { children: React.ReactNode }) {
10
+ return <>{children}</>;
11
+ }
12
+ OptionalTabs.displayName = "OptionalTabs";
13
+
14
+ export interface MultiTabInputProps
15
+ extends React.InputHTMLAttributes<HTMLInputElement> {
16
+ children:
17
+ | ReactElement<typeof Tab>
18
+ | ReactElement<typeof Tab>[]
19
+ | ReactElement<typeof OptionalTabs>;
20
+ tabData: Record<string, string>;
21
+ errorData?: Map<string, string>;
22
+ onTabChanged?: (key: string, data: string, optional?: boolean) => void;
23
+ onChanged: (value: string, name: string) => void;
24
+ placeholder?: string;
25
+ label?: string;
26
+ name: string;
27
+ classNames?: {
28
+ tabsDivClassName?: string;
29
+ rootDivClassName?: string;
30
+ };
31
+ measurement?: NastranInputSize;
32
+ }
33
+
34
+ const MultiTabInput = React.forwardRef<HTMLInputElement, MultiTabInputProps>(
35
+ (props, ref) => {
36
+ const {
37
+ className,
38
+ name,
39
+ classNames,
40
+ children,
41
+ tabData,
42
+ errorData,
43
+ onTabChanged,
44
+ onChanged,
45
+ placeholder,
46
+ label,
47
+ measurement,
48
+ ...rest
49
+ } = props;
50
+ const { tabsDivClassName, rootDivClassName } = classNames || {};
51
+ // Separate mandatory and optional tabs (memoized)
52
+ const { mandatoryTabs, optionalTabs } = useMemo(() => {
53
+ const mandatory: React.ReactElement<any>[] = [];
54
+ const optional: React.ReactElement<any>[] = [];
55
+
56
+ React.Children.forEach(children, (child) => {
57
+ if (!React.isValidElement(child)) return;
58
+
59
+ // Type-safe element access
60
+ const element = child as React.ReactElement<any>;
61
+ const typeName = (element.type as any)?.displayName;
62
+
63
+ if (typeName === "OptionalTabs") {
64
+ React.Children.forEach(element.props.children, (c) => {
65
+ if (React.isValidElement(c))
66
+ optional.push(c as React.ReactElement<any>);
67
+ });
68
+ } else if (typeName === "Tab") {
69
+ mandatory.push(element);
70
+ }
71
+ });
72
+
73
+ return { mandatoryTabs: mandatory, optionalTabs: optional };
74
+ }, [children]);
75
+
76
+ // Initialize state
77
+ const [tabState, setTabState] = useState({
78
+ active: mandatoryTabs[0]?.props.children || "",
79
+ mandatory: mandatoryTabs[0]?.props.children || "",
80
+ optional: optionalTabs[0]?.props.children || "",
81
+ });
82
+
83
+ const selectionName = `${name}_selections`;
84
+
85
+ const handleTabChange = (tabName: string, optional = false) => {
86
+ setTabState((prev) => ({
87
+ ...prev,
88
+ active: tabName,
89
+ [optional ? "optional" : "mandatory"]: tabName,
90
+ }));
91
+ onTabChanged?.(selectionName, tabName, optional);
92
+ };
93
+
94
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
95
+ onChanged(e.target.value, e.target.name);
96
+ };
97
+
98
+ const generateUniqueName = (base: string, key: string) => `${base}_${key}`;
99
+
100
+ const getTabState = (tabName: string, optional = false): TabState => {
101
+ if (tabName === tabState.active) return "active";
102
+ if (optional)
103
+ return tabName === tabState.optional ? "selected" : "unselected";
104
+ return tabName === tabState.mandatory ? "selected" : "unselected";
105
+ };
106
+
107
+ const renderTabs = (tabs: React.ReactElement<any>[], optional = false) =>
108
+ tabs.map((tab, idx) => {
109
+ const tabName = tab.props.children;
110
+ const state: TabState = getTabState(tabName, optional);
111
+
112
+ return React.cloneElement(tab, {
113
+ key: `${optional ? "opt" : "mand"}-${idx}`,
114
+ state,
115
+ optional,
116
+ onClick: () => handleTabChange(tabName, optional),
117
+ className: tab.props.className,
118
+ });
119
+ });
120
+
121
+ const activeTabName = generateUniqueName(name, tabState.active);
122
+ const selectTabValue = tabData[activeTabName] || "";
123
+ const errorMessages = errorData?.get(activeTabName)
124
+ ? [errorData.get(activeTabName)!]
125
+ : [];
126
+
127
+ const direction =
128
+ activeTabName.endsWith("farsi") || activeTabName.endsWith("pashto")
129
+ ? "rtl"
130
+ : "ltr";
131
+ return (
132
+ <div className={cn("flex flex-col select-none", rootDivClassName)}>
133
+ <div className="flex flex-col-reverse sm:flex-row sm:justify-between sm:items-end gap-4">
134
+ {label && (
135
+ <h1 className="font-semibold relative top-1 rtl:text-lg ltr:text-[13px] text-start">
136
+ {label}
137
+ </h1>
138
+ )}
139
+
140
+ <div
141
+ className={cn(
142
+ "flex flex-wrap gap-2 items-center",
143
+ tabsDivClassName
144
+ )}
145
+ >
146
+ {renderTabs(mandatoryTabs)}
147
+ {optionalTabs.length > 0 && (
148
+ <>
149
+ <span className="bg-primary/30 sm:w-px w-full h-px sm:my-0 sm:mx-2 sm:h-4" />
150
+ {renderTabs(optionalTabs, true)}
151
+ </>
152
+ )}
153
+ </div>
154
+ </div>
155
+
156
+ <Input
157
+ measurement={measurement}
158
+ dir={direction}
159
+ {...rest}
160
+ ref={ref}
161
+ name={activeTabName}
162
+ value={selectTabValue}
163
+ placeholder={placeholder}
164
+ onChange={handleInputChange}
165
+ className={cn(
166
+ `mt-2 ${errorMessages.length > 0 && "border-red-400 border-b!"}`,
167
+ className
168
+ )}
169
+ />
170
+
171
+ {errorMessages.map((error: string, index) => (
172
+ <AnimatedItem
173
+ key={index}
174
+ springProps={{
175
+ from: { opacity: 0, transform: "translateY(-8px)" },
176
+ to: {
177
+ opacity: 1,
178
+ transform: "translateY(0px)",
179
+ delay: index * 100,
180
+ },
181
+ config: { mass: 1, tension: 210, friction: 20 },
182
+ delay: index * 100,
183
+ }}
184
+ intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
185
+ >
186
+ <h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-[11px]">
187
+ {error}
188
+ </h1>
189
+ </AnimatedItem>
190
+ ))}
191
+ </div>
192
+ );
193
+ }
194
+ );
195
+
196
+ export default MultiTabInput;
@@ -0,0 +1,290 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+
4
+ import MultiTabInput, { OptionalTabs } from "./multi-tab-input";
5
+ import Tab from "../tab/tab";
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,191 @@
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 { TabState } from "../tab/tab";
6
+ import Tab from "../tab/tab";
7
+
8
+ // OptionalTabs wrapper
9
+ export function OptionalTabs({ children }: { children: React.ReactNode }) {
10
+ return <>{children}</>;
11
+ }
12
+ OptionalTabs.displayName = "OptionalTabs";
13
+
14
+ export interface MultiTabTextareaProps
15
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
16
+ children:
17
+ | ReactElement<typeof Tab>
18
+ | ReactElement<typeof Tab>[]
19
+ | ReactElement<typeof OptionalTabs>;
20
+ tabData: Record<string, string>;
21
+ errorData?: Map<string, string>;
22
+ onTabChanged?: (key: string, data: string, optional?: boolean) => void;
23
+ onChanged: (value: string, name: string) => void;
24
+ placeholder?: string;
25
+ label?: string;
26
+ name: string;
27
+ classNames?: {
28
+ tabsDivClassName?: string;
29
+ rootDivClassName?: string;
30
+ };
31
+ }
32
+
33
+ const MultiTabTextarea = React.forwardRef<
34
+ HTMLTextAreaElement,
35
+ MultiTabTextareaProps
36
+ >((props, ref) => {
37
+ const {
38
+ className,
39
+ name,
40
+ classNames,
41
+ children,
42
+ tabData,
43
+ errorData,
44
+ onTabChanged,
45
+ onChanged,
46
+ placeholder,
47
+ label,
48
+ ...rest
49
+ } = props;
50
+ const { tabsDivClassName, rootDivClassName } = classNames || {};
51
+ // Separate mandatory and optional tabs (memoized)
52
+ const { mandatoryTabs, optionalTabs } = useMemo(() => {
53
+ const mandatory: React.ReactElement<any>[] = [];
54
+ const optional: React.ReactElement<any>[] = [];
55
+
56
+ React.Children.forEach(children, (child) => {
57
+ if (!React.isValidElement(child)) return;
58
+
59
+ // Type-safe element access
60
+ const element = child as React.ReactElement<any>;
61
+ const typeName = (element.type as any)?.displayName;
62
+
63
+ if (typeName === "OptionalTabs") {
64
+ React.Children.forEach(element.props.children, (c) => {
65
+ if (React.isValidElement(c))
66
+ optional.push(c as React.ReactElement<any>);
67
+ });
68
+ } else if (typeName === "Tab") {
69
+ mandatory.push(element);
70
+ }
71
+ });
72
+
73
+ return { mandatoryTabs: mandatory, optionalTabs: optional };
74
+ }, [children]);
75
+
76
+ // Initialize state
77
+ const [tabState, setTabState] = useState({
78
+ active: mandatoryTabs[0]?.props.children || "",
79
+ mandatory: mandatoryTabs[0]?.props.children || "",
80
+ optional: optionalTabs[0]?.props.children || "",
81
+ });
82
+
83
+ const selectionName = `${name}_selections`;
84
+
85
+ const handleTabChange = (tabName: string, optional = false) => {
86
+ setTabState((prev) => ({
87
+ ...prev,
88
+ active: tabName,
89
+ [optional ? "optional" : "mandatory"]: tabName,
90
+ }));
91
+ onTabChanged?.(selectionName, tabName, optional);
92
+ };
93
+
94
+ const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
95
+ onChanged(e.target.value, e.target.name);
96
+ };
97
+
98
+ const generateUniqueName = (base: string, key: string) => `${base}_${key}`;
99
+
100
+ const getTabState = (tabName: string, optional = false): TabState => {
101
+ if (tabName === tabState.active) return "active";
102
+ if (optional)
103
+ return tabName === tabState.optional ? "selected" : "unselected";
104
+ return tabName === tabState.mandatory ? "selected" : "unselected";
105
+ };
106
+
107
+ const renderTabs = (tabs: React.ReactElement<any>[], optional = false) =>
108
+ tabs.map((tab, idx) => {
109
+ const tabName = tab.props.children;
110
+ const state: TabState = getTabState(tabName, optional);
111
+
112
+ return React.cloneElement(tab, {
113
+ key: `${optional ? "opt" : "mand"}-${idx}`,
114
+ state,
115
+ optional,
116
+ onClick: () => handleTabChange(tabName, optional),
117
+ className: tab.props.className,
118
+ });
119
+ });
120
+
121
+ const activeTabName = generateUniqueName(name, tabState.active);
122
+ const selectTabValue = tabData[activeTabName] || "";
123
+ const errorMessages = errorData?.get(activeTabName)
124
+ ? [errorData.get(activeTabName)!]
125
+ : [];
126
+
127
+ const direction =
128
+ activeTabName.endsWith("farsi") || activeTabName.endsWith("pashto")
129
+ ? "rtl"
130
+ : "ltr";
131
+ return (
132
+ <div className={cn("flex flex-col select-none", rootDivClassName)}>
133
+ <div className="flex flex-col-reverse sm:flex-row sm:justify-between sm:items-end gap-4">
134
+ {label && (
135
+ <h1 className="font-semibold relative top-1 rtl:text-lg ltr:text-[13px] text-start">
136
+ {label}
137
+ </h1>
138
+ )}
139
+
140
+ <div
141
+ className={cn("flex flex-wrap gap-2 items-center", tabsDivClassName)}
142
+ >
143
+ {renderTabs(mandatoryTabs)}
144
+ {optionalTabs.length > 0 && (
145
+ <>
146
+ <span className="bg-primary/30 sm:w-px w-full h-px sm:my-0 sm:mx-2 sm:h-4" />
147
+ {renderTabs(optionalTabs, true)}
148
+ </>
149
+ )}
150
+ </div>
151
+ </div>
152
+
153
+ <Textarea
154
+ dir={direction}
155
+ {...rest}
156
+ ref={ref}
157
+ name={activeTabName}
158
+ value={selectTabValue}
159
+ placeholder={placeholder}
160
+ onChange={handleInputChange}
161
+ className={cn(
162
+ `mt-2 ${errorMessages.length > 0 ? "border-red-400 border-b!" : ""}`,
163
+ className
164
+ )}
165
+ />
166
+
167
+ {errorMessages.map((error: string, index) => (
168
+ <AnimatedItem
169
+ key={index}
170
+ springProps={{
171
+ from: { opacity: 0, transform: "translateY(-8px)" },
172
+ to: {
173
+ opacity: 1,
174
+ transform: "translateY(0px)",
175
+ delay: index * 100,
176
+ },
177
+ config: { mass: 1, tension: 210, friction: 20 },
178
+ delay: index * 100,
179
+ }}
180
+ intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
181
+ >
182
+ <h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-[11px]">
183
+ {error}
184
+ </h1>
185
+ </AnimatedItem>
186
+ ))}
187
+ </div>
188
+ );
189
+ });
190
+
191
+ export default MultiTabTextarea;