@orsetra/shared-ui 1.1.29 → 1.1.32

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.
@@ -1,6 +1,6 @@
1
1
  // Layout components exports
2
2
  export { MainSidebar, type SidebarMode } from './sidebar/main-sidebar'
3
- export { resolveIcon, type MainMenuItem, type SubMenuItem, type SidebarMenus, type MenuApiResponse, type ApiMenuData, type ApiMainMenuItem, type ApiSubMenuItem, type ApiSubMenu } from './sidebar/data'
3
+ export { resolveIcon, type MainMenuItem, type SubMenuItem, type SidebarMenus, type MenuApiResponse, type ApiMenuData, type ApiMainMenuItem, type ApiSubMenuItem, type ApiSubMenu, type OrgInfo, type Project } from './sidebar/data'
4
4
  export {
5
5
  Sidebar,
6
6
  SidebarContent,
@@ -1,14 +1,114 @@
1
1
  "use client"
2
2
 
3
- import { useState, useEffect } from "react"
3
+ import { useState, useEffect, useRef } from "react"
4
4
  import { usePathname, useSearchParams, useParams } from "next/navigation"
5
5
  import { MainSidebar, type SidebarMode } from "./sidebar/main-sidebar"
6
6
  import { Sidebar, SidebarProvider, useSidebar } from "./sidebar/sidebar"
7
- import { type SidebarMenus, type MainMenuItem, type MenuApiResponse, resolveIcon } from "./sidebar/data"
7
+ import { type SidebarMenus, type MainMenuItem, type MenuApiResponse, type OrgInfo, type Project, resolveIcon } from "./sidebar/data"
8
8
  import { UserMenu, Button, type UserMenuConfig } from "../ui"
9
9
  import { getMenuFromPath } from "../../lib/menu-utils"
10
10
  import { useIsMobile } from "../../hooks/use-mobile"
11
- import { Menu } from "lucide-react"
11
+ import { cn } from "../../lib/utils"
12
+ import { Menu, ChevronDown, Check } from "lucide-react"
13
+
14
+ // ─── Dropdown Organisation / Projets ────────────────────────────────────────
15
+
16
+ function OrgProjectDropdown({
17
+ loadOrg,
18
+ loadProjects,
19
+ currentProject,
20
+ onProjectChange,
21
+ }: {
22
+ loadOrg: () => Promise<OrgInfo>
23
+ loadProjects: () => Promise<Project[]>
24
+ currentProject?: string | null
25
+ onProjectChange: (id: string) => void
26
+ }) {
27
+ const [org, setOrg] = useState<OrgInfo | null>(null)
28
+ const [open, setOpen] = useState(false)
29
+ const [projects, setProjects] = useState<Project[]>([])
30
+ const [loading, setLoading] = useState(false)
31
+ const ref = useRef<HTMLDivElement>(null)
32
+
33
+ useEffect(() => {
34
+ loadOrg().then(setOrg).catch(() => {})
35
+ // eslint-disable-next-line react-hooks/exhaustive-deps
36
+ }, [])
37
+
38
+ useEffect(() => {
39
+ if (!open) return
40
+ function handleOutside(e: MouseEvent) {
41
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
42
+ }
43
+ document.addEventListener("mousedown", handleOutside)
44
+ return () => document.removeEventListener("mousedown", handleOutside)
45
+ }, [open])
46
+
47
+ if (!org) return null
48
+
49
+ const handleToggle = async () => {
50
+ if (!open && projects.length === 0) {
51
+ setLoading(true)
52
+ try {
53
+ const data = await loadProjects()
54
+ setProjects(data)
55
+ } finally {
56
+ setLoading(false)
57
+ }
58
+ }
59
+ setOpen((v) => !v)
60
+ }
61
+
62
+ return (
63
+ <div className="relative" ref={ref}>
64
+ <button
65
+ onClick={handleToggle}
66
+ className="flex items-center gap-2 h-9 px-3 text-sm text-text-primary hover:bg-ui-background transition-colors"
67
+ >
68
+ {org.logo ? (
69
+ <img src={org.logo} alt={org.nom} className="h-5 w-5 object-cover flex-shrink-0" />
70
+ ) : (
71
+ <div className="h-5 w-5 bg-interactive flex items-center justify-center text-white text-[10px] font-bold flex-shrink-0">
72
+ {org.nom.charAt(0).toUpperCase()}
73
+ </div>
74
+ )}
75
+ <span className="font-medium">{org.nom}</span>
76
+ <ChevronDown className={cn("h-3 w-3 text-text-secondary transition-transform duration-150", open && "rotate-180")} />
77
+ </button>
78
+
79
+ {open && (
80
+ <div className="absolute left-0 top-full bg-white border border-ui-border shadow-lg z-[100] min-w-[220px] max-h-[280px] overflow-y-auto">
81
+ {loading ? (
82
+ <div className="px-4 py-3 text-sm text-text-secondary">Chargement…</div>
83
+ ) : projects.length === 0 ? (
84
+ <div className="px-4 py-3 text-sm text-text-secondary">Aucun projet disponible</div>
85
+ ) : (
86
+ projects.map((project) => {
87
+ const isActive = currentProject === project.id
88
+ return (
89
+ <button
90
+ key={project.id}
91
+ onClick={() => { onProjectChange(project.id); setOpen(false) }}
92
+ className={cn(
93
+ "flex items-center gap-3 w-full px-4 py-2 text-sm text-left transition-colors border-l-4",
94
+ isActive
95
+ ? "bg-interactive/10 text-interactive font-medium border-interactive"
96
+ : "text-text-secondary hover:bg-ui-background hover:text-text-primary border-transparent"
97
+ )}
98
+ >
99
+ <Check className={cn("h-4 w-4 flex-shrink-0", !isActive && "invisible")} />
100
+ {project.name}
101
+ </button>
102
+ )
103
+ })
104
+ )}
105
+ </div>
106
+ )}
107
+ </div>
108
+ )
109
+ }
110
+
111
+ // ─── Layout ──────────────────────────────────────────────────────────────────
12
112
 
13
113
  interface LayoutContainerProps {
14
114
  main_base_url: string
@@ -19,12 +119,32 @@ interface LayoutContainerProps {
19
119
  mode?: SidebarMode
20
120
  userMenuConfig?: UserMenuConfig
21
121
  fetchMenus?: () => Promise<MenuApiResponse>
22
- getSidebarMode?: (pathname: string,params: Record<string, string>, searchParams: URLSearchParams) => SidebarMode
122
+ getSidebarMode?: (pathname: string, params: Record<string, string>, searchParams: URLSearchParams) => SidebarMode
23
123
  getCurrentMenu?: (pathname: string, searchParams: URLSearchParams) => string
24
124
  getCurrentMenuItem?: (pathname: string, searchParams: URLSearchParams) => string
125
+ loadOrg?: () => Promise<OrgInfo>
126
+ loadProjects?: () => Promise<Project[]>
127
+ currentProject?: string | null
128
+ onProjectChange?: (id: string) => void
25
129
  }
26
130
 
27
- function LayoutContent({ children, sidebarMenus = {}, user, onSignOut, mode = 'expanded', userMenuConfig, main_base_url, fetchMenus, getSidebarMode, getCurrentMenu, getCurrentMenuItem }: LayoutContainerProps) {
131
+ function LayoutContent({
132
+ children,
133
+ sidebarMenus = {},
134
+ user,
135
+ onSignOut,
136
+ mode = 'expanded',
137
+ userMenuConfig,
138
+ main_base_url,
139
+ fetchMenus,
140
+ getSidebarMode,
141
+ getCurrentMenu,
142
+ getCurrentMenuItem,
143
+ loadOrg,
144
+ loadProjects,
145
+ currentProject,
146
+ onProjectChange,
147
+ }: LayoutContainerProps) {
28
148
  const pathname = usePathname()
29
149
  const searchParams = useSearchParams()
30
150
  const params = useParams()
@@ -32,23 +152,21 @@ function LayoutContent({ children, sidebarMenus = {}, user, onSignOut, mode = 'e
32
152
  const isMobile = useIsMobile()
33
153
  const [isMainSidebarOpen, setIsMainSidebarOpen] = useState(false)
34
154
  const [currentMenu, setCurrentMenu] = useState<string>("overview")
35
-
36
- // Use custom mode function if provided, otherwise use prop
155
+
37
156
  const effectiveMode = getSidebarMode ? getSidebarMode(pathname, params as Record<string, string>, searchParams) : mode
38
157
  const isMinimized = effectiveMode === 'minimized'
39
- // Force hidden mode on mobile
40
158
  const isHidden = effectiveMode === 'hidden' || isMobile
41
159
 
42
160
  const [mainMenuItems, setMainMenuItems] = useState<MainMenuItem[]>([])
43
161
  const [fetchedSidebarMenus, setFetchedSidebarMenus] = useState<SidebarMenus>({})
44
162
  const [sectionLabels, setSectionLabels] = useState<Record<string, string>>({})
163
+ const [fetchedUserMenu, setFetchedUserMenu] = useState<Record<string, { id: string; label: string; href: string; icon: string }[]>>({})
45
164
 
46
- // Load menus from API when fetchMenus is provided
47
165
  useEffect(() => {
48
166
  if (!fetchMenus) return
49
167
  fetchMenus().then((response) => {
50
168
  if (!response.success) return
51
- const { main, subMenus } = response.data
169
+ const { main, subMenus, userMenu: apiUserMenu } = response.data
52
170
 
53
171
  setMainMenuItems(
54
172
  main.map((item) => ({
@@ -71,41 +189,53 @@ function LayoutContent({ children, sidebarMenus = {}, user, onSignOut, mode = 'e
71
189
  })
72
190
  setFetchedSidebarMenus(menus)
73
191
  setSectionLabels(labels)
192
+
193
+ if (apiUserMenu) {
194
+ const userMenuMap: Record<string, { id: string; label: string; href: string; icon: string }[]> = {}
195
+ Object.entries(apiUserMenu).forEach(([key, subMenu]) => {
196
+ userMenuMap[key] = subMenu.items.map((item) => ({
197
+ id: item.id,
198
+ label: item.label,
199
+ href: item.href,
200
+ icon: item.icon,
201
+ }))
202
+ })
203
+ setFetchedUserMenu(userMenuMap)
204
+ }
74
205
  })
75
206
  // eslint-disable-next-line react-hooks/exhaustive-deps
76
207
  }, [])
77
208
 
78
- // Effective sidebarMenus: fetched provides section keys, prop provides items key (micro apps)
79
209
  const effectiveSidebarMenus: SidebarMenus = { ...fetchedSidebarMenus, ...sidebarMenus }
80
210
 
81
- // Close sidebar on route change (mobile)
211
+ // UserMenuConfig : priorité API (context-sensitive par currentMenu), sinon prop
212
+ const effectiveUserMenuConfig: UserMenuConfig | undefined =
213
+ fetchedUserMenu[currentMenu]?.length
214
+ ? {
215
+ items: fetchedUserMenu[currentMenu].map((item) => ({
216
+ id: item.id,
217
+ label: item.label,
218
+ href: item.href,
219
+ icon: resolveIcon(item.icon),
220
+ })),
221
+ }
222
+ : userMenuConfig
223
+
82
224
  useEffect(() => {
83
- if (isMobile) {
84
- setIsMainSidebarOpen(false)
85
- }
225
+ if (isMobile) setIsMainSidebarOpen(false)
86
226
  }, [pathname, isMobile])
87
227
 
88
228
  useEffect(() => {
89
- // Use custom menu function if provided, otherwise use default getMenuFromPath
90
229
  const contextualMenu = getCurrentMenu ? getCurrentMenu(pathname, searchParams) : getMenuFromPath(pathname)
91
230
  setCurrentMenu(contextualMenu)
92
231
  }, [pathname, searchParams, getCurrentMenu])
93
232
 
94
- const handleMainSidebarToggle = () => {
95
- setIsMainSidebarOpen(!isMainSidebarOpen)
96
- }
97
-
98
- const handleMenuSelect = (menu: string) => {
99
- setCurrentMenu(menu)
100
- }
101
-
102
- const handleSecondarySidebarOpen = () => {
103
- setOpen(true)
104
- }
233
+ const handleMainSidebarToggle = () => setIsMainSidebarOpen((v) => !v)
234
+ const handleMenuSelect = (menu: string) => setCurrentMenu(menu)
235
+ const handleSecondarySidebarOpen = () => setOpen(true)
105
236
 
106
237
  return (
107
- <div className="flex h-screen w-full bg-white">
108
- {/* Desktop sidebar - hidden on mobile (isHidden is true on mobile) */}
238
+ <div className="flex h-screen w-full bg-white overflow-visible">
109
239
  {!isMinimized && !isHidden && (
110
240
  <Sidebar
111
241
  currentMenu={currentMenu}
@@ -114,10 +244,10 @@ function LayoutContent({ children, sidebarMenus = {}, user, onSignOut, mode = 'e
114
244
  main_base_url={main_base_url}
115
245
  sectionLabels={sectionLabels}
116
246
  getCurrentMenuItem={getCurrentMenuItem}
247
+ loadOrg={loadOrg}
117
248
  />
118
249
  )}
119
250
 
120
- {/* MainSidebar - always available, opens as overlay when isHidden */}
121
251
  <MainSidebar
122
252
  main_base_url={main_base_url}
123
253
  isOpen={isMainSidebarOpen}
@@ -133,7 +263,7 @@ function LayoutContent({ children, sidebarMenus = {}, user, onSignOut, mode = 'e
133
263
  <div className="flex-1 flex flex-col min-w-0">
134
264
  <header className="h-14 bg-gray-50 border-b border-ui-border flex-shrink-0">
135
265
  <div className="h-full px-4 md:px-6 flex items-center justify-between">
136
- {/* Menu button - shown when sidebar is hidden (including on mobile) */}
266
+ {/* Gauche : bouton menu (mobile/hidden) */}
137
267
  {isHidden ? (
138
268
  <Button
139
269
  variant="ghost"
@@ -147,11 +277,23 @@ function LayoutContent({ children, sidebarMenus = {}, user, onSignOut, mode = 'e
147
277
  ) : (
148
278
  <div />
149
279
  )}
150
- <UserMenu
151
- username={user?.profile?.email || user?.profile?.preferred_username}
152
- onSignOut={onSignOut || (() => {})}
153
- menuConfig={userMenuConfig}
154
- />
280
+
281
+ {/* Droite : dropdown organisation + user menu */}
282
+ <div className="flex items-center gap-1">
283
+ {loadOrg && loadProjects && onProjectChange && (
284
+ <OrgProjectDropdown
285
+ loadOrg={loadOrg}
286
+ loadProjects={loadProjects}
287
+ currentProject={currentProject}
288
+ onProjectChange={onProjectChange}
289
+ />
290
+ )}
291
+ <UserMenu
292
+ username={user?.profile?.email || user?.profile?.preferred_username}
293
+ onSignOut={onSignOut || (() => {})}
294
+ menuConfig={effectiveUserMenuConfig}
295
+ />
296
+ </div>
155
297
  </div>
156
298
  </header>
157
299
 
@@ -42,6 +42,17 @@ export interface SidebarMenus {
42
42
  [key: string]: SubMenuItem[]
43
43
  }
44
44
 
45
+ export interface OrgInfo {
46
+ nom: string
47
+ id: string
48
+ logo?: string
49
+ }
50
+
51
+ export interface Project {
52
+ id: string
53
+ name: string
54
+ }
55
+
45
56
  // API response types
46
57
  export interface ApiMainMenuItem {
47
58
  id: string
@@ -65,6 +76,7 @@ export interface ApiSubMenu {
65
76
  export interface ApiMenuData {
66
77
  main: ApiMainMenuItem[]
67
78
  subMenus: Record<string, ApiSubMenu>
79
+ userMenu?: Record<string, ApiSubMenu>
68
80
  }
69
81
 
70
82
  export interface MenuApiResponse {
@@ -59,28 +59,42 @@ export function MainSidebar({
59
59
  onSecondarySidebarOpen()
60
60
  }
61
61
 
62
- const targetUrl = `${main_base_url}/${menuId}`
63
- const path = targetUrl.startsWith('http://') || targetUrl.startsWith('https://')
64
- ? new URL(targetUrl).pathname
65
- : targetUrl
66
- window.location.href = path
67
-
62
+ const targetUrl = `${main_base_url}/${menuId}`
63
+ const path = targetUrl.startsWith('http://') || targetUrl.startsWith('https://')
64
+ ? new URL(targetUrl).pathname
65
+ : targetUrl
66
+ window.location.href = path
67
+
68
68
  if (!isMinimized) {
69
69
  onToggle()
70
70
  }
71
71
  }
72
72
 
73
- const handleSubMenuClick = (e: React.MouseEvent, href: string) => {
73
+ const buildSubItemHref = (menuId: string, href: string): string => {
74
+ if (href.startsWith('http://') || href.startsWith('https://')) {
75
+ return new URL(href).pathname
76
+ }
77
+ const cleanHref = href.replace(/^\//, '')
78
+ const cleanBase = (main_base_url.startsWith('http://') || main_base_url.startsWith('https://'))
79
+ ? new URL(main_base_url).pathname.replace(/\/$/, '')
80
+ : main_base_url.replace(/\/$/, '')
81
+ return `${cleanBase}/${menuId}/${cleanHref}`
82
+ }
83
+
84
+ const handleSubMenuClick = (e: React.MouseEvent, menuId: string, href: string) => {
74
85
  e.preventDefault()
75
- const path = href.startsWith('http://') || href.startsWith('https://')
76
- ? new URL(href).pathname
77
- : href
78
- window.location.href = path
86
+ window.location.href = buildSubItemHref(menuId, href)
79
87
  setHoveredMenu(null)
80
88
  }
81
89
 
82
90
  return (
83
91
  <>
92
+ {isOpen && !isMinimized && !hoveredMenu && (
93
+ <div
94
+ className="fixed inset-0 bg-black/40 z-40"
95
+ onClick={onToggle}
96
+ />
97
+ )}
84
98
 
85
99
  <div className={cn(
86
100
  "fixed left-0 top-0 h-full bg-white border-r border-ui-border z-50 transform transition-all duration-300 ease-in-out",
@@ -112,10 +126,11 @@ export function MainSidebar({
112
126
  "overflow-y-auto h-full",
113
127
  isMinimized ? "p-2" : "p-3"
114
128
  )}>
115
- <nav className="space-y-2">
129
+ <nav className="space-y-1">
116
130
  {mainMenuItems.map((item) => {
117
131
  const Icon = item.icon
118
132
  const hasSubMenu = sidebarMenus[item.id]?.length > 0
133
+ const isActive = currentMenu === item.id
119
134
  return (
120
135
  <div
121
136
  key={item.id}
@@ -126,21 +141,19 @@ export function MainSidebar({
126
141
  <button
127
142
  onClick={() => handleMenuClick(item.id)}
128
143
  className={cn(
129
- "w-full flex items-center text-left rounded-lg transition-all duration-200 font-medium",
130
- isMinimized
131
- ? "justify-center p-3"
132
- : "gap-3 px-3 py-3",
133
- currentMenu === item.id
134
- ? "bg-interactive/10 text-interactive"
135
- : "text-text-primary hover:bg-ui-background hover:text-text-primary"
144
+ "w-full flex items-center text-left transition-all duration-150 font-medium",
145
+ isMinimized ? "justify-center p-3" : "gap-3 px-3 py-2",
146
+ isActive
147
+ ? "bg-interactive/10 text-interactive border-l-4 border-interactive"
148
+ : "text-text-primary hover:bg-ui-background border-l-4 border-transparent"
136
149
  )}
137
150
  title={isMinimized ? item.label : undefined}
138
151
  >
139
152
  <Icon className={cn(
140
- "h-6 w-6",
141
- currentMenu === item.id ? "text-interactive" : "text-text-secondary"
153
+ "h-5 w-5 flex-shrink-0",
154
+ isActive ? "text-interactive" : "text-text-secondary"
142
155
  )} />
143
- {!isMinimized && <span className="text-base">{item.label}</span>}
156
+ {!isMinimized && <span className="text-sm">{item.label}</span>}
144
157
  </button>
145
158
  </div>
146
159
  )
@@ -154,24 +167,25 @@ export function MainSidebar({
154
167
  if (!activeItem) return null
155
168
  return (
156
169
  <div
157
- className="fixed bg-white border border-ui-border rounded-lg shadow-xl z-[9999] min-w-[200px] py-2"
170
+ className="fixed bg-white border border-ui-border shadow-lg z-[9999] min-w-[220px] py-1"
158
171
  style={{ top: flyoutPos.top, left: flyoutPos.left }}
159
172
  onMouseEnter={() => handleFlyoutMouseEnter(hoveredMenu)}
160
173
  onMouseLeave={handleFlyoutMouseLeave}
161
174
  >
162
- <div className="absolute -left-[5px] top-[18px] w-[10px] h-[10px] bg-white border-l border-t border-ui-border -rotate-45" />
175
+ {/* Arrow pointing left */}
176
+ <div className="absolute -left-[5px] top-[14px] w-[10px] h-[10px] bg-white border-l border-t border-ui-border -rotate-45" />
163
177
  <div className="px-4 py-2 border-b border-ui-border">
164
- <h3 className="text-sm font-semibold text-text-primary">{activeItem.label}</h3>
178
+ <p className="text-xs font-semibold text-text-primary uppercase tracking-wide">{activeItem.label}</p>
165
179
  </div>
166
- <div className="py-1">
180
+ <div>
167
181
  {sidebarMenus[hoveredMenu].map((subItem) => (
168
182
  <a
169
183
  key={subItem.id}
170
- href={subItem.href}
171
- onClick={(e) => handleSubMenuClick(e, subItem.href)}
172
- 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"
184
+ href={buildSubItemHref(hoveredMenu, subItem.href)}
185
+ onClick={(e) => handleSubMenuClick(e, hoveredMenu, subItem.href)}
186
+ 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"
173
187
  >
174
- <subItem.icon className="h-4 w-4" />
188
+ <subItem.icon className="h-4 w-4 flex-shrink-0 text-text-secondary" />
175
189
  {subItem.name}
176
190
  </a>
177
191
  ))}
@@ -21,12 +21,11 @@ import {
21
21
  import {
22
22
  Settings,
23
23
  Menu,
24
- Building2,
25
24
  User,
26
25
  ChevronUp,
27
26
  } from "lucide-react"
28
27
  import { Logo } from "../../ui/logo"
29
- import { type SidebarMenus, type SubMenuItem } from "./data"
28
+ import { type SidebarMenus, type SubMenuItem, type OrgInfo } from "./data"
30
29
  import { Skeleton } from "../skeleton"
31
30
 
32
31
  const SIDEBAR_COOKIE_NAME = "sidebar:state"
@@ -173,16 +172,29 @@ interface SidebarProps {
173
172
  main_base_url?: string
174
173
  sectionLabels?: Record<string, string>
175
174
  getCurrentMenuItem?: (pathname: string, searchParams: URLSearchParams) => string
175
+ loadOrg?: () => Promise<OrgInfo>
176
176
  }
177
177
 
178
178
 
179
179
 
180
- function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_url = "", sectionLabels = {}, getCurrentMenuItem }: SidebarProps = {}) {
180
+ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_url = "", sectionLabels = {}, getCurrentMenuItem, loadOrg }: SidebarProps = {}) {
181
181
  const pathname = usePathname()
182
182
  const searchParams = useSearchParams()
183
183
  const { state } = useSidebar()
184
184
  const [settingsOpen, setSettingsOpen] = React.useState(false)
185
185
  const settingsRef = React.useRef<HTMLDivElement>(null)
186
+ const [orgData, setOrgData] = React.useState<OrgInfo | null>(null)
187
+ const [orgLoading, setOrgLoading] = React.useState(false)
188
+
189
+ // Load org lazily when settings panel opens
190
+ React.useEffect(() => {
191
+ if (settingsOpen && loadOrg && !orgData && !orgLoading) {
192
+ setOrgLoading(true)
193
+ loadOrg()
194
+ .then((data) => { setOrgData(data); setOrgLoading(false) })
195
+ .catch(() => setOrgLoading(false))
196
+ }
197
+ }, [settingsOpen, loadOrg, orgData, orgLoading])
186
198
 
187
199
  // Close dropdown on click outside
188
200
  React.useEffect(() => {
@@ -204,7 +216,7 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
204
216
  || []
205
217
 
206
218
  return (
207
- <div className="h-screen sticky top-0 flex flex-col bg-gray-50 border-r border-ui-border min-w-[var(--sidebar-width-icon)] transition-[width] duration-200"
219
+ <div className="h-screen sticky top-0 flex flex-col bg-gray-50 border-r border-ui-border min-w-[var(--sidebar-width-icon)] transition-[width] duration-200 overflow-visible"
208
220
  style={{ width: state === "expanded" ? "var(--sidebar-width)" : "var(--sidebar-width-icon)" }}>
209
221
  {/* Logo avec bouton de menu principal */}
210
222
  <div className="h-14 flex items-center justify-between px-4 border-b border-ui-border">
@@ -252,13 +264,13 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
252
264
  href={item.href}
253
265
  onClick={handleClick}
254
266
  className={cn(
255
- "flex items-center px-3 py-2 text-sm rounded-md gap-x-3 transition-colors",
267
+ "flex items-center px-3 py-2 text-sm gap-x-3 transition-colors border-l-4",
256
268
  isActive
257
- ? "bg-interactive/10 text-interactive font-medium"
258
- : "text-text-secondary hover:bg-ui-background hover:text-text-primary"
269
+ ? "bg-interactive/10 text-interactive font-medium border-interactive"
270
+ : "text-text-secondary hover:bg-ui-background hover:text-text-primary border-transparent"
259
271
  )}
260
272
  >
261
- <item.icon className="h-4 w-4" />
273
+ <item.icon className={cn("h-4 w-4 flex-shrink-0", isActive ? "text-interactive" : "text-text-secondary")} />
262
274
  {item.name}
263
275
  </Link>
264
276
  )
@@ -269,25 +281,42 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
269
281
  <div className="relative border-t border-ui-border px-3 py-3" ref={settingsRef}>
270
282
  {/* Dropdown vers le haut */}
271
283
  {settingsOpen && (
272
- <div className="absolute bottom-full left-2 right-2 mb-1 bg-gray-50 border border-ui-border rounded-none shadow-lg z-50">
273
- <div className="py-1">
274
- <Link
275
- href={`${main_base_url}/organization`}
276
- onClick={() => setSettingsOpen(false)}
277
- 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"
278
- >
279
- <Building2 className="h-4 w-4" />
280
- Organization
281
- </Link>
282
- <Link
283
- href={`${main_base_url}/profile`}
284
- onClick={() => setSettingsOpen(false)}
285
- 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"
286
- >
287
- <User className="h-4 w-4" />
288
- Compte
289
- </Link>
290
- </div>
284
+ <div className="absolute bottom-full left-0 mb-3 bg-white border border-ui-border shadow-2xl z-50 min-w-[380px]">
285
+ {/* Flèche pointant vers le bas en direction du bouton Paramètres */}
286
+ <div className="absolute -bottom-[5px] left-6 w-[10px] h-[10px] bg-white border-r border-b border-ui-border rotate-45" />
287
+
288
+ {/* Organisation */}
289
+ <Link
290
+ href={`${main_base_url}/organization`}
291
+ onClick={() => setSettingsOpen(false)}
292
+ className="flex items-center gap-4 px-5 py-4 hover:bg-ui-background transition-colors border-b border-ui-border no-underline"
293
+ >
294
+ {orgLoading ? (
295
+ <div className="h-14 w-14 bg-ui-background animate-pulse flex-shrink-0" />
296
+ ) : orgData?.logo ? (
297
+ <img src={orgData.logo} alt={orgData.nom} className="h-14 w-14 object-cover flex-shrink-0" />
298
+ ) : (
299
+ <div className="h-14 w-14 bg-interactive flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
300
+ {orgData?.nom?.charAt(0)?.toUpperCase() ?? 'O'}
301
+ </div>
302
+ )}
303
+ <div className="flex flex-col min-w-0">
304
+ <span className="text-xs text-text-secondary uppercase tracking-wide mb-1">Organisation</span>
305
+ <span className="text-base font-semibold text-text-primary truncate">
306
+ {orgLoading ? '…' : (orgData?.nom ?? 'Organisation')}
307
+ </span>
308
+ </div>
309
+ </Link>
310
+
311
+ {/* Paramètres du compte */}
312
+ <Link
313
+ href={`${main_base_url}/profile`}
314
+ onClick={() => setSettingsOpen(false)}
315
+ className="flex items-center gap-3 px-5 py-3 text-sm text-text-secondary hover:bg-ui-background hover:text-text-primary transition-colors no-underline"
316
+ >
317
+ <User className="h-5 w-5 flex-shrink-0" />
318
+ <span>Paramètres du compte</span>
319
+ </Link>
291
320
  </div>
292
321
  )}
293
322
  <button
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.1.29",
3
+ "version": "1.1.32",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",