@scripso-homepad/ui 0.3.9 → 0.4.1

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.
@@ -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,67 @@
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 transition-[background-color,justify-content,gap,padding] duration-300 ease-in-out',
39
+ isOpen ? 'justify-between gap-3 bg-black/12 p-3' : 'justify-center px-3 py-3',
40
+ className,
41
+ )}
42
+ >
43
+ <div
44
+ className="flex size-11 shrink-0 items-center justify-center rounded-full bg-white/5 text-sm font-bold text-white"
45
+ aria-hidden
46
+ >
47
+ {initials}
48
+ </div>
49
+ {isOpen ? (
50
+ <>
51
+ <div className="flex min-w-0 flex-1 flex-col overflow-hidden">
52
+ <p className="truncate text-sm font-bold text-white">{user.fullName}</p>
53
+ <p className="truncate text-xs text-storm-gray-100">{user.email}</p>
54
+ </div>
55
+ <button
56
+ type="button"
57
+ onClick={() => onLogout?.()}
58
+ className="shrink-0 cursor-pointer text-navy-150 transition-[opacity,transform] duration-300 ease-in-out hover:opacity-80 active:scale-95"
59
+ aria-label={logoutLabel}
60
+ >
61
+ <LogOut size={20} strokeWidth={1.75} />
62
+ </button>
63
+ </>
64
+ ) : null}
65
+ </div>
66
+ );
67
+ }
@@ -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
+ }