@orsetra/shared-ui 1.1.29 → 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 {
|
|
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({
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
{/*
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 {
|
|
@@ -59,28 +59,42 @@ export function MainSidebar({
|
|
|
59
59
|
onSecondarySidebarOpen()
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
130
|
-
isMinimized
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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-
|
|
141
|
-
|
|
153
|
+
"h-5 w-5 flex-shrink-0",
|
|
154
|
+
isActive ? "text-interactive" : "text-text-secondary"
|
|
142
155
|
)} />
|
|
143
|
-
{!isMinimized && <span className="text-
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
178
|
+
<p className="text-xs font-semibold text-text-primary uppercase tracking-wide">{activeItem.label}</p>
|
|
165
179
|
</div>
|
|
166
|
-
<div
|
|
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,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
|
|
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-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
>
|
|
287
|
-
<
|
|
288
|
-
|
|
289
|
-
</
|
|
290
|
-
</
|
|
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
|