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.
Files changed (106) hide show
  1. package/README.md +53 -84
  2. package/bin/cli.js +1717 -6
  3. package/components.json +18 -18
  4. package/package.json +98 -109
  5. package/postcss.config.js +3 -3
  6. package/scripts/install.ps1 +1 -1
  7. package/scripts/install.sh +1 -1
  8. package/scripts/postinstall.js +47 -58
  9. package/shared/ai.ts +118 -202
  10. package/shared/config.ts +37 -39
  11. package/shared/logger.ts +6 -14
  12. package/shared/paths.ts +10 -16
  13. package/shared/relay.ts +89 -98
  14. package/supervisor/backend.ts +77 -89
  15. package/supervisor/chat/ARCHITECTURE.md +13 -14
  16. package/supervisor/chat/OnboardWizard.tsx +2194 -3419
  17. package/supervisor/chat/fluxy-main.tsx +495 -633
  18. package/supervisor/chat/fluxy.html +24 -36
  19. package/supervisor/chat/onboard-main.tsx +8 -8
  20. package/supervisor/chat/onboard.html +10 -13
  21. package/supervisor/chat/src/components/Chat/AudioBubble.tsx +94 -108
  22. package/supervisor/chat/src/components/Chat/ChatView.tsx +14 -36
  23. package/supervisor/chat/src/components/Chat/ImageLightbox.tsx +71 -89
  24. package/supervisor/chat/src/components/Chat/InputBar.tsx +498 -728
  25. package/supervisor/chat/src/components/Chat/MessageBubble.tsx +154 -246
  26. package/supervisor/chat/src/components/Chat/MessageList.tsx +114 -138
  27. package/supervisor/chat/src/components/Chat/TypingIndicator.tsx +38 -50
  28. package/supervisor/chat/src/components/LoginScreen.tsx +245 -296
  29. package/supervisor/chat/src/hooks/useChat.ts +210 -259
  30. package/supervisor/chat/src/hooks/useFluxyChat.ts +232 -322
  31. package/supervisor/chat/src/hooks/useSpeechRecognition.ts +126 -148
  32. package/supervisor/chat/src/lib/auth.ts +20 -23
  33. package/supervisor/chat/src/lib/ws-client.ts +113 -125
  34. package/supervisor/chat/src/styles/globals.css +76 -85
  35. package/supervisor/file-saver.ts +39 -52
  36. package/supervisor/fluxy-agent.ts +213 -292
  37. package/supervisor/index.ts +895 -1263
  38. package/supervisor/scheduler.ts +237 -299
  39. package/supervisor/tunnel.ts +113 -167
  40. package/supervisor/vite-dev.ts +51 -61
  41. package/supervisor/widget.js +135 -147
  42. package/supervisor/worker.ts +45 -49
  43. package/tsconfig.json +18 -24
  44. package/vite.config.ts +48 -55
  45. package/vite.fluxy.config.ts +34 -34
  46. package/worker/claude-auth.ts +202 -265
  47. package/worker/codex-auth.ts +144 -181
  48. package/worker/db.ts +63 -191
  49. package/worker/index.ts +601 -842
  50. package/workspace/MYSELF.md +1 -1
  51. package/workspace/PULSE.json +6 -6
  52. package/workspace/backend/index.ts +16 -19
  53. package/workspace/client/index.html +47 -60
  54. package/workspace/client/public/manifest.json +21 -21
  55. package/workspace/client/public/sw.js +29 -31
  56. package/workspace/client/src/App.tsx +91 -136
  57. package/workspace/client/src/components/Dashboard/DashboardPage.tsx +68 -72
  58. package/workspace/client/src/components/ErrorBoundary.tsx +11 -11
  59. package/workspace/client/src/components/Layout/DashboardLayout.tsx +35 -35
  60. package/workspace/client/src/components/Layout/Footer.tsx +12 -12
  61. package/workspace/client/src/components/Layout/MobileNav.tsx +23 -23
  62. package/workspace/client/src/components/Layout/Sidebar.tsx +81 -81
  63. package/workspace/client/src/components/ui/avatar.tsx +85 -85
  64. package/workspace/client/src/components/ui/badge.tsx +40 -41
  65. package/workspace/client/src/components/ui/button.tsx +56 -57
  66. package/workspace/client/src/components/ui/card.tsx +75 -75
  67. package/workspace/client/src/components/ui/dialog.tsx +114 -117
  68. package/workspace/client/src/components/ui/dropdown-menu.tsx +191 -199
  69. package/workspace/client/src/components/ui/input.tsx +17 -17
  70. package/workspace/client/src/components/ui/scroll-area.tsx +47 -47
  71. package/workspace/client/src/components/ui/select.tsx +145 -151
  72. package/workspace/client/src/components/ui/separator.tsx +21 -21
  73. package/workspace/client/src/components/ui/sheet.tsx +102 -102
  74. package/workspace/client/src/components/ui/skeleton.tsx +10 -10
  75. package/workspace/client/src/components/ui/switch.tsx +26 -26
  76. package/workspace/client/src/components/ui/tabs.tsx +69 -69
  77. package/workspace/client/src/components/ui/textarea.tsx +14 -14
  78. package/workspace/client/src/components/ui/tooltip.tsx +37 -37
  79. package/workspace/client/src/lib/utils.ts +1 -1
  80. package/workspace/client/src/main.tsx +3 -3
  81. package/workspace/client/src/styles/globals.css +76 -85
  82. package/workspace/skills/code-reviewer/.claude-plugin/plugin.json +3 -3
  83. package/workspace/skills/code-reviewer/skills/code-reviewer/SKILL.md +0 -6
  84. package/workspace/skills/daily-standup/.claude-plugin/plugin.json +3 -3
  85. package/workspace/skills/daily-standup/skills/daily-standup/SKILL.md +0 -7
  86. package/workspace/skills/workspace-helper/.claude-plugin/plugin.json +3 -3
  87. package/workspace/skills/workspace-helper/skills/workspace-helper/SKILL.md +0 -2
  88. package/bin/cli.backup.js +0 -2138
  89. package/cli/commands/daemon.ts +0 -42
  90. package/cli/commands/init.ts +0 -32
  91. package/cli/commands/start.ts +0 -113
  92. package/cli/commands/tunnel.temp.ts +0 -206
  93. package/cli/commands/tunnel.ts +0 -227
  94. package/cli/commands/update.ts +0 -163
  95. package/cli/core/base-adapter.ts +0 -156
  96. package/cli/core/cloudflared.ts +0 -113
  97. package/cli/core/config.ts +0 -73
  98. package/cli/core/os-detector.ts +0 -43
  99. package/cli/core/server.ts +0 -109
  100. package/cli/core/types.ts +0 -15
  101. package/cli/index.ts +0 -72
  102. package/cli/platforms/darwin.ts +0 -146
  103. package/cli/platforms/index.ts +0 -21
  104. package/cli/platforms/linux.ts +0 -156
  105. package/cli/platforms/win32.ts +0 -34
  106. package/cli/utils/ui.ts +0 -37
@@ -1,331 +1,280 @@
1
1
  import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
2
- import {
3
- Lock,
4
- LoaderCircle,
5
- ArrowRight,
6
- ArrowLeft,
7
- Shield,
8
- Check
9
- } from 'lucide-react';
2
+ import { Lock, LoaderCircle, ArrowRight, ArrowLeft, Shield, Check } from 'lucide-react';
10
3
  import { motion, AnimatePresence } from 'framer-motion';
11
4
 
12
5
  const LOGIN_STORAGE_KEY = 'fluxy_login_totp';
13
6
 
14
7
  interface Props {
15
- onLogin: (token: string) => void;
16
- totpEnabled?: boolean;
8
+ onLogin: (token: string) => void;
9
+ totpEnabled?: boolean;
17
10
  }
18
11
 
19
- export default function LoginScreen({ onLogin }: Props) {
20
- const [password, setPassword] = useState('');
21
- const [error, setError] = useState('');
22
- const [loading, setLoading] = useState(false);
12
+ export default function LoginScreen({ onLogin, totpEnabled }: Props) {
13
+ const [password, setPassword] = useState('');
14
+ const [error, setError] = useState('');
15
+ const [loading, setLoading] = useState(false);
23
16
 
24
- // TOTP state
25
- const [phase, setPhase] = useState<'password' | 'totp'>('password');
26
- const [totpCode, setTotpCode] = useState('');
27
- const [pendingToken, setPendingToken] = useState('');
28
- const [trustDevice, setTrustDevice] = useState(true);
29
- const [useRecovery, setUseRecovery] = useState(false);
30
- const totpInputRef = useRef<HTMLInputElement>(null);
17
+ // TOTP state
18
+ const [phase, setPhase] = useState<'password' | 'totp'>('password');
19
+ const [totpCode, setTotpCode] = useState('');
20
+ const [pendingToken, setPendingToken] = useState('');
21
+ const [trustDevice, setTrustDevice] = useState(true);
22
+ const [useRecovery, setUseRecovery] = useState(false);
23
+ const totpInputRef = useRef<HTMLInputElement>(null);
31
24
 
32
- // Restore TOTP phase after page reload (mobile: OS suspends PWA on app-switch)
33
- useEffect(() => {
34
- try {
35
- const raw = sessionStorage.getItem(LOGIN_STORAGE_KEY);
36
- if (!raw) return;
37
- const saved = JSON.parse(raw);
38
- if (saved.pendingToken && saved.expiresAt > Date.now()) {
39
- setPendingToken(saved.pendingToken);
40
- setPhase('totp');
41
- } else {
42
- sessionStorage.removeItem(LOGIN_STORAGE_KEY);
43
- }
44
- } catch {}
45
- }, []);
25
+ // Restore TOTP phase after page reload (mobile: OS suspends PWA on app-switch)
26
+ useEffect(() => {
27
+ try {
28
+ const raw = sessionStorage.getItem(LOGIN_STORAGE_KEY);
29
+ if (!raw) return;
30
+ const saved = JSON.parse(raw);
31
+ if (saved.pendingToken && saved.expiresAt > Date.now()) {
32
+ setPendingToken(saved.pendingToken);
33
+ setPhase('totp');
34
+ } else {
35
+ sessionStorage.removeItem(LOGIN_STORAGE_KEY);
36
+ }
37
+ } catch {}
38
+ }, []);
46
39
 
47
- // Auto-focus TOTP input when switching to TOTP phase
48
- useEffect(() => {
49
- if (phase === 'totp') {
50
- setTimeout(() => totpInputRef.current?.focus(), 100);
51
- }
52
- }, [phase]);
53
-
54
- function saveLoginState(token: string) {
55
- try {
56
- // Pending token has 5min server expiry — save with matching client expiry
57
- sessionStorage.setItem(
58
- LOGIN_STORAGE_KEY,
59
- JSON.stringify({
60
- pendingToken: token,
61
- expiresAt: Date.now() + 4.5 * 60 * 1000
62
- })
63
- );
64
- } catch {}
40
+ // Auto-focus TOTP input when switching to TOTP phase
41
+ useEffect(() => {
42
+ if (phase === 'totp') {
43
+ setTimeout(() => totpInputRef.current?.focus(), 100);
65
44
  }
45
+ }, [phase]);
66
46
 
67
- function clearLoginState() {
68
- try {
69
- sessionStorage.removeItem(LOGIN_STORAGE_KEY);
70
- } catch {}
71
- }
47
+ function saveLoginState(token: string) {
48
+ try {
49
+ // Pending token has 5min server expiry — save with matching client expiry
50
+ sessionStorage.setItem(LOGIN_STORAGE_KEY, JSON.stringify({
51
+ pendingToken: token,
52
+ expiresAt: Date.now() + 4.5 * 60 * 1000,
53
+ }));
54
+ } catch {}
55
+ }
72
56
 
73
- const handleSubmit = async () => {
74
- if (!password.trim() || loading) return;
75
- setLoading(true);
76
- setError('');
57
+ function clearLoginState() {
58
+ try { sessionStorage.removeItem(LOGIN_STORAGE_KEY); } catch {}
59
+ }
77
60
 
78
- try {
79
- const credentials = btoa(`admin:${password}`);
80
- const res = await fetch('/api/portal/login', {
81
- headers: { Authorization: `Basic ${credentials}` },
82
- credentials: 'include'
83
- });
84
- const data = await res.json();
61
+ const handleSubmit = async () => {
62
+ if (!password.trim() || loading) return;
63
+ setLoading(true);
64
+ setError('');
85
65
 
86
- if (res.ok && data.token) {
87
- clearLoginState();
88
- onLogin(data.token);
89
- } else if (res.ok && data.requiresTOTP) {
90
- setPendingToken(data.pendingToken);
91
- saveLoginState(data.pendingToken);
92
- setPhase('totp');
93
- setError('');
94
- } else {
95
- setError(data.error || 'Invalid password');
96
- }
97
- } catch {
98
- setError('Could not reach server');
99
- } finally {
100
- setLoading(false);
101
- }
102
- };
66
+ try {
67
+ const credentials = btoa(`admin:${password}`);
68
+ const res = await fetch('/api/portal/login', {
69
+ headers: { 'Authorization': `Basic ${credentials}` },
70
+ credentials: 'include',
71
+ });
72
+ const data = await res.json();
103
73
 
104
- const handleTotpSubmit = async () => {
105
- if (!totpCode.trim() || loading) return;
106
- setLoading(true);
74
+ if (res.ok && data.token) {
75
+ clearLoginState();
76
+ onLogin(data.token);
77
+ } else if (res.ok && data.requiresTOTP) {
78
+ setPendingToken(data.pendingToken);
79
+ saveLoginState(data.pendingToken);
80
+ setPhase('totp');
107
81
  setError('');
82
+ } else {
83
+ setError(data.error || 'Invalid password');
84
+ }
85
+ } catch {
86
+ setError('Could not reach server');
87
+ } finally {
88
+ setLoading(false);
89
+ }
90
+ };
108
91
 
109
- try {
110
- const res = await fetch(
111
- `/api/portal/login/totp?pending=${encodeURIComponent(pendingToken)}&code=${encodeURIComponent(totpCode)}&trust=${trustDevice ? '1' : '0'}`,
112
- { credentials: 'include' }
113
- );
114
- const data = await res.json();
115
-
116
- if (res.ok && data.token) {
117
- clearLoginState();
118
- onLogin(data.token);
119
- } else {
120
- setError(data.error || 'Invalid code');
121
- }
122
- } catch {
123
- setError('Could not reach server');
124
- } finally {
125
- setLoading(false);
126
- }
127
- };
92
+ const handleTotpSubmit = async () => {
93
+ if (!totpCode.trim() || loading) return;
94
+ setLoading(true);
95
+ setError('');
128
96
 
129
- const handleKeyDown = (e: KeyboardEvent) => {
130
- if (e.key === 'Enter') {
131
- if (phase === 'password') handleSubmit();
132
- else handleTotpSubmit();
133
- }
134
- };
97
+ try {
98
+ const res = await fetch(
99
+ `/api/portal/login/totp?pending=${encodeURIComponent(pendingToken)}&code=${encodeURIComponent(totpCode)}&trust=${trustDevice ? '1' : '0'}`,
100
+ { credentials: 'include' },
101
+ );
102
+ const data = await res.json();
135
103
 
136
- const handleBackToPassword = () => {
137
- setPhase('password');
138
- setError('');
139
- setTotpCode('');
140
- setUseRecovery(false);
104
+ if (res.ok && data.token) {
141
105
  clearLoginState();
142
- };
106
+ onLogin(data.token);
107
+ } else {
108
+ setError(data.error || 'Invalid code');
109
+ }
110
+ } catch {
111
+ setError('Could not reach server');
112
+ } finally {
113
+ setLoading(false);
114
+ }
115
+ };
116
+
117
+ const handleKeyDown = (e: KeyboardEvent) => {
118
+ if (e.key === 'Enter') {
119
+ if (phase === 'password') handleSubmit();
120
+ else handleTotpSubmit();
121
+ }
122
+ };
123
+
124
+ const handleBackToPassword = () => {
125
+ setPhase('password');
126
+ setError('');
127
+ setTotpCode('');
128
+ setUseRecovery(false);
129
+ clearLoginState();
130
+ };
143
131
 
144
- const inputCls =
145
- 'w-full bg-white/[0.05] border border-white/[0.08] text-white rounded-xl px-4 py-3 text-base outline-none input-glow placeholder:text-white/20 transition-all';
132
+ const inputCls = 'w-full bg-white/[0.05] border border-white/[0.08] text-white rounded-xl px-4 py-3 text-base outline-none input-glow placeholder:text-white/20 transition-all';
146
133
 
147
- return (
148
- <div className="flex flex-col items-center justify-center h-dvh px-6">
149
- <div className="w-full max-w-[320px] flex flex-col items-center">
150
- <AnimatePresence mode="wait">
151
- {phase === 'password' ? (
152
- <motion.div
153
- key="password"
154
- initial={{ opacity: 0, x: -20 }}
155
- animate={{ opacity: 1, x: 0 }}
156
- exit={{ opacity: 0, x: -20 }}
157
- transition={{ duration: 0.2 }}
158
- className="w-full flex flex-col items-center"
159
- >
160
- <div className="w-14 h-14 rounded-2xl bg-white/[0.04] border border-white/[0.08] flex items-center justify-center mb-5">
161
- <Lock className="h-6 w-6 text-white/40" />
162
- </div>
134
+ return (
135
+ <div className="flex flex-col items-center justify-center h-dvh px-6">
136
+ <div className="w-full max-w-[320px] flex flex-col items-center">
137
+ <AnimatePresence mode="wait">
138
+ {phase === 'password' ? (
139
+ <motion.div
140
+ key="password"
141
+ initial={{ opacity: 0, x: -20 }}
142
+ animate={{ opacity: 1, x: 0 }}
143
+ exit={{ opacity: 0, x: -20 }}
144
+ transition={{ duration: 0.2 }}
145
+ className="w-full flex flex-col items-center"
146
+ >
147
+ <div className="w-14 h-14 rounded-2xl bg-white/[0.04] border border-white/[0.08] flex items-center justify-center mb-5">
148
+ <Lock className="h-6 w-6 text-white/40" />
149
+ </div>
163
150
 
164
- <h1 className="text-xl font-bold text-white tracking-tight mb-1">
165
- Welcome back
166
- </h1>
167
- <p className="text-white/40 text-[13px] mb-6">
168
- Enter your password to continue.
169
- </p>
151
+ <h1 className="text-xl font-bold text-white tracking-tight mb-1">
152
+ Welcome back
153
+ </h1>
154
+ <p className="text-white/40 text-[13px] mb-6">
155
+ Enter your password to continue.
156
+ </p>
170
157
 
171
- {error && (
172
- <div className="w-full bg-red-500/8 border border-red-500/15 rounded-xl px-4 py-2.5 mb-4">
173
- <p className="text-red-400/90 text-[12px]">
174
- {error}
175
- </p>
176
- </div>
177
- )}
158
+ {error && (
159
+ <div className="w-full bg-red-500/8 border border-red-500/15 rounded-xl px-4 py-2.5 mb-4">
160
+ <p className="text-red-400/90 text-[12px]">{error}</p>
161
+ </div>
162
+ )}
178
163
 
179
- <input
180
- type="password"
181
- value={password}
182
- onChange={e => setPassword(e.target.value)}
183
- onKeyDown={handleKeyDown}
184
- placeholder="Password"
185
- autoComplete="current-password"
186
- className={inputCls}
187
- />
164
+ <input
165
+ type="password"
166
+ value={password}
167
+ onChange={(e) => setPassword(e.target.value)}
168
+ onKeyDown={handleKeyDown}
169
+ placeholder="Password"
170
+ autoFocus
171
+ autoComplete="current-password"
172
+ className={inputCls}
173
+ />
188
174
 
189
- <button
190
- onClick={handleSubmit}
191
- disabled={!password.trim() || loading}
192
- className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
193
- >
194
- {loading ? (
195
- <>
196
- <LoaderCircle className="h-4 w-4 animate-spin" />
197
- Signing in...
198
- </>
199
- ) : (
200
- <>
201
- Sign In
202
- <ArrowRight className="h-4 w-4" />
203
- </>
204
- )}
205
- </button>
206
- </motion.div>
207
- ) : (
208
- <motion.div
209
- key="totp"
210
- initial={{ opacity: 0, x: 20 }}
211
- animate={{ opacity: 1, x: 0 }}
212
- exit={{ opacity: 0, x: 20 }}
213
- transition={{ duration: 0.2 }}
214
- className="w-full flex flex-col items-center"
215
- >
216
- <div className="w-14 h-14 rounded-2xl bg-[#AF27E3]/10 border border-[#AF27E3]/20 flex items-center justify-center mb-5">
217
- <Shield className="h-6 w-6 text-[#AF27E3]" />
218
- </div>
175
+ <button
176
+ onClick={handleSubmit}
177
+ disabled={!password.trim() || loading}
178
+ className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
179
+ >
180
+ {loading ? (
181
+ <><LoaderCircle className="h-4 w-4 animate-spin" />Signing in...</>
182
+ ) : (
183
+ <>Sign In<ArrowRight className="h-4 w-4" /></>
184
+ )}
185
+ </button>
186
+ </motion.div>
187
+ ) : (
188
+ <motion.div
189
+ key="totp"
190
+ initial={{ opacity: 0, x: 20 }}
191
+ animate={{ opacity: 1, x: 0 }}
192
+ exit={{ opacity: 0, x: 20 }}
193
+ transition={{ duration: 0.2 }}
194
+ className="w-full flex flex-col items-center"
195
+ >
196
+ <div className="w-14 h-14 rounded-2xl bg-[#AF27E3]/10 border border-[#AF27E3]/20 flex items-center justify-center mb-5">
197
+ <Shield className="h-6 w-6 text-[#AF27E3]" />
198
+ </div>
219
199
 
220
- <h1 className="text-xl font-bold text-white tracking-tight mb-1">
221
- {useRecovery
222
- ? 'Recovery code'
223
- : 'Enter your 2FA code'}
224
- </h1>
225
- <p className="text-white/40 text-[13px] mb-6">
226
- {useRecovery
227
- ? 'Enter one of your recovery codes.'
228
- : 'Open your authenticator app and enter the 6-digit code.'}
229
- </p>
200
+ <h1 className="text-xl font-bold text-white tracking-tight mb-1">
201
+ {useRecovery ? 'Recovery code' : 'Enter your 2FA code'}
202
+ </h1>
203
+ <p className="text-white/40 text-[13px] mb-6">
204
+ {useRecovery
205
+ ? 'Enter one of your recovery codes.'
206
+ : 'Open your authenticator app and enter the 6-digit code.'}
207
+ </p>
230
208
 
231
- {error && (
232
- <div className="w-full bg-red-500/8 border border-red-500/15 rounded-xl px-4 py-2.5 mb-4">
233
- <p className="text-red-400/90 text-[12px]">
234
- {error}
235
- </p>
236
- </div>
237
- )}
209
+ {error && (
210
+ <div className="w-full bg-red-500/8 border border-red-500/15 rounded-xl px-4 py-2.5 mb-4">
211
+ <p className="text-red-400/90 text-[12px]">{error}</p>
212
+ </div>
213
+ )}
238
214
 
239
- <input
240
- ref={totpInputRef}
241
- type="text"
242
- inputMode={useRecovery ? 'text' : 'numeric'}
243
- autoComplete="one-time-code"
244
- maxLength={useRecovery ? 20 : 6}
245
- value={totpCode}
246
- onChange={e => {
247
- setTotpCode(
248
- useRecovery
249
- ? e.target.value
250
- : e.target.value.replace(/\D/g, '')
251
- );
252
- setError('');
253
- }}
254
- onKeyDown={handleKeyDown}
255
- placeholder={
256
- useRecovery ? 'Recovery code' : '000000'
257
- }
258
- className={
259
- inputCls +
260
- (useRecovery
261
- ? ''
262
- : ' tracking-[0.3em] text-center font-mono')
263
- }
264
- />
215
+ <input
216
+ ref={totpInputRef}
217
+ type="text"
218
+ inputMode={useRecovery ? 'text' : 'numeric'}
219
+ autoComplete="one-time-code"
220
+ maxLength={useRecovery ? 20 : 6}
221
+ value={totpCode}
222
+ onChange={(e) => {
223
+ setTotpCode(useRecovery ? e.target.value : e.target.value.replace(/\D/g, ''));
224
+ setError('');
225
+ }}
226
+ onKeyDown={handleKeyDown}
227
+ placeholder={useRecovery ? 'Recovery code' : '000000'}
228
+ autoFocus
229
+ className={inputCls + (useRecovery ? '' : ' tracking-[0.3em] text-center font-mono')}
230
+ />
265
231
 
266
- {/* Trust device checkbox */}
267
- <label className="flex items-center gap-2.5 mt-4 w-full cursor-pointer">
268
- <div
269
- role="presentation"
270
- onClick={() => setTrustDevice(v => !v)}
271
- className={`w-5 h-5 rounded-md border flex items-center justify-center transition-all ${
272
- trustDevice
273
- ? 'bg-[#AF27E3] border-[#AF27E3]'
274
- : 'bg-white/[0.04] border-white/[0.12]'
275
- }`}
276
- >
277
- {trustDevice && (
278
- <Check className="h-3.5 w-3.5 text-white" />
279
- )}
280
- </div>
281
- <span className="text-[13px] text-white/50">
282
- Trust this device for 90 days
283
- </span>
284
- </label>
232
+ {/* Trust device checkbox */}
233
+ <label className="flex items-center gap-2.5 mt-4 w-full cursor-pointer">
234
+ <div
235
+ onClick={() => setTrustDevice(v => !v)}
236
+ className={`w-5 h-5 rounded-md border flex items-center justify-center transition-all ${
237
+ trustDevice
238
+ ? 'bg-[#AF27E3] border-[#AF27E3]'
239
+ : 'bg-white/[0.04] border-white/[0.12]'
240
+ }`}
241
+ >
242
+ {trustDevice && <Check className="h-3.5 w-3.5 text-white" />}
243
+ </div>
244
+ <span className="text-[13px] text-white/50">Trust this device for 90 days</span>
245
+ </label>
285
246
 
286
- <button
287
- onClick={handleTotpSubmit}
288
- disabled={!totpCode.trim() || loading}
289
- className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
290
- >
291
- {loading ? (
292
- <>
293
- <LoaderCircle className="h-4 w-4 animate-spin" />
294
- Verifying...
295
- </>
296
- ) : (
297
- <>
298
- Verify
299
- <ArrowRight className="h-4 w-4" />
300
- </>
301
- )}
302
- </button>
247
+ <button
248
+ onClick={handleTotpSubmit}
249
+ disabled={!totpCode.trim() || loading}
250
+ className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
251
+ >
252
+ {loading ? (
253
+ <><LoaderCircle className="h-4 w-4 animate-spin" />Verifying...</>
254
+ ) : (
255
+ <>Verify<ArrowRight className="h-4 w-4" /></>
256
+ )}
257
+ </button>
303
258
 
304
- <div className="flex items-center gap-4 mt-4">
305
- <button
306
- onClick={handleBackToPassword}
307
- className="text-[12px] text-white/30 hover:text-white/50 flex items-center gap-1 transition-colors"
308
- >
309
- <ArrowLeft className="h-3 w-3" />
310
- Back
311
- </button>
312
- <button
313
- onClick={() => {
314
- setUseRecovery(v => !v);
315
- setTotpCode('');
316
- setError('');
317
- }}
318
- className="text-[12px] text-white/30 hover:text-white/50 transition-colors"
319
- >
320
- {useRecovery
321
- ? 'Use authenticator code'
322
- : 'Use a recovery code'}
323
- </button>
324
- </div>
325
- </motion.div>
326
- )}
327
- </AnimatePresence>
328
- </div>
329
- </div>
330
- );
259
+ <div className="flex items-center gap-4 mt-4">
260
+ <button
261
+ onClick={handleBackToPassword}
262
+ className="text-[12px] text-white/30 hover:text-white/50 flex items-center gap-1 transition-colors"
263
+ >
264
+ <ArrowLeft className="h-3 w-3" />
265
+ Back
266
+ </button>
267
+ <button
268
+ onClick={() => { setUseRecovery(v => !v); setTotpCode(''); setError(''); }}
269
+ className="text-[12px] text-white/30 hover:text-white/50 transition-colors"
270
+ >
271
+ {useRecovery ? 'Use authenticator code' : 'Use a recovery code'}
272
+ </button>
273
+ </div>
274
+ </motion.div>
275
+ )}
276
+ </AnimatePresence>
277
+ </div>
278
+ </div>
279
+ );
331
280
  }