@orsetra/shared-ui 1.1.30 → 1.1.33
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.
|
@@ -9,26 +9,32 @@ import { UserMenu, Button, type UserMenuConfig } from "../ui"
|
|
|
9
9
|
import { getMenuFromPath } from "../../lib/menu-utils"
|
|
10
10
|
import { useIsMobile } from "../../hooks/use-mobile"
|
|
11
11
|
import { cn } from "../../lib/utils"
|
|
12
|
-
import { Menu, ChevronDown
|
|
12
|
+
import { Menu, ChevronDown } from "lucide-react"
|
|
13
13
|
|
|
14
14
|
// ─── Dropdown Organisation / Projets ────────────────────────────────────────
|
|
15
15
|
|
|
16
16
|
function OrgProjectDropdown({
|
|
17
|
-
|
|
17
|
+
loadOrg,
|
|
18
18
|
loadProjects,
|
|
19
19
|
currentProject,
|
|
20
20
|
onProjectChange,
|
|
21
21
|
}: {
|
|
22
|
-
|
|
22
|
+
loadOrg: () => Promise<OrgInfo>
|
|
23
23
|
loadProjects: () => Promise<Project[]>
|
|
24
24
|
currentProject?: string | null
|
|
25
25
|
onProjectChange: (id: string) => void
|
|
26
26
|
}) {
|
|
27
|
+
const [org, setOrg] = useState<OrgInfo | null>(null)
|
|
27
28
|
const [open, setOpen] = useState(false)
|
|
28
29
|
const [projects, setProjects] = useState<Project[]>([])
|
|
29
30
|
const [loading, setLoading] = useState(false)
|
|
30
31
|
const ref = useRef<HTMLDivElement>(null)
|
|
31
32
|
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
loadOrg().then(setOrg).catch(() => {})
|
|
35
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
32
38
|
useEffect(() => {
|
|
33
39
|
if (!open) return
|
|
34
40
|
function handleOutside(e: MouseEvent) {
|
|
@@ -38,6 +44,8 @@ function OrgProjectDropdown({
|
|
|
38
44
|
return () => document.removeEventListener("mousedown", handleOutside)
|
|
39
45
|
}, [open])
|
|
40
46
|
|
|
47
|
+
if (!org) return null
|
|
48
|
+
|
|
41
49
|
const handleToggle = async () => {
|
|
42
50
|
if (!open && projects.length === 0) {
|
|
43
51
|
setLoading(true)
|
|
@@ -69,7 +77,7 @@ function OrgProjectDropdown({
|
|
|
69
77
|
</button>
|
|
70
78
|
|
|
71
79
|
{open && (
|
|
72
|
-
<div className="absolute left-
|
|
80
|
+
<div className="absolute left-1/2 -translate-x-1/2 top-full mt-1 bg-white border border-ui-border shadow-lg z-[100] w-56 max-h-[280px] overflow-y-auto">
|
|
73
81
|
{loading ? (
|
|
74
82
|
<div className="px-4 py-3 text-sm text-text-secondary">Chargement…</div>
|
|
75
83
|
) : projects.length === 0 ? (
|
|
@@ -82,14 +90,19 @@ function OrgProjectDropdown({
|
|
|
82
90
|
key={project.id}
|
|
83
91
|
onClick={() => { onProjectChange(project.id); setOpen(false) }}
|
|
84
92
|
className={cn(
|
|
85
|
-
"flex items-center gap-3 w-full px-4 py-2 text-sm text-left transition-colors
|
|
93
|
+
"flex items-center gap-3 w-full px-4 py-2 text-sm text-left transition-colors",
|
|
86
94
|
isActive
|
|
87
|
-
? "bg-interactive/10 text-interactive font-medium
|
|
88
|
-
: "text-text-secondary hover:bg-ui-background hover:text-text-primary
|
|
95
|
+
? "bg-interactive/10 text-interactive font-medium"
|
|
96
|
+
: "text-text-secondary hover:bg-ui-background hover:text-text-primary"
|
|
89
97
|
)}
|
|
90
98
|
>
|
|
91
|
-
<
|
|
92
|
-
|
|
99
|
+
<div className={cn(
|
|
100
|
+
"h-3 w-3 rounded-full flex-shrink-0 ring-2",
|
|
101
|
+
isActive
|
|
102
|
+
? "bg-interactive ring-interactive/30"
|
|
103
|
+
: "bg-transparent ring-transparent"
|
|
104
|
+
)} />
|
|
105
|
+
<span>{project.name}</span>
|
|
93
106
|
</button>
|
|
94
107
|
)
|
|
95
108
|
})
|
|
@@ -114,7 +127,7 @@ interface LayoutContainerProps {
|
|
|
114
127
|
getSidebarMode?: (pathname: string, params: Record<string, string>, searchParams: URLSearchParams) => SidebarMode
|
|
115
128
|
getCurrentMenu?: (pathname: string, searchParams: URLSearchParams) => string
|
|
116
129
|
getCurrentMenuItem?: (pathname: string, searchParams: URLSearchParams) => string
|
|
117
|
-
|
|
130
|
+
loadOrg?: () => Promise<OrgInfo>
|
|
118
131
|
loadProjects?: () => Promise<Project[]>
|
|
119
132
|
currentProject?: string | null
|
|
120
133
|
onProjectChange?: (id: string) => void
|
|
@@ -132,7 +145,7 @@ function LayoutContent({
|
|
|
132
145
|
getSidebarMode,
|
|
133
146
|
getCurrentMenu,
|
|
134
147
|
getCurrentMenuItem,
|
|
135
|
-
|
|
148
|
+
loadOrg,
|
|
136
149
|
loadProjects,
|
|
137
150
|
currentProject,
|
|
138
151
|
onProjectChange,
|
|
@@ -227,7 +240,7 @@ function LayoutContent({
|
|
|
227
240
|
const handleSecondarySidebarOpen = () => setOpen(true)
|
|
228
241
|
|
|
229
242
|
return (
|
|
230
|
-
<div className="flex h-screen w-full bg-white">
|
|
243
|
+
<div className="flex h-screen w-full bg-white overflow-visible">
|
|
231
244
|
{!isMinimized && !isHidden && (
|
|
232
245
|
<Sidebar
|
|
233
246
|
currentMenu={currentMenu}
|
|
@@ -236,7 +249,7 @@ function LayoutContent({
|
|
|
236
249
|
main_base_url={main_base_url}
|
|
237
250
|
sectionLabels={sectionLabels}
|
|
238
251
|
getCurrentMenuItem={getCurrentMenuItem}
|
|
239
|
-
|
|
252
|
+
loadOrg={loadOrg}
|
|
240
253
|
/>
|
|
241
254
|
)}
|
|
242
255
|
|
|
@@ -272,9 +285,9 @@ function LayoutContent({
|
|
|
272
285
|
|
|
273
286
|
{/* Droite : dropdown organisation + user menu */}
|
|
274
287
|
<div className="flex items-center gap-1">
|
|
275
|
-
{
|
|
288
|
+
{loadOrg && loadProjects && onProjectChange && (
|
|
276
289
|
<OrgProjectDropdown
|
|
277
|
-
|
|
290
|
+
loadOrg={loadOrg}
|
|
278
291
|
loadProjects={loadProjects}
|
|
279
292
|
currentProject={currentProject}
|
|
280
293
|
onProjectChange={onProjectChange}
|
|
@@ -83,19 +83,24 @@ export function PageWithSidePanel({
|
|
|
83
83
|
<>
|
|
84
84
|
<div
|
|
85
85
|
className={cn(
|
|
86
|
-
"hidden lg:flex flex-col flex-shrink-0 sticky top-0 self-start h-[calc(100vh-3.5rem)] overflow-hidden transition-[width] duration-300 ease-in-out",
|
|
87
|
-
|
|
86
|
+
"hidden lg:flex flex-col flex-shrink-0 sticky top-0 self-start h-[calc(100vh-3.5rem)] overflow-hidden transition-[width,border-color] duration-300 ease-in-out",
|
|
87
|
+
showBorder ? "border-l" : "",
|
|
88
|
+
showBorder && isOpen ? "border-ibm-gray-40" : "border-transparent",
|
|
88
89
|
isOpen ? panelWidthClass : "w-0",
|
|
89
90
|
sidePanelClassName
|
|
90
91
|
)}
|
|
91
92
|
>
|
|
92
93
|
{/* Inner wrapper at fixed width to prevent content reflow during animation */}
|
|
93
|
-
<div className={
|
|
94
|
+
<div className={cn(
|
|
95
|
+
`flex flex-col h-full bg-white ${panelWidthClass}`,
|
|
96
|
+
"transition-opacity duration-200 ease-in-out",
|
|
97
|
+
isOpen ? "opacity-100 delay-100" : "opacity-0"
|
|
98
|
+
)}>
|
|
94
99
|
|
|
95
|
-
{/* Panel header */}
|
|
100
|
+
{/* Panel header — no bottom border */}
|
|
96
101
|
{(sidePanelHeader || closable) && (
|
|
97
102
|
<div className={cn(
|
|
98
|
-
"flex items-center justify-between h-14 flex-shrink-0 px-4
|
|
103
|
+
"flex items-center justify-between h-14 flex-shrink-0 px-4",
|
|
99
104
|
sidePanelHeaderClassName
|
|
100
105
|
)}>
|
|
101
106
|
<div className="flex-1 min-w-0">
|
|
@@ -122,10 +127,13 @@ export function PageWithSidePanel({
|
|
|
122
127
|
</div>
|
|
123
128
|
|
|
124
129
|
{/* Toggle button — fixed keeps it vertically centered in the viewport */}
|
|
125
|
-
{closable &&
|
|
130
|
+
{closable && (
|
|
126
131
|
<button
|
|
127
|
-
onClick={handleOpen}
|
|
128
|
-
className=
|
|
132
|
+
onClick={isOpen ? handleClose : handleOpen}
|
|
133
|
+
className={cn(
|
|
134
|
+
"hidden lg:flex fixed top-1/2 right-0 -translate-y-1/2 items-center justify-center w-6 h-16 bg-white border border-ibm-gray-40 border-r-0 rounded-l-lg shadow-md hover:bg-ibm-gray-10 transition-[opacity,transform] duration-300 ease-in-out",
|
|
135
|
+
isOpen ? "opacity-0 pointer-events-none translate-x-2" : "opacity-100 translate-x-0"
|
|
136
|
+
)}
|
|
129
137
|
aria-label="Open side panel"
|
|
130
138
|
>
|
|
131
139
|
<ChevronLeft className="h-4 w-4 text-ibm-gray-70" />
|
|
@@ -89,7 +89,7 @@ export function MainSidebar({
|
|
|
89
89
|
|
|
90
90
|
return (
|
|
91
91
|
<>
|
|
92
|
-
{isOpen && !isMinimized &&
|
|
92
|
+
{isOpen && !isMinimized && (
|
|
93
93
|
<div
|
|
94
94
|
className="fixed inset-0 bg-black/40 z-40"
|
|
95
95
|
onClick={onToggle}
|
|
@@ -153,7 +153,7 @@ export function MainSidebar({
|
|
|
153
153
|
"h-5 w-5 flex-shrink-0",
|
|
154
154
|
isActive ? "text-interactive" : "text-text-secondary"
|
|
155
155
|
)} />
|
|
156
|
-
{!isMinimized && <span className="text-
|
|
156
|
+
{!isMinimized && <span className="text-base">{item.label}</span>}
|
|
157
157
|
</button>
|
|
158
158
|
</div>
|
|
159
159
|
)
|
|
@@ -172,17 +172,29 @@ interface SidebarProps {
|
|
|
172
172
|
main_base_url?: string
|
|
173
173
|
sectionLabels?: Record<string, string>
|
|
174
174
|
getCurrentMenuItem?: (pathname: string, searchParams: URLSearchParams) => string
|
|
175
|
-
|
|
175
|
+
loadOrg?: () => Promise<OrgInfo>
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
|
|
179
179
|
|
|
180
|
-
function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_url = "", sectionLabels = {}, getCurrentMenuItem,
|
|
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">
|
|
@@ -248,7 +260,7 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
|
|
|
248
260
|
|
|
249
261
|
return (
|
|
250
262
|
<Link
|
|
251
|
-
key={item.
|
|
263
|
+
key={item.id}
|
|
252
264
|
href={item.href}
|
|
253
265
|
onClick={handleClick}
|
|
254
266
|
className={cn(
|
|
@@ -269,33 +281,41 @@ 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-0
|
|
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
|
+
|
|
273
288
|
{/* Organisation */}
|
|
274
289
|
<Link
|
|
275
290
|
href={`${main_base_url}/organization`}
|
|
276
291
|
onClick={() => setSettingsOpen(false)}
|
|
277
|
-
className="flex items-center gap-
|
|
292
|
+
className="flex items-center gap-4 px-5 py-4 hover:bg-ui-background transition-colors border-b border-ui-border no-underline"
|
|
278
293
|
>
|
|
279
|
-
{
|
|
280
|
-
<
|
|
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" />
|
|
281
298
|
) : (
|
|
282
|
-
<div className="h-
|
|
283
|
-
{
|
|
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'}
|
|
284
301
|
</div>
|
|
285
302
|
)}
|
|
286
303
|
<div className="flex flex-col min-w-0">
|
|
287
|
-
<span className="text-xs text-text-secondary
|
|
288
|
-
<span className="text-
|
|
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>
|
|
289
308
|
</div>
|
|
290
309
|
</Link>
|
|
310
|
+
|
|
291
311
|
{/* Paramètres du compte */}
|
|
292
312
|
<Link
|
|
293
313
|
href={`${main_base_url}/profile`}
|
|
294
314
|
onClick={() => setSettingsOpen(false)}
|
|
295
|
-
className="flex items-center gap-3 px-
|
|
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"
|
|
296
316
|
>
|
|
297
|
-
<User className="h-
|
|
298
|
-
Paramètres du compte
|
|
317
|
+
<User className="h-5 w-5 flex-shrink-0" />
|
|
318
|
+
<span>Paramètres du compte</span>
|
|
299
319
|
</Link>
|
|
300
320
|
</div>
|
|
301
321
|
)}
|
|
@@ -66,7 +66,7 @@ const SidePanel = ({
|
|
|
66
66
|
)}
|
|
67
67
|
>
|
|
68
68
|
{/* Header fixe */}
|
|
69
|
-
<div className="flex items-center justify-between
|
|
69
|
+
<div className="flex items-center justify-between px-6 py-4 flex-shrink-0">
|
|
70
70
|
<div className="flex-1">
|
|
71
71
|
<DialogPrimitive.Title className="text-lg font-semibold text-ibm-gray-100">
|
|
72
72
|
{title}
|
|
@@ -92,7 +92,7 @@ const SidePanel = ({
|
|
|
92
92
|
</div>
|
|
93
93
|
|
|
94
94
|
{/* Footer fixe */}
|
|
95
|
-
<div className="flex items-center justify-end gap-3
|
|
95
|
+
<div className="flex items-center justify-end gap-3 px-6 py-4 flex-shrink-0">
|
|
96
96
|
{actions}
|
|
97
97
|
</div>
|
|
98
98
|
</DialogPrimitive.Content>
|
|
@@ -140,7 +140,7 @@ const SidePanelHeader = React.forwardRef<
|
|
|
140
140
|
<div
|
|
141
141
|
ref={ref}
|
|
142
142
|
className={cn(
|
|
143
|
-
"flex items-center justify-between
|
|
143
|
+
"flex items-center justify-between px-6 py-4 flex-shrink-0",
|
|
144
144
|
className
|
|
145
145
|
)}
|
|
146
146
|
{...props}
|
|
@@ -191,7 +191,7 @@ const SidePanelFooter = React.forwardRef<
|
|
|
191
191
|
<div
|
|
192
192
|
ref={ref}
|
|
193
193
|
className={cn(
|
|
194
|
-
"flex items-center justify-end gap-3
|
|
194
|
+
"flex items-center justify-end gap-3 px-6 py-4 flex-shrink-0",
|
|
195
195
|
className
|
|
196
196
|
)}
|
|
197
197
|
{...props}
|