@trycompai/design-system 1.0.18 → 1.0.20

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.20",
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 };
@@ -18,13 +18,13 @@ function Tabs({
18
18
  }
19
19
 
20
20
  const tabsListVariants = cva(
21
- 'rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none data-[variant=underline]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col',
21
+ 'rounded-lg p-[3px] group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none data-[variant=underline]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col overflow-x-auto scrollbar-none',
22
22
  {
23
23
  variants: {
24
24
  variant: {
25
25
  default: 'bg-muted',
26
26
  line: 'gap-1 bg-transparent',
27
- underline: 'p-0 pb-px bg-transparent w-full justify-start items-stretch shadow-[inset_0_-1px_0_0_var(--color-border)]',
27
+ underline: 'p-0 pb-px bg-transparent w-full max-w-full justify-start items-stretch shadow-[inset_0_-1px_0_0_var(--color-border)]',
28
28
  },
29
29
  },
30
30
  defaultVariants: {
@@ -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';
@@ -4,6 +4,7 @@ import * as React from 'react';
4
4
  import { cn } from '../../../lib/utils';
5
5
  import { Stack } from '../atoms/stack';
6
6
  import { Skeleton } from '../atoms/skeleton';
7
+ import { Heading } from '../atoms/heading';
7
8
 
8
9
  const pageLayoutVariants = cva('min-h-full bg-background text-foreground', {
9
10
  variants: {
@@ -51,16 +52,26 @@ interface PageLayoutProps
51
52
  gap?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '0' | '1' | '2' | '3' | '4' | '6' | '8';
52
53
  /** Whether the page is loading. Shows skeleton placeholder when true. */
53
54
  loading?: boolean;
55
+ /** @deprecated Use header prop instead. Page title to display during loading state. */
56
+ loadingTitle?: string;
57
+ /** Header element (e.g., PageHeader) that renders regardless of loading state. */
58
+ header?: React.ReactNode;
54
59
  }
55
60
 
56
- function PageLayoutSkeleton() {
61
+ function PageLayoutSkeleton({ title, includeHeader = true }: { title?: string; includeHeader?: boolean }) {
57
62
  return (
58
63
  <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
+ {/* Header skeleton - only shown if no header prop is provided */}
65
+ {includeHeader && (
66
+ <div className="space-y-2">
67
+ {title ? (
68
+ <Heading level="1">{title}</Heading>
69
+ ) : (
70
+ <Skeleton style={{ width: '30%', height: 24 }} />
71
+ )}
72
+ <Skeleton style={{ width: '50%', height: 16 }} />
73
+ </div>
74
+ )}
64
75
  {/* Content skeleton */}
65
76
  <div className="space-y-4">
66
77
  <Skeleton style={{ width: '100%', height: 200 }} />
@@ -81,6 +92,8 @@ function PageLayout({
81
92
  maxWidth,
82
93
  gap = 'lg',
83
94
  loading = false,
95
+ loadingTitle,
96
+ header,
84
97
  children,
85
98
  ...props
86
99
  }: PageLayoutProps) {
@@ -88,9 +101,13 @@ function PageLayout({
88
101
  const resolvedMaxWidth = maxWidth ?? (variant === 'center' ? 'sm' : 'xl');
89
102
 
90
103
  const content = loading ? (
91
- <PageLayoutSkeleton />
104
+ <Stack gap={gap}>
105
+ {header}
106
+ <PageLayoutSkeleton title={loadingTitle} includeHeader={!header} />
107
+ </Stack>
92
108
  ) : (
93
109
  <Stack gap={gap}>
110
+ {header}
94
111
  {children}
95
112
  </Stack>
96
113
  );
@@ -254,6 +254,15 @@
254
254
  .font-bold {
255
255
  font-weight: 700;
256
256
  }
257
+
258
+ /* Hide scrollbar while keeping scroll functionality */
259
+ .scrollbar-none {
260
+ scrollbar-width: none;
261
+ -ms-overflow-style: none;
262
+ }
263
+ .scrollbar-none::-webkit-scrollbar {
264
+ display: none;
265
+ }
257
266
  }
258
267
 
259
268
  /* View Transitions API for smooth page transitions */