@scripso-homepad/ui 0.3.9 → 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.
- package/dist/chunk-C7GHBVMM.js +614 -0
- package/dist/chunk-C7GHBVMM.js.map +1 -0
- package/dist/index.js +29 -635
- package/dist/index.js.map +1 -1
- package/dist/web/index.cjs +1211 -0
- package/dist/web/index.cjs.map +1 -0
- package/dist/web/index.d.cts +156 -0
- package/dist/web/index.d.ts +156 -0
- package/dist/web/index.js +761 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +26 -2
- package/src/web/hooks/useMediaQuery.ts +23 -0
- package/src/web/hooks/useOnClickOutside.ts +30 -0
- package/src/web/icons/BellIcon.tsx +27 -0
- package/src/web/icons/BuildingIcon.tsx +55 -0
- package/src/web/index.ts +37 -0
- package/src/web/layout/AppHeader.stories.tsx +85 -0
- package/src/web/layout/AppHeader.tsx +115 -0
- package/src/web/layout/BuildingSelect.stories.tsx +60 -0
- package/src/web/layout/BuildingSelect.tsx +208 -0
- package/src/web/layout/DashboardLayout.stories.tsx +87 -0
- package/src/web/layout/DashboardLayout.tsx +37 -0
- package/src/web/layout/Sidebar.stories.tsx +80 -0
- package/src/web/layout/Sidebar.tsx +244 -0
- package/src/web/layout/SidebarMobileHeader.stories.tsx +47 -0
- package/src/web/layout/SidebarMobileHeader.tsx +48 -0
- package/src/web/layout/SidebarNavItem.tsx +60 -0
- package/src/web/layout/SidebarUserCard.tsx +79 -0
- package/src/web/layout/story-fixtures.tsx +93 -0
- package/src/web/layout/story-helpers.tsx +5 -0
- package/src/web/layout/types.ts +48 -0
- package/src/web/utils/cn.ts +6 -0
|
@@ -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,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
|
+
};
|