@orsetra/shared-ui 1.0.20 → 1.0.23

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 { usePathname } from "next/navigation"
5
5
  import { MainSidebar, type SidebarMode } from "./sidebar/main-sidebar"
6
6
  import { Sidebar, SidebarProvider, useSidebar } from "./sidebar/sidebar"
7
7
  import { type SidebarMenus } from "./sidebar/data"
8
- import { UserMenu, Button } from "../ui"
8
+ 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 { Menu } from "lucide-react"
@@ -16,9 +16,10 @@ interface LayoutContainerProps {
16
16
  user?: { profile?: { email?: string; preferred_username?: string } } | null
17
17
  onSignOut?: () => void
18
18
  mode?: SidebarMode
19
+ userMenuConfig?: UserMenuConfig
19
20
  }
20
21
 
21
- function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expanded' }: LayoutContainerProps) {
22
+ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expanded', userMenuConfig }: LayoutContainerProps) {
22
23
  const pathname = usePathname()
23
24
  const { setOpen } = useSidebar()
24
25
  const isMobile = useIsMobile()
@@ -96,7 +97,8 @@ function LayoutContent({ children, sidebarMenus, user, onSignOut, mode = 'expand
96
97
  )}
97
98
  <UserMenu
98
99
  username={user?.profile?.email || user?.profile?.preferred_username}
99
- onSignOut={onSignOut || (() => {})}
100
+ onSignOut={onSignOut || (() => {})}
101
+ menuConfig={userMenuConfig}
100
102
  />
101
103
  </div>
102
104
  </header>
@@ -0,0 +1,131 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+ import { X, CheckCircle, AlertCircle, AlertTriangle, Info } from "lucide-react"
6
+ import { cn } from "../../lib/utils"
7
+
8
+ const alertBannerVariants = cva(
9
+ "relative w-full flex items-center gap-3 px-4 py-3 text-sm font-medium transition-all duration-300",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ success: "bg-green-50 text-green-800 border-b border-green-200",
14
+ error: "bg-red-50 text-red-800 border-b border-red-200",
15
+ warning: "bg-amber-50 text-amber-800 border-b border-amber-200",
16
+ info: "bg-blue-50 text-blue-800 border-b border-blue-200",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "info",
21
+ },
22
+ }
23
+ )
24
+
25
+ const iconMap = {
26
+ success: CheckCircle,
27
+ error: AlertCircle,
28
+ warning: AlertTriangle,
29
+ info: Info,
30
+ }
31
+
32
+ export interface AlertBannerProps
33
+ extends React.HTMLAttributes<HTMLDivElement>,
34
+ VariantProps<typeof alertBannerVariants> {
35
+ message: string
36
+ onClose?: () => void
37
+ closable?: boolean
38
+ icon?: React.ReactNode
39
+ }
40
+
41
+ const AlertBanner = React.forwardRef<HTMLDivElement, AlertBannerProps>(
42
+ ({ className, variant = "info", message, onClose, closable = true, icon, ...props }, ref) => {
43
+ const Icon = iconMap[variant || "info"]
44
+
45
+ return (
46
+ <div
47
+ ref={ref}
48
+ role="alert"
49
+ className={cn(alertBannerVariants({ variant }), className)}
50
+ {...props}
51
+ >
52
+ {icon || <Icon className="h-5 w-5 flex-shrink-0" />}
53
+ <span className="flex-1">{message}</span>
54
+ {closable && onClose && (
55
+ <button
56
+ onClick={onClose}
57
+ className="flex-shrink-0 p-1 rounded-md hover:bg-black/5 transition-colors"
58
+ aria-label="Fermer"
59
+ >
60
+ <X className="h-4 w-4" />
61
+ </button>
62
+ )}
63
+ </div>
64
+ )
65
+ }
66
+ )
67
+ AlertBanner.displayName = "AlertBanner"
68
+
69
+ // Hook pour gérer les alertes
70
+ export interface AlertState {
71
+ show: boolean
72
+ variant: "success" | "error" | "warning" | "info"
73
+ message: string
74
+ }
75
+
76
+ export function useAlertBanner() {
77
+ const [alert, setAlert] = React.useState<AlertState>({
78
+ show: false,
79
+ variant: "info",
80
+ message: "",
81
+ })
82
+
83
+ const showAlert = React.useCallback(
84
+ (variant: AlertState["variant"], message: string, duration = 5000) => {
85
+ setAlert({ show: true, variant, message })
86
+
87
+ if (duration > 0) {
88
+ setTimeout(() => {
89
+ setAlert((prev) => ({ ...prev, show: false }))
90
+ }, duration)
91
+ }
92
+ },
93
+ []
94
+ )
95
+
96
+ const hideAlert = React.useCallback(() => {
97
+ setAlert((prev) => ({ ...prev, show: false }))
98
+ }, [])
99
+
100
+ const showSuccess = React.useCallback(
101
+ (message: string, duration?: number) => showAlert("success", message, duration),
102
+ [showAlert]
103
+ )
104
+
105
+ const showError = React.useCallback(
106
+ (message: string, duration?: number) => showAlert("error", message, duration),
107
+ [showAlert]
108
+ )
109
+
110
+ const showWarning = React.useCallback(
111
+ (message: string, duration?: number) => showAlert("warning", message, duration),
112
+ [showAlert]
113
+ )
114
+
115
+ const showInfo = React.useCallback(
116
+ (message: string, duration?: number) => showAlert("info", message, duration),
117
+ [showAlert]
118
+ )
119
+
120
+ return {
121
+ alert,
122
+ showAlert,
123
+ hideAlert,
124
+ showSuccess,
125
+ showError,
126
+ showWarning,
127
+ showInfo,
128
+ }
129
+ }
130
+
131
+ export { AlertBanner }
@@ -5,7 +5,7 @@ export { Avatar, AvatarImage, AvatarFallback } from './avatar'
5
5
  export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from './dropdown-menu'
6
6
  export { Separator } from './separator'
7
7
  export { Logo } from './logo'
8
- export { UserMenu } from './user-menu'
8
+ export { UserMenu, type UserMenuItem, type UserMenuConfig } from './user-menu'
9
9
  export { Input } from './input'
10
10
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './tooltip'
11
11
  export { PageHeader } from './page-header'
@@ -21,6 +21,7 @@ export { ImageCropDialog } from './image-crop-dialog'
21
21
  export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from './accordion'
22
22
  export { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel } from './alert-dialog'
23
23
  export { Alert, AlertTitle, AlertDescription } from './alert'
24
+ export { AlertBanner, useAlertBanner, type AlertBannerProps, type AlertState } from './alert-banner'
24
25
  export { AspectRatio } from './aspect-ratio'
25
26
  export { Badge } from './badge'
26
27
  export { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis } from './breadcrumb'
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import { useRouter } from "next/navigation"
4
- import { LogOut, Settings, Building2, User, ChevronDown } from "lucide-react"
4
+ import { LogOut, ChevronDown, type LucideIcon } from "lucide-react"
5
5
  import Avatar from "react-avatar"
6
6
  import { Button } from "./button"
7
7
  import {
@@ -12,15 +12,30 @@ import {
12
12
  DropdownMenuSeparator,
13
13
  DropdownMenuTrigger,
14
14
  } from "./dropdown-menu"
15
+
16
+ // Type for configurable user menu items
17
+ export interface UserMenuItem {
18
+ id: string
19
+ label: string
20
+ href: string
21
+ icon: LucideIcon
22
+ }
23
+
24
+ export interface UserMenuConfig {
25
+ items: UserMenuItem[]
26
+ }
27
+
15
28
  interface UserMenuProps {
16
29
  username?: string
17
30
  onSignOut: () => void
31
+ menuConfig?: UserMenuConfig
18
32
  }
19
33
 
20
- export function UserMenu({ username, onSignOut }: UserMenuProps) {
34
+ export function UserMenu({ username, onSignOut, menuConfig }: UserMenuProps) {
21
35
  const router = useRouter()
22
36
 
23
37
  const displayName = username || "User"
38
+ const menuItems = menuConfig?.items || []
24
39
 
25
40
  return (
26
41
  <DropdownMenu>
@@ -50,28 +65,24 @@ export function UserMenu({ username, onSignOut }: UserMenuProps) {
50
65
  <p className="text-sm font-medium leading-none">{displayName}</p>
51
66
  </div>
52
67
  </DropdownMenuLabel>
53
- <DropdownMenuSeparator />
54
- <DropdownMenuItem
55
- className="cursor-pointer"
56
- onClick={() => router.push('/profile')}
57
- >
58
- <User className="mr-2 h-4 w-4" />
59
- <span>Profile</span>
60
- </DropdownMenuItem>
61
- <DropdownMenuItem
62
- className="cursor-pointer"
63
- onClick={() => router.push('/organization')}
64
- >
65
- <Building2 className="mr-2 h-4 w-4" />
66
- <span>Organization</span>
67
- </DropdownMenuItem>
68
- <DropdownMenuItem
69
- className="cursor-pointer"
70
- onClick={() => router.push('/settings')}
71
- >
72
- <Settings className="mr-2 h-4 w-4" />
73
- <span>Settings</span>
74
- </DropdownMenuItem>
68
+ {menuItems.length > 0 && (
69
+ <>
70
+ <DropdownMenuSeparator />
71
+ {menuItems.map((item) => {
72
+ const Icon = item.icon
73
+ return (
74
+ <DropdownMenuItem
75
+ key={item.id}
76
+ className="cursor-pointer"
77
+ onClick={() => router.push(item.href)}
78
+ >
79
+ <Icon className="mr-2 h-4 w-4" />
80
+ <span>{item.label}</span>
81
+ </DropdownMenuItem>
82
+ )
83
+ })}
84
+ </>
85
+ )}
75
86
  <DropdownMenuSeparator />
76
87
  <DropdownMenuItem
77
88
  className="cursor-pointer text-red-600 focus:text-red-600 focus:bg-red-50"
@@ -89,6 +89,14 @@ class HttpClient {
89
89
  });
90
90
  }
91
91
 
92
+ async patch<T>(url: string, body?: any, options: RequestInit = {}): Promise<T | null> {
93
+ return this.request<T>(url, {
94
+ ...options,
95
+ method: 'PATCH',
96
+ body: body ? JSON.stringify(body) : undefined
97
+ });
98
+ }
99
+
92
100
  async delete<T>(url: string, options: RequestInit = {}): Promise<T | null> {
93
101
  return this.request<T>(url, {
94
102
  ...options,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.0.20",
3
+ "version": "1.0.23",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",