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.
Files changed (35) hide show
  1. package/.storybook/main.ts +19 -13
  2. package/.storybook/preview.css +0 -16
  3. package/package.json +7 -2
  4. package/src/notion-ui/animated-item/animated-item.tsx +1 -1
  5. package/src/notion-ui/animated-item/index.ts +1 -1
  6. package/src/notion-ui/button/Button.stories.tsx +31 -8
  7. package/src/notion-ui/button/button.tsx +10 -2
  8. package/src/notion-ui/button-spinner/ButtonSpinner.stories.tsx +42 -34
  9. package/src/notion-ui/button-spinner/button-spinner.tsx +4 -5
  10. package/src/notion-ui/cache-svg/CachedSvg.stories.tsx +74 -0
  11. package/src/notion-ui/cache-svg/cached-svg.tsx +150 -0
  12. package/src/notion-ui/cache-svg/index.ts +3 -0
  13. package/src/notion-ui/cache-svg/utils.ts +7 -0
  14. package/src/notion-ui/cached-image/cached-image.stories.tsx +109 -0
  15. package/src/notion-ui/cached-image/cached-image.tsx +213 -0
  16. package/src/notion-ui/cached-image/index.ts +3 -0
  17. package/src/notion-ui/cached-image/utils.ts +7 -0
  18. package/src/notion-ui/date-picker/DatePicker.stories.tsx +0 -2
  19. package/src/notion-ui/date-picker/date-picker.tsx +5 -5
  20. package/src/notion-ui/input/Input.stories.tsx +1 -1
  21. package/src/notion-ui/input/input.tsx +5 -4
  22. package/src/notion-ui/multi-date-picker/multi-date-picker.tsx +5 -5
  23. package/src/notion-ui/page-size-select/index.ts +3 -0
  24. package/src/notion-ui/page-size-select/page-size-select.stories.tsx +117 -0
  25. package/src/notion-ui/page-size-select/page-size-select.tsx +283 -0
  26. package/src/notion-ui/password-input/password-input.tsx +3 -3
  27. package/src/notion-ui/phone-input/phone-input.tsx +38 -8
  28. package/src/notion-ui/shimmer/shimmer.tsx +9 -3
  29. package/src/notion-ui/shining-text/shining-text.tsx +2 -6
  30. package/src/notion-ui/sidebar/index.ts +3 -0
  31. package/src/notion-ui/sidebar/sidebar-item.tsx +198 -0
  32. package/src/notion-ui/sidebar/sidebar.stories.tsx +181 -0
  33. package/src/notion-ui/sidebar/sidebar.tsx +284 -0
  34. package/src/notion-ui/textarea/Textarea.stories.tsx +1 -1
  35. package/src/notion-ui/textarea/textarea.tsx +3 -3
@@ -0,0 +1,181 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { MemoryRouter, Route, Routes } from "react-router";
3
+ import { I18nextProvider } from "react-i18next";
4
+ import i18n from "i18next";
5
+ import { initReactI18next } from "react-i18next";
6
+
7
+ import { LucideCircleArrowOutDownLeft } from "lucide-react";
8
+ import { useCallback, useMemo, useState } from "react";
9
+ import { SidebarItem } from "@/components/notion-ui/sidebar/sidebar-item";
10
+ import Sidebar, { Separator } from "@/components/notion-ui/sidebar/sidebar";
11
+ import Button from "@/components/notion-ui/button";
12
+
13
+ /* ------------------ i18n Mock ------------------ */
14
+ i18n.use(initReactI18next).init({
15
+ lng: "en",
16
+ fallbackLng: "en",
17
+ resources: {
18
+ en: {
19
+ translation: {
20
+ app_name: "My App",
21
+ dashboard: "Dashboard",
22
+ settings: "Settings",
23
+ users: "Users",
24
+ roles: "Roles",
25
+ exit_dashb: "Exit",
26
+ },
27
+ },
28
+ },
29
+ });
30
+
31
+ /* ------------------ Mock Data ------------------ */
32
+
33
+ const permissions = new Map([
34
+ [
35
+ "users",
36
+ {
37
+ id: 1,
38
+ visible: true,
39
+ permission: "users",
40
+ icon: "icons/users.svg",
41
+ sub: new Map([
42
+ [1, { id: 1, name: "roles", is_category: true }],
43
+ [2, { id: 2, name: "permissions", is_category: true }],
44
+ ]),
45
+ },
46
+ ],
47
+ [
48
+ "reports",
49
+ {
50
+ id: 2,
51
+ visible: true,
52
+ permission: "reports",
53
+ icon: "icons/reports.svg",
54
+ sub: new Map(),
55
+ },
56
+ ],
57
+ ]);
58
+
59
+ /* ------------------ Story Component ------------------ */
60
+
61
+ function StorySidebar() {
62
+ const [collapsed, setCollapsed] = useState(false);
63
+
64
+ const navigateTo = useCallback((path: string) => {
65
+ console.log("Navigate:", path);
66
+ }, []);
67
+
68
+ const sidebarItems = useMemo(() => {
69
+ return (
70
+ <>
71
+ <SidebarItem
72
+ path="/dashboard"
73
+ permission={{
74
+ id: 0,
75
+ visible: true,
76
+ permission: "dashboard",
77
+ icon: "icons/home.svg",
78
+ sub: new Map(),
79
+ }}
80
+ isActive
81
+ navigateTo={navigateTo}
82
+ translate={(k) => i18n.t(k)}
83
+ icon={{
84
+ apiConfig: {
85
+ src: "https://www.svgrepo.com/show/521994/bag.svg",
86
+ },
87
+ }}
88
+ />
89
+
90
+ {Array.from(permissions.values()).map((perm) => (
91
+ <SidebarItem
92
+ key={perm.permission}
93
+ path={`/dashboard/${perm.permission}`}
94
+ permission={perm}
95
+ isActive={false}
96
+ navigateTo={navigateTo}
97
+ translate={(k) => i18n.t(k)}
98
+ icon={{
99
+ apiConfig: {
100
+ src: "https://www.svgrepo.com/show/521994/bag.svg",
101
+ },
102
+ }}
103
+ />
104
+ ))}
105
+
106
+ <Separator className="my-4" />
107
+
108
+ <SidebarItem
109
+ path="/dashboard/settings"
110
+ permission={{
111
+ id: 99,
112
+ visible: true,
113
+ permission: "settings",
114
+ icon: "icons/settings.svg",
115
+ sub: new Map(),
116
+ }}
117
+ isActive={false}
118
+ navigateTo={navigateTo}
119
+ translate={(k) => i18n.t(k)}
120
+ icon={{
121
+ apiConfig: {
122
+ src: "https://www.svgrepo.com/show/521994/bag.svg",
123
+ },
124
+ }}
125
+ />
126
+ </>
127
+ );
128
+ }, [navigateTo]);
129
+
130
+ return (
131
+ <Sidebar collapsed={collapsed} setCollapsed={setCollapsed}>
132
+ <Sidebar.Header>
133
+ <img
134
+ src="https://placehold.co/48x40"
135
+ className="w-12 h-10 rounded-lg"
136
+ />
137
+ <h1 className={`text-sm font-semibold ${collapsed && "lg:hidden"}`}>
138
+ {i18n.t("app_name")}
139
+ </h1>
140
+ </Sidebar.Header>
141
+
142
+ <Sidebar.Content className="pt-5">{sidebarItems}</Sidebar.Content>
143
+
144
+ <Sidebar.Footer>
145
+ <Button
146
+ variant="icon"
147
+ onClick={() => console.log("Exit")}
148
+ className="mb-4 mx-auto text-xs font-semibold"
149
+ >
150
+ <LucideCircleArrowOutDownLeft className="size-[18px]" />
151
+ <span className={collapsed ? "lg:hidden" : ""}>
152
+ {i18n.t("exit_dashb")}
153
+ </span>
154
+ </Button>
155
+ </Sidebar.Footer>
156
+ </Sidebar>
157
+ );
158
+ }
159
+
160
+ /* ------------------ Storybook Meta ------------------ */
161
+
162
+ const meta: Meta = {
163
+ title: "Layout/AppSidebar",
164
+ parameters: {
165
+ layout: "fullscreen",
166
+ },
167
+ };
168
+
169
+ export default meta;
170
+
171
+ export const Default: StoryObj = {
172
+ render: () => (
173
+ <MemoryRouter initialEntries={["/dashboard"]}>
174
+ <I18nextProvider i18n={i18n}>
175
+ <Routes>
176
+ <Route path="*" element={<StorySidebar />} />
177
+ </Routes>
178
+ </I18nextProvider>
179
+ </MemoryRouter>
180
+ ),
181
+ };
@@ -0,0 +1,284 @@
1
+ import React, { forwardRef, useEffect, useRef, useState } from "react";
2
+ import { cn } from "@/utils/cn";
3
+ import { AlignLeft, type LucideIcon } from "lucide-react";
4
+
5
+ export interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
6
+ children: React.ReactNode;
7
+ collapsed?: boolean;
8
+ setCollapsed?: (open: boolean) => void;
9
+ mobileHamburgerIcon?: {
10
+ icon: LucideIcon;
11
+ className?: string;
12
+ onClick?: (e: React.MouseEvent<SVGSVGElement, MouseEvent>) => void;
13
+ };
14
+ desktopHamburgerIcon?: {
15
+ icon: LucideIcon;
16
+ className?: string;
17
+ onClick?: (e: React.MouseEvent<SVGSVGElement, MouseEvent>) => void;
18
+ };
19
+ classNames?: {
20
+ wrapperClassName?: string;
21
+ };
22
+ }
23
+ interface SidebarExpandEventDetail {
24
+ hasChildren: boolean;
25
+ }
26
+ type SidebarCompound = React.FC<SidebarProps> & {
27
+ Header: typeof SidebarHeader;
28
+ Footer: typeof SidebarFooter;
29
+ Content: typeof SidebarContent;
30
+ };
31
+
32
+ const Sidebar: SidebarCompound = ({
33
+ children,
34
+ className,
35
+ collapsed,
36
+ setCollapsed,
37
+ mobileHamburgerIcon = { icon: AlignLeft },
38
+ desktopHamburgerIcon = { icon: AlignLeft },
39
+ classNames,
40
+ ...rest
41
+ }) => {
42
+ const { wrapperClassName } = classNames || {};
43
+ const [internalCollapsed, setInternalCollapsed] = useState<boolean>(() => {
44
+ const stored = localStorage.getItem("sidebar_collapsed");
45
+ return stored ? JSON.parse(stored) : false;
46
+ });
47
+ const sidebarRef = useRef<HTMLElement>(null); // This is already your nav ref
48
+ const open = collapsed ?? internalCollapsed;
49
+ const [hideSiderbar, setHideSiderbar] = useState(false);
50
+
51
+ useEffect(() => {
52
+ const handleExpandEvent = (e: Event) => {
53
+ const customEvent = e as CustomEvent;
54
+ const detail = customEvent.detail as SidebarExpandEventDetail;
55
+ if (detail?.hasChildren) {
56
+ if (open) {
57
+ if (setCollapsed) {
58
+ // Controlled: call parent callback
59
+ setCollapsed(false);
60
+ } else {
61
+ // Uncontrolled: update internal state + localStorage
62
+ setInternalCollapsed(() => {
63
+ localStorage.setItem("sidebar_collapsed", JSON.stringify(false));
64
+ return false;
65
+ });
66
+ }
67
+ }
68
+ }
69
+ };
70
+
71
+ if (sidebarRef.current) {
72
+ sidebarRef.current.addEventListener(
73
+ "sidebar-item-expand",
74
+ handleExpandEvent
75
+ );
76
+ }
77
+
78
+ return () => {
79
+ if (sidebarRef.current) {
80
+ sidebarRef.current.removeEventListener(
81
+ "sidebar-item-expand",
82
+ handleExpandEvent
83
+ );
84
+ }
85
+ };
86
+ }, [open]);
87
+ const toggle = (e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
88
+ if (desktopHamburgerIcon.onClick) desktopHamburgerIcon.onClick(e);
89
+ if (setCollapsed) {
90
+ // Controlled: call parent callback
91
+ setCollapsed(!open);
92
+ } else {
93
+ // Uncontrolled: update internal state + localStorage
94
+ setInternalCollapsed((prev) => {
95
+ const newValue = !prev;
96
+ localStorage.setItem("sidebar_collapsed", JSON.stringify(newValue));
97
+ return newValue;
98
+ });
99
+ }
100
+ };
101
+
102
+ let header: React.ReactNode = null;
103
+ let footer: React.ReactNode = null;
104
+ let content: React.ReactNode = null;
105
+
106
+ React.Children.forEach(children, (child) => {
107
+ if (!React.isValidElement(child)) return;
108
+
109
+ switch (child.type) {
110
+ case SidebarHeader:
111
+ header = child;
112
+ break;
113
+ case SidebarFooter:
114
+ footer = child;
115
+ break;
116
+ case SidebarContent:
117
+ content = child;
118
+ break;
119
+ }
120
+ });
121
+ // In Sidebar component, add this useEffect
122
+ useEffect(() => {
123
+ const closeAllDropdowns = () => {
124
+ if (open) {
125
+ // When sidebar is collapsed (w-12)
126
+ // Dispatch an event to tell all sidebar items to close their dropdowns
127
+ const closeEvent = new CustomEvent("sidebar-close-dropdowns", {
128
+ bubbles: true,
129
+ composed: true,
130
+ detail: { forceClose: true },
131
+ });
132
+
133
+ if (sidebarRef.current) {
134
+ sidebarRef.current.dispatchEvent(closeEvent);
135
+ }
136
+ }
137
+ };
138
+
139
+ closeAllDropdowns();
140
+ }, [open]); // Run whenever collapsed state changes
141
+
142
+ return (
143
+ <>
144
+ <div
145
+ onClick={() => setHideSiderbar(false)}
146
+ className={cn(
147
+ "fixed z-50 ltr:left-0 rtl:right-0 lg:hidden top-0 px-2 pt-1",
148
+ hideSiderbar
149
+ ? "w-screen h-screen bg-black/20" // Full screen with semi-transparent bg
150
+ : "w-auto h-auto", // Just icon size
151
+ wrapperClassName
152
+ )}
153
+ >
154
+ <mobileHamburgerIcon.icon
155
+ onClick={(e) => {
156
+ e.stopPropagation(); // Prevent event from bubbling to parent
157
+ if (mobileHamburgerIcon.onClick) mobileHamburgerIcon.onClick(e);
158
+ setHideSiderbar(true);
159
+ }}
160
+ className={cn(
161
+ `size-5 mt-2 text-tertiary hover:scale-105 transition-transform mx-auto cursor-pointer`,
162
+ hideSiderbar ? "hidden" : "block",
163
+ mobileHamburgerIcon.className
164
+ )}
165
+ />
166
+ </div>
167
+ <nav
168
+ ref={sidebarRef}
169
+ {...rest}
170
+ className={cn(
171
+ "overflow-x-hidden bg-card fixed grid lg:grid-rows-[auto_auto_1fr_auto] grid-rows-[auto_1fr_auto] overflow-hidden z-50 transition-all duration-200 dark:text-card-foreground text-primary-foreground h-screen",
172
+ // Mobile behavior
173
+ hideSiderbar
174
+ ? "ltr:left-0 rtl:right-0"
175
+ : "ltr:left-[-300px] rtl:right-[-300px]",
176
+ // Desktop behavior - always visible with proper width
177
+ "lg:relative! lg:left-0! lg:right-0! lg:w-[280px]!", // Default desktop width
178
+ // Collapsed state on desktop
179
+ open ? "lg:w-12!" : "lg:w-[280px]",
180
+ // Mobile width
181
+ `w-[280px]`, // Set a proper width for mobile too
182
+ className
183
+ )}
184
+ >
185
+ <desktopHamburgerIcon.icon
186
+ onClick={toggle}
187
+ className={cn(
188
+ `size-5 mt-2 text-tertiary hidden lg:block hover:scale-105 transition-transform mx-auto cursor-pointer`,
189
+ desktopHamburgerIcon.className
190
+ )}
191
+ />
192
+ {header}
193
+ {content}
194
+ {footer}
195
+ </nav>
196
+ </>
197
+ );
198
+ };
199
+ export interface SidebarContentProps
200
+ extends React.HTMLAttributes<HTMLDivElement> {}
201
+
202
+ // Use forwardRef to accept ref prop
203
+ export const SidebarContent = forwardRef<HTMLDivElement, SidebarContentProps>(
204
+ function SidebarContent({ children, className, ...rest }, ref) {
205
+ return (
206
+ <div
207
+ ref={ref} // Pass the ref here
208
+ {...rest}
209
+ className={cn(
210
+ "overflow-y-auto flex flex-col overflow-x-hidden pb-12",
211
+ className
212
+ )}
213
+ >
214
+ {children}
215
+ </div>
216
+ );
217
+ }
218
+ );
219
+
220
+ SidebarContent.displayName = "SidebarContent";
221
+
222
+ export interface SidebarFooterProps
223
+ extends React.HTMLAttributes<HTMLDivElement> {}
224
+
225
+ export const SidebarFooter = forwardRef<HTMLDivElement, SidebarContentProps>(
226
+ ({ children, className, ...rest }, ref) => {
227
+ return (
228
+ <div
229
+ ref={ref}
230
+ {...rest}
231
+ className={cn(
232
+ "sticky w-full overflow-x-hidden bottom-0 z-50 flex flex-col items-center gap-y-1 border-t border-secondary-foreground/15 pt-4 mt-4 bg-transparent",
233
+ className
234
+ )}
235
+ >
236
+ {children}
237
+ </div>
238
+ );
239
+ }
240
+ );
241
+
242
+ SidebarFooter.displayName = "SidebarFooter";
243
+
244
+ export interface SidebarHeaderProps
245
+ extends React.HTMLAttributes<HTMLDivElement> {}
246
+
247
+ export const SidebarHeader = forwardRef<HTMLDivElement, SidebarContentProps>(
248
+ ({ children, className, ...rest }, ref) => {
249
+ return (
250
+ <div
251
+ ref={ref}
252
+ {...rest}
253
+ className={cn(
254
+ "sticky w-full overflow-x-hidden top-0 z-50 flex flex-col justify-center items-center gap-y-1 border-b border-secondary-foreground/15 pb-4 mt-2 bg-transparent",
255
+ className
256
+ )}
257
+ >
258
+ {children}
259
+ </div>
260
+ );
261
+ }
262
+ );
263
+
264
+ SidebarHeader.displayName = "SidebarHeader";
265
+
266
+ export interface SeparatorProps
267
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {}
268
+
269
+ export function Separator({ className, ...rest }: SeparatorProps) {
270
+ return (
271
+ <div
272
+ {...rest}
273
+ className={cn("w-full bg-secondary-foreground/15 h-px", className)}
274
+ />
275
+ );
276
+ }
277
+ Separator.displayName = "Separator";
278
+
279
+ /* Compound attachments */
280
+ Sidebar.Header = SidebarHeader;
281
+ Sidebar.Footer = SidebarFooter;
282
+ Sidebar.Content = SidebarContent;
283
+
284
+ export default Sidebar;
@@ -1,5 +1,5 @@
1
+ import Textarea from "./textarea";
1
2
  import type { Meta, StoryObj } from "@storybook/react";
2
- import { Textarea } from "./textarea";
3
3
 
4
4
  const meta: Meta<typeof Textarea> = {
5
5
  title: "Form/Textarea",
@@ -44,7 +44,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
44
44
  >
45
45
  <div
46
46
  className={cn(
47
- "relative text-start select-none h-fit rtl:text-lg-rtl ltr:text-lg-ltr"
47
+ "relative text-start select-none h-fit rtl:text-[17px] ltr:text-[13px]"
48
48
  )}
49
49
  >
50
50
  {/* Required Hint */}
@@ -64,7 +64,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
64
64
  <label
65
65
  htmlFor={label}
66
66
  className={cn(
67
- "font-semibold rtl:text-xl-rtl ltr:text-lg-ltr inline-block pb-1"
67
+ "font-semibold rtl:text-md ltr:text-[13px] inline-block pb-1"
68
68
  )}
69
69
  >
70
70
  {label}
@@ -108,7 +108,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
108
108
  }}
109
109
  intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
110
110
  >
111
- <h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-sm-ltr">
111
+ <h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-[11px]">
112
112
  {errorMessage}
113
113
  </h1>
114
114
  </AnimatedItem>