@orsetra/shared-ui 1.0.37 → 1.0.39
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/ui/alert-banner.tsx +4 -4
- package/components/ui/side-panel.tsx +90 -30
- package/index.ts +2 -1
- package/lib/error-utils.ts +127 -0
- package/package.json +1 -1
|
@@ -10,10 +10,10 @@ const alertBannerVariants = cva(
|
|
|
10
10
|
{
|
|
11
11
|
variants: {
|
|
12
12
|
variant: {
|
|
13
|
-
success: "bg-green-50 text-green-800
|
|
14
|
-
error: "bg-red-50 text-red-800
|
|
15
|
-
warning: "bg-amber-50 text-amber-800
|
|
16
|
-
info: "bg-blue-50 text-blue-800
|
|
13
|
+
success: "bg-green-50 text-green-800",
|
|
14
|
+
error: "bg-red-50 text-red-800",
|
|
15
|
+
warning: "bg-amber-50 text-amber-800",
|
|
16
|
+
info: "bg-blue-50 text-blue-800",
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
19
|
defaultVariants: {
|
|
@@ -5,7 +5,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
|
5
5
|
import { X } from "lucide-react"
|
|
6
6
|
import { cn } from "../../lib/utils"
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const SidePanelRoot = DialogPrimitive.Root
|
|
9
9
|
|
|
10
10
|
const SidePanelTrigger = DialogPrimitive.Trigger
|
|
11
11
|
|
|
@@ -28,6 +28,80 @@ const SidePanelOverlay = React.forwardRef<
|
|
|
28
28
|
))
|
|
29
29
|
SidePanelOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
30
30
|
|
|
31
|
+
interface SidePanelProps {
|
|
32
|
+
open: boolean
|
|
33
|
+
onOpenChange: (open: boolean) => void
|
|
34
|
+
title: string
|
|
35
|
+
description?: string
|
|
36
|
+
children: React.ReactNode
|
|
37
|
+
actions: React.ReactNode
|
|
38
|
+
side?: "left" | "right"
|
|
39
|
+
width?: string
|
|
40
|
+
onClose?: () => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SidePanel = ({
|
|
44
|
+
open,
|
|
45
|
+
onOpenChange,
|
|
46
|
+
title,
|
|
47
|
+
description,
|
|
48
|
+
children,
|
|
49
|
+
actions,
|
|
50
|
+
side = "right",
|
|
51
|
+
width = "max-w-lg",
|
|
52
|
+
onClose,
|
|
53
|
+
}: SidePanelProps) => {
|
|
54
|
+
return (
|
|
55
|
+
<SidePanelRoot open={open} onOpenChange={onOpenChange}>
|
|
56
|
+
<SidePanelPortal>
|
|
57
|
+
<SidePanelOverlay />
|
|
58
|
+
<DialogPrimitive.Content
|
|
59
|
+
className={cn(
|
|
60
|
+
"fixed z-50 bg-white shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 flex flex-col",
|
|
61
|
+
side === "right" &&
|
|
62
|
+
"inset-y-0 right-0 h-full w-full border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right",
|
|
63
|
+
side === "left" &&
|
|
64
|
+
"inset-y-0 left-0 h-full w-full border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left",
|
|
65
|
+
width
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{/* Header fixe */}
|
|
69
|
+
<div className="flex items-center justify-between border-b border-ibm-gray-20 px-6 py-4 flex-shrink-0">
|
|
70
|
+
<div className="flex-1">
|
|
71
|
+
<DialogPrimitive.Title className="text-lg font-semibold text-ibm-gray-100">
|
|
72
|
+
{title}
|
|
73
|
+
</DialogPrimitive.Title>
|
|
74
|
+
{description && (
|
|
75
|
+
<DialogPrimitive.Description className="text-sm text-ibm-gray-60">
|
|
76
|
+
{description}
|
|
77
|
+
</DialogPrimitive.Description>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
<DialogPrimitive.Close
|
|
81
|
+
className="rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ibm-blue-60 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-ibm-gray-10"
|
|
82
|
+
onClick={onClose}
|
|
83
|
+
>
|
|
84
|
+
<X className="h-5 w-5 text-ibm-gray-70" />
|
|
85
|
+
<span className="sr-only">Close</span>
|
|
86
|
+
</DialogPrimitive.Close>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Body scrollable */}
|
|
90
|
+
<div className="flex-1 overflow-y-auto px-6 py-6">
|
|
91
|
+
{children}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Footer fixe */}
|
|
95
|
+
<div className="flex items-center justify-end gap-3 border-t border-ibm-gray-20 px-6 py-4 flex-shrink-0">
|
|
96
|
+
{actions}
|
|
97
|
+
</div>
|
|
98
|
+
</DialogPrimitive.Content>
|
|
99
|
+
</SidePanelPortal>
|
|
100
|
+
</SidePanelRoot>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Composants legacy pour compatibilité ascendante
|
|
31
105
|
interface SidePanelContentProps
|
|
32
106
|
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
|
|
33
107
|
side?: "left" | "right"
|
|
@@ -59,34 +133,19 @@ const SidePanelContent = React.forwardRef<
|
|
|
59
133
|
))
|
|
60
134
|
SidePanelContent.displayName = DialogPrimitive.Content.displayName
|
|
61
135
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{...props}
|
|
76
|
-
>
|
|
77
|
-
<div className="flex-1">{children}</div>
|
|
78
|
-
{showCloseButton && (
|
|
79
|
-
<DialogPrimitive.Close
|
|
80
|
-
className="rounded-sm opacity-70 ring-offset-white transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ibm-blue-60 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-ibm-gray-10"
|
|
81
|
-
onClick={onClose}
|
|
82
|
-
>
|
|
83
|
-
<X className="h-5 w-5 text-ibm-gray-70" />
|
|
84
|
-
<span className="sr-only">Close</span>
|
|
85
|
-
</DialogPrimitive.Close>
|
|
86
|
-
)}
|
|
87
|
-
</div>
|
|
88
|
-
)
|
|
89
|
-
)
|
|
136
|
+
const SidePanelHeader = React.forwardRef<
|
|
137
|
+
HTMLDivElement,
|
|
138
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
139
|
+
>(({ className, ...props }, ref) => (
|
|
140
|
+
<div
|
|
141
|
+
ref={ref}
|
|
142
|
+
className={cn(
|
|
143
|
+
"flex items-center justify-between border-b border-ibm-gray-20 px-6 py-4 flex-shrink-0",
|
|
144
|
+
className
|
|
145
|
+
)}
|
|
146
|
+
{...props}
|
|
147
|
+
/>
|
|
148
|
+
))
|
|
90
149
|
SidePanelHeader.displayName = "SidePanelHeader"
|
|
91
150
|
|
|
92
151
|
const SidePanelTitle = React.forwardRef<
|
|
@@ -132,7 +191,7 @@ const SidePanelFooter = React.forwardRef<
|
|
|
132
191
|
<div
|
|
133
192
|
ref={ref}
|
|
134
193
|
className={cn(
|
|
135
|
-
"flex items-center justify-end gap-3 border-t border-ibm-gray-20 px-6 py-4",
|
|
194
|
+
"flex items-center justify-end gap-3 border-t border-ibm-gray-20 px-6 py-4 flex-shrink-0",
|
|
136
195
|
className
|
|
137
196
|
)}
|
|
138
197
|
{...props}
|
|
@@ -150,4 +209,5 @@ export {
|
|
|
150
209
|
SidePanelDescription,
|
|
151
210
|
SidePanelBody,
|
|
152
211
|
SidePanelFooter,
|
|
212
|
+
SidePanelRoot,
|
|
153
213
|
}
|
package/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Utilities
|
|
2
2
|
export * from './lib/utils'
|
|
3
3
|
export * from './lib/menu-utils'
|
|
4
|
+
export * from './lib/error-utils'
|
|
4
5
|
export { BaseService } from './lib/base-service'
|
|
5
|
-
export { default as HttpClient, useHttpClient } from './lib/http-client'
|
|
6
|
+
export { default as HttpClient, useHttpClient, ApiError } from './lib/http-client'
|
|
6
7
|
|
|
7
8
|
// UI Components
|
|
8
9
|
export * from './components/ui'
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for parsing API errors
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ApiError } from './http-client'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Standard API error response format
|
|
9
|
+
*/
|
|
10
|
+
export interface ApiErrorResponse {
|
|
11
|
+
BusinessCode?: number
|
|
12
|
+
Message?: string
|
|
13
|
+
message?: string
|
|
14
|
+
error?: string
|
|
15
|
+
code?: number
|
|
16
|
+
details?: unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extracts a user-friendly error message from various error formats
|
|
21
|
+
*
|
|
22
|
+
* Supports:
|
|
23
|
+
* - ApiError from http-client
|
|
24
|
+
* - Standard Error objects
|
|
25
|
+
* - API responses with BusinessCode/Message format
|
|
26
|
+
* - API responses with message/error format
|
|
27
|
+
*
|
|
28
|
+
* @param error - The error object to parse
|
|
29
|
+
* @param fallbackMessage - Default message if no error message can be extracted
|
|
30
|
+
* @returns A user-friendly error message
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* try {
|
|
35
|
+
* await apiCall()
|
|
36
|
+
* } catch (error) {
|
|
37
|
+
* const message = parseApiError(error)
|
|
38
|
+
* showError(message)
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function parseApiError(error: unknown, fallbackMessage = "An unexpected error occurred"): string {
|
|
43
|
+
// Handle ApiError from http-client
|
|
44
|
+
if (error instanceof ApiError) {
|
|
45
|
+
// Check raw response for BusinessCode/Message format
|
|
46
|
+
if (error.raw && typeof error.raw === 'object') {
|
|
47
|
+
const rawError = error.raw as ApiErrorResponse
|
|
48
|
+
if (rawError.Message) {
|
|
49
|
+
return rawError.Message
|
|
50
|
+
}
|
|
51
|
+
if (rawError.message) {
|
|
52
|
+
return rawError.message
|
|
53
|
+
}
|
|
54
|
+
if (rawError.error) {
|
|
55
|
+
return rawError.error
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Use ApiError message
|
|
59
|
+
return error.message
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle standard Error objects
|
|
63
|
+
if (error instanceof Error) {
|
|
64
|
+
return error.message
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Handle plain objects with error properties
|
|
68
|
+
if (error && typeof error === 'object') {
|
|
69
|
+
const errorObj = error as ApiErrorResponse
|
|
70
|
+
|
|
71
|
+
// Check for BusinessCode/Message format (custom API format)
|
|
72
|
+
if (errorObj.Message) {
|
|
73
|
+
return errorObj.Message
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for standard message/error properties
|
|
77
|
+
if (errorObj.message) {
|
|
78
|
+
return errorObj.message
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (errorObj.error) {
|
|
82
|
+
return errorObj.error
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle string errors
|
|
87
|
+
if (typeof error === 'string') {
|
|
88
|
+
return error
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fallback
|
|
92
|
+
return fallbackMessage
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Checks if an error is an ApiError with a specific status code
|
|
97
|
+
*
|
|
98
|
+
* @param error - The error to check
|
|
99
|
+
* @param status - The status code to match
|
|
100
|
+
* @returns true if the error is an ApiError with the specified status
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* if (isApiErrorWithStatus(error, 404)) {
|
|
105
|
+
* showError("Resource not found")
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function isApiErrorWithStatus(error: unknown, status: number): boolean {
|
|
110
|
+
return error instanceof ApiError && error.status === status
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Checks if an error is a network error (no response from server)
|
|
115
|
+
*
|
|
116
|
+
* @param error - The error to check
|
|
117
|
+
* @returns true if the error is a network error
|
|
118
|
+
*/
|
|
119
|
+
export function isNetworkError(error: unknown): boolean {
|
|
120
|
+
if (error instanceof Error) {
|
|
121
|
+
const message = error.message.toLowerCase()
|
|
122
|
+
return message.includes('network') ||
|
|
123
|
+
message.includes('fetch') ||
|
|
124
|
+
message.includes('connection')
|
|
125
|
+
}
|
|
126
|
+
return false
|
|
127
|
+
}
|