@mountsqli/studio 0.1.6 → 0.1.7

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 (2) hide show
  1. package/dist/index.js +1063 -306
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,40 +6,91 @@ function getDashboardHTML() {
6
6
  <meta charset="UTF-8">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
8
  <title>MountSQLi Studio</title>
9
- <style>
10
- ${getCSS()}
11
- </style>
9
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>\u25C6</text></svg>">
10
+ <style>${getCSS()}</style>
12
11
  </head>
13
12
  <body>
14
13
  <div id="app">
15
- <nav id="sidebar">
16
- <div class="logo">
17
- <span class="logo-icon">\u25C6</span>
18
- <span class="logo-text">MountSQLi</span>
19
- <span class="logo-badge">Studio</span>
14
+ <!-- Top bar -->
15
+ <header id="topbar">
16
+ <div class="topbar-left">
17
+ <div class="logo">
18
+ <span class="logo-icon">\u25C6</span>
19
+ <span class="logo-text">MountSQLi</span>
20
+ <span class="logo-badge">Studio</span>
21
+ </div>
22
+ </div>
23
+ <div class="topbar-center">
24
+ <div class="search-box">
25
+ <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
26
+ <input type="text" id="global-search" placeholder="Search tables, columns... (Ctrl+K)" autocomplete="off">
27
+ <kbd class="search-kbd">\u2318K</kbd>
28
+ </div>
20
29
  </div>
21
- <ul class="nav-links">
22
- <li><a href="#" data-page="tables" class="active">\u{1F4CA} Tables</a></li>
23
- <li><a href="#" data-page="query">\u{1F4BB} Query</a></li>
24
- <li><a href="#" data-page="erd">\u{1F517} ER Diagram</a></li>
25
- <li><a href="#" data-page="migrations">\u{1F4E6} Migrations</a></li>
26
- <li><a href="#" data-page="settings">\u2699\uFE0F Settings</a></li>
27
- </ul>
28
- <div class="nav-footer">
29
- <div id="connection-status" class="status-badge status-ok">\u25CF Connected</div>
30
+ <div class="topbar-right">
31
+ <div id="connection-pill" class="pill pill-ok">
32
+ <span class="pill-dot"></span>
33
+ <span id="conn-dialect">PostgreSQL</span>
34
+ </div>
35
+ </div>
36
+ </header>
37
+
38
+ <div id="body">
39
+ <!-- Sidebar -->
40
+ <nav id="sidebar">
41
+ <div class="nav-section">
42
+ <div class="nav-label">Explorer</div>
43
+ <a href="#" data-page="tables" class="nav-link active">
44
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
45
+ <span>Tables</span>
46
+ </a>
47
+ <a href="#" data-page="query" class="nav-link">
48
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
49
+ <span>Query</span>
50
+ </a>
51
+ <a href="#" data-page="erd" class="nav-link">
52
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="5" r="3"/><circle cx="5" cy="19" r="3"/><circle cx="19" cy="19" r="3"/><line x1="12" y1="8" x2="5" y2="16"/><line x1="12" y1="8" x2="19" y2="16"/></svg>
53
+ <span>ER Diagram</span>
54
+ </a>
55
+ <a href="#" data-page="migrations" class="nav-link">
56
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
57
+ <span>Migrations</span>
58
+ </a>
59
+ </div>
60
+ <div class="nav-section">
61
+ <div class="nav-label">System</div>
62
+ <a href="#" data-page="settings" class="nav-link">
63
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
64
+ <span>Settings</span>
65
+ </a>
66
+ </div>
67
+ </nav>
68
+
69
+ <!-- Main content -->
70
+ <main id="content">
71
+ <div id="page-tables" class="page active"></div>
72
+ <div id="page-query" class="page"></div>
73
+ <div id="page-erd" class="page"></div>
74
+ <div id="page-migrations" class="page"></div>
75
+ <div id="page-settings" class="page"></div>
76
+ </main>
77
+ </div>
78
+ </div>
79
+
80
+ <!-- Search modal -->
81
+ <div id="search-modal" class="modal" style="display:none">
82
+ <div class="modal-backdrop"></div>
83
+ <div class="modal-content search-modal-content">
84
+ <div class="search-modal-input">
85
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
86
+ <input type="text" id="modal-search-input" placeholder="Search tables, columns..." autocomplete="off">
87
+ <kbd>ESC</kbd>
30
88
  </div>
31
- </nav>
32
- <main id="content">
33
- <div id="page-tables" class="page active"></div>
34
- <div id="page-query" class="page"></div>
35
- <div id="page-erd" class="page"></div>
36
- <div id="page-migrations" class="page"></div>
37
- <div id="page-settings" class="page"></div>
38
- </main>
89
+ <div id="search-results" class="search-results"></div>
90
+ </div>
39
91
  </div>
40
- <script>
41
- ${getJS()}
42
- </script>
92
+
93
+ <script>${getJS()}</script>
43
94
  </body>
44
95
  </html>`;
45
96
  }
@@ -48,367 +99,767 @@ function getCSS() {
48
99
  * { margin: 0; padding: 0; box-sizing: border-box; }
49
100
 
50
101
  :root {
51
- --bg: #0f1117;
52
- --bg-surface: #161822;
53
- --bg-elevated: #1e2030;
54
- --bg-hover: #252840;
55
- --border: #2e3148;
56
- --text: #e0e0e0;
57
- --text-dim: #8888a0;
58
- --accent: #7c5cfc;
59
- --accent-hover: #9b7ffd;
60
- --success: #4ade80;
61
- --warning: #fbbf24;
62
- --error: #f87171;
63
- --radius: 8px;
64
- --font: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
65
- --font-ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
102
+ --bg-0: #0a0b10;
103
+ --bg-1: #111219;
104
+ --bg-2: #181924;
105
+ --bg-3: #1f2133;
106
+ --bg-4: #272a3f;
107
+ --border: rgba(255,255,255,0.06);
108
+ --border-strong: rgba(255,255,255,0.12);
109
+ --text: #e8eaed;
110
+ --text-2: #9ba1b0;
111
+ --text-3: #5f6577;
112
+ --accent: #6c5ce7;
113
+ --accent-2: #a29bfe;
114
+ --accent-bg: rgba(108,92,231,0.12);
115
+ --green: #00d68f;
116
+ --green-bg: rgba(0,214,143,0.1);
117
+ --yellow: #ffc048;
118
+ --yellow-bg: rgba(255,192,72,0.1);
119
+ --red: #ff6b6b;
120
+ --red-bg: rgba(255,107,107,0.1);
121
+ --blue: #4fc3f7;
122
+ --blue-bg: rgba(79,195,247,0.1);
123
+ --radius-sm: 6px;
124
+ --radius: 10px;
125
+ --radius-lg: 14px;
126
+ --shadow: 0 2px 8px rgba(0,0,0,0.3);
127
+ --shadow-lg: 0 8px 32px rgba(0,0,0,0.4);
128
+ --font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'JetBrains Mono', monospace;
129
+ --font: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
130
+ --transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
66
131
  }
67
132
 
68
133
  body {
69
- font-family: var(--font-ui);
70
- background: var(--bg);
134
+ font-family: var(--font);
135
+ background: var(--bg-0);
71
136
  color: var(--text);
72
137
  height: 100vh;
73
138
  overflow: hidden;
139
+ -webkit-font-smoothing: antialiased;
74
140
  }
75
141
 
76
142
  #app {
77
143
  display: flex;
144
+ flex-direction: column;
78
145
  height: 100vh;
79
146
  }
80
147
 
81
- /* Sidebar */
82
- #sidebar {
83
- width: 220px;
84
- background: var(--bg-surface);
85
- border-right: 1px solid var(--border);
148
+ /* ---- Top Bar ---- */
149
+ #topbar {
150
+ height: 52px;
151
+ background: var(--bg-1);
152
+ border-bottom: 1px solid var(--border);
86
153
  display: flex;
87
- flex-direction: column;
88
- padding: 16px 0;
154
+ align-items: center;
155
+ padding: 0 16px;
156
+ gap: 16px;
157
+ flex-shrink: 0;
158
+ z-index: 10;
89
159
  }
90
160
 
161
+ .topbar-left { display: flex; align-items: center; }
162
+ .topbar-center { flex: 1; display: flex; justify-content: center; max-width: 480px; margin: 0 auto; }
163
+ .topbar-right { display: flex; align-items: center; gap: 12px; }
164
+
91
165
  .logo {
92
166
  display: flex;
93
167
  align-items: center;
94
168
  gap: 8px;
95
- padding: 0 20px 20px;
96
- border-bottom: 1px solid var(--border);
97
169
  }
98
170
 
99
- .logo-icon { font-size: 20px; color: var(--accent); }
100
- .logo-text { font-weight: 700; font-size: 15px; }
171
+ .logo-icon { font-size: 18px; color: var(--accent); }
172
+ .logo-text { font-weight: 700; font-size: 14px; letter-spacing: -0.3px; }
101
173
  .logo-badge {
102
- font-size: 10px;
174
+ font-size: 9px;
103
175
  background: var(--accent);
104
176
  color: white;
105
177
  padding: 2px 6px;
106
178
  border-radius: 4px;
179
+ font-weight: 700;
180
+ letter-spacing: 0.5px;
181
+ text-transform: uppercase;
182
+ }
183
+
184
+ .search-box {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 8px;
188
+ background: var(--bg-2);
189
+ border: 1px solid var(--border);
190
+ border-radius: var(--radius);
191
+ padding: 6px 12px;
192
+ width: 100%;
193
+ cursor: text;
194
+ transition: border-color var(--transition);
195
+ }
196
+
197
+ .search-box:focus-within { border-color: var(--accent); }
198
+ .search-icon { color: var(--text-3); flex-shrink: 0; }
199
+ .search-box input {
200
+ background: none;
201
+ border: none;
202
+ outline: none;
203
+ color: var(--text);
204
+ font-size: 13px;
205
+ width: 100%;
206
+ font-family: var(--font);
207
+ }
208
+ .search-box input::placeholder { color: var(--text-3); }
209
+ .search-kbd {
210
+ font-size: 10px;
211
+ padding: 2px 5px;
212
+ border-radius: 4px;
213
+ background: var(--bg-3);
214
+ border: 1px solid var(--border);
215
+ color: var(--text-3);
216
+ font-family: var(--font);
217
+ flex-shrink: 0;
218
+ }
219
+
220
+ .pill {
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 6px;
224
+ padding: 4px 10px;
225
+ border-radius: 20px;
226
+ font-size: 11px;
107
227
  font-weight: 600;
108
228
  }
109
229
 
110
- .nav-links {
111
- list-style: none;
112
- padding: 12px 0;
230
+ .pill-ok { background: var(--green-bg); color: var(--green); }
231
+ .pill-err { background: var(--red-bg); color: var(--red); }
232
+ .pill-dot {
233
+ width: 6px;
234
+ height: 6px;
235
+ border-radius: 50%;
236
+ background: currentColor;
237
+ animation: pulse 2s infinite;
238
+ }
239
+
240
+ @keyframes pulse {
241
+ 0%, 100% { opacity: 1; }
242
+ 50% { opacity: 0.4; }
243
+ }
244
+
245
+ /* ---- Body ---- */
246
+ #body {
247
+ display: flex;
113
248
  flex: 1;
249
+ overflow: hidden;
114
250
  }
115
251
 
116
- .nav-links a {
117
- display: block;
118
- padding: 10px 20px;
119
- color: var(--text-dim);
120
- text-decoration: none;
121
- font-size: 13px;
122
- transition: all 0.15s;
252
+ /* ---- Sidebar ---- */
253
+ #sidebar {
254
+ width: 200px;
255
+ background: var(--bg-1);
256
+ border-right: 1px solid var(--border);
257
+ display: flex;
258
+ flex-direction: column;
259
+ padding: 12px 8px;
260
+ gap: 16px;
261
+ flex-shrink: 0;
123
262
  }
124
263
 
125
- .nav-links a:hover { background: var(--bg-hover); color: var(--text); }
126
- .nav-links a.active { color: var(--accent); background: var(--bg-hover); border-right: 2px solid var(--accent); }
264
+ .nav-section { display: flex; flex-direction: column; gap: 2px; }
265
+ .nav-label {
266
+ font-size: 10px;
267
+ font-weight: 700;
268
+ text-transform: uppercase;
269
+ letter-spacing: 0.8px;
270
+ color: var(--text-3);
271
+ padding: 4px 12px 6px;
272
+ }
127
273
 
128
- .nav-footer {
129
- padding: 12px 20px;
130
- border-top: 1px solid var(--border);
274
+ .nav-link {
275
+ display: flex;
276
+ align-items: center;
277
+ gap: 10px;
278
+ padding: 8px 12px;
279
+ border-radius: var(--radius-sm);
280
+ color: var(--text-2);
281
+ text-decoration: none;
282
+ font-size: 13px;
283
+ font-weight: 500;
284
+ transition: all var(--transition);
131
285
  }
132
286
 
133
- .status-badge {
134
- font-size: 11px;
135
- padding: 4px 8px;
136
- border-radius: 4px;
137
- display: inline-block;
287
+ .nav-link:hover { background: var(--bg-3); color: var(--text); }
288
+ .nav-link.active {
289
+ background: var(--accent-bg);
290
+ color: var(--accent-2);
138
291
  }
139
292
 
140
- .status-ok { color: var(--success); }
141
- .status-err { color: var(--error); }
293
+ .nav-link svg { flex-shrink: 0; opacity: 0.7; }
294
+ .nav-link.active svg { opacity: 1; }
142
295
 
143
- /* Main content */
296
+ /* ---- Main Content ---- */
144
297
  #content {
145
298
  flex: 1;
146
299
  overflow: auto;
147
- padding: 24px;
300
+ padding: 20px 24px;
148
301
  }
149
302
 
150
- .page { display: none; }
303
+ .page { display: none; animation: fadeIn 200ms ease; }
151
304
  .page.active { display: block; }
152
305
 
153
- /* Tables page */
306
+ @keyframes fadeIn {
307
+ from { opacity: 0; transform: translateY(4px); }
308
+ to { opacity: 1; transform: translateY(0); }
309
+ }
310
+
311
+ .page-title {
312
+ font-size: 20px;
313
+ font-weight: 700;
314
+ margin-bottom: 4px;
315
+ letter-spacing: -0.3px;
316
+ }
317
+
318
+ .page-subtitle {
319
+ font-size: 13px;
320
+ color: var(--text-3);
321
+ margin-bottom: 20px;
322
+ }
323
+
324
+ /* ---- Cards ---- */
325
+ .card {
326
+ background: var(--bg-2);
327
+ border: 1px solid var(--border);
328
+ border-radius: var(--radius-lg);
329
+ overflow: hidden;
330
+ }
331
+
332
+ .card-header {
333
+ padding: 14px 18px;
334
+ border-bottom: 1px solid var(--border);
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: space-between;
338
+ }
339
+
340
+ .card-title {
341
+ font-size: 13px;
342
+ font-weight: 600;
343
+ display: flex;
344
+ align-items: center;
345
+ gap: 8px;
346
+ }
347
+
348
+ .card-body { padding: 0; }
349
+
350
+ /* ---- Table Grid ---- */
154
351
  .table-grid {
155
352
  display: grid;
156
- grid-template-columns: 220px 1fr;
353
+ grid-template-columns: 240px 1fr;
157
354
  gap: 16px;
158
- height: calc(100vh - 120px);
355
+ height: calc(100vh - 140px);
159
356
  }
160
357
 
161
- .table-list {
162
- background: var(--bg-surface);
163
- border-radius: var(--radius);
358
+ .table-sidebar {
359
+ display: flex;
360
+ flex-direction: column;
361
+ gap: 12px;
362
+ }
363
+
364
+ .table-search {
365
+ display: flex;
366
+ align-items: center;
367
+ gap: 8px;
368
+ background: var(--bg-2);
164
369
  border: 1px solid var(--border);
165
- overflow-y: auto;
370
+ border-radius: var(--radius);
371
+ padding: 8px 12px;
166
372
  }
167
373
 
168
- .table-list-header {
169
- padding: 12px 16px;
170
- font-weight: 600;
171
- font-size: 13px;
172
- border-bottom: 1px solid var(--border);
173
- color: var(--text-dim);
374
+ .table-search input {
375
+ background: none;
376
+ border: none;
377
+ outline: none;
378
+ color: var(--text);
379
+ font-size: 12px;
380
+ width: 100%;
381
+ font-family: var(--font);
382
+ }
383
+
384
+ .table-search input::placeholder { color: var(--text-3); }
385
+
386
+ .table-list {
387
+ flex: 1;
388
+ overflow-y: auto;
389
+ display: flex;
390
+ flex-direction: column;
391
+ gap: 1px;
174
392
  }
175
393
 
176
394
  .table-item {
177
- padding: 10px 16px;
395
+ padding: 9px 14px;
178
396
  cursor: pointer;
179
397
  font-size: 13px;
180
- border-bottom: 1px solid var(--border);
181
- transition: background 0.1s;
398
+ font-family: var(--font-mono);
399
+ border-radius: var(--radius-sm);
400
+ transition: all var(--transition);
401
+ display: flex;
402
+ align-items: center;
403
+ gap: 8px;
404
+ color: var(--text-2);
182
405
  }
183
406
 
184
- .table-item:hover { background: var(--bg-hover); }
185
- .table-item.active { color: var(--accent); background: var(--bg-hover); }
407
+ .table-item:hover { background: var(--bg-3); color: var(--text); }
408
+ .table-item.active { background: var(--accent-bg); color: var(--accent-2); }
409
+ .table-item .table-icon { font-size: 12px; opacity: 0.5; }
410
+ .table-item .table-count {
411
+ margin-left: auto;
412
+ font-size: 10px;
413
+ background: var(--bg-4);
414
+ padding: 1px 6px;
415
+ border-radius: 10px;
416
+ color: var(--text-3);
417
+ }
186
418
 
187
- /* Data table */
419
+ .table-count-wrap {
420
+ display: flex;
421
+ justify-content: space-between;
422
+ padding: 8px 14px;
423
+ font-size: 11px;
424
+ color: var(--text-3);
425
+ border-bottom: 1px solid var(--border);
426
+ }
427
+
428
+ /* ---- Data Table ---- */
188
429
  .data-panel {
189
- background: var(--bg-surface);
190
- border-radius: var(--radius);
191
- border: 1px solid var(--border);
192
- overflow: hidden;
193
430
  display: flex;
194
431
  flex-direction: column;
432
+ gap: 12px;
433
+ overflow: hidden;
195
434
  }
196
435
 
197
- .data-header {
198
- padding: 12px 16px;
199
- border-bottom: 1px solid var(--border);
436
+ .data-toolbar {
200
437
  display: flex;
201
- justify-content: space-between;
202
438
  align-items: center;
439
+ gap: 10px;
203
440
  }
204
441
 
205
- .data-header h3 { font-size: 14px; font-weight: 600; }
206
- .data-meta { font-size: 12px; color: var(--text-dim); }
442
+ .data-toolbar-title {
443
+ font-size: 16px;
444
+ font-weight: 700;
445
+ display: flex;
446
+ align-items: center;
447
+ gap: 8px;
448
+ }
207
449
 
208
- .data-table-wrap {
450
+ .data-toolbar-title .badge {
451
+ font-size: 11px;
452
+ font-weight: 600;
453
+ background: var(--bg-4);
454
+ padding: 2px 8px;
455
+ border-radius: 10px;
456
+ color: var(--text-2);
457
+ }
458
+
459
+ .data-filter {
460
+ display: flex;
461
+ align-items: center;
462
+ gap: 6px;
463
+ margin-left: auto;
464
+ }
465
+
466
+ .data-filter input {
467
+ background: var(--bg-2);
468
+ border: 1px solid var(--border);
469
+ border-radius: var(--radius-sm);
470
+ padding: 6px 10px;
471
+ font-size: 12px;
472
+ color: var(--text);
473
+ outline: none;
474
+ width: 180px;
475
+ font-family: var(--font);
476
+ transition: border-color var(--transition);
477
+ }
478
+
479
+ .data-filter input:focus { border-color: var(--accent); }
480
+ .data-filter input::placeholder { color: var(--text-3); }
481
+
482
+ .data-table-card {
209
483
  flex: 1;
484
+ overflow: hidden;
485
+ }
486
+
487
+ .data-table-scroll {
210
488
  overflow: auto;
489
+ height: calc(100vh - 230px);
211
490
  }
212
491
 
213
492
  table {
214
493
  width: 100%;
215
494
  border-collapse: collapse;
216
495
  font-size: 13px;
217
- font-family: var(--font);
496
+ font-family: var(--font-mono);
218
497
  }
219
498
 
220
- th {
499
+ thead th {
221
500
  position: sticky;
222
501
  top: 0;
223
- background: var(--bg-elevated);
224
- padding: 10px 12px;
502
+ background: var(--bg-3);
503
+ padding: 10px 14px;
225
504
  text-align: left;
226
505
  font-weight: 600;
227
506
  font-size: 11px;
228
507
  text-transform: uppercase;
229
- color: var(--text-dim);
230
- border-bottom: 1px solid var(--border);
508
+ letter-spacing: 0.5px;
509
+ color: var(--text-3);
510
+ border-bottom: 1px solid var(--border-strong);
511
+ white-space: nowrap;
512
+ z-index: 1;
231
513
  }
232
514
 
233
- td {
234
- padding: 8px 12px;
515
+ thead th:hover { color: var(--text-2); }
516
+
517
+ tbody td {
518
+ padding: 8px 14px;
235
519
  border-bottom: 1px solid var(--border);
236
- max-width: 300px;
520
+ max-width: 280px;
237
521
  overflow: hidden;
238
522
  text-overflow: ellipsis;
239
523
  white-space: nowrap;
524
+ font-size: 12px;
525
+ color: var(--text-2);
526
+ transition: background var(--transition);
240
527
  }
241
528
 
242
- tr:hover td { background: var(--bg-hover); }
529
+ tbody tr:hover td { background: var(--bg-3); color: var(--text); }
243
530
 
244
- /* Query console */
245
- .query-editor {
246
- background: var(--bg-elevated);
531
+ td.cell-null { color: var(--text-3); font-style: italic; }
532
+ td.cell-pk { color: var(--yellow); }
533
+
534
+ /* ---- Buttons ---- */
535
+ .btn {
536
+ padding: 7px 14px;
537
+ border-radius: var(--radius-sm);
247
538
  border: 1px solid var(--border);
248
- border-radius: var(--radius);
249
- padding: 16px;
539
+ background: var(--bg-2);
540
+ color: var(--text-2);
541
+ cursor: pointer;
542
+ font-size: 12px;
543
+ font-weight: 500;
250
544
  font-family: var(--font);
251
- font-size: 14px;
252
- color: var(--text);
253
- width: 100%;
254
- min-height: 120px;
255
- resize: vertical;
256
- outline: none;
545
+ transition: all var(--transition);
546
+ display: inline-flex;
547
+ align-items: center;
548
+ gap: 6px;
257
549
  }
258
550
 
259
- .query-editor:focus { border-color: var(--accent); }
551
+ .btn:hover { background: var(--bg-3); color: var(--text); border-color: var(--border-strong); }
552
+ .btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
553
+ .btn-primary:hover { background: #5b4bd5; }
554
+ .btn-sm { padding: 4px 10px; font-size: 11px; }
260
555
 
261
- .query-actions {
262
- margin-top: 12px;
556
+ /* ---- Query Console ---- */
557
+ .query-layout {
263
558
  display: flex;
264
- gap: 8px;
559
+ flex-direction: column;
560
+ gap: 16px;
561
+ height: calc(100vh - 140px);
265
562
  }
266
563
 
267
- .btn {
268
- padding: 8px 16px;
269
- border-radius: var(--radius);
564
+ .query-editor-wrap {
565
+ background: var(--bg-2);
270
566
  border: 1px solid var(--border);
271
- background: var(--bg-elevated);
272
- color: var(--text);
273
- cursor: pointer;
274
- font-size: 13px;
275
- transition: all 0.15s;
567
+ border-radius: var(--radius);
568
+ overflow: hidden;
276
569
  }
277
570
 
278
- .btn:hover { background: var(--bg-hover); border-color: var(--accent); }
279
- .btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
280
- .btn-primary:hover { background: var(--accent-hover); }
571
+ .query-editor-header {
572
+ padding: 8px 14px;
573
+ border-bottom: 1px solid var(--border);
574
+ display: flex;
575
+ align-items: center;
576
+ justify-content: space-between;
577
+ }
578
+
579
+ .query-editor-header span {
580
+ font-size: 11px;
581
+ color: var(--text-3);
582
+ font-weight: 600;
583
+ text-transform: uppercase;
584
+ letter-spacing: 0.5px;
585
+ }
281
586
 
282
- .query-result {
283
- margin-top: 16px;
587
+ .query-editor {
588
+ background: transparent;
589
+ border: none;
590
+ padding: 14px;
591
+ font-family: var(--font-mono);
592
+ font-size: 13px;
593
+ color: var(--text);
594
+ width: 100%;
595
+ min-height: 100px;
596
+ max-height: 200px;
597
+ resize: vertical;
598
+ outline: none;
599
+ line-height: 1.6;
600
+ }
601
+
602
+ .query-result-area {
603
+ flex: 1;
604
+ overflow: hidden;
284
605
  }
285
606
 
286
607
  .query-stats {
608
+ display: flex;
609
+ align-items: center;
610
+ gap: 12px;
611
+ padding: 8px 14px;
287
612
  font-size: 12px;
288
- color: var(--text-dim);
289
- margin-bottom: 8px;
613
+ color: var(--text-3);
614
+ border-top: 1px solid var(--border);
290
615
  }
291
616
 
292
- /* ER Diagram */
293
- .erd-container {
294
- background: var(--bg-surface);
295
- border-radius: var(--radius);
296
- border: 1px solid var(--border);
297
- height: calc(100vh - 120px);
617
+ .query-stats .stat { display: flex; align-items: center; gap: 4px; }
618
+ .query-stats .stat-ok { color: var(--green); }
619
+ .query-stats .stat-err { color: var(--red); }
620
+
621
+ .query-result-table {
298
622
  overflow: auto;
299
- padding: 24px;
623
+ flex: 1;
624
+ height: calc(100vh - 360px);
625
+ }
626
+
627
+ /* ---- ER Diagram ---- */
628
+ .erd-canvas {
300
629
  display: flex;
301
630
  flex-wrap: wrap;
302
- gap: 24px;
631
+ gap: 20px;
303
632
  align-items: flex-start;
633
+ padding: 8px 0;
304
634
  }
305
635
 
306
- .erd-table {
307
- background: var(--bg-elevated);
636
+ .erd-card {
637
+ background: var(--bg-2);
308
638
  border: 1px solid var(--border);
309
639
  border-radius: var(--radius);
310
- min-width: 200px;
640
+ min-width: 220px;
641
+ overflow: hidden;
642
+ transition: border-color var(--transition);
311
643
  }
312
644
 
313
- .erd-table-header {
645
+ .erd-card:hover { border-color: var(--accent); }
646
+
647
+ .erd-card-header {
314
648
  padding: 10px 14px;
315
- font-weight: 600;
316
- font-size: 13px;
649
+ font-weight: 700;
650
+ font-size: 12px;
317
651
  border-bottom: 1px solid var(--border);
318
- color: var(--accent);
652
+ color: var(--accent-2);
653
+ font-family: var(--font-mono);
654
+ display: flex;
655
+ align-items: center;
656
+ gap: 6px;
319
657
  }
320
658
 
321
- .erd-column {
659
+ .erd-col {
322
660
  padding: 6px 14px;
323
661
  font-size: 12px;
324
- font-family: var(--font);
662
+ font-family: var(--font-mono);
325
663
  display: flex;
326
664
  justify-content: space-between;
327
- gap: 8px;
665
+ gap: 12px;
666
+ border-bottom: 1px solid var(--border);
667
+ transition: background var(--transition);
328
668
  }
329
669
 
330
- .erd-column:hover { background: var(--bg-hover); }
670
+ .erd-col:last-child { border-bottom: none; }
671
+ .erd-col:hover { background: var(--bg-3); }
331
672
  .erd-col-name { color: var(--text); }
332
- .erd-col-type { color: var(--text-dim); font-size: 11px; }
333
- .erd-col-pk { color: var(--warning); }
673
+ .erd-col-type { color: var(--text-3); font-size: 11px; }
674
+ .erd-col-pk { color: var(--yellow); }
334
675
 
335
- /* Empty state */
336
- .empty-state {
676
+ /* ---- Migrations ---- */
677
+ .migration-list {
337
678
  display: flex;
338
679
  flex-direction: column;
339
- align-items: center;
340
- justify-content: center;
341
- height: 300px;
342
- color: var(--text-dim);
680
+ gap: 6px;
343
681
  }
344
682
 
345
- .empty-state-icon { font-size: 48px; margin-bottom: 12px; }
346
- .empty-state-text { font-size: 14px; }
347
-
348
- /* Migrations */
349
683
  .migration-item {
350
684
  display: flex;
351
685
  align-items: center;
352
686
  gap: 12px;
353
687
  padding: 12px 16px;
354
- background: var(--bg-surface);
688
+ background: var(--bg-2);
355
689
  border: 1px solid var(--border);
356
690
  border-radius: var(--radius);
357
- margin-bottom: 8px;
691
+ transition: border-color var(--transition);
358
692
  }
359
693
 
360
- .migration-icon { font-size: 16px; }
361
- .migration-name { font-size: 13px; font-family: var(--font); flex: 1; }
694
+ .migration-item:hover { border-color: var(--border-strong); }
695
+
696
+ .migration-icon { font-size: 16px; flex-shrink: 0; }
697
+ .migration-name { font-size: 13px; font-family: var(--font-mono); flex: 1; }
362
698
  .migration-status {
363
- font-size: 11px;
364
- padding: 2px 8px;
365
- border-radius: 4px;
699
+ font-size: 10px;
700
+ font-weight: 600;
701
+ padding: 3px 8px;
702
+ border-radius: 10px;
703
+ text-transform: uppercase;
704
+ letter-spacing: 0.5px;
366
705
  }
367
706
 
368
- .migration-applied { color: var(--success); background: rgba(74,222,128,0.1); }
369
- .migration-pending { color: var(--warning); background: rgba(251,191,36,0.1); }
707
+ .migration-applied { color: var(--green); background: var(--green-bg); }
708
+ .migration-pending { color: var(--yellow); background: var(--yellow-bg); }
370
709
 
371
- /* Settings */
372
- .settings-group {
373
- background: var(--bg-surface);
710
+ /* ---- Settings ---- */
711
+ .settings-grid {
712
+ display: grid;
713
+ grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
714
+ gap: 16px;
715
+ }
716
+
717
+ .settings-card {
718
+ background: var(--bg-2);
374
719
  border: 1px solid var(--border);
375
- border-radius: var(--radius);
376
- padding: 20px;
377
- margin-bottom: 16px;
720
+ border-radius: var(--radius-lg);
721
+ overflow: hidden;
378
722
  }
379
723
 
380
- .settings-group h3 {
724
+ .settings-card-header {
725
+ padding: 14px 18px;
726
+ border-bottom: 1px solid var(--border);
727
+ display: flex;
728
+ align-items: center;
729
+ gap: 10px;
730
+ }
731
+
732
+ .settings-card-icon {
733
+ width: 32px;
734
+ height: 32px;
735
+ border-radius: var(--radius-sm);
736
+ display: flex;
737
+ align-items: center;
738
+ justify-content: center;
739
+ font-size: 16px;
740
+ }
741
+
742
+ .settings-card-title {
381
743
  font-size: 14px;
382
- margin-bottom: 16px;
383
- color: var(--text-dim);
744
+ font-weight: 600;
384
745
  }
385
746
 
747
+ .settings-card-desc {
748
+ font-size: 11px;
749
+ color: var(--text-3);
750
+ }
751
+
752
+ .settings-rows { padding: 4px 0; }
753
+
386
754
  .setting-row {
387
755
  display: flex;
388
756
  align-items: center;
389
- margin-bottom: 12px;
757
+ padding: 10px 18px;
758
+ border-bottom: 1px solid var(--border);
759
+ transition: background var(--transition);
390
760
  }
391
761
 
392
- .setting-label {
762
+ .setting-row:last-child { border-bottom: none; }
763
+ .setting-row:hover { background: var(--bg-3); }
764
+
765
+ .setting-key {
393
766
  width: 140px;
394
- font-size: 13px;
395
- color: var(--text-dim);
767
+ font-size: 12px;
768
+ color: var(--text-3);
769
+ font-weight: 500;
770
+ flex-shrink: 0;
396
771
  }
397
772
 
398
- .setting-value {
399
- font-size: 13px;
773
+ .setting-val {
774
+ font-size: 12px;
775
+ font-family: var(--font-mono);
776
+ color: var(--text);
777
+ word-break: break-all;
778
+ }
779
+
780
+ .setting-val.masked { color: var(--text-3); }
781
+
782
+ /* ---- Empty State ---- */
783
+ .empty-state {
784
+ display: flex;
785
+ flex-direction: column;
786
+ align-items: center;
787
+ justify-content: center;
788
+ padding: 48px 20px;
789
+ color: var(--text-3);
790
+ }
791
+
792
+ .empty-state-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.5; }
793
+ .empty-state-text { font-size: 13px; }
794
+
795
+ /* ---- Search Modal ---- */
796
+ .modal { position: fixed; inset: 0; z-index: 100; display: flex; align-items: flex-start; justify-content: center; padding-top: 15vh; }
797
+ .modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); }
798
+ .modal-content { position: relative; background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius-lg); width: 520px; max-height: 400px; overflow: hidden; box-shadow: var(--shadow-lg); }
799
+
800
+ .search-modal-input {
801
+ display: flex;
802
+ align-items: center;
803
+ gap: 10px;
804
+ padding: 14px 18px;
805
+ border-bottom: 1px solid var(--border);
806
+ }
807
+
808
+ .search-modal-input input {
809
+ background: none;
810
+ border: none;
811
+ outline: none;
812
+ color: var(--text);
813
+ font-size: 15px;
814
+ width: 100%;
400
815
  font-family: var(--font);
401
816
  }
817
+
818
+ .search-modal-input input::placeholder { color: var(--text-3); }
819
+ .search-modal-input kbd { font-size: 10px; padding: 2px 5px; border-radius: 4px; background: var(--bg-4); border: 1px solid var(--border); color: var(--text-3); font-family: var(--font); }
820
+
821
+ .search-results { max-height: 320px; overflow-y: auto; padding: 6px; }
822
+
823
+ .search-result-item {
824
+ display: flex;
825
+ align-items: center;
826
+ gap: 10px;
827
+ padding: 8px 12px;
828
+ border-radius: var(--radius-sm);
829
+ cursor: pointer;
830
+ transition: background var(--transition);
831
+ }
832
+
833
+ .search-result-item:hover { background: var(--bg-3); }
834
+ .search-result-item .sr-icon { font-size: 12px; color: var(--text-3); }
835
+ .search-result-item .sr-name { font-size: 13px; font-family: var(--font-mono); }
836
+ .search-result-item .sr-detail { font-size: 11px; color: var(--text-3); margin-left: auto; }
837
+
838
+ .search-empty {
839
+ padding: 24px;
840
+ text-align: center;
841
+ color: var(--text-3);
842
+ font-size: 13px;
843
+ }
844
+
845
+ /* ---- Scrollbar ---- */
846
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
847
+ ::-webkit-scrollbar-track { background: transparent; }
848
+ ::-webkit-scrollbar-thumb { background: var(--bg-4); border-radius: 3px; }
849
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-3); }
402
850
  `;
403
851
  }
404
852
  function getJS() {
405
853
  return `
854
+ let allTables = [];
855
+ let currentTable = null;
856
+
406
857
  // ---- Navigation ----
407
- document.querySelectorAll('.nav-links a').forEach(link => {
858
+ document.querySelectorAll('.nav-link').forEach(link => {
408
859
  link.addEventListener('click', (e) => {
409
860
  e.preventDefault();
410
861
  const page = link.dataset.page;
411
- document.querySelectorAll('.nav-links a').forEach(l => l.classList.remove('active'));
862
+ document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
412
863
  link.classList.add('active');
413
864
  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
414
865
  document.getElementById('page-' + page)?.classList.add('active');
@@ -416,11 +867,7 @@ function getJS() {
416
867
  });
417
868
  });
418
869
 
419
- // ---- Page loaders ----
420
- const loaded = {};
421
870
  function loadPage(page) {
422
- if (loaded[page]) return;
423
- loaded[page] = true;
424
871
  switch (page) {
425
872
  case 'tables': loadTables(); break;
426
873
  case 'query': loadQuery(); break;
@@ -430,32 +877,117 @@ function getJS() {
430
877
  }
431
878
  }
432
879
 
880
+ // ---- Global Search (Ctrl+K) ----
881
+ const modal = document.getElementById('search-modal');
882
+ const modalInput = document.getElementById('modal-search-input');
883
+ const searchResults = document.getElementById('search-results');
884
+
885
+ document.getElementById('global-search').addEventListener('focus', openSearch);
886
+ document.addEventListener('keydown', (e) => {
887
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); openSearch(); }
888
+ if (e.key === 'Escape') closeSearch();
889
+ });
890
+
891
+ modal.querySelector('.modal-backdrop').addEventListener('click', closeSearch);
892
+
893
+ function openSearch() {
894
+ modal.style.display = 'flex';
895
+ modalInput.value = '';
896
+ modalInput.focus();
897
+ renderSearchResults('');
898
+ }
899
+
900
+ function closeSearch() {
901
+ modal.style.display = 'none';
902
+ document.getElementById('global-search').blur();
903
+ }
904
+
905
+ modalInput.addEventListener('input', (e) => renderSearchResults(e.target.value));
906
+
907
+ function renderSearchResults(q) {
908
+ const query = q.toLowerCase().trim();
909
+ if (!query) {
910
+ searchResults.innerHTML = '<div class="search-empty">Type to search tables and columns...</div>';
911
+ return;
912
+ }
913
+
914
+ const matches = [];
915
+ for (const t of allTables) {
916
+ if (t.name.toLowerCase().includes(query)) {
917
+ matches.push({ type: 'table', name: t.name, detail: t.count + ' rows' });
918
+ }
919
+ if (t.columns) {
920
+ for (const c of t.columns) {
921
+ if (c.toLowerCase().includes(query)) {
922
+ matches.push({ type: 'column', name: c, detail: t.name });
923
+ }
924
+ }
925
+ }
926
+ }
927
+
928
+ if (matches.length === 0) {
929
+ searchResults.innerHTML = '<div class="search-empty">No results found</div>';
930
+ return;
931
+ }
932
+
933
+ searchResults.innerHTML = matches.slice(0, 20).map(m => \`
934
+ <div class="search-result-item" onclick="\${m.type === 'table' ? "closeSearch();selectTable('\${m.name}')" : "closeSearch();goToTable('\${m.detail}')"}">
935
+ <span class="sr-icon">\${m.type === 'table' ? '\u{1F4CA}' : '\u{1F4CB}'}</span>
936
+ <span class="sr-name">\${m.name}</span>
937
+ <span class="sr-detail">\${m.detail}</span>
938
+ </div>
939
+ \`).join('');
940
+ }
941
+
942
+ function goToTable(tableName) {
943
+ document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
944
+ document.querySelector('[data-page="tables"]').classList.add('active');
945
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
946
+ document.getElementById('page-tables').classList.add('active');
947
+ loadTables().then(() => selectTable(tableName));
948
+ }
949
+
433
950
  // ---- Tables ----
434
951
  async function loadTables() {
435
952
  const el = document.getElementById('page-tables');
436
953
  try {
437
954
  const res = await fetch('/api/tables');
438
955
  const data = await res.json();
439
- const tables = data.tables || [];
956
+ allTables = (data.tables || []).map(name => ({ name, columns: [], count: 0 }));
440
957
 
441
958
  el.innerHTML = \`
442
- <h2 style="margin-bottom:16px;font-size:18px">\u{1F4CA} Tables</h2>
959
+ <div class="page-title">\u{1F4CA} Tables</div>
960
+ <div class="page-subtitle">Browse and inspect your database tables</div>
443
961
  <div class="table-grid">
444
- <div class="table-list">
445
- <div class="table-list-header">Tables (\${tables.length})</div>
446
- \${tables.map(t => \`
447
- <div class="table-item" data-table="\${t}" onclick="loadTableData('\${t}')">\${t}</div>
448
- \`).join('')}
962
+ <div class="table-sidebar">
963
+ <div class="table-search">
964
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
965
+ <input type="text" id="table-search-input" placeholder="Filter tables..." oninput="filterTables(this.value)">
966
+ </div>
967
+ <div class="table-count-wrap">
968
+ <span>\${allTables.length} tables</span>
969
+ </div>
970
+ <div class="table-list" id="table-list">
971
+ \${allTables.map(t => \`
972
+ <div class="table-item" data-table="\${t.name}" onclick="selectTable('\${t.name}')">
973
+ <span class="table-icon">\u25FB</span>
974
+ <span>\${t.name}</span>
975
+ </div>
976
+ \`).join('')}
977
+ </div>
449
978
  </div>
450
979
  <div class="data-panel">
451
- <div class="data-header">
452
- <h3 id="table-title">Select a table</h3>
453
- <span class="data-meta" id="table-meta"></span>
980
+ <div class="data-toolbar" id="data-toolbar">
981
+ <div class="data-toolbar-title" id="table-title">
982
+ <span>Select a table</span>
983
+ </div>
454
984
  </div>
455
- <div class="data-table-wrap" id="table-data">
456
- <div class="empty-state">
457
- <div class="empty-state-icon">\u{1F4CB}</div>
458
- <div class="empty-state-text">Select a table to view its data</div>
985
+ <div class="data-table-card card">
986
+ <div class="data-table-scroll" id="table-data">
987
+ <div class="empty-state">
988
+ <div class="empty-state-icon">\u{1F4CB}</div>
989
+ <div class="empty-state-text">Select a table to view its data</div>
990
+ </div>
459
991
  </div>
460
992
  </div>
461
993
  </div>
@@ -466,46 +998,104 @@ function getJS() {
466
998
  }
467
999
  }
468
1000
 
469
- async function loadTableData(tableName) {
1001
+ function filterTables(q) {
1002
+ const query = q.toLowerCase();
1003
+ document.querySelectorAll('#table-list .table-item').forEach(item => {
1004
+ const name = item.dataset.table;
1005
+ item.style.display = name.toLowerCase().includes(query) ? '' : 'none';
1006
+ });
1007
+ }
1008
+
1009
+ window.selectTable = async function(name) {
1010
+ currentTable = name;
470
1011
  document.querySelectorAll('.table-item').forEach(i => i.classList.remove('active'));
471
- document.querySelector(\`.table-item[data-table="\${tableName}"]\`)?.classList.add('active');
472
- document.getElementById('table-title').textContent = tableName;
1012
+ document.querySelector(\`.table-item[data-table="\${name}"]\`)?.classList.add('active');
1013
+ document.getElementById('table-title').innerHTML = \`\${name}<span class="badge" id="table-count-badge">...</span>\`;
473
1014
 
474
1015
  const el = document.getElementById('table-data');
475
1016
  el.innerHTML = '<div class="empty-state"><div class="empty-state-text">Loading...</div></div>';
476
1017
 
1018
+ // Add filter toolbar
1019
+ document.getElementById('data-toolbar').innerHTML = \`
1020
+ <div class="data-toolbar-title">\${name}<span class="badge" id="table-count-badge">...</span></div>
1021
+ <div class="data-filter">
1022
+ <input type="text" id="row-filter" placeholder="Filter rows..." oninput="filterRows(this.value)">
1023
+ </div>
1024
+ \`;
1025
+
477
1026
  try {
478
- const res = await fetch(\`/api/tables/\${tableName}\`);
1027
+ const res = await fetch(\`/api/tables/\${name}\`);
479
1028
  const data = await res.json();
480
- document.getElementById('table-meta').textContent = \`\${data.count} rows\`;
1029
+ document.getElementById('table-count-badge').textContent = \`\${data.count} rows\`;
481
1030
 
482
1031
  if (data.rows.length === 0) {
483
- el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u{1F4ED}</div><div class="empty-state-text">No rows</div></div>';
1032
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u{1F4ED}</div><div class="empty-state-text">No rows in this table</div></div>';
484
1033
  return;
485
1034
  }
486
1035
 
487
- el.innerHTML = \`
488
- <table>
489
- <thead><tr>\${data.columns.map(c => \`<th>\${c}</th>\`).join('')}</tr></thead>
490
- <tbody>\${data.rows.map(row => \`<tr>\${data.columns.map(c => \`<td title="\${String(row[c] ?? '')}">\${String(row[c] ?? 'NULL')}</td>\`).join('')}</tr>\`).join('')}</tbody>
491
- </table>
492
- \`;
1036
+ // Store for filtering
1037
+ window._tableData = data;
1038
+ renderTableData(data);
493
1039
  } catch (err) {
494
1040
  el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u274C</div><div class="empty-state-text">Failed to load data</div></div>';
495
1041
  }
1042
+ };
1043
+
1044
+ function renderTableData(data, filter) {
1045
+ const el = document.getElementById('table-data');
1046
+ let rows = data.rows;
1047
+ if (filter) {
1048
+ const q = filter.toLowerCase();
1049
+ rows = rows.filter(row => Object.values(row).some(v => String(v ?? '').toLowerCase().includes(q)));
1050
+ }
1051
+
1052
+ el.innerHTML = \`
1053
+ <table>
1054
+ <thead><tr>\${data.columns.map(c => \`<th>\${c}</th>\`).join('')}</tr></thead>
1055
+ <tbody>\${rows.map(row => \`<tr>\${data.columns.map(c => {
1056
+ const v = row[c];
1057
+ const isNull = v === null || v === undefined;
1058
+ const display = isNull ? 'NULL' : String(v);
1059
+ return \`<td class="\${isNull ? 'cell-null' : ''}" title="\${display}">\${display}</td>\`;
1060
+ }).join('')}</tr>\`).join('')}</tbody>
1061
+ </table>
1062
+ \`;
496
1063
  }
497
1064
 
498
- // ---- Query console ----
1065
+ window.filterRows = function(q) {
1066
+ if (window._tableData) renderTableData(window._tableData, q);
1067
+ };
1068
+
1069
+ // ---- Query Console ----
499
1070
  function loadQuery() {
500
1071
  document.getElementById('page-query').innerHTML = \`
501
- <h2 style="margin-bottom:16px;font-size:18px">\u{1F4BB} SQL Console</h2>
502
- <textarea class="query-editor" id="sql-input" placeholder="SELECT * FROM users LIMIT 100">SELECT * FROM users LIMIT 100</textarea>
503
- <div class="query-actions">
504
- <button class="btn btn-primary" onclick="runQuery()">\u25B6 Run Query</button>
505
- <button class="btn" onclick="document.getElementById('sql-input').value='';document.getElementById('query-result').innerHTML=''">Clear</button>
1072
+ <div class="page-title">\u{1F4BB} SQL Console</div>
1073
+ <div class="page-subtitle">Execute raw SQL queries against your database</div>
1074
+ <div class="query-layout">
1075
+ <div class="query-editor-wrap">
1076
+ <div class="query-editor-header">
1077
+ <span>Editor</span>
1078
+ <div style="display:flex;gap:6px">
1079
+ <button class="btn btn-primary btn-sm" onclick="runQuery()">\u25B6 Run</button>
1080
+ <button class="btn btn-sm" onclick="clearQuery()">Clear</button>
1081
+ </div>
1082
+ </div>
1083
+ <textarea class="query-editor" id="sql-input" placeholder="SELECT * FROM users LIMIT 100" spellcheck="false">SELECT * FROM users LIMIT 100</textarea>
1084
+ </div>
1085
+ <div class="query-result-area card">
1086
+ <div class="query-result-table" id="query-result">
1087
+ <div class="empty-state">
1088
+ <div class="empty-state-text">Results will appear here</div>
1089
+ </div>
1090
+ </div>
1091
+ </div>
506
1092
  </div>
507
- <div id="query-result" class="query-result"></div>
508
1093
  \`;
1094
+
1095
+ // Ctrl+Enter to run
1096
+ document.getElementById('sql-input').addEventListener('keydown', (e) => {
1097
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); runQuery(); }
1098
+ });
509
1099
  }
510
1100
 
511
1101
  async function runQuery() {
@@ -523,42 +1113,68 @@ function getJS() {
523
1113
  const data = await res.json();
524
1114
 
525
1115
  if (data.error) {
526
- el.innerHTML = \`<div class="query-stats" style="color:var(--error)">Error: \${data.error}</div>\`;
1116
+ el.innerHTML = \`
1117
+ <div class="query-stats">
1118
+ <span class="stat stat-err">\u2717 Error</span>
1119
+ </div>
1120
+ <div style="padding:16px;color:var(--red);font-family:var(--font-mono);font-size:12px">\${data.error}</div>
1121
+ \`;
527
1122
  return;
528
1123
  }
529
1124
 
530
- let html = \`<div class="query-stats">\${data.rowCount} row(s) in \${data.duration}ms</div>\`;
1125
+ let html = \`<div class="query-stats">
1126
+ <span class="stat stat-ok">\u2713 \${data.rowCount} row(s)</span>
1127
+ <span class="stat">\${data.duration}ms</span>
1128
+ </div>\`;
1129
+
531
1130
  if (data.rows.length > 0) {
532
- html += \`<table><thead><tr>\${data.columns.map(c => \`<th>\${c}</th>\`).join('')}</tr></thead><tbody>\${data.rows.map(row => \`<tr>\${data.columns.map(c => \`<td>\${String(row[c] ?? 'NULL')}</td>\`).join('')}</tr>\`).join('')}</tbody></table>\`;
1131
+ html += \`<div class="query-result-table"><table>
1132
+ <thead><tr>\${data.columns.map(c => \`<th>\${c}</th>\`).join('')}</tr></thead>
1133
+ <tbody>\${data.rows.map(row => \`<tr>\${data.columns.map(c => {
1134
+ const v = row[c];
1135
+ const isNull = v === null || v === undefined;
1136
+ return \`<td class="\${isNull ? 'cell-null' : ''}">\${isNull ? 'NULL' : String(v)}</td>\`;
1137
+ }).join('')}</tr>\`).join('')}</tbody>
1138
+ </table></div>\`;
533
1139
  }
534
1140
  el.innerHTML = html;
535
1141
  } catch (err) {
536
- el.innerHTML = \`<div class="query-stats" style="color:var(--error)">Error: \${err.message}</div>\`;
1142
+ el.innerHTML = \`<div class="query-stats"><span class="stat stat-err">\u2717 Error</span></div><div style="padding:16px;color:var(--red);font-size:12px">\${err.message}</div>\`;
537
1143
  }
538
1144
  }
539
1145
 
1146
+ window.clearQuery = function() {
1147
+ document.getElementById('sql-input').value = '';
1148
+ document.getElementById('query-result').innerHTML = '<div class="empty-state"><div class="empty-state-text">Results will appear here</div></div>';
1149
+ };
1150
+
540
1151
  // ---- ER Diagram ----
541
1152
  async function loadERD() {
542
1153
  const el = document.getElementById('page-erd');
1154
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-text">Loading ER diagram...</div></div>';
1155
+
543
1156
  try {
544
1157
  const res = await fetch('/api/erd');
545
1158
  const data = await res.json();
546
1159
 
547
1160
  if (data.tables.length === 0) {
548
- el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u{1F517}</div><div class="empty-state-text">No tables found</div></div>';
1161
+ el.innerHTML = '<div class="page-title">\u{1F517} ER Diagram</div><div class="page-subtitle">Entity-relationship diagram of your database</div><div class="empty-state"><div class="empty-state-icon">\u{1F517}</div><div class="empty-state-text">No tables found</div></div>';
549
1162
  return;
550
1163
  }
551
1164
 
552
1165
  el.innerHTML = \`
553
- <h2 style="margin-bottom:16px;font-size:18px">\u{1F517} ER Diagram</h2>
554
- <div class="erd-container">
1166
+ <div class="page-title">\u{1F517} ER Diagram</div>
1167
+ <div class="page-subtitle">\${data.tables.length} tables \xB7 \${data.relations.length} relations</div>
1168
+ <div class="erd-canvas">
555
1169
  \${data.tables.map(t => \`
556
- <div class="erd-table">
557
- <div class="erd-table-header">\${t.name}</div>
1170
+ <div class="erd-card">
1171
+ <div class="erd-card-header">
1172
+ <span>\u25FB</span> \${t.name}
1173
+ </div>
558
1174
  \${t.columns.map(c => \`
559
- <div class="erd-column">
1175
+ <div class="erd-col">
560
1176
  <span class="erd-col-name \${c.isPrimaryKey ? 'erd-col-pk' : ''}">\${c.isPrimaryKey ? '\u{1F511} ' : ''}\${c.name}</span>
561
- <span class="erd-col-type">\${c.type}\${c.nullable ? '' : ' NOT NULL'}</span>
1177
+ <span class="erd-col-type">\${c.type}\${c.nullable ? '?' : ''}</span>
562
1178
  </div>
563
1179
  \`).join('')}
564
1180
  </div>
@@ -581,31 +1197,35 @@ function getJS() {
581
1197
 
582
1198
  if (data.applied.length === 0 && data.pending.length === 0) {
583
1199
  el.innerHTML = \`
584
- <h2 style="margin-bottom:16px;font-size:18px">\u{1F4E6} Migrations</h2>
1200
+ <div class="page-title">\u{1F4E6} Migrations</div>
1201
+ <div class="page-subtitle">Database schema version control</div>
585
1202
  <div class="empty-state">
586
1203
  <div class="empty-state-icon">\u{1F4E6}</div>
587
- <div class="empty-state-text">No migrations found. Run "mountsqli generate" to create one.</div>
1204
+ <div class="empty-state-text">No migrations found. Run "npx @mountsqli/cli generate" to create one.</div>
588
1205
  </div>
589
1206
  \`;
590
1207
  return;
591
1208
  }
592
1209
 
593
1210
  el.innerHTML = \`
594
- <h2 style="margin-bottom:16px;font-size:18px">\u{1F4E6} Migrations</h2>
595
- \${data.applied.map(m => \`
596
- <div class="migration-item">
597
- <span class="migration-icon">\u2705</span>
598
- <span class="migration-name">\${m.name}</span>
599
- <span class="migration-status migration-applied">applied</span>
600
- </div>
601
- \`).join('')}
602
- \${data.pending.map(m => \`
603
- <div class="migration-item">
604
- <span class="migration-icon">\u23F3</span>
605
- <span class="migration-name">\${m}</span>
606
- <span class="migration-status migration-pending">pending</span>
607
- </div>
608
- \`).join('')}
1211
+ <div class="page-title">\u{1F4E6} Migrations</div>
1212
+ <div class="page-subtitle">\${data.applied.length} applied \xB7 \${data.pending.length} pending</div>
1213
+ <div class="migration-list">
1214
+ \${data.applied.map(m => \`
1215
+ <div class="migration-item">
1216
+ <span class="migration-icon">\u2705</span>
1217
+ <span class="migration-name">\${m.name}</span>
1218
+ <span class="migration-status migration-applied">applied</span>
1219
+ </div>
1220
+ \`).join('')}
1221
+ \${data.pending.map(m => \`
1222
+ <div class="migration-item">
1223
+ <span class="migration-icon">\u23F3</span>
1224
+ <span class="migration-name">\${m}</span>
1225
+ <span class="migration-status migration-pending">pending</span>
1226
+ </div>
1227
+ \`).join('')}
1228
+ </div>
609
1229
  \`;
610
1230
  } catch (err) {
611
1231
  el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u274C</div><div class="empty-state-text">Failed to load migrations</div></div>';
@@ -613,36 +1233,108 @@ function getJS() {
613
1233
  }
614
1234
 
615
1235
  // ---- Settings ----
616
- function loadSettings() {
617
- document.getElementById('page-settings').innerHTML = \`
618
- <h2 style="margin-bottom:16px;font-size:18px">\u2699\uFE0F Settings</h2>
619
- <div class="settings-group">
620
- <h3>Connection</h3>
621
- <div class="setting-row">
622
- <span class="setting-label">Status</span>
623
- <span class="setting-value" style="color:var(--success)">\u25CF Connected</span>
624
- </div>
625
- <div class="setting-row">
626
- <span class="setting-label">Dialect</span>
627
- <span class="setting-value">PostgreSQL</span>
628
- </div>
629
- </div>
630
- <div class="settings-group">
631
- <h3>Studio</h3>
632
- <div class="setting-row">
633
- <span class="setting-label">Version</span>
634
- <span class="setting-value">0.1.0</span>
635
- </div>
636
- <div class="setting-row">
637
- <span class="setting-label">URL</span>
638
- <span class="setting-value">\${window.location.origin}</span>
1236
+ async function loadSettings() {
1237
+ const el = document.getElementById('page-settings');
1238
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-text">Loading settings...</div></div>';
1239
+
1240
+ try {
1241
+ const res = await fetch('/api/settings');
1242
+ const s = await res.json();
1243
+
1244
+ el.innerHTML = \`
1245
+ <div class="page-title">\u2699\uFE0F Settings</div>
1246
+ <div class="page-subtitle">Configuration and connection details</div>
1247
+ <div class="settings-grid">
1248
+ <!-- Connection -->
1249
+ <div class="settings-card">
1250
+ <div class="settings-card-header">
1251
+ <div class="settings-card-icon" style="background:var(--green-bg);color:var(--green)">\u{1F50C}</div>
1252
+ <div>
1253
+ <div class="settings-card-title">Connection</div>
1254
+ <div class="settings-card-desc">Database connection details</div>
1255
+ </div>
1256
+ </div>
1257
+ <div class="settings-rows">
1258
+ <div class="setting-row"><span class="setting-key">Dialect</span><span class="setting-val">\${s.connection.dialect}</span></div>
1259
+ <div class="setting-row"><span class="setting-key">Host</span><span class="setting-val">\${s.connection.host}</span></div>
1260
+ <div class="setting-row"><span class="setting-key">Port</span><span class="setting-val">\${s.connection.port}</span></div>
1261
+ <div class="setting-row"><span class="setting-key">Database</span><span class="setting-val">\${s.connection.database}</span></div>
1262
+ <div class="setting-row"><span class="setting-key">Username</span><span class="setting-val">\${s.connection.username || '\u2014'}</span></div>
1263
+ <div class="setting-row"><span class="setting-key">Password</span><span class="setting-val masked">\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022</span></div>
1264
+ <div class="setting-row"><span class="setting-key">SSL</span><span class="setting-val">\${s.connection.ssl ? '\u2713 Enabled' : '\u2717 Disabled'}</span></div>
1265
+ <div class="setting-row"><span class="setting-key">Connection URL</span><span class="setting-val masked" style="font-size:11px">\${s.connection.connectionString}</span></div>
1266
+ </div>
1267
+ </div>
1268
+
1269
+ <!-- Schema -->
1270
+ <div class="settings-card">
1271
+ <div class="settings-card-header">
1272
+ <div class="settings-card-icon" style="background:var(--blue-bg);color:var(--blue)">\u{1F4C1}</div>
1273
+ <div>
1274
+ <div class="settings-card-title">Schema</div>
1275
+ <div class="settings-card-desc">Schema definition settings</div>
1276
+ </div>
1277
+ </div>
1278
+ <div class="settings-rows">
1279
+ <div class="setting-row"><span class="setting-key">Directory</span><span class="setting-val">\${s.schema.directory}</span></div>
1280
+ <div class="setting-row"><span class="setting-key">Casing</span><span class="setting-val">\${s.schema.casing}</span></div>
1281
+ </div>
1282
+ </div>
1283
+
1284
+ <!-- Migrations -->
1285
+ <div class="settings-card">
1286
+ <div class="settings-card-header">
1287
+ <div class="settings-card-icon" style="background:var(--yellow-bg);color:var(--yellow)">\u{1F4E6}</div>
1288
+ <div>
1289
+ <div class="settings-card-title">Migrations</div>
1290
+ <div class="settings-card-desc">Migration configuration</div>
1291
+ </div>
1292
+ </div>
1293
+ <div class="settings-rows">
1294
+ <div class="setting-row"><span class="setting-key">Directory</span><span class="setting-val">\${s.migrations.directory}</span></div>
1295
+ <div class="setting-row"><span class="setting-key">History Table</span><span class="setting-val">\${s.migrations.table}</span></div>
1296
+ </div>
1297
+ </div>
1298
+
1299
+ <!-- Studio -->
1300
+ <div class="settings-card">
1301
+ <div class="settings-card-header">
1302
+ <div class="settings-card-icon" style="background:var(--accent-bg);color:var(--accent-2)">\u{1F3A8}</div>
1303
+ <div>
1304
+ <div class="settings-card-title">Studio</div>
1305
+ <div class="settings-card-desc">Dashboard configuration</div>
1306
+ </div>
1307
+ </div>
1308
+ <div class="settings-rows">
1309
+ <div class="setting-row"><span class="setting-key">Version</span><span class="setting-val">\${s.studio.version}</span></div>
1310
+ <div class="setting-row"><span class="setting-key">Host</span><span class="setting-val">\${s.studio.host}</span></div>
1311
+ <div class="setting-row"><span class="setting-key">Port</span><span class="setting-val">\${s.studio.port}</span></div>
1312
+ <div class="setting-row"><span class="setting-key">URL</span><span class="setting-val">\${s.studio.url}</span></div>
1313
+ </div>
1314
+ </div>
1315
+
1316
+ <!-- Logger -->
1317
+ <div class="settings-card">
1318
+ <div class="settings-card-header">
1319
+ <div class="settings-card-icon" style="background:var(--red-bg);color:var(--red)">\u{1F4DD}</div>
1320
+ <div>
1321
+ <div class="settings-card-title">Logging</div>
1322
+ <div class="settings-card-desc">Query logging configuration</div>
1323
+ </div>
1324
+ </div>
1325
+ <div class="settings-rows">
1326
+ <div class="setting-row"><span class="setting-key">Query Logger</span><span class="setting-val">\${s.logger ? '\u2713 Enabled' : '\u2717 Disabled'}</span></div>
1327
+ </div>
1328
+ </div>
639
1329
  </div>
640
- </div>
641
- \`;
1330
+ \`;
1331
+ } catch (err) {
1332
+ el.innerHTML = '<div class="empty-state"><div class="empty-state-icon">\u274C</div><div class="empty-state-text">Failed to load settings</div></div>';
1333
+ }
642
1334
  }
643
1335
 
644
1336
  // ---- Init ----
645
- loadPage('tables');
1337
+ loadTables();
646
1338
  `;
647
1339
  }
648
1340
 
@@ -703,6 +1395,9 @@ async function handleRequest(req, res, options) {
703
1395
  if (path === "/api/health" && req.method === "GET") {
704
1396
  return jsonResponse(res, { status: "ok", dialect: options.config.dialect });
705
1397
  }
1398
+ if (path === "/api/settings" && req.method === "GET") {
1399
+ return jsonResponse(res, await getSettings(options));
1400
+ }
706
1401
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
707
1402
  res.end(getDashboardHTML());
708
1403
  }
@@ -906,6 +1601,69 @@ async function getERD(options) {
906
1601
  }
907
1602
  return { tables: [], relations: [] };
908
1603
  }
1604
+ async function getSettings(options) {
1605
+ const config = options.config;
1606
+ const connStr = typeof config.connection === "string" ? config.connection : "";
1607
+ let maskedConn = connStr;
1608
+ try {
1609
+ const url = new URL(connStr);
1610
+ if (url.password) {
1611
+ url.password = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
1612
+ maskedConn = url.toString();
1613
+ }
1614
+ } catch {
1615
+ }
1616
+ return {
1617
+ connection: {
1618
+ dialect: config.dialect,
1619
+ connectionString: maskedConn,
1620
+ host: (() => {
1621
+ try {
1622
+ return new URL(connStr).hostname;
1623
+ } catch {
1624
+ return "\u2014";
1625
+ }
1626
+ })(),
1627
+ port: (() => {
1628
+ try {
1629
+ return new URL(connStr).port || "5432";
1630
+ } catch {
1631
+ return "\u2014";
1632
+ }
1633
+ })(),
1634
+ database: (() => {
1635
+ try {
1636
+ return new URL(connStr).pathname.slice(1);
1637
+ } catch {
1638
+ return "\u2014";
1639
+ }
1640
+ })(),
1641
+ username: (() => {
1642
+ try {
1643
+ return new URL(connStr).username;
1644
+ } catch {
1645
+ return "\u2014";
1646
+ }
1647
+ })(),
1648
+ ssl: connStr.includes("sslmode=")
1649
+ },
1650
+ schema: {
1651
+ directory: config.schema,
1652
+ casing: config.casing ?? "camelCase"
1653
+ },
1654
+ migrations: {
1655
+ directory: config.out,
1656
+ table: config.migrationsTable ?? "_mountsqli_migrations"
1657
+ },
1658
+ studio: {
1659
+ port: options.port,
1660
+ host: options.host,
1661
+ version: "0.1.0",
1662
+ url: `http://${options.host}:${options.port}`
1663
+ },
1664
+ logger: config.logger ?? false
1665
+ };
1666
+ }
909
1667
  function jsonResponse(res, data) {
910
1668
  res.writeHead(200, { "Content-Type": "application/json" });
911
1669
  res.end(JSON.stringify(data));
@@ -944,12 +1702,11 @@ async function startStudio(options) {
944
1702
  \u{1F3A8} MountSQLi Studio running at http://${host}:${port}
945
1703
  `);
946
1704
  console.log(" Pages:");
947
- console.log(" / \u2014 Dashboard home");
948
- console.log(" /tables \u2014 Table explorer");
949
- console.log(" /query \u2014 SQL console");
950
- console.log(" /migrations \u2014 Migration status");
951
- console.log(" /erd \u2014 ER diagram");
952
- console.log(" /settings \u2014 Connection settings");
1705
+ console.log(" / \u2014 Dashboard (SPA)");
1706
+ console.log(" /api/tables \u2014 Table data API");
1707
+ console.log(" /api/query \u2014 SQL query API");
1708
+ console.log(" /api/erd \u2014 ER diagram API");
1709
+ console.log(" /api/settings \u2014 Settings API");
953
1710
  console.log("\n Press Ctrl+C to stop\n");
954
1711
  });
955
1712
  }