notionsoft-ui 1.0.30 → 1.0.33
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 +1 -1
- package/src/notion-ui/multi-select-input/multi-select-input.tsx +2 -2
- package/src/notion-ui/multi-tab-input/multi-tab-input.tsx +40 -33
- package/src/notion-ui/multi-tab-input/multi.tab.input.stories.tsx +2 -2
- package/src/notion-ui/multi-tab-textarea/multi-tab-textarea.tsx +39 -34
- package/src/notion-ui/multi-tab-textarea/multi.tab.textarea.stories.tsx +2 -2
- package/src/notion-ui/shimmer/index.ts +3 -0
- package/src/notion-ui/shimmer/shimmer.stories.tsx +25 -0
- package/src/notion-ui/shimmer/shimmer.tsx +47 -0
- package/src/notion-ui/tab/tab.tsx +6 -1
package/package.json
CHANGED
|
@@ -410,7 +410,7 @@ function MultiSelectInputInner<T = any>(
|
|
|
410
410
|
setShowSelectedOnly(true); // Show only selected items
|
|
411
411
|
updatePosition(); // Recalculate dropdown position
|
|
412
412
|
}}
|
|
413
|
-
className="flex items-center hover:bg-tertiary/10 hover:text-tertiary cursor-pointer text-primary/60 rounded transition-colors"
|
|
413
|
+
className="flex items-center pointer-events-auto hover:bg-tertiary/10 hover:text-tertiary cursor-pointer text-primary/60 rounded transition-colors"
|
|
414
414
|
>
|
|
415
415
|
<List className="size-[38px] p-3" />
|
|
416
416
|
<span className="text-sm px-1">{selectedItems.length}</span>
|
|
@@ -555,7 +555,7 @@ const Dropdown = <T,>(
|
|
|
555
555
|
"focus-visible:border-tertiary/60",
|
|
556
556
|
"[&::-webkit-outer-spin-button]:appearance-none",
|
|
557
557
|
"[&::-webkit-inner-spin-button]:appearance-none",
|
|
558
|
-
"[-moz-appearance:textfield]
|
|
558
|
+
"[-moz-appearance:textfield]"
|
|
559
559
|
)}
|
|
560
560
|
placeholder={text.maxRecord}
|
|
561
561
|
/>
|
|
@@ -3,13 +3,7 @@ import { cn } from "../../utils/cn";
|
|
|
3
3
|
import AnimatedItem from "../animated-item";
|
|
4
4
|
import type { TabState } from "../tab/tab";
|
|
5
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";
|
|
6
|
+
import { OptionalTabs, Tab } from "../tab/tab";
|
|
13
7
|
|
|
14
8
|
export interface MultiTabInputProps
|
|
15
9
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
@@ -108,26 +102,55 @@ const MultiTabInput = React.forwardRef<HTMLInputElement, MultiTabInputProps>(
|
|
|
108
102
|
tabs.map((tab, idx) => {
|
|
109
103
|
const tabName = tab.props.children;
|
|
110
104
|
const state: TabState = getTabState(tabName, optional);
|
|
105
|
+
const tabHasError = hasError(tabName);
|
|
111
106
|
|
|
112
107
|
return React.cloneElement(tab, {
|
|
113
108
|
key: `${optional ? "opt" : "mand"}-${idx}`,
|
|
114
109
|
state,
|
|
115
110
|
optional,
|
|
116
111
|
onClick: () => handleTabChange(tabName, optional),
|
|
117
|
-
className:
|
|
112
|
+
className: cn(
|
|
113
|
+
tab.props.className,
|
|
114
|
+
tabHasError && "text-red-400 border-red-400"
|
|
115
|
+
),
|
|
118
116
|
});
|
|
119
117
|
});
|
|
120
118
|
|
|
119
|
+
const hasError = (tabKey: string) => {
|
|
120
|
+
if (!errorData) return false;
|
|
121
|
+
return errorData.has(generateUniqueName(name, tabKey));
|
|
122
|
+
};
|
|
121
123
|
const activeTabName = generateUniqueName(name, tabState.active);
|
|
122
124
|
const selectTabValue = tabData[activeTabName] || "";
|
|
123
|
-
const errorMessages = errorData?.get(activeTabName)
|
|
124
|
-
? [errorData.get(activeTabName)!]
|
|
125
|
-
: [];
|
|
126
125
|
|
|
127
126
|
const direction =
|
|
128
127
|
activeTabName.endsWith("farsi") || activeTabName.endsWith("pashto")
|
|
129
128
|
? "rtl"
|
|
130
129
|
: "ltr";
|
|
130
|
+
|
|
131
|
+
const errorMessage = useMemo(() => {
|
|
132
|
+
if (!errorData) return null;
|
|
133
|
+
|
|
134
|
+
return Array.from(errorData.entries())
|
|
135
|
+
.filter(([key]) => key.startsWith(`${name}_`))
|
|
136
|
+
.map(([key, value], index) => (
|
|
137
|
+
<AnimatedItem
|
|
138
|
+
key={key}
|
|
139
|
+
springProps={{
|
|
140
|
+
from: { opacity: 0, transform: "translateY(-8px)" },
|
|
141
|
+
to: { opacity: 1, transform: "translateY(0px)" },
|
|
142
|
+
delay: index * 100,
|
|
143
|
+
config: { mass: 1, tension: 210, friction: 20 },
|
|
144
|
+
}}
|
|
145
|
+
intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
|
|
146
|
+
>
|
|
147
|
+
<h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-[11px]">
|
|
148
|
+
{value}
|
|
149
|
+
</h1>
|
|
150
|
+
</AnimatedItem>
|
|
151
|
+
));
|
|
152
|
+
}, [errorData, name]);
|
|
153
|
+
|
|
131
154
|
return (
|
|
132
155
|
<div className={cn("flex flex-col select-none", rootDivClassName)}>
|
|
133
156
|
<div className="flex flex-col-reverse sm:flex-row sm:justify-between sm:items-end gap-4">
|
|
@@ -163,31 +186,15 @@ const MultiTabInput = React.forwardRef<HTMLInputElement, MultiTabInputProps>(
|
|
|
163
186
|
placeholder={placeholder}
|
|
164
187
|
onChange={handleInputChange}
|
|
165
188
|
className={cn(
|
|
166
|
-
`mt-2 ${
|
|
189
|
+
`mt-2 ${
|
|
190
|
+
errorMessage &&
|
|
191
|
+
errorMessage.length > 0 &&
|
|
192
|
+
"border-red-400 border-b!"
|
|
193
|
+
}`,
|
|
167
194
|
className
|
|
168
195
|
)}
|
|
169
196
|
/>
|
|
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
|
-
))}
|
|
197
|
+
{errorMessage}
|
|
191
198
|
</div>
|
|
192
199
|
);
|
|
193
200
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import { OptionalTabs, Tab } from "../tab/tab";
|
|
5
|
+
import MultiTabInput from "./multi-tab-input";
|
|
6
6
|
|
|
7
7
|
const meta: Meta<typeof MultiTabInput> = {
|
|
8
8
|
title: "Form/MultiTabInput",
|
|
@@ -2,14 +2,7 @@ import React, { type ReactElement, useState, useMemo } from "react";
|
|
|
2
2
|
import { cn } from "../../utils/cn";
|
|
3
3
|
import Textarea from "../textarea";
|
|
4
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";
|
|
5
|
+
import type { OptionalTabs, Tab, TabState } from "../tab/tab";
|
|
13
6
|
|
|
14
7
|
export interface MultiTabTextareaProps
|
|
15
8
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
@@ -108,26 +101,53 @@ const MultiTabTextarea = React.forwardRef<
|
|
|
108
101
|
tabs.map((tab, idx) => {
|
|
109
102
|
const tabName = tab.props.children;
|
|
110
103
|
const state: TabState = getTabState(tabName, optional);
|
|
104
|
+
const tabHasError = hasError(tabName);
|
|
111
105
|
|
|
112
106
|
return React.cloneElement(tab, {
|
|
113
107
|
key: `${optional ? "opt" : "mand"}-${idx}`,
|
|
114
108
|
state,
|
|
115
109
|
optional,
|
|
116
110
|
onClick: () => handleTabChange(tabName, optional),
|
|
117
|
-
className:
|
|
111
|
+
className: cn(
|
|
112
|
+
tab.props.className,
|
|
113
|
+
tabHasError && "text-red-400 border-red-400"
|
|
114
|
+
),
|
|
118
115
|
});
|
|
119
116
|
});
|
|
120
|
-
|
|
117
|
+
const hasError = (tabKey: string) => {
|
|
118
|
+
if (!errorData) return false;
|
|
119
|
+
return errorData.has(generateUniqueName(name, tabKey));
|
|
120
|
+
};
|
|
121
121
|
const activeTabName = generateUniqueName(name, tabState.active);
|
|
122
122
|
const selectTabValue = tabData[activeTabName] || "";
|
|
123
|
-
const errorMessages = errorData?.get(activeTabName)
|
|
124
|
-
? [errorData.get(activeTabName)!]
|
|
125
|
-
: [];
|
|
126
123
|
|
|
127
124
|
const direction =
|
|
128
125
|
activeTabName.endsWith("farsi") || activeTabName.endsWith("pashto")
|
|
129
126
|
? "rtl"
|
|
130
127
|
: "ltr";
|
|
128
|
+
const errorMessage = useMemo(() => {
|
|
129
|
+
if (!errorData) return null;
|
|
130
|
+
|
|
131
|
+
return Array.from(errorData.entries())
|
|
132
|
+
.filter(([key]) => key.startsWith(`${name}_`))
|
|
133
|
+
.map(([key, value], index) => (
|
|
134
|
+
<AnimatedItem
|
|
135
|
+
key={key}
|
|
136
|
+
springProps={{
|
|
137
|
+
from: { opacity: 0, transform: "translateY(-8px)" },
|
|
138
|
+
to: { opacity: 1, transform: "translateY(0px)" },
|
|
139
|
+
delay: index * 100,
|
|
140
|
+
config: { mass: 1, tension: 210, friction: 20 },
|
|
141
|
+
}}
|
|
142
|
+
intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
|
|
143
|
+
>
|
|
144
|
+
<h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-[11px]">
|
|
145
|
+
{value}
|
|
146
|
+
</h1>
|
|
147
|
+
</AnimatedItem>
|
|
148
|
+
));
|
|
149
|
+
}, [errorData, name]);
|
|
150
|
+
|
|
131
151
|
return (
|
|
132
152
|
<div className={cn("flex flex-col select-none", rootDivClassName)}>
|
|
133
153
|
<div className="flex flex-col-reverse sm:flex-row sm:justify-between sm:items-end gap-4">
|
|
@@ -159,31 +179,16 @@ const MultiTabTextarea = React.forwardRef<
|
|
|
159
179
|
placeholder={placeholder}
|
|
160
180
|
onChange={handleInputChange}
|
|
161
181
|
className={cn(
|
|
162
|
-
`mt-2 ${
|
|
182
|
+
`mt-2 ${
|
|
183
|
+
errorMessage &&
|
|
184
|
+
errorMessage.length > 0 &&
|
|
185
|
+
"border-red-400 border-b!"
|
|
186
|
+
}`,
|
|
163
187
|
className
|
|
164
188
|
)}
|
|
165
189
|
/>
|
|
166
190
|
|
|
167
|
-
{
|
|
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
|
-
))}
|
|
191
|
+
{errorMessage}
|
|
187
192
|
</div>
|
|
188
193
|
);
|
|
189
194
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import { OptionalTabs, Tab } from "../tab/tab";
|
|
5
|
+
import MultiTabTextarea from "../multi-tab-textarea";
|
|
6
6
|
|
|
7
7
|
const meta: Meta<typeof MultiTabTextarea> = {
|
|
8
8
|
title: "Form/MultiTabTextarea",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import Shimmer, { ShimmerItem, ShimmerProps } from "./shimmer";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Shimmer> = {
|
|
6
|
+
title: "Shimmer/Shimmer",
|
|
7
|
+
component: Shimmer,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
|
|
13
|
+
type Story = StoryObj<typeof Shimmer>;
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: (args: ShimmerProps) => (
|
|
17
|
+
<div className="space-y-2 p-4 w-96">
|
|
18
|
+
<Shimmer {...args}>
|
|
19
|
+
<ShimmerItem className="w-full" />
|
|
20
|
+
<ShimmerItem className="w-3/4" />
|
|
21
|
+
<ShimmerItem className="w-1/2" />
|
|
22
|
+
</Shimmer>
|
|
23
|
+
</div>
|
|
24
|
+
),
|
|
25
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cn } from "../../utils/cn";
|
|
2
|
+
|
|
3
|
+
export interface ShimmerProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
4
|
+
|
|
5
|
+
export default function Shimmer({ className, children }: ShimmerProps) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
className={cn("relative w-full overflow-hidden *:rounded-sm", className)}
|
|
9
|
+
>
|
|
10
|
+
{/* Scoped CSS */}
|
|
11
|
+
<style>{`
|
|
12
|
+
@keyframes shimmer {
|
|
13
|
+
0% {
|
|
14
|
+
background-position: -1200px 0;
|
|
15
|
+
}
|
|
16
|
+
100% {
|
|
17
|
+
background-position: 1200px 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
`}</style>
|
|
21
|
+
|
|
22
|
+
{/* Shimmer overlay */}
|
|
23
|
+
<div
|
|
24
|
+
className="absolute inset-0 pointer-events-none"
|
|
25
|
+
style={{
|
|
26
|
+
backgroundImage: `linear-gradient(
|
|
27
|
+
to right,
|
|
28
|
+
var(--from-shimmer) 10%,
|
|
29
|
+
var(--to-shimmer) 18%,
|
|
30
|
+
var(--from-shimmer) 25%
|
|
31
|
+
)`,
|
|
32
|
+
backgroundSize: "1200px 100%",
|
|
33
|
+
animation: "shimmer 2.2s linear infinite",
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
36
|
+
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
export interface ShimmerItemProps
|
|
42
|
+
extends React.HTMLAttributes<HTMLDivElement> {}
|
|
43
|
+
|
|
44
|
+
export function ShimmerItem(props: ShimmerItemProps) {
|
|
45
|
+
const { className } = props;
|
|
46
|
+
return <div className={cn(`h-10 bg-primary/5`, className)} />;
|
|
47
|
+
}
|
|
@@ -12,7 +12,7 @@ interface TabProps {
|
|
|
12
12
|
state?: TabState; // <-- now strongly typed
|
|
13
13
|
optional?: boolean;
|
|
14
14
|
}
|
|
15
|
-
export
|
|
15
|
+
export function Tab({
|
|
16
16
|
children,
|
|
17
17
|
className,
|
|
18
18
|
onClick,
|
|
@@ -44,3 +44,8 @@ export default function Tab({
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
Tab.displayName = "Tab";
|
|
47
|
+
|
|
48
|
+
export function OptionalTabs({ children }: { children: React.ReactNode }) {
|
|
49
|
+
return <>{children}</>;
|
|
50
|
+
}
|
|
51
|
+
OptionalTabs.displayName = "OptionalTabs";
|