@trycompai/design-system 1.0.0 → 1.0.2
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 +6 -3
- package/src/components/atoms/badge.tsx +49 -0
- package/src/components/{ui → atoms}/button.tsx +6 -1
- package/src/components/{ui → atoms}/checkbox.tsx +3 -3
- package/src/components/{ui → atoms}/heading.tsx +6 -6
- package/src/components/atoms/index.ts +21 -0
- package/src/components/atoms/kbd.tsx +21 -0
- package/src/components/atoms/logo.tsx +52 -0
- package/src/components/{ui → atoms}/slider.tsx +4 -4
- package/src/components/{ui → atoms}/spinner.tsx +3 -3
- package/src/components/atoms/stack.tsx +97 -0
- package/src/components/{ui → atoms}/switch.tsx +1 -1
- package/src/components/{ui → atoms}/text.tsx +5 -1
- package/src/components/{ui → atoms}/textarea.tsx +8 -2
- package/src/components/{ui → atoms}/toggle.tsx +3 -6
- package/src/components/{ui → molecules}/accordion.tsx +3 -3
- package/src/components/molecules/ai-chat.tsx +217 -0
- package/src/components/{ui → molecules}/alert.tsx +5 -5
- package/src/components/{ui → molecules}/breadcrumb.tsx +9 -8
- package/src/components/{ui → molecules}/card.tsx +24 -5
- package/src/components/molecules/command-search.tsx +147 -0
- package/src/components/molecules/empty.tsx +82 -0
- package/src/components/{ui → molecules}/field.tsx +16 -37
- package/src/components/{ui → molecules}/hover-card.tsx +2 -8
- package/src/components/molecules/index.ts +29 -0
- package/src/components/{ui → molecules}/input-group.tsx +1 -1
- package/src/components/molecules/input-otp.tsx +70 -0
- package/src/components/{ui → molecules}/item.tsx +18 -36
- package/src/components/molecules/page-header.tsx +80 -0
- package/src/components/{ui → molecules}/pagination.tsx +14 -23
- package/src/components/{ui → molecules}/popover.tsx +4 -2
- package/src/components/molecules/radio-group.tsx +33 -0
- package/src/components/{ui → molecules}/scroll-area.tsx +8 -11
- package/src/components/{ui → molecules}/section.tsx +3 -3
- package/src/components/{ui → molecules}/select.tsx +22 -10
- package/src/components/molecules/settings.tsx +169 -0
- package/src/components/{ui → molecules}/table.tsx +16 -3
- package/src/components/molecules/tabs.tsx +70 -0
- package/src/components/molecules/theme-switcher.tsx +176 -0
- package/src/components/{ui → molecules}/toggle-group.tsx +1 -1
- package/src/components/organisms/alert-dialog.tsx +135 -0
- package/src/components/organisms/app-shell.tsx +822 -0
- package/src/components/{ui → organisms}/calendar.tsx +6 -7
- package/src/components/{ui → organisms}/carousel.tsx +9 -11
- package/src/components/{ui → organisms}/chart.tsx +9 -24
- package/src/components/{ui → organisms}/combobox.tsx +7 -7
- package/src/components/{ui → organisms}/command.tsx +3 -3
- package/src/components/{ui → organisms}/context-menu.tsx +23 -53
- package/src/components/{ui → organisms}/dialog.tsx +3 -3
- package/src/components/{ui → organisms}/dropdown-menu.tsx +8 -6
- package/src/components/organisms/index.ts +17 -0
- package/src/components/{ui → organisms}/menubar.tsx +3 -3
- package/src/components/organisms/navigation-menu.tsx +137 -0
- package/src/components/organisms/page-layout.tsx +95 -0
- package/src/components/{ui → organisms}/sheet.tsx +7 -7
- package/src/components/{ui → organisms}/sidebar.tsx +61 -86
- package/src/components/organisms/sonner.tsx +41 -0
- package/src/components/ui/index.ts +3 -61
- package/src/fonts/TWKLausanne/TWKLausanne-300-Italic.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-300-Italic.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-300.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-300.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-350-Italic.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-350-Italic.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-350.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-350.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-400-Italic.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-400-Italic.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-400.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-400.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-700-Italic.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-700-Italic.woff2 +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-700.woff +0 -0
- package/src/fonts/TWKLausanne/TWKLausanne-700.woff2 +0 -0
- package/src/styles/globals.css +167 -23
- package/src/components/ui/alert-dialog.tsx +0 -161
- package/src/components/ui/badge.tsx +0 -48
- package/src/components/ui/empty.tsx +0 -94
- package/src/components/ui/input-otp.tsx +0 -84
- package/src/components/ui/kbd.tsx +0 -26
- package/src/components/ui/navigation-menu.tsx +0 -147
- package/src/components/ui/page-header.tsx +0 -51
- package/src/components/ui/page-layout.tsx +0 -65
- package/src/components/ui/radio-group.tsx +0 -37
- package/src/components/ui/sonner.tsx +0 -43
- package/src/components/ui/stack.tsx +0 -72
- package/src/components/ui/tabs.tsx +0 -69
- /package/src/components/{ui → atoms}/aspect-ratio.tsx +0 -0
- /package/src/components/{ui → atoms}/avatar.tsx +0 -0
- /package/src/components/{ui → atoms}/container.tsx +0 -0
- /package/src/components/{ui → atoms}/input.tsx +0 -0
- /package/src/components/{ui → atoms}/label.tsx +0 -0
- /package/src/components/{ui → atoms}/progress.tsx +0 -0
- /package/src/components/{ui → atoms}/separator.tsx +0 -0
- /package/src/components/{ui → atoms}/skeleton.tsx +0 -0
- /package/src/components/{ui → molecules}/button-group.tsx +0 -0
- /package/src/components/{ui → molecules}/collapsible.tsx +0 -0
- /package/src/components/{ui → molecules}/grid.tsx +0 -0
- /package/src/components/{ui → molecules}/resizable.tsx +0 -0
- /package/src/components/{ui → molecules}/tooltip.tsx +0 -0
- /package/src/components/{ui → organisms}/drawer.tsx +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
4
|
+
import { Close, MagicWand, Send } from '@carbon/icons-react';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
const aiChatTriggerVariants = cva(
|
|
8
|
+
'fixed bottom-6 right-6 z-50 flex items-center justify-center rounded-full transition-all duration-200 cursor-pointer',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
size: {
|
|
12
|
+
default: 'size-14',
|
|
13
|
+
sm: 'size-12',
|
|
14
|
+
lg: 'size-16',
|
|
15
|
+
},
|
|
16
|
+
variant: {
|
|
17
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90 active:scale-95',
|
|
18
|
+
secondary: 'bg-foreground text-background hover:bg-foreground/90 active:scale-95',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
size: 'default',
|
|
23
|
+
variant: 'default',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const aiChatPanelVariants = cva(
|
|
29
|
+
'fixed bottom-24 right-6 z-50 flex flex-col bg-background border border-border/40 rounded-2xl overflow-hidden transition-all duration-200 origin-bottom-right',
|
|
30
|
+
{
|
|
31
|
+
variants: {
|
|
32
|
+
size: {
|
|
33
|
+
default: 'w-96 h-[500px]',
|
|
34
|
+
sm: 'w-80 h-[400px]',
|
|
35
|
+
lg: 'w-[450px] h-[600px]',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: {
|
|
39
|
+
size: 'default',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
interface AIChatProps extends VariantProps<typeof aiChatTriggerVariants> {
|
|
45
|
+
/** Whether the chat panel is open */
|
|
46
|
+
open?: boolean;
|
|
47
|
+
/** Default open state (uncontrolled) */
|
|
48
|
+
defaultOpen?: boolean;
|
|
49
|
+
/** Callback when open state changes */
|
|
50
|
+
onOpenChange?: (open: boolean) => void;
|
|
51
|
+
/** Custom trigger icon */
|
|
52
|
+
triggerIcon?: React.ReactNode;
|
|
53
|
+
/** Panel size */
|
|
54
|
+
panelSize?: 'sm' | 'default' | 'lg';
|
|
55
|
+
/** Content to render inside the chat panel */
|
|
56
|
+
children?: React.ReactNode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function AIChat({
|
|
60
|
+
open: openProp,
|
|
61
|
+
defaultOpen = false,
|
|
62
|
+
onOpenChange,
|
|
63
|
+
triggerIcon,
|
|
64
|
+
size = 'default',
|
|
65
|
+
variant = 'default',
|
|
66
|
+
panelSize = 'default',
|
|
67
|
+
children,
|
|
68
|
+
}: AIChatProps) {
|
|
69
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
|
|
70
|
+
const isOpen = openProp ?? internalOpen;
|
|
71
|
+
|
|
72
|
+
const handleToggle = () => {
|
|
73
|
+
const newValue = !isOpen;
|
|
74
|
+
if (openProp === undefined) {
|
|
75
|
+
setInternalOpen(newValue);
|
|
76
|
+
}
|
|
77
|
+
onOpenChange?.(newValue);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
{/* Chat Panel */}
|
|
83
|
+
<div
|
|
84
|
+
data-slot="ai-chat-panel"
|
|
85
|
+
className={`${aiChatPanelVariants({ size: panelSize })} ${
|
|
86
|
+
isOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0 pointer-events-none'
|
|
87
|
+
}`}
|
|
88
|
+
style={{
|
|
89
|
+
boxShadow: '0 8px 32px -4px rgb(0 0 0 / 0.12), 0 4px 16px -2px rgb(0 0 0 / 0.08)',
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{children || <AIChatDefaultContent onClose={handleToggle} />}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{/* Floating Trigger Button */}
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
data-slot="ai-chat-trigger"
|
|
99
|
+
onClick={handleToggle}
|
|
100
|
+
className={aiChatTriggerVariants({ size, variant })}
|
|
101
|
+
style={{
|
|
102
|
+
boxShadow: '0 4px 16px -2px rgb(0 0 0 / 0.15), 0 2px 8px -2px rgb(0 0 0 / 0.1)',
|
|
103
|
+
}}
|
|
104
|
+
aria-label={isOpen ? 'Close chat' : 'Open chat'}
|
|
105
|
+
>
|
|
106
|
+
<span
|
|
107
|
+
className={`absolute transition-all duration-200 ${
|
|
108
|
+
isOpen ? 'scale-0 opacity-0 rotate-90' : 'scale-100 opacity-100 rotate-0'
|
|
109
|
+
}`}
|
|
110
|
+
>
|
|
111
|
+
{triggerIcon || <MagicWand className="size-6" />}
|
|
112
|
+
</span>
|
|
113
|
+
<span
|
|
114
|
+
className={`absolute transition-all duration-200 ${
|
|
115
|
+
isOpen ? 'scale-100 opacity-100 rotate-0' : 'scale-0 opacity-0 -rotate-90'
|
|
116
|
+
}`}
|
|
117
|
+
>
|
|
118
|
+
<Close className="size-6" />
|
|
119
|
+
</span>
|
|
120
|
+
</button>
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Default content when no children provided
|
|
126
|
+
function AIChatDefaultContent({ onClose }: { onClose: () => void }) {
|
|
127
|
+
return (
|
|
128
|
+
<>
|
|
129
|
+
{/* Header */}
|
|
130
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border/40">
|
|
131
|
+
<div className="flex items-center gap-2">
|
|
132
|
+
<span className="flex size-8 items-center justify-center rounded-full bg-primary/10 text-primary">
|
|
133
|
+
<MagicWand className="size-4" />
|
|
134
|
+
</span>
|
|
135
|
+
<div>
|
|
136
|
+
<div className="font-semibold text-sm">AI Assistant</div>
|
|
137
|
+
<div className="text-xs text-muted-foreground">Ask me anything</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
onClick={onClose}
|
|
143
|
+
className="size-8 flex items-center justify-center rounded-md hover:bg-muted/50 text-muted-foreground hover:text-foreground transition-colors"
|
|
144
|
+
>
|
|
145
|
+
<Close className="size-4" />
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Messages Area */}
|
|
150
|
+
<div className="flex-1 overflow-auto p-4">
|
|
151
|
+
<div className="flex flex-col gap-4">
|
|
152
|
+
{/* AI Welcome Message */}
|
|
153
|
+
<div className="flex gap-3">
|
|
154
|
+
<span className="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
|
|
155
|
+
<MagicWand className="size-3.5" />
|
|
156
|
+
</span>
|
|
157
|
+
<div className="flex-1 rounded-2xl rounded-tl-sm bg-muted/50 dark:bg-muted px-3 py-2 text-sm">
|
|
158
|
+
Hi! I'm your AI assistant. How can I help you today?
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Input Area */}
|
|
165
|
+
<div className="p-3 border-t border-border/40">
|
|
166
|
+
<div className="flex items-center gap-2 rounded-xl bg-muted/50 dark:bg-muted px-3 py-2">
|
|
167
|
+
<input
|
|
168
|
+
type="text"
|
|
169
|
+
placeholder="Ask a question..."
|
|
170
|
+
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
171
|
+
/>
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
className="flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
175
|
+
>
|
|
176
|
+
<Send className="size-4" />
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="mt-2 text-center text-xs text-muted-foreground">
|
|
180
|
+
Powered by AI
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Compound components for custom content
|
|
188
|
+
function AIChatHeader({ children, ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
189
|
+
return (
|
|
190
|
+
<div
|
|
191
|
+
data-slot="ai-chat-header"
|
|
192
|
+
className="flex items-center justify-between px-4 py-3 border-b border-border/40"
|
|
193
|
+
{...props}
|
|
194
|
+
>
|
|
195
|
+
{children}
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function AIChatBody({ children, ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
201
|
+
return (
|
|
202
|
+
<div data-slot="ai-chat-body" className="flex-1 overflow-auto p-4" {...props}>
|
|
203
|
+
{children}
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function AIChatFooter({ children, ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
209
|
+
return (
|
|
210
|
+
<div data-slot="ai-chat-footer" className="p-3 border-t border-border/40" {...props}>
|
|
211
|
+
{children}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { AIChat, AIChatHeader, AIChatBody, AIChatFooter, aiChatTriggerVariants, aiChatPanelVariants };
|
|
217
|
+
export type { AIChatProps };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
-
import {
|
|
2
|
+
import { CheckmarkFilled, Information, Misuse, Warning } from '@carbon/icons-react';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
|
|
5
5
|
const alertVariants = cva(
|
|
@@ -25,10 +25,10 @@ const alertVariants = cva(
|
|
|
25
25
|
|
|
26
26
|
const variantIcons = {
|
|
27
27
|
default: null,
|
|
28
|
-
info:
|
|
29
|
-
success:
|
|
30
|
-
warning:
|
|
31
|
-
destructive:
|
|
28
|
+
info: Information,
|
|
29
|
+
success: CheckmarkFilled,
|
|
30
|
+
warning: Warning,
|
|
31
|
+
destructive: Misuse,
|
|
32
32
|
} as const;
|
|
33
33
|
|
|
34
34
|
type AlertVariant = NonNullable<VariantProps<typeof alertVariants>['variant']>;
|
|
@@ -2,13 +2,13 @@ import { mergeProps } from '@base-ui/react/merge-props';
|
|
|
2
2
|
import { useRender } from '@base-ui/react/use-render';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { ArrowRight, ChevronRight, OverflowMenuHorizontal } from '@carbon/icons-react';
|
|
6
6
|
import {
|
|
7
7
|
DropdownMenu,
|
|
8
8
|
DropdownMenuContent,
|
|
9
9
|
DropdownMenuItem,
|
|
10
10
|
DropdownMenuTrigger,
|
|
11
|
-
} from '
|
|
11
|
+
} from '../organisms/dropdown-menu';
|
|
12
12
|
|
|
13
13
|
interface BreadcrumbItemData {
|
|
14
14
|
/** The text label for the breadcrumb item */
|
|
@@ -26,9 +26,10 @@ interface BreadcrumbItemData {
|
|
|
26
26
|
type BreadcrumbSeparatorType = 'chevron' | 'slash' | 'arrow';
|
|
27
27
|
|
|
28
28
|
const separatorIcons: Record<BreadcrumbSeparatorType, React.ReactNode> = {
|
|
29
|
-
chevron: <
|
|
30
|
-
slash
|
|
31
|
-
|
|
29
|
+
chevron: <ChevronRight />,
|
|
30
|
+
// Carbon doesn't ship a slash glyph icon; render it as text.
|
|
31
|
+
slash: <span className="text-xs leading-none">/</span>,
|
|
32
|
+
arrow: <ArrowRight />,
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
interface BreadcrumbProps extends Omit<React.ComponentProps<'nav'>, 'children' | 'className'> {
|
|
@@ -201,7 +202,7 @@ function BreadcrumbSeparator({
|
|
|
201
202
|
className="[&>svg]:size-3.5"
|
|
202
203
|
{...props}
|
|
203
204
|
>
|
|
204
|
-
{children ?? <
|
|
205
|
+
{children ?? <ChevronRight />}
|
|
205
206
|
</li>
|
|
206
207
|
);
|
|
207
208
|
}
|
|
@@ -215,7 +216,7 @@ function BreadcrumbEllipsis({ ...props }: Omit<React.ComponentProps<'span'>, 'cl
|
|
|
215
216
|
className="size-5 [&>svg]:size-4 flex items-center justify-center"
|
|
216
217
|
{...props}
|
|
217
218
|
>
|
|
218
|
-
<
|
|
219
|
+
<OverflowMenuHorizontal />
|
|
219
220
|
<span className="sr-only">More</span>
|
|
220
221
|
</span>
|
|
221
222
|
);
|
|
@@ -229,7 +230,7 @@ function BreadcrumbEllipsisMenu({ collapsedItems }: { collapsedItems?: Breadcrum
|
|
|
229
230
|
return (
|
|
230
231
|
<DropdownMenu>
|
|
231
232
|
<DropdownMenuTrigger variant="ellipsis" aria-label="Show hidden breadcrumb items">
|
|
232
|
-
<
|
|
233
|
+
<OverflowMenuHorizontal />
|
|
233
234
|
</DropdownMenuTrigger>
|
|
234
235
|
<DropdownMenuContent align="start">
|
|
235
236
|
{collapsedItems.map((item, index) => (
|
|
@@ -2,7 +2,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
|
|
4
4
|
const cardVariants = cva(
|
|
5
|
-
'
|
|
5
|
+
'bg-card text-card-foreground border border-border/40 shadow-[0_1px_3px_0_rgb(0_0_0/0.06)] overflow-hidden rounded-xl py-4 text-sm has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',
|
|
6
6
|
{
|
|
7
7
|
variants: {
|
|
8
8
|
width: {
|
|
@@ -25,14 +25,18 @@ const cardVariants = cva(
|
|
|
25
25
|
full: 'max-w-full',
|
|
26
26
|
},
|
|
27
27
|
spacing: {
|
|
28
|
-
default: '',
|
|
29
|
-
tight: 'gap-3',
|
|
30
|
-
relaxed: 'gap-6',
|
|
28
|
+
default: 'gap-4 data-[size=sm]:gap-3',
|
|
29
|
+
tight: 'gap-3 data-[size=sm]:gap-2.5',
|
|
30
|
+
relaxed: 'gap-6 data-[size=sm]:gap-4',
|
|
31
|
+
},
|
|
32
|
+
disabled: {
|
|
33
|
+
true: 'opacity-60 pointer-events-none select-none',
|
|
31
34
|
},
|
|
32
35
|
},
|
|
33
36
|
defaultVariants: {
|
|
34
37
|
width: 'auto',
|
|
35
38
|
spacing: 'default',
|
|
39
|
+
disabled: undefined,
|
|
36
40
|
},
|
|
37
41
|
},
|
|
38
42
|
);
|
|
@@ -56,6 +60,8 @@ function Card({
|
|
|
56
60
|
size = 'default',
|
|
57
61
|
width,
|
|
58
62
|
maxWidth,
|
|
63
|
+
spacing,
|
|
64
|
+
disabled,
|
|
59
65
|
title,
|
|
60
66
|
description,
|
|
61
67
|
headerAction,
|
|
@@ -68,6 +74,13 @@ function Card({
|
|
|
68
74
|
// Check if children contain compound components (have data-slot)
|
|
69
75
|
const hasCompoundChildren = React.Children.toArray(children).some((child) => {
|
|
70
76
|
if (React.isValidElement(child)) {
|
|
77
|
+
// Prefer checking component identity. `data-slot` is applied inside the component render,
|
|
78
|
+
// so it won't exist on `child.props` unless manually passed in.
|
|
79
|
+
if (child.type === CardHeader || child.type === CardContent || child.type === CardFooter) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fallback for direct DOM usage.
|
|
71
84
|
const props = child.props as Record<string, unknown>;
|
|
72
85
|
return (
|
|
73
86
|
props['data-slot'] === 'card-header' ||
|
|
@@ -79,7 +92,13 @@ function Card({
|
|
|
79
92
|
});
|
|
80
93
|
|
|
81
94
|
return (
|
|
82
|
-
<div
|
|
95
|
+
<div
|
|
96
|
+
data-slot="card"
|
|
97
|
+
data-size={size}
|
|
98
|
+
data-disabled={disabled ? '' : undefined}
|
|
99
|
+
className={cardVariants({ width, maxWidth, spacing, disabled: disabled ? true : undefined })}
|
|
100
|
+
{...props}
|
|
101
|
+
>
|
|
83
102
|
{hasHeader && (
|
|
84
103
|
<CardHeader>
|
|
85
104
|
{title && <CardTitle>{title}</CardTitle>}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Search } from '@carbon/icons-react';
|
|
5
|
+
import {
|
|
6
|
+
Command,
|
|
7
|
+
CommandDialog,
|
|
8
|
+
CommandEmpty,
|
|
9
|
+
CommandGroup,
|
|
10
|
+
CommandInput,
|
|
11
|
+
CommandItem,
|
|
12
|
+
CommandList,
|
|
13
|
+
CommandShortcut,
|
|
14
|
+
} from '../organisms/command';
|
|
15
|
+
import { Kbd } from '../atoms/kbd';
|
|
16
|
+
|
|
17
|
+
// ============ TYPES ============
|
|
18
|
+
|
|
19
|
+
export interface CommandSearchItem {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
icon?: React.ReactNode;
|
|
23
|
+
shortcut?: string;
|
|
24
|
+
onSelect?: () => void;
|
|
25
|
+
keywords?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CommandSearchGroup {
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
items: CommandSearchItem[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CommandSearchProps {
|
|
35
|
+
/** Placeholder text for the search input */
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
/** Text shown when no results are found */
|
|
38
|
+
emptyText?: string;
|
|
39
|
+
/** Groups of searchable items */
|
|
40
|
+
groups?: CommandSearchGroup[];
|
|
41
|
+
/** Flat list of items (alternative to groups) */
|
|
42
|
+
items?: CommandSearchItem[];
|
|
43
|
+
/** Callback when an item is selected */
|
|
44
|
+
onSelect?: (item: CommandSearchItem) => void;
|
|
45
|
+
/** Controlled open state */
|
|
46
|
+
open?: boolean;
|
|
47
|
+
/** Callback when open state changes */
|
|
48
|
+
onOpenChange?: (open: boolean) => void;
|
|
49
|
+
/** Whether to show the trigger input */
|
|
50
|
+
showTrigger?: boolean;
|
|
51
|
+
/** Width of the trigger input */
|
|
52
|
+
triggerWidth?: 'sm' | 'md' | 'lg' | 'full';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============ COMPONENT ============
|
|
56
|
+
|
|
57
|
+
function CommandSearch({
|
|
58
|
+
placeholder = 'Search...',
|
|
59
|
+
emptyText = 'No results found.',
|
|
60
|
+
groups = [],
|
|
61
|
+
items = [],
|
|
62
|
+
onSelect,
|
|
63
|
+
open: openProp,
|
|
64
|
+
onOpenChange,
|
|
65
|
+
showTrigger = true,
|
|
66
|
+
triggerWidth = 'md',
|
|
67
|
+
}: CommandSearchProps) {
|
|
68
|
+
const [_open, _setOpen] = React.useState(false);
|
|
69
|
+
const open = openProp ?? _open;
|
|
70
|
+
const setOpen = onOpenChange ?? _setOpen;
|
|
71
|
+
|
|
72
|
+
// Listen for Cmd+K / Ctrl+K
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
75
|
+
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
setOpen(!open);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
82
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
83
|
+
}, [open, setOpen]);
|
|
84
|
+
|
|
85
|
+
const handleSelect = (item: CommandSearchItem) => {
|
|
86
|
+
setOpen(false);
|
|
87
|
+
item.onSelect?.();
|
|
88
|
+
onSelect?.(item);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const widthClasses = {
|
|
92
|
+
sm: 'w-48 md:w-56',
|
|
93
|
+
md: 'w-56 md:w-72',
|
|
94
|
+
lg: 'w-72 md:w-96',
|
|
95
|
+
full: 'w-full max-w-md',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Combine flat items into a default group if provided
|
|
99
|
+
const allGroups = items.length > 0
|
|
100
|
+
? [{ id: 'default', label: '', items }, ...groups]
|
|
101
|
+
: groups;
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
{showTrigger && (
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={() => setOpen(true)}
|
|
109
|
+
className={`${widthClasses[triggerWidth]} inline-flex items-center gap-2 rounded-lg border border-input/50 bg-background/50 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-background hover:border-input focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring`}
|
|
110
|
+
>
|
|
111
|
+
<Search className="size-4" />
|
|
112
|
+
<span className="flex-1 text-left">{placeholder}</span>
|
|
113
|
+
<Kbd>⌘K</Kbd>
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
<CommandDialog open={open} onOpenChange={setOpen}>
|
|
118
|
+
<Command>
|
|
119
|
+
<CommandInput placeholder={placeholder} />
|
|
120
|
+
<CommandList>
|
|
121
|
+
<CommandEmpty>{emptyText}</CommandEmpty>
|
|
122
|
+
{allGroups.map((group) => (
|
|
123
|
+
<CommandGroup key={group.id} heading={group.label || undefined}>
|
|
124
|
+
{group.items.map((item) => (
|
|
125
|
+
<CommandItem
|
|
126
|
+
key={item.id}
|
|
127
|
+
value={item.label}
|
|
128
|
+
keywords={item.keywords}
|
|
129
|
+
onSelect={() => handleSelect(item)}
|
|
130
|
+
>
|
|
131
|
+
{item.icon}
|
|
132
|
+
<span>{item.label}</span>
|
|
133
|
+
{item.shortcut && (
|
|
134
|
+
<CommandShortcut>{item.shortcut}</CommandShortcut>
|
|
135
|
+
)}
|
|
136
|
+
</CommandItem>
|
|
137
|
+
))}
|
|
138
|
+
</CommandGroup>
|
|
139
|
+
))}
|
|
140
|
+
</CommandList>
|
|
141
|
+
</Command>
|
|
142
|
+
</CommandDialog>
|
|
143
|
+
</>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { CommandSearch };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
|
|
3
|
+
function Empty({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
data-slot="empty"
|
|
7
|
+
className="gap-4 rounded-lg border-dashed p-12 flex w-full min-w-0 flex-1 flex-col items-center justify-center text-center text-balance"
|
|
8
|
+
{...props}
|
|
9
|
+
/>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function EmptyHeader({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
data-slot="empty-header"
|
|
17
|
+
className="gap-2 flex max-w-sm flex-col items-center"
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const emptyMediaVariants = cva(
|
|
24
|
+
'mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0',
|
|
25
|
+
{
|
|
26
|
+
variants: {
|
|
27
|
+
variant: {
|
|
28
|
+
default: 'bg-transparent',
|
|
29
|
+
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: 'default',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
function EmptyMedia({
|
|
39
|
+
variant = 'default',
|
|
40
|
+
...props
|
|
41
|
+
}: Omit<React.ComponentProps<'div'>, 'className'> & VariantProps<typeof emptyMediaVariants>) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
data-slot="empty-icon"
|
|
45
|
+
data-variant={variant}
|
|
46
|
+
className={emptyMediaVariants({ variant })}
|
|
47
|
+
{...props}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function EmptyTitle({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
data-slot="empty-title"
|
|
56
|
+
className="text-lg font-medium tracking-tight"
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function EmptyDescription({ ...props }: Omit<React.ComponentProps<'p'>, 'className'>) {
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
data-slot="empty-description"
|
|
66
|
+
className="text-sm/relaxed text-muted-foreground [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4"
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function EmptyContent({ ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
data-slot="empty-content"
|
|
76
|
+
className="gap-4 text-sm flex w-full max-w-sm min-w-0 flex-col items-center text-balance"
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle };
|