@scripso-homepad/ui 0.4.25 → 0.4.26

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scripso-homepad/ui",
3
- "version": "0.4.25",
3
+ "version": "0.4.26",
4
4
  "type": "module",
5
5
  "description": "Cross-platform UI components for Homepad (React Web + React Native)",
6
6
  "license": "MIT",
@@ -218,7 +218,7 @@ export function Sidebar({
218
218
  </button>
219
219
  </div>
220
220
 
221
- <nav className="flex flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden">
221
+ <nav className="flex w-full min-h-0 flex-1 flex-col gap-2 overflow-x-hidden overflow-y-auto">
222
222
  {visibleNavItems.map((item) => (
223
223
  <SidebarNavItem
224
224
  key={item.to}
@@ -228,27 +228,26 @@ export function Sidebar({
228
228
  onNavigate={handleNavItemNavigate}
229
229
  />
230
230
  ))}
231
+ <div className="mt-auto flex w-full flex-col gap-2">
232
+ {visibleFooterNavItems.map((item) => (
233
+ <SidebarNavItem
234
+ key={item.to}
235
+ item={item}
236
+ isOpen={isExpanded}
237
+ isActive={isNavItemActive(pathname, item)}
238
+ onNavigate={handleNavItemNavigate}
239
+ />
240
+ ))}
241
+ {user ? (
242
+ <SidebarUserCard
243
+ user={user}
244
+ isOpen={isExpanded}
245
+ logoutLabel={labels.logout}
246
+ onLogout={onLogout}
247
+ />
248
+ ) : null}
249
+ </div>
231
250
  </nav>
232
-
233
- <div className="flex w-full flex-col gap-4">
234
- {visibleFooterNavItems.map((item) => (
235
- <SidebarNavItem
236
- key={item.to}
237
- item={item}
238
- isOpen={isExpanded}
239
- isActive={isNavItemActive(pathname, item)}
240
- onNavigate={handleNavItemNavigate}
241
- />
242
- ))}
243
- {user ? (
244
- <SidebarUserCard
245
- user={user}
246
- isOpen={isExpanded}
247
- logoutLabel={labels.logout}
248
- onLogout={onLogout}
249
- />
250
- ) : null}
251
- </div>
252
251
  </aside>
253
252
  </>
254
253
  );
@@ -36,9 +36,9 @@ export function SidebarNavItem({ item, isOpen, isActive, onNavigate }: SidebarNa
36
36
  tabIndex={item.disabled ? -1 : undefined}
37
37
  className={cn(
38
38
  'relative flex items-center rounded-xl px-4 py-3 transition-[background-color,color,gap,padding] duration-300 ease-in-out',
39
- isOpen ? 'w-full gap-4' : 'justify-center gap-0 px-3',
39
+ isOpen ? 'w-full gap-4' : 'w-full justify-center gap-0 px-3',
40
40
  item.disabled && 'pointer-events-none opacity-50',
41
- isActive ? 'bg-black/12 text-white' : 'text-navy-100 hover:bg-white/5',
41
+ isActive ? 'bg-black/12 text-white shadow-none' : 'text-navy-100 shadow-none hover:bg-white/5',
42
42
  )}
43
43
  >
44
44
  <span
@@ -1,4 +1,5 @@
1
1
  import { LogOut } from 'lucide-react';
2
+ import { useEffect, useId, useRef, useState } from 'react';
2
3
 
3
4
  import { cn } from '../utils/cn';
4
5
  import type { AdminSidebarUser } from './types';
@@ -23,6 +24,9 @@ function getInitials(fullName: string, email: string): string {
23
24
  return email.slice(0, 2).toUpperCase();
24
25
  }
25
26
 
27
+ const avatarCircleClassName =
28
+ 'flex size-11 shrink-0 items-center justify-center rounded-full bg-white/5';
29
+
26
30
  export function SidebarUserCard({
27
31
  user,
28
32
  isOpen,
@@ -31,37 +35,112 @@ export function SidebarUserCard({
31
35
  className,
32
36
  }: SidebarUserCardProps) {
33
37
  const initials = user.initials ?? getInitials(user.fullName, user.email);
38
+ const [logoutOpen, setLogoutOpen] = useState(false);
39
+ const rootRef = useRef<HTMLDivElement>(null);
40
+ const logoutMenuId = useId();
34
41
 
35
- return (
42
+ useEffect(() => {
43
+ if (isOpen) {
44
+ setLogoutOpen(false);
45
+ }
46
+ }, [isOpen]);
47
+
48
+ useEffect(() => {
49
+ if (isOpen || !logoutOpen) {
50
+ return;
51
+ }
52
+
53
+ function handlePointerDown(event: MouseEvent) {
54
+ if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
55
+ setLogoutOpen(false);
56
+ }
57
+ }
58
+
59
+ function handleKeyDown(event: KeyboardEvent) {
60
+ if (event.key === 'Escape') {
61
+ setLogoutOpen(false);
62
+ }
63
+ }
64
+
65
+ document.addEventListener('mousedown', handlePointerDown);
66
+ document.addEventListener('keydown', handleKeyDown);
67
+
68
+ return () => {
69
+ document.removeEventListener('mousedown', handlePointerDown);
70
+ document.removeEventListener('keydown', handleKeyDown);
71
+ };
72
+ }, [isOpen, logoutOpen]);
73
+
74
+ const avatar = (
36
75
  <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
- )}
76
+ className={cn(avatarCircleClassName, 'typography-14 font-bold text-white')}
77
+ aria-hidden
42
78
  >
79
+ {initials}
80
+ </div>
81
+ );
82
+
83
+ if (!isOpen) {
84
+ return (
43
85
  <div
44
- className="flex size-11 shrink-0 items-center justify-center rounded-full bg-white/5 typography-14 font-bold text-white"
45
- aria-hidden
86
+ ref={rootRef}
87
+ className={cn('flex w-full flex-col items-center gap-2', className)}
46
88
  >
47
- {initials}
48
- </div>
49
- {isOpen ? (
50
- <>
51
- <div className="flex min-w-0 flex-1 flex-col overflow-hidden">
52
- <p className="truncate typography-14 font-bold text-white">{user.fullName}</p>
53
- <p className="truncate typography-12 text-storm-gray-100">{user.email}</p>
54
- </div>
89
+ {logoutOpen ? (
55
90
  <button
56
91
  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"
92
+ id={logoutMenuId}
93
+ onClick={() => {
94
+ setLogoutOpen(false);
95
+ onLogout?.();
96
+ }}
97
+ className={cn(
98
+ avatarCircleClassName,
99
+ 'cursor-pointer bg-black/20 text-white',
100
+ 'transition-[background-color,transform] duration-300 ease-in-out',
101
+ 'hover:bg-white/10 active:scale-95',
102
+ )}
59
103
  aria-label={logoutLabel}
60
104
  >
61
105
  <LogOut size={20} strokeWidth={1.75} />
62
106
  </button>
63
- </>
64
- ) : null}
107
+ ) : null}
108
+ <button
109
+ type="button"
110
+ onClick={() => setLogoutOpen((open) => !open)}
111
+ className="flex cursor-pointer items-center justify-center transition-transform duration-300 ease-in-out active:scale-95"
112
+ aria-expanded={logoutOpen}
113
+ aria-haspopup="true"
114
+ aria-controls={logoutOpen ? logoutMenuId : undefined}
115
+ aria-label={user.fullName}
116
+ >
117
+ {avatar}
118
+ </button>
119
+ </div>
120
+ );
121
+ }
122
+
123
+ return (
124
+ <div
125
+ className={cn(
126
+ 'flex w-full items-center justify-between gap-3 rounded-xl bg-black/12 p-3',
127
+ 'transition-[background-color,justify-content,gap,padding] duration-300 ease-in-out',
128
+ className,
129
+ )}
130
+ >
131
+ {avatar}
132
+ <div className="flex min-w-0 flex-1 flex-col overflow-hidden">
133
+ <p className="truncate typography-14 font-bold text-white">{user.fullName}</p>
134
+ <p className="truncate typography-12 text-storm-gray-100">{user.email}</p>
135
+ </div>
136
+ <button
137
+ type="button"
138
+ onClick={() => onLogout?.()}
139
+ className="shrink-0 cursor-pointer text-navy-150 transition-[opacity,transform] duration-300 ease-in-out hover:opacity-80 active:scale-95"
140
+ aria-label={logoutLabel}
141
+ >
142
+ <LogOut size={20} strokeWidth={1.75} />
143
+ </button>
65
144
  </div>
66
145
  );
67
146
  }