@orsetra/shared-ui 1.0.38 → 1.0.40

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.
@@ -10,10 +10,10 @@ const alertBannerVariants = cva(
10
10
  {
11
11
  variants: {
12
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",
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: {
@@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
18
18
  >(({ className, ...props }, ref) => (
19
19
  <AlertDialogPrimitive.Overlay
20
20
  className={cn(
21
- "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
21
+ "fixed inset-0 z-50 bg-ibm-gray-100/50 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
22
  className
23
23
  )}
24
24
  {...props}
@@ -32,11 +32,10 @@ const AlertDialogContent = React.forwardRef<
32
32
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
33
33
  >(({ className, ...props }, ref) => (
34
34
  <AlertDialogPortal>
35
- <AlertDialogOverlay />
36
35
  <AlertDialogPrimitive.Content
37
36
  ref={ref}
38
37
  className={cn(
39
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
38
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-ibm-gray-20 bg-white p-6 shadow-xl duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
40
39
  className
41
40
  )}
42
41
  {...props}
@@ -79,7 +78,7 @@ const AlertDialogTitle = React.forwardRef<
79
78
  >(({ className, ...props }, ref) => (
80
79
  <AlertDialogPrimitive.Title
81
80
  ref={ref}
82
- className={cn("text-lg font-semibold", className)}
81
+ className={cn("text-lg font-semibold text-ibm-gray-100", className)}
83
82
  {...props}
84
83
  />
85
84
  ))
@@ -91,7 +90,7 @@ const AlertDialogDescription = React.forwardRef<
91
90
  >(({ className, ...props }, ref) => (
92
91
  <AlertDialogPrimitive.Description
93
92
  ref={ref}
94
- className={cn("text-sm text-muted-foreground", className)}
93
+ className={cn("text-sm text-ibm-gray-70", className)}
95
94
  {...props}
96
95
  />
97
96
  ))
@@ -0,0 +1,147 @@
1
+ "use client"
2
+
3
+ import { useState, useEffect } from "react"
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ AlertDialogTrigger,
14
+ } from "./alert-dialog"
15
+ import { AlertBanner, useAlertBanner } from "./alert-banner"
16
+ import { Input } from "./input"
17
+ import { Label } from "./label"
18
+ import type { ReactNode } from "react"
19
+
20
+ export interface ConfirmationDialogProps {
21
+ trigger: ReactNode
22
+ title: string
23
+ description: string | ReactNode
24
+ confirmText?: string
25
+ cancelText?: string
26
+ confirmVariant?: "default" | "destructive"
27
+ onConfirm: () => void | Promise<void>
28
+ onCancel?: () => void
29
+ requireTextConfirmation?: {
30
+ expectedText: string
31
+ label?: string
32
+ placeholder?: string
33
+ }
34
+ open?: boolean
35
+ onOpenChange?: (open: boolean) => void
36
+ }
37
+
38
+ export function ConfirmationDialog({
39
+ trigger,
40
+ title,
41
+ description,
42
+ confirmText = "Confirm",
43
+ cancelText = "Cancel",
44
+ confirmVariant = "default",
45
+ onConfirm,
46
+ onCancel,
47
+ requireTextConfirmation,
48
+ open: controlledOpen,
49
+ onOpenChange: controlledOnOpenChange,
50
+ }: ConfirmationDialogProps) {
51
+ const { alert, showSuccess, showError, hideAlert } = useAlertBanner()
52
+ const [loading, setLoading] = useState(false)
53
+ const [internalOpen, setInternalOpen] = useState(false)
54
+ const [confirmationText, setConfirmationText] = useState("")
55
+
56
+ const isControlled = controlledOpen !== undefined
57
+ const open = isControlled ? controlledOpen : internalOpen
58
+ const setOpen = isControlled ? controlledOnOpenChange! : setInternalOpen
59
+
60
+ const isConfirmDisabled =
61
+ loading ||
62
+ (requireTextConfirmation &&
63
+ confirmationText !== requireTextConfirmation.expectedText)
64
+
65
+ useEffect(() => {
66
+ if (!open) {
67
+ setConfirmationText("")
68
+ hideAlert()
69
+ }
70
+ }, [open])
71
+
72
+ const handleConfirm = async () => {
73
+ setLoading(true)
74
+ hideAlert()
75
+ try {
76
+ await onConfirm()
77
+ setOpen(false)
78
+ } catch (error: any) {
79
+ showError(error?.message || "Operation failed")
80
+ } finally {
81
+ setLoading(false)
82
+ }
83
+ }
84
+
85
+ const handleCancel = () => {
86
+ onCancel?.()
87
+ setOpen(false)
88
+ }
89
+
90
+ const confirmButtonClass =
91
+ confirmVariant === "destructive"
92
+ ? "bg-red-600 hover:bg-red-700"
93
+ : undefined
94
+
95
+ return (
96
+ <AlertDialog open={open} onOpenChange={setOpen}>
97
+ <AlertDialogTrigger asChild>{trigger}</AlertDialogTrigger>
98
+ <AlertDialogContent>
99
+ <AlertDialogHeader>
100
+ <AlertDialogTitle>{title}</AlertDialogTitle>
101
+ <AlertDialogDescription>{description}</AlertDialogDescription>
102
+ </AlertDialogHeader>
103
+
104
+ {alert.show && (
105
+ <AlertBanner
106
+ variant={alert.variant}
107
+ message={alert.message}
108
+ onClose={hideAlert}
109
+ />
110
+ )}
111
+
112
+ {requireTextConfirmation && (
113
+ <div className="space-y-2">
114
+ <Label htmlFor="confirmation-input">
115
+ {requireTextConfirmation.label ||
116
+ `Type "${requireTextConfirmation.expectedText}" to confirm`}
117
+ </Label>
118
+ <Input
119
+ id="confirmation-input"
120
+ value={confirmationText}
121
+ onChange={(e) => setConfirmationText(e.target.value)}
122
+ placeholder={
123
+ requireTextConfirmation.placeholder ||
124
+ requireTextConfirmation.expectedText
125
+ }
126
+ disabled={loading}
127
+ autoComplete="off"
128
+ />
129
+ </div>
130
+ )}
131
+
132
+ <AlertDialogFooter className="mt-6">
133
+ <AlertDialogCancel disabled={loading} onClick={handleCancel}>
134
+ {cancelText}
135
+ </AlertDialogCancel>
136
+ <AlertDialogAction
137
+ className={confirmButtonClass}
138
+ onClick={handleConfirm}
139
+ disabled={isConfirmDisabled}
140
+ >
141
+ {confirmText}
142
+ </AlertDialogAction>
143
+ </AlertDialogFooter>
144
+ </AlertDialogContent>
145
+ </AlertDialog>
146
+ )
147
+ }
@@ -23,6 +23,7 @@ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from './
23
23
  export { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel } from './alert-dialog'
24
24
  export { Alert, AlertTitle, AlertDescription } from './alert'
25
25
  export { AlertBanner, useAlertBanner, type AlertBannerProps, type AlertState } from './alert-banner'
26
+ export { ConfirmationDialog, type ConfirmationDialogProps } from './confirmation-dialog'
26
27
  export { AspectRatio } from './aspect-ratio'
27
28
  export { Badge } from './badge'
28
29
  export { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbPage, BreadcrumbSeparator, BreadcrumbEllipsis } from './breadcrumb'
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",