notionsoft-ui 1.0.15 → 1.0.17
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/button/Button.stories.tsx +6 -0
- package/src/notion-ui/button/button.tsx +10 -6
- package/src/notion-ui/button-spinner/ButtonSpinner.stories.tsx +1 -1
- package/src/notion-ui/circle-loader/CircleLoader.stories.tsx +1 -1
- package/src/notion-ui/input/input.tsx +21 -21
- package/src/notion-ui/multi-select-input/index.ts +3 -0
- package/src/notion-ui/multi-select-input/multi-select-input.stories.tsx +166 -0
- package/src/notion-ui/multi-select-input/multi-select-input.tsx +507 -0
- package/src/notion-ui/search-input/index.ts +3 -0
- package/src/notion-ui/search-input/search-input.tsx +425 -0
- package/src/notion-ui/search-input/search.Input.stories.tsx +131 -0
- package/src/notion-ui/{boolean-status-button/BooleanStatusButton.stories.tsx → status-button/status-button.stories.tsx} +3 -3
- package/src/notion-ui/{boolean-status-button/BooleanStatusButton.tsx → status-button/status-button.tsx} +2 -2
- package/src/utils/cn.ts +26 -0
- /package/src/notion-ui/{boolean-status-button → status-button}/index.ts +0 -0
package/package.json
CHANGED
|
@@ -3,11 +3,11 @@ import { cn } from "../../utils/cn";
|
|
|
3
3
|
// import { cn } from "@/utils/cn";
|
|
4
4
|
|
|
5
5
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
|
-
variant?: "primary" | "secondary" | "warning" | "success";
|
|
6
|
+
variant?: "primary" | "secondary" | "warning" | "success" | "outline";
|
|
7
7
|
}
|
|
8
8
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
9
9
|
(props, ref: any) => {
|
|
10
|
-
const { className, children, variant, ...rest } = props;
|
|
10
|
+
const { className, children, variant, disabled, ...rest } = props;
|
|
11
11
|
const style =
|
|
12
12
|
variant == "secondary"
|
|
13
13
|
? "border hover:bg-primary hover:text-primary-foreground"
|
|
@@ -15,16 +15,20 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
15
15
|
? "bg-red-500 text-primary-foreground"
|
|
16
16
|
: variant == "success"
|
|
17
17
|
? "bg-green-500 text-primary-foreground"
|
|
18
|
-
:
|
|
18
|
+
: variant == "outline"
|
|
19
|
+
? "text-primary border border-primary/10 hover:bg-primary/5"
|
|
20
|
+
: "bg-primary hover:shadow hover:bg-primary shadow shadow-primary/50 text-primary-foreground/80 hover:opacity-90 hover:text-primary-foreground";
|
|
19
21
|
return (
|
|
20
22
|
<button
|
|
21
23
|
{...rest}
|
|
24
|
+
disabled={disabled}
|
|
22
25
|
ref={ref}
|
|
23
26
|
className={cn(
|
|
24
|
-
`rounded-sm cursor-pointer
|
|
25
|
-
|
|
26
|
-
sm:px-4 py-[6px] leading-normal duration-200 ease-linear`,
|
|
27
|
+
`rounded-sm flex items-center gap-x-1 cursor-pointer font-medium ltr:text-xs leading-snug li rtl:text-[13px] sm:rtl:text-sm rtl:font-semibold
|
|
28
|
+
transition w-fit px-3 py-1.5 duration-200 ease-linear`,
|
|
27
29
|
style,
|
|
30
|
+
disabled &&
|
|
31
|
+
"opacity-35 pointer-events-none disabled:cursor-not-allowed",
|
|
28
32
|
className
|
|
29
33
|
)}
|
|
30
34
|
>
|
|
@@ -3,7 +3,7 @@ import ButtonSpinner from "./button-spinner";
|
|
|
3
3
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
4
4
|
|
|
5
5
|
const meta: Meta<typeof ButtonSpinner> = {
|
|
6
|
-
title: "
|
|
6
|
+
title: "Loader/ButtonSpinner",
|
|
7
7
|
component: ButtonSpinner,
|
|
8
8
|
parameters: {
|
|
9
9
|
layout: "centered",
|
|
@@ -3,7 +3,7 @@ import { CircleLoaderProps } from "./circle-loader";
|
|
|
3
3
|
|
|
4
4
|
// Meta information for Storybook
|
|
5
5
|
export default {
|
|
6
|
-
title: "
|
|
6
|
+
title: "Loader/CircleLoader", // This will be the folder and component name in Storybook's sidebar
|
|
7
7
|
component: CircleLoader, // The component being showcased
|
|
8
8
|
argTypes: {
|
|
9
9
|
className: { control: "text" },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useMemo } from "react";
|
|
2
2
|
// import { cn } from "@/utils/cn";
|
|
3
3
|
import { cn } from "../../utils/cn";
|
|
4
|
-
import AnimatedItem from "
|
|
4
|
+
import AnimatedItem from "../../notion-ui/animated-item";
|
|
5
5
|
// import AnimatedItem from "../animated-item";
|
|
6
6
|
|
|
7
7
|
export type NastranInputSize = "sm" | "md" | "lg";
|
|
@@ -14,7 +14,7 @@ export interface InputProps
|
|
|
14
14
|
endContent?: React.ReactNode;
|
|
15
15
|
errorMessage?: string;
|
|
16
16
|
parentClassName?: string;
|
|
17
|
-
measurement
|
|
17
|
+
measurement?: NastranInputSize;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
@@ -26,7 +26,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
26
26
|
startContent,
|
|
27
27
|
endContent,
|
|
28
28
|
parentClassName = "",
|
|
29
|
-
measurement = "
|
|
29
|
+
measurement = "sm",
|
|
30
30
|
errorMessage,
|
|
31
31
|
label,
|
|
32
32
|
readOnly,
|
|
@@ -47,50 +47,50 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
47
47
|
height: "50px",
|
|
48
48
|
paddingBottom: "pb-[3px]",
|
|
49
49
|
endContent: label
|
|
50
|
-
? "top-
|
|
50
|
+
? "ltr:top-[48px] rtl:top-[54px]-translate-y-1/2"
|
|
51
51
|
: "top-[26px] -translate-y-1/2",
|
|
52
52
|
startContent: label
|
|
53
|
-
? "top-
|
|
53
|
+
? "ltr:top-[48px] rtl:top-[54px] -translate-y-1/2"
|
|
54
54
|
: "top-[26px] -translate-y-1/2",
|
|
55
|
-
required: "top-[
|
|
55
|
+
required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
|
|
56
56
|
}
|
|
57
57
|
: measurement == "md"
|
|
58
58
|
? {
|
|
59
59
|
height: "44px",
|
|
60
60
|
paddingBottom: "pb-[2px]",
|
|
61
61
|
endContent: label
|
|
62
|
-
? "top-[45px] -translate-y-1/2"
|
|
62
|
+
? "ltr:top-[45px] rtl:top-[51px] -translate-y-1/2"
|
|
63
63
|
: "top-[22px] -translate-y-1/2",
|
|
64
64
|
startContent: label
|
|
65
|
-
? "top-[45px] -translate-y-1/2"
|
|
65
|
+
? "ltr:top-[45px] rtl:top-[51px] -translate-y-1/2"
|
|
66
66
|
: "top-[22px] -translate-y-1/2",
|
|
67
|
-
required: "top-[4px]",
|
|
67
|
+
required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
|
|
68
68
|
}
|
|
69
69
|
: {
|
|
70
70
|
height: "40px",
|
|
71
71
|
paddingBottom: "pb-[2px]",
|
|
72
72
|
endContent: label
|
|
73
|
-
? "top-[44px] -translate-y-1/2"
|
|
73
|
+
? "ltr:top-[44px] rtl:top-[50px] -translate-y-1/2"
|
|
74
74
|
: "top-[20px] -translate-y-1/2",
|
|
75
75
|
startContent: label
|
|
76
|
-
? "top-[44px] -translate-y-1/2"
|
|
76
|
+
? "ltr:top-[44px] rtl:top-[50px] -translate-y-1/2"
|
|
77
77
|
: "top-[20px] -translate-y-1/2",
|
|
78
|
-
required: "top-[4px]",
|
|
78
|
+
required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
|
|
79
79
|
},
|
|
80
|
-
[]
|
|
80
|
+
[measurement, label]
|
|
81
81
|
);
|
|
82
82
|
return (
|
|
83
|
-
<div className={cn(parentClassName, "flex flex-col justify-end")}>
|
|
83
|
+
<div className={cn(parentClassName, "flex w-full flex-col justify-end")}>
|
|
84
84
|
<div
|
|
85
85
|
className={cn(
|
|
86
|
-
"relative select-none h-fit rtl:text-lg-rtl ltr:text-lg-ltr"
|
|
86
|
+
"relative text-start select-none h-fit rtl:text-lg-rtl ltr:text-lg-ltr"
|
|
87
87
|
)}
|
|
88
88
|
>
|
|
89
89
|
{/* Start Content */}
|
|
90
90
|
{startContent && (
|
|
91
91
|
<span
|
|
92
92
|
className={cn(
|
|
93
|
-
"absolute flex items-center ltr:left-
|
|
93
|
+
"absolute flex items-center ltr:left-3 rtl:right-3",
|
|
94
94
|
heightStyle.startContent
|
|
95
95
|
)}
|
|
96
96
|
>
|
|
@@ -114,7 +114,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
114
114
|
{requiredHint && (
|
|
115
115
|
<span
|
|
116
116
|
className={cn(
|
|
117
|
-
"absolute font-semibold text-red-600 rtl:text-[13px] ltr:text-[11px] ltr:right-
|
|
117
|
+
"absolute font-semibold text-red-600 rtl:text-[13px] ltr:text-[11px] ltr:right-2.5 rtl:left-2.5",
|
|
118
118
|
heightStyle.required
|
|
119
119
|
)}
|
|
120
120
|
>
|
|
@@ -145,13 +145,13 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
145
145
|
height: heightStyle.height,
|
|
146
146
|
}}
|
|
147
147
|
className={cn(
|
|
148
|
-
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded
|
|
148
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded border bg-transparent px-3 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
149
149
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
150
|
-
"appearance-none placeholder:text-primary/60 ltr:text-sm rtl:text-sm rtl:font-semibold focus-visible:ring-0
|
|
151
|
-
"focus-visible:border-
|
|
150
|
+
"appearance-none placeholder:text-primary/60 ltr:text-sm rtl:text-sm rtl:font-semibold focus-visible:ring-0 focus-visible:shadow-sm focus-visible:ring-offset-0 transition-[border] bg-card dark:bg-black/30",
|
|
151
|
+
"focus-visible:border-tertiary/60",
|
|
152
152
|
"[&::-webkit-outer-spin-button]:appearance-none",
|
|
153
153
|
"[&::-webkit-inner-spin-button]:appearance-none",
|
|
154
|
-
"[-moz-appearance:textfield]",
|
|
154
|
+
"[-moz-appearance:textfield] ",
|
|
155
155
|
inputPaddingClass,
|
|
156
156
|
hasError ? "border-red-400 border" : "border-primary/25",
|
|
157
157
|
readOnly && "cursor-not-allowed",
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Meta, StoryFn } from "@storybook/react";
|
|
3
|
+
import MultiSelectInputForward, {
|
|
4
|
+
MultiSelectInputProps,
|
|
5
|
+
} from "./multi-select-input";
|
|
6
|
+
|
|
7
|
+
interface User {
|
|
8
|
+
uuid: string;
|
|
9
|
+
name: string;
|
|
10
|
+
email: string;
|
|
11
|
+
active: boolean;
|
|
12
|
+
admin: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
title: "Select/MultiSelectInput",
|
|
17
|
+
component: MultiSelectInputForward,
|
|
18
|
+
} as Meta<typeof MultiSelectInputForward>;
|
|
19
|
+
|
|
20
|
+
// ------------------ Mock data ------------------
|
|
21
|
+
const mockUsers: User[] = [
|
|
22
|
+
{
|
|
23
|
+
uuid: "1",
|
|
24
|
+
name: "Alice",
|
|
25
|
+
email: "alice@example.com",
|
|
26
|
+
active: true,
|
|
27
|
+
admin: false,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
uuid: "2",
|
|
31
|
+
name: "Bob",
|
|
32
|
+
email: "bob@example.com",
|
|
33
|
+
active: false,
|
|
34
|
+
admin: true,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
uuid: "3",
|
|
38
|
+
name: "Charlie",
|
|
39
|
+
email: "charlie@example.com",
|
|
40
|
+
active: true,
|
|
41
|
+
admin: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
uuid: "4",
|
|
45
|
+
name: "David",
|
|
46
|
+
email: "david@example.com",
|
|
47
|
+
active: false,
|
|
48
|
+
admin: false,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// ------------------ Mock async fetch function ------------------
|
|
53
|
+
const fetchUsers = async (
|
|
54
|
+
query: string,
|
|
55
|
+
filters?: Record<string, boolean>,
|
|
56
|
+
maxFetch?: number
|
|
57
|
+
) => {
|
|
58
|
+
let result = mockUsers;
|
|
59
|
+
|
|
60
|
+
if (filters) {
|
|
61
|
+
result = result.filter((user) =>
|
|
62
|
+
Object.entries(filters).every(([key, value]) =>
|
|
63
|
+
value ? (user as any)[key] : true
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (query) {
|
|
69
|
+
const q = query.toLowerCase();
|
|
70
|
+
result = result.filter(
|
|
71
|
+
(u) =>
|
|
72
|
+
u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (maxFetch) result = result.slice(0, maxFetch);
|
|
77
|
+
|
|
78
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ------------------ Template ------------------
|
|
84
|
+
const Template: StoryFn<MultiSelectInputProps<User>> = (args) => {
|
|
85
|
+
const [selected, setSelected] = useState<User[]>([]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div style={{ width: 400, padding: 20 }}>
|
|
89
|
+
<MultiSelectInputForward
|
|
90
|
+
{...args}
|
|
91
|
+
selected={selected}
|
|
92
|
+
onItemsSelect={(selectedItems) => {
|
|
93
|
+
if (Array.isArray(selectedItems)) setSelected(selectedItems);
|
|
94
|
+
else if (selectedItems) setSelected([selectedItems]);
|
|
95
|
+
else setSelected([]);
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
<div style={{ marginTop: 20 }}>
|
|
99
|
+
<strong>Selected Users:</strong>
|
|
100
|
+
<pre>{JSON.stringify(selected, null, 2)}</pre>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ------------------ Stories ------------------
|
|
107
|
+
|
|
108
|
+
// Multiple selection story
|
|
109
|
+
export const MultipleSelection = Template.bind({});
|
|
110
|
+
MultipleSelection.args = {
|
|
111
|
+
fetch: fetchUsers,
|
|
112
|
+
selectionMode: "multiple",
|
|
113
|
+
searchBy: ["name", "email"],
|
|
114
|
+
itemKey: "uuid",
|
|
115
|
+
filters: [
|
|
116
|
+
{ key: "active", name: "Active" },
|
|
117
|
+
{ key: "admin", name: "Admin" },
|
|
118
|
+
],
|
|
119
|
+
text: {
|
|
120
|
+
fetch: "Loading users...",
|
|
121
|
+
notItem: "No users found",
|
|
122
|
+
maxRecord: "Max results",
|
|
123
|
+
clearFilters: "Clear Filters",
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Single selection story
|
|
128
|
+
export const SingleSelection = Template.bind({});
|
|
129
|
+
SingleSelection.args = {
|
|
130
|
+
fetch: fetchUsers,
|
|
131
|
+
selectionMode: "single",
|
|
132
|
+
searchBy: ["name", "email"],
|
|
133
|
+
itemKey: "uuid",
|
|
134
|
+
filters: [
|
|
135
|
+
{ key: "active", name: "Active" },
|
|
136
|
+
{ key: "admin", name: "Admin" },
|
|
137
|
+
],
|
|
138
|
+
text: {
|
|
139
|
+
fetch: "Loading users...",
|
|
140
|
+
notItem: "No users found",
|
|
141
|
+
maxRecord: "Max results",
|
|
142
|
+
clearFilters: "Clear Filters",
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// ------------------ API Config story ------------------
|
|
147
|
+
export const APIConfigExample = Template.bind({});
|
|
148
|
+
APIConfigExample.args = {
|
|
149
|
+
apiConfig: {
|
|
150
|
+
url: "https://jsonplaceholder.typicode.com/users",
|
|
151
|
+
headers: { "Content-Type": "application/json" },
|
|
152
|
+
},
|
|
153
|
+
selectionMode: "multiple",
|
|
154
|
+
searchBy: ["name", "email"],
|
|
155
|
+
filters: [
|
|
156
|
+
{ key: "active", name: "Active" },
|
|
157
|
+
{ key: "admin", name: "Admin" },
|
|
158
|
+
],
|
|
159
|
+
itemKey: "id", // JSONPlaceholder uses `id` as key
|
|
160
|
+
text: {
|
|
161
|
+
fetch: "Fetching users from API...",
|
|
162
|
+
notItem: "No users found",
|
|
163
|
+
maxRecord: "Max results",
|
|
164
|
+
clearFilters: "Clear Filters",
|
|
165
|
+
},
|
|
166
|
+
};
|