@trycompai/design-system 1.0.18 → 1.0.19
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/package.json +1 -1
- package/src/components/atoms/text.tsx +2 -0
- package/src/components/molecules/index.ts +1 -0
- package/src/components/molecules/select.tsx +11 -0
- package/src/components/molecules/split-button.tsx +190 -0
- package/src/components/organisms/alert-dialog.tsx +2 -2
- package/src/components/organisms/approval-banner.tsx +369 -0
- package/src/components/organisms/index.ts +1 -0
package/package.json
CHANGED
|
@@ -136,6 +136,16 @@ function SelectSeparator({ ...props }: Omit<SelectPrimitive.Separator.Props, 'cl
|
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
function SelectEmpty({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
140
|
+
return (
|
|
141
|
+
<div
|
|
142
|
+
data-slot="select-empty"
|
|
143
|
+
className="text-muted-foreground py-6 text-center text-sm"
|
|
144
|
+
{...props}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
139
149
|
function SelectScrollUpButton({
|
|
140
150
|
...props
|
|
141
151
|
}: Omit<React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>, 'className'>) {
|
|
@@ -167,6 +177,7 @@ function SelectScrollDownButton({
|
|
|
167
177
|
export {
|
|
168
178
|
Select,
|
|
169
179
|
SelectContent,
|
|
180
|
+
SelectEmpty,
|
|
170
181
|
SelectGroup,
|
|
171
182
|
SelectItem,
|
|
172
183
|
SelectLabel,
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Menu as MenuPrimitive } from '@base-ui/react/menu';
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
4
|
+
import { ChevronDown } from '@carbon/icons-react';
|
|
5
|
+
|
|
6
|
+
import { Spinner } from '../atoms/spinner';
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
} from '../organisms/dropdown-menu';
|
|
12
|
+
|
|
13
|
+
const splitButtonVariants = cva(
|
|
14
|
+
'inline-flex items-stretch rounded-md',
|
|
15
|
+
{
|
|
16
|
+
variants: {
|
|
17
|
+
variant: {
|
|
18
|
+
default: 'bg-primary text-primary-foreground [&_[data-slot=split-button-divider]]:bg-primary-foreground/20',
|
|
19
|
+
outline: 'border border-border bg-background [&_[data-slot=split-button-divider]]:bg-border',
|
|
20
|
+
secondary: 'bg-secondary text-secondary-foreground [&_[data-slot=split-button-divider]]:bg-secondary-foreground/20',
|
|
21
|
+
ghost: '[&_[data-slot=split-button-divider]]:bg-border',
|
|
22
|
+
destructive: 'bg-destructive/10 text-destructive [&_[data-slot=split-button-divider]]:bg-destructive/20',
|
|
23
|
+
},
|
|
24
|
+
size: {
|
|
25
|
+
xs: 'h-5 text-[11px]',
|
|
26
|
+
sm: 'h-6 text-xs',
|
|
27
|
+
default: 'h-7 text-[13px]',
|
|
28
|
+
lg: 'h-8 text-[13px]',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
variant: 'default',
|
|
33
|
+
size: 'default',
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const splitButtonMainVariants = cva(
|
|
39
|
+
'inline-flex items-center justify-center gap-1 font-medium leading-none rounded-l-md transition-all duration-200 ease-out outline-none select-none cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
|
40
|
+
{
|
|
41
|
+
variants: {
|
|
42
|
+
variant: {
|
|
43
|
+
default: 'hover:bg-primary/90 active:bg-primary/80',
|
|
44
|
+
outline: 'hover:bg-muted active:bg-muted/80',
|
|
45
|
+
secondary: 'hover:bg-secondary/80 active:bg-secondary/70',
|
|
46
|
+
ghost: 'hover:bg-accent active:bg-accent/80',
|
|
47
|
+
destructive: 'hover:bg-destructive/15 active:bg-destructive/20',
|
|
48
|
+
},
|
|
49
|
+
size: {
|
|
50
|
+
xs: "px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
51
|
+
sm: "px-2 [&_svg:not([class*='size-'])]:size-3.5",
|
|
52
|
+
default: "px-2 [&_svg:not([class*='size-'])]:size-4",
|
|
53
|
+
lg: "px-2.5 [&_svg:not([class*='size-'])]:size-4",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
defaultVariants: {
|
|
57
|
+
variant: 'default',
|
|
58
|
+
size: 'default',
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const splitButtonTriggerVariants = cva(
|
|
64
|
+
'inline-flex items-center justify-center rounded-r-md transition-all duration-200 ease-out outline-none select-none cursor-pointer disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
|
65
|
+
{
|
|
66
|
+
variants: {
|
|
67
|
+
variant: {
|
|
68
|
+
default: 'hover:bg-primary/90 active:bg-primary/80',
|
|
69
|
+
outline: 'hover:bg-muted active:bg-muted/80',
|
|
70
|
+
secondary: 'hover:bg-secondary/80 active:bg-secondary/70',
|
|
71
|
+
ghost: 'hover:bg-accent active:bg-accent/80',
|
|
72
|
+
destructive: 'hover:bg-destructive/15 active:bg-destructive/20',
|
|
73
|
+
},
|
|
74
|
+
size: {
|
|
75
|
+
xs: "w-5 [&_svg:not([class*='size-'])]:size-3",
|
|
76
|
+
sm: "w-6 [&_svg:not([class*='size-'])]:size-3.5",
|
|
77
|
+
default: "w-7 [&_svg:not([class*='size-'])]:size-4",
|
|
78
|
+
lg: "w-8 [&_svg:not([class*='size-'])]:size-4",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
defaultVariants: {
|
|
82
|
+
variant: 'default',
|
|
83
|
+
size: 'default',
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
type SplitButtonAction = {
|
|
89
|
+
/** Unique identifier for the action */
|
|
90
|
+
id: string;
|
|
91
|
+
/** Label to display in the dropdown */
|
|
92
|
+
label: React.ReactNode;
|
|
93
|
+
/** Callback when action is clicked */
|
|
94
|
+
onClick?: () => void;
|
|
95
|
+
/** Whether the action is destructive */
|
|
96
|
+
variant?: 'default' | 'destructive';
|
|
97
|
+
/** Icon to show before the label */
|
|
98
|
+
icon?: React.ReactNode;
|
|
99
|
+
/** Whether to show a separator after this item */
|
|
100
|
+
separator?: boolean;
|
|
101
|
+
/** Whether this action is disabled */
|
|
102
|
+
disabled?: boolean;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
type SplitButtonProps = VariantProps<typeof splitButtonVariants> & {
|
|
106
|
+
/** Content of the main button */
|
|
107
|
+
children: React.ReactNode;
|
|
108
|
+
/** Additional actions shown in the dropdown */
|
|
109
|
+
actions: SplitButtonAction[];
|
|
110
|
+
/** Callback when main button is clicked */
|
|
111
|
+
onClick?: () => void;
|
|
112
|
+
/** Whether the button is disabled */
|
|
113
|
+
disabled?: boolean;
|
|
114
|
+
/** Show loading spinner and disable button */
|
|
115
|
+
loading?: boolean;
|
|
116
|
+
/** Icon to show on the left side of the button */
|
|
117
|
+
iconLeft?: React.ReactNode;
|
|
118
|
+
/** Dropdown menu alignment */
|
|
119
|
+
menuAlign?: 'start' | 'center' | 'end';
|
|
120
|
+
/** Side of the trigger to show the dropdown */
|
|
121
|
+
menuSide?: 'top' | 'bottom';
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function SplitButton({
|
|
125
|
+
children,
|
|
126
|
+
actions,
|
|
127
|
+
onClick,
|
|
128
|
+
variant = 'default',
|
|
129
|
+
size = 'default',
|
|
130
|
+
menuAlign = 'end',
|
|
131
|
+
menuSide = 'bottom',
|
|
132
|
+
disabled,
|
|
133
|
+
loading,
|
|
134
|
+
iconLeft,
|
|
135
|
+
}: SplitButtonProps) {
|
|
136
|
+
const isDisabled = disabled || loading;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
data-slot="split-button"
|
|
141
|
+
className={splitButtonVariants({ variant, size })}
|
|
142
|
+
>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
data-slot="split-button-main"
|
|
146
|
+
onClick={onClick}
|
|
147
|
+
disabled={isDisabled}
|
|
148
|
+
className={splitButtonMainVariants({ variant, size })}
|
|
149
|
+
>
|
|
150
|
+
{loading ? (
|
|
151
|
+
<Spinner />
|
|
152
|
+
) : iconLeft ? (
|
|
153
|
+
<span data-icon="inline-start">{iconLeft}</span>
|
|
154
|
+
) : null}
|
|
155
|
+
{children}
|
|
156
|
+
</button>
|
|
157
|
+
<span
|
|
158
|
+
data-slot="split-button-divider"
|
|
159
|
+
className="w-px self-stretch"
|
|
160
|
+
aria-hidden="true"
|
|
161
|
+
/>
|
|
162
|
+
<MenuPrimitive.Root>
|
|
163
|
+
<MenuPrimitive.Trigger
|
|
164
|
+
disabled={isDisabled}
|
|
165
|
+
className={splitButtonTriggerVariants({ variant, size })}
|
|
166
|
+
>
|
|
167
|
+
<ChevronDown />
|
|
168
|
+
</MenuPrimitive.Trigger>
|
|
169
|
+
<DropdownMenuContent align={menuAlign} side={menuSide}>
|
|
170
|
+
{actions.map((action) => (
|
|
171
|
+
<React.Fragment key={action.id}>
|
|
172
|
+
<DropdownMenuItem
|
|
173
|
+
onClick={action.onClick}
|
|
174
|
+
variant={action.variant}
|
|
175
|
+
disabled={action.disabled}
|
|
176
|
+
>
|
|
177
|
+
{action.icon}
|
|
178
|
+
{action.label}
|
|
179
|
+
</DropdownMenuItem>
|
|
180
|
+
{action.separator && <DropdownMenuSeparator />}
|
|
181
|
+
</React.Fragment>
|
|
182
|
+
))}
|
|
183
|
+
</DropdownMenuContent>
|
|
184
|
+
</MenuPrimitive.Root>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export { SplitButton };
|
|
190
|
+
export type { SplitButtonProps, SplitButtonAction };
|
|
@@ -79,7 +79,7 @@ function AlertDialogTitle({
|
|
|
79
79
|
return (
|
|
80
80
|
<AlertDialogPrimitive.Title
|
|
81
81
|
data-slot="alert-dialog-title"
|
|
82
|
-
className="text-sm font-
|
|
82
|
+
className="text-sm font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2"
|
|
83
83
|
{...props}
|
|
84
84
|
/>
|
|
85
85
|
);
|
|
@@ -91,7 +91,7 @@ function AlertDialogDescription({
|
|
|
91
91
|
return (
|
|
92
92
|
<AlertDialogPrimitive.Description
|
|
93
93
|
data-slot="alert-dialog-description"
|
|
94
|
-
className="text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3"
|
|
94
|
+
className="text-muted-foreground font-normal *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3"
|
|
95
95
|
{...props}
|
|
96
96
|
/>
|
|
97
97
|
);
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import {
|
|
4
|
+
Time,
|
|
5
|
+
Information,
|
|
6
|
+
CheckmarkOutline,
|
|
7
|
+
Checkmark,
|
|
8
|
+
Close,
|
|
9
|
+
} from '@carbon/icons-react';
|
|
10
|
+
|
|
11
|
+
import { Button } from '../atoms/button';
|
|
12
|
+
import { Stack, HStack } from '../atoms/stack';
|
|
13
|
+
import { Text } from '../atoms/text';
|
|
14
|
+
import { SplitButton, type SplitButtonAction } from '../molecules/split-button';
|
|
15
|
+
import {
|
|
16
|
+
AlertDialog,
|
|
17
|
+
AlertDialogAction,
|
|
18
|
+
AlertDialogCancel,
|
|
19
|
+
AlertDialogContent,
|
|
20
|
+
AlertDialogDescription,
|
|
21
|
+
AlertDialogFooter,
|
|
22
|
+
AlertDialogHeader,
|
|
23
|
+
AlertDialogTitle,
|
|
24
|
+
} from './alert-dialog';
|
|
25
|
+
|
|
26
|
+
const approvalBannerVariants = cva(
|
|
27
|
+
'rounded-lg border border-l-4 bg-background p-4',
|
|
28
|
+
{
|
|
29
|
+
variants: {
|
|
30
|
+
variant: {
|
|
31
|
+
warning: 'border-l-warning border-border',
|
|
32
|
+
info: 'border-l-info border-border',
|
|
33
|
+
default: 'border-l-primary border-border',
|
|
34
|
+
},
|
|
35
|
+
layout: {
|
|
36
|
+
stacked: '',
|
|
37
|
+
inline:
|
|
38
|
+
'[&>[data-slot=stack]]:flex-row [&>[data-slot=stack]]:items-center [&>[data-slot=stack]]:justify-between [&_[data-slot=approval-banner-content]]:min-w-0 [&_[data-slot=approval-banner-content]]:flex-1 [&_[data-slot=approval-banner-actions]]:shrink-0 [&_[data-slot=approval-banner-description]]:truncate',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
defaultVariants: {
|
|
42
|
+
variant: 'warning',
|
|
43
|
+
layout: 'stacked',
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const textVariantMap = {
|
|
49
|
+
warning: 'warning',
|
|
50
|
+
info: 'info',
|
|
51
|
+
default: 'primary',
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
const iconMap = {
|
|
55
|
+
warning: Time,
|
|
56
|
+
info: Information,
|
|
57
|
+
default: CheckmarkOutline,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type ConfirmationConfig = {
|
|
61
|
+
/** Title of the confirmation dialog */
|
|
62
|
+
title: string;
|
|
63
|
+
/** Description of the confirmation dialog */
|
|
64
|
+
description?: string;
|
|
65
|
+
/** Custom content to render in the dialog body (below description) */
|
|
66
|
+
content?: React.ReactNode;
|
|
67
|
+
/** Confirm button text */
|
|
68
|
+
confirmText?: string;
|
|
69
|
+
/** Cancel button text */
|
|
70
|
+
cancelText?: string;
|
|
71
|
+
/** Called when the dialog is cancelled/dismissed */
|
|
72
|
+
onCancel?: () => void;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type ApprovalBannerProps = VariantProps<typeof approvalBannerVariants> & {
|
|
76
|
+
/** Title of the approval banner */
|
|
77
|
+
title: string;
|
|
78
|
+
/** Description text */
|
|
79
|
+
description: React.ReactNode;
|
|
80
|
+
/** Callback when approve is confirmed */
|
|
81
|
+
onApprove?: () => void | Promise<void>;
|
|
82
|
+
/** Callback when reject is confirmed */
|
|
83
|
+
onReject?: () => void | Promise<void>;
|
|
84
|
+
/** Custom approve button text */
|
|
85
|
+
approveText?: string;
|
|
86
|
+
/** Custom reject button text */
|
|
87
|
+
rejectText?: string;
|
|
88
|
+
/** Whether approve action is loading */
|
|
89
|
+
approveLoading?: boolean;
|
|
90
|
+
/** Whether reject action is loading */
|
|
91
|
+
rejectLoading?: boolean;
|
|
92
|
+
/** Custom icon to display */
|
|
93
|
+
icon?: React.ReactNode;
|
|
94
|
+
/** Hide the reject button */
|
|
95
|
+
hideReject?: boolean;
|
|
96
|
+
/** Additional actions to show in dropdown (inline layout only) */
|
|
97
|
+
additionalActions?: SplitButtonAction[];
|
|
98
|
+
/** Confirmation dialog config for approve action. If provided, shows dialog before calling onApprove */
|
|
99
|
+
approveConfirmation?: ConfirmationConfig;
|
|
100
|
+
/** Confirmation dialog config for reject action. If provided, shows dialog before calling onReject */
|
|
101
|
+
rejectConfirmation?: ConfirmationConfig;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function ApprovalBanner({
|
|
105
|
+
variant = 'warning',
|
|
106
|
+
layout = 'stacked',
|
|
107
|
+
title,
|
|
108
|
+
description,
|
|
109
|
+
onApprove,
|
|
110
|
+
onReject,
|
|
111
|
+
approveText = 'Approve',
|
|
112
|
+
rejectText = 'Reject',
|
|
113
|
+
approveLoading = false,
|
|
114
|
+
rejectLoading = false,
|
|
115
|
+
icon,
|
|
116
|
+
hideReject = false,
|
|
117
|
+
additionalActions = [],
|
|
118
|
+
approveConfirmation,
|
|
119
|
+
rejectConfirmation,
|
|
120
|
+
}: ApprovalBannerProps) {
|
|
121
|
+
const [approveDialogOpen, setApproveDialogOpen] = React.useState(false);
|
|
122
|
+
const [rejectDialogOpen, setRejectDialogOpen] = React.useState(false);
|
|
123
|
+
const [isApproving, setIsApproving] = React.useState(false);
|
|
124
|
+
const [isRejecting, setIsRejecting] = React.useState(false);
|
|
125
|
+
|
|
126
|
+
const IconComponent = iconMap[variant ?? 'warning'];
|
|
127
|
+
const textVariant = textVariantMap[variant ?? 'warning'];
|
|
128
|
+
const isLoading = approveLoading || rejectLoading || isApproving || isRejecting;
|
|
129
|
+
|
|
130
|
+
const handleApproveClick = () => {
|
|
131
|
+
if (approveConfirmation) {
|
|
132
|
+
setApproveDialogOpen(true);
|
|
133
|
+
} else {
|
|
134
|
+
onApprove?.();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handleRejectClick = () => {
|
|
139
|
+
if (rejectConfirmation) {
|
|
140
|
+
setRejectDialogOpen(true);
|
|
141
|
+
} else {
|
|
142
|
+
onReject?.();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleApproveConfirm = async () => {
|
|
147
|
+
setIsApproving(true);
|
|
148
|
+
try {
|
|
149
|
+
await onApprove?.();
|
|
150
|
+
} finally {
|
|
151
|
+
setIsApproving(false);
|
|
152
|
+
setApproveDialogOpen(false);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const handleRejectConfirm = async () => {
|
|
157
|
+
setIsRejecting(true);
|
|
158
|
+
try {
|
|
159
|
+
await onReject?.();
|
|
160
|
+
} finally {
|
|
161
|
+
setIsRejecting(false);
|
|
162
|
+
setRejectDialogOpen(false);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleApproveDialogChange = (open: boolean) => {
|
|
167
|
+
setApproveDialogOpen(open);
|
|
168
|
+
if (!open && !isApproving) {
|
|
169
|
+
approveConfirmation?.onCancel?.();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleRejectDialogChange = (open: boolean) => {
|
|
174
|
+
setRejectDialogOpen(open);
|
|
175
|
+
if (!open && !isRejecting) {
|
|
176
|
+
rejectConfirmation?.onCancel?.();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Build dropdown actions for inline layout
|
|
181
|
+
const dropdownActions: SplitButtonAction[] = React.useMemo(() => {
|
|
182
|
+
const actions: SplitButtonAction[] = [];
|
|
183
|
+
|
|
184
|
+
if (!hideReject) {
|
|
185
|
+
actions.push({
|
|
186
|
+
id: 'reject',
|
|
187
|
+
label: rejectText,
|
|
188
|
+
icon: <Close size={16} />,
|
|
189
|
+
onClick: handleRejectClick,
|
|
190
|
+
variant: 'destructive',
|
|
191
|
+
separator: additionalActions.length > 0,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return [...actions, ...additionalActions];
|
|
196
|
+
}, [hideReject, rejectText, additionalActions, rejectConfirmation]);
|
|
197
|
+
|
|
198
|
+
const isInline = layout === 'inline';
|
|
199
|
+
|
|
200
|
+
const bannerContent = isInline ? (
|
|
201
|
+
<div
|
|
202
|
+
data-slot="approval-banner"
|
|
203
|
+
className={approvalBannerVariants({ variant, layout })}
|
|
204
|
+
>
|
|
205
|
+
<HStack gap="3" align="start">
|
|
206
|
+
<HStack gap="3" align="start" data-slot="approval-banner-content">
|
|
207
|
+
<Text as="span" variant={textVariant}>
|
|
208
|
+
{icon ?? <IconComponent size={20} />}
|
|
209
|
+
</Text>
|
|
210
|
+
<Stack gap="0">
|
|
211
|
+
<Text
|
|
212
|
+
size="sm"
|
|
213
|
+
weight="medium"
|
|
214
|
+
leading="tight"
|
|
215
|
+
variant={textVariant}
|
|
216
|
+
>
|
|
217
|
+
{title}
|
|
218
|
+
</Text>
|
|
219
|
+
<Text
|
|
220
|
+
as="span"
|
|
221
|
+
size="sm"
|
|
222
|
+
variant="muted"
|
|
223
|
+
data-slot="approval-banner-description"
|
|
224
|
+
>
|
|
225
|
+
{description}
|
|
226
|
+
</Text>
|
|
227
|
+
</Stack>
|
|
228
|
+
</HStack>
|
|
229
|
+
<HStack data-slot="approval-banner-actions">
|
|
230
|
+
{dropdownActions.length > 0 ? (
|
|
231
|
+
<SplitButton
|
|
232
|
+
onClick={handleApproveClick}
|
|
233
|
+
disabled={isLoading}
|
|
234
|
+
loading={approveLoading || isApproving}
|
|
235
|
+
iconLeft={<Checkmark size={16} />}
|
|
236
|
+
actions={dropdownActions}
|
|
237
|
+
>
|
|
238
|
+
{approveText}
|
|
239
|
+
</SplitButton>
|
|
240
|
+
) : (
|
|
241
|
+
<Button
|
|
242
|
+
onClick={handleApproveClick}
|
|
243
|
+
disabled={isLoading}
|
|
244
|
+
loading={approveLoading || isApproving}
|
|
245
|
+
iconLeft={<Checkmark size={16} />}
|
|
246
|
+
>
|
|
247
|
+
{approveText}
|
|
248
|
+
</Button>
|
|
249
|
+
)}
|
|
250
|
+
</HStack>
|
|
251
|
+
</HStack>
|
|
252
|
+
</div>
|
|
253
|
+
) : (
|
|
254
|
+
<div
|
|
255
|
+
data-slot="approval-banner"
|
|
256
|
+
className={approvalBannerVariants({ variant, layout })}
|
|
257
|
+
>
|
|
258
|
+
<HStack gap="3" align="start">
|
|
259
|
+
<Text as="span" variant={textVariant}>
|
|
260
|
+
{icon ?? <IconComponent size={20} />}
|
|
261
|
+
</Text>
|
|
262
|
+
<Stack gap="3">
|
|
263
|
+
<Stack gap="1">
|
|
264
|
+
<Text
|
|
265
|
+
size="sm"
|
|
266
|
+
weight="medium"
|
|
267
|
+
leading="tight"
|
|
268
|
+
variant={textVariant}
|
|
269
|
+
>
|
|
270
|
+
{title}
|
|
271
|
+
</Text>
|
|
272
|
+
<Text size="sm" variant="muted">
|
|
273
|
+
{description}
|
|
274
|
+
</Text>
|
|
275
|
+
</Stack>
|
|
276
|
+
<HStack gap="3">
|
|
277
|
+
{!hideReject && (
|
|
278
|
+
<Button
|
|
279
|
+
variant="outline"
|
|
280
|
+
onClick={handleRejectClick}
|
|
281
|
+
disabled={isLoading}
|
|
282
|
+
loading={rejectLoading || isRejecting}
|
|
283
|
+
iconLeft={<Close size={16} />}
|
|
284
|
+
>
|
|
285
|
+
{rejectText}
|
|
286
|
+
</Button>
|
|
287
|
+
)}
|
|
288
|
+
<Button
|
|
289
|
+
onClick={handleApproveClick}
|
|
290
|
+
disabled={isLoading}
|
|
291
|
+
loading={approveLoading || isApproving}
|
|
292
|
+
iconLeft={<Checkmark size={16} />}
|
|
293
|
+
>
|
|
294
|
+
{approveText}
|
|
295
|
+
</Button>
|
|
296
|
+
</HStack>
|
|
297
|
+
</Stack>
|
|
298
|
+
</HStack>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<>
|
|
304
|
+
{bannerContent}
|
|
305
|
+
|
|
306
|
+
{/* Approve Confirmation Dialog */}
|
|
307
|
+
<AlertDialog open={approveDialogOpen} onOpenChange={handleApproveDialogChange}>
|
|
308
|
+
<AlertDialogContent>
|
|
309
|
+
<AlertDialogHeader>
|
|
310
|
+
<AlertDialogTitle>
|
|
311
|
+
{approveConfirmation?.title ?? 'Confirm Approval'}
|
|
312
|
+
</AlertDialogTitle>
|
|
313
|
+
{approveConfirmation?.description && (
|
|
314
|
+
<AlertDialogDescription>
|
|
315
|
+
{approveConfirmation.description}
|
|
316
|
+
</AlertDialogDescription>
|
|
317
|
+
)}
|
|
318
|
+
</AlertDialogHeader>
|
|
319
|
+
{approveConfirmation?.content}
|
|
320
|
+
<AlertDialogFooter>
|
|
321
|
+
<AlertDialogCancel disabled={isApproving}>
|
|
322
|
+
{approveConfirmation?.cancelText ?? 'Cancel'}
|
|
323
|
+
</AlertDialogCancel>
|
|
324
|
+
<AlertDialogAction
|
|
325
|
+
onClick={handleApproveConfirm}
|
|
326
|
+
loading={isApproving}
|
|
327
|
+
disabled={isApproving}
|
|
328
|
+
>
|
|
329
|
+
{approveConfirmation?.confirmText ?? 'Approve'}
|
|
330
|
+
</AlertDialogAction>
|
|
331
|
+
</AlertDialogFooter>
|
|
332
|
+
</AlertDialogContent>
|
|
333
|
+
</AlertDialog>
|
|
334
|
+
|
|
335
|
+
{/* Reject Confirmation Dialog */}
|
|
336
|
+
<AlertDialog open={rejectDialogOpen} onOpenChange={handleRejectDialogChange}>
|
|
337
|
+
<AlertDialogContent>
|
|
338
|
+
<AlertDialogHeader>
|
|
339
|
+
<AlertDialogTitle>
|
|
340
|
+
{rejectConfirmation?.title ?? 'Confirm Rejection'}
|
|
341
|
+
</AlertDialogTitle>
|
|
342
|
+
{rejectConfirmation?.description && (
|
|
343
|
+
<AlertDialogDescription>
|
|
344
|
+
{rejectConfirmation.description}
|
|
345
|
+
</AlertDialogDescription>
|
|
346
|
+
)}
|
|
347
|
+
</AlertDialogHeader>
|
|
348
|
+
{rejectConfirmation?.content}
|
|
349
|
+
<AlertDialogFooter>
|
|
350
|
+
<AlertDialogCancel disabled={isRejecting}>
|
|
351
|
+
{rejectConfirmation?.cancelText ?? 'Cancel'}
|
|
352
|
+
</AlertDialogCancel>
|
|
353
|
+
<AlertDialogAction
|
|
354
|
+
variant="destructive"
|
|
355
|
+
onClick={handleRejectConfirm}
|
|
356
|
+
loading={isRejecting}
|
|
357
|
+
disabled={isRejecting}
|
|
358
|
+
>
|
|
359
|
+
{rejectConfirmation?.confirmText ?? 'Reject'}
|
|
360
|
+
</AlertDialogAction>
|
|
361
|
+
</AlertDialogFooter>
|
|
362
|
+
</AlertDialogContent>
|
|
363
|
+
</AlertDialog>
|
|
364
|
+
</>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export { ApprovalBanner };
|
|
369
|
+
export type { ApprovalBannerProps, ConfirmationConfig };
|