@orsetra/shared-ui 1.9.2 → 1.10.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.
- package/components/layout/index.ts +2 -1
- package/components/layout/layout-container.tsx +10 -1
- package/components/layout/sidebar/data.tsx +5 -0
- package/components/layout/sidebar/main-sidebar-flyout.tsx +40 -3
- package/components/layout/sidebar/main-sidebar.tsx +9 -6
- package/components/layout/sidebar/quick-access-tracker.tsx +36 -0
- package/hooks/index.ts +1 -0
- package/hooks/use-quick-access.ts +74 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
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, type OrgInfo, type Project } from './sidebar/data'
|
|
3
|
+
export { resolveIcon, type MainMenuItem, type SubMenuItem, type SidebarMenus, type SidebarMenuMeta, type MenuApiResponse, type ApiMenuData, type ApiMainMenuItem, type ApiSubMenuItem, type ApiSubMenu, type OrgInfo, type Project } from './sidebar/data'
|
|
4
|
+
export { QuickAccessTracker } from './sidebar/quick-access-tracker'
|
|
4
5
|
export {
|
|
5
6
|
Sidebar,
|
|
6
7
|
SidebarContent,
|
|
@@ -80,7 +80,16 @@ function LayoutContent({
|
|
|
80
80
|
icon: resolveIcon(item.icon),
|
|
81
81
|
}))
|
|
82
82
|
labels[key] = subMenu.header
|
|
83
|
-
meta[key] = {
|
|
83
|
+
meta[key] = {
|
|
84
|
+
header: subMenu.header,
|
|
85
|
+
description: subMenu.description,
|
|
86
|
+
links: subMenu.links?.map(l => ({
|
|
87
|
+
label: l.label,
|
|
88
|
+
href: l.href,
|
|
89
|
+
description: l.description,
|
|
90
|
+
icon: l.icon,
|
|
91
|
+
})),
|
|
92
|
+
}
|
|
84
93
|
})
|
|
85
94
|
setFetchedSidebarMenus(menus)
|
|
86
95
|
setSectionLabels(labels)
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
ArrowRightLeft,
|
|
37
37
|
ArrowLeftRight,
|
|
38
38
|
Puzzle,
|
|
39
|
+
LifeBuoy,
|
|
39
40
|
} from "lucide-react"
|
|
40
41
|
|
|
41
42
|
export interface MainMenuItem {
|
|
@@ -91,12 +92,15 @@ export interface ApiSubMenu {
|
|
|
91
92
|
id: string
|
|
92
93
|
header: string
|
|
93
94
|
description?: string
|
|
95
|
+
/** Resource/doc links shown in the flyout default state (distinct from nav items) */
|
|
96
|
+
links?: ApiSubMenuItem[]
|
|
94
97
|
items: ApiSubMenuItem[]
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
export interface SidebarMenuMeta {
|
|
98
101
|
header?: string
|
|
99
102
|
description?: string
|
|
103
|
+
links?: Array<{ label: string; href: string; description?: string; icon?: string }>
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
export interface ApiMenuData {
|
|
@@ -156,6 +160,7 @@ const ICON_MAP: Record<string, LucideIcon> = {
|
|
|
156
160
|
FileText,
|
|
157
161
|
Code,
|
|
158
162
|
Cloud,
|
|
163
|
+
LifeBuoy,
|
|
159
164
|
}
|
|
160
165
|
|
|
161
166
|
export function resolveIcon(name: string): LucideIcon {
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import { cn } from "../../../lib/utils"
|
|
5
5
|
import type { MainMenuItem, SubMenuItem } from "./data"
|
|
6
|
+
import { resolveIcon } from "./data"
|
|
6
7
|
import { Home, ExternalLink, type LucideIcon } from "lucide-react"
|
|
8
|
+
import type { QuickAccessItem } from "../../../hooks/use-quick-access"
|
|
7
9
|
|
|
8
10
|
export interface FlyoutDefaultLink {
|
|
9
11
|
label: string
|
|
10
12
|
href: string
|
|
11
13
|
description?: string
|
|
12
|
-
icon?: LucideIcon
|
|
14
|
+
icon?: LucideIcon | string
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
interface MainSidebarFlyoutProps {
|
|
@@ -21,6 +23,7 @@ interface MainSidebarFlyoutProps {
|
|
|
21
23
|
defaultTitle?: string
|
|
22
24
|
defaultDescription?: string
|
|
23
25
|
defaultLinks?: FlyoutDefaultLink[]
|
|
26
|
+
quickAccessItems?: QuickAccessItem[]
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function MainSidebarFlyout({
|
|
@@ -32,6 +35,7 @@ export function MainSidebarFlyout({
|
|
|
32
35
|
defaultTitle = "Camel X Platform",
|
|
33
36
|
defaultDescription,
|
|
34
37
|
defaultLinks = [],
|
|
38
|
+
quickAccessItems = [],
|
|
35
39
|
}: MainSidebarFlyoutProps) {
|
|
36
40
|
const homePath = React.useMemo(() => {
|
|
37
41
|
if (!main_base_url) return "/"
|
|
@@ -44,6 +48,8 @@ export function MainSidebarFlyout({
|
|
|
44
48
|
}
|
|
45
49
|
}, [main_base_url])
|
|
46
50
|
|
|
51
|
+
const hasQuickAccess = quickAccessItems.length > 0
|
|
52
|
+
|
|
47
53
|
return (
|
|
48
54
|
<div
|
|
49
55
|
onMouseEnter={onMouseEnter}
|
|
@@ -91,6 +97,33 @@ export function MainSidebarFlyout({
|
|
|
91
97
|
})}
|
|
92
98
|
</nav>
|
|
93
99
|
</>
|
|
100
|
+
) : hasQuickAccess ? (
|
|
101
|
+
/* ── Quick access: pages recently visited ────────────────────────── */
|
|
102
|
+
<div className="flex flex-col h-full items-center justify-center px-6 gap-6">
|
|
103
|
+
<div className="w-full">
|
|
104
|
+
<p className="px-2 pb-1 text-[10px] font-semibold text-text-secondary uppercase tracking-widest">
|
|
105
|
+
Accès rapide
|
|
106
|
+
</p>
|
|
107
|
+
<nav className="space-y-0.5">
|
|
108
|
+
{quickAccessItems.map((qa) => {
|
|
109
|
+
const QAIcon = qa.icon ? resolveIcon(qa.icon) : null
|
|
110
|
+
return (
|
|
111
|
+
<a
|
|
112
|
+
key={qa.id}
|
|
113
|
+
href={qa.href}
|
|
114
|
+
className="flex items-center gap-x-3 px-2 py-2 border-l-4 border-transparent hover:bg-ui-background hover:border-interactive transition-colors"
|
|
115
|
+
>
|
|
116
|
+
{QAIcon
|
|
117
|
+
? <QAIcon className="h-4 w-4 flex-shrink-0 text-text-secondary" />
|
|
118
|
+
: <span className="h-4 w-4 flex-shrink-0" />
|
|
119
|
+
}
|
|
120
|
+
<p className="text-sm font-medium text-text-primary leading-snug truncate">{qa.label}</p>
|
|
121
|
+
</a>
|
|
122
|
+
)
|
|
123
|
+
})}
|
|
124
|
+
</nav>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
94
127
|
) : (
|
|
95
128
|
/* ── Default state: centré verticalement ────────────────────────── */
|
|
96
129
|
<div className="flex flex-col h-full items-center justify-center px-6 gap-6">
|
|
@@ -102,7 +135,7 @@ export function MainSidebarFlyout({
|
|
|
102
135
|
)}
|
|
103
136
|
<a
|
|
104
137
|
href={homePath}
|
|
105
|
-
className="flex items-center justify-center gap-2 w-full px-3 py-2 border border-
|
|
138
|
+
className="flex items-center justify-center gap-2 w-full px-3 py-2 border border-interactive text-interactive hover:bg-interactive/10 text-sm font-medium transition-colors"
|
|
106
139
|
>
|
|
107
140
|
<Home className="h-4 w-4 flex-shrink-0" />
|
|
108
141
|
Tableau de bord
|
|
@@ -116,8 +149,12 @@ export function MainSidebarFlyout({
|
|
|
116
149
|
Ressources
|
|
117
150
|
</p>
|
|
118
151
|
{defaultLinks.map((link, i) => {
|
|
119
|
-
const LinkIcon = link.icon ?? ExternalLink
|
|
120
152
|
const isExternal = link.href.startsWith("http://") || link.href.startsWith("https://")
|
|
153
|
+
const LinkIcon: LucideIcon = !link.icon
|
|
154
|
+
? ExternalLink
|
|
155
|
+
: typeof link.icon === "string"
|
|
156
|
+
? resolveIcon(link.icon)
|
|
157
|
+
: link.icon
|
|
121
158
|
return (
|
|
122
159
|
<a
|
|
123
160
|
key={i}
|
|
@@ -10,6 +10,7 @@ export type { SidebarMenuMeta }
|
|
|
10
10
|
import { X, Menu, ChevronDown, ChevronRight } from "lucide-react"
|
|
11
11
|
import { useIsMobile } from "../../../hooks/use-mobile"
|
|
12
12
|
import { MainSidebarFlyout, type FlyoutDefaultLink } from "./main-sidebar-flyout"
|
|
13
|
+
import { useQuickAccess } from "../../../hooks/use-quick-access"
|
|
13
14
|
|
|
14
15
|
export type { FlyoutDefaultLink }
|
|
15
16
|
|
|
@@ -43,6 +44,7 @@ export function MainSidebar({
|
|
|
43
44
|
}: MainSidebarProps) {
|
|
44
45
|
const isMinimized = mode === 'minimized'
|
|
45
46
|
const isMobile = useIsMobile()
|
|
47
|
+
const { items: quickAccessItems } = useQuickAccess()
|
|
46
48
|
|
|
47
49
|
// Accordion — mobile only
|
|
48
50
|
const [expandedMenu, setExpandedMenu] = React.useState<string | null>(null)
|
|
@@ -152,15 +154,15 @@ export function MainSidebar({
|
|
|
152
154
|
}
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
// Default flyout content derived from the
|
|
157
|
+
// Default flyout content derived from the menu metadata
|
|
156
158
|
const mainMeta = sidebarMenuMeta["main"] ?? {}
|
|
157
159
|
const defaultTitle = mainMeta.header ?? "Camel X Platform"
|
|
158
160
|
const defaultDesc = mainMeta.description ?? undefined
|
|
159
|
-
const defaultLinks: FlyoutDefaultLink[] = (
|
|
160
|
-
label:
|
|
161
|
-
href:
|
|
162
|
-
description:
|
|
163
|
-
icon:
|
|
161
|
+
const defaultLinks: FlyoutDefaultLink[] = (mainMeta.links ?? []).map(l => ({
|
|
162
|
+
label: l.label,
|
|
163
|
+
href: l.href,
|
|
164
|
+
description: l.description,
|
|
165
|
+
icon: l.icon,
|
|
164
166
|
}))
|
|
165
167
|
|
|
166
168
|
const handleSubMenuClick = (e: React.MouseEvent, menuId: string, href: string) => {
|
|
@@ -315,6 +317,7 @@ export function MainSidebar({
|
|
|
315
317
|
defaultTitle={defaultTitle}
|
|
316
318
|
defaultDescription={defaultDesc}
|
|
317
319
|
defaultLinks={defaultLinks}
|
|
320
|
+
quickAccessItems={quickAccessItems}
|
|
318
321
|
/>
|
|
319
322
|
)}
|
|
320
323
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react"
|
|
4
|
+
import { usePathname } from "next/navigation"
|
|
5
|
+
import { useQuickAccess } from "../../../hooks/use-quick-access"
|
|
6
|
+
|
|
7
|
+
interface QuickAccessTrackerProps {
|
|
8
|
+
/** Human-readable label shown in the quick-access list */
|
|
9
|
+
label: string
|
|
10
|
+
/** Icon name (resolved via resolveIcon). Defaults to "Box". */
|
|
11
|
+
icon?: string
|
|
12
|
+
/** Set to false to skip tracking this page (e.g. list/index pages) */
|
|
13
|
+
enabled?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Drop this inside any page or layout to register that page in the
|
|
18
|
+
* sidebar's quick-access list. Tracks on mount and whenever pathname changes.
|
|
19
|
+
* Renders nothing.
|
|
20
|
+
*/
|
|
21
|
+
export function QuickAccessTracker({ label, icon, enabled = true }: QuickAccessTrackerProps) {
|
|
22
|
+
const pathname = usePathname()
|
|
23
|
+
const { addItem } = useQuickAccess()
|
|
24
|
+
// Stable ref so the effect doesn't re-run when addItem reference changes
|
|
25
|
+
const addRef = useRef(addItem)
|
|
26
|
+
addRef.current = addItem
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!enabled || !label || !pathname) return
|
|
30
|
+
addRef.current({ label, href: window.location.href, icon })
|
|
31
|
+
// pathname triggers the effect on navigation; window.location.href captures the full URL
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
}, [pathname, label, icon, enabled])
|
|
34
|
+
|
|
35
|
+
return null
|
|
36
|
+
}
|
package/hooks/index.ts
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react"
|
|
4
|
+
|
|
5
|
+
const STORAGE_KEY = "sidebar:quick-access"
|
|
6
|
+
const MAX_ITEMS = 4
|
|
7
|
+
|
|
8
|
+
export interface QuickAccessItem {
|
|
9
|
+
id: string
|
|
10
|
+
label: string
|
|
11
|
+
href: string
|
|
12
|
+
icon?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readStorage(): QuickAccessItem[] {
|
|
16
|
+
if (typeof window === "undefined") return []
|
|
17
|
+
try {
|
|
18
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
19
|
+
if (!raw) return []
|
|
20
|
+
const parsed = JSON.parse(raw)
|
|
21
|
+
return Array.isArray(parsed)
|
|
22
|
+
? parsed.filter(
|
|
23
|
+
(i): i is QuickAccessItem =>
|
|
24
|
+
i && typeof i.id === "string" && typeof i.label === "string" && typeof i.href === "string"
|
|
25
|
+
)
|
|
26
|
+
: []
|
|
27
|
+
} catch {
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeStorage(items: QuickAccessItem[]): void {
|
|
33
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(items)) } catch {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useQuickAccess() {
|
|
37
|
+
const [items, setItems] = useState<QuickAccessItem[]>(readStorage)
|
|
38
|
+
|
|
39
|
+
// Sync across tabs
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const onStorage = (e: StorageEvent) => {
|
|
42
|
+
if (e.key === STORAGE_KEY) setItems(readStorage())
|
|
43
|
+
}
|
|
44
|
+
window.addEventListener("storage", onStorage)
|
|
45
|
+
return () => window.removeEventListener("storage", onStorage)
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const addItem = useCallback((item: Omit<QuickAccessItem, "id"> & { id?: string }) => {
|
|
49
|
+
setItems(prev => {
|
|
50
|
+
const id = item.id ?? item.href
|
|
51
|
+
const next = [
|
|
52
|
+
{ id, label: item.label, href: item.href, icon: item.icon },
|
|
53
|
+
...prev.filter(i => i.href !== item.href),
|
|
54
|
+
].slice(0, MAX_ITEMS)
|
|
55
|
+
writeStorage(next)
|
|
56
|
+
return next
|
|
57
|
+
})
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
const removeItem = useCallback((href: string) => {
|
|
61
|
+
setItems(prev => {
|
|
62
|
+
const next = prev.filter(i => i.href !== href)
|
|
63
|
+
writeStorage(next)
|
|
64
|
+
return next
|
|
65
|
+
})
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
const clearItems = useCallback(() => {
|
|
69
|
+
setItems([])
|
|
70
|
+
try { localStorage.removeItem(STORAGE_KEY) } catch {}
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
return { items, addItem, removeItem, clearItems }
|
|
74
|
+
}
|