@korajs/cli 0.3.4 → 0.5.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 (104) hide show
  1. package/dist/bin.cjs +1839 -829
  2. package/dist/bin.cjs.map +1 -1
  3. package/dist/bin.js +1107 -146
  4. package/dist/bin.js.map +1 -1
  5. package/dist/{chunk-HNVCNPER.js → chunk-3WNFM3QB.js} +107 -97
  6. package/dist/chunk-3WNFM3QB.js.map +1 -0
  7. package/dist/{chunk-ULTO5NFA.js → chunk-PSRM56B7.js} +2 -2
  8. package/dist/chunk-PSRM56B7.js.map +1 -0
  9. package/dist/{chunk-MIMLCXSX.js → chunk-SOTZIWIF.js} +267 -228
  10. package/dist/chunk-SOTZIWIF.js.map +1 -0
  11. package/dist/create.cjs +58 -48
  12. package/dist/create.cjs.map +1 -1
  13. package/dist/create.js +2 -2
  14. package/dist/index.cjs +322 -273
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +31 -29
  17. package/dist/index.d.ts +31 -29
  18. package/dist/index.js +2 -2
  19. package/package.json +9 -3
  20. package/templates/react-basic/README.md.hbs +10 -1
  21. package/templates/react-basic/src/App.tsx +112 -126
  22. package/templates/react-basic/src/index.css +165 -159
  23. package/templates/react-basic/src/kora-worker.ts +0 -1
  24. package/templates/react-basic/src/main.tsx +12 -12
  25. package/templates/react-basic/src/modules/todos/todo.mutations.ts +25 -0
  26. package/templates/react-basic/src/modules/todos/todo.queries.ts +5 -0
  27. package/templates/react-basic/src/modules/todos/todo.schema.ts +10 -0
  28. package/templates/react-basic/src/modules/todos/useTodos.ts +31 -0
  29. package/templates/react-basic/src/schema.ts +6 -12
  30. package/templates/react-basic/src/vite-env.d.ts +17 -0
  31. package/templates/react-basic/{tsconfig.json → tsconfig.json.hbs} +1 -2
  32. package/templates/react-basic/vite.config.ts +57 -50
  33. package/templates/react-sync/.env.example +19 -0
  34. package/templates/react-sync/README.md.hbs +43 -1
  35. package/templates/react-sync/package.json.hbs +3 -0
  36. package/templates/react-sync/server.ts +90 -26
  37. package/templates/react-sync/src/App.tsx +132 -130
  38. package/templates/react-sync/src/auth.ts +25 -0
  39. package/templates/react-sync/src/index.css +212 -165
  40. package/templates/react-sync/src/kora-worker.ts +0 -1
  41. package/templates/react-sync/src/main.tsx +34 -18
  42. package/templates/react-sync/src/modules/todos/todo.mutations.ts +25 -0
  43. package/templates/react-sync/src/modules/todos/todo.queries.ts +5 -0
  44. package/templates/react-sync/src/modules/todos/todo.schema.ts +10 -0
  45. package/templates/react-sync/src/modules/todos/useTodos.ts +31 -0
  46. package/templates/react-sync/src/schema.ts +6 -12
  47. package/templates/react-sync/src/vite-env.d.ts +17 -0
  48. package/templates/react-sync/{tsconfig.json → tsconfig.json.hbs} +1 -2
  49. package/templates/react-sync/vite.config.ts +67 -60
  50. package/templates/react-tailwind/README.md.hbs +10 -1
  51. package/templates/react-tailwind/src/App.tsx +186 -197
  52. package/templates/react-tailwind/src/index.css +3 -3
  53. package/templates/react-tailwind/src/kora-worker.ts +0 -1
  54. package/templates/react-tailwind/src/main.tsx +19 -12
  55. package/templates/react-tailwind/src/modules/todos/todo.mutations.ts +25 -0
  56. package/templates/react-tailwind/src/modules/todos/todo.queries.ts +5 -0
  57. package/templates/react-tailwind/src/modules/todos/todo.schema.ts +10 -0
  58. package/templates/react-tailwind/src/modules/todos/useTodos.ts +31 -0
  59. package/templates/react-tailwind/src/schema.ts +6 -12
  60. package/templates/react-tailwind/src/vite-env.d.ts +17 -0
  61. package/templates/react-tailwind/{tsconfig.json → tsconfig.json.hbs} +1 -2
  62. package/templates/react-tailwind/vite.config.ts +57 -50
  63. package/templates/react-tailwind-sync/.env.example +19 -0
  64. package/templates/react-tailwind-sync/README.md.hbs +43 -1
  65. package/templates/react-tailwind-sync/package.json.hbs +3 -0
  66. package/templates/react-tailwind-sync/server.ts +90 -26
  67. package/templates/react-tailwind-sync/src/App.tsx +246 -222
  68. package/templates/react-tailwind-sync/src/auth.ts +25 -0
  69. package/templates/react-tailwind-sync/src/index.css +3 -3
  70. package/templates/react-tailwind-sync/src/kora-worker.ts +0 -1
  71. package/templates/react-tailwind-sync/src/main.tsx +48 -18
  72. package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +25 -0
  73. package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +5 -0
  74. package/templates/react-tailwind-sync/src/modules/todos/todo.schema.ts +10 -0
  75. package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +31 -0
  76. package/templates/react-tailwind-sync/src/schema.ts +6 -12
  77. package/templates/react-tailwind-sync/src/vite-env.d.ts +17 -0
  78. package/templates/react-tailwind-sync/{tsconfig.json → tsconfig.json.hbs} +1 -2
  79. package/templates/react-tailwind-sync/vite.config.ts +67 -60
  80. package/templates/tauri-react/.env.example +20 -0
  81. package/templates/tauri-react/README.md.hbs +36 -1
  82. package/templates/tauri-react/dev.ts +11 -11
  83. package/templates/tauri-react/package.json.hbs +2 -0
  84. package/templates/tauri-react/server.ts +86 -12
  85. package/templates/tauri-react/src/App.tsx +769 -544
  86. package/templates/tauri-react/src/AppShell.tsx +125 -83
  87. package/templates/tauri-react/src/SetupScreen.tsx +257 -208
  88. package/templates/tauri-react/src/auth.ts +45 -0
  89. package/templates/tauri-react/src/main.tsx +3 -3
  90. package/templates/tauri-react/src/modules/todos/todo.mutations.ts +25 -0
  91. package/templates/tauri-react/src/modules/todos/todo.queries.ts +5 -0
  92. package/templates/tauri-react/src/modules/todos/todo.schema.ts +10 -0
  93. package/templates/tauri-react/src/modules/todos/useTodos.ts +31 -0
  94. package/templates/tauri-react/src/schema.ts +6 -12
  95. package/templates/tauri-react/src/sync-config.ts +47 -47
  96. package/templates/tauri-react/src/updater.ts +15 -15
  97. package/templates/tauri-react/src/vite-env.d.ts +17 -0
  98. package/templates/tauri-react/src-tauri/tauri.conf.json +2 -2
  99. package/templates/tauri-react/tsconfig.json.hbs +14 -0
  100. package/templates/tauri-react/vite.config.ts +9 -9
  101. package/dist/chunk-HNVCNPER.js.map +0 -1
  102. package/dist/chunk-MIMLCXSX.js.map +0 -1
  103. package/dist/chunk-ULTO5NFA.js.map +0 -1
  104. package/templates/tauri-react/tsconfig.json +0 -15
@@ -1,278 +1,397 @@
1
- import { useState } from 'react'
2
- import { useQuery, useMutation, useCollection } from '@korajs/react'
1
+ import { useAuth } from '@korajs/auth/react'
3
2
  import {
4
- CheckCircle2,
5
- Circle,
6
- Monitor,
7
- Loader2,
8
- Plus,
9
- Trash2,
10
- Wifi,
11
- WifiOff,
12
- Settings,
13
- AlertTriangle,
3
+ AlertTriangle,
4
+ CheckCircle2,
5
+ Circle,
6
+ Loader2,
7
+ Monitor,
8
+ Plus,
9
+ Settings,
10
+ Trash2,
11
+ Wifi,
12
+ WifiOff,
14
13
  } from 'lucide-react'
14
+ import { useState } from 'react'
15
+ import { useTodos } from './modules/todos/useTodos'
15
16
  import { testConnection } from './sync-config'
16
17
 
17
18
  type Filter = 'all' | 'active' | 'completed'
18
19
 
19
20
  interface AppProps {
20
- syncUrl: string | null
21
- onChangeServer: (newUrl: string | null) => void
22
- onFactoryReset: () => void
21
+ syncUrl: string | null
22
+ onChangeServer: (newUrl: string | null) => void
23
+ onFactoryReset: () => void
23
24
  }
24
25
 
25
26
  export function App({ syncUrl, onChangeServer, onFactoryReset }: AppProps) {
26
- const todos = useCollection('todos')
27
- const allTodos = useQuery(todos.where({}).orderBy('createdAt', 'desc'))
28
- const { mutate: addTodo, isLoading: isAdding } = useMutation(
29
- (data: { title: string }) => todos.insert(data)
30
- )
31
- const { mutate: toggleTodo } = useMutation(
32
- (id: string, data: { completed: boolean }) => todos.update(id, data)
33
- )
34
- const { mutate: deleteTodo } = useMutation(
35
- (id: string) => todos.delete(id)
36
- )
37
-
38
- const [filter, setFilter] = useState<Filter>('all')
39
- const [input, setInput] = useState('')
40
- const [showSettings, setShowSettings] = useState(false)
41
-
42
- const activeTodos = allTodos.filter((t) => !t.completed)
43
- const completedTodos = allTodos.filter((t) => !!t.completed)
44
- const filteredTodos =
45
- filter === 'active' ? activeTodos : filter === 'completed' ? completedTodos : allTodos
46
-
47
- const handleSubmit = (e: React.FormEvent) => {
48
- e.preventDefault()
49
- const title = input.trim()
50
- if (title) {
51
- addTodo({ title })
52
- setInput('')
53
- }
54
- }
55
-
56
- const clearCompleted = () => {
57
- for (const todo of completedTodos) {
58
- deleteTodo(todo.id)
59
- }
60
- }
61
-
62
- return (
63
- <div style={{ minHeight: '100vh', background: '#0a0a0a', color: '#f3f4f6' }}>
64
- <div style={{ maxWidth: '640px', margin: '0 auto', padding: '48px 16px' }}>
65
- {/* Header */}
66
- <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '32px' }}>
67
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
68
- <Monitor style={{ width: '32px', height: '32px', color: '#818cf8' }} />
69
- <h1 style={{ fontSize: '24px', fontWeight: 'bold' }}>Desktop Tasks</h1>
70
- </div>
71
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
72
- <span style={{
73
- display: 'flex',
74
- alignItems: 'center',
75
- gap: '6px',
76
- fontSize: '12px',
77
- color: syncUrl ? '#34d399' : '#6b7280',
78
- background: '#1f2937',
79
- padding: '4px 12px',
80
- borderRadius: '9999px',
81
- }}>
82
- {syncUrl ? (
83
- <Wifi style={{ width: '12px', height: '12px' }} />
84
- ) : (
85
- <WifiOff style={{ width: '12px', height: '12px' }} />
86
- )}
87
- {syncUrl ? 'Syncing' : 'Local only'}
88
- </span>
89
- <button
90
- onClick={() => setShowSettings(!showSettings)}
91
- style={{
92
- background: showSettings ? '#374151' : '#1f2937',
93
- border: 'none',
94
- borderRadius: '9999px',
95
- padding: '6px',
96
- cursor: 'pointer',
97
- color: showSettings ? '#f3f4f6' : '#6b7280',
98
- display: 'flex',
99
- }}
100
- >
101
- <Settings style={{ width: '14px', height: '14px' }} />
102
- </button>
103
- </div>
104
- </div>
105
-
106
- {/* Settings panel */}
107
- {showSettings && (
108
- <SettingsPanel
109
- syncUrl={syncUrl}
110
- onChangeServer={onChangeServer}
111
- onFactoryReset={onFactoryReset}
112
- onClose={() => setShowSettings(false)}
113
- />
114
- )}
115
-
116
- {/* Stats */}
117
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px', marginBottom: '32px' }}>
118
- <StatCard label="Total" value={allTodos.length} color="#d1d5db" />
119
- <StatCard label="Remaining" value={activeTodos.length} color="#fbbf24" />
120
- <StatCard label="Done" value={completedTodos.length} color="#34d399" />
121
- </div>
122
-
123
- {/* Add form */}
124
- <form onSubmit={handleSubmit} style={{ display: 'flex', gap: '12px', marginBottom: '32px' }}>
125
- <input
126
- type="text"
127
- value={input}
128
- onChange={(e) => setInput(e.target.value)}
129
- placeholder="What needs to be done?"
130
- style={{
131
- flex: 1,
132
- borderRadius: '8px',
133
- border: '1px solid #374151',
134
- background: '#111827',
135
- padding: '12px 16px',
136
- color: '#f3f4f6',
137
- outline: 'none',
138
- fontSize: '14px',
139
- }}
140
- />
141
- <button
142
- type="submit"
143
- disabled={isAdding || !input.trim()}
144
- style={{
145
- display: 'flex',
146
- alignItems: 'center',
147
- gap: '8px',
148
- borderRadius: '8px',
149
- background: '#4f46e5',
150
- padding: '12px 20px',
151
- fontWeight: '500',
152
- color: 'white',
153
- border: 'none',
154
- cursor: 'pointer',
155
- opacity: isAdding || !input.trim() ? 0.5 : 1,
156
- fontSize: '14px',
157
- }}
158
- >
159
- {isAdding ? <Loader2 style={{ width: '16px', height: '16px', animation: 'spin 1s linear infinite' }} /> : <Plus style={{ width: '16px', height: '16px' }} />}
160
- Add
161
- </button>
162
- </form>
163
-
164
- {/* Filter tabs */}
165
- <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
166
- {(['all', 'active', 'completed'] as const).map((f) => {
167
- const count = f === 'all' ? allTodos.length : f === 'active' ? activeTodos.length : completedTodos.length
168
- const isActive = filter === f
169
- return (
170
- <button
171
- key={f}
172
- onClick={() => setFilter(f)}
173
- style={{
174
- display: 'flex',
175
- alignItems: 'center',
176
- gap: '8px',
177
- borderRadius: '8px',
178
- padding: '8px 16px',
179
- fontSize: '14px',
180
- fontWeight: '500',
181
- border: 'none',
182
- cursor: 'pointer',
183
- background: isActive ? '#4f46e5' : '#1f2937',
184
- color: isActive ? 'white' : '#9ca3af',
185
- }}
186
- >
187
- {f.charAt(0).toUpperCase() + f.slice(1)}
188
- <span style={{
189
- borderRadius: '9999px',
190
- padding: '2px 8px',
191
- fontSize: '12px',
192
- background: isActive ? '#4338ca' : '#374151',
193
- color: isActive ? 'white' : '#9ca3af',
194
- }}>
195
- {count}
196
- </span>
197
- </button>
198
- )
199
- })}
200
- </div>
201
-
202
- {/* Todo list */}
203
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
204
- {filteredTodos.length === 0 ? (
205
- <div style={{ borderRadius: '8px', border: '1px dashed #1f2937', padding: '48px 0', textAlign: 'center', color: '#4b5563' }}>
206
- {filter === 'all' ? 'No tasks yet. Add one above!' : filter === 'active' ? 'All caught up!' : 'No completed tasks yet.'}
207
- </div>
208
- ) : (
209
- filteredTodos.map((todo) => (
210
- <div
211
- key={todo.id}
212
- style={{
213
- display: 'flex',
214
- alignItems: 'center',
215
- gap: '12px',
216
- borderRadius: '8px',
217
- border: '1px solid #1f2937',
218
- background: '#111827',
219
- padding: '12px 16px',
220
- }}
221
- >
222
- <button
223
- onClick={() => toggleTodo(todo.id, { completed: !todo.completed })}
224
- style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0 }}
225
- >
226
- {todo.completed ? (
227
- <CheckCircle2 style={{ width: '20px', height: '20px', color: '#34d399' }} />
228
- ) : (
229
- <Circle style={{ width: '20px', height: '20px', color: '#6b7280' }} />
230
- )}
231
- </button>
232
- <span style={{
233
- flex: 1,
234
- color: todo.completed ? '#6b7280' : '#f3f4f6',
235
- textDecoration: todo.completed ? 'line-through' : 'none',
236
- }}>
237
- {String(todo.title)}
238
- </span>
239
- {todo.createdAt && (
240
- <span style={{ fontSize: '12px', color: '#374151' }}>
241
- {formatTime(Number(todo.createdAt))}
242
- </span>
243
- )}
244
- <button
245
- onClick={() => deleteTodo(todo.id)}
246
- style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: '#4b5563' }}
247
- >
248
- <Trash2 style={{ width: '16px', height: '16px' }} />
249
- </button>
250
- </div>
251
- ))
252
- )}
253
- </div>
254
-
255
- {/* Footer */}
256
- {allTodos.length > 0 && (
257
- <div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between', fontSize: '14px', color: '#6b7280' }}>
258
- <span>{activeTodos.length} item{activeTodos.length !== 1 ? 's' : ''} left</span>
259
- {completedTodos.length > 0 && (
260
- <button
261
- onClick={clearCompleted}
262
- style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#6b7280', fontSize: '14px' }}
263
- >
264
- Clear completed
265
- </button>
266
- )}
267
- </div>
268
- )}
269
-
270
- <p style={{ marginTop: '48px', textAlign: 'center', fontSize: '12px', color: '#374151' }}>
271
- Powered by Kora &mdash; native SQLite, offline-first
272
- </p>
273
- </div>
274
- </div>
275
- )
27
+ const { allTodos, activeTodos, completedTodos, addTodo, toggleTodo, deleteTodo } = useTodos()
28
+ const { user, isAuthenticated, signOut, error } = useAuth()
29
+ const isAdding = addTodo.isLoading
30
+
31
+ const [filter, setFilter] = useState<Filter>('all')
32
+ const [input, setInput] = useState('')
33
+ const [showSettings, setShowSettings] = useState(false)
34
+
35
+ const filteredTodos =
36
+ filter === 'active' ? activeTodos : filter === 'completed' ? completedTodos : allTodos
37
+
38
+ const handleSubmit = (e: React.FormEvent) => {
39
+ e.preventDefault()
40
+ const title = input.trim()
41
+ if (title) {
42
+ addTodo.mutate({ title })
43
+ setInput('')
44
+ }
45
+ }
46
+
47
+ const clearCompleted = () => {
48
+ for (const todo of completedTodos) {
49
+ deleteTodo.mutate(todo.id)
50
+ }
51
+ }
52
+
53
+ return (
54
+ <div style={{ minHeight: '100vh', background: '#0a0a0a', color: '#f3f4f6' }}>
55
+ <div style={{ maxWidth: '640px', margin: '0 auto', padding: '48px 16px' }}>
56
+ {/* Header */}
57
+ <div
58
+ style={{
59
+ display: 'flex',
60
+ alignItems: 'center',
61
+ justifyContent: 'space-between',
62
+ marginBottom: '32px',
63
+ }}
64
+ >
65
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
66
+ <Monitor style={{ width: '32px', height: '32px', color: '#818cf8' }} />
67
+ <h1 style={{ fontSize: '24px', fontWeight: 'bold' }}>Desktop Tasks</h1>
68
+ </div>
69
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
70
+ <span
71
+ style={{
72
+ display: 'flex',
73
+ alignItems: 'center',
74
+ gap: '6px',
75
+ fontSize: '12px',
76
+ color: syncUrl ? '#34d399' : '#6b7280',
77
+ background: '#1f2937',
78
+ padding: '4px 12px',
79
+ borderRadius: '9999px',
80
+ }}
81
+ >
82
+ {syncUrl ? (
83
+ <Wifi style={{ width: '12px', height: '12px' }} />
84
+ ) : (
85
+ <WifiOff style={{ width: '12px', height: '12px' }} />
86
+ )}
87
+ {syncUrl ? 'Syncing' : 'Local only'}
88
+ </span>
89
+ <AuthButton
90
+ isAuthenticated={isAuthenticated}
91
+ label={user?.email || 'Sign out'}
92
+ onSignOut={signOut}
93
+ />
94
+ <button
95
+ type="button"
96
+ onClick={() => setShowSettings(!showSettings)}
97
+ style={{
98
+ background: showSettings ? '#374151' : '#1f2937',
99
+ border: 'none',
100
+ borderRadius: '9999px',
101
+ padding: '6px',
102
+ cursor: 'pointer',
103
+ color: showSettings ? '#f3f4f6' : '#6b7280',
104
+ display: 'flex',
105
+ }}
106
+ >
107
+ <Settings style={{ width: '14px', height: '14px' }} />
108
+ </button>
109
+ </div>
110
+ </div>
111
+
112
+ {/* Settings panel */}
113
+ {showSettings && (
114
+ <SettingsPanel
115
+ syncUrl={syncUrl}
116
+ onChangeServer={onChangeServer}
117
+ onFactoryReset={onFactoryReset}
118
+ onClose={() => setShowSettings(false)}
119
+ />
120
+ )}
121
+
122
+ {error && (
123
+ <div
124
+ style={{
125
+ border: '1px solid rgba(248, 113, 113, 0.35)',
126
+ borderRadius: '8px',
127
+ color: '#fca5a5',
128
+ fontSize: '13px',
129
+ marginBottom: '16px',
130
+ padding: '10px 12px',
131
+ }}
132
+ >
133
+ {error}
134
+ </div>
135
+ )}
136
+
137
+ {/* Stats */}
138
+ <div
139
+ style={{
140
+ display: 'grid',
141
+ gridTemplateColumns: '1fr 1fr 1fr',
142
+ gap: '16px',
143
+ marginBottom: '32px',
144
+ }}
145
+ >
146
+ <StatCard label="Total" value={allTodos.length} color="#d1d5db" />
147
+ <StatCard label="Remaining" value={activeTodos.length} color="#fbbf24" />
148
+ <StatCard label="Done" value={completedTodos.length} color="#34d399" />
149
+ </div>
150
+
151
+ {/* Add form */}
152
+ <form
153
+ onSubmit={handleSubmit}
154
+ style={{ display: 'flex', gap: '12px', marginBottom: '32px' }}
155
+ >
156
+ <input
157
+ type="text"
158
+ value={input}
159
+ onChange={(e) => setInput(e.target.value)}
160
+ placeholder="What needs to be done?"
161
+ style={{
162
+ flex: 1,
163
+ borderRadius: '8px',
164
+ border: '1px solid #374151',
165
+ background: '#111827',
166
+ padding: '12px 16px',
167
+ color: '#f3f4f6',
168
+ outline: 'none',
169
+ fontSize: '14px',
170
+ }}
171
+ />
172
+ <button
173
+ type="submit"
174
+ disabled={isAdding || !input.trim()}
175
+ style={{
176
+ display: 'flex',
177
+ alignItems: 'center',
178
+ gap: '8px',
179
+ borderRadius: '8px',
180
+ background: '#4f46e5',
181
+ padding: '12px 20px',
182
+ fontWeight: '500',
183
+ color: 'white',
184
+ border: 'none',
185
+ cursor: 'pointer',
186
+ opacity: isAdding || !input.trim() ? 0.5 : 1,
187
+ fontSize: '14px',
188
+ }}
189
+ >
190
+ {isAdding ? (
191
+ <Loader2
192
+ style={{ width: '16px', height: '16px', animation: 'spin 1s linear infinite' }}
193
+ />
194
+ ) : (
195
+ <Plus style={{ width: '16px', height: '16px' }} />
196
+ )}
197
+ Add
198
+ </button>
199
+ </form>
200
+
201
+ {/* Filter tabs */}
202
+ <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
203
+ {(['all', 'active', 'completed'] as const).map((f) => {
204
+ const count =
205
+ f === 'all'
206
+ ? allTodos.length
207
+ : f === 'active'
208
+ ? activeTodos.length
209
+ : completedTodos.length
210
+ const isActive = filter === f
211
+ return (
212
+ <button
213
+ key={f}
214
+ onClick={() => setFilter(f)}
215
+ style={{
216
+ display: 'flex',
217
+ alignItems: 'center',
218
+ gap: '8px',
219
+ borderRadius: '8px',
220
+ padding: '8px 16px',
221
+ fontSize: '14px',
222
+ fontWeight: '500',
223
+ border: 'none',
224
+ cursor: 'pointer',
225
+ background: isActive ? '#4f46e5' : '#1f2937',
226
+ color: isActive ? 'white' : '#9ca3af',
227
+ }}
228
+ >
229
+ {f.charAt(0).toUpperCase() + f.slice(1)}
230
+ <span
231
+ style={{
232
+ borderRadius: '9999px',
233
+ padding: '2px 8px',
234
+ fontSize: '12px',
235
+ background: isActive ? '#4338ca' : '#374151',
236
+ color: isActive ? 'white' : '#9ca3af',
237
+ }}
238
+ >
239
+ {count}
240
+ </span>
241
+ </button>
242
+ )
243
+ })}
244
+ </div>
245
+
246
+ {/* Todo list */}
247
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
248
+ {filteredTodos.length === 0 ? (
249
+ <div
250
+ style={{
251
+ borderRadius: '8px',
252
+ border: '1px dashed #1f2937',
253
+ padding: '48px 0',
254
+ textAlign: 'center',
255
+ color: '#4b5563',
256
+ }}
257
+ >
258
+ {filter === 'all'
259
+ ? 'No tasks yet. Add one above!'
260
+ : filter === 'active'
261
+ ? 'All caught up!'
262
+ : 'No completed tasks yet.'}
263
+ </div>
264
+ ) : (
265
+ filteredTodos.map((todo) => (
266
+ <div
267
+ key={todo.id}
268
+ style={{
269
+ display: 'flex',
270
+ alignItems: 'center',
271
+ gap: '12px',
272
+ borderRadius: '8px',
273
+ border: '1px solid #1f2937',
274
+ background: '#111827',
275
+ padding: '12px 16px',
276
+ }}
277
+ >
278
+ <button
279
+ onClick={() => toggleTodo.mutate(todo.id, { completed: !todo.completed })}
280
+ style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0 }}
281
+ >
282
+ {todo.completed ? (
283
+ <CheckCircle2 style={{ width: '20px', height: '20px', color: '#34d399' }} />
284
+ ) : (
285
+ <Circle style={{ width: '20px', height: '20px', color: '#6b7280' }} />
286
+ )}
287
+ </button>
288
+ <span
289
+ style={{
290
+ flex: 1,
291
+ color: todo.completed ? '#6b7280' : '#f3f4f6',
292
+ textDecoration: todo.completed ? 'line-through' : 'none',
293
+ }}
294
+ >
295
+ {String(todo.title)}
296
+ </span>
297
+ {todo.createdAt && (
298
+ <span style={{ fontSize: '12px', color: '#374151' }}>
299
+ {formatTime(Number(todo.createdAt))}
300
+ </span>
301
+ )}
302
+ <button
303
+ onClick={() => deleteTodo.mutate(todo.id)}
304
+ style={{
305
+ background: 'none',
306
+ border: 'none',
307
+ cursor: 'pointer',
308
+ padding: 0,
309
+ color: '#4b5563',
310
+ }}
311
+ >
312
+ <Trash2 style={{ width: '16px', height: '16px' }} />
313
+ </button>
314
+ </div>
315
+ ))
316
+ )}
317
+ </div>
318
+
319
+ {/* Footer */}
320
+ {allTodos.length > 0 && (
321
+ <div
322
+ style={{
323
+ marginTop: '24px',
324
+ display: 'flex',
325
+ justifyContent: 'space-between',
326
+ fontSize: '14px',
327
+ color: '#6b7280',
328
+ }}
329
+ >
330
+ <span>
331
+ {activeTodos.length} item{activeTodos.length !== 1 ? 's' : ''} left
332
+ </span>
333
+ {completedTodos.length > 0 && (
334
+ <button
335
+ onClick={clearCompleted}
336
+ style={{
337
+ background: 'none',
338
+ border: 'none',
339
+ cursor: 'pointer',
340
+ color: '#6b7280',
341
+ fontSize: '14px',
342
+ }}
343
+ >
344
+ Clear completed
345
+ </button>
346
+ )}
347
+ </div>
348
+ )}
349
+
350
+ <p style={{ marginTop: '48px', textAlign: 'center', fontSize: '12px', color: '#374151' }}>
351
+ Powered by Kora &mdash; native SQLite, offline-first
352
+ </p>
353
+ </div>
354
+ </div>
355
+ )
356
+ }
357
+
358
+ function AuthButton({
359
+ isAuthenticated,
360
+ label,
361
+ onSignOut,
362
+ }: {
363
+ isAuthenticated: boolean
364
+ label: string
365
+ onSignOut: () => Promise<void>
366
+ }) {
367
+ const { getOAuthAuthorizationUrl } = useAuth()
368
+
369
+ const handleSignIn = async () => {
370
+ const { url } = await getOAuthAuthorizationUrl('google')
371
+ window.open(url, '_blank', 'noopener,noreferrer')
372
+ }
373
+
374
+ return (
375
+ <button
376
+ type="button"
377
+ onClick={() => (isAuthenticated ? onSignOut() : handleSignIn())}
378
+ style={{
379
+ background: '#1f2937',
380
+ border: '1px solid #374151',
381
+ borderRadius: '9999px',
382
+ color: '#d1d5db',
383
+ cursor: 'pointer',
384
+ fontSize: '12px',
385
+ maxWidth: '160px',
386
+ overflow: 'hidden',
387
+ padding: '4px 12px',
388
+ textOverflow: 'ellipsis',
389
+ whiteSpace: 'nowrap',
390
+ }}
391
+ >
392
+ {isAuthenticated ? label : 'Sign in'}
393
+ </button>
394
+ )
276
395
  }
277
396
 
278
397
  // ---------------------------------------------------------------------------
@@ -280,279 +399,378 @@ export function App({ syncUrl, onChangeServer, onFactoryReset }: AppProps) {
280
399
  // ---------------------------------------------------------------------------
281
400
 
282
401
  interface SettingsPanelProps {
283
- syncUrl: string | null
284
- onChangeServer: (newUrl: string | null) => void
285
- onFactoryReset: () => void
286
- onClose: () => void
402
+ syncUrl: string | null
403
+ onChangeServer: (newUrl: string | null) => void
404
+ onFactoryReset: () => void
405
+ onClose: () => void
287
406
  }
288
407
 
289
408
  function SettingsPanel({ syncUrl, onChangeServer, onFactoryReset, onClose }: SettingsPanelProps) {
290
- const [editing, setEditing] = useState(false)
291
- const [newUrl, setNewUrl] = useState(syncUrl ?? '')
292
- const [testing, setTesting] = useState(false)
293
- const [testResult, setTestResult] = useState<'success' | 'failure' | null>(null)
294
- const [showResetConfirm, setShowResetConfirm] = useState(false)
295
- const [showChangeConfirm, setShowChangeConfirm] = useState(false)
296
-
297
- const handleTestConnection = async () => {
298
- const url = editing ? newUrl.trim() : syncUrl
299
- if (!url) return
300
- setTesting(true)
301
- setTestResult(null)
302
- const ok = await testConnection(url)
303
- setTestResult(ok ? 'success' : 'failure')
304
- setTesting(false)
305
- }
306
-
307
- const handleSaveUrl = () => {
308
- const trimmed = newUrl.trim()
309
- if (trimmed === syncUrl) {
310
- setEditing(false)
311
- return
312
- }
313
- // If there's existing data and the URL is changing, warn about data isolation
314
- setShowChangeConfirm(true)
315
- }
316
-
317
- const confirmChangeServer = (keepData: boolean) => {
318
- setShowChangeConfirm(false)
319
- if (keepData) {
320
- onChangeServer(newUrl.trim() || null)
321
- } else {
322
- // Factory reset + change server
323
- // Store the new URL so it's picked up after reload
324
- if (newUrl.trim()) {
325
- localStorage.setItem('kora-sync-url', newUrl.trim())
326
- localStorage.setItem('kora-sync-configured', 'true')
327
- }
328
- onFactoryReset()
329
- }
330
- }
331
-
332
- const handleDisconnect = () => {
333
- onChangeServer(null)
334
- }
335
-
336
- const handleConnect = () => {
337
- setEditing(true)
338
- setNewUrl('')
339
- }
340
-
341
- return (
342
- <div style={{
343
- marginBottom: '24px',
344
- borderRadius: '8px',
345
- border: '1px solid #1f2937',
346
- background: '#111827',
347
- overflow: 'hidden',
348
- }}>
349
- {/* Server URL section */}
350
- <div style={{ padding: '16px' }}>
351
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '12px' }}>
352
- <p style={{ fontSize: '14px', fontWeight: '500', color: '#d1d5db' }}>Sync Server</p>
353
- <button
354
- onClick={onClose}
355
- style={{ fontSize: '12px', color: '#6b7280', background: 'none', border: 'none', cursor: 'pointer' }}
356
- >
357
- Close
358
- </button>
359
- </div>
360
-
361
- {editing ? (
362
- <div>
363
- <input
364
- type="text"
365
- value={newUrl}
366
- onChange={(e) => { setNewUrl(e.target.value); setTestResult(null) }}
367
- placeholder="wss://your-server.example.com/kora-sync"
368
- autoFocus
369
- style={{
370
- width: '100%',
371
- borderRadius: '6px',
372
- border: '1px solid #374151',
373
- background: '#0a0a0a',
374
- padding: '8px 12px',
375
- color: '#f3f4f6',
376
- outline: 'none',
377
- fontSize: '13px',
378
- boxSizing: 'border-box',
379
- marginBottom: '8px',
380
- }}
381
- />
382
- <div style={{ display: 'flex', gap: '8px' }}>
383
- <button onClick={handleSaveUrl} disabled={!newUrl.trim()} style={btnStyle('#4f46e5', !newUrl.trim())}>
384
- Save
385
- </button>
386
- <button onClick={handleTestConnection} disabled={testing || !newUrl.trim()} style={btnStyle('#374151', testing || !newUrl.trim())}>
387
- {testing ? 'Testing...' : 'Test'}
388
- </button>
389
- <button onClick={() => { setEditing(false); setTestResult(null) }} style={btnStyle('#374151', false)}>
390
- Cancel
391
- </button>
392
- </div>
393
- </div>
394
- ) : (
395
- <div>
396
- <p style={{ fontSize: '13px', color: '#9ca3af', marginBottom: '8px', wordBreak: 'break-all' }}>
397
- {syncUrl ?? 'Not connected — running in local-only mode'}
398
- </p>
399
- <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
400
- {syncUrl ? (
401
- <>
402
- <button onClick={() => { setEditing(true); setNewUrl(syncUrl) }} style={btnStyle('#374151', false)}>
403
- Change URL
404
- </button>
405
- <button onClick={handleTestConnection} disabled={testing} style={btnStyle('#374151', testing)}>
406
- {testing ? 'Testing...' : 'Test Connection'}
407
- </button>
408
- <button onClick={handleDisconnect} style={btnStyle('#7f1d1d', false, '#ef4444')}>
409
- Disconnect
410
- </button>
411
- </>
412
- ) : (
413
- <button onClick={handleConnect} style={btnStyle('#4f46e5', false)}>
414
- Connect to Server
415
- </button>
416
- )}
417
- </div>
418
- </div>
419
- )}
420
-
421
- {/* Test result */}
422
- {testResult && (
423
- <p style={{
424
- fontSize: '12px',
425
- marginTop: '8px',
426
- color: testResult === 'success' ? '#34d399' : '#fbbf24',
427
- }}>
428
- {testResult === 'success' ? 'Connection successful' : 'Server unreachable — it may be down temporarily'}
429
- </p>
430
- )}
431
- </div>
432
-
433
- {/* Danger zone */}
434
- <div style={{ padding: '12px 16px', borderTop: '1px solid #1f2937', background: '#0a0a0a' }}>
435
- {showResetConfirm ? (
436
- <div>
437
- <div style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '8px' }}>
438
- <AlertTriangle style={{ width: '16px', height: '16px', color: '#ef4444', flexShrink: 0, marginTop: '1px' }} />
439
- <p style={{ fontSize: '13px', color: '#fca5a5' }}>
440
- This will delete all local data and reset the app to its initial state. This cannot be undone.
441
- </p>
442
- </div>
443
- <div style={{ display: 'flex', gap: '8px' }}>
444
- <button onClick={onFactoryReset} style={btnStyle('#7f1d1d', false, '#ef4444')}>
445
- Confirm Reset
446
- </button>
447
- <button onClick={() => setShowResetConfirm(false)} style={btnStyle('#374151', false)}>
448
- Cancel
449
- </button>
450
- </div>
451
- </div>
452
- ) : (
453
- <button onClick={() => setShowResetConfirm(true)} style={{ fontSize: '13px', color: '#6b7280', background: 'none', border: 'none', cursor: 'pointer' }}>
454
- Reset app (clear all local data)
455
- </button>
456
- )}
457
- </div>
458
-
459
- {/* Server change confirmation dialog */}
460
- {showChangeConfirm && (
461
- <div style={{
462
- position: 'fixed',
463
- inset: 0,
464
- background: 'rgba(0,0,0,0.7)',
465
- display: 'flex',
466
- alignItems: 'center',
467
- justifyContent: 'center',
468
- zIndex: 100,
469
- }}>
470
- <div style={{
471
- maxWidth: '420px',
472
- width: '100%',
473
- margin: '0 16px',
474
- borderRadius: '12px',
475
- border: '1px solid #374151',
476
- background: '#111827',
477
- padding: '24px',
478
- }}>
479
- <div style={{ display: 'flex', gap: '12px', alignItems: 'flex-start', marginBottom: '16px' }}>
480
- <AlertTriangle style={{ width: '20px', height: '20px', color: '#fbbf24', flexShrink: 0, marginTop: '2px' }} />
481
- <div>
482
- <p style={{ fontSize: '15px', fontWeight: '500', color: '#f3f4f6', marginBottom: '8px' }}>
483
- Changing sync servers
484
- </p>
485
- <p style={{ fontSize: '13px', color: '#9ca3af', lineHeight: '1.6' }}>
486
- Existing local data was created for the current server. Keeping it and connecting to a different server may sync this data to the wrong place.
487
- </p>
488
- </div>
489
- </div>
490
- <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
491
- <button
492
- onClick={() => confirmChangeServer(false)}
493
- style={{
494
- width: '100%',
495
- padding: '10px',
496
- borderRadius: '8px',
497
- border: '1px solid #374151',
498
- background: '#4f46e5',
499
- color: 'white',
500
- fontSize: '14px',
501
- cursor: 'pointer',
502
- fontWeight: '500',
503
- }}
504
- >
505
- Clear data and switch (recommended)
506
- </button>
507
- <button
508
- onClick={() => confirmChangeServer(true)}
509
- style={{
510
- width: '100%',
511
- padding: '10px',
512
- borderRadius: '8px',
513
- border: '1px solid #374151',
514
- background: '#1f2937',
515
- color: '#9ca3af',
516
- fontSize: '14px',
517
- cursor: 'pointer',
518
- }}
519
- >
520
- Keep data and switch
521
- </button>
522
- <button
523
- onClick={() => setShowChangeConfirm(false)}
524
- style={{
525
- width: '100%',
526
- padding: '10px',
527
- borderRadius: '8px',
528
- border: 'none',
529
- background: 'transparent',
530
- color: '#6b7280',
531
- fontSize: '14px',
532
- cursor: 'pointer',
533
- }}
534
- >
535
- Cancel
536
- </button>
537
- </div>
538
- </div>
539
- </div>
540
- )}
541
- </div>
542
- )
409
+ const [editing, setEditing] = useState(false)
410
+ const [newUrl, setNewUrl] = useState(syncUrl ?? '')
411
+ const [testing, setTesting] = useState(false)
412
+ const [testResult, setTestResult] = useState<'success' | 'failure' | null>(null)
413
+ const [showResetConfirm, setShowResetConfirm] = useState(false)
414
+ const [showChangeConfirm, setShowChangeConfirm] = useState(false)
415
+
416
+ const handleTestConnection = async () => {
417
+ const url = editing ? newUrl.trim() : syncUrl
418
+ if (!url) return
419
+ setTesting(true)
420
+ setTestResult(null)
421
+ const ok = await testConnection(url)
422
+ setTestResult(ok ? 'success' : 'failure')
423
+ setTesting(false)
424
+ }
425
+
426
+ const handleSaveUrl = () => {
427
+ const trimmed = newUrl.trim()
428
+ if (trimmed === syncUrl) {
429
+ setEditing(false)
430
+ return
431
+ }
432
+ // If there's existing data and the URL is changing, warn about data isolation
433
+ setShowChangeConfirm(true)
434
+ }
435
+
436
+ const confirmChangeServer = (keepData: boolean) => {
437
+ setShowChangeConfirm(false)
438
+ if (keepData) {
439
+ onChangeServer(newUrl.trim() || null)
440
+ } else {
441
+ // Factory reset + change server
442
+ // Store the new URL so it's picked up after reload
443
+ if (newUrl.trim()) {
444
+ localStorage.setItem('kora-sync-url', newUrl.trim())
445
+ localStorage.setItem('kora-sync-configured', 'true')
446
+ }
447
+ onFactoryReset()
448
+ }
449
+ }
450
+
451
+ const handleDisconnect = () => {
452
+ onChangeServer(null)
453
+ }
454
+
455
+ const handleConnect = () => {
456
+ setEditing(true)
457
+ setNewUrl('')
458
+ }
459
+
460
+ return (
461
+ <div
462
+ style={{
463
+ marginBottom: '24px',
464
+ borderRadius: '8px',
465
+ border: '1px solid #1f2937',
466
+ background: '#111827',
467
+ overflow: 'hidden',
468
+ }}
469
+ >
470
+ {/* Server URL section */}
471
+ <div style={{ padding: '16px' }}>
472
+ <div
473
+ style={{
474
+ display: 'flex',
475
+ justifyContent: 'space-between',
476
+ alignItems: 'flex-start',
477
+ marginBottom: '12px',
478
+ }}
479
+ >
480
+ <p style={{ fontSize: '14px', fontWeight: '500', color: '#d1d5db' }}>Sync Server</p>
481
+ <button
482
+ onClick={onClose}
483
+ style={{
484
+ fontSize: '12px',
485
+ color: '#6b7280',
486
+ background: 'none',
487
+ border: 'none',
488
+ cursor: 'pointer',
489
+ }}
490
+ >
491
+ Close
492
+ </button>
493
+ </div>
494
+
495
+ {editing ? (
496
+ <div>
497
+ <input
498
+ type="text"
499
+ value={newUrl}
500
+ onChange={(e) => {
501
+ setNewUrl(e.target.value)
502
+ setTestResult(null)
503
+ }}
504
+ placeholder="wss://your-server.example.com/kora-sync"
505
+ style={{
506
+ width: '100%',
507
+ borderRadius: '6px',
508
+ border: '1px solid #374151',
509
+ background: '#0a0a0a',
510
+ padding: '8px 12px',
511
+ color: '#f3f4f6',
512
+ outline: 'none',
513
+ fontSize: '13px',
514
+ boxSizing: 'border-box',
515
+ marginBottom: '8px',
516
+ }}
517
+ />
518
+ <div style={{ display: 'flex', gap: '8px' }}>
519
+ <button
520
+ onClick={handleSaveUrl}
521
+ disabled={!newUrl.trim()}
522
+ style={btnStyle('#4f46e5', !newUrl.trim())}
523
+ >
524
+ Save
525
+ </button>
526
+ <button
527
+ onClick={handleTestConnection}
528
+ disabled={testing || !newUrl.trim()}
529
+ style={btnStyle('#374151', testing || !newUrl.trim())}
530
+ >
531
+ {testing ? 'Testing...' : 'Test'}
532
+ </button>
533
+ <button
534
+ onClick={() => {
535
+ setEditing(false)
536
+ setTestResult(null)
537
+ }}
538
+ style={btnStyle('#374151', false)}
539
+ >
540
+ Cancel
541
+ </button>
542
+ </div>
543
+ </div>
544
+ ) : (
545
+ <div>
546
+ <p
547
+ style={{
548
+ fontSize: '13px',
549
+ color: '#9ca3af',
550
+ marginBottom: '8px',
551
+ wordBreak: 'break-all',
552
+ }}
553
+ >
554
+ {syncUrl ?? 'Not connected — running in local-only mode'}
555
+ </p>
556
+ <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
557
+ {syncUrl ? (
558
+ <>
559
+ <button
560
+ onClick={() => {
561
+ setEditing(true)
562
+ setNewUrl(syncUrl)
563
+ }}
564
+ style={btnStyle('#374151', false)}
565
+ >
566
+ Change URL
567
+ </button>
568
+ <button
569
+ onClick={handleTestConnection}
570
+ disabled={testing}
571
+ style={btnStyle('#374151', testing)}
572
+ >
573
+ {testing ? 'Testing...' : 'Test Connection'}
574
+ </button>
575
+ <button onClick={handleDisconnect} style={btnStyle('#7f1d1d', false, '#ef4444')}>
576
+ Disconnect
577
+ </button>
578
+ </>
579
+ ) : (
580
+ <button onClick={handleConnect} style={btnStyle('#4f46e5', false)}>
581
+ Connect to Server
582
+ </button>
583
+ )}
584
+ </div>
585
+ </div>
586
+ )}
587
+
588
+ {/* Test result */}
589
+ {testResult && (
590
+ <p
591
+ style={{
592
+ fontSize: '12px',
593
+ marginTop: '8px',
594
+ color: testResult === 'success' ? '#34d399' : '#fbbf24',
595
+ }}
596
+ >
597
+ {testResult === 'success'
598
+ ? 'Connection successful'
599
+ : 'Server unreachable it may be down temporarily'}
600
+ </p>
601
+ )}
602
+ </div>
603
+
604
+ {/* Danger zone */}
605
+ <div style={{ padding: '12px 16px', borderTop: '1px solid #1f2937', background: '#0a0a0a' }}>
606
+ {showResetConfirm ? (
607
+ <div>
608
+ <div
609
+ style={{ display: 'flex', gap: '8px', alignItems: 'flex-start', marginBottom: '8px' }}
610
+ >
611
+ <AlertTriangle
612
+ style={{
613
+ width: '16px',
614
+ height: '16px',
615
+ color: '#ef4444',
616
+ flexShrink: 0,
617
+ marginTop: '1px',
618
+ }}
619
+ />
620
+ <p style={{ fontSize: '13px', color: '#fca5a5' }}>
621
+ This will delete all local data and reset the app to its initial state. This cannot
622
+ be undone.
623
+ </p>
624
+ </div>
625
+ <div style={{ display: 'flex', gap: '8px' }}>
626
+ <button onClick={onFactoryReset} style={btnStyle('#7f1d1d', false, '#ef4444')}>
627
+ Confirm Reset
628
+ </button>
629
+ <button onClick={() => setShowResetConfirm(false)} style={btnStyle('#374151', false)}>
630
+ Cancel
631
+ </button>
632
+ </div>
633
+ </div>
634
+ ) : (
635
+ <button
636
+ onClick={() => setShowResetConfirm(true)}
637
+ style={{
638
+ fontSize: '13px',
639
+ color: '#6b7280',
640
+ background: 'none',
641
+ border: 'none',
642
+ cursor: 'pointer',
643
+ }}
644
+ >
645
+ Reset app (clear all local data)
646
+ </button>
647
+ )}
648
+ </div>
649
+
650
+ {/* Server change confirmation dialog */}
651
+ {showChangeConfirm && (
652
+ <div
653
+ style={{
654
+ position: 'fixed',
655
+ inset: 0,
656
+ background: 'rgba(0,0,0,0.7)',
657
+ display: 'flex',
658
+ alignItems: 'center',
659
+ justifyContent: 'center',
660
+ zIndex: 100,
661
+ }}
662
+ >
663
+ <div
664
+ style={{
665
+ maxWidth: '420px',
666
+ width: '100%',
667
+ margin: '0 16px',
668
+ borderRadius: '12px',
669
+ border: '1px solid #374151',
670
+ background: '#111827',
671
+ padding: '24px',
672
+ }}
673
+ >
674
+ <div
675
+ style={{
676
+ display: 'flex',
677
+ gap: '12px',
678
+ alignItems: 'flex-start',
679
+ marginBottom: '16px',
680
+ }}
681
+ >
682
+ <AlertTriangle
683
+ style={{
684
+ width: '20px',
685
+ height: '20px',
686
+ color: '#fbbf24',
687
+ flexShrink: 0,
688
+ marginTop: '2px',
689
+ }}
690
+ />
691
+ <div>
692
+ <p
693
+ style={{
694
+ fontSize: '15px',
695
+ fontWeight: '500',
696
+ color: '#f3f4f6',
697
+ marginBottom: '8px',
698
+ }}
699
+ >
700
+ Changing sync servers
701
+ </p>
702
+ <p style={{ fontSize: '13px', color: '#9ca3af', lineHeight: '1.6' }}>
703
+ Existing local data was created for the current server. Keeping it and connecting
704
+ to a different server may sync this data to the wrong place.
705
+ </p>
706
+ </div>
707
+ </div>
708
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
709
+ <button
710
+ onClick={() => confirmChangeServer(false)}
711
+ style={{
712
+ width: '100%',
713
+ padding: '10px',
714
+ borderRadius: '8px',
715
+ border: '1px solid #374151',
716
+ background: '#4f46e5',
717
+ color: 'white',
718
+ fontSize: '14px',
719
+ cursor: 'pointer',
720
+ fontWeight: '500',
721
+ }}
722
+ >
723
+ Clear data and switch (recommended)
724
+ </button>
725
+ <button
726
+ onClick={() => confirmChangeServer(true)}
727
+ style={{
728
+ width: '100%',
729
+ padding: '10px',
730
+ borderRadius: '8px',
731
+ border: '1px solid #374151',
732
+ background: '#1f2937',
733
+ color: '#9ca3af',
734
+ fontSize: '14px',
735
+ cursor: 'pointer',
736
+ }}
737
+ >
738
+ Keep data and switch
739
+ </button>
740
+ <button
741
+ onClick={() => setShowChangeConfirm(false)}
742
+ style={{
743
+ width: '100%',
744
+ padding: '10px',
745
+ borderRadius: '8px',
746
+ border: 'none',
747
+ background: 'transparent',
748
+ color: '#6b7280',
749
+ fontSize: '14px',
750
+ cursor: 'pointer',
751
+ }}
752
+ >
753
+ Cancel
754
+ </button>
755
+ </div>
756
+ </div>
757
+ </div>
758
+ )}
759
+ </div>
760
+ )
543
761
  }
544
762
 
545
763
  function btnStyle(bg: string, disabled: boolean, color = '#d1d5db'): React.CSSProperties {
546
- return {
547
- fontSize: '13px',
548
- color,
549
- background: bg,
550
- border: 'none',
551
- borderRadius: '6px',
552
- padding: '6px 12px',
553
- cursor: disabled ? 'default' : 'pointer',
554
- opacity: disabled ? 0.5 : 1,
555
- }
764
+ return {
765
+ fontSize: '13px',
766
+ color,
767
+ background: bg,
768
+ border: 'none',
769
+ borderRadius: '6px',
770
+ padding: '6px 12px',
771
+ cursor: disabled ? 'default' : 'pointer',
772
+ opacity: disabled ? 0.5 : 1,
773
+ }
556
774
  }
557
775
 
558
776
  // ---------------------------------------------------------------------------
@@ -560,19 +778,26 @@ function btnStyle(bg: string, disabled: boolean, color = '#d1d5db'): React.CSSPr
560
778
  // ---------------------------------------------------------------------------
561
779
 
562
780
  function StatCard({ label, value, color }: { label: string; value: number; color: string }) {
563
- return (
564
- <div style={{ borderRadius: '8px', border: '1px solid #1f2937', background: '#111827', padding: '16px' }}>
565
- <p style={{ fontSize: '14px', color: '#6b7280' }}>{label}</p>
566
- <p style={{ fontSize: '24px', fontWeight: 'bold', color }}>{value}</p>
567
- </div>
568
- )
781
+ return (
782
+ <div
783
+ style={{
784
+ borderRadius: '8px',
785
+ border: '1px solid #1f2937',
786
+ background: '#111827',
787
+ padding: '16px',
788
+ }}
789
+ >
790
+ <p style={{ fontSize: '14px', color: '#6b7280' }}>{label}</p>
791
+ <p style={{ fontSize: '24px', fontWeight: 'bold', color }}>{value}</p>
792
+ </div>
793
+ )
569
794
  }
570
795
 
571
796
  function formatTime(timestamp: number): string {
572
- const date = new Date(timestamp)
573
- const now = new Date()
574
- if (date.toDateString() === now.toDateString()) {
575
- return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
576
- }
577
- return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
797
+ const date = new Date(timestamp)
798
+ const now = new Date()
799
+ if (date.toDateString() === now.toDateString()) {
800
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
801
+ }
802
+ return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
578
803
  }