fluxy-bot 0.8.9 → 0.9.0
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/README.md +53 -84
- package/bin/cli.js +1717 -6
- package/components.json +18 -18
- package/package.json +98 -109
- package/postcss.config.js +3 -3
- package/scripts/install.ps1 +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/postinstall.js +47 -58
- package/shared/ai.ts +118 -202
- package/shared/config.ts +37 -39
- package/shared/logger.ts +6 -14
- package/shared/paths.ts +10 -16
- package/shared/relay.ts +89 -98
- package/supervisor/backend.ts +77 -89
- package/supervisor/chat/ARCHITECTURE.md +13 -14
- package/supervisor/chat/OnboardWizard.tsx +2194 -3419
- package/supervisor/chat/fluxy-main.tsx +495 -633
- package/supervisor/chat/fluxy.html +24 -36
- package/supervisor/chat/onboard-main.tsx +8 -8
- package/supervisor/chat/onboard.html +10 -13
- package/supervisor/chat/src/components/Chat/AudioBubble.tsx +94 -108
- package/supervisor/chat/src/components/Chat/ChatView.tsx +14 -36
- package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +71 -89
- package/supervisor/chat/src/components/Chat/InputBar.tsx +498 -728
- package/supervisor/chat/src/components/Chat/MessageBubble.tsx +154 -246
- package/supervisor/chat/src/components/Chat/MessageList.tsx +114 -138
- package/supervisor/chat/src/components/Chat/TypingIndicator.tsx +38 -50
- package/supervisor/chat/src/components/LoginScreen.tsx +245 -296
- package/supervisor/chat/src/hooks/useChat.ts +210 -259
- package/supervisor/chat/src/hooks/useFluxyChat.ts +232 -322
- package/supervisor/chat/src/hooks/useSpeechRecognition.ts +126 -148
- package/supervisor/chat/src/lib/auth.ts +20 -23
- package/supervisor/chat/src/lib/ws-client.ts +113 -125
- package/supervisor/chat/src/styles/globals.css +76 -85
- package/supervisor/file-saver.ts +39 -52
- package/supervisor/fluxy-agent.ts +213 -292
- package/supervisor/index.ts +895 -1263
- package/supervisor/scheduler.ts +237 -299
- package/supervisor/tunnel.ts +113 -167
- package/supervisor/vite-dev.ts +51 -61
- package/supervisor/widget.js +135 -147
- package/supervisor/worker.ts +45 -49
- package/tsconfig.json +18 -24
- package/vite.config.ts +48 -55
- package/vite.fluxy.config.ts +34 -34
- package/worker/claude-auth.ts +202 -265
- package/worker/codex-auth.ts +144 -181
- package/worker/db.ts +63 -191
- package/worker/index.ts +601 -842
- package/workspace/MYSELF.md +1 -1
- package/workspace/PULSE.json +6 -6
- package/workspace/backend/index.ts +16 -19
- package/workspace/client/index.html +47 -60
- package/workspace/client/public/manifest.json +21 -21
- package/workspace/client/public/sw.js +29 -31
- package/workspace/client/src/App.tsx +91 -136
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +68 -72
- package/workspace/client/src/components/ErrorBoundary.tsx +11 -11
- package/workspace/client/src/components/Layout/DashboardLayout.tsx +35 -35
- package/workspace/client/src/components/Layout/Footer.tsx +12 -12
- package/workspace/client/src/components/Layout/MobileNav.tsx +23 -23
- package/workspace/client/src/components/Layout/Sidebar.tsx +81 -81
- package/workspace/client/src/components/ui/avatar.tsx +85 -85
- package/workspace/client/src/components/ui/badge.tsx +40 -41
- package/workspace/client/src/components/ui/button.tsx +56 -57
- package/workspace/client/src/components/ui/card.tsx +75 -75
- package/workspace/client/src/components/ui/dialog.tsx +114 -117
- package/workspace/client/src/components/ui/dropdown-menu.tsx +191 -199
- package/workspace/client/src/components/ui/input.tsx +17 -17
- package/workspace/client/src/components/ui/scroll-area.tsx +47 -47
- package/workspace/client/src/components/ui/select.tsx +145 -151
- package/workspace/client/src/components/ui/separator.tsx +21 -21
- package/workspace/client/src/components/ui/sheet.tsx +102 -102
- package/workspace/client/src/components/ui/skeleton.tsx +10 -10
- package/workspace/client/src/components/ui/switch.tsx +26 -26
- package/workspace/client/src/components/ui/tabs.tsx +69 -69
- package/workspace/client/src/components/ui/textarea.tsx +14 -14
- package/workspace/client/src/components/ui/tooltip.tsx +37 -37
- package/workspace/client/src/lib/utils.ts +1 -1
- package/workspace/client/src/main.tsx +3 -3
- package/workspace/client/src/styles/globals.css +76 -85
- package/workspace/skills/code-reviewer/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/code-reviewer/skills/code-reviewer/SKILL.md +0 -6
- package/workspace/skills/daily-standup/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/daily-standup/skills/daily-standup/SKILL.md +0 -7
- package/workspace/skills/workspace-helper/.claude-plugin/plugin.json +3 -3
- package/workspace/skills/workspace-helper/skills/workspace-helper/SKILL.md +0 -2
- package/bin/cli.backup.js +0 -2138
- package/cli/commands/daemon.ts +0 -42
- package/cli/commands/init.ts +0 -32
- package/cli/commands/start.ts +0 -113
- package/cli/commands/tunnel.temp.ts +0 -206
- package/cli/commands/tunnel.ts +0 -227
- package/cli/commands/update.ts +0 -163
- package/cli/core/base-adapter.ts +0 -156
- package/cli/core/cloudflared.ts +0 -113
- package/cli/core/config.ts +0 -73
- package/cli/core/os-detector.ts +0 -43
- package/cli/core/server.ts +0 -109
- package/cli/core/types.ts +0 -15
- package/cli/index.ts +0 -72
- package/cli/platforms/darwin.ts +0 -146
- package/cli/platforms/index.ts +0 -21
- package/cli/platforms/linux.ts +0 -156
- package/cli/platforms/win32.ts +0 -34
- package/cli/utils/ui.ts +0 -37
|
@@ -1,80 +1,76 @@
|
|
|
1
1
|
const starterSuggestions = [
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
'Build me a CRM',
|
|
3
|
+
'Create a habit tracker',
|
|
4
|
+
'Make a finance dashboard',
|
|
5
|
+
'Build a task manager',
|
|
6
6
|
];
|
|
7
7
|
|
|
8
8
|
export default function DashboardPage() {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
input.focus();
|
|
34
|
-
}
|
|
35
|
-
}, 400);
|
|
36
|
-
};
|
|
9
|
+
const handleSuggestion = (text: string) => {
|
|
10
|
+
// Open the chat widget and pre-fill the suggestion
|
|
11
|
+
const panel = document.getElementById('fluxy-widget-panel');
|
|
12
|
+
if (panel && !panel.classList.contains('open')) {
|
|
13
|
+
const toggle = document.getElementById('fluxy-widget-toggle');
|
|
14
|
+
toggle?.click();
|
|
15
|
+
}
|
|
16
|
+
// Give the widget a moment to open, then try to fill the input
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
const input = document.querySelector<HTMLTextAreaElement>(
|
|
19
|
+
'#fluxy-widget-panel textarea, #fluxy-widget-panel input[type="text"]'
|
|
20
|
+
);
|
|
21
|
+
if (input) {
|
|
22
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
23
|
+
window.HTMLTextAreaElement.prototype, 'value'
|
|
24
|
+
)?.set || Object.getOwnPropertyDescriptor(
|
|
25
|
+
window.HTMLInputElement.prototype, 'value'
|
|
26
|
+
)?.set;
|
|
27
|
+
nativeSetter?.call(input, text);
|
|
28
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
29
|
+
input.focus();
|
|
30
|
+
}
|
|
31
|
+
}, 400);
|
|
32
|
+
};
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex flex-col items-center justify-start h-full px-4 pt-16 sm:pt-24">
|
|
36
|
+
{/* Welcome message */}
|
|
37
|
+
<h1 className="text-2xl sm:text-3xl font-bold text-center mb-2">
|
|
38
|
+
Let's get started
|
|
39
|
+
</h1>
|
|
40
|
+
<p className="text-muted-foreground text-sm sm:text-base text-center max-w-md mb-8">
|
|
41
|
+
Tell me what to build and I'll create it for you, right here.
|
|
42
|
+
</p>
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
{/* Starter suggestion chips */}
|
|
45
|
+
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-3 max-w-lg mb-12">
|
|
46
|
+
{starterSuggestions.map((suggestion) => (
|
|
47
|
+
<button
|
|
48
|
+
key={suggestion}
|
|
49
|
+
onClick={() => handleSuggestion(suggestion)}
|
|
50
|
+
className="px-4 py-2 sm:px-5 sm:py-2.5 rounded-full border border-border bg-card text-sm text-muted-foreground hover:text-foreground hover:border-primary/40 hover:bg-card/80 transition-all duration-200 active:scale-[0.97]"
|
|
51
|
+
>
|
|
52
|
+
{suggestion}
|
|
53
|
+
</button>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
60
56
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
57
|
+
{/* Arrow pointing to chat */}
|
|
58
|
+
{/* Desktop */}
|
|
59
|
+
<div
|
|
60
|
+
id="chat-arrow-desktop"
|
|
61
|
+
className="hidden md:flex fixed flex-col items-center pointer-events-none"
|
|
62
|
+
style={{ bottom: 100, right: 100 }}
|
|
63
|
+
>
|
|
64
|
+
<img src="/arrow.png" alt="" style={{ width: 120 }} />
|
|
65
|
+
</div>
|
|
66
|
+
{/* Mobile */}
|
|
67
|
+
<img
|
|
68
|
+
id="chat-arrow-mobile"
|
|
69
|
+
src="/arrow.png"
|
|
70
|
+
alt=""
|
|
71
|
+
className="block md:hidden fixed pointer-events-none"
|
|
72
|
+
style={{ width: 90, bottom: 75, right: 60 }}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
80
76
|
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { Component, type ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
interface Props {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
fallback: ReactNode;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
interface State {
|
|
9
|
-
|
|
9
|
+
hasError: boolean;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export default class ErrorBoundary extends Component<Props, State> {
|
|
13
|
-
|
|
13
|
+
state: State = { hasError: false };
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
static getDerivedStateFromError(): State {
|
|
16
|
+
return { hasError: true };
|
|
17
|
+
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
render() {
|
|
20
|
+
if (this.state.hasError) return this.props.fallback;
|
|
21
|
+
return this.props.children;
|
|
22
|
+
}
|
|
23
23
|
}
|
|
@@ -5,47 +5,47 @@ import Footer from './Footer';
|
|
|
5
5
|
import MobileNav from './MobileNav';
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
|
-
|
|
8
|
+
children: ReactNode;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export default function DashboardLayout({ children }: Props) {
|
|
12
|
-
|
|
12
|
+
const [connected, setConnected] = useState(true);
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const check = () => {
|
|
16
|
+
fetch('/api/health', { method: 'HEAD' })
|
|
17
|
+
.then(() => setConnected(true))
|
|
18
|
+
.catch(() => setConnected(false));
|
|
19
|
+
};
|
|
20
|
+
check();
|
|
21
|
+
const id = setInterval(check, 15_000);
|
|
22
|
+
return () => clearInterval(id);
|
|
23
|
+
}, []);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex h-dvh flex-col bg-background">
|
|
27
|
+
{/* Mobile header */}
|
|
28
|
+
<header className="flex items-center justify-between px-4 py-3 md:hidden">
|
|
29
|
+
<MobileNav />
|
|
30
|
+
<div className="flex items-center gap-2">
|
|
31
|
+
<img src="/fluxy.png" alt="Fluxy" className="h-6 w-auto" />
|
|
32
|
+
<span className="font-semibold text-base">Fluxy</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div className="w-10" />
|
|
35
|
+
</header>
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
<div className="flex flex-1 overflow-hidden">
|
|
38
|
+
{/* Desktop sidebar */}
|
|
39
|
+
<div className="hidden md:flex shrink-0">
|
|
40
|
+
<Sidebar />
|
|
41
|
+
</div>
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
43
|
+
{/* Main content + footer */}
|
|
44
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
45
|
+
<main className="flex-1 overflow-y-auto">{children}</main>
|
|
46
|
+
<Footer connected={connected} />
|
|
49
47
|
</div>
|
|
50
|
-
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
51
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export default function Footer({ connected }: { connected: boolean }) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
2
|
+
return (
|
|
3
|
+
<footer className="flex items-center px-4 md:px-6 py-2.5 text-[11px] text-muted-foreground/60 shrink-0">
|
|
4
|
+
<div className="flex items-center gap-1.5">
|
|
5
|
+
<div
|
|
6
|
+
className={`h-1.5 w-1.5 rounded-full ${
|
|
7
|
+
connected ? 'bg-emerald-500' : 'bg-red-500'
|
|
8
|
+
}`}
|
|
9
|
+
/>
|
|
10
|
+
<span>{connected ? 'Connected' : 'Disconnected'}</span>
|
|
11
|
+
</div>
|
|
12
|
+
</footer>
|
|
13
|
+
);
|
|
14
14
|
}
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { Menu } from 'lucide-react';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Sheet,
|
|
5
|
+
SheetContent,
|
|
6
|
+
SheetTitle,
|
|
7
|
+
} from '@/components/ui/sheet';
|
|
4
8
|
import Sidebar from './Sidebar';
|
|
5
9
|
|
|
6
10
|
export default function MobileNav() {
|
|
7
|
-
|
|
11
|
+
const [open, setOpen] = useState(false);
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
</SheetContent>
|
|
27
|
-
</Sheet>
|
|
28
|
-
</>
|
|
29
|
-
);
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
<button
|
|
16
|
+
onClick={() => setOpen(true)}
|
|
17
|
+
className="flex items-center justify-center h-10 w-10 rounded-lg text-muted-foreground hover:text-foreground transition-colors md:hidden"
|
|
18
|
+
>
|
|
19
|
+
<Menu className="h-5 w-5" />
|
|
20
|
+
<span className="sr-only">Open navigation</span>
|
|
21
|
+
</button>
|
|
22
|
+
<Sheet open={open} onOpenChange={setOpen}>
|
|
23
|
+
<SheetContent side="left" className="p-0 w-64" showCloseButton={false}>
|
|
24
|
+
<SheetTitle className="sr-only">Navigation</SheetTitle>
|
|
25
|
+
<Sidebar />
|
|
26
|
+
</SheetContent>
|
|
27
|
+
</Sheet>
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
30
|
}
|
|
@@ -1,102 +1,102 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
LayoutDashboard,
|
|
4
|
+
AppWindow,
|
|
5
|
+
BarChart3,
|
|
6
|
+
Search,
|
|
7
|
+
HelpCircle,
|
|
8
|
+
ChevronDown,
|
|
9
9
|
} from 'lucide-react';
|
|
10
10
|
import { cn } from '@/lib/utils';
|
|
11
11
|
|
|
12
12
|
function getGreeting(): string {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
const hour = new Date().getHours();
|
|
14
|
+
if (hour < 12) return 'Good\nMorning';
|
|
15
|
+
if (hour < 18) return 'Good\nAfternoon';
|
|
16
|
+
return 'Good\nEvening';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export default function Sidebar() {
|
|
20
|
-
|
|
20
|
+
const [appsOpen, setAppsOpen] = useState(false);
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
return (
|
|
23
|
+
<aside className="flex flex-col h-full w-64 border-r border-border/50 bg-sidebar p-5 pt-8">
|
|
24
|
+
{/* Logo */}
|
|
25
|
+
<div className="flex items-center gap-2.5 mb-8">
|
|
26
|
+
<img src="/fluxy.png" alt="Fluxy" className="h-7 w-auto" />
|
|
27
|
+
<span className="font-semibold text-lg">Fluxy</span>
|
|
28
|
+
</div>
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
{/* Greeting */}
|
|
31
|
+
<div className="mb-10">
|
|
32
|
+
<h1 className="text-4xl font-bold leading-[1.1] whitespace-pre-line">
|
|
33
|
+
{getGreeting()}
|
|
34
|
+
</h1>
|
|
35
|
+
<h2 className="text-4xl font-bold text-primary mt-0.5"></h2>
|
|
36
|
+
</div>
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
{/* Navigation */}
|
|
39
|
+
<nav className="flex-1 space-y-0.5">
|
|
40
|
+
<NavButton icon={LayoutDashboard} label="Dashboard" active />
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
42
|
+
{/* My Apps with dropdown */}
|
|
43
|
+
<div>
|
|
44
|
+
<button
|
|
45
|
+
onClick={() => setAppsOpen(!appsOpen)}
|
|
46
|
+
className="flex items-center gap-3 w-full px-3 py-2.5 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 transition-colors"
|
|
47
|
+
>
|
|
48
|
+
<AppWindow className="h-[18px] w-[18px]" />
|
|
49
|
+
My Apps
|
|
50
|
+
<ChevronDown
|
|
51
|
+
className={cn(
|
|
52
|
+
'h-3.5 w-3.5 ml-auto transition-transform duration-200',
|
|
53
|
+
appsOpen && 'rotate-180',
|
|
54
|
+
)}
|
|
55
|
+
/>
|
|
56
|
+
</button>
|
|
57
|
+
{appsOpen && (
|
|
58
|
+
<div className="ml-9 mt-0.5 space-y-0.5">
|
|
59
|
+
<button className="block w-full text-left px-3 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 transition-colors">
|
|
60
|
+
App 1
|
|
61
|
+
</button>
|
|
62
|
+
<button className="block w-full text-left px-3 py-2 rounded-lg text-sm text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 transition-colors">
|
|
63
|
+
App 2
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
<NavButton icon={BarChart3} label="Reports" />
|
|
70
|
+
<NavButton icon={Search} label="Research" />
|
|
71
|
+
<NavButton icon={HelpCircle} label="What Else?" />
|
|
72
|
+
</nav>
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
{/* Bottom spacer */}
|
|
75
|
+
<div className="pt-4 border-t border-border/50" />
|
|
76
|
+
</aside>
|
|
77
|
+
);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function NavButton({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
icon: Icon,
|
|
82
|
+
label,
|
|
83
|
+
active,
|
|
84
84
|
}: {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
icon: React.ComponentType<{ className?: string }>;
|
|
86
|
+
label: string;
|
|
87
|
+
active?: boolean;
|
|
88
88
|
}) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
return (
|
|
90
|
+
<button
|
|
91
|
+
className={cn(
|
|
92
|
+
'flex items-center gap-3 w-full px-3 py-2.5 rounded-lg text-sm transition-colors',
|
|
93
|
+
active
|
|
94
|
+
? 'bg-sidebar-accent text-foreground font-medium'
|
|
95
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50',
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<Icon className="h-[18px] w-[18px]" />
|
|
99
|
+
{label}
|
|
100
|
+
</button>
|
|
101
|
+
);
|
|
102
102
|
}
|