@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trycompai/design-system",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "description": "Design system for Comp AI - shadcn-style components with Tailwind CSS",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -15,6 +15,8 @@ const textVariants = cva('', {
15
15
  primary: 'text-primary',
16
16
  destructive: 'text-destructive',
17
17
  success: 'text-green-600 dark:text-green-400',
18
+ warning: 'text-warning',
19
+ info: 'text-info',
18
20
  },
19
21
  weight: {
20
22
  normal: 'font-normal',
@@ -23,6 +23,7 @@ export * from './scroll-area';
23
23
  export * from './section';
24
24
  export * from './select';
25
25
  export * from './settings';
26
+ export * from './split-button';
26
27
  export * from './table';
27
28
  export * from './tabs';
28
29
  export * from './theme-switcher';
@@ -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-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2"
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 };
@@ -1,4 +1,5 @@
1
1
  export * from './alert-dialog';
2
+ export * from './approval-banner';
2
3
  export * from './app-shell';
3
4
  export * from './calendar';
4
5
  export * from './carousel';