@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, Check } from "lucide-react"
12
+ import { Menu, ChevronDown } from "lucide-react"
13
13
 
14
14
  // ─── Dropdown Organisation / Projets ────────────────────────────────────────
15
15
 
16
16
  function OrgProjectDropdown({
17
- org,
17
+ loadOrg,
18
18
  loadProjects,
19
19
  currentProject,
20
20
  onProjectChange,
21
21
  }: {
22
- org: OrgInfo
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-0 top-full bg-white border border-ui-border shadow-lg z-[100] min-w-[220px] max-h-[280px] overflow-y-auto">
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 border-l-4",
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 border-interactive"
88
- : "text-text-secondary hover:bg-ui-background hover:text-text-primary border-transparent"
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
- <Check className={cn("h-4 w-4 flex-shrink-0", !isActive && "invisible")} />
92
- {project.name}
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
- org?: OrgInfo
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
- org,
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
- org={org}
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
- {org && loadProjects && onProjectChange && (
288
+ {loadOrg && loadProjects && onProjectChange && (
276
289
  <OrgProjectDropdown
277
- org={org}
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
- isOpen && showBorder && "border-l border-ibm-gray-40",
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={`flex flex-col h-full bg-white ${panelWidthClass}`}>
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 border-b border-ibm-gray-20",
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 && !isOpen && (
130
+ {closable && (
126
131
  <button
127
- onClick={handleOpen}
128
- className="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-colors duration-200"
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 && !hoveredMenu && (
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-sm">{item.label}</span>}
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
- org?: OrgInfo
175
+ loadOrg?: () => Promise<OrgInfo>
176
176
  }
177
177
 
178
178
 
179
179
 
180
- function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_url = "", sectionLabels = {}, getCurrentMenuItem, org }: 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">
@@ -248,7 +260,7 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
248
260
 
249
261
  return (
250
262
  <Link
251
- key={item.name}
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 right-0 mb-0 bg-white border border-ui-border shadow-lg z-50">
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-3 px-3 py-3 hover:bg-ui-background transition-colors border-b border-ui-border no-underline"
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
- {org?.logo ? (
280
- <img src={org.logo} alt={org.nom} className="h-8 w-8 object-cover flex-shrink-0" />
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-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'}
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 leading-none mb-0.5">Organisation</span>
288
- <span className="text-sm font-medium text-text-primary truncate">{org?.nom ?? 'Organisation'}</span>
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-3 py-2 text-sm text-text-secondary hover:bg-ui-background hover:text-text-primary transition-colors no-underline"
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-4 w-4 flex-shrink-0" />
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 border-b border-ibm-gray-20 px-6 py-4 flex-shrink-0">
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 border-t border-ibm-gray-20 px-6 py-4 flex-shrink-0">
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 border-b border-ibm-gray-20 px-6 py-4 flex-shrink-0",
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 border-t border-ibm-gray-20 px-6 py-4 flex-shrink-0",
194
+ "flex items-center justify-end gap-3 px-6 py-4 flex-shrink-0",
195
195
  className
196
196
  )}
197
197
  {...props}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.1.30",
3
+ "version": "1.1.33",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",