@orsetra/shared-ui 1.8.5 → 1.9.1
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.
|
@@ -6,6 +6,7 @@ import { MainSidebar, type SidebarMode } from "./sidebar/main-sidebar"
|
|
|
6
6
|
import { Sidebar, SidebarProvider, useSidebar } from "./sidebar/sidebar"
|
|
7
7
|
import { type SidebarMenus, type MainMenuItem, resolveIcon } from "./sidebar/data"
|
|
8
8
|
import { Button } from "../ui"
|
|
9
|
+
import { Logo } from "../ui/logo"
|
|
9
10
|
import { getMenuFromPath } from "../../lib/menu-utils"
|
|
10
11
|
import { useIsMobile } from "../../hooks/use-mobile"
|
|
11
12
|
import { Menu } from "lucide-react"
|
|
@@ -100,51 +101,54 @@ function LayoutContent({
|
|
|
100
101
|
const handleSecondarySidebarOpen = () => setOpen(true)
|
|
101
102
|
|
|
102
103
|
return (
|
|
103
|
-
<div className="flex h-screen w-full bg-white overflow-hidden">
|
|
104
|
-
|
|
105
|
-
<
|
|
104
|
+
<div className="flex flex-col h-screen w-full bg-white overflow-hidden">
|
|
105
|
+
<header className="h-14 bg-gray-50 border-b border-ui-border flex-shrink-0">
|
|
106
|
+
<div className="h-full flex items-center justify-between">
|
|
107
|
+
<div
|
|
108
|
+
className="h-full flex items-center justify-between flex-shrink-0 px-4 md:px-6"
|
|
109
|
+
style={{ width: 'var(--sidebar-width)' }}
|
|
110
|
+
>
|
|
111
|
+
<Logo href={main_base_url} />
|
|
112
|
+
<Button
|
|
113
|
+
variant="ghost"
|
|
114
|
+
size="sm"
|
|
115
|
+
onClick={handleMainSidebarToggle}
|
|
116
|
+
className="h-8 w-8 p-0 hover:bg-ui-background text-text-secondary"
|
|
117
|
+
title="Menu principal"
|
|
118
|
+
>
|
|
119
|
+
<Menu className="h-5 w-5" />
|
|
120
|
+
</Button>
|
|
121
|
+
</div>
|
|
122
|
+
<div className="flex-1 flex items-center justify-end px-4 md:px-6">
|
|
123
|
+
<UserPanel main_base_url={main_base_url} />
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</header>
|
|
127
|
+
|
|
128
|
+
<div className="flex flex-1 min-h-0 overflow-hidden">
|
|
129
|
+
{!isMinimized && !isHidden && (
|
|
130
|
+
<Sidebar
|
|
131
|
+
currentMenu={currentMenu}
|
|
132
|
+
onMainMenuToggle={handleMainSidebarToggle}
|
|
133
|
+
sidebarMenus={effectiveSidebarMenus}
|
|
134
|
+
main_base_url={main_base_url}
|
|
135
|
+
sectionLabels={sectionLabels}
|
|
136
|
+
getCurrentMenuItem={getCurrentMenuItem}
|
|
137
|
+
mainMenuItems={mainMenuItems}
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
<MainSidebar
|
|
142
|
+
main_base_url={main_base_url}
|
|
143
|
+
isOpen={isMainSidebarOpen}
|
|
144
|
+
onToggle={handleMainSidebarToggle}
|
|
145
|
+
onMenuSelect={handleMenuSelect}
|
|
106
146
|
currentMenu={currentMenu}
|
|
107
|
-
|
|
147
|
+
onSecondarySidebarOpen={handleSecondarySidebarOpen}
|
|
148
|
+
mode={isHidden ? "expanded" : mode}
|
|
108
149
|
sidebarMenus={effectiveSidebarMenus}
|
|
109
|
-
main_base_url={main_base_url}
|
|
110
|
-
sectionLabels={sectionLabels}
|
|
111
|
-
getCurrentMenuItem={getCurrentMenuItem}
|
|
112
150
|
mainMenuItems={mainMenuItems}
|
|
113
151
|
/>
|
|
114
|
-
)}
|
|
115
|
-
|
|
116
|
-
<MainSidebar
|
|
117
|
-
main_base_url={main_base_url}
|
|
118
|
-
isOpen={isMainSidebarOpen}
|
|
119
|
-
onToggle={handleMainSidebarToggle}
|
|
120
|
-
onMenuSelect={handleMenuSelect}
|
|
121
|
-
currentMenu={currentMenu}
|
|
122
|
-
onSecondarySidebarOpen={handleSecondarySidebarOpen}
|
|
123
|
-
mode={isHidden ? "expanded" : mode}
|
|
124
|
-
sidebarMenus={effectiveSidebarMenus}
|
|
125
|
-
mainMenuItems={mainMenuItems}
|
|
126
|
-
/>
|
|
127
|
-
|
|
128
|
-
<div className="flex-1 flex flex-col min-w-0">
|
|
129
|
-
<header className="h-14 bg-gray-50 border-b border-ui-border flex-shrink-0">
|
|
130
|
-
<div className="h-full px-4 md:px-6 flex items-center justify-between">
|
|
131
|
-
{isHidden ? (
|
|
132
|
-
<Button
|
|
133
|
-
variant="ghost"
|
|
134
|
-
size="sm"
|
|
135
|
-
onClick={handleMainSidebarToggle}
|
|
136
|
-
className="h-8 w-8 p-0 hover:bg-ui-background text-text-secondary"
|
|
137
|
-
title="Ouvrir le menu"
|
|
138
|
-
>
|
|
139
|
-
<Menu className="h-5 w-5" />
|
|
140
|
-
</Button>
|
|
141
|
-
) : (
|
|
142
|
-
<div />
|
|
143
|
-
)}
|
|
144
|
-
|
|
145
|
-
<UserPanel main_base_url={main_base_url} />
|
|
146
|
-
</div>
|
|
147
|
-
</header>
|
|
148
152
|
|
|
149
153
|
<main className="flex-1 bg-ibm-gray-10 min-h-0 overflow-auto">
|
|
150
154
|
{children}
|
|
@@ -3,13 +3,24 @@
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import { cn } from "../../../lib/utils"
|
|
5
5
|
import type { MainMenuItem, SubMenuItem } from "./data"
|
|
6
|
+
import { Home, ExternalLink, type LucideIcon } from "lucide-react"
|
|
7
|
+
|
|
8
|
+
export interface FlyoutDefaultLink {
|
|
9
|
+
label: string
|
|
10
|
+
href: string
|
|
11
|
+
description?: string
|
|
12
|
+
icon?: LucideIcon
|
|
13
|
+
}
|
|
6
14
|
|
|
7
15
|
interface MainSidebarFlyoutProps {
|
|
8
|
-
item:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
item: MainMenuItem | null
|
|
17
|
+
subItems: SubMenuItem[]
|
|
18
|
+
onMouseEnter: () => void
|
|
19
|
+
onMouseLeave: () => void
|
|
20
|
+
main_base_url?: string
|
|
21
|
+
defaultTitle?: string
|
|
22
|
+
defaultDescription?: string
|
|
23
|
+
defaultLinks?: FlyoutDefaultLink[]
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
export function MainSidebarFlyout({
|
|
@@ -17,8 +28,21 @@ export function MainSidebarFlyout({
|
|
|
17
28
|
subItems,
|
|
18
29
|
onMouseEnter,
|
|
19
30
|
onMouseLeave,
|
|
31
|
+
main_base_url = "/",
|
|
32
|
+
defaultTitle = "Camel X Platform",
|
|
33
|
+
defaultDescription,
|
|
34
|
+
defaultLinks = [],
|
|
20
35
|
}: MainSidebarFlyoutProps) {
|
|
21
|
-
const
|
|
36
|
+
const homePath = React.useMemo(() => {
|
|
37
|
+
if (!main_base_url) return "/"
|
|
38
|
+
try {
|
|
39
|
+
return (main_base_url.startsWith("http://") || main_base_url.startsWith("https://"))
|
|
40
|
+
? new URL(main_base_url).pathname
|
|
41
|
+
: main_base_url
|
|
42
|
+
} catch {
|
|
43
|
+
return "/"
|
|
44
|
+
}
|
|
45
|
+
}, [main_base_url])
|
|
22
46
|
|
|
23
47
|
return (
|
|
24
48
|
<div
|
|
@@ -26,46 +50,107 @@ export function MainSidebarFlyout({
|
|
|
26
50
|
onMouseLeave={onMouseLeave}
|
|
27
51
|
className="h-full flex flex-col flex-shrink-0 overflow-hidden bg-gray-50 border-r border-ui-border shadow-[4px_0_20px_-4px_rgba(0,0,0,0.10)] w-72 lg:w-80"
|
|
28
52
|
>
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
{item ? (
|
|
54
|
+
/* ── Hovered item: sub-menu list ─────────────────────────────────── */
|
|
55
|
+
<>
|
|
56
|
+
<div className="flex items-center px-4 lg:px-5 pt-5 pb-1 flex-shrink-0 gap-2.5">
|
|
57
|
+
<item.icon className="h-5 w-5 flex-shrink-0" style={{ color: "#4589ff" }} />
|
|
58
|
+
<h2 className="text-sm font-semibold text-text-primary tracking-wide truncate flex-1">
|
|
59
|
+
{item.label}
|
|
60
|
+
</h2>
|
|
61
|
+
</div>
|
|
36
62
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
{item.description && (
|
|
64
|
+
<p className="px-4 lg:px-5 pt-1 pb-4 text-xs text-text-secondary leading-relaxed">
|
|
65
|
+
{item.description}
|
|
66
|
+
</p>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
<nav className="flex-1 overflow-y-auto px-2 lg:px-3 py-2 space-y-0.5">
|
|
70
|
+
{subItems.map((sub) => {
|
|
71
|
+
const SubIcon = sub.icon
|
|
72
|
+
return (
|
|
73
|
+
<a
|
|
74
|
+
key={sub.id}
|
|
75
|
+
href={sub.href}
|
|
76
|
+
className={cn(
|
|
77
|
+
"flex items-start transition-colors border-l-4",
|
|
78
|
+
"px-2 lg:px-3 py-2 gap-x-3",
|
|
79
|
+
"hover:bg-ui-background border-transparent hover:border-interactive",
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
<SubIcon className="h-4 w-4 flex-shrink-0 mt-0.5" style={{ color: "#4589ff" }} />
|
|
83
|
+
<div className="min-w-0 flex-1">
|
|
84
|
+
<p className="text-sm font-medium text-text-primary leading-snug truncate">{sub.name}</p>
|
|
85
|
+
{sub.description && (
|
|
86
|
+
<p className="text-xs text-text-secondary leading-relaxed mt-0.5 line-clamp-2">{sub.description}</p>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</a>
|
|
90
|
+
)
|
|
91
|
+
})}
|
|
92
|
+
</nav>
|
|
93
|
+
</>
|
|
94
|
+
) : (
|
|
95
|
+
/* ── Default state: welcome + CTA + doc links ───────────────────── */
|
|
96
|
+
<div className="flex flex-col h-full">
|
|
97
|
+
{/* Header */}
|
|
98
|
+
<div className="px-5 pt-6 pb-5 border-b border-ui-border flex-shrink-0">
|
|
99
|
+
<div className="flex items-center gap-2.5 mb-3">
|
|
100
|
+
<div className="h-8 w-8 rounded-md bg-interactive flex items-center justify-center flex-shrink-0">
|
|
101
|
+
<Home className="h-4 w-4 text-white" />
|
|
102
|
+
</div>
|
|
103
|
+
<span className="text-sm font-semibold text-text-primary truncate">{defaultTitle}</span>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{defaultDescription && (
|
|
107
|
+
<p className="text-xs text-text-secondary leading-relaxed mb-4">
|
|
108
|
+
{defaultDescription}
|
|
109
|
+
</p>
|
|
110
|
+
)}
|
|
43
111
|
|
|
44
|
-
{/* ── Nav items ────────────────────────────────────────────────────────── */}
|
|
45
|
-
<nav className="flex-1 overflow-y-auto px-2 lg:px-3 py-2 space-y-0.5">
|
|
46
|
-
{subItems.map((sub) => {
|
|
47
|
-
const SubIcon = sub.icon
|
|
48
|
-
return (
|
|
49
112
|
<a
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
className={cn(
|
|
53
|
-
"flex items-start transition-colors border-l-4",
|
|
54
|
-
"px-2 lg:px-3 py-2 gap-x-3",
|
|
55
|
-
"hover:bg-ui-background border-transparent hover:border-interactive",
|
|
56
|
-
)}
|
|
113
|
+
href={homePath}
|
|
114
|
+
className="flex items-center justify-center gap-2 w-full px-3 py-2 bg-interactive hover:bg-interactive-hover text-white text-sm font-medium transition-colors"
|
|
57
115
|
>
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
<p className="text-sm font-medium text-text-primary leading-snug truncate">{sub.name}</p>
|
|
61
|
-
{sub.description && (
|
|
62
|
-
<p className="text-xs text-text-secondary leading-relaxed mt-0.5 line-clamp-2">{sub.description}</p>
|
|
63
|
-
)}
|
|
64
|
-
</div>
|
|
116
|
+
<Home className="h-4 w-4 flex-shrink-0" />
|
|
117
|
+
Tableau de bord
|
|
65
118
|
</a>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Doc / resource links */}
|
|
122
|
+
{defaultLinks.length > 0 && (
|
|
123
|
+
<nav className="flex-1 overflow-y-auto px-2 lg:px-3 py-3 space-y-0.5">
|
|
124
|
+
<p className="px-2 pb-1 text-[10px] font-semibold text-text-secondary uppercase tracking-widest">
|
|
125
|
+
Ressources
|
|
126
|
+
</p>
|
|
127
|
+
{defaultLinks.map((link, i) => {
|
|
128
|
+
const LinkIcon = link.icon ?? ExternalLink
|
|
129
|
+
const isExternal = link.href.startsWith("http://") || link.href.startsWith("https://")
|
|
130
|
+
return (
|
|
131
|
+
<a
|
|
132
|
+
key={i}
|
|
133
|
+
href={link.href}
|
|
134
|
+
{...(isExternal ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
135
|
+
className="flex items-start gap-x-3 px-2 lg:px-3 py-2 border-l-4 border-transparent hover:bg-ui-background hover:border-interactive transition-colors"
|
|
136
|
+
>
|
|
137
|
+
<LinkIcon className="h-4 w-4 flex-shrink-0 mt-0.5 text-text-secondary" />
|
|
138
|
+
<div className="min-w-0 flex-1">
|
|
139
|
+
<p className="text-sm font-medium text-text-primary leading-snug flex items-center gap-1.5">
|
|
140
|
+
{link.label}
|
|
141
|
+
{isExternal && <ExternalLink className="h-3 w-3 text-text-secondary flex-shrink-0" />}
|
|
142
|
+
</p>
|
|
143
|
+
{link.description && (
|
|
144
|
+
<p className="text-xs text-text-secondary leading-relaxed mt-0.5 line-clamp-2">{link.description}</p>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
</a>
|
|
148
|
+
)
|
|
149
|
+
})}
|
|
150
|
+
</nav>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
69
154
|
</div>
|
|
70
155
|
)
|
|
71
156
|
}
|
|
@@ -7,7 +7,9 @@ import { Logo } from "../../ui/logo"
|
|
|
7
7
|
import { type MainMenuItem, type SidebarMenus } from "./data"
|
|
8
8
|
import { X, Menu, ChevronDown, ChevronRight } from "lucide-react"
|
|
9
9
|
import { useIsMobile } from "../../../hooks/use-mobile"
|
|
10
|
-
import { MainSidebarFlyout } from "./main-sidebar-flyout"
|
|
10
|
+
import { MainSidebarFlyout, type FlyoutDefaultLink } from "./main-sidebar-flyout"
|
|
11
|
+
|
|
12
|
+
export type { FlyoutDefaultLink }
|
|
11
13
|
|
|
12
14
|
export type SidebarMode = 'expanded' | 'minimized' | 'hidden'
|
|
13
15
|
|
|
@@ -21,6 +23,9 @@ interface MainSidebarProps {
|
|
|
21
23
|
onSecondarySidebarOpen?: () => void
|
|
22
24
|
sidebarMenus?: SidebarMenus
|
|
23
25
|
mainMenuItems?: MainMenuItem[]
|
|
26
|
+
flyoutDefaultTitle?: string
|
|
27
|
+
flyoutDefaultDescription?: string
|
|
28
|
+
flyoutDefaultLinks?: FlyoutDefaultLink[]
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
export function MainSidebar({
|
|
@@ -33,6 +38,9 @@ export function MainSidebar({
|
|
|
33
38
|
onSecondarySidebarOpen,
|
|
34
39
|
sidebarMenus = {},
|
|
35
40
|
mainMenuItems = [],
|
|
41
|
+
flyoutDefaultTitle,
|
|
42
|
+
flyoutDefaultDescription,
|
|
43
|
+
flyoutDefaultLinks = [],
|
|
36
44
|
}: MainSidebarProps) {
|
|
37
45
|
const isMinimized = mode === 'minimized'
|
|
38
46
|
const isMobile = useIsMobile()
|
|
@@ -66,6 +74,40 @@ export function MainSidebar({
|
|
|
66
74
|
setHoveredItem(item)
|
|
67
75
|
}
|
|
68
76
|
|
|
77
|
+
const applyTemplateVars = (href: string): string => {
|
|
78
|
+
// Guard: SSR has no window/localStorage
|
|
79
|
+
if (typeof window === "undefined") return href
|
|
80
|
+
try {
|
|
81
|
+
const projRaw = localStorage.getItem("current-business-unit")
|
|
82
|
+
const envRaw = localStorage.getItem("current-environment")
|
|
83
|
+
// Parse carefully: value may be null (not yet set), invalid JSON, or a non-object
|
|
84
|
+
const currentProject = typeof projRaw === "string"
|
|
85
|
+
? (JSON.parse(projRaw)?.state?.currentProject ?? "")
|
|
86
|
+
: ""
|
|
87
|
+
const currentEnv = typeof envRaw === "string"
|
|
88
|
+
? (JSON.parse(envRaw)?.state?.currentEnv ?? "")
|
|
89
|
+
: ""
|
|
90
|
+
// Only replace when the value is a non-empty string; if empty, leave the
|
|
91
|
+
// placeholder in the URL so the link is visibly broken rather than silently
|
|
92
|
+
// producing a protocol-relative URL (e.g. "//path") that navigates incorrectly.
|
|
93
|
+
let result = href
|
|
94
|
+
if (currentProject && typeof currentProject === "string") {
|
|
95
|
+
result = result.replace(/\$\{currentProject\}/g, currentProject)
|
|
96
|
+
}
|
|
97
|
+
if (currentEnv && typeof currentEnv === "string") {
|
|
98
|
+
result = result.replace(/\$\{currentEnv\}/g, currentEnv)
|
|
99
|
+
}
|
|
100
|
+
return result
|
|
101
|
+
} catch {
|
|
102
|
+
// localStorage blocked (Safari private mode), JSON parse error, etc. — return unchanged
|
|
103
|
+
return href
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const safeUrlPathname = (url: string): string => {
|
|
108
|
+
try { return new URL(url).pathname } catch { return "/" }
|
|
109
|
+
}
|
|
110
|
+
|
|
69
111
|
const handleMenuClick = (item: MainMenuItem) => {
|
|
70
112
|
const menuId = item.id
|
|
71
113
|
const hasSubMenu = (sidebarMenus[menuId]?.length ?? 0) > 0
|
|
@@ -81,24 +123,34 @@ export function MainSidebar({
|
|
|
81
123
|
if (!isMinimized && onSecondarySidebarOpen) onSecondarySidebarOpen()
|
|
82
124
|
|
|
83
125
|
if (item.href) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
126
|
+
try {
|
|
127
|
+
const resolved = applyTemplateVars(item.href)
|
|
128
|
+
const path = resolved.startsWith('http://') || resolved.startsWith('https://')
|
|
129
|
+
? safeUrlPathname(resolved)
|
|
130
|
+
: resolved
|
|
131
|
+
window.location.href = path
|
|
132
|
+
} catch {
|
|
133
|
+
// navigation error — don't crash the sidebar
|
|
134
|
+
}
|
|
88
135
|
}
|
|
89
136
|
|
|
90
137
|
if (!isMinimized) onToggle()
|
|
91
138
|
}
|
|
92
139
|
|
|
93
140
|
const buildSubItemHref = (menuId: string, href: string): string => {
|
|
94
|
-
|
|
95
|
-
|
|
141
|
+
try {
|
|
142
|
+
const resolved = applyTemplateVars(href)
|
|
143
|
+
if (resolved.startsWith('http://') || resolved.startsWith('https://')) {
|
|
144
|
+
return safeUrlPathname(resolved)
|
|
145
|
+
}
|
|
146
|
+
const cleanHref = resolved.replace(/^\//, '')
|
|
147
|
+
const cleanBase = (main_base_url.startsWith('http://') || main_base_url.startsWith('https://'))
|
|
148
|
+
? safeUrlPathname(main_base_url).replace(/\/$/, '')
|
|
149
|
+
: (main_base_url ?? "").replace(/\/$/, '')
|
|
150
|
+
return `${cleanBase}/${menuId}/${cleanHref}`
|
|
151
|
+
} catch {
|
|
152
|
+
return "/"
|
|
96
153
|
}
|
|
97
|
-
const cleanHref = href.replace(/^\//, '')
|
|
98
|
-
const cleanBase = (main_base_url.startsWith('http://') || main_base_url.startsWith('https://'))
|
|
99
|
-
? new URL(main_base_url).pathname.replace(/\/$/, '')
|
|
100
|
-
: main_base_url.replace(/\/$/, '')
|
|
101
|
-
return `${cleanBase}/${menuId}/${cleanHref}`
|
|
102
154
|
}
|
|
103
155
|
|
|
104
156
|
const handleSubMenuClick = (e: React.MouseEvent, menuId: string, href: string) => {
|
|
@@ -119,7 +171,8 @@ export function MainSidebar({
|
|
|
119
171
|
The flyout is never "in front of" the nav; it's always to its right.
|
|
120
172
|
*/}
|
|
121
173
|
<div className={cn(
|
|
122
|
-
"fixed left-0
|
|
174
|
+
"fixed left-0 z-50 flex",
|
|
175
|
+
isMinimized ? "top-14 h-[calc(100%-3.5rem)]" : "top-0 h-full",
|
|
123
176
|
"transform transition-all duration-300 ease-in-out",
|
|
124
177
|
isMinimized ? "translate-x-0" : (isOpen ? "translate-x-0" : "-translate-x-full"),
|
|
125
178
|
)}>
|
|
@@ -131,27 +184,20 @@ export function MainSidebar({
|
|
|
131
184
|
// Shadow only when flyout is not open (avoid double shadow)
|
|
132
185
|
!hoveredItem && !isMinimized && "shadow-xl",
|
|
133
186
|
)}>
|
|
134
|
-
{/* Header */}
|
|
135
|
-
|
|
136
|
-
"flex items-center h-14 bg-white sticky top-0 z-10 flex-shrink-0"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
className="h-8 w-8 p-0 hover:bg-ui-background text-text-secondary"
|
|
149
|
-
>
|
|
150
|
-
<X className="h-4 w-4" />
|
|
151
|
-
</Button>
|
|
152
|
-
</>
|
|
153
|
-
)}
|
|
154
|
-
</div>
|
|
187
|
+
{/* Header — only in overlay (non-minimized) mode; logo lives in the global header */}
|
|
188
|
+
{!isMinimized && (
|
|
189
|
+
<div className="flex items-center justify-between h-14 bg-white sticky top-0 z-10 flex-shrink-0 px-3 lg:px-4">
|
|
190
|
+
<Logo className="text-2xl font-semibold text-text-primary" href={main_base_url} />
|
|
191
|
+
<Button
|
|
192
|
+
variant="ghost"
|
|
193
|
+
size="sm"
|
|
194
|
+
onClick={onToggle}
|
|
195
|
+
className="h-8 w-8 p-0 hover:bg-ui-background text-text-secondary"
|
|
196
|
+
>
|
|
197
|
+
<X className="h-4 w-4" />
|
|
198
|
+
</Button>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
155
201
|
|
|
156
202
|
{/* Nav items */}
|
|
157
203
|
<nav className={cn("flex-1 space-y-0.5 lg:space-y-1", isMinimized ? "p-2" : "p-2 lg:p-3")}>
|
|
@@ -163,9 +209,14 @@ export function MainSidebar({
|
|
|
163
209
|
const isFlyActive = hoveredItem?.id === item.id
|
|
164
210
|
|
|
165
211
|
const itemHref = !hasSubMenu && item.href
|
|
166
|
-
? (
|
|
167
|
-
|
|
168
|
-
|
|
212
|
+
? (() => {
|
|
213
|
+
try {
|
|
214
|
+
const resolved = applyTemplateVars(item.href)
|
|
215
|
+
return resolved.startsWith('http://') || resolved.startsWith('https://')
|
|
216
|
+
? safeUrlPathname(resolved)
|
|
217
|
+
: resolved
|
|
218
|
+
} catch { return item.href }
|
|
219
|
+
})()
|
|
169
220
|
: undefined
|
|
170
221
|
|
|
171
222
|
const itemCls = cn(
|
|
@@ -237,15 +288,23 @@ export function MainSidebar({
|
|
|
237
288
|
</div>
|
|
238
289
|
|
|
239
290
|
{/* ── Flyout — desktop only, flex child to the right of nav ─────────── */}
|
|
240
|
-
{
|
|
291
|
+
{/* Shown when the sidebar is open (default state) or an item is hovered (sub-menu state) */}
|
|
292
|
+
{!isMobile && (hoveredItem || (isOpen && !isMinimized)) && (
|
|
241
293
|
<MainSidebarFlyout
|
|
242
294
|
item={hoveredItem}
|
|
243
|
-
subItems={
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
295
|
+
subItems={hoveredItem
|
|
296
|
+
? (sidebarMenus[hoveredItem.id] ?? []).map(sub => ({
|
|
297
|
+
...sub,
|
|
298
|
+
href: buildSubItemHref(hoveredItem.id, sub.href),
|
|
299
|
+
}))
|
|
300
|
+
: []
|
|
301
|
+
}
|
|
247
302
|
onMouseEnter={cancelCloseTimer}
|
|
248
303
|
onMouseLeave={startCloseTimer}
|
|
304
|
+
main_base_url={main_base_url}
|
|
305
|
+
defaultTitle={flyoutDefaultTitle}
|
|
306
|
+
defaultDescription={flyoutDefaultDescription}
|
|
307
|
+
defaultLinks={flyoutDefaultLinks}
|
|
249
308
|
/>
|
|
250
309
|
)}
|
|
251
310
|
|
|
@@ -18,8 +18,6 @@ import {
|
|
|
18
18
|
TooltipProvider,
|
|
19
19
|
TooltipTrigger,
|
|
20
20
|
} from "../../ui/tooltip"
|
|
21
|
-
import { Menu } from "lucide-react"
|
|
22
|
-
import { Logo } from "../../ui/logo"
|
|
23
21
|
import { type SidebarMenus, type SubMenuItem, type MainMenuItem } from "./data"
|
|
24
22
|
import { Skeleton } from "../skeleton"
|
|
25
23
|
|
|
@@ -232,7 +230,7 @@ interface SidebarProps {
|
|
|
232
230
|
|
|
233
231
|
|
|
234
232
|
|
|
235
|
-
function Sidebar({ currentMenu,
|
|
233
|
+
function Sidebar({ currentMenu, sidebarMenus = {}, main_base_url = "", sectionLabels = {}, getCurrentMenuItem, mainMenuItems = [] }: SidebarProps = {}) {
|
|
236
234
|
const pathname = usePathname()
|
|
237
235
|
const searchParams = useSearchParams()
|
|
238
236
|
const router = useRouter()
|
|
@@ -259,36 +257,9 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
|
|
|
259
257
|
|
|
260
258
|
return (
|
|
261
259
|
<div
|
|
262
|
-
className="h-
|
|
260
|
+
className="h-full flex-shrink-0 flex flex-col bg-gray-50 min-w-[var(--sidebar-width-icon)] transition-[width] duration-200 overflow-visible"
|
|
263
261
|
style={{ width: isCollapsed ? "var(--sidebar-width-icon)" : "var(--sidebar-width)" }}
|
|
264
262
|
>
|
|
265
|
-
{/* Header */}
|
|
266
|
-
<div className={cn(
|
|
267
|
-
"h-14 flex items-center flex-shrink-0",
|
|
268
|
-
isCollapsed ? "justify-center px-2" : "justify-between px-4"
|
|
269
|
-
)}>
|
|
270
|
-
{isCollapsed ? (
|
|
271
|
-
<button type="button" onClick={toggleSidebar} title="Expand sidebar" className="flex items-center justify-center">
|
|
272
|
-
<Logo iconOnly />
|
|
273
|
-
</button>
|
|
274
|
-
) : (
|
|
275
|
-
<>
|
|
276
|
-
<Logo />
|
|
277
|
-
{onMainMenuToggle && (
|
|
278
|
-
<Button
|
|
279
|
-
variant="ghost"
|
|
280
|
-
size="sm"
|
|
281
|
-
onClick={onMainMenuToggle}
|
|
282
|
-
className="h-8 w-8 p-0 hover:bg-ui-background rounded-lg"
|
|
283
|
-
title="Ouvrir le menu principal"
|
|
284
|
-
>
|
|
285
|
-
<Menu className="h-4 w-4" />
|
|
286
|
-
</Button>
|
|
287
|
-
)}
|
|
288
|
-
</>
|
|
289
|
-
)}
|
|
290
|
-
</div>
|
|
291
|
-
|
|
292
263
|
{/* Section label — hidden when collapsed */}
|
|
293
264
|
{!isCollapsed && (
|
|
294
265
|
injectedItems === undefined ? (
|