@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.7",
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 { cva, type VariantProps } from 'class-variance-authority';
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
- 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 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={`${aiChatPanelVariants({ size: panelSize })} ${
86
- isOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0 pointer-events-none'
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: '0 8px 32px -4px rgb(0 0 0 / 0.12), 0 4px 16px -2px rgb(0 0 0 / 0.08)',
73
+ boxShadow: isOpen ? '-4px 0 24px -4px rgb(0 0 0 / 0.08)' : 'none',
90
74
  }}
91
75
  >
92
- {children || <AIChatDefaultContent onClose={handleToggle} />}
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-2">
132
- <span className="flex size-8 items-center justify-center rounded-full bg-primary/10 text-primary">
133
- <MagicWand className="size-4" />
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/50 text-muted-foreground hover:text-foreground transition-colors"
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-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary">
155
- <MagicWand className="size-3.5" />
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-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?
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-3 border-t border-border">
166
- <div className="flex items-center gap-2 rounded-xl bg-muted/50 dark:bg-muted px-3 py-2">
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
- className="flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
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-2 text-center text-xs text-muted-foreground">
180
- Powered by AI
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-3 border-t border-border" {...props}>
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, AIChatHeader, AIChatBody, AIChatFooter, aiChatTriggerVariants, aiChatPanelVariants };
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 border-r border-border',
73
- muted: 'bg-muted border-r border-border',
74
- primary: 'bg-primary border-r border-primary-foreground/10',
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
- /** Show the floating AI chat button */
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 && <AIChat>{aiChatContent}</AIChat>}
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
- return (
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-md transition-all duration-200 cursor-pointer ${
558
+ className={`flex size-10 items-center justify-center rounded-lg transition-all duration-200 cursor-pointer ${
540
559
  isActive
541
- ? 'bg-primary/10 text-primary'
542
- : 'text-muted-foreground hover:text-foreground hover:bg-background/50'
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-4 py-2" {...props}>
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
- <Stack gap="1">{children}</Stack>
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-md px-2 py-1.5 text-sm cursor-pointer',
753
- // Smooth transitions for premium feel
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
- // Active state - default variant: softer in light, stronger in dark
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,
@@ -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.2 0 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.2 0 0);
155
+ --sidebar-border: oklch(0.28 0 0);
156
156
  --sidebar-ring: oklch(0.556 0 0);
157
157
  }
158
158
  }