@liorandb/studio 0.0.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.
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "liorandb-studio",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@liorandb/driver": "^1.0.4",
13
+ "@monaco-editor/react": "^4.7.0",
14
+ "clsx": "^2.1.1",
15
+ "lucide-react": "^0.563.0",
16
+ "next": "16.1.6",
17
+ "react": "19.2.3",
18
+ "react-dom": "19.2.3",
19
+ "zustand": "^5.0.11"
20
+ },
21
+ "devDependencies": {
22
+ "@tailwindcss/postcss": "^4",
23
+ "@types/node": "^20",
24
+ "@types/react": "^19",
25
+ "@types/react-dom": "^19",
26
+ "eslint": "^9",
27
+ "eslint-config-next": "16.1.6",
28
+ "tailwindcss": "^4",
29
+ "typescript": "^5"
30
+ }
31
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,240 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useAppStore } from '@/store';
6
+ import { LioranDBService } from '@/lib/lioran';
7
+ import { Sidebar } from '@/components/Sidebar';
8
+ import { Navbar } from '@/components/Navbar';
9
+ import { DocumentViewer } from '@/components/DocumentViewer';
10
+ import { QueryEditor } from '@/components/QueryEditor';
11
+ import { InputModal, JsonInputModal } from '@/components/Modal';
12
+ import { useToast } from '@/components/Toast';
13
+ import { Document } from '@/types';
14
+
15
+ export default function DashboardPage() {
16
+ const router = useRouter();
17
+ const { addToast } = useToast();
18
+
19
+ const {
20
+ isLoggedIn,
21
+ currentDatabase,
22
+ selectedCollection,
23
+ logout,
24
+ setCurrentDatabase,
25
+ setSelectedCollection,
26
+ setDatabases,
27
+ setCollections,
28
+ } = useAppStore();
29
+
30
+ // Modal states
31
+ const [createDbModal, setCreateDbModal] = useState(false);
32
+ const [createColModal, setCreateColModal] = useState(false);
33
+ const [addDocModal, setAddDocModal] = useState(false);
34
+ const [editDocModal, setEditDocModal] = useState(false);
35
+ const [editingDoc, setEditingDoc] = useState<Document | null>(null);
36
+
37
+ // Check authentication
38
+ useEffect(() => {
39
+ if (!isLoggedIn) {
40
+ router.push('/login');
41
+ }
42
+ }, [isLoggedIn, router]);
43
+
44
+ // Load databases on mount
45
+ useEffect(() => {
46
+ if (isLoggedIn) {
47
+ loadDatabases();
48
+ }
49
+ }, [isLoggedIn]);
50
+
51
+ // Load collections when database changes
52
+ useEffect(() => {
53
+ if (currentDatabase) {
54
+ loadCollections(currentDatabase);
55
+ }
56
+ }, [currentDatabase]);
57
+
58
+ async function loadDatabases() {
59
+ try {
60
+ const databases = await LioranDBService.listDatabases();
61
+ setDatabases(databases);
62
+ } catch (error) {
63
+ addToast(`Error loading databases: ${error}`, 'error');
64
+ }
65
+ }
66
+
67
+ async function loadCollections(dbName: string) {
68
+ try {
69
+ const collections = await LioranDBService.listCollections(dbName);
70
+ setCollections(dbName, collections);
71
+ } catch (error) {
72
+ addToast(`Error loading collections: ${error}`, 'error');
73
+ }
74
+ }
75
+
76
+ async function handleCreateDatabase(name: string) {
77
+ try {
78
+ await LioranDBService.createDatabase(name);
79
+ await loadDatabases();
80
+ setCurrentDatabase(name);
81
+ addToast(`Database "${name}" created`, 'success');
82
+ } catch (error) {
83
+ addToast(`Error creating database: ${error}`, 'error');
84
+ }
85
+ }
86
+
87
+ async function handleCreateCollection(name: string) {
88
+ if (!currentDatabase) return;
89
+ try {
90
+ await LioranDBService.createCollection(currentDatabase, name);
91
+ await loadCollections(currentDatabase);
92
+ setSelectedCollection(name);
93
+ addToast(`Collection "${name}" created`, 'success');
94
+ } catch (error) {
95
+ addToast(`Error creating collection: ${error}`, 'error');
96
+ }
97
+ }
98
+
99
+ async function handleAddDocument(doc: Record<string, any>) {
100
+ if (!currentDatabase || !selectedCollection) return;
101
+ try {
102
+ await LioranDBService.insertOne(currentDatabase, selectedCollection, doc);
103
+ addToast('Document added', 'success');
104
+ setAddDocModal(false);
105
+ // Reload documents
106
+ const docViewer = document.querySelector('[data-reload-documents]');
107
+ if (docViewer) {
108
+ const event = new CustomEvent('reload');
109
+ docViewer.dispatchEvent(event);
110
+ }
111
+ } catch (error) {
112
+ addToast(`Error adding document: ${error}`, 'error');
113
+ }
114
+ }
115
+
116
+ async function handleLogout() {
117
+ if (confirm('Are you sure you want to logout?')) {
118
+ logout();
119
+ LioranDBService.disconnect();
120
+ router.push('/login');
121
+ }
122
+ }
123
+
124
+ return (
125
+ <div className="h-screen flex flex-col bg-slate-950">
126
+ {/* Navbar */}
127
+ <Navbar onLogout={handleLogout} />
128
+
129
+ {/* Main Content */}
130
+ <div className="flex-1 flex overflow-hidden">
131
+ {/* Sidebar */}
132
+ <Sidebar
133
+ onDatabaseSelect={setCurrentDatabase}
134
+ onCollectionSelect={(db, col) => {
135
+ setCurrentDatabase(db);
136
+ setSelectedCollection(col);
137
+ }}
138
+ onCreateDatabase={() => setCreateDbModal(true)}
139
+ onCreateCollection={() => setCreateColModal(true)}
140
+ />
141
+
142
+ {/* Workspace */}
143
+ <div className="flex-1 flex flex-col overflow-hidden">
144
+ {!currentDatabase ? (
145
+ // Empty State
146
+ <div className="flex-1 flex items-center justify-center text-slate-400">
147
+ <div className="text-center">
148
+ <div className="text-4xl mb-4">📦</div>
149
+ <p className="text-lg">No database selected</p>
150
+ <p className="text-sm text-slate-500">Create or select a database to get started</p>
151
+ </div>
152
+ </div>
153
+ ) : !selectedCollection ? (
154
+ // Collection Selection State
155
+ <div className="flex-1 flex items-center justify-center text-slate-400">
156
+ <div className="text-center">
157
+ <div className="text-4xl mb-4">📚</div>
158
+ <p className="text-lg">No collection selected</p>
159
+ <p className="text-sm text-slate-500">Select or create a collection to view documents</p>
160
+ </div>
161
+ </div>
162
+ ) : (
163
+ // Two Panel Layout
164
+ <div className="flex-1 flex overflow-hidden gap-4 p-4 bg-slate-900">
165
+ {/* Left: Document Viewer */}
166
+ <div className="flex-1 bg-slate-950 rounded-lg border border-slate-800 overflow-hidden flex flex-col">
167
+ <DocumentViewer
168
+ onAddDocument={() => setAddDocModal(true)}
169
+ onEditDocument={(doc) => {
170
+ setEditingDoc(doc);
171
+ setEditDocModal(true);
172
+ }}
173
+ />
174
+ </div>
175
+
176
+ {/* Right: Query Editor */}
177
+ <div className="w-96 bg-slate-950 rounded-lg border border-slate-800 overflow-hidden">
178
+ <QueryEditor />
179
+ </div>
180
+ </div>
181
+ )}
182
+ </div>
183
+ </div>
184
+
185
+ {/* Modals */}
186
+ <InputModal
187
+ isOpen={createDbModal}
188
+ title="Create Database"
189
+ label="Database Name"
190
+ placeholder="mydb"
191
+ onClose={() => setCreateDbModal(false)}
192
+ onConfirm={handleCreateDatabase}
193
+ />
194
+
195
+ <InputModal
196
+ isOpen={createColModal}
197
+ title="Create Collection"
198
+ label="Collection Name"
199
+ placeholder="users"
200
+ onClose={() => setCreateColModal(false)}
201
+ onConfirm={handleCreateCollection}
202
+ />
203
+
204
+ <JsonInputModal
205
+ isOpen={addDocModal}
206
+ title="Add Document"
207
+ defaultValue='{}'
208
+ onClose={() => setAddDocModal(false)}
209
+ onConfirm={handleAddDocument}
210
+ />
211
+
212
+ <JsonInputModal
213
+ isOpen={editDocModal}
214
+ title="Edit Document"
215
+ defaultValue={editingDoc ? JSON.stringify(editingDoc, null, 2) : '{}'}
216
+ onClose={() => {
217
+ setEditDocModal(false);
218
+ setEditingDoc(null);
219
+ }}
220
+ onConfirm={async (doc) => {
221
+ if (!currentDatabase || !selectedCollection || !editingDoc) return;
222
+ try {
223
+ const _id = editingDoc._id;
224
+ await LioranDBService.updateMany(
225
+ currentDatabase,
226
+ selectedCollection,
227
+ { _id },
228
+ { $set: doc }
229
+ );
230
+ addToast('Document updated', 'success');
231
+ setEditDocModal(false);
232
+ setEditingDoc(null);
233
+ } catch (error) {
234
+ addToast(`Error updating document: ${error}`, 'error');
235
+ }
236
+ }}
237
+ />
238
+ </div>
239
+ );
240
+ }
@@ -0,0 +1,233 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import { useAppStore } from '@/store';
6
+ import { LioranDBService } from '@/lib/lioran';
7
+ import { parseConnectionUri } from '@/lib/utils';
8
+ import { useToast } from '@/components/Toast';
9
+
10
+ export default function LoginPage() {
11
+ const router = useRouter();
12
+ const { addToast } = useToast();
13
+ const { setLoggedIn } = useAppStore();
14
+
15
+ const [uri, setUri] = useState('');
16
+ const [isLoading, setIsLoading] = useState(false);
17
+ const [showAdvanced, setShowAdvanced] = useState(false);
18
+
19
+ const [username, setUsername] = useState('admin');
20
+ const [password, setPassword] = useState('');
21
+ const [host, setHost] = useState('localhost');
22
+ const [port, setPort] = useState('4000');
23
+
24
+ useEffect(() => {
25
+ // Load saved URI from localStorage
26
+ const savedUri = localStorage.getItem('liorandb_uri');
27
+ const savedToken = localStorage.getItem('liorandb_token');
28
+
29
+ if (savedUri && savedToken) {
30
+ // Try to auto-login
31
+ attemptAutoLogin(savedUri, savedToken);
32
+ }
33
+ }, []);
34
+
35
+ async function attemptAutoLogin(uri: string, token: string) {
36
+ try {
37
+ setIsLoading(true);
38
+ LioranDBService.initialize(uri);
39
+
40
+ // Test connection
41
+ const databases = await LioranDBService.listDatabases();
42
+
43
+ setLoggedIn(true, token, uri);
44
+ useAppStore.setState({ databases });
45
+ addToast('Connected successfully', 'success');
46
+ router.push('/dashboard');
47
+ } catch (error) {
48
+ localStorage.removeItem('liorandb_token');
49
+ localStorage.removeItem('liorandb_uri');
50
+ addToast(`Connection failed: ${error}`, 'error');
51
+ } finally {
52
+ setIsLoading(false);
53
+ }
54
+ }
55
+
56
+ async function handleLogin(e: React.FormEvent) {
57
+ e.preventDefault();
58
+
59
+ const loginUri = showAdvanced
60
+ ? `lioran://${username}:${password}@${host}:${port}`
61
+ : uri;
62
+
63
+ if (!loginUri) {
64
+ addToast('Please enter a connection URI', 'warning');
65
+ return;
66
+ }
67
+
68
+ try {
69
+ setIsLoading(true);
70
+
71
+ // Validate URI format
72
+ try {
73
+ parseConnectionUri(loginUri);
74
+ } catch (err) {
75
+ addToast(String(err), 'error');
76
+ return;
77
+ }
78
+
79
+ // Initialize client
80
+ await LioranDBService.initialize(loginUri);
81
+
82
+ // List databases to verify connection
83
+ const databases = await LioranDBService.listDatabases();
84
+
85
+ // Store session (in a real app, you'd get a token from the server)
86
+ const token = `token_${Date.now()}`;
87
+
88
+ setLoggedIn(true, token, loginUri);
89
+ useAppStore.setState({ databases });
90
+
91
+ addToast('Connected successfully', 'success');
92
+ router.push('/dashboard');
93
+ } catch (error) {
94
+ addToast(`Login failed: ${error}`, 'error');
95
+ } finally {
96
+ setIsLoading(false);
97
+ }
98
+ }
99
+
100
+ return (
101
+ <div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-800 flex items-center justify-center p-4">
102
+ {/* Background Effect */}
103
+ <div className="absolute inset-0 overflow-hidden">
104
+ <div className="absolute -top-40 -right-40 w-80 h-80 bg-emerald-500/5 rounded-full blur-3xl" />
105
+ <div className="absolute -bottom-40 -left-40 w-80 h-80 bg-cyan-500/5 rounded-full blur-3xl" />
106
+ </div>
107
+
108
+ {/* Login Card */}
109
+ <div className="relative w-full max-w-md">
110
+ <div className="bg-slate-900/80 backdrop-blur border border-slate-800 rounded-xl shadow-2xl overflow-hidden">
111
+ {/* Header */}
112
+ <div className="px-8 pt-8 pb-6 border-b border-slate-800">
113
+ <div className="flex items-center gap-3 mb-2">
114
+ <div className="w-10 h-10 bg-gradient-to-br from-emerald-400 to-cyan-400 rounded-lg flex items-center justify-center">
115
+ <span className="text-slate-900 font-bold text-lg">⚡</span>
116
+ </div>
117
+ <h1 className="text-2xl font-bold text-slate-100">LioranDB</h1>
118
+ </div>
119
+ <p className="text-slate-400 text-sm">Database Studio</p>
120
+ </div>
121
+
122
+ {/* Form */}
123
+ <form onSubmit={handleLogin} className="px-8 py-8 space-y-6">
124
+ {!showAdvanced ? (
125
+ // Quick Connect
126
+ <div>
127
+ <label className="block text-sm font-medium text-slate-300 mb-2">
128
+ Connection URI
129
+ </label>
130
+ <input
131
+ type="text"
132
+ value={uri}
133
+ onChange={(e) => setUri(e.target.value)}
134
+ placeholder="lioran://admin:password@localhost:4000"
135
+ className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-3 text-slate-100 placeholder-slate-500 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition"
136
+ disabled={isLoading}
137
+ />
138
+ </div>
139
+ ) : (
140
+ // Advanced Connect
141
+ <div className="space-y-4">
142
+ <div>
143
+ <label className="block text-sm font-medium text-slate-300 mb-2">
144
+ Username
145
+ </label>
146
+ <input
147
+ type="text"
148
+ value={username}
149
+ onChange={(e) => setUsername(e.target.value)}
150
+ className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-3 text-slate-100 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition"
151
+ disabled={isLoading}
152
+ />
153
+ </div>
154
+
155
+ <div>
156
+ <label className="block text-sm font-medium text-slate-300 mb-2">
157
+ Password
158
+ </label>
159
+ <input
160
+ type="password"
161
+ value={password}
162
+ onChange={(e) => setPassword(e.target.value)}
163
+ className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-3 text-slate-100 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition"
164
+ disabled={isLoading}
165
+ />
166
+ </div>
167
+
168
+ <div className="grid grid-cols-2 gap-4">
169
+ <div>
170
+ <label className="block text-sm font-medium text-slate-300 mb-2">
171
+ Host
172
+ </label>
173
+ <input
174
+ type="text"
175
+ value={host}
176
+ onChange={(e) => setHost(e.target.value)}
177
+ className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-3 text-slate-100 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition"
178
+ disabled={isLoading}
179
+ />
180
+ </div>
181
+
182
+ <div>
183
+ <label className="block text-sm font-medium text-slate-300 mb-2">
184
+ Port
185
+ </label>
186
+ <input
187
+ type="text"
188
+ value={port}
189
+ onChange={(e) => setPort(e.target.value)}
190
+ className="w-full bg-slate-800/50 border border-slate-700 rounded-lg px-4 py-3 text-slate-100 focus:outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20 transition"
191
+ disabled={isLoading}
192
+ />
193
+ </div>
194
+ </div>
195
+ </div>
196
+ )}
197
+
198
+ {/* Toggle Advanced */}
199
+ <button
200
+ type="button"
201
+ onClick={() => setShowAdvanced(!showAdvanced)}
202
+ className="text-sm text-emerald-400 hover:text-emerald-300 transition"
203
+ disabled={isLoading}
204
+ >
205
+ {showAdvanced ? '← Back to Quick Connect' : 'Advanced Options →'}
206
+ </button>
207
+
208
+ {/* Submit Button */}
209
+ <button
210
+ type="submit"
211
+ disabled={isLoading || (!showAdvanced && !uri) || (showAdvanced && !password)}
212
+ className="w-full bg-gradient-to-r from-emerald-600 to-emerald-500 hover:from-emerald-700 hover:to-emerald-600 text-white font-semibold py-3 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
213
+ >
214
+ {isLoading ? 'Connecting...' : 'Connect to LioranDB'}
215
+ </button>
216
+ </form>
217
+
218
+ {/* Footer */}
219
+ <div className="px-8 py-6 bg-slate-800/30 border-t border-slate-800">
220
+ <p className="text-xs text-slate-400 text-center">
221
+ Default: <code className="bg-slate-900 px-2 py-1 rounded">lioran://admin:password@localhost:4000</code>
222
+ </p>
223
+ </div>
224
+ </div>
225
+
226
+ {/* Beta Badge */}
227
+ <div className="absolute -top-3 right-4 bg-amber-500 text-slate-900 px-3 py-1 rounded-full text-xs font-semibold">
228
+ BETA
229
+ </div>
230
+ </div>
231
+ </div>
232
+ );
233
+ }