@trycompai/design-system 1.0.17 → 1.0.18
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 -2
- package/src/components/atoms/text.tsx +0 -2
- package/src/components/molecules/index.ts +0 -1
- package/src/components/organisms/alert-dialog.tsx +2 -2
- package/src/components/organisms/index.ts +0 -1
- package/src/components/organisms/organization-selector.tsx +223 -32
- package/src/components/organisms/page-layout.tsx +29 -1
- package/src/components/molecules/split-button.tsx +0 -190
- package/src/components/organisms/approval-banner.tsx +0 -369
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trycompai/design-system",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"description": "Design system for Comp AI - shadcn-style components with Tailwind CSS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -97,7 +97,6 @@
|
|
|
97
97
|
"tailwindcss": "^4.0.0"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
|
-
"@repo/typescript-config": "workspace:*",
|
|
101
100
|
"@tailwindcss/postcss": "^4.1.10",
|
|
102
101
|
"@types/node": "^22.18.0",
|
|
103
102
|
"@types/react": "^19.2.7",
|
|
@@ -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-medium 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
|
|
94
|
+
className="text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3"
|
|
95
95
|
{...props}
|
|
96
96
|
/>
|
|
97
97
|
);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { Command as CommandPrimitive } from 'cmdk';
|
|
4
|
+
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
|
|
3
5
|
import { Combobox as ComboboxPrimitive } from '@base-ui/react';
|
|
4
6
|
import { Checkmark, ChevronDown, Search } from '@carbon/icons-react';
|
|
5
7
|
import * as React from 'react';
|
|
@@ -33,39 +35,70 @@ export interface OrganizationSelectorProps {
|
|
|
33
35
|
/** Search input placeholder */
|
|
34
36
|
searchPlaceholder?: string;
|
|
35
37
|
/** Text shown when no results match search */
|
|
36
|
-
|
|
38
|
+
emptySearchText?: string;
|
|
39
|
+
/** Text shown when organizations list is empty */
|
|
40
|
+
emptyListText?: string;
|
|
37
41
|
/** Whether the selector is disabled */
|
|
38
42
|
disabled?: boolean;
|
|
43
|
+
/** Whether organizations are being loaded */
|
|
44
|
+
loading?: boolean;
|
|
45
|
+
/** Text shown while loading */
|
|
46
|
+
loadingText?: string;
|
|
47
|
+
/** Error message to display */
|
|
48
|
+
error?: string;
|
|
39
49
|
/** Size variant for the trigger */
|
|
40
50
|
size?: 'sm' | 'default';
|
|
51
|
+
/** Max width for the organization name in trigger (e.g., '150px') */
|
|
52
|
+
maxWidth?: string;
|
|
53
|
+
/** Keyboard shortcut to open (e.g., 'o' for Cmd+O). Set to null to disable. */
|
|
54
|
+
hotkey?: string | null;
|
|
55
|
+
/** Whether to open as a full-screen modal (like Cmd+K) instead of dropdown */
|
|
56
|
+
modal?: boolean;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
// ============================================================================
|
|
44
60
|
// Internal Components
|
|
45
61
|
// ============================================================================
|
|
46
62
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
function LoadingSpinner() {
|
|
64
|
+
return (
|
|
65
|
+
<svg
|
|
66
|
+
className="size-4 animate-spin text-muted-foreground"
|
|
67
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
68
|
+
fill="none"
|
|
69
|
+
viewBox="0 0 24 24"
|
|
70
|
+
>
|
|
71
|
+
<circle
|
|
72
|
+
className="opacity-25"
|
|
73
|
+
cx="12"
|
|
74
|
+
cy="12"
|
|
75
|
+
r="10"
|
|
76
|
+
stroke="currentColor"
|
|
77
|
+
strokeWidth="4"
|
|
78
|
+
/>
|
|
79
|
+
<path
|
|
80
|
+
className="opacity-75"
|
|
81
|
+
fill="currentColor"
|
|
82
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
83
|
+
/>
|
|
84
|
+
</svg>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
56
87
|
|
|
57
|
-
|
|
88
|
+
function EmptyState({ icon, text }: { icon?: React.ReactNode; text: string }) {
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex flex-col items-center justify-center gap-2 py-6 text-center">
|
|
91
|
+
{icon && <span className="text-muted-foreground">{icon}</span>}
|
|
92
|
+
<span className="text-muted-foreground text-sm">{text}</span>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
58
95
|
}
|
|
59
96
|
|
|
60
|
-
function OrganizationItemContent({ org }: { org: Organization }) {
|
|
97
|
+
function OrganizationItemContent({ org, maxWidth }: { org: Organization; maxWidth?: string }) {
|
|
61
98
|
return (
|
|
62
99
|
<>
|
|
63
|
-
{org.icon
|
|
64
|
-
|
|
65
|
-
) : (
|
|
66
|
-
<OrganizationColorDot color={org.color} />
|
|
67
|
-
)}
|
|
68
|
-
<span className="truncate">{org.name}</span>
|
|
100
|
+
{org.icon && <span className="shrink-0">{org.icon}</span>}
|
|
101
|
+
<span className="truncate" style={maxWidth ? { maxWidth } : undefined}>{org.name}</span>
|
|
69
102
|
</>
|
|
70
103
|
);
|
|
71
104
|
}
|
|
@@ -81,16 +114,44 @@ function OrganizationSelector({
|
|
|
81
114
|
onValueChange,
|
|
82
115
|
placeholder = 'Select organization',
|
|
83
116
|
searchPlaceholder = 'Search by name or ID...',
|
|
84
|
-
|
|
117
|
+
emptySearchText = 'No organizations found',
|
|
118
|
+
emptyListText = 'No organizations available',
|
|
85
119
|
disabled = false,
|
|
120
|
+
loading = false,
|
|
121
|
+
loadingText = 'Loading organizations...',
|
|
122
|
+
error,
|
|
86
123
|
size = 'default',
|
|
124
|
+
maxWidth = '150px',
|
|
125
|
+
hotkey = 'o',
|
|
126
|
+
modal = false,
|
|
87
127
|
}: OrganizationSelectorProps) {
|
|
88
128
|
const [internalValue, setInternalValue] = React.useState(defaultValue ?? '');
|
|
89
129
|
const [searchQuery, setSearchQuery] = React.useState('');
|
|
130
|
+
const [open, setOpen] = React.useState(false);
|
|
131
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
90
132
|
|
|
91
133
|
const selectedValue = value ?? internalValue;
|
|
92
134
|
const selectedOrg = organizations.find((org) => org.id === selectedValue);
|
|
93
135
|
|
|
136
|
+
// Keyboard shortcut to open (Cmd/Ctrl + hotkey)
|
|
137
|
+
React.useEffect(() => {
|
|
138
|
+
if (!hotkey || disabled || loading) return;
|
|
139
|
+
|
|
140
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
141
|
+
if (event.key.toLowerCase() === hotkey.toLowerCase() && (event.metaKey || event.ctrlKey)) {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
if (modal) {
|
|
144
|
+
setOpen(true);
|
|
145
|
+
} else {
|
|
146
|
+
triggerRef.current?.click();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
152
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
153
|
+
}, [hotkey, disabled, loading, modal]);
|
|
154
|
+
|
|
94
155
|
// Filter organizations by ID or name (case-insensitive)
|
|
95
156
|
const filteredOrganizations = React.useMemo(() => {
|
|
96
157
|
if (!searchQuery.trim()) return organizations;
|
|
@@ -110,28 +171,154 @@ function OrganizationSelector({
|
|
|
110
171
|
}
|
|
111
172
|
onValueChange?.(orgId);
|
|
112
173
|
setSearchQuery(''); // Clear search on selection
|
|
174
|
+
setOpen(false); // Close modal on selection
|
|
113
175
|
},
|
|
114
176
|
[value, onValueChange]
|
|
115
177
|
);
|
|
116
178
|
|
|
117
179
|
const triggerSizeClass = size === 'sm' ? 'h-7' : 'h-8';
|
|
118
180
|
|
|
181
|
+
// Determine content state
|
|
182
|
+
const hasError = Boolean(error);
|
|
183
|
+
const isEmpty = organizations.length === 0;
|
|
184
|
+
const hasNoResults = filteredOrganizations.length === 0 && !isEmpty;
|
|
185
|
+
|
|
186
|
+
const triggerButton = (
|
|
187
|
+
<button
|
|
188
|
+
ref={triggerRef}
|
|
189
|
+
type="button"
|
|
190
|
+
data-slot="organization-selector-trigger"
|
|
191
|
+
data-size={size}
|
|
192
|
+
disabled={disabled || loading}
|
|
193
|
+
onClick={modal ? () => setOpen(true) : undefined}
|
|
194
|
+
className={`gap-1.5 rounded-lg border border-transparent bg-transparent py-2 pr-2.5 pl-2.5 text-sm transition-colors select-none hover:bg-accent [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 ${triggerSizeClass}`}
|
|
195
|
+
>
|
|
196
|
+
{loading ? (
|
|
197
|
+
<>
|
|
198
|
+
<LoadingSpinner />
|
|
199
|
+
<span className="text-muted-foreground">Loading...</span>
|
|
200
|
+
</>
|
|
201
|
+
) : selectedOrg ? (
|
|
202
|
+
<OrganizationItemContent org={selectedOrg} maxWidth={maxWidth} />
|
|
203
|
+
) : (
|
|
204
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
205
|
+
)}
|
|
206
|
+
{!loading && <ChevronDown className="text-muted-foreground size-3.5 shrink-0" />}
|
|
207
|
+
</button>
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Modal mode: full-screen dialog like Cmd+K
|
|
211
|
+
if (modal) {
|
|
212
|
+
return (
|
|
213
|
+
<>
|
|
214
|
+
{triggerButton}
|
|
215
|
+
<DialogPrimitive.Root open={open} onOpenChange={setOpen}>
|
|
216
|
+
<DialogPrimitive.Portal>
|
|
217
|
+
<DialogPrimitive.Backdrop
|
|
218
|
+
data-slot="dialog-overlay"
|
|
219
|
+
className="data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50"
|
|
220
|
+
/>
|
|
221
|
+
<DialogPrimitive.Popup
|
|
222
|
+
data-slot="command-dialog-content"
|
|
223
|
+
className="data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 rounded-xl p-0 ring-1 duration-100 sm:max-w-lg group/dialog-content fixed top-1/2 left-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 overflow-hidden outline-none"
|
|
224
|
+
>
|
|
225
|
+
<div className="sr-only">
|
|
226
|
+
<DialogPrimitive.Title>Select Organization</DialogPrimitive.Title>
|
|
227
|
+
<DialogPrimitive.Description>
|
|
228
|
+
Search and select an organization from the list
|
|
229
|
+
</DialogPrimitive.Description>
|
|
230
|
+
</div>
|
|
231
|
+
<CommandPrimitive
|
|
232
|
+
data-slot="command"
|
|
233
|
+
className="bg-popover text-popover-foreground rounded-xl p-1 flex size-full flex-col overflow-hidden"
|
|
234
|
+
filter={(value, search) => {
|
|
235
|
+
const org = organizations.find((o) => o.id === value);
|
|
236
|
+
if (!org) return 0;
|
|
237
|
+
const query = search.toLowerCase();
|
|
238
|
+
if (org.id.toLowerCase().includes(query)) return 1;
|
|
239
|
+
if (org.name.toLowerCase().includes(query)) return 1;
|
|
240
|
+
return 0;
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{/* Search Input */}
|
|
244
|
+
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
|
245
|
+
<div
|
|
246
|
+
data-slot="input-group"
|
|
247
|
+
role="group"
|
|
248
|
+
className="border-input/30 bg-input/30 h-8 rounded-lg border shadow-none transition-[color,box-shadow] has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] group/input-group relative flex w-full min-w-0 items-center outline-none"
|
|
249
|
+
>
|
|
250
|
+
<CommandPrimitive.Input
|
|
251
|
+
data-slot="command-input"
|
|
252
|
+
placeholder={searchPlaceholder}
|
|
253
|
+
className="w-full flex-1 bg-transparent px-2.5 py-1 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
|
|
254
|
+
/>
|
|
255
|
+
<div
|
|
256
|
+
data-slot="input-group-addon"
|
|
257
|
+
data-align="inline-end"
|
|
258
|
+
className="text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 pr-2 text-sm font-medium select-none"
|
|
259
|
+
>
|
|
260
|
+
<Search className="size-4 shrink-0 opacity-50" />
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{/* Organization List */}
|
|
266
|
+
<CommandPrimitive.List
|
|
267
|
+
data-slot="command-list"
|
|
268
|
+
className="no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto"
|
|
269
|
+
>
|
|
270
|
+
{loading ? (
|
|
271
|
+
<EmptyState icon={<LoadingSpinner />} text={loadingText} />
|
|
272
|
+
) : hasError ? (
|
|
273
|
+
<EmptyState text={error!} />
|
|
274
|
+
) : isEmpty ? (
|
|
275
|
+
<EmptyState text={emptyListText} />
|
|
276
|
+
) : (
|
|
277
|
+
<CommandPrimitive.Group data-slot="command-group" className="text-foreground overflow-hidden p-1">
|
|
278
|
+
<CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm">
|
|
279
|
+
{emptySearchText}
|
|
280
|
+
</CommandPrimitive.Empty>
|
|
281
|
+
{organizations.map((org) => (
|
|
282
|
+
<CommandPrimitive.Item
|
|
283
|
+
key={org.id}
|
|
284
|
+
data-slot="command-item"
|
|
285
|
+
value={org.id}
|
|
286
|
+
onSelect={() => handleValueChange(org.id)}
|
|
287
|
+
className="data-[selected=true]:bg-muted data-[selected=true]:text-foreground relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
|
288
|
+
>
|
|
289
|
+
<OrganizationItemContent org={org} />
|
|
290
|
+
{selectedValue === org.id && (
|
|
291
|
+
<Checkmark className="ml-auto size-4" />
|
|
292
|
+
)}
|
|
293
|
+
</CommandPrimitive.Item>
|
|
294
|
+
))}
|
|
295
|
+
</CommandPrimitive.Group>
|
|
296
|
+
)}
|
|
297
|
+
</CommandPrimitive.List>
|
|
298
|
+
</CommandPrimitive>
|
|
299
|
+
</DialogPrimitive.Popup>
|
|
300
|
+
</DialogPrimitive.Portal>
|
|
301
|
+
</DialogPrimitive.Root>
|
|
302
|
+
</>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Dropdown mode (default): inline combobox
|
|
119
307
|
return (
|
|
120
308
|
<ComboboxPrimitive.Root value={selectedValue} onValueChange={handleValueChange}>
|
|
121
309
|
<ComboboxPrimitive.Trigger
|
|
310
|
+
ref={triggerRef}
|
|
122
311
|
data-slot="organization-selector-trigger"
|
|
123
312
|
data-size={size}
|
|
124
313
|
disabled={disabled}
|
|
125
|
-
className={`
|
|
314
|
+
className={`gap-1.5 rounded-lg border border-transparent bg-transparent py-2 pr-2.5 pl-2.5 text-sm transition-colors select-none hover:bg-accent [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 ${triggerSizeClass}`}
|
|
126
315
|
>
|
|
127
|
-
|
|
128
|
-
{selectedOrg
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</span>
|
|
134
|
-
<ChevronDown className="text-muted-foreground size-4 shrink-0" />
|
|
316
|
+
{selectedOrg ? (
|
|
317
|
+
<OrganizationItemContent org={selectedOrg} maxWidth={maxWidth} />
|
|
318
|
+
) : (
|
|
319
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
320
|
+
)}
|
|
321
|
+
<ChevronDown className="text-muted-foreground size-3.5 shrink-0" />
|
|
135
322
|
</ComboboxPrimitive.Trigger>
|
|
136
323
|
|
|
137
324
|
<ComboboxPrimitive.Portal>
|
|
@@ -164,10 +351,14 @@ function OrganizationSelector({
|
|
|
164
351
|
data-slot="organization-selector-list"
|
|
165
352
|
className="no-scrollbar max-h-64 scroll-py-1 overflow-y-auto p-1 overscroll-contain"
|
|
166
353
|
>
|
|
167
|
-
{
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
354
|
+
{loading ? (
|
|
355
|
+
<EmptyState icon={<LoadingSpinner />} text={loadingText} />
|
|
356
|
+
) : hasError ? (
|
|
357
|
+
<EmptyState text={error!} />
|
|
358
|
+
) : isEmpty ? (
|
|
359
|
+
<EmptyState text={emptyListText} />
|
|
360
|
+
) : hasNoResults ? (
|
|
361
|
+
<EmptyState text={emptySearchText} />
|
|
171
362
|
) : (
|
|
172
363
|
filteredOrganizations.map((org) => (
|
|
173
364
|
<ComboboxPrimitive.Item
|
|
@@ -3,6 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { cn } from '../../../lib/utils';
|
|
5
5
|
import { Stack } from '../atoms/stack';
|
|
6
|
+
import { Skeleton } from '../atoms/skeleton';
|
|
6
7
|
|
|
7
8
|
const pageLayoutVariants = cva('min-h-full bg-background text-foreground', {
|
|
8
9
|
variants: {
|
|
@@ -48,6 +49,29 @@ interface PageLayoutProps
|
|
|
48
49
|
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
|
49
50
|
/** Vertical gap between children. Defaults to 'lg' (gap-6). */
|
|
50
51
|
gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '0' | '1' | '2' | '3' | '4' | '6' | '8';
|
|
52
|
+
/** Whether the page is loading. Shows skeleton placeholder when true. */
|
|
53
|
+
loading?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function PageLayoutSkeleton() {
|
|
57
|
+
return (
|
|
58
|
+
<Stack gap="lg">
|
|
59
|
+
{/* Header skeleton */}
|
|
60
|
+
<div className="space-y-2">
|
|
61
|
+
<Skeleton style={{ width: '30%', height: 24 }} />
|
|
62
|
+
<Skeleton style={{ width: '50%', height: 16 }} />
|
|
63
|
+
</div>
|
|
64
|
+
{/* Content skeleton */}
|
|
65
|
+
<div className="space-y-4">
|
|
66
|
+
<Skeleton style={{ width: '100%', height: 200 }} />
|
|
67
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
68
|
+
<Skeleton style={{ width: '100%', height: 120 }} />
|
|
69
|
+
<Skeleton style={{ width: '100%', height: 120 }} />
|
|
70
|
+
<Skeleton style={{ width: '100%', height: 120 }} />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</Stack>
|
|
74
|
+
);
|
|
51
75
|
}
|
|
52
76
|
|
|
53
77
|
function PageLayout({
|
|
@@ -56,13 +80,16 @@ function PageLayout({
|
|
|
56
80
|
container = true,
|
|
57
81
|
maxWidth,
|
|
58
82
|
gap = 'lg',
|
|
83
|
+
loading = false,
|
|
59
84
|
children,
|
|
60
85
|
...props
|
|
61
86
|
}: PageLayoutProps) {
|
|
62
87
|
// For center variant, default to smaller max-width (sm) for auth-style pages
|
|
63
88
|
const resolvedMaxWidth = maxWidth ?? (variant === 'center' ? 'sm' : 'xl');
|
|
64
89
|
|
|
65
|
-
const content = (
|
|
90
|
+
const content = loading ? (
|
|
91
|
+
<PageLayoutSkeleton />
|
|
92
|
+
) : (
|
|
66
93
|
<Stack gap={gap}>
|
|
67
94
|
{children}
|
|
68
95
|
</Stack>
|
|
@@ -72,6 +99,7 @@ function PageLayout({
|
|
|
72
99
|
<div
|
|
73
100
|
data-slot="page-layout"
|
|
74
101
|
data-variant={variant}
|
|
102
|
+
data-loading={loading || undefined}
|
|
75
103
|
className={pageLayoutVariants({ variant, padding })}
|
|
76
104
|
{...props}
|
|
77
105
|
>
|
|
@@ -1,190 +0,0 @@
|
|
|
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 };
|
|
@@ -1,369 +0,0 @@
|
|
|
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 };
|