@trycompai/design-system 1.0.7 → 1.0.9
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.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Design system for Comp AI - shadcn-style components with Tailwind CSS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -65,12 +65,6 @@
|
|
|
65
65
|
"shadcn",
|
|
66
66
|
"ui"
|
|
67
67
|
],
|
|
68
|
-
"scripts": {
|
|
69
|
-
"clean": "rm -rf .turbo node_modules",
|
|
70
|
-
"format": "prettier --write .",
|
|
71
|
-
"lint": "prettier --check .",
|
|
72
|
-
"typecheck": "tsc --noEmit"
|
|
73
|
-
},
|
|
74
68
|
"dependencies": {
|
|
75
69
|
"@base-ui/react": "^1.0.0",
|
|
76
70
|
"@carbon/icons-react": "^11.72.0",
|
|
@@ -97,7 +91,6 @@
|
|
|
97
91
|
"tailwindcss": "^4.0.0"
|
|
98
92
|
},
|
|
99
93
|
"devDependencies": {
|
|
100
|
-
"@repo/typescript-config": "workspace:*",
|
|
101
94
|
"@tailwindcss/postcss": "^4.1.10",
|
|
102
95
|
"@types/node": "^22.18.0",
|
|
103
96
|
"@types/react": "^19.2.7",
|
|
@@ -106,6 +99,13 @@
|
|
|
106
99
|
"react": "^19.1.1",
|
|
107
100
|
"react-dom": "^19.1.0",
|
|
108
101
|
"tailwindcss": "^4.1.8",
|
|
109
|
-
"typescript": "^5.9.3"
|
|
102
|
+
"typescript": "^5.9.3",
|
|
103
|
+
"@repo/typescript-config": "0.0.0"
|
|
104
|
+
},
|
|
105
|
+
"scripts": {
|
|
106
|
+
"clean": "rm -rf .turbo node_modules",
|
|
107
|
+
"format": "prettier --write .",
|
|
108
|
+
"lint": "prettier --check .",
|
|
109
|
+
"typecheck": "tsc --noEmit"
|
|
110
110
|
}
|
|
111
|
-
}
|
|
111
|
+
}
|
|
@@ -89,6 +89,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
89
89
|
iconRight,
|
|
90
90
|
disabled,
|
|
91
91
|
children,
|
|
92
|
+
render,
|
|
92
93
|
...props
|
|
93
94
|
},
|
|
94
95
|
ref,
|
|
@@ -102,6 +103,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
102
103
|
data-loading={loading || undefined}
|
|
103
104
|
disabled={isDisabled}
|
|
104
105
|
className={buttonVariants({ variant, width, size })}
|
|
106
|
+
render={render}
|
|
107
|
+
nativeButton={!render}
|
|
105
108
|
{...props}
|
|
106
109
|
>
|
|
107
110
|
{loading ? (
|
|
@@ -1,146 +1,150 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { Close, MagicWand, Send } from '@carbon/icons-react';
|
|
3
|
+
import { Close, MagicWand, Send, Keyboard } from '@carbon/icons-react';
|
|
5
4
|
import * as React from 'react';
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 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> {
|
|
6
|
+
import { Kbd } from '../atoms/kbd';
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
|
|
8
|
+
|
|
9
|
+
interface AIChatProps {
|
|
45
10
|
/** Whether the chat panel is open */
|
|
46
11
|
open?: boolean;
|
|
47
12
|
/** Default open state (uncontrolled) */
|
|
48
13
|
defaultOpen?: boolean;
|
|
49
14
|
/** Callback when open state changes */
|
|
50
15
|
onOpenChange?: (open: boolean) => void;
|
|
51
|
-
/** Custom trigger icon */
|
|
52
|
-
triggerIcon?: React.ReactNode;
|
|
53
|
-
/** Panel size */
|
|
54
|
-
panelSize?: 'sm' | 'default' | 'lg';
|
|
55
16
|
/** Content to render inside the chat panel */
|
|
56
17
|
children?: React.ReactNode;
|
|
18
|
+
/** Keyboard shortcut to toggle (default: Cmd+J) */
|
|
19
|
+
shortcut?: string;
|
|
57
20
|
}
|
|
58
21
|
|
|
59
22
|
function AIChat({
|
|
60
23
|
open: openProp,
|
|
61
24
|
defaultOpen = false,
|
|
62
25
|
onOpenChange,
|
|
63
|
-
triggerIcon,
|
|
64
|
-
size = 'default',
|
|
65
|
-
variant = 'default',
|
|
66
|
-
panelSize = 'default',
|
|
67
26
|
children,
|
|
27
|
+
shortcut = 'j',
|
|
68
28
|
}: AIChatProps) {
|
|
69
29
|
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
|
|
70
30
|
const isOpen = openProp ?? internalOpen;
|
|
71
31
|
|
|
72
|
-
const handleToggle = () => {
|
|
32
|
+
const handleToggle = React.useCallback(() => {
|
|
73
33
|
const newValue = !isOpen;
|
|
74
34
|
if (openProp === undefined) {
|
|
75
35
|
setInternalOpen(newValue);
|
|
76
36
|
}
|
|
77
37
|
onOpenChange?.(newValue);
|
|
78
|
-
};
|
|
38
|
+
}, [isOpen, openProp, onOpenChange]);
|
|
39
|
+
|
|
40
|
+
const handleClose = React.useCallback(() => {
|
|
41
|
+
if (openProp === undefined) {
|
|
42
|
+
setInternalOpen(false);
|
|
43
|
+
}
|
|
44
|
+
onOpenChange?.(false);
|
|
45
|
+
}, [openProp, onOpenChange]);
|
|
46
|
+
|
|
47
|
+
// Keyboard shortcut listener
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
50
|
+
if (e.key.toLowerCase() === shortcut && (e.metaKey || e.ctrlKey)) {
|
|
51
|
+
e.preventDefault();
|
|
52
|
+
handleToggle();
|
|
53
|
+
}
|
|
54
|
+
// Close on Escape when open
|
|
55
|
+
if (e.key === 'Escape' && isOpen) {
|
|
56
|
+
handleClose();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
61
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
62
|
+
}, [shortcut, handleToggle, handleClose, isOpen]);
|
|
79
63
|
|
|
80
64
|
return (
|
|
81
65
|
<>
|
|
82
|
-
{/* Chat Panel */}
|
|
66
|
+
{/* Chat Panel - persistent side panel, no backdrop */}
|
|
83
67
|
<div
|
|
84
68
|
data-slot="ai-chat-panel"
|
|
85
|
-
className={
|
|
86
|
-
isOpen ? '
|
|
69
|
+
className={`fixed top-14 right-0 bottom-0 z-30 w-full max-w-md flex flex-col bg-background border-l border-border transition-transform duration-300 ease-out ${
|
|
70
|
+
isOpen ? 'translate-x-0' : 'translate-x-full'
|
|
87
71
|
}`}
|
|
88
72
|
style={{
|
|
89
|
-
boxShadow:
|
|
73
|
+
boxShadow: isOpen ? '-4px 0 24px -4px rgb(0 0 0 / 0.08)' : 'none',
|
|
90
74
|
}}
|
|
91
75
|
>
|
|
92
|
-
{children || <AIChatDefaultContent onClose={
|
|
76
|
+
{children || <AIChatDefaultContent onClose={handleClose} />}
|
|
93
77
|
</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
78
|
</>
|
|
122
79
|
);
|
|
123
80
|
}
|
|
124
81
|
|
|
82
|
+
// Navbar trigger button for AI chat
|
|
83
|
+
interface AIChatTriggerProps {
|
|
84
|
+
onClick?: () => void;
|
|
85
|
+
isOpen?: boolean;
|
|
86
|
+
shortcut?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function AIChatTrigger({ onClick, isOpen, shortcut = 'J' }: AIChatTriggerProps) {
|
|
90
|
+
return (
|
|
91
|
+
<Tooltip>
|
|
92
|
+
<TooltipTrigger
|
|
93
|
+
render={
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={onClick}
|
|
97
|
+
className={`inline-flex items-center gap-2 h-8 px-3 rounded-lg text-sm font-medium transition-all cursor-pointer ${
|
|
98
|
+
isOpen
|
|
99
|
+
? 'bg-primary text-primary-foreground'
|
|
100
|
+
: 'bg-muted hover:bg-accent text-foreground'
|
|
101
|
+
}`}
|
|
102
|
+
aria-label={isOpen ? 'Close AI Chat' : 'Open AI Chat'}
|
|
103
|
+
>
|
|
104
|
+
<MagicWand className="size-4" />
|
|
105
|
+
<span className="hidden sm:inline">Ask AI</span>
|
|
106
|
+
<span className="hidden sm:inline-flex ml-1 opacity-60 text-xs bg-foreground/10 px-1.5 py-0.5 rounded">
|
|
107
|
+
{navigator?.platform?.includes('Mac') ? '⌘' : 'Ctrl+'}
|
|
108
|
+
{shortcut}
|
|
109
|
+
</span>
|
|
110
|
+
</button>
|
|
111
|
+
}
|
|
112
|
+
/>
|
|
113
|
+
<TooltipContent side="bottom">
|
|
114
|
+
{isOpen ? 'Close AI Chat' : 'Open AI Chat'}
|
|
115
|
+
</TooltipContent>
|
|
116
|
+
</Tooltip>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
125
120
|
// Default content when no children provided
|
|
126
121
|
function AIChatDefaultContent({ onClose }: { onClose: () => void }) {
|
|
122
|
+
const [message, setMessage] = React.useState('');
|
|
123
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
124
|
+
|
|
125
|
+
// Focus input when panel opens
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
const timer = setTimeout(() => inputRef.current?.focus(), 100);
|
|
128
|
+
return () => clearTimeout(timer);
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
127
131
|
return (
|
|
128
132
|
<>
|
|
129
133
|
{/* Header */}
|
|
130
|
-
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
|
131
|
-
<div className="flex items-center gap-
|
|
132
|
-
<span className="flex size-
|
|
133
|
-
<MagicWand className="size-
|
|
134
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
|
|
135
|
+
<div className="flex items-center gap-3">
|
|
136
|
+
<span className="flex size-9 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
|
137
|
+
<MagicWand className="size-5" />
|
|
134
138
|
</span>
|
|
135
139
|
<div>
|
|
136
140
|
<div className="font-semibold text-sm">AI Assistant</div>
|
|
137
|
-
<div className="text-xs text-muted-foreground">Ask me anything</div>
|
|
141
|
+
<div className="text-xs text-muted-foreground">Ask me anything about your compliance</div>
|
|
138
142
|
</div>
|
|
139
143
|
</div>
|
|
140
144
|
<button
|
|
141
145
|
type="button"
|
|
142
146
|
onClick={onClose}
|
|
143
|
-
className="size-8 flex items-center justify-center rounded-md hover:bg-muted
|
|
147
|
+
className="size-8 flex items-center justify-center rounded-md hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
144
148
|
>
|
|
145
149
|
<Close className="size-4" />
|
|
146
150
|
</button>
|
|
@@ -151,33 +155,50 @@ function AIChatDefaultContent({ onClose }: { onClose: () => void }) {
|
|
|
151
155
|
<div className="flex flex-col gap-4">
|
|
152
156
|
{/* AI Welcome Message */}
|
|
153
157
|
<div className="flex gap-3">
|
|
154
|
-
<span className="flex size-
|
|
155
|
-
<MagicWand className="size-
|
|
158
|
+
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
|
159
|
+
<MagicWand className="size-4" />
|
|
156
160
|
</span>
|
|
157
|
-
<div className="flex-1 rounded-2xl rounded-tl-
|
|
158
|
-
Hi! I
|
|
161
|
+
<div className="flex-1 rounded-2xl rounded-tl-md bg-muted px-4 py-3 text-sm">
|
|
162
|
+
<p className="mb-2">Hi! I can help you with:</p>
|
|
163
|
+
<ul className="space-y-1 text-muted-foreground">
|
|
164
|
+
<li>Understanding compliance requirements</li>
|
|
165
|
+
<li>Reviewing and creating policies</li>
|
|
166
|
+
<li>Analyzing evidence and controls</li>
|
|
167
|
+
<li>Answering questions about SOC 2, ISO 27001, and more</li>
|
|
168
|
+
</ul>
|
|
159
169
|
</div>
|
|
160
170
|
</div>
|
|
161
171
|
</div>
|
|
162
172
|
</div>
|
|
163
173
|
|
|
164
174
|
{/* Input Area */}
|
|
165
|
-
<div className="p-
|
|
166
|
-
<div className="flex items-center gap-2 rounded-xl bg-muted
|
|
175
|
+
<div className="p-4 border-t border-border shrink-0">
|
|
176
|
+
<div className="flex items-center gap-2 rounded-xl bg-muted px-4 py-2.5">
|
|
167
177
|
<input
|
|
178
|
+
ref={inputRef}
|
|
168
179
|
type="text"
|
|
180
|
+
value={message}
|
|
181
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
169
182
|
placeholder="Ask a question..."
|
|
170
183
|
className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
184
|
+
onKeyDown={(e) => {
|
|
185
|
+
if (e.key === 'Enter' && message.trim()) {
|
|
186
|
+
// Handle send
|
|
187
|
+
setMessage('');
|
|
188
|
+
}
|
|
189
|
+
}}
|
|
171
190
|
/>
|
|
172
191
|
<button
|
|
173
192
|
type="button"
|
|
174
|
-
|
|
193
|
+
disabled={!message.trim()}
|
|
194
|
+
className="flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
175
195
|
>
|
|
176
196
|
<Send className="size-4" />
|
|
177
197
|
</button>
|
|
178
198
|
</div>
|
|
179
|
-
<div className="mt-
|
|
180
|
-
|
|
199
|
+
<div className="mt-3 flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
|
200
|
+
<Keyboard className="size-3" />
|
|
201
|
+
<span>Press <Kbd>Esc</Kbd> to close</span>
|
|
181
202
|
</div>
|
|
182
203
|
</div>
|
|
183
204
|
</>
|
|
@@ -189,7 +210,7 @@ function AIChatHeader({ children, ...props }: Omit<React.ComponentProps<'div'>,
|
|
|
189
210
|
return (
|
|
190
211
|
<div
|
|
191
212
|
data-slot="ai-chat-header"
|
|
192
|
-
className="flex items-center justify-between px-4 py-3 border-b border-border"
|
|
213
|
+
className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0"
|
|
193
214
|
{...props}
|
|
194
215
|
>
|
|
195
216
|
{children}
|
|
@@ -207,11 +228,11 @@ function AIChatBody({ children, ...props }: Omit<React.ComponentProps<'div'>, 'c
|
|
|
207
228
|
|
|
208
229
|
function AIChatFooter({ children, ...props }: Omit<React.ComponentProps<'div'>, 'className'>) {
|
|
209
230
|
return (
|
|
210
|
-
<div data-slot="ai-chat-footer" className="p-
|
|
231
|
+
<div data-slot="ai-chat-footer" className="p-4 border-t border-border shrink-0" {...props}>
|
|
211
232
|
{children}
|
|
212
233
|
</div>
|
|
213
234
|
);
|
|
214
235
|
}
|
|
215
236
|
|
|
216
|
-
export { AIChat,
|
|
217
|
-
export type { AIChatProps };
|
|
237
|
+
export { AIChat, AIChatBody, AIChatFooter, AIChatHeader, AIChatTrigger };
|
|
238
|
+
export type { AIChatProps, AIChatTriggerProps };
|
|
@@ -51,7 +51,7 @@ function TabsTrigger({ ...props }: Omit<TabsPrimitive.Tab.Props, 'className'>) {
|
|
|
51
51
|
return (
|
|
52
52
|
<TabsPrimitive.Tab
|
|
53
53
|
data-slot="tabs-trigger"
|
|
54
|
-
className="gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none group-data-[variant=underline]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=underline]/tabs-list:bg-transparent group-data-[variant=underline]/tabs-list:data-active:bg-transparent group-data-[variant=underline]/tabs-list:px-4 group-data-[variant=underline]/tabs-list:py-3 group-data-[variant=underline]/tabs-list:mb-0 group-data-[variant=underline]/tabs-list:rounded-none group-data-[variant=underline]/tabs-list:border-none group-data-[variant=underline]/tabs-list:hover:bg-muted group-data-[variant=underline]/tabs-list:flex-none group-data-[variant=underline]/tabs-list:justify-start group-data-[variant=underline]/tabs-list:text-left dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=underline]/tabs-list:data-active:border-transparent dark:group-data-[variant=underline]/tabs-list:data-active:bg-transparent data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:after:bg-foreground group-data-[variant=underline]/tabs-list:after:bg-primary group-data-[variant=line]/tabs-list:data-active:after:opacity-100 group-data-[variant=underline]/tabs-list:after:h-[3px] group-data-[variant=underline]/tabs-list:after:bottom-0 group-data-[variant=underline]/tabs-list:after:rounded-none group-data-[variant=underline]/tabs-list:h-auto group-data-[variant=underline]/tabs-list:data-active:after:opacity-100"
|
|
54
|
+
className="cursor-pointer gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none group-data-[variant=underline]/tabs-list:data-active:shadow-none [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=underline]/tabs-list:bg-transparent group-data-[variant=underline]/tabs-list:data-active:bg-transparent group-data-[variant=underline]/tabs-list:px-4 group-data-[variant=underline]/tabs-list:py-3 group-data-[variant=underline]/tabs-list:mb-0 group-data-[variant=underline]/tabs-list:rounded-none group-data-[variant=underline]/tabs-list:border-none group-data-[variant=underline]/tabs-list:hover:bg-muted group-data-[variant=underline]/tabs-list:flex-none group-data-[variant=underline]/tabs-list:justify-start group-data-[variant=underline]/tabs-list:text-left dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=underline]/tabs-list:data-active:border-transparent dark:group-data-[variant=underline]/tabs-list:data-active:bg-transparent data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:after:bg-foreground group-data-[variant=underline]/tabs-list:after:bg-primary group-data-[variant=line]/tabs-list:data-active:after:opacity-100 group-data-[variant=underline]/tabs-list:after:h-[3px] group-data-[variant=underline]/tabs-list:after:bottom-0 group-data-[variant=underline]/tabs-list:after:rounded-none group-data-[variant=underline]/tabs-list:h-auto group-data-[variant=underline]/tabs-list:data-active:after:opacity-100"
|
|
55
55
|
{...props}
|
|
56
56
|
/>
|
|
57
57
|
);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
-
import { Search, SidePanelClose, SidePanelOpen } from '@carbon/icons-react';
|
|
2
|
+
import { ChevronDown, Search, SidePanelClose, SidePanelOpen } from '@carbon/icons-react';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
|
|
5
5
|
import { Kbd } from '../atoms/kbd';
|
|
6
6
|
import { Stack } from '../atoms/stack';
|
|
7
|
-
import { AIChat } from '../molecules/ai-chat';
|
|
7
|
+
import { AIChat, AIChatTrigger } from '../molecules/ai-chat';
|
|
8
8
|
import { InputGroup, InputGroupAddon, InputGroupInput } from '../molecules/input-group';
|
|
9
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../molecules/tooltip';
|
|
9
10
|
|
|
10
11
|
// ============ CONTEXT ============
|
|
11
12
|
|
|
@@ -27,6 +28,10 @@ type AppShellContextProps = {
|
|
|
27
28
|
/** Sidebar variant for mobile drawer styling */
|
|
28
29
|
sidebarVariant: 'default' | 'muted' | 'primary';
|
|
29
30
|
setSidebarVariant: (variant: 'default' | 'muted' | 'primary') => void;
|
|
31
|
+
/** AI Chat state */
|
|
32
|
+
aiChatOpen: boolean;
|
|
33
|
+
setAIChatOpen: (open: boolean) => void;
|
|
34
|
+
toggleAIChat: () => void;
|
|
30
35
|
};
|
|
31
36
|
|
|
32
37
|
const AppShellContext = React.createContext<AppShellContextProps | null>(null);
|
|
@@ -69,9 +74,9 @@ const appShellSidebarVariants = cva(
|
|
|
69
74
|
{
|
|
70
75
|
variants: {
|
|
71
76
|
variant: {
|
|
72
|
-
default: 'bg-background
|
|
73
|
-
muted: 'bg-muted
|
|
74
|
-
primary: 'bg-primary
|
|
77
|
+
default: 'bg-background',
|
|
78
|
+
muted: 'bg-muted',
|
|
79
|
+
primary: 'bg-primary',
|
|
75
80
|
},
|
|
76
81
|
},
|
|
77
82
|
defaultVariants: {
|
|
@@ -80,7 +85,7 @@ const appShellSidebarVariants = cva(
|
|
|
80
85
|
},
|
|
81
86
|
);
|
|
82
87
|
|
|
83
|
-
const appShellContentVariants = cva('flex flex-1 flex-col overflow-auto bg-background min-h-0', {
|
|
88
|
+
const appShellContentVariants = cva('flex flex-1 flex-col overflow-auto bg-background min-h-0 border-l border-border', {
|
|
84
89
|
variants: {
|
|
85
90
|
padding: {
|
|
86
91
|
none: '',
|
|
@@ -117,7 +122,7 @@ interface AppShellProps extends Omit<React.ComponentProps<'div'>, 'className'> {
|
|
|
117
122
|
sidebarOpen?: boolean;
|
|
118
123
|
/** Callback when sidebar state changes */
|
|
119
124
|
onSidebarOpenChange?: (open: boolean) => void;
|
|
120
|
-
/**
|
|
125
|
+
/** Enable AI chat feature */
|
|
121
126
|
showAIChat?: boolean;
|
|
122
127
|
/** Custom content for the AI chat panel */
|
|
123
128
|
aiChatContent?: React.ReactNode;
|
|
@@ -198,6 +203,9 @@ function AppShell({
|
|
|
198
203
|
// Mobile drawer state (always starts closed)
|
|
199
204
|
const [mobileDrawerOpen, setMobileDrawerOpen] = React.useState(false);
|
|
200
205
|
|
|
206
|
+
// AI Chat state
|
|
207
|
+
const [aiChatOpen, setAIChatOpen] = React.useState(false);
|
|
208
|
+
|
|
201
209
|
// Content for mobile drawer (populated by Rail and Sidebar components)
|
|
202
210
|
const [railContent, setRailContent] = React.useState<React.ReactNode>(null);
|
|
203
211
|
const [sidebarContent, setSidebarContent] = React.useState<React.ReactNode>(null);
|
|
@@ -222,6 +230,10 @@ function AppShell({
|
|
|
222
230
|
setMobileDrawerOpen(!mobileDrawerOpen);
|
|
223
231
|
}, [mobileDrawerOpen]);
|
|
224
232
|
|
|
233
|
+
const toggleAIChat = React.useCallback(() => {
|
|
234
|
+
setAIChatOpen(!aiChatOpen);
|
|
235
|
+
}, [aiChatOpen]);
|
|
236
|
+
|
|
225
237
|
// Listen for Cmd+\ to toggle sidebar (desktop) or mobile drawer
|
|
226
238
|
React.useEffect(() => {
|
|
227
239
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
@@ -255,8 +267,11 @@ function AppShell({
|
|
|
255
267
|
setSidebarContent,
|
|
256
268
|
sidebarVariant,
|
|
257
269
|
setSidebarVariant,
|
|
270
|
+
aiChatOpen,
|
|
271
|
+
setAIChatOpen,
|
|
272
|
+
toggleAIChat,
|
|
258
273
|
}),
|
|
259
|
-
[sidebarOpen, setSidebarOpen, toggleSidebar, mobileDrawerOpen, toggleMobileDrawer, railContent, sidebarContent, sidebarVariant],
|
|
274
|
+
[sidebarOpen, setSidebarOpen, toggleSidebar, mobileDrawerOpen, toggleMobileDrawer, railContent, sidebarContent, sidebarVariant, aiChatOpen, toggleAIChat],
|
|
260
275
|
);
|
|
261
276
|
|
|
262
277
|
return (
|
|
@@ -268,7 +283,11 @@ function AppShell({
|
|
|
268
283
|
{...props}
|
|
269
284
|
>
|
|
270
285
|
{children}
|
|
271
|
-
{showAIChat &&
|
|
286
|
+
{showAIChat && (
|
|
287
|
+
<AIChat open={aiChatOpen} onOpenChange={setAIChatOpen}>
|
|
288
|
+
{aiChatContent}
|
|
289
|
+
</AIChat>
|
|
290
|
+
)}
|
|
272
291
|
</div>
|
|
273
292
|
</AppShellContext.Provider>
|
|
274
293
|
);
|
|
@@ -332,7 +351,7 @@ function AppShellBody({ children, ...props }: AppShellBodyProps) {
|
|
|
332
351
|
const { mobileDrawerOpen, setMobileDrawerOpen, railContent, sidebarContent, sidebarVariant } = useAppShell();
|
|
333
352
|
|
|
334
353
|
return (
|
|
335
|
-
<div data-slot="app-shell-body" className="flex flex-1 overflow-hidden bg-background/50 min-h-0 gap-0" {...props}>
|
|
354
|
+
<div data-slot="app-shell-body" className=" flex flex-1 overflow-hidden bg-background/50 min-h-0 gap-0" {...props}>
|
|
336
355
|
{/* Mobile drawer - shows both rail and sidebar */}
|
|
337
356
|
<div className="md:hidden">
|
|
338
357
|
{/* Backdrop */}
|
|
@@ -376,7 +395,7 @@ function AppShellMain({ children, ...props }: Omit<React.ComponentProps<'div'>,
|
|
|
376
395
|
return (
|
|
377
396
|
<div
|
|
378
397
|
data-slot="app-shell-main"
|
|
379
|
-
className="flex flex-1 min-h-0 ml-2 mr-2 mb-2 rounded-xl overflow-hidden bg-background"
|
|
398
|
+
className="flex flex-1 min-h-0 ml-2 mr-2 mb-2 rounded-xl overflow-hidden bg-background border-border border"
|
|
380
399
|
{...props}
|
|
381
400
|
>
|
|
382
401
|
{children}
|
|
@@ -531,23 +550,35 @@ function AppShellRailItem({ isActive, icon, label, ...props }: AppShellRailItemP
|
|
|
531
550
|
}
|
|
532
551
|
}, [isActive, context, itemId]);
|
|
533
552
|
|
|
534
|
-
|
|
553
|
+
const button = (
|
|
535
554
|
<button
|
|
536
555
|
ref={buttonRef}
|
|
537
556
|
data-slot="app-shell-rail-item"
|
|
538
557
|
data-active={isActive}
|
|
539
|
-
className={`flex size-10 items-center justify-center rounded-
|
|
558
|
+
className={`flex size-10 items-center justify-center rounded-lg transition-all duration-200 cursor-pointer ${
|
|
540
559
|
isActive
|
|
541
|
-
? 'bg-primary
|
|
542
|
-
: 'text-muted-foreground hover:text-foreground hover:bg-
|
|
560
|
+
? 'bg-primary text-primary-foreground shadow-md'
|
|
561
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent dark:hover:bg-muted hover:shadow active:scale-95'
|
|
543
562
|
}`}
|
|
544
|
-
title={label}
|
|
545
563
|
aria-label={label}
|
|
546
564
|
{...props}
|
|
547
565
|
>
|
|
548
566
|
<span className="size-5 [&>svg]:size-5">{icon}</span>
|
|
549
567
|
</button>
|
|
550
568
|
);
|
|
569
|
+
|
|
570
|
+
if (label) {
|
|
571
|
+
return (
|
|
572
|
+
<Tooltip>
|
|
573
|
+
<TooltipTrigger render={button} />
|
|
574
|
+
<TooltipContent side="right" sideOffset={8}>
|
|
575
|
+
{label}
|
|
576
|
+
</TooltipContent>
|
|
577
|
+
</Tooltip>
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return button;
|
|
551
582
|
}
|
|
552
583
|
|
|
553
584
|
function AppShellSidebar({
|
|
@@ -716,7 +747,7 @@ function AppShellSidebarHeader({ icon, title, description, action, children, ...
|
|
|
716
747
|
|
|
717
748
|
function AppShellNav({ children, ...props }: AppShellNavProps) {
|
|
718
749
|
return (
|
|
719
|
-
<nav data-slot="app-shell-nav" className="flex-1 space-y-
|
|
750
|
+
<nav data-slot="app-shell-nav" className="flex-1 space-y-1 py-2" {...props}>
|
|
720
751
|
{children}
|
|
721
752
|
</nav>
|
|
722
753
|
);
|
|
@@ -738,7 +769,7 @@ function AppShellNavGroup({ label, children, ...props }: AppShellNavGroupProps)
|
|
|
738
769
|
{label}
|
|
739
770
|
</div>
|
|
740
771
|
)}
|
|
741
|
-
<
|
|
772
|
+
<div className="space-y-0.5">{children}</div>
|
|
742
773
|
</div>
|
|
743
774
|
);
|
|
744
775
|
}
|
|
@@ -749,34 +780,102 @@ function AppShellNavItem({ isActive, icon, children, ...props }: AppShellNavItem
|
|
|
749
780
|
data-slot="app-shell-nav-item"
|
|
750
781
|
data-active={isActive}
|
|
751
782
|
className={[
|
|
752
|
-
'flex w-full items-center gap-3 rounded-
|
|
753
|
-
|
|
783
|
+
'flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm cursor-pointer',
|
|
784
|
+
'transition-all duration-150 ease-out',
|
|
785
|
+
isActive
|
|
786
|
+
? 'bg-muted text-foreground font-medium'
|
|
787
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50',
|
|
788
|
+
].join(' ')}
|
|
789
|
+
{...props}
|
|
790
|
+
>
|
|
791
|
+
{icon && <span className="size-4 shrink-0 [&>svg]:size-4">{icon}</span>}
|
|
792
|
+
{children}
|
|
793
|
+
</button>
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
interface AppShellNavItemCollapsibleProps extends Omit<React.ComponentProps<'div'>, 'className'> {
|
|
798
|
+
/** Whether any child is currently active */
|
|
799
|
+
isActive?: boolean;
|
|
800
|
+
/** Whether the collapsible is expanded */
|
|
801
|
+
defaultOpen?: boolean;
|
|
802
|
+
icon?: React.ReactNode;
|
|
803
|
+
label: string;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function AppShellNavItemCollapsible({
|
|
807
|
+
isActive,
|
|
808
|
+
defaultOpen,
|
|
809
|
+
icon,
|
|
810
|
+
label,
|
|
811
|
+
children,
|
|
812
|
+
...props
|
|
813
|
+
}: AppShellNavItemCollapsibleProps) {
|
|
814
|
+
const [isOpen, setIsOpen] = React.useState(defaultOpen ?? isActive ?? false);
|
|
815
|
+
|
|
816
|
+
// Auto-expand when a child becomes active
|
|
817
|
+
React.useEffect(() => {
|
|
818
|
+
if (isActive) {
|
|
819
|
+
setIsOpen(true);
|
|
820
|
+
}
|
|
821
|
+
}, [isActive]);
|
|
822
|
+
|
|
823
|
+
return (
|
|
824
|
+
<div data-slot="app-shell-nav-item-collapsible" {...props}>
|
|
825
|
+
{/* Parent trigger */}
|
|
826
|
+
<button
|
|
827
|
+
type="button"
|
|
828
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
829
|
+
className={[
|
|
830
|
+
'flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm cursor-pointer',
|
|
831
|
+
'transition-all duration-150 ease-out',
|
|
832
|
+
isActive || isOpen
|
|
833
|
+
? 'text-foreground font-semibold'
|
|
834
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50',
|
|
835
|
+
].join(' ')}
|
|
836
|
+
>
|
|
837
|
+
{icon && <span className="size-4 shrink-0 [&>svg]:size-4">{icon}</span>}
|
|
838
|
+
<span className="flex-1 text-left">{label}</span>
|
|
839
|
+
<ChevronDown
|
|
840
|
+
className={`size-4 text-muted-foreground transition-transform duration-200 ${
|
|
841
|
+
isOpen ? 'rotate-180' : ''
|
|
842
|
+
}`}
|
|
843
|
+
/>
|
|
844
|
+
</button>
|
|
845
|
+
|
|
846
|
+
{/* Children - collapsible card area */}
|
|
847
|
+
<div
|
|
848
|
+
className={`overflow-hidden transition-all duration-200 ease-out ${
|
|
849
|
+
isOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
|
|
850
|
+
}`}
|
|
851
|
+
>
|
|
852
|
+
<div className="mt-1 ml-2 rounded-xl bg-muted/40 dark:bg-muted/20 p-1.5 space-y-0.5">
|
|
853
|
+
{children}
|
|
854
|
+
</div>
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Sub-item for collapsible nav (no icon, smaller padding)
|
|
861
|
+
interface AppShellNavSubItemProps extends Omit<React.ComponentProps<'button'>, 'className'> {
|
|
862
|
+
isActive?: boolean;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function AppShellNavSubItem({ isActive, children, ...props }: AppShellNavSubItemProps) {
|
|
866
|
+
return (
|
|
867
|
+
<button
|
|
868
|
+
data-slot="app-shell-nav-sub-item"
|
|
869
|
+
data-active={isActive}
|
|
870
|
+
className={[
|
|
871
|
+
'flex w-full items-center rounded-lg px-3 py-2 text-sm cursor-pointer',
|
|
754
872
|
'transition-all duration-150 ease-out',
|
|
755
|
-
// Subtle scale on hover for premium touch
|
|
756
|
-
'active:scale-[0.98]',
|
|
757
|
-
// Base styles for default/muted sidebar variants
|
|
758
873
|
isActive
|
|
759
|
-
?
|
|
760
|
-
|
|
761
|
-
'[[data-variant=default]_&]:bg-muted/50 [[data-variant=default]_&]:dark:bg-muted [[data-variant=default]_&]:text-foreground',
|
|
762
|
-
// Active state - muted variant (gray bg sidebar): use white bg
|
|
763
|
-
'[[data-variant=muted]_&]:bg-background [[data-variant=muted]_&]:text-foreground [[data-variant=muted]_&]:shadow-sm',
|
|
764
|
-
// Active state - primary variant: use white/10 overlay
|
|
765
|
-
'[[data-variant=primary]_&]:bg-primary-foreground/15 [[data-variant=primary]_&]:text-primary-foreground',
|
|
766
|
-
'font-medium',
|
|
767
|
-
].join(' ')
|
|
768
|
-
: [
|
|
769
|
-
// Inactive - default/muted variants
|
|
770
|
-
'text-muted-foreground hover:text-foreground',
|
|
771
|
-
'[[data-variant=default]_&]:hover:bg-muted/30 [[data-variant=default]_&]:dark:hover:bg-muted/60',
|
|
772
|
-
'[[data-variant=muted]_&]:hover:bg-background/60',
|
|
773
|
-
// Inactive - primary variant
|
|
774
|
-
'[[data-variant=primary]_&]:text-primary-foreground/70 [[data-variant=primary]_&]:hover:text-primary-foreground [[data-variant=primary]_&]:hover:bg-primary-foreground/10',
|
|
775
|
-
].join(' '),
|
|
874
|
+
? 'bg-background text-foreground font-medium shadow-sm'
|
|
875
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-background/50',
|
|
776
876
|
].join(' ')}
|
|
777
877
|
{...props}
|
|
778
878
|
>
|
|
779
|
-
{icon && <span className="size-4 shrink-0 [&>svg]:size-4 transition-transform duration-150 group-hover:scale-110">{icon}</span>}
|
|
780
879
|
{children}
|
|
781
880
|
</button>
|
|
782
881
|
);
|
|
@@ -798,8 +897,15 @@ function AppShellNavFooter({ children, ...props }: AppShellNavFooterProps) {
|
|
|
798
897
|
);
|
|
799
898
|
}
|
|
800
899
|
|
|
900
|
+
// AI Chat trigger for navbar - uses context to control AI chat panel
|
|
901
|
+
function AppShellAIChatTrigger() {
|
|
902
|
+
const { aiChatOpen, toggleAIChat } = useAppShell();
|
|
903
|
+
return <AIChatTrigger onClick={toggleAIChat} isOpen={aiChatOpen} />;
|
|
904
|
+
}
|
|
905
|
+
|
|
801
906
|
export {
|
|
802
907
|
AppShell,
|
|
908
|
+
AppShellAIChatTrigger,
|
|
803
909
|
AppShellBody,
|
|
804
910
|
AppShellContent,
|
|
805
911
|
AppShellMain,
|
|
@@ -808,6 +914,8 @@ export {
|
|
|
808
914
|
AppShellNavFooter,
|
|
809
915
|
AppShellNavGroup,
|
|
810
916
|
AppShellNavItem,
|
|
917
|
+
AppShellNavItemCollapsible,
|
|
918
|
+
AppShellNavSubItem,
|
|
811
919
|
AppShellRail,
|
|
812
920
|
AppShellRailItem,
|
|
813
921
|
AppShellSearch,
|
package/src/styles/globals.css
CHANGED
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
--success: oklch(0.7 0.16 145);
|
|
139
139
|
--warning: oklch(0.8 0.15 85);
|
|
140
140
|
--info: oklch(0.7 0.15 250);
|
|
141
|
-
--border: oklch(0.
|
|
141
|
+
--border: oklch(0.28 0 0);
|
|
142
142
|
--input: oklch(1 0 0 / 15%);
|
|
143
143
|
--ring: oklch(0.556 0 0);
|
|
144
144
|
--chart-1: oklch(0.845 0.143 164.978);
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
--sidebar-primary-foreground: oklch(0.262 0.051 172.552);
|
|
153
153
|
--sidebar-accent: oklch(0.22 0 0);
|
|
154
154
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
155
|
-
--sidebar-border: oklch(0.
|
|
155
|
+
--sidebar-border: oklch(0.28 0 0);
|
|
156
156
|
--sidebar-ring: oklch(0.556 0 0);
|
|
157
157
|
}
|
|
158
158
|
}
|