@nextclaw/ui 0.2.1

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.
Files changed (48) hide show
  1. package/.eslintrc.cjs +28 -0
  2. package/CHANGELOG.md +7 -0
  3. package/dist/assets/index-BrN4G7FO.js +240 -0
  4. package/dist/assets/index-VjHB2nG6.css +1 -0
  5. package/dist/index.html +14 -0
  6. package/index.html +13 -0
  7. package/package.json +50 -0
  8. package/postcss.config.js +6 -0
  9. package/src/App.tsx +51 -0
  10. package/src/api/client.ts +40 -0
  11. package/src/api/config.ts +86 -0
  12. package/src/api/types.ts +78 -0
  13. package/src/api/websocket.ts +77 -0
  14. package/src/components/common/KeyValueEditor.tsx +65 -0
  15. package/src/components/common/MaskedInput.tsx +39 -0
  16. package/src/components/common/StatusBadge.tsx +56 -0
  17. package/src/components/common/TagInput.tsx +56 -0
  18. package/src/components/config/ChannelForm.tsx +259 -0
  19. package/src/components/config/ChannelsList.tsx +102 -0
  20. package/src/components/config/ModelConfig.tsx +181 -0
  21. package/src/components/config/ProviderForm.tsx +147 -0
  22. package/src/components/config/ProvidersList.tsx +90 -0
  23. package/src/components/config/UiConfig.tsx +189 -0
  24. package/src/components/layout/AppLayout.tsx +20 -0
  25. package/src/components/layout/Header.tsx +36 -0
  26. package/src/components/layout/Sidebar.tsx +103 -0
  27. package/src/components/ui/HighlightCard.tsx +40 -0
  28. package/src/components/ui/button.tsx +50 -0
  29. package/src/components/ui/card.tsx +78 -0
  30. package/src/components/ui/dialog.tsx +120 -0
  31. package/src/components/ui/input.tsx +23 -0
  32. package/src/components/ui/label.tsx +20 -0
  33. package/src/components/ui/scroll-area.tsx +21 -0
  34. package/src/components/ui/skeleton.tsx +15 -0
  35. package/src/components/ui/switch.tsx +37 -0
  36. package/src/components/ui/tabs-custom.tsx +45 -0
  37. package/src/components/ui/tabs.tsx +88 -0
  38. package/src/hooks/useConfig.ts +95 -0
  39. package/src/hooks/useWebSocket.ts +38 -0
  40. package/src/index.css +177 -0
  41. package/src/lib/i18n.ts +119 -0
  42. package/src/lib/utils.ts +6 -0
  43. package/src/main.tsx +10 -0
  44. package/src/stores/ui.store.ts +39 -0
  45. package/src/vite-env.d.ts +9 -0
  46. package/tailwind.config.js +43 -0
  47. package/tsconfig.json +18 -0
  48. package/vite.config.ts +25 -0
@@ -0,0 +1,147 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useConfig, useConfigMeta, useUpdateProvider } from '@/hooks/useConfig';
3
+ import { useUiStore } from '@/stores/ui.store';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ } from '@/components/ui/dialog';
12
+ import { Button } from '@/components/ui/button';
13
+ import { Input } from '@/components/ui/input';
14
+ import { Label } from '@/components/ui/label';
15
+ import { MaskedInput } from '@/components/common/MaskedInput';
16
+ import { KeyValueEditor } from '@/components/common/KeyValueEditor';
17
+ import { t } from '@/lib/i18n';
18
+ import type { ProviderConfigUpdate } from '@/api/types';
19
+ import { KeyRound, Globe, Hash } from 'lucide-react';
20
+
21
+ export function ProviderForm() {
22
+ const { providerModal, closeProviderModal } = useUiStore();
23
+ const { data: config } = useConfig();
24
+ const { data: meta } = useConfigMeta();
25
+ const updateProvider = useUpdateProvider();
26
+
27
+ const [apiKey, setApiKey] = useState('');
28
+ const [apiBase, setApiBase] = useState('');
29
+ const [extraHeaders, setExtraHeaders] = useState<Record<string, string> | null>(null);
30
+
31
+ const providerName = providerModal.provider;
32
+ const providerSpec = meta?.providers.find((p) => p.name === providerName);
33
+ const providerConfig = providerName ? config?.providers[providerName] : null;
34
+
35
+ useEffect(() => {
36
+ if (providerConfig) {
37
+ setApiBase(providerConfig.apiBase || providerSpec?.defaultApiBase || '');
38
+ setExtraHeaders(providerConfig.extraHeaders || null);
39
+ setApiKey(''); // Always start with empty for security
40
+ }
41
+ }, [providerConfig, providerSpec]);
42
+
43
+ const handleSubmit = (e: React.FormEvent) => {
44
+ e.preventDefault();
45
+
46
+ const payload: ProviderConfigUpdate = {};
47
+
48
+ // Only include apiKey if user has entered something
49
+ if (apiKey !== '') {
50
+ payload.apiKey = apiKey;
51
+ }
52
+
53
+ if (apiBase && apiBase !== providerSpec?.defaultApiBase) {
54
+ payload.apiBase = apiBase;
55
+ }
56
+
57
+ if (extraHeaders && Object.keys(extraHeaders).length > 0) {
58
+ payload.extraHeaders = extraHeaders;
59
+ }
60
+
61
+ if (!providerName) return;
62
+
63
+ updateProvider.mutate(
64
+ { provider: providerName, data: payload },
65
+ { onSuccess: () => closeProviderModal() }
66
+ );
67
+ };
68
+
69
+ return (
70
+ <Dialog open={providerModal.open} onOpenChange={closeProviderModal}>
71
+ <DialogContent className="sm:max-w-[500px]">
72
+ <DialogHeader>
73
+ <div className="flex items-center gap-3">
74
+ <div className="h-10 w-10 rounded-xl bg-gradient-to-br from-orange-400 to-amber-500 flex items-center justify-center">
75
+ <KeyRound className="h-5 w-5 text-white" />
76
+ </div>
77
+ <div>
78
+ <DialogTitle>{providerSpec?.displayName || providerName}</DialogTitle>
79
+ <DialogDescription>Configure API keys and parameters for AI provider</DialogDescription>
80
+ </div>
81
+ </div>
82
+ </DialogHeader>
83
+
84
+ <form onSubmit={handleSubmit} className="space-y-5 pt-2">
85
+ <div className="space-y-2.5">
86
+ <Label htmlFor="apiKey" className="text-sm font-medium text-[hsl(30,20%,12%)] flex items-center gap-2">
87
+ <KeyRound className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />
88
+ {t('apiKey')}
89
+ </Label>
90
+ <MaskedInput
91
+ id="apiKey"
92
+ value={apiKey}
93
+ isSet={providerConfig?.apiKeySet}
94
+ onChange={(e) => setApiKey(e.target.value)}
95
+ placeholder={providerConfig?.apiKeySet ? t('apiKeySet') : 'Enter API Key'}
96
+ className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
97
+ />
98
+ </div>
99
+
100
+ <div className="space-y-2.5">
101
+ <Label htmlFor="apiBase" className="text-sm font-medium text-[hsl(30,20%,12%)] flex items-center gap-2">
102
+ <Globe className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />
103
+ {t('apiBase')}
104
+ </Label>
105
+ <Input
106
+ id="apiBase"
107
+ type="text"
108
+ value={apiBase}
109
+ onChange={(e) => setApiBase(e.target.value)}
110
+ placeholder={providerSpec?.defaultApiBase || 'https://api.example.com'}
111
+ className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
112
+ />
113
+ </div>
114
+
115
+ <div className="space-y-2.5">
116
+ <Label className="text-sm font-medium text-[hsl(30,20%,12%)] flex items-center gap-2">
117
+ <Hash className="h-3.5 w-3.5 text-[hsl(30,8%,45%)]" />
118
+ {t('extraHeaders')}
119
+ </Label>
120
+ <KeyValueEditor
121
+ value={extraHeaders}
122
+ onChange={setExtraHeaders}
123
+ />
124
+ </div>
125
+
126
+ <DialogFooter className="pt-4">
127
+ <Button
128
+ type="button"
129
+ variant="outline"
130
+ onClick={closeProviderModal}
131
+ className="rounded-xl border-[hsl(40,20%,90%)] bg-white hover:bg-[hsl(40,20%,96%)]"
132
+ >
133
+ {t('cancel')}
134
+ </Button>
135
+ <Button
136
+ type="submit"
137
+ disabled={updateProvider.isPending}
138
+ className="rounded-xl bg-gradient-to-r from-orange-400 to-amber-500 hover:from-orange-500 hover:to-amber-600 text-white border-0"
139
+ >
140
+ {updateProvider.isPending ? 'Saving...' : t('save')}
141
+ </Button>
142
+ </DialogFooter>
143
+ </form>
144
+ </DialogContent>
145
+ </Dialog>
146
+ );
147
+ }
@@ -0,0 +1,90 @@
1
+ import { useConfig, useConfigMeta } from '@/hooks/useConfig';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Skeleton } from '@/components/ui/skeleton';
4
+ import { KeyRound, Lock, Check, Plus, MoreHorizontal } from 'lucide-react';
5
+ import { useState } from 'react';
6
+ import { ProviderForm } from './ProviderForm';
7
+ import { useUiStore } from '@/stores/ui.store';
8
+ import { cn } from '@/lib/utils';
9
+ import { Tabs } from '@/components/ui/tabs-custom';
10
+ import { HighlightCard } from '@/components/ui/HighlightCard';
11
+
12
+ export function ProvidersList() {
13
+ const { data: config } = useConfig();
14
+ const { data: meta } = useConfigMeta();
15
+ const { openProviderModal } = useUiStore();
16
+ const [activeTab, setActiveTab] = useState('featured');
17
+
18
+ if (!config || !meta) {
19
+ return <div className="p-8">Loading...</div>; // Skeleton optimization can follow
20
+ }
21
+
22
+ const tabs = [
23
+ { id: 'installed', label: 'Configured', count: config.providers ? Object.keys(config.providers).filter(k => config.providers[k].apiKeySet).length : 0 },
24
+ { id: 'all', label: 'All Providers' }
25
+ ];
26
+
27
+ return (
28
+ <div className="animate-fade-in pb-20">
29
+ <div className="flex items-center justify-between mb-8">
30
+ <h2 className="text-2xl font-bold text-[hsl(30,15%,10%)]">AI Providers</h2>
31
+ </div>
32
+
33
+ {/* Tabs */}
34
+ <Tabs tabs={tabs} activeTab={activeTab === 'featured' ? 'all' : activeTab} onChange={setActiveTab} />
35
+
36
+ {/* Provider List Row-Style */}
37
+ <div className="space-y-1">
38
+ {(activeTab === 'installed'
39
+ ? meta.providers.filter((p) => config.providers[p.name]?.apiKeySet)
40
+ : meta.providers
41
+ ).map((provider) => {
42
+ const providerConfig = config.providers[provider.name];
43
+ const hasConfig = providerConfig?.apiKeySet;
44
+
45
+ return (
46
+ <div
47
+ key={provider.name}
48
+ className="group flex items-center gap-5 p-3 rounded-2xl hover:bg-[hsl(40,10%,96%)] transition-all cursor-pointer border border-transparent hover:border-[hsl(40,10%,94%)]"
49
+ onClick={() => openProviderModal(provider.name)}
50
+ >
51
+ {/* Logo/Icon */}
52
+ <div className="h-10 w-10 flex items-center justify-center bg-transparent border border-[hsl(40,10%,92%)] rounded-xl group-hover:scale-105 transition-transform overflow-hidden">
53
+ <span className="text-xl font-bold uppercase text-[hsl(30,15%,10%)]">{provider.name[0]}</span>
54
+ </div>
55
+
56
+ {/* Info */}
57
+ <div className="flex-1 min-w-0">
58
+ <h3 className="text-[14px] font-bold text-[hsl(30,15%,10%)] truncate">
59
+ {provider.displayName || provider.name}
60
+ </h3>
61
+ <p className="text-[12px] text-[hsl(30,8%,55%)] truncate leading-tight">
62
+ {provider.name === 'openai' ? 'TypeScript authentication framework integration guide' : 'Configure AI services for your agents'}
63
+ </p>
64
+ </div>
65
+
66
+ {/* Status/Actions */}
67
+ <div className="flex items-center gap-4">
68
+ {hasConfig ? (
69
+ <div className="flex items-center gap-1.5 text-emerald-600">
70
+ <Check className="h-4 w-4" />
71
+ <span className="text-[11px] font-bold">Ready</span>
72
+ </div>
73
+ ) : (
74
+ <button className="h-8 w-8 flex items-center justify-center text-[hsl(30,8%,45%)] hover:text-[hsl(30,15%,10%)] group-hover:opacity-100 opacity-0 transition-opacity">
75
+ <Plus className="h-4 w-4" />
76
+ </button>
77
+ )}
78
+ <button className="h-8 w-8 flex items-center justify-center text-[hsl(30,8%,45%)]">
79
+ <MoreHorizontal className="h-4 w-4" />
80
+ </button>
81
+ </div>
82
+ </div>
83
+ );
84
+ })}
85
+ </div>
86
+
87
+ <ProviderForm />
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,189 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useConfig, useUpdateUiConfig, useReloadConfig } from '@/hooks/useConfig';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Input } from '@/components/ui/input';
5
+ import { Label } from '@/components/ui/label';
6
+ import { Switch } from '@/components/ui/switch';
7
+ import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
8
+ import { Skeleton } from '@/components/ui/skeleton';
9
+ import { RefreshCw, Save, Monitor, Power } from 'lucide-react';
10
+ import { cn } from '@/lib/utils';
11
+ import { t } from '@/lib/i18n';
12
+
13
+ export function UiConfig() {
14
+ const { data: config, isLoading } = useConfig();
15
+ const updateUiConfig = useUpdateUiConfig();
16
+ const reloadConfig = useReloadConfig();
17
+
18
+ const [enabled, setEnabled] = useState(false);
19
+ const [host, setHost] = useState('127.0.0.1');
20
+ const [port, setPort] = useState(18791);
21
+
22
+ useEffect(() => {
23
+ if (config?.ui) {
24
+ setEnabled(config.ui.enabled);
25
+ setHost(config.ui.host);
26
+ setPort(config.ui.port);
27
+ }
28
+ }, [config]);
29
+
30
+ const handleSubmit = (e: React.FormEvent) => {
31
+ e.preventDefault();
32
+ updateUiConfig.mutate({ enabled, host, port, open: true });
33
+ };
34
+
35
+ const handleReload = () => {
36
+ reloadConfig.mutate();
37
+ };
38
+
39
+ if (isLoading) {
40
+ return (
41
+ <div className="max-w-2xl space-y-6">
42
+ <div className="space-y-2">
43
+ <Skeleton className="h-8 w-32" />
44
+ <Skeleton className="h-4 w-48" />
45
+ </div>
46
+ <Card className="rounded-2xl border-[hsl(40,20%,90%)] p-6">
47
+ <div className="flex items-center gap-4 mb-6">
48
+ <Skeleton className="h-12 w-12 rounded-xl" />
49
+ <div className="space-y-2">
50
+ <Skeleton className="h-5 w-28" />
51
+ <Skeleton className="h-3 w-36" />
52
+ </div>
53
+ </div>
54
+ <Skeleton className="h-16 w-full rounded-xl mb-4" />
55
+ <div className="grid grid-cols-2 gap-4">
56
+ <div>
57
+ <Skeleton className="h-4 w-16 mb-2" />
58
+ <Skeleton className="h-10 w-full rounded-xl" />
59
+ </div>
60
+ <div>
61
+ <Skeleton className="h-4 w-12 mb-2" />
62
+ <Skeleton className="h-10 w-full rounded-xl" />
63
+ </div>
64
+ </div>
65
+ </Card>
66
+ </div>
67
+ );
68
+ }
69
+
70
+ return (
71
+ <div className="max-w-2xl space-y-6">
72
+ {/* Header */}
73
+ <div>
74
+ <h2 className="text-xl font-semibold text-[hsl(30,20%,12%)]">{t('uiConfig')}</h2>
75
+ <p className="text-sm text-[hsl(30,8%,45%)] mt-1">Configure Web UI server and access options</p>
76
+ </div>
77
+
78
+ <Card className="rounded-2xl border-[hsl(40,20%,90%)] bg-white">
79
+ <CardContent className="p-6">
80
+ <div className="flex items-center gap-4 mb-6">
81
+ <div className="h-12 w-12 rounded-xl bg-gradient-to-br from-purple-400 to-indigo-500 flex items-center justify-center">
82
+ <Monitor className="h-5 w-5 text-white" />
83
+ </div>
84
+ <div>
85
+ <CardTitle className="text-base font-semibold text-[hsl(30,20%,12%)]">Web UI Server</CardTitle>
86
+ <CardDescription className="text-xs text-[hsl(30,8%,45%)]">Configure server runtime parameters</CardDescription>
87
+ </div>
88
+ </div>
89
+
90
+ <form onSubmit={handleSubmit} className="space-y-6">
91
+ <div className={cn(
92
+ "flex items-center justify-between p-4 rounded-xl transition-colors",
93
+ enabled ? "bg-emerald-50" : "bg-[hsl(40,20%,96%)]"
94
+ )}>
95
+ <div className="flex items-center gap-3">
96
+ <div className={cn(
97
+ "h-10 w-10 rounded-lg flex items-center justify-center",
98
+ enabled ? "bg-emerald-100 text-emerald-600" : "bg-[hsl(40,20%,94%)] text-[hsl(30,8%,45%)]"
99
+ )}>
100
+ <Power className="h-5 w-5" />
101
+ </div>
102
+ <div>
103
+ <h3 className="font-medium text-[hsl(30,20%,12%)]">Enable Web UI</h3>
104
+ <p className={cn(
105
+ "text-xs",
106
+ enabled ? "text-emerald-600" : "text-[hsl(30,8%,45%)]"
107
+ )}>
108
+ {enabled ? t('connected') : t('disconnected')}
109
+ </p>
110
+ </div>
111
+ </div>
112
+ <Switch
113
+ id="enabled"
114
+ checked={enabled}
115
+ onCheckedChange={setEnabled}
116
+ className="data-[state=checked]:bg-emerald-500"
117
+ />
118
+ </div>
119
+
120
+ <div className="grid grid-cols-2 gap-4">
121
+ <div className="space-y-2">
122
+ <Label htmlFor="host" className="text-sm font-medium text-[hsl(30,20%,12%)]">{t('host')}</Label>
123
+ <Input
124
+ id="host"
125
+ type="text"
126
+ value={host}
127
+ onChange={(e) => setHost(e.target.value)}
128
+ placeholder="127.0.0.1"
129
+ className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
130
+ />
131
+ </div>
132
+
133
+ <div className="space-y-2">
134
+ <Label htmlFor="port" className="text-sm font-medium text-[hsl(30,20%,12%)]">{t('port')}</Label>
135
+ <Input
136
+ id="port"
137
+ type="number"
138
+ value={port}
139
+ onChange={(e) => setPort(parseInt(e.target.value) || 18791)}
140
+ placeholder="18791"
141
+ min="1"
142
+ max="65535"
143
+ className="rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] focus:bg-white"
144
+ />
145
+ </div>
146
+ </div>
147
+
148
+ <div className="flex justify-end">
149
+ <Button
150
+ type="submit"
151
+ disabled={updateUiConfig.isPending}
152
+ className="gap-2 rounded-xl bg-gradient-to-r from-orange-400 to-amber-500 hover:from-orange-500 hover:to-amber-600 text-white border-0"
153
+ >
154
+ <Save className="h-4 w-4" />
155
+ {t('save')}
156
+ </Button>
157
+ </div>
158
+ </form>
159
+ </CardContent>
160
+ </Card>
161
+
162
+ <Card className="rounded-2xl border-[hsl(40,20%,90%)] bg-white">
163
+ <CardContent className="p-6">
164
+ <div className="flex items-center gap-4 mb-6">
165
+ <div className="h-12 w-12 rounded-xl bg-gradient-to-br from-slate-400 to-gray-500 flex items-center justify-center">
166
+ <RefreshCw className="h-5 w-5 text-white" />
167
+ </div>
168
+ <div>
169
+ <CardTitle className="text-base font-semibold text-[hsl(30,20%,12%)]">{t('reloadConfig')}</CardTitle>
170
+ <CardDescription className="text-xs text-[hsl(30,8%,45%)]">Apply changes and restart services</CardDescription>
171
+ </div>
172
+ </div>
173
+ <p className="text-sm text-[hsl(30,8%,45%)] mb-4">
174
+ Click the button below to reload the configuration file and apply all changes.
175
+ </p>
176
+ <Button
177
+ variant="outline"
178
+ onClick={handleReload}
179
+ disabled={reloadConfig.isPending}
180
+ className="w-full gap-2 rounded-xl border-[hsl(40,20%,90%)] bg-[hsl(40,20%,98%)] hover:bg-[hsl(40,20%,94%)] text-[hsl(30,10%,35%)]"
181
+ >
182
+ <RefreshCw className="h-4 w-4" />
183
+ {t('reloadConfig')}
184
+ </Button>
185
+ </CardContent>
186
+ </Card>
187
+ </div>
188
+ );
189
+ }
@@ -0,0 +1,20 @@
1
+ import { Sidebar } from './Sidebar';
2
+
3
+ interface AppLayoutProps {
4
+ children: React.ReactNode;
5
+ }
6
+
7
+ export function AppLayout({ children }: AppLayoutProps) {
8
+ return (
9
+ <div className="h-screen flex bg-[hsl(40,20%,98%)] p-2">
10
+ <Sidebar />
11
+ <div className="flex-1 flex flex-col min-w-0 bg-white rounded-[2rem] shadow-sm overflow-hidden border border-[hsl(40,10%,94%)]">
12
+ <main className="flex-1 overflow-auto custom-scrollbar p-8">
13
+ <div className="max-w-6xl mx-auto animate-fade-in h-full">
14
+ {children}
15
+ </div>
16
+ </main>
17
+ </div>
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,36 @@
1
+ import { Bell, Search } from 'lucide-react';
2
+
3
+ interface HeaderProps {
4
+ title?: string;
5
+ description?: string;
6
+ }
7
+
8
+ export function Header({ title, description }: HeaderProps) {
9
+ return (
10
+ <header className="h-16 bg-white/80 backdrop-blur-md sticky top-0 z-10 border-b border-[hsl(40,20%,90%)] flex items-center justify-between px-6 transition-all duration-300">
11
+ <div className="flex items-center gap-4">
12
+ {title && (
13
+ <div>
14
+ <h2 className="text-base font-semibold text-[hsl(30,20%,12%)]">{title}</h2>
15
+ {description && (
16
+ <p className="text-xs text-[hsl(30,8%,45%)]">{description}</p>
17
+ )}
18
+ </div>
19
+ )}
20
+ </div>
21
+
22
+ <div className="flex items-center gap-3">
23
+ <button className="h-9 w-9 rounded-lg bg-[hsl(40,20%,96%)] flex items-center justify-center text-[hsl(30,8%,45%)] hover:bg-[hsl(40,20%,94%)] hover:text-[hsl(30,20%,12%)] transition-colors">
24
+ <Search className="h-4 w-4" />
25
+ </button>
26
+ <button className="h-9 w-9 rounded-lg bg-[hsl(40,20%,96%)] flex items-center justify-center text-[hsl(30,8%,45%)] hover:bg-[hsl(40,20%,94%)] hover:text-[hsl(30,20%,12%)] transition-colors relative">
27
+ <Bell className="h-4 w-4" />
28
+ <span className="absolute top-1.5 right-1.5 h-2 w-2 rounded-full bg-orange-500" />
29
+ </button>
30
+ <div className="h-9 w-9 rounded-lg bg-gradient-to-br from-orange-400 to-amber-500 flex items-center justify-center">
31
+ <span className="text-xs font-semibold text-white">N</span>
32
+ </div>
33
+ </div>
34
+ </header>
35
+ );
36
+ }
@@ -0,0 +1,103 @@
1
+ import { useUiStore } from '@/stores/ui.store';
2
+ import { cn } from '@/lib/utils';
3
+ import {
4
+ Cpu,
5
+ MessageSquare,
6
+ Sparkles,
7
+ ChevronRight,
8
+ Settings
9
+ } from 'lucide-react';
10
+
11
+ const navItems = [
12
+ {
13
+ id: 'model' as const,
14
+ label: 'Models',
15
+ icon: Cpu,
16
+ color: 'text-[hsl(30,15%,10%)]'
17
+ },
18
+ {
19
+ id: 'providers' as const,
20
+ label: 'Providers',
21
+ icon: Sparkles,
22
+ color: 'text-[hsl(30,15%,10%)]'
23
+ },
24
+ {
25
+ id: 'channels' as const,
26
+ label: 'Channels',
27
+ icon: MessageSquare,
28
+ color: 'text-[hsl(30,15%,10%)]'
29
+ },
30
+ {
31
+ id: 'ui' as const,
32
+ label: 'Appearance',
33
+ icon: Settings,
34
+ color: 'text-[hsl(30,15%,10%)]'
35
+ }
36
+ ];
37
+
38
+ export function Sidebar() {
39
+ const { activeTab, setActiveTab } = useUiStore();
40
+
41
+ return (
42
+ <aside className="w-[240px] bg-transparent flex flex-col h-full py-6 px-4">
43
+ {/* Logo Area */}
44
+ <div className="px-3 mb-8">
45
+ <div className="flex items-center gap-2 group cursor-pointer">
46
+ <div className="h-6 w-6 rounded-md bg-[hsl(30,15%,10%)] flex items-center justify-center transition-transform group-hover:scale-110">
47
+ <Sparkles className="h-4 w-4 text-white" />
48
+ </div>
49
+ <h1 className="text-lg font-bold text-[hsl(30,15%,10%)] tracking-tight">nextclaw</h1>
50
+ </div>
51
+ </div>
52
+
53
+ {/* Navigation */}
54
+ <nav className="flex-1">
55
+ <ul className="space-y-1">
56
+ {navItems.map((item) => {
57
+ const Icon = item.icon;
58
+ const isActive = activeTab === item.id;
59
+
60
+ return (
61
+ <li key={item.id}>
62
+ <button
63
+ onClick={() => setActiveTab(item.id)}
64
+ className={cn(
65
+ 'group w-full flex items-center gap-3 px-3 py-2 rounded-lg text-[13px] font-medium transition-all duration-200',
66
+ isActive
67
+ ? 'bg-[hsl(40,10%,92%)] text-[hsl(30,15%,10%)]'
68
+ : 'text-[hsl(30,8%,45%)] hover:bg-[hsl(40,10%,94%)] hover:text-[hsl(30,15%,10%)]'
69
+ )}
70
+ >
71
+ <Icon className={cn('h-4 w-4 transition-transform group-hover:scale-110', isActive ? 'text-[hsl(30,15%,10%)]' : 'text-[hsl(30,8%,45%)]')} />
72
+ <span className="flex-1 text-left">{item.label}</span>
73
+ </button>
74
+ </li>
75
+ );
76
+ })}
77
+ </ul>
78
+
79
+ </nav>
80
+
81
+ {/* Bottom Profile Section */}
82
+ <div className="mt-auto px-1 pt-4">
83
+ <div className="flex items-center gap-2 mb-4 px-2">
84
+ <div className="flex gap-1.5 items-center">
85
+ <div className="w-2 h-2 rounded-full bg-amber-400" />
86
+ <span className="text-[11px] text-[hsl(30,8%,65%)]">Starting...</span>
87
+ </div>
88
+ </div>
89
+
90
+ <button className="w-full flex items-center gap-3 p-2 rounded-xl hover:bg-[hsl(40,10%,94%)] transition-all group">
91
+ <div className="h-8 w-8 rounded-full bg-gradient-to-br from-amber-400 to-orange-500 flex items-center justify-center text-white text-xs font-bold shadow-sm group-hover:scale-105 transition-transform">
92
+ WX
93
+ </div>
94
+ <div className="flex-1 text-left min-w-0">
95
+ <p className="text-[13px] font-semibold text-[hsl(30,15%,10%)] truncate">Wang Xiaotiao</p>
96
+ <p className="text-[11px] text-[hsl(30,8%,55%)] truncate">Free plan</p>
97
+ </div>
98
+ <ChevronRight className="h-4 w-4 text-[hsl(30,8%,65%)] group-hover:translate-x-0.5 transition-transform" />
99
+ </button>
100
+ </div>
101
+ </aside>
102
+ );
103
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { cn } from '@/lib/utils';
3
+ import { ArrowRight } from 'lucide-react';
4
+
5
+ interface HighlightCardProps {
6
+ category: string;
7
+ title: string;
8
+ description: string;
9
+ image: string;
10
+ gradient: string;
11
+ className?: string;
12
+ }
13
+
14
+ export function HighlightCard({ category, title, description, image, gradient, className }: HighlightCardProps) {
15
+ return (
16
+ <div className={cn(
17
+ 'group relative overflow-hidden rounded-[1.5rem] bg-white border border-[hsl(40,10%,94%)] flex h-[180px] transition-all duration-300 hover:shadow-premium cursor-pointer',
18
+ className
19
+ )}>
20
+ <div className="flex-1 p-6 flex flex-col">
21
+ <span className="text-[10px] font-bold text-[hsl(30,8%,65%)] uppercase tracking-widest mb-2">{category}</span>
22
+ <h3 className="text-xl font-bold text-[hsl(30,15%,10%)] leading-tight mb-2 group-hover:text-amber-600 transition-colors">{title}</h3>
23
+ <p className="text-[13px] text-[hsl(30,8%,55%)] line-clamp-2 leading-relaxed max-w-[200px]">{description}</p>
24
+
25
+ <div className="mt-auto flex items-center gap-1 text-[11px] font-bold text-[hsl(30,15%,10%)] opacity-0 group-hover:opacity-100 transition-opacity">
26
+ Learn more <ArrowRight className="h-3 w-3" />
27
+ </div>
28
+ </div>
29
+
30
+ <div className={cn('w-[160px] relative overflow-hidden', gradient)}>
31
+ <img
32
+ src={image}
33
+ alt={title}
34
+ className="w-full h-full object-cover mix-blend-multiply opacity-90 group-hover:scale-110 transition-transform duration-500"
35
+ />
36
+ <div className="absolute inset-0 bg-gradient-to-l from-transparent to-white/10" />
37
+ </div>
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from '@/lib/utils';
4
+
5
+ const buttonVariants = cva(
6
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-200 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
11
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
12
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
13
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
14
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
15
+ link: 'text-primary underline-offset-4 hover:underline'
16
+ },
17
+ size: {
18
+ default: 'h-10 px-4 py-2',
19
+ sm: 'h-9 rounded-md px-3',
20
+ lg: 'h-11 rounded-md px-8',
21
+ icon: 'h-10 w-10'
22
+ }
23
+ },
24
+ defaultVariants: {
25
+ variant: 'default',
26
+ size: 'default'
27
+ }
28
+ }
29
+ );
30
+
31
+ export interface ButtonProps
32
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
33
+ VariantProps<typeof buttonVariants> {
34
+ asChild?: boolean;
35
+ }
36
+
37
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
38
+ ({ className, variant, size, ...props }, ref) => {
39
+ return (
40
+ <button
41
+ className={cn(buttonVariants({ variant, size, className }))}
42
+ ref={ref}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+ );
48
+ Button.displayName = 'Button';
49
+
50
+ export { Button, buttonVariants };