@orsetra/shared-ui 1.6.0 → 1.6.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.
|
@@ -5,7 +5,7 @@ import { Slot } from "@radix-ui/react-slot"
|
|
|
5
5
|
import { VariantProps, cva } from "class-variance-authority"
|
|
6
6
|
import { PanelLeft, ChevronLeft, ChevronRight } from "lucide-react"
|
|
7
7
|
import Link from "next/link"
|
|
8
|
-
import { usePathname, useSearchParams } from "next/navigation"
|
|
8
|
+
import { usePathname, useSearchParams, useRouter } from "next/navigation"
|
|
9
9
|
|
|
10
10
|
import { useIsMobile } from "../../../hooks/use-mobile"
|
|
11
11
|
import { cn } from "../../../lib/utils"
|
|
@@ -26,17 +26,21 @@ import { Skeleton } from "../skeleton"
|
|
|
26
26
|
// ── Dynamic secondary navigation ──────────────────────────────────────────────
|
|
27
27
|
|
|
28
28
|
interface SidebarNavigationContextValue {
|
|
29
|
-
injectedItems:
|
|
30
|
-
injectedTitle:
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
injectedItems: SubMenuItem[] | null | undefined // null = not injected, undefined = injected but loading
|
|
30
|
+
injectedTitle: string | null
|
|
31
|
+
injectedBackHref: string | null
|
|
32
|
+
setInjectedItems: React.Dispatch<React.SetStateAction<SubMenuItem[] | null | undefined>>
|
|
33
|
+
setInjectedTitle: React.Dispatch<React.SetStateAction<string | null>>
|
|
34
|
+
setInjectedBackHref: React.Dispatch<React.SetStateAction<string | null>>
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const SidebarNavigationContext = React.createContext<SidebarNavigationContextValue>({
|
|
36
|
-
injectedItems:
|
|
37
|
-
injectedTitle:
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
injectedItems: null,
|
|
39
|
+
injectedTitle: null,
|
|
40
|
+
injectedBackHref: null,
|
|
41
|
+
setInjectedItems: () => {},
|
|
42
|
+
setInjectedTitle: () => {},
|
|
43
|
+
setInjectedBackHref: () => {},
|
|
40
44
|
})
|
|
41
45
|
|
|
42
46
|
export function useSidebarNavigation() {
|
|
@@ -48,16 +52,18 @@ export function useSidebarNavigation() {
|
|
|
48
52
|
* (and optionally its section title) for as long as the component is mounted.
|
|
49
53
|
* `items` must be referentially stable (useMemo / defined outside render).
|
|
50
54
|
*/
|
|
51
|
-
export function useInjectSidebarNavigation(items: SubMenuItem[] | undefined, title?: string) {
|
|
52
|
-
const { setInjectedItems, setInjectedTitle } = useSidebarNavigation()
|
|
55
|
+
export function useInjectSidebarNavigation(items: SubMenuItem[] | undefined, title?: string, backHref?: string) {
|
|
56
|
+
const { setInjectedItems, setInjectedTitle, setInjectedBackHref } = useSidebarNavigation()
|
|
53
57
|
React.useEffect(() => {
|
|
54
58
|
setInjectedItems(items)
|
|
55
59
|
setInjectedTitle(title ?? null)
|
|
60
|
+
setInjectedBackHref(backHref ?? null)
|
|
56
61
|
return () => {
|
|
57
62
|
setInjectedItems(null)
|
|
58
63
|
setInjectedTitle(null)
|
|
64
|
+
setInjectedBackHref(null)
|
|
59
65
|
}
|
|
60
|
-
}, [items, title]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
66
|
+
}, [items, title, backHref]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
// ── Sidebar storage ───────────────────────────────────────────────────────────
|
|
@@ -183,9 +189,10 @@ const SidebarProvider = React.forwardRef<
|
|
|
183
189
|
|
|
184
190
|
const [injectedItems, setInjectedItems] = React.useState<SubMenuItem[] | null | undefined>(null)
|
|
185
191
|
const [injectedTitle, setInjectedTitle] = React.useState<string | null>(null)
|
|
192
|
+
const [injectedBackHref, setInjectedBackHref] = React.useState<string | null>(null)
|
|
186
193
|
const navContextValue = React.useMemo<SidebarNavigationContextValue>(
|
|
187
|
-
() => ({ injectedItems, injectedTitle, setInjectedItems, setInjectedTitle }),
|
|
188
|
-
[injectedItems, injectedTitle]
|
|
194
|
+
() => ({ injectedItems, injectedTitle, injectedBackHref, setInjectedItems, setInjectedTitle, setInjectedBackHref }),
|
|
195
|
+
[injectedItems, injectedTitle, injectedBackHref]
|
|
189
196
|
)
|
|
190
197
|
|
|
191
198
|
return (
|
|
@@ -234,10 +241,11 @@ interface SidebarProps {
|
|
|
234
241
|
|
|
235
242
|
|
|
236
243
|
function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_url = "", sectionLabels = {}, getCurrentMenuItem, mainMenuItems = [] }: SidebarProps = {}) {
|
|
237
|
-
const pathname
|
|
244
|
+
const pathname = usePathname()
|
|
238
245
|
const searchParams = useSearchParams()
|
|
246
|
+
const router = useRouter()
|
|
239
247
|
const { state, toggleSidebar } = useSidebar()
|
|
240
|
-
const { injectedItems, injectedTitle } = useSidebarNavigation()
|
|
248
|
+
const { injectedItems, injectedTitle, injectedBackHref } = useSidebarNavigation()
|
|
241
249
|
|
|
242
250
|
const isInjecting = injectedItems !== null // null = default nav, undefined/array = detail page mounted
|
|
243
251
|
const currentNavigation: SubMenuItem[] = isInjecting
|
|
@@ -245,6 +253,12 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
|
|
|
245
253
|
: (sidebarMenus['items'] ?? (currentMenu && sidebarMenus[currentMenu]) ?? [])
|
|
246
254
|
|
|
247
255
|
const isCollapsed = state === "collapsed"
|
|
256
|
+
|
|
257
|
+
const currentMainMenuItem = React.useMemo(
|
|
258
|
+
() => mainMenuItems.find(m => m.id === currentMenu),
|
|
259
|
+
[mainMenuItems, currentMenu]
|
|
260
|
+
)
|
|
261
|
+
|
|
248
262
|
const filteredMainItems = React.useMemo(() => {
|
|
249
263
|
if (!isCollapsed || mainMenuItems.length === 0 || isInjecting) return []
|
|
250
264
|
const navIds = new Set(currentNavigation.map((n) => n.id))
|
|
@@ -286,12 +300,32 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
|
|
|
286
300
|
{/* Section label — hidden when collapsed */}
|
|
287
301
|
{!isCollapsed && (
|
|
288
302
|
injectedItems === undefined ? (
|
|
289
|
-
<div className="h-10 flex items-center px-4 flex-shrink-0">
|
|
303
|
+
<div className="h-10 flex items-center px-4 flex-shrink-0 gap-2 border-b border-ui-border">
|
|
304
|
+
<div className="h-4 w-4 bg-gray-200 animate-pulse rounded flex-shrink-0" />
|
|
290
305
|
<div className="h-2.5 w-20 bg-gray-200 animate-pulse rounded" />
|
|
291
306
|
</div>
|
|
292
307
|
) : (injectedTitle || (currentMenu && sectionLabels[currentMenu])) ? (
|
|
293
|
-
<div className="h-10 flex items-center px-4 flex-shrink-0">
|
|
294
|
-
|
|
308
|
+
<div className="h-10 flex items-center px-4 flex-shrink-0 gap-2 border-b border-ui-border">
|
|
309
|
+
{isInjecting ? (
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
onClick={() => injectedBackHref ? router.push(injectedBackHref) : router.back()}
|
|
313
|
+
className="h-6 w-6 flex items-center justify-center text-text-secondary hover:text-text-primary hover:bg-ui-background rounded transition-colors flex-shrink-0"
|
|
314
|
+
title="Back"
|
|
315
|
+
>
|
|
316
|
+
<ChevronLeft className="h-3.5 w-3.5" />
|
|
317
|
+
</button>
|
|
318
|
+
) : currentMainMenuItem ? (
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
onClick={() => router.push(currentMainMenuItem.href)}
|
|
322
|
+
className="h-6 w-6 flex items-center justify-center text-text-secondary hover:text-text-primary hover:bg-ui-background rounded transition-colors flex-shrink-0"
|
|
323
|
+
title={currentMainMenuItem.label}
|
|
324
|
+
>
|
|
325
|
+
<currentMainMenuItem.icon className="h-3.5 w-3.5" />
|
|
326
|
+
</button>
|
|
327
|
+
) : null}
|
|
328
|
+
<h2 className="text-xs font-semibold text-text-secondary capitalize tracking-wide truncate flex-1">
|
|
295
329
|
{injectedTitle || sectionLabels[currentMenu!]}
|
|
296
330
|
</h2>
|
|
297
331
|
</div>
|
|
@@ -318,12 +352,13 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
|
|
|
318
352
|
</div>
|
|
319
353
|
))
|
|
320
354
|
) : currentNavigation.map((item) => {
|
|
321
|
-
const activeItemId = !
|
|
355
|
+
const activeItemId = !isInjecting && getCurrentMenuItem
|
|
322
356
|
? getCurrentMenuItem(pathname, searchParams)
|
|
323
357
|
: null
|
|
358
|
+
const itemPath = item.href.split('?')[0]
|
|
324
359
|
const isActive = activeItemId
|
|
325
360
|
? item.id === activeItemId
|
|
326
|
-
: (pathname ===
|
|
361
|
+
: (pathname === itemPath || pathname.startsWith(`${itemPath}/`))
|
|
327
362
|
|
|
328
363
|
const handleClick = (e: React.MouseEvent) => {
|
|
329
364
|
const href = item.href
|
|
@@ -351,12 +386,7 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
|
|
|
351
386
|
style={{ color: isActive ? '#0f62fe' : '#4589ff' }}
|
|
352
387
|
/>
|
|
353
388
|
{!isCollapsed && (
|
|
354
|
-
|
|
355
|
-
<span className="flex-1 truncate">{item.name}</span>
|
|
356
|
-
{item.applied && (
|
|
357
|
-
<span className="h-1.5 w-1.5 rounded-full bg-green-500 flex-shrink-0" />
|
|
358
|
-
)}
|
|
359
|
-
</>
|
|
389
|
+
<span className="flex-1 truncate">{item.name}</span>
|
|
360
390
|
)}
|
|
361
391
|
</Link>
|
|
362
392
|
)
|
|
@@ -375,7 +405,7 @@ function Sidebar({ currentMenu, onMainMenuToggle, sidebarMenus = {}, main_base_u
|
|
|
375
405
|
{/* Main menu items — only in collapsed mode, below secondary items, neutral color */}
|
|
376
406
|
{filteredMainItems.length > 0 && (
|
|
377
407
|
<>
|
|
378
|
-
<div className="border-
|
|
408
|
+
<div className="border-b border-ui-border mx-1 my-2" />
|
|
379
409
|
{filteredMainItems.map((item) => {
|
|
380
410
|
const handleClick = (e: React.MouseEvent) => {
|
|
381
411
|
const href = item.href ?? ""
|