@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: SubMenuItem[] | null | undefined // null = not injected, undefined = injected but loading
30
- injectedTitle: string | null
31
- setInjectedItems: React.Dispatch<React.SetStateAction<SubMenuItem[] | null | undefined>>
32
- setInjectedTitle: React.Dispatch<React.SetStateAction<string | null>>
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: null,
37
- injectedTitle: null,
38
- setInjectedItems: () => {},
39
- setInjectedTitle: () => {},
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 = usePathname()
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
- <h2 className="text-xs font-semibold text-text-secondary uppercase tracking-wide truncate">
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 = !injectedItems && getCurrentMenuItem
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 === item.href || pathname.startsWith(`${item.href}/`))
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-t border-ui-border mx-1 my-2" />
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 ?? ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",