@orsetra/shared-ui 1.1.28 → 1.1.30

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