@orsetra/shared-ui 1.5.0 → 1.5.2

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.
@@ -34,41 +34,23 @@ export function MainSidebar({
34
34
  mainMenuItems = [],
35
35
  }: MainSidebarProps) {
36
36
  const isMinimized = mode === 'minimized'
37
- const [hoveredMenu, setHoveredMenu] = React.useState<string | null>(null)
38
- const [flyoutPos, setFlyoutPos] = React.useState<{ top: number; left: number } | null>(null)
39
37
  const [expandedMenu, setExpandedMenu] = React.useState<string | null>(null)
40
- const closeTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
41
38
 
42
- React.useEffect(() => {
43
- return () => { if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current) }
44
- }, [])
45
-
46
- // Reset expanded menu when sidebar closes
39
+ // Reset accordion when sidebar closes
47
40
  React.useEffect(() => {
48
41
  if (!isOpen) setExpandedMenu(null)
49
42
  }, [isOpen])
50
43
 
51
- const handleFlyoutMouseEnter = (menuId: string, rect?: DOMRect) => {
52
- if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current)
53
- if (rect) setFlyoutPos({ top: rect.top, left: rect.right + 8 })
54
- setHoveredMenu(menuId)
55
- }
56
-
57
- const handleFlyoutMouseLeave = () => {
58
- closeTimeoutRef.current = setTimeout(() => setHoveredMenu(null), 150)
59
- }
60
-
61
44
  const handleMenuClick = (menuId: string) => {
62
45
  const hasSubMenu = (sidebarMenus[menuId]?.length ?? 0) > 0
63
46
 
64
- // In mobile overlay mode (not minimized, overlay open): expand inline instead of navigating
65
- if (!isMinimized && isOpen && hasSubMenu) {
47
+ // Accordion: expand/collapse inline for any non-minimized mode
48
+ if (!isMinimized && hasSubMenu) {
66
49
  setExpandedMenu((prev) => (prev === menuId ? null : menuId))
67
50
  return
68
51
  }
69
52
 
70
53
  onMenuSelect(menuId)
71
-
72
54
  if (!isMinimized && onSecondarySidebarOpen) {
73
55
  onSecondarySidebarOpen()
74
56
  }
@@ -98,7 +80,7 @@ export function MainSidebar({
98
80
  const handleSubMenuClick = (e: React.MouseEvent, menuId: string, href: string) => {
99
81
  e.preventDefault()
100
82
  window.location.href = buildSubItemHref(menuId, href)
101
- setHoveredMenu(null)
83
+ setExpandedMenu(null)
102
84
  }
103
85
 
104
86
  return (
@@ -111,14 +93,14 @@ export function MainSidebar({
111
93
  )}
112
94
 
113
95
  <div className={cn(
114
- "fixed left-0 top-0 h-full bg-white border-r border-ui-border z-50 transform transition-all duration-300 ease-in-out",
96
+ "fixed left-0 top-0 h-full bg-white border-r border-ui-border z-50 transform transition-all duration-300 ease-in-out overflow-y-auto",
115
97
  isMinimized ? "w-16" : "w-64 shadow-xl",
116
98
  isMinimized
117
99
  ? "translate-x-0"
118
100
  : (isOpen ? "translate-x-0" : "-translate-x-full")
119
101
  )}>
120
102
  <div className={cn(
121
- "flex items-center h-16 border-b border-ui-border bg-white",
103
+ "flex items-center h-16 border-b border-ui-border bg-white sticky top-0 z-10",
122
104
  isMinimized ? "justify-center px-2" : "justify-between px-4"
123
105
  )}>
124
106
  {!isMinimized && <Logo className="text-2xl font-semibold text-text-primary" href={main_base_url} />}
@@ -136,99 +118,62 @@ export function MainSidebar({
136
118
  )}
137
119
  </div>
138
120
 
139
- <div className={cn(
140
- "overflow-y-auto h-full",
141
- isMinimized ? "p-2" : "p-3"
142
- )}>
143
- <nav className="space-y-1">
144
- {mainMenuItems.map((item) => {
145
- const Icon = item.icon
146
- const hasSubMenu = sidebarMenus[item.id]?.length > 0
147
- const isActive = currentMenu === item.id
148
- return (
149
- <div
150
- key={item.id}
151
- className="relative"
152
- onMouseEnter={(e) => hasSubMenu && handleFlyoutMouseEnter(item.id, e.currentTarget.getBoundingClientRect())}
153
- onMouseLeave={() => hasSubMenu && handleFlyoutMouseLeave()}
121
+ <nav className={cn("space-y-1", isMinimized ? "p-2" : "p-3")}>
122
+ {mainMenuItems.map((item) => {
123
+ const Icon = item.icon
124
+ const hasSubMenu = (sidebarMenus[item.id]?.length ?? 0) > 0
125
+ const isActive = currentMenu === item.id
126
+ const isExpanded = expandedMenu === item.id
127
+
128
+ return (
129
+ <div key={item.id}>
130
+ <button
131
+ onClick={() => handleMenuClick(item.id)}
132
+ className={cn(
133
+ "w-full flex items-center text-left transition-all duration-150 font-medium",
134
+ isMinimized ? "justify-center p-3" : "gap-3 px-3 py-2",
135
+ isActive
136
+ ? "bg-interactive/10 text-interactive border-l-4 border-interactive"
137
+ : "text-text-primary hover:bg-ui-background border-l-4 border-transparent"
138
+ )}
139
+ title={isMinimized ? item.label : undefined}
154
140
  >
155
- <button
156
- onClick={() => handleMenuClick(item.id)}
157
- className={cn(
158
- "w-full flex items-center text-left transition-all duration-150 font-medium",
159
- isMinimized ? "justify-center p-3" : "gap-3 px-3 py-2",
160
- isActive
161
- ? "bg-interactive/10 text-interactive border-l-4 border-interactive"
162
- : "text-text-primary hover:bg-ui-background border-l-4 border-transparent"
163
- )}
164
- title={isMinimized ? item.label : undefined}
165
- >
166
- <Icon className={cn(
167
- "h-5 w-5 flex-shrink-0",
168
- isActive ? "text-interactive" : "text-text-secondary"
169
- )} />
170
- {!isMinimized && <span className="text-base flex-1">{item.label}</span>}
171
- {!isMinimized && hasSubMenu && isOpen && (
172
- expandedMenu === item.id
173
- ? <ChevronDown className="h-4 w-4 text-text-secondary flex-shrink-0" />
174
- : <ChevronRight className="h-4 w-4 text-text-secondary flex-shrink-0" />
175
- )}
176
- </button>
177
-
178
- {/* Inline accordion sub-items — shown on mobile (overlay open, not minimized) */}
179
- {!isMinimized && isOpen && expandedMenu === item.id && hasSubMenu && (
180
- <div className="pl-4 pb-1">
181
- {sidebarMenus[item.id].map((subItem) => (
182
- <a
183
- key={subItem.id}
184
- href={buildSubItemHref(item.id, subItem.href)}
185
- onClick={(e) => handleSubMenuClick(e, item.id, subItem.href)}
186
- className="flex items-center gap-3 px-3 py-2 text-sm text-text-secondary hover:bg-ui-background hover:text-text-primary transition-colors no-underline border-l-2 border-ui-border hover:border-interactive"
187
- >
188
- <subItem.icon className="h-4 w-4 flex-shrink-0 text-text-secondary" />
189
- {subItem.name}
190
- </a>
191
- ))}
192
- </div>
141
+ <Icon className={cn(
142
+ "h-5 w-5 flex-shrink-0",
143
+ isActive ? "text-interactive" : "text-text-secondary"
144
+ )} />
145
+ {!isMinimized && (
146
+ <>
147
+ <span className="text-base flex-1">{item.label}</span>
148
+ {hasSubMenu && (
149
+ isExpanded
150
+ ? <ChevronDown className="h-4 w-4 text-text-secondary flex-shrink-0" />
151
+ : <ChevronRight className="h-4 w-4 text-text-secondary flex-shrink-0" />
152
+ )}
153
+ </>
193
154
  )}
194
- </div>
195
- )
196
- })}
197
- </nav>
198
- </div>
199
-
200
- {/* Flyout en position fixed : échappe overflow-y-auto et tout contexte d'empilement */}
201
- {hoveredMenu && flyoutPos && sidebarMenus[hoveredMenu]?.length > 0 && (() => {
202
- const activeItem = mainMenuItems.find(i => i.id === hoveredMenu)
203
- if (!activeItem) return null
204
- return (
205
- <div
206
- className="fixed bg-white border border-ui-border shadow-lg z-[9999] min-w-[220px] py-1"
207
- style={{ top: flyoutPos.top, left: flyoutPos.left }}
208
- onMouseEnter={() => handleFlyoutMouseEnter(hoveredMenu)}
209
- onMouseLeave={handleFlyoutMouseLeave}
210
- >
211
- {/* Arrow pointing left */}
212
- <div className="absolute -left-[5px] top-[14px] w-[10px] h-[10px] bg-white border-l border-t border-ui-border -rotate-45" />
213
- <div className="px-4 py-2 border-b border-ui-border">
214
- <p className="text-xs font-semibold text-text-primary uppercase tracking-wide">{activeItem.label}</p>
215
- </div>
216
- <div>
217
- {sidebarMenus[hoveredMenu].map((subItem) => (
218
- <a
219
- key={subItem.id}
220
- href={buildSubItemHref(hoveredMenu, subItem.href)}
221
- onClick={(e) => handleSubMenuClick(e, hoveredMenu, subItem.href)}
222
- className="flex items-center gap-3 px-4 py-2 text-sm text-text-secondary hover:bg-ui-background hover:text-text-primary transition-colors no-underline border-l-4 border-transparent hover:border-interactive"
223
- >
224
- <subItem.icon className="h-4 w-4 flex-shrink-0 text-text-secondary" />
225
- {subItem.name}
226
- </a>
227
- ))}
155
+ </button>
156
+
157
+ {/* Inline accordion sub-items */}
158
+ {!isMinimized && isExpanded && hasSubMenu && (
159
+ <div className="pl-4 pb-1">
160
+ {sidebarMenus[item.id].map((subItem) => (
161
+ <a
162
+ key={subItem.id}
163
+ href={buildSubItemHref(item.id, subItem.href)}
164
+ onClick={(e) => handleSubMenuClick(e, item.id, subItem.href)}
165
+ className="flex items-center gap-3 px-3 py-2 text-sm text-text-secondary hover:bg-ui-background hover:text-text-primary transition-colors no-underline border-l-2 border-ui-border hover:border-interactive"
166
+ >
167
+ <subItem.icon className="h-4 w-4 flex-shrink-0 text-text-secondary" />
168
+ {subItem.name}
169
+ </a>
170
+ ))}
171
+ </div>
172
+ )}
228
173
  </div>
229
- </div>
230
- )
231
- })()}
174
+ )
175
+ })}
176
+ </nav>
232
177
  </div>
233
178
  </>
234
179
  )
@@ -19,9 +19,9 @@ export interface DetailPageHeaderTab {
19
19
 
20
20
  export interface DetailPageHeaderProps {
21
21
  /** Back breadcrumb link destination */
22
- backHref: string
22
+ backHref?: string
23
23
  /** Back breadcrumb label */
24
- backLabel: string
24
+ backLabel?: string
25
25
  /** Optional icon rendered in a blue square container */
26
26
  icon?: ReactNode
27
27
  /** Page title – truncated to one line */
@@ -57,13 +57,15 @@ export function DetailPageHeader({
57
57
  <div className="px-4 sm:px-2 pt-1 pb-0">
58
58
 
59
59
  {/* Breadcrumb */}
60
- <Link
61
- href={backHref}
62
- className="inline-flex items-center gap-1.5 text-xs text-ibm-gray-50 hover:text-ibm-blue-60 transition-colors mb-3"
63
- >
64
- <ArrowLeft className="h-3 w-3" />
65
- {backLabel}
66
- </Link>
60
+ {backHref && backLabel && (
61
+ <Link
62
+ href={backHref}
63
+ className="inline-flex items-center gap-1.5 text-xs text-ibm-gray-50 hover:text-ibm-blue-60 transition-colors mb-3"
64
+ >
65
+ <ArrowLeft className="h-3 w-3" />
66
+ {backLabel}
67
+ </Link>
68
+ )}
67
69
 
68
70
  {/* Title row */}
69
71
  <div className="flex items-center justify-between gap-4 pb-4">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",