notionsoft-ui 1.0.33 → 1.0.35
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/.storybook/main.ts +19 -13
- package/.storybook/preview.css +0 -16
- package/package.json +7 -2
- package/src/notion-ui/animated-item/animated-item.tsx +1 -1
- package/src/notion-ui/animated-item/index.ts +1 -1
- package/src/notion-ui/button/Button.stories.tsx +31 -8
- package/src/notion-ui/button/button.tsx +10 -2
- package/src/notion-ui/button-spinner/ButtonSpinner.stories.tsx +42 -34
- package/src/notion-ui/button-spinner/button-spinner.tsx +4 -5
- package/src/notion-ui/cache-svg/CachedSvg.stories.tsx +74 -0
- package/src/notion-ui/cache-svg/cached-svg.tsx +150 -0
- package/src/notion-ui/cache-svg/index.ts +3 -0
- package/src/notion-ui/cache-svg/utils.ts +7 -0
- package/src/notion-ui/cached-image/cached-image.stories.tsx +109 -0
- package/src/notion-ui/cached-image/cached-image.tsx +213 -0
- package/src/notion-ui/cached-image/index.ts +3 -0
- package/src/notion-ui/cached-image/utils.ts +7 -0
- package/src/notion-ui/date-picker/DatePicker.stories.tsx +0 -2
- package/src/notion-ui/date-picker/date-picker.tsx +5 -5
- package/src/notion-ui/input/Input.stories.tsx +1 -1
- package/src/notion-ui/input/input.tsx +5 -4
- package/src/notion-ui/multi-date-picker/multi-date-picker.tsx +5 -5
- package/src/notion-ui/page-size-select/index.ts +3 -0
- package/src/notion-ui/page-size-select/page-size-select.stories.tsx +117 -0
- package/src/notion-ui/page-size-select/page-size-select.tsx +283 -0
- package/src/notion-ui/password-input/password-input.tsx +3 -3
- package/src/notion-ui/phone-input/phone-input.tsx +38 -8
- package/src/notion-ui/shimmer/shimmer.tsx +9 -3
- package/src/notion-ui/shining-text/shining-text.tsx +2 -6
- package/src/notion-ui/sidebar/index.ts +3 -0
- package/src/notion-ui/sidebar/sidebar-item.tsx +198 -0
- package/src/notion-ui/sidebar/sidebar.stories.tsx +181 -0
- package/src/notion-ui/sidebar/sidebar.tsx +284 -0
- package/src/notion-ui/textarea/Textarea.stories.tsx +1 -1
- package/src/notion-ui/textarea/textarea.tsx +3 -3
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import CachedImage, { ImageProps } from "./cached-image";
|
|
3
|
+
|
|
4
|
+
/* ---------------------------------- */
|
|
5
|
+
/* Helpers */
|
|
6
|
+
/* ---------------------------------- */
|
|
7
|
+
|
|
8
|
+
// Basic fetch wrapper
|
|
9
|
+
const fetchImage = async (src: string): Promise<Response> => {
|
|
10
|
+
return fetch(src);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Delayed fetch to show shimmer/loading
|
|
14
|
+
const delayedFetch = async (src: string): Promise<Response> => {
|
|
15
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
16
|
+
return fetch(src);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/* ---------------------------------- */
|
|
20
|
+
/* Meta */
|
|
21
|
+
/* ---------------------------------- */
|
|
22
|
+
|
|
23
|
+
const meta: Meta<ImageProps> = {
|
|
24
|
+
title: "Media/CachedImage",
|
|
25
|
+
component: CachedImage,
|
|
26
|
+
parameters: {
|
|
27
|
+
layout: "centered",
|
|
28
|
+
},
|
|
29
|
+
argTypes: {
|
|
30
|
+
className: {
|
|
31
|
+
control: "text",
|
|
32
|
+
},
|
|
33
|
+
classNames: {
|
|
34
|
+
control: "object",
|
|
35
|
+
},
|
|
36
|
+
fetch: {
|
|
37
|
+
table: { disable: true },
|
|
38
|
+
},
|
|
39
|
+
apiConfig: {
|
|
40
|
+
table: { disable: true },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
type Story = StoryObj<ImageProps>;
|
|
47
|
+
|
|
48
|
+
/* ---------------------------------- */
|
|
49
|
+
/* Stories */
|
|
50
|
+
/* ---------------------------------- */
|
|
51
|
+
|
|
52
|
+
// Default (fetch + cache)
|
|
53
|
+
export const Default: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
src: "/images/sample.jpg",
|
|
56
|
+
fetch: fetchImage,
|
|
57
|
+
className: "w-40 h-40 rounded-md",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// API config variant
|
|
62
|
+
export const WithApiConfig: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
apiConfig: {
|
|
65
|
+
src: "/images/sample.jpg",
|
|
66
|
+
},
|
|
67
|
+
className: "w-40 h-40 rounded-lg",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Cross-origin image (bypasses cache)
|
|
72
|
+
export const CrossOrigin: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
src: "https://picsum.photos/200",
|
|
75
|
+
fetch: fetchImage,
|
|
76
|
+
className: "w-40 h-40 rounded-full",
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Loading / shimmer state
|
|
81
|
+
export const LoadingState: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
src: "/images/sample.jpg",
|
|
84
|
+
fetch: delayedFetch,
|
|
85
|
+
className: "w-32 h-32",
|
|
86
|
+
classNames: {
|
|
87
|
+
shimmerClassName: "bg-gray-200",
|
|
88
|
+
shimmerIconClassName: "stroke-gray-400",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Small size
|
|
94
|
+
export const Small: Story = {
|
|
95
|
+
args: {
|
|
96
|
+
src: "/images/sample.jpg",
|
|
97
|
+
fetch: fetchImage,
|
|
98
|
+
className: "w-16 h-16 rounded",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Large size
|
|
103
|
+
export const Large: Story = {
|
|
104
|
+
args: {
|
|
105
|
+
src: "/images/sample.jpg",
|
|
106
|
+
fetch: fetchImage,
|
|
107
|
+
className: "w-64 h-64 rounded-xl",
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import Shimmer from "@/components/notion-ui/shimmer";
|
|
2
|
+
import { cn } from "@/utils/cn";
|
|
3
|
+
import React, { useEffect, useState, forwardRef, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
/* ---------------------------------- */
|
|
6
|
+
/* Types */
|
|
7
|
+
/* ---------------------------------- */
|
|
8
|
+
|
|
9
|
+
interface FetchConfig {
|
|
10
|
+
src: string;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
params?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface BaseImageProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
16
|
+
classNames?: {
|
|
17
|
+
shimmerClassName?: string;
|
|
18
|
+
shimmerIconClassName?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface FetchImageProps extends BaseImageProps {
|
|
23
|
+
src: string;
|
|
24
|
+
fetch: (src: string) => Promise<Response>;
|
|
25
|
+
apiConfig?: never;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ApiConfigImageProps extends BaseImageProps {
|
|
29
|
+
apiConfig: FetchConfig;
|
|
30
|
+
src?: never;
|
|
31
|
+
fetch?: never;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type ImageProps = FetchImageProps | ApiConfigImageProps;
|
|
35
|
+
|
|
36
|
+
/* ---------------------------------- */
|
|
37
|
+
/* Cache helpers */
|
|
38
|
+
/* ---------------------------------- */
|
|
39
|
+
|
|
40
|
+
const IMAGE_CACHE = "image-cache-v1";
|
|
41
|
+
|
|
42
|
+
async function getCachedImage(url: string): Promise<string | null> {
|
|
43
|
+
const cache = await caches.open(IMAGE_CACHE);
|
|
44
|
+
const cached = await cache.match(url);
|
|
45
|
+
|
|
46
|
+
if (!cached) return null;
|
|
47
|
+
|
|
48
|
+
const blob = await cached.blob();
|
|
49
|
+
return URL.createObjectURL(blob);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function cacheImage(url: string, response: Response) {
|
|
53
|
+
const cache = await caches.open(IMAGE_CACHE);
|
|
54
|
+
await cache.put(url, response.clone());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ---------------------------------- */
|
|
58
|
+
/* Utils */
|
|
59
|
+
/* ---------------------------------- */
|
|
60
|
+
|
|
61
|
+
function isCrossOrigin(url: string): boolean {
|
|
62
|
+
try {
|
|
63
|
+
return new URL(url, window.location.href).origin !== window.location.origin;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ---------------------------------- */
|
|
70
|
+
/* Component */
|
|
71
|
+
/* ---------------------------------- */
|
|
72
|
+
|
|
73
|
+
const CachedImage = forwardRef<HTMLDivElement, ImageProps>((props, ref) => {
|
|
74
|
+
const { className, classNames, fetch, apiConfig, src, ...imgProps } = props;
|
|
75
|
+
|
|
76
|
+
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
77
|
+
const [loading, setLoading] = useState(true);
|
|
78
|
+
|
|
79
|
+
const { shimmerClassName, shimmerIconClassName } = classNames || {};
|
|
80
|
+
|
|
81
|
+
const fetchRef = useRef(fetch);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
fetchRef.current = fetch;
|
|
84
|
+
}, [fetch]);
|
|
85
|
+
|
|
86
|
+
async function loadImage() {
|
|
87
|
+
try {
|
|
88
|
+
const resolvedSrc = apiConfig?.src ?? src;
|
|
89
|
+
|
|
90
|
+
if (!resolvedSrc) {
|
|
91
|
+
setImageUrl(null);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ---------------------------------- */
|
|
96
|
+
/* Cross-origin → use <img src> */
|
|
97
|
+
/* ---------------------------------- */
|
|
98
|
+
if (isCrossOrigin(resolvedSrc)) {
|
|
99
|
+
setImageUrl(resolvedSrc);
|
|
100
|
+
setLoading(false);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ---------------------------------- */
|
|
105
|
+
/* Cache Storage */
|
|
106
|
+
/* ---------------------------------- */
|
|
107
|
+
const cached = await getCachedImage(resolvedSrc);
|
|
108
|
+
if (cached) {
|
|
109
|
+
setImageUrl(cached);
|
|
110
|
+
setLoading(false);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ---------------------------------- */
|
|
115
|
+
/* Fetch */
|
|
116
|
+
/* ---------------------------------- */
|
|
117
|
+
const response = fetchRef.current
|
|
118
|
+
? await fetchRef.current(resolvedSrc)
|
|
119
|
+
: await window.fetch(resolvedSrc, {
|
|
120
|
+
headers: apiConfig?.headers,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
!response.ok ||
|
|
127
|
+
(!contentType.startsWith("image/") &&
|
|
128
|
+
contentType !== "application/octet-stream")
|
|
129
|
+
) {
|
|
130
|
+
throw new Error(`Invalid image response: ${contentType}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await cacheImage(resolvedSrc, response.clone());
|
|
134
|
+
|
|
135
|
+
const blob = await response.blob();
|
|
136
|
+
setImageUrl(URL.createObjectURL(blob));
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(err);
|
|
139
|
+
} finally {
|
|
140
|
+
setLoading(false);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
loadImage();
|
|
146
|
+
}, [src, apiConfig?.src]);
|
|
147
|
+
|
|
148
|
+
/* ---------------------------------- */
|
|
149
|
+
/* Cleanup */
|
|
150
|
+
/* ---------------------------------- */
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
return () => {
|
|
154
|
+
if (imageUrl?.startsWith("blob:")) {
|
|
155
|
+
URL.revokeObjectURL(imageUrl);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}, [imageUrl]);
|
|
159
|
+
|
|
160
|
+
/* ---------------------------------- */
|
|
161
|
+
/* UI */
|
|
162
|
+
/* ---------------------------------- */
|
|
163
|
+
|
|
164
|
+
if (loading || !imageUrl) {
|
|
165
|
+
const stop = loading ? false : !imageUrl && true;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<Shimmer
|
|
169
|
+
className={cn(
|
|
170
|
+
"bg-primary/10 mx-auto flex p-2 items-center size-8 rounded border border-tertiary/10",
|
|
171
|
+
shimmerClassName
|
|
172
|
+
)}
|
|
173
|
+
stop={stop}
|
|
174
|
+
>
|
|
175
|
+
<svg
|
|
176
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
177
|
+
width="24"
|
|
178
|
+
height="24"
|
|
179
|
+
viewBox="0 0 24 24"
|
|
180
|
+
fill="none"
|
|
181
|
+
strokeLinecap="round"
|
|
182
|
+
strokeLinejoin="round"
|
|
183
|
+
className={cn(
|
|
184
|
+
"stroke-primary/40 mx-auto stroke-2",
|
|
185
|
+
shimmerIconClassName
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
<rect x="1" y="1" width="22" height="22" rx="2" ry="2" />
|
|
189
|
+
<polyline points="3,20 8,13 13,17 17,12 21,16" />
|
|
190
|
+
<circle cx="16" cy="6" r="2" />
|
|
191
|
+
</svg>
|
|
192
|
+
</Shimmer>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div
|
|
198
|
+
ref={ref}
|
|
199
|
+
// src={imageUrl}
|
|
200
|
+
style={{ backgroundImage: `url(${imageUrl})` }}
|
|
201
|
+
// alt={alt}
|
|
202
|
+
className={cn(
|
|
203
|
+
"cursor-pointer shadow-lg bg-cover bg-center mx-auto",
|
|
204
|
+
className
|
|
205
|
+
)}
|
|
206
|
+
{...imgProps}
|
|
207
|
+
/>
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
CachedImage.displayName = "CachedImage";
|
|
212
|
+
|
|
213
|
+
export default CachedImage;
|
|
@@ -42,7 +42,6 @@ export const Default: StoryObj<DatePickerProps> = {
|
|
|
42
42
|
render: (args) => <Wrapper {...args} />,
|
|
43
43
|
args: {
|
|
44
44
|
placeholder: "Select date...",
|
|
45
|
-
required: false,
|
|
46
45
|
label: "Date",
|
|
47
46
|
measurement: "md",
|
|
48
47
|
},
|
|
@@ -83,7 +82,6 @@ export const WithError: StoryObj<DatePickerProps> = {
|
|
|
83
82
|
placeholder: "Select date...",
|
|
84
83
|
label: "Birthday",
|
|
85
84
|
errorMessage: "This field is required",
|
|
86
|
-
required: true,
|
|
87
85
|
requiredHint: "*",
|
|
88
86
|
},
|
|
89
87
|
};
|
|
@@ -212,7 +212,7 @@ export default function DatePicker(props: DatePickerProps) {
|
|
|
212
212
|
<label
|
|
213
213
|
htmlFor={label}
|
|
214
214
|
className={cn(
|
|
215
|
-
"font-semibold
|
|
215
|
+
"font-semibold ltr:text-[13px] rtl:text-[18px] inline-block pb-1"
|
|
216
216
|
)}
|
|
217
217
|
>
|
|
218
218
|
{label}
|
|
@@ -224,18 +224,18 @@ export default function DatePicker(props: DatePickerProps) {
|
|
|
224
224
|
height: heightStyle.height,
|
|
225
225
|
}}
|
|
226
226
|
className={cn(
|
|
227
|
-
"flex items-center text-start px-3 border select-none rounded-sm rtl:text-
|
|
227
|
+
"flex items-center text-start px-3 border select-none rounded-sm rtl:text-[17px] ltr:text-[13px]",
|
|
228
228
|
className
|
|
229
229
|
)}
|
|
230
230
|
onClick={onVisibilityChange}
|
|
231
231
|
>
|
|
232
232
|
{selectedDates ? (
|
|
233
|
-
<h1 className="flex items-center gap-x-2 text-ellipsis rtl:text-
|
|
233
|
+
<h1 className="flex items-center gap-x-2 text-ellipsis rtl:text-[17px] ltr:text-[13px] text-primary/80 whitespace-nowrap overflow-hidden">
|
|
234
234
|
<CalendarDays className="size-4 inline-block text-tertiary rtl:ml-2 rtl:mr-2" />
|
|
235
235
|
{formatHijriDate(selectedDates)}
|
|
236
236
|
</h1>
|
|
237
237
|
) : (
|
|
238
|
-
<h1 className="flex items-center gap-x-2 text-ellipsis rtl:text-
|
|
238
|
+
<h1 className="flex items-center gap-x-2 text-ellipsis rtl:text-[17px] ltr:text-[13px] font-semibold text-primary whitespace-nowrap overflow-hidden">
|
|
239
239
|
<CalendarDays className="size-4 inline-block text-tertiary" />
|
|
240
240
|
{placeholder}
|
|
241
241
|
</h1>
|
|
@@ -261,7 +261,7 @@ export default function DatePicker(props: DatePickerProps) {
|
|
|
261
261
|
}}
|
|
262
262
|
intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
|
|
263
263
|
>
|
|
264
|
-
<h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-
|
|
264
|
+
<h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-[11px]">
|
|
265
265
|
{errorMessage}
|
|
266
266
|
</h1>
|
|
267
267
|
</AnimatedItem>
|
|
@@ -19,7 +19,7 @@ export interface InputProps
|
|
|
19
19
|
measurement?: NastranInputSize;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
23
23
|
(
|
|
24
24
|
{
|
|
25
25
|
className,
|
|
@@ -86,12 +86,13 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
86
86
|
className={cn(
|
|
87
87
|
rootDivClassName,
|
|
88
88
|
"flex w-full flex-col justify-end",
|
|
89
|
+
requiredHint && !label && "mt-5",
|
|
89
90
|
readOnlyStyle
|
|
90
91
|
)}
|
|
91
92
|
>
|
|
92
93
|
<div
|
|
93
94
|
className={cn(
|
|
94
|
-
"relative text-start select-none h-fit
|
|
95
|
+
"relative text-start select-none h-fit ltr:text-[13px] rtl:text-[18px]"
|
|
95
96
|
)}
|
|
96
97
|
>
|
|
97
98
|
{/* Start Content */}
|
|
@@ -135,7 +136,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
135
136
|
<label
|
|
136
137
|
htmlFor={label}
|
|
137
138
|
className={cn(
|
|
138
|
-
"font-semibold
|
|
139
|
+
"font-semibold ltr:text-[13px] rtl:text-[18px] inline-block pb-1"
|
|
139
140
|
)}
|
|
140
141
|
>
|
|
141
142
|
{label}
|
|
@@ -189,7 +190,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
189
190
|
}}
|
|
190
191
|
intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
|
|
191
192
|
>
|
|
192
|
-
<h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-
|
|
193
|
+
<h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-[11px]">
|
|
193
194
|
{errorMessage}
|
|
194
195
|
</h1>
|
|
195
196
|
</AnimatedItem>
|
|
@@ -195,7 +195,7 @@ export default function MultiDatePicker(props: MultiDatePickerProps) {
|
|
|
195
195
|
<label
|
|
196
196
|
htmlFor={label}
|
|
197
197
|
className={cn(
|
|
198
|
-
"font-semibold
|
|
198
|
+
"font-semibold ltr:text-[13px] rtl:text-[18px] inline-block pb-1"
|
|
199
199
|
)}
|
|
200
200
|
>
|
|
201
201
|
{label}
|
|
@@ -206,13 +206,13 @@ export default function MultiDatePicker(props: MultiDatePickerProps) {
|
|
|
206
206
|
height: heightStyle.height,
|
|
207
207
|
}}
|
|
208
208
|
className={cn(
|
|
209
|
-
"relative flex items-center text-start px-3 border select-none rounded-sm rtl:text-
|
|
209
|
+
"relative flex items-center text-start px-3 border select-none rounded-sm rtl:text-[17px] ltr:text-[13px]",
|
|
210
210
|
className
|
|
211
211
|
)}
|
|
212
212
|
onClick={onVisibilityChange}
|
|
213
213
|
>
|
|
214
214
|
{selectedDates && selectedDates.length > 0 ? (
|
|
215
|
-
<div className="flex items-center gap-x-2 text-ellipsis rtl:text-
|
|
215
|
+
<div className="flex items-center gap-x-2 text-ellipsis rtl:text-[17px] ltr:text-[13px] text-primary/80 text-nowrap">
|
|
216
216
|
<CalendarDays className="size-4 inline-block text-tertiary rtl:ml-2 rtl:mr-2" />
|
|
217
217
|
{selectedDates.map((date: DateObject, index: number) => (
|
|
218
218
|
<div key={index} className="flex gap-x-2">
|
|
@@ -229,7 +229,7 @@ export default function MultiDatePicker(props: MultiDatePickerProps) {
|
|
|
229
229
|
))}
|
|
230
230
|
</div>
|
|
231
231
|
) : (
|
|
232
|
-
<h1 className="flex items-center gap-x-2 text-ellipsis rtl:text-
|
|
232
|
+
<h1 className="flex items-center gap-x-2 text-ellipsis rtl:text-[17px] ltr:text-[13px] text-primary/80 text-nowrap">
|
|
233
233
|
<CalendarDays className="size-4 inline-block text-tertiary" />
|
|
234
234
|
{placeholder}
|
|
235
235
|
</h1>
|
|
@@ -255,7 +255,7 @@ export default function MultiDatePicker(props: MultiDatePickerProps) {
|
|
|
255
255
|
}}
|
|
256
256
|
intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
|
|
257
257
|
>
|
|
258
|
-
<h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-
|
|
258
|
+
<h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-[11px]">
|
|
259
259
|
{errorMessage}
|
|
260
260
|
</h1>
|
|
261
261
|
</AnimatedItem>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import PageSizeSelect from "./page-size-select";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof PageSizeSelect> = {
|
|
6
|
+
title: "Select/PageSizeSelect",
|
|
7
|
+
component: PageSizeSelect,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component: `
|
|
13
|
+
A pagination page-size selector with:
|
|
14
|
+
- Preset options
|
|
15
|
+
- Custom numeric input
|
|
16
|
+
- LocalStorage persistence (or custom save/load)
|
|
17
|
+
- Smart dropdown positioning (up/down)
|
|
18
|
+
`,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
argTypes: {
|
|
23
|
+
onChange: { action: "changed" },
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
|
|
29
|
+
type Story = StoryObj<typeof PageSizeSelect>;
|
|
30
|
+
|
|
31
|
+
const OPTIONS = [
|
|
32
|
+
{ value: "10", label: "10 / page" },
|
|
33
|
+
{ value: "20", label: "20 / page" },
|
|
34
|
+
{ value: "50", label: "50 / page" },
|
|
35
|
+
{ value: "100", label: "100 / page" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/* ---------------------------------------------
|
|
39
|
+
Default
|
|
40
|
+
--------------------------------------------- */
|
|
41
|
+
export const Default: Story = {
|
|
42
|
+
args: {
|
|
43
|
+
placeholder: "Select page size",
|
|
44
|
+
emptyPlaceholder: "No options",
|
|
45
|
+
rangePlaceholder: "Custom size",
|
|
46
|
+
paginationKey: "storybook-page-size",
|
|
47
|
+
options: OPTIONS,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/* ---------------------------------------------
|
|
52
|
+
With State Preview
|
|
53
|
+
--------------------------------------------- */
|
|
54
|
+
export const WithState: Story = {
|
|
55
|
+
render: (args) => {
|
|
56
|
+
const [value, setValue] = useState<string>("");
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="w-64 space-y-3">
|
|
60
|
+
<PageSizeSelect
|
|
61
|
+
{...args}
|
|
62
|
+
onChange={(v) => {
|
|
63
|
+
setValue(v);
|
|
64
|
+
args.onChange?.(v);
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
<div className="text-sm text-muted-foreground">
|
|
69
|
+
Selected value: <strong>{value || "-"}</strong>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
args: {
|
|
75
|
+
placeholder: "Page size",
|
|
76
|
+
emptyPlaceholder: "No options available",
|
|
77
|
+
rangePlaceholder: "Enter number",
|
|
78
|
+
paginationKey: "storybook-with-state",
|
|
79
|
+
options: OPTIONS,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/* ---------------------------------------------
|
|
84
|
+
Empty Options
|
|
85
|
+
--------------------------------------------- */
|
|
86
|
+
export const EmptyOptions: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
placeholder: "Page size",
|
|
89
|
+
emptyPlaceholder: "Nothing to show",
|
|
90
|
+
rangePlaceholder: "Enter number",
|
|
91
|
+
paginationKey: "storybook-empty",
|
|
92
|
+
options: [],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/* ---------------------------------------------
|
|
97
|
+
Custom Storage (Mock)
|
|
98
|
+
--------------------------------------------- */
|
|
99
|
+
export const CustomStorage: Story = {
|
|
100
|
+
args: {
|
|
101
|
+
placeholder: "Page size",
|
|
102
|
+
emptyPlaceholder: "No data",
|
|
103
|
+
rangePlaceholder: "Custom size",
|
|
104
|
+
paginationKey: "storybook-custom-storage",
|
|
105
|
+
options: OPTIONS,
|
|
106
|
+
save: async (key, data) => {
|
|
107
|
+
console.log("Saved:", key, data);
|
|
108
|
+
},
|
|
109
|
+
load: async () => {
|
|
110
|
+
return {
|
|
111
|
+
key: "storybook-custom-storage",
|
|
112
|
+
value: "20",
|
|
113
|
+
option: 1,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|