notionsoft-ui 1.0.34 → 1.0.36

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 (33) 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/cached-image/cached-image.stories.tsx +109 -0
  11. package/src/notion-ui/cached-image/cached-image.tsx +213 -0
  12. package/src/notion-ui/cached-image/index.ts +3 -0
  13. package/src/notion-ui/cached-image/utils.ts +7 -0
  14. package/src/notion-ui/cached-svg/CachedSvg.stories.tsx +74 -0
  15. package/src/notion-ui/cached-svg/cached-svg.tsx +150 -0
  16. package/src/notion-ui/cached-svg/index.ts +3 -0
  17. package/src/notion-ui/cached-svg/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/page-size-select.tsx +29 -12
  24. package/src/notion-ui/password-input/password-input.tsx +3 -3
  25. package/src/notion-ui/phone-input/phone-input.tsx +38 -8
  26. package/src/notion-ui/shimmer/shimmer.tsx +9 -3
  27. package/src/notion-ui/shining-text/shining-text.tsx +2 -6
  28. package/src/notion-ui/sidebar/index.ts +3 -0
  29. package/src/notion-ui/sidebar/sidebar-item.tsx +198 -0
  30. package/src/notion-ui/sidebar/sidebar.stories.tsx +181 -0
  31. package/src/notion-ui/sidebar/sidebar.tsx +284 -0
  32. package/src/notion-ui/textarea/Textarea.stories.tsx +1 -1
  33. package/src/notion-ui/textarea/textarea.tsx +3 -3
@@ -0,0 +1,198 @@
1
+ import { useMemo, useCallback, memo, useEffect, useState } from "react";
2
+ import { ChevronRight } from "lucide-react";
3
+ import { useLocation } from "react-router";
4
+ import CachedSvg, {
5
+ CachedSvgProps,
6
+ } from "@/components/notion-ui/cached-svg/cached-svg";
7
+ import AnimatedItem from "@/components/notion-ui/animated-item";
8
+ import { cn } from "@/utils/cn";
9
+
10
+ export interface SubPermission {
11
+ id: number;
12
+ name: string;
13
+ is_category: boolean;
14
+ }
15
+
16
+ export type Permission = {
17
+ id: number;
18
+ visible: boolean;
19
+ permission: string;
20
+ icon: string;
21
+ sub: Map<number, SubPermission>;
22
+ };
23
+
24
+ export interface SidebarItemProps {
25
+ path: string;
26
+ isActive: boolean;
27
+ permission: Permission;
28
+ icon: CachedSvgProps;
29
+ navigateTo: (path: string) => void;
30
+ translate?: (key: string) => string;
31
+ classNames?: {};
32
+ }
33
+
34
+ export const SidebarItem = memo(function SidebarItem({
35
+ isActive,
36
+ navigateTo,
37
+ permission,
38
+ path,
39
+ icon,
40
+ translate,
41
+ }: SidebarItemProps) {
42
+ const location = useLocation();
43
+ const [showDropdown, setShowDropdown] = useState(false);
44
+
45
+ // Calculate categories and selectedSubId
46
+ const { categories, selectedSubId } = useMemo(() => {
47
+ const subs = Array.from(permission.sub.values()).filter(
48
+ (sub) => sub.is_category
49
+ );
50
+ const selectedId = Number(location.pathname.split("/").pop());
51
+ return { categories: subs, selectedSubId: selectedId };
52
+ }, [permission.sub, location.pathname]);
53
+
54
+ // Auto-open dropdown if current URL matches any category
55
+ useEffect(() => {
56
+ const matched = categories.find((sub) =>
57
+ location.pathname.includes(`${path}/${sub.id}`)
58
+ );
59
+ setShowDropdown(matched ? true : false);
60
+ }, [location.pathname, categories, path]);
61
+ useEffect(() => {
62
+ const handleCloseDropdowns = (e: Event) => {
63
+ const customEvent = e as CustomEvent;
64
+ if (customEvent.detail?.forceClose) {
65
+ setShowDropdown(false);
66
+ }
67
+ };
68
+
69
+ // Listen for close events
70
+ window.addEventListener("sidebar-close-dropdowns", handleCloseDropdowns);
71
+
72
+ return () => {
73
+ window.removeEventListener(
74
+ "sidebar-close-dropdowns",
75
+ handleCloseDropdowns
76
+ );
77
+ };
78
+ }, []);
79
+ const handleClick = useCallback(
80
+ (e: React.MouseEvent) => {
81
+ if (categories.length === 0) {
82
+ navigateTo(path);
83
+ } else {
84
+ setShowDropdown((prev) => !prev);
85
+ // Dispatch custom event to parent (Sidebar)
86
+ const expandEvent = new CustomEvent("sidebar-item-expand", {
87
+ bubbles: true, // This makes the event bubble up through DOM
88
+ composed: true, // This allows it to cross shadow DOM boundaries if any
89
+ detail: {
90
+ hasChildren: true,
91
+ },
92
+ });
93
+
94
+ // Dispatch from the clicked element
95
+ e.currentTarget.dispatchEvent(expandEvent);
96
+ }
97
+ },
98
+ [categories.length, navigateTo, path]
99
+ );
100
+
101
+ const handleCategoryClick = useCallback(
102
+ (cat: SubPermission) => {
103
+ navigateTo(`${path}/${cat.id}`);
104
+ },
105
+ [navigateTo, path]
106
+ );
107
+
108
+ const spring = useMemo(
109
+ () => ({
110
+ springProps: {
111
+ from: { opacity: 0, transform: "translateY(-8px)" },
112
+ config: { mass: 1, tension: 210, friction: 20 },
113
+ to: { opacity: 1, transform: "translateY(0px)" },
114
+ },
115
+ intersectionArgs: { rootMargin: "-10% 0%", once: true },
116
+ }),
117
+ []
118
+ );
119
+
120
+ const dropdownContent = useMemo(() => {
121
+ if (!showDropdown || categories.length === 0) return null;
122
+ return (
123
+ <div className="relative ltr:ml-5 rtl:mr-5 mt-1 mb-4 space-y-1 ltr:pl-2 rtl:pr-2 before:absolute before:top-3 before:bottom-0 rtl:before:right-1 ltr:before:left-1 before:w-px rounded-full before:bg-primary/30">
124
+ {categories.map((cat, index) => {
125
+ const selected = selectedSubId === cat.id;
126
+ return (
127
+ <AnimatedItem
128
+ key={cat.id}
129
+ springProps={{
130
+ ...spring.springProps,
131
+ delay: index * 100,
132
+ to: {
133
+ ...spring.springProps.to,
134
+ delay: index * 100,
135
+ },
136
+ }}
137
+ intersectionArgs={spring.intersectionArgs}
138
+ >
139
+ <div className="relative flex items-center before:absolute ltr:before:left-1 rtl:before:right-1 before:top-1/2 before:w-3 before:h-px before:bg-primary/40">
140
+ <button
141
+ onClick={handleCategoryClick.bind(null, cat)}
142
+ className={`cursor-pointer text-primary/80 ltr:ml-5 rtl:mr-5 rtl:text-sm rtl:font-bold ltr:text-xs flex items-center gap-x-2 py-1 px-2 w-[85%] rounded-sm transition-colors ${
143
+ selected
144
+ ? "font-semibold bg-tertiary/10 text-primary"
145
+ : "hover:opacity-75"
146
+ }`}
147
+ >
148
+ {translate ? translate(cat.name) : cat.name}
149
+ </button>
150
+ </div>
151
+ </AnimatedItem>
152
+ );
153
+ })}
154
+ </div>
155
+ );
156
+ }, [
157
+ showDropdown,
158
+ categories,
159
+ selectedSubId,
160
+ spring,
161
+ handleCategoryClick,
162
+ translate,
163
+ ]);
164
+ return (
165
+ <>
166
+ <div
167
+ onClick={handleClick}
168
+ className={cn(
169
+ `grid grid-cols-[1fr_auto] ltr:py-2 rtl:p-1 ltr:pl-2.5 rtl:pr-2 ltr:mx-1 rtl:mx-1.5 text-primary items-center rtl:text-lg ltr:text-xs cursor-pointer rounded-md ${
170
+ isActive
171
+ ? `bg-tertiary/90 text-card font-semibold`
172
+ : "hover:opacity-75"
173
+ }`
174
+ )}
175
+ key={permission.permission}
176
+ >
177
+ <div className="flex items-center gap-x-4 w-full">
178
+ <CachedSvg {...icon} className={cn("rtl", icon.className)} />
179
+ <h1 className="truncate">
180
+ {translate
181
+ ? translate(permission.permission)
182
+ : permission.permission}
183
+ </h1>
184
+ </div>
185
+
186
+ {categories.length > 0 && (
187
+ <ChevronRight
188
+ className={`size-3.5 min-h-3.5 min-w-3.5 text-primary ltr:mr-2 transition-transform duration-300 ease-in-out ${
189
+ showDropdown ? "rotate-90" : "rtl:rotate-180"
190
+ }`}
191
+ />
192
+ )}
193
+ </div>
194
+
195
+ {dropdownContent}
196
+ </>
197
+ );
198
+ }); // Pass custom comparison function
@@ -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>