@scripso-homepad/ui 0.3.8 → 0.4.0

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 (37) hide show
  1. package/dist/chunk-C7GHBVMM.js +614 -0
  2. package/dist/chunk-C7GHBVMM.js.map +1 -0
  3. package/dist/index.cjs +33 -22
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +5 -1
  6. package/dist/index.d.ts +5 -1
  7. package/dist/index.js +29 -624
  8. package/dist/index.js.map +1 -1
  9. package/dist/web/index.cjs +1211 -0
  10. package/dist/web/index.cjs.map +1 -0
  11. package/dist/web/index.d.cts +156 -0
  12. package/dist/web/index.d.ts +156 -0
  13. package/dist/web/index.js +761 -0
  14. package/dist/web/index.js.map +1 -0
  15. package/package.json +26 -2
  16. package/src/components/Input.tsx +12 -1
  17. package/src/web/hooks/useMediaQuery.ts +23 -0
  18. package/src/web/hooks/useOnClickOutside.ts +30 -0
  19. package/src/web/icons/BellIcon.tsx +27 -0
  20. package/src/web/icons/BuildingIcon.tsx +55 -0
  21. package/src/web/index.ts +37 -0
  22. package/src/web/layout/AppHeader.stories.tsx +85 -0
  23. package/src/web/layout/AppHeader.tsx +115 -0
  24. package/src/web/layout/BuildingSelect.stories.tsx +60 -0
  25. package/src/web/layout/BuildingSelect.tsx +208 -0
  26. package/src/web/layout/DashboardLayout.stories.tsx +87 -0
  27. package/src/web/layout/DashboardLayout.tsx +37 -0
  28. package/src/web/layout/Sidebar.stories.tsx +80 -0
  29. package/src/web/layout/Sidebar.tsx +244 -0
  30. package/src/web/layout/SidebarMobileHeader.stories.tsx +47 -0
  31. package/src/web/layout/SidebarMobileHeader.tsx +48 -0
  32. package/src/web/layout/SidebarNavItem.tsx +60 -0
  33. package/src/web/layout/SidebarUserCard.tsx +79 -0
  34. package/src/web/layout/story-fixtures.tsx +93 -0
  35. package/src/web/layout/story-helpers.tsx +5 -0
  36. package/src/web/layout/types.ts +48 -0
  37. package/src/web/utils/cn.ts +6 -0
@@ -0,0 +1,244 @@
1
+ import { PanelLeftClose, PanelLeftOpen, X } from 'lucide-react';
2
+ import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
3
+ import { useLocation } from 'react-router-dom';
4
+
5
+ import { useMediaQuery } from '../hooks/useMediaQuery';
6
+ import { cn } from '../utils/cn';
7
+ import { SidebarNavItem } from './SidebarNavItem';
8
+ import { SidebarUserCard } from './SidebarUserCard';
9
+ import type { AdminNavItem, AdminSidebarBranding, AdminSidebarLabels, AdminSidebarUser } from './types';
10
+
11
+ export type SidebarProps = {
12
+ navItems: AdminNavItem[];
13
+ footerNavItems: AdminNavItem[];
14
+ branding: AdminSidebarBranding;
15
+ labels: AdminSidebarLabels;
16
+ sidebarStorageKey: string;
17
+ user?: AdminSidebarUser;
18
+ onLogout?: () => void;
19
+ className?: string;
20
+ mobileOpen: boolean;
21
+ onMobileClose?: () => void;
22
+ onNavItemClick?: (item: AdminNavItem) => void;
23
+ collapseIcon?: ReactNode;
24
+ expandIcon?: ReactNode;
25
+ closeIcon?: ReactNode;
26
+ initialCollapsed?: boolean;
27
+ collapsed?: boolean;
28
+ onCollapsedChange?: (collapsed: boolean) => void;
29
+ };
30
+
31
+ function readInitialCollapsedState(storageKey: string, initialCollapsed?: boolean): boolean {
32
+ if (initialCollapsed !== undefined) {
33
+ return initialCollapsed;
34
+ }
35
+
36
+ if (typeof window === 'undefined') {
37
+ return false;
38
+ }
39
+
40
+ const stored = window.localStorage.getItem(storageKey);
41
+ if (stored === null) {
42
+ return false;
43
+ }
44
+
45
+ return stored !== 'true';
46
+ }
47
+
48
+ export function Sidebar({
49
+ navItems,
50
+ footerNavItems,
51
+ branding,
52
+ labels,
53
+ sidebarStorageKey,
54
+ user,
55
+ onLogout,
56
+ className,
57
+ mobileOpen,
58
+ onMobileClose,
59
+ onNavItemClick,
60
+ collapseIcon,
61
+ expandIcon,
62
+ closeIcon,
63
+ initialCollapsed,
64
+ collapsed,
65
+ onCollapsedChange,
66
+ }: SidebarProps) {
67
+ const location = useLocation();
68
+ const isDesktop = useMediaQuery('(min-width: 768px)');
69
+ const [internalCollapsed, setInternalCollapsed] = useState(() =>
70
+ readInitialCollapsedState(sidebarStorageKey, initialCollapsed),
71
+ );
72
+
73
+ const isCollapsed = collapsed ?? internalCollapsed;
74
+ const isExpanded = isDesktop ? !isCollapsed : true;
75
+
76
+ const setCollapsed = useCallback(
77
+ (next: boolean) => {
78
+ if (collapsed === undefined) {
79
+ setInternalCollapsed(next);
80
+ window.localStorage.setItem(sidebarStorageKey, String(!next));
81
+ }
82
+
83
+ onCollapsedChange?.(next);
84
+ },
85
+ [collapsed, onCollapsedChange, sidebarStorageKey],
86
+ );
87
+
88
+ const toggleCollapsed = useCallback(() => {
89
+ setCollapsed(!isCollapsed);
90
+ }, [isCollapsed, setCollapsed]);
91
+
92
+ const handleHeaderAction = useCallback(() => {
93
+ if (isDesktop) {
94
+ toggleCollapsed();
95
+
96
+ return;
97
+ }
98
+
99
+ onMobileClose?.();
100
+ }, [isDesktop, onMobileClose, toggleCollapsed]);
101
+
102
+ const handleNavItemNavigate = useCallback(
103
+ (item: AdminNavItem) => {
104
+ onNavItemClick?.(item);
105
+
106
+ if (!isDesktop) {
107
+ onMobileClose?.();
108
+ }
109
+ },
110
+ [isDesktop, onMobileClose, onNavItemClick],
111
+ );
112
+
113
+ const pathnameRef = useRef(location.pathname);
114
+
115
+ useEffect(() => {
116
+ if (pathnameRef.current === location.pathname) {
117
+ return;
118
+ }
119
+
120
+ pathnameRef.current = location.pathname;
121
+
122
+ if (!isDesktop) {
123
+ onMobileClose?.();
124
+ }
125
+ }, [location.pathname, isDesktop, onMobileClose]);
126
+
127
+ useEffect(() => {
128
+ if (!mobileOpen || isDesktop) {
129
+ return;
130
+ }
131
+
132
+ const previousOverflow = document.body.style.overflow;
133
+ document.body.style.overflow = 'hidden';
134
+
135
+ return () => {
136
+ document.body.style.overflow = previousOverflow;
137
+ };
138
+ }, [mobileOpen, isDesktop]);
139
+
140
+ const visibleNavItems = navItems.filter((item) => !item.hidden);
141
+ const visibleFooterNavItems = footerNavItems.filter((item) => !item.hidden);
142
+
143
+ return (
144
+ <>
145
+ <button
146
+ type="button"
147
+ aria-label={labels.closeMenu}
148
+ onClick={onMobileClose}
149
+ className={cn(
150
+ 'fixed inset-0 z-40 bg-navy-800/60 backdrop-blur-[1px] transition-opacity duration-300 ease-in-out md:hidden',
151
+ mobileOpen ? 'opacity-100' : 'pointer-events-none opacity-0',
152
+ )}
153
+ />
154
+
155
+ <aside
156
+ className={cn(
157
+ 'fixed inset-y-0 left-0 z-50 flex h-screen w-[min(300px,85vw)] flex-col gap-8 overflow-hidden bg-navy p-4 shadow-xl',
158
+ 'transition-[transform,width,padding,gap] duration-300 ease-in-out will-change-[transform,width]',
159
+ mobileOpen ? 'translate-x-0' : '-translate-x-full',
160
+ 'md:relative md:z-auto md:w-[300px] md:translate-x-0 md:shadow-none',
161
+ isExpanded ? 'md:w-[300px]' : 'md:w-[52px] md:items-center md:gap-4 md:px-3 md:py-4',
162
+ className,
163
+ )}
164
+ aria-hidden={!isDesktop && !mobileOpen}
165
+ >
166
+ <div
167
+ className={cn(
168
+ 'flex w-full items-center transition-[justify-content] duration-300 ease-in-out',
169
+ isExpanded ? 'justify-between' : 'justify-center',
170
+ )}
171
+ >
172
+ <div
173
+ className={cn(
174
+ 'flex min-w-0 items-center gap-1.5 overflow-hidden transition-[max-width,opacity] duration-300 ease-in-out',
175
+ isExpanded ? 'max-w-[240px] opacity-100' : 'max-w-0 opacity-0',
176
+ )}
177
+ >
178
+ <img src={branding.logoIconSrc} alt="" className="h-5 w-[29px] shrink-0" />
179
+ <div className="flex min-w-0 flex-col leading-none">
180
+ <span className="text-[21px] font-bold tracking-tight whitespace-nowrap text-white">
181
+ {branding.logoTitle}
182
+ </span>
183
+ <span className="text-[6.5px] font-normal tracking-[-0.26px] text-navy-300 uppercase">
184
+ {branding.logoTagline}
185
+ </span>
186
+ </div>
187
+ </div>
188
+ <button
189
+ type="button"
190
+ onClick={handleHeaderAction}
191
+ className="shrink-0 cursor-pointer text-white transition-[opacity,transform] duration-200 hover:opacity-80 active:scale-95"
192
+ aria-label={
193
+ isDesktop
194
+ ? isExpanded
195
+ ? labels.collapse
196
+ : labels.expand
197
+ : labels.closeMenu
198
+ }
199
+ >
200
+ {isDesktop ? (
201
+ isExpanded ? (
202
+ collapseIcon ?? <PanelLeftClose size={20} strokeWidth={1.75} />
203
+ ) : (
204
+ expandIcon ?? <PanelLeftOpen size={20} strokeWidth={1.75} />
205
+ )
206
+ ) : (
207
+ closeIcon ?? <X size={20} strokeWidth={1.75} />
208
+ )}
209
+ </button>
210
+ </div>
211
+
212
+ <nav className="flex flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden">
213
+ {visibleNavItems.map((item) => (
214
+ <SidebarNavItem
215
+ key={item.to}
216
+ item={item}
217
+ isOpen={isExpanded}
218
+ onNavigate={handleNavItemNavigate}
219
+ />
220
+ ))}
221
+ </nav>
222
+
223
+ <div className="flex w-full flex-col gap-4">
224
+ {visibleFooterNavItems.map((item) => (
225
+ <SidebarNavItem
226
+ key={item.to}
227
+ item={item}
228
+ isOpen={isExpanded}
229
+ onNavigate={handleNavItemNavigate}
230
+ />
231
+ ))}
232
+ {user ? (
233
+ <SidebarUserCard
234
+ user={user}
235
+ isOpen={isExpanded}
236
+ logoutLabel={labels.logout}
237
+ onLogout={onLogout}
238
+ />
239
+ ) : null}
240
+ </div>
241
+ </aside>
242
+ </>
243
+ );
244
+ }
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+
4
+ import { WebLayoutCanvas } from './story-helpers';
5
+ import { SidebarMobileHeader } from './SidebarMobileHeader';
6
+ import { storyBranding, storyLabels, storyLogoIconDarkSrc } from './story-fixtures';
7
+
8
+ const meta = {
9
+ title: 'Web Layout/SidebarMobileHeader',
10
+ component: SidebarMobileHeader,
11
+ parameters: {
12
+ layout: 'fullscreen',
13
+ webLayout: true,
14
+ },
15
+ args: {
16
+ branding: {
17
+ logoIconSrc: storyLogoIconDarkSrc,
18
+ logoTitle: storyBranding.logoTitle,
19
+ logoTagline: storyBranding.logoTagline,
20
+ },
21
+ openMenuLabel: storyLabels.sidebar.openMenu,
22
+ onOpenMenu: fn(),
23
+ menuOpen: false,
24
+ },
25
+ } satisfies Meta<typeof SidebarMobileHeader>;
26
+
27
+ export default meta;
28
+ type Story = StoryObj<typeof meta>;
29
+
30
+ export const Default: Story = {
31
+ render: (args) => (
32
+ <WebLayoutCanvas>
33
+ <SidebarMobileHeader {...args} />
34
+ </WebLayoutCanvas>
35
+ ),
36
+ };
37
+
38
+ export const MenuOpen: Story = {
39
+ args: {
40
+ menuOpen: true,
41
+ },
42
+ render: (args) => (
43
+ <WebLayoutCanvas>
44
+ <SidebarMobileHeader {...args} />
45
+ </WebLayoutCanvas>
46
+ ),
47
+ };
@@ -0,0 +1,48 @@
1
+ import { Menu } from 'lucide-react';
2
+
3
+ import { cn } from '../utils/cn';
4
+ import type { AdminSidebarBranding } from './types';
5
+
6
+ export type SidebarMobileHeaderProps = {
7
+ branding: Pick<AdminSidebarBranding, 'logoIconSrc' | 'logoTitle' | 'logoTagline'>;
8
+ openMenuLabel: string;
9
+ onOpenMenu: () => void;
10
+ menuOpen?: boolean;
11
+ className?: string;
12
+ };
13
+
14
+ export function SidebarMobileHeader({
15
+ branding,
16
+ openMenuLabel,
17
+ onOpenMenu,
18
+ menuOpen = false,
19
+ className,
20
+ }: SidebarMobileHeaderProps) {
21
+ return (
22
+ <header
23
+ className={cn(
24
+ 'sticky top-0 z-30 flex h-14 shrink-0 items-center gap-3 border-b border-storm-gray-50 bg-storm-gray-0 px-4 md:hidden',
25
+ className,
26
+ )}
27
+ >
28
+ <button
29
+ type="button"
30
+ onClick={onOpenMenu}
31
+ className="flex size-10 items-center justify-center rounded-xl text-navy transition-colors hover:bg-storm-gray-50"
32
+ aria-label={openMenuLabel}
33
+ aria-expanded={menuOpen}
34
+ >
35
+ <Menu size={22} strokeWidth={1.75} />
36
+ </button>
37
+ <div className="flex min-w-0 items-center gap-2">
38
+ <img src={branding.logoIconSrc} alt="" className="h-[30px] w-[42px] shrink-0" />
39
+ <div className="min-w-0 leading-none">
40
+ <p className="truncate text-base font-bold tracking-tight text-navy">{branding.logoTitle}</p>
41
+ <p className="truncate text-[6.5px] font-normal tracking-[-0.26px] text-navy-300 uppercase">
42
+ {branding.logoTagline}
43
+ </p>
44
+ </div>
45
+ </div>
46
+ </header>
47
+ );
48
+ }
@@ -0,0 +1,60 @@
1
+ import { NavLink } from 'react-router-dom';
2
+
3
+ import { cn } from '../utils/cn';
4
+ import type { AdminNavItem } from './types';
5
+
6
+ export type SidebarNavItemProps = {
7
+ item: AdminNavItem;
8
+ isOpen: boolean;
9
+ onNavigate?: (item: AdminNavItem) => void;
10
+ };
11
+
12
+ export function SidebarNavItem({ item, isOpen, onNavigate }: SidebarNavItemProps) {
13
+ if (item.hidden) {
14
+ return null;
15
+ }
16
+
17
+ const handleClick = () => {
18
+ item.onClick?.();
19
+ onNavigate?.(item);
20
+ };
21
+
22
+ return (
23
+ <NavLink
24
+ to={item.to}
25
+ end={item.end}
26
+ onClick={handleClick}
27
+ aria-disabled={item.disabled}
28
+ tabIndex={item.disabled ? -1 : undefined}
29
+ className={({ isActive }) =>
30
+ cn(
31
+ 'relative flex items-center rounded-xl px-4 py-3 transition-[background-color,color,gap,padding] duration-300 ease-in-out',
32
+ isOpen ? 'w-full gap-4' : 'justify-center gap-0 px-3',
33
+ item.disabled && 'pointer-events-none opacity-50',
34
+ isActive ? 'bg-black/12 text-white' : 'text-navy-100 hover:bg-white/5',
35
+ )
36
+ }
37
+ >
38
+ {({ isActive }) => (
39
+ <>
40
+ <span
41
+ aria-hidden
42
+ className={cn(
43
+ 'absolute top-1.5 -left-1 h-8 w-2 rounded-full bg-white transition-[opacity,transform] duration-300 ease-in-out',
44
+ isActive ? 'scale-100 opacity-100' : 'scale-75 opacity-0',
45
+ )}
46
+ />
47
+ <span className="flex size-5 shrink-0 items-center justify-center">{item.icon}</span>
48
+ <span
49
+ className={cn(
50
+ 'overflow-hidden text-sm font-semibold whitespace-nowrap transition-[max-width,opacity] duration-300 ease-in-out',
51
+ isOpen ? 'max-w-[180px] opacity-100' : 'max-w-0 opacity-0',
52
+ )}
53
+ >
54
+ {item.label}
55
+ </span>
56
+ </>
57
+ )}
58
+ </NavLink>
59
+ );
60
+ }
@@ -0,0 +1,79 @@
1
+ import { LogOut } from 'lucide-react';
2
+
3
+ import { cn } from '../utils/cn';
4
+ import type { AdminSidebarUser } from './types';
5
+
6
+ export type SidebarUserCardProps = {
7
+ user: AdminSidebarUser;
8
+ isOpen: boolean;
9
+ logoutLabel: string;
10
+ onLogout?: () => void;
11
+ className?: string;
12
+ };
13
+
14
+ function getInitials(fullName: string, email: string): string {
15
+ const parts = fullName.trim().split(/\s+/).filter(Boolean);
16
+ if (parts.length > 0) {
17
+ return parts
18
+ .slice(0, 2)
19
+ .map((part) => part[0]?.toUpperCase() ?? '')
20
+ .join('');
21
+ }
22
+
23
+ return email.slice(0, 2).toUpperCase();
24
+ }
25
+
26
+ export function SidebarUserCard({
27
+ user,
28
+ isOpen,
29
+ logoutLabel,
30
+ onLogout,
31
+ className,
32
+ }: SidebarUserCardProps) {
33
+ const initials = user.initials ?? getInitials(user.fullName, user.email);
34
+
35
+ return (
36
+ <div
37
+ className={cn(
38
+ 'flex w-full items-center rounded-xl p-3 transition-[background-color,justify-content,gap] duration-300 ease-in-out',
39
+ isOpen ? 'justify-between gap-3 bg-black/12' : 'justify-center',
40
+ className,
41
+ )}
42
+ >
43
+ <div
44
+ className={cn(
45
+ 'flex min-w-0 items-center transition-[gap,flex] duration-300 ease-in-out',
46
+ isOpen ? 'flex-1 gap-3' : 'gap-0',
47
+ )}
48
+ >
49
+ <div
50
+ className="flex size-11 shrink-0 items-center justify-center rounded-full bg-white/5 text-sm font-bold text-white"
51
+ aria-hidden
52
+ >
53
+ {initials}
54
+ </div>
55
+ <div
56
+ className={cn(
57
+ 'min-w-0 overflow-hidden transition-[max-width,opacity] duration-300 ease-in-out',
58
+ isOpen ? 'max-w-[160px] opacity-100' : 'max-w-0 opacity-0',
59
+ )}
60
+ >
61
+ <p className="truncate text-sm font-bold text-white">{user.fullName}</p>
62
+ <p className="truncate text-xs text-storm-gray-100">{user.email}</p>
63
+ </div>
64
+ </div>
65
+ <button
66
+ type="button"
67
+ onClick={() => onLogout?.()}
68
+ className={cn(
69
+ 'shrink-0 cursor-pointer text-navy-150 transition-[opacity,transform,max-width] duration-300 ease-in-out hover:opacity-80 active:scale-95',
70
+ isOpen ? 'max-w-8 scale-100 opacity-100' : 'pointer-events-none max-w-0 scale-90 opacity-0',
71
+ )}
72
+ aria-label={logoutLabel}
73
+ tabIndex={isOpen ? 0 : -1}
74
+ >
75
+ <LogOut size={20} strokeWidth={1.75} />
76
+ </button>
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,93 @@
1
+ import {
2
+ FilePenLine,
3
+ LayoutGrid,
4
+ Megaphone,
5
+ Settings,
6
+ UserRoundCog,
7
+ Wallet,
8
+ } from 'lucide-react';
9
+
10
+ import type {
11
+ AdminLayoutLabels,
12
+ AdminNavItem,
13
+ AdminSidebarBranding,
14
+ AdminSidebarUser,
15
+ } from './types';
16
+
17
+ const iconSize = { size: 20, strokeWidth: 1.75 } as const;
18
+
19
+ export const storyLogoIconSrc = '/logo-icon.svg';
20
+ export const storyLogoIconDarkSrc = '/logo-icon-dark.svg';
21
+
22
+ export const storyBranding: AdminSidebarBranding = {
23
+ logoIconSrc: storyLogoIconSrc,
24
+ logoTitle: 'HomePad',
25
+ logoTagline: 'Building Management Platform',
26
+ };
27
+
28
+ export const storyLabels: AdminLayoutLabels = {
29
+ sidebar: {
30
+ collapse: 'Collapse sidebar',
31
+ expand: 'Expand sidebar',
32
+ openMenu: 'Open menu',
33
+ closeMenu: 'Close menu',
34
+ logout: 'Log out',
35
+ },
36
+ header: {
37
+ searchPlaceholder: 'Search...',
38
+ selectBuilding: 'Select building',
39
+ notifications: 'Notifications',
40
+ },
41
+ };
42
+
43
+ export const storyBuildingOptions = [
44
+ { value: 'komitas-15-3', label: 'Komitas 15/3' },
45
+ { value: 'abovyan-12', label: 'Abovyan 12' },
46
+ { value: 'saryan-8', label: 'Saryan 8' },
47
+ { value: 'tumanyan-4', label: 'Tumanyan 4' },
48
+ { value: 'mashtots-21', label: 'Mashtots 21' },
49
+ { value: 'baghramyan-9', label: 'Baghramyan 9' },
50
+ { value: 'pushkin-2', label: 'Pushkin 2' },
51
+ ];
52
+
53
+ export const storyUser: AdminSidebarUser = {
54
+ fullName: 'Anna Grigoryan',
55
+ email: 'anna466@gmail.com',
56
+ };
57
+
58
+ export const storyNavItems: AdminNavItem[] = [
59
+ {
60
+ to: '/',
61
+ label: 'Dashboard',
62
+ icon: <LayoutGrid {...iconSize} />,
63
+ end: true,
64
+ },
65
+ {
66
+ to: '/residents',
67
+ label: 'Residents',
68
+ icon: <UserRoundCog {...iconSize} />,
69
+ },
70
+ {
71
+ to: '/requests',
72
+ label: 'Requests',
73
+ icon: <FilePenLine {...iconSize} />,
74
+ },
75
+ {
76
+ to: '/payments',
77
+ label: 'Payments',
78
+ icon: <Wallet {...iconSize} />,
79
+ },
80
+ {
81
+ to: '/announcements',
82
+ label: 'Announcements',
83
+ icon: <Megaphone {...iconSize} />,
84
+ },
85
+ ];
86
+
87
+ export const storyFooterNavItems: AdminNavItem[] = [
88
+ {
89
+ to: '/settings',
90
+ label: 'Settings',
91
+ icon: <Settings {...iconSize} />,
92
+ },
93
+ ];
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export function WebLayoutCanvas({ children }: { children: ReactNode }) {
4
+ return <div className="w-full min-w-0">{children}</div>;
5
+ }
@@ -0,0 +1,48 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export type AdminNavItem = {
4
+ to: string;
5
+ label: string;
6
+ icon: ReactNode;
7
+ end?: boolean;
8
+ hidden?: boolean;
9
+ disabled?: boolean;
10
+ onClick?: () => void;
11
+ };
12
+
13
+ export type AdminSidebarUser = {
14
+ fullName: string;
15
+ email: string;
16
+ initials?: string;
17
+ };
18
+
19
+ export type AdminSidebarBranding = {
20
+ logoIconSrc: string;
21
+ logoTitle: string;
22
+ logoTagline: string;
23
+ };
24
+
25
+ export type AdminSidebarLabels = {
26
+ collapse: string;
27
+ expand: string;
28
+ openMenu: string;
29
+ closeMenu: string;
30
+ logout: string;
31
+ };
32
+
33
+ export type AdminHeaderLabels = {
34
+ searchPlaceholder: string;
35
+ selectBuilding: string;
36
+ notifications: string;
37
+ };
38
+
39
+ export type AdminBuildingOption = {
40
+ value: string;
41
+ label: string;
42
+ disabled?: boolean;
43
+ };
44
+
45
+ export type AdminLayoutLabels = {
46
+ sidebar: AdminSidebarLabels;
47
+ header: AdminHeaderLabels;
48
+ };
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs));
6
+ }