@hienlh/ppm 0.9.3 → 0.9.4

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 (28) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/web/assets/{browser-tab-LFNnCzgB.js → browser-tab-B8EBpCT6.js} +1 -1
  3. package/dist/web/assets/{chat-tab-rYBo5Mff.js → chat-tab-DVuWNtNl.js} +1 -1
  4. package/dist/web/assets/{code-editor-BdM11-0K.js → code-editor-Bre-_HLS.js} +1 -1
  5. package/dist/web/assets/{database-viewer-CINo6teP.js → database-viewer-As9Pwu58.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-_MPL-DRu.js → diff-viewer-DTw_Cqpy.js} +1 -1
  7. package/dist/web/assets/{extension-webview-BU1T2a8n.js → extension-webview-CgobD-e5.js} +1 -1
  8. package/dist/web/assets/{git-graph-Dde-j8cK.js → git-graph-CA1TsLfJ.js} +1 -1
  9. package/dist/web/assets/index-Dx21KBME.css +2 -0
  10. package/dist/web/assets/{index-CyXEMb4g.js → index-iaC9D6YI.js} +4 -4
  11. package/dist/web/assets/keybindings-store-BzWEhrZs.js +1 -0
  12. package/dist/web/assets/{markdown-renderer-Djgmbi23.js → markdown-renderer-zGERfKXC.js} +1 -1
  13. package/dist/web/assets/{postgres-viewer-FCpA6nh4.js → postgres-viewer-q-a11Zs_.js} +1 -1
  14. package/dist/web/assets/{settings-tab-Y37tD1kM.js → settings-tab-9ISb6u9A.js} +1 -1
  15. package/dist/web/assets/{sqlite-viewer-Cjl4uXyo.js → sqlite-viewer-CEfqfnpC.js} +1 -1
  16. package/dist/web/assets/{terminal-tab-CnYqGghP.js → terminal-tab-BGthbegR.js} +1 -1
  17. package/dist/web/assets/{use-monaco-theme-DGjkK3eO.js → use-monaco-theme-0QrlcbfM.js} +1 -1
  18. package/dist/web/index.html +2 -2
  19. package/dist/web/sw.js +1 -1
  20. package/package.json +1 -1
  21. package/packages/ext-database/package.json +9 -2
  22. package/packages/ext-database/src/connection-tree.ts +64 -5
  23. package/packages/ext-database/src/extension.ts +234 -2
  24. package/packages/ext-database/src/query-panel.ts +29 -16
  25. package/packages/ext-database/src/table-viewer-panel.ts +16 -6
  26. package/src/web/components/layout/mobile-nav.tsx +5 -5
  27. package/dist/web/assets/index-BkidPsSC.css +0 -2
  28. package/dist/web/assets/keybindings-store-6_p_JT0B.js +0 -1
@@ -6,13 +6,17 @@
6
6
  interface ConnectionNode {
7
7
  id: string;
8
8
  name: string;
9
- type: "connection" | "table" | "column";
9
+ type: "connection" | "group" | "table" | "column";
10
10
  connectionId?: number;
11
11
  connectionName?: string;
12
12
  connectionType?: string;
13
13
  connectionColor?: string | null;
14
+ connectionReadonly?: number;
15
+ groupName?: string | null;
14
16
  schemaName?: string;
15
17
  dataType?: string;
18
+ rowCount?: number;
19
+ children?: ConnectionNode[];
16
20
  }
17
21
 
18
22
  interface ApiConnection {
@@ -20,6 +24,8 @@ interface ApiConnection {
20
24
  name: string;
21
25
  type: string;
22
26
  color: string | null;
27
+ readonly: number;
28
+ group_name: string | null;
23
29
  }
24
30
 
25
31
  interface ApiTable {
@@ -54,7 +60,8 @@ export class ConnectionTreeProvider {
54
60
  }
55
61
 
56
62
  async getChildren(element?: ConnectionNode): Promise<ConnectionNode[]> {
57
- if (!element) return this.getConnections();
63
+ if (!element) return this.getRootNodes();
64
+ if (element.type === "group") return element.children ?? [];
58
65
  if (element.type === "connection") return this.getTables(element);
59
66
  if (element.type === "table") return this.getColumns(element);
60
67
  return [];
@@ -62,13 +69,26 @@ export class ConnectionTreeProvider {
62
69
 
63
70
  getTreeItem(element: ConnectionNode): Record<string, unknown> {
64
71
  const isConn = element.type === "connection";
72
+ const isGroup = element.type === "group";
65
73
  const isTable = element.type === "table";
66
74
  const isCol = element.type === "column";
67
75
 
76
+ // Table description: row count
77
+ let description: string | undefined;
78
+ if (isCol) description = element.dataType;
79
+ else if (isTable && element.rowCount !== undefined) description = `${element.rowCount.toLocaleString()} rows`;
80
+
81
+ // Connection badge: type + readonly
82
+ let badge: string | undefined;
83
+ if (isConn) {
84
+ badge = element.connectionType === "postgres" ? "PG" : "DB";
85
+ if (element.connectionReadonly === 1) badge += " 🔒";
86
+ }
87
+
68
88
  return {
69
89
  id: element.id,
70
90
  label: element.name,
71
- description: isCol ? element.dataType : undefined,
91
+ description,
72
92
  collapsibleState: isCol ? "none" : "collapsed",
73
93
  contextValue: element.type,
74
94
  command: isTable ? "ppm-db.openViewer" : undefined,
@@ -76,13 +96,49 @@ export class ConnectionTreeProvider {
76
96
  ? [element.connectionId, element.connectionName ?? "Database", element.name, element.schemaName ?? "public"]
77
97
  : undefined,
78
98
  color: isConn ? (element.connectionColor ?? undefined) : undefined,
79
- badge: isConn ? (element.connectionType === "postgres" ? "PG" : "DB") : undefined,
99
+ badge,
80
100
  actions: isConn ? [
81
101
  { icon: "refresh", tooltip: "Refresh tables", command: "ppm-db.refreshConnection", commandArgs: [element.connectionId] },
82
- ] : undefined,
102
+ { icon: "edit", tooltip: "Edit connection", command: "ppm-db.editConnection", commandArgs: [element.connectionId] },
103
+ { icon: "trash", tooltip: "Delete connection", command: "ppm-db.deleteConnection", commandArgs: [element.connectionId, element.name] },
104
+ ] : isGroup ? [] : undefined,
83
105
  };
84
106
  }
85
107
 
108
+ /** Build root tree: group nodes wrapping connection nodes, or flat if no groups */
109
+ private async getRootNodes(): Promise<ConnectionNode[]> {
110
+ const connections = await this.getConnections();
111
+ // Group by group_name
112
+ const groups = new Map<string, ConnectionNode[]>();
113
+ for (const conn of connections) {
114
+ const key = conn.groupName ?? "__ungrouped__";
115
+ const list = groups.get(key) ?? [];
116
+ list.push(conn);
117
+ groups.set(key, list);
118
+ }
119
+ // If only one group (ungrouped), return connections flat
120
+ if (groups.size <= 1 && groups.has("__ungrouped__")) {
121
+ return connections;
122
+ }
123
+ // Build group nodes
124
+ const result: ConnectionNode[] = [];
125
+ const sortedKeys = Array.from(groups.keys()).sort((a, b) => {
126
+ if (a === "__ungrouped__") return 1;
127
+ if (b === "__ungrouped__") return -1;
128
+ return a.localeCompare(b);
129
+ });
130
+ for (const key of sortedKeys) {
131
+ const label = key === "__ungrouped__" ? "Ungrouped" : key;
132
+ result.push({
133
+ id: `group:${key}`,
134
+ name: label,
135
+ type: "group",
136
+ children: groups.get(key) ?? [],
137
+ });
138
+ }
139
+ return result;
140
+ }
141
+
86
142
  private async getConnections(): Promise<ConnectionNode[]> {
87
143
  try {
88
144
  const res = await fetch(`${this.baseUrl}/api/db/connections`);
@@ -95,6 +151,8 @@ export class ConnectionTreeProvider {
95
151
  connectionId: c.id,
96
152
  connectionType: c.type,
97
153
  connectionColor: c.color,
154
+ connectionReadonly: c.readonly,
155
+ groupName: c.group_name,
98
156
  }));
99
157
  } catch {
100
158
  return [];
@@ -114,6 +172,7 @@ export class ConnectionTreeProvider {
114
172
  connectionName: conn.name,
115
173
  connectionType: conn.connectionType,
116
174
  schemaName: t.schema,
175
+ rowCount: t.rowCount,
117
176
  }));
118
177
  } catch {
119
178
  return [];
@@ -96,6 +96,67 @@ export function activate(context: ExtensionContext, vscode: VscodeApi): void {
96
96
  }),
97
97
  );
98
98
 
99
+ context.subscriptions.push(
100
+ vscode.commands.registerCommand("ppm-db.editConnection", async (...args: unknown[]) => {
101
+ const connectionId = args[0] as number;
102
+ if (connectionId) await editConnection(vscode, treeProvider, connectionId);
103
+ }),
104
+ );
105
+
106
+ context.subscriptions.push(
107
+ vscode.commands.registerCommand("ppm-db.deleteConnection", async (...args: unknown[]) => {
108
+ const connectionId = args[0] as number;
109
+ const connectionName = (args[1] as string) ?? "this connection";
110
+ if (!connectionId) return;
111
+ const confirm = await vscode.window.showQuickPick(["Yes, delete", "Cancel"], {
112
+ placeHolder: `Delete "${connectionName}"?`,
113
+ });
114
+ if (confirm !== "Yes, delete") return;
115
+ try {
116
+ const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}`, { method: "DELETE" });
117
+ const json = await res.json() as { ok: boolean; error?: string };
118
+ if (json.ok) {
119
+ await vscode.window.showInformationMessage(`Connection "${connectionName}" deleted`);
120
+ treeProvider.refresh();
121
+ } else {
122
+ await vscode.window.showErrorMessage(json.error ?? "Failed to delete connection");
123
+ }
124
+ } catch (e) {
125
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
126
+ }
127
+ }),
128
+ );
129
+
130
+ context.subscriptions.push(
131
+ vscode.commands.registerCommand("ppm-db.testConnection", async (...args: unknown[]) => {
132
+ const connectionId = args[0] as number;
133
+ if (!connectionId) return;
134
+ try {
135
+ const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}/test`, { method: "POST" });
136
+ const json = await res.json() as { ok: boolean; data?: { ok: boolean; error?: string } };
137
+ if (json.ok && json.data?.ok) {
138
+ await vscode.window.showInformationMessage("Connection successful");
139
+ } else {
140
+ await vscode.window.showErrorMessage(`Connection failed: ${json.data?.error ?? "Unknown error"}`);
141
+ }
142
+ } catch (e) {
143
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
144
+ }
145
+ }),
146
+ );
147
+
148
+ context.subscriptions.push(
149
+ vscode.commands.registerCommand("ppm-db.exportConnections", async () => {
150
+ await exportConnections(vscode);
151
+ }),
152
+ );
153
+
154
+ context.subscriptions.push(
155
+ vscode.commands.registerCommand("ppm-db.importConnections", async () => {
156
+ await importConnections(vscode, treeProvider);
157
+ }),
158
+ );
159
+
99
160
  // --- Status Bar ---
100
161
  const statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 10);
101
162
  statusItem.text = "DB";
@@ -297,6 +358,27 @@ function openQueryPanel(
297
358
  // Add Connection — collect info via QuickPick + InputBox, then POST to API
298
359
  // ---------------------------------------------------------------------------
299
360
 
361
+ /** Collect optional group/color/readonly via InputBox + QuickPick */
362
+ async function collectConnectionExtras(vscode: VscodeApi): Promise<{ groupName?: string; color?: string; readonly?: number } | null> {
363
+ const groupName = await vscode.window.showInputBox({ prompt: "Group name (optional)", placeHolder: "e.g. Production" });
364
+ if (groupName === undefined) return null; // cancelled
365
+
366
+ const COLOR_OPTIONS = ["None", "#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899"];
367
+ const color = await vscode.window.showQuickPick(COLOR_OPTIONS, { placeHolder: "Pick a color (optional)" });
368
+ if (color === undefined) return null;
369
+
370
+ const readonlyChoice = await vscode.window.showQuickPick(["Yes (recommended)", "No"], {
371
+ placeHolder: "Readonly mode? (blocks write queries)",
372
+ });
373
+ if (readonlyChoice === undefined) return null;
374
+
375
+ return {
376
+ groupName: groupName || undefined,
377
+ color: color === "None" ? undefined : color,
378
+ readonly: readonlyChoice.startsWith("Yes") ? 1 : 0,
379
+ };
380
+ }
381
+
300
382
  async function addConnection(
301
383
  vscode: VscodeApi,
302
384
  treeProvider: ConnectionTreeProvider,
@@ -326,12 +408,16 @@ async function addConnection(
326
408
  connectionConfig = { type: "postgres", connectionString: connStr };
327
409
  }
328
410
 
329
- // 4. Create via API
411
+ // 4. Group, color, readonly
412
+ const extras = await collectConnectionExtras(vscode);
413
+ if (extras === null) return;
414
+
415
+ // 5. Create via API
330
416
  try {
331
417
  const res = await fetch(`${baseUrl}/api/db/connections`, {
332
418
  method: "POST",
333
419
  headers: { "Content-Type": "application/json" },
334
- body: JSON.stringify({ type, name, connectionConfig }),
420
+ body: JSON.stringify({ type, name, connectionConfig, ...extras }),
335
421
  });
336
422
  const json = await res.json() as { ok: boolean; error?: string };
337
423
  if (json.ok) {
@@ -344,3 +430,149 @@ async function addConnection(
344
430
  await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
345
431
  }
346
432
  }
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // Edit Connection — update name, group, color, readonly via QuickPick/InputBox
436
+ // ---------------------------------------------------------------------------
437
+
438
+ async function editConnection(
439
+ vscode: VscodeApi,
440
+ treeProvider: ConnectionTreeProvider,
441
+ connectionId: number,
442
+ ): Promise<void> {
443
+ // Fetch current connection data
444
+ let conn: { id: number; name: string; type: string; group_name?: string; color?: string; readonly?: number };
445
+ try {
446
+ const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}`);
447
+ const json = await res.json() as { ok: boolean; data?: typeof conn };
448
+ if (!json.ok || !json.data) {
449
+ await vscode.window.showErrorMessage("Failed to load connection");
450
+ return;
451
+ }
452
+ conn = json.data;
453
+ } catch (e) {
454
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
455
+ return;
456
+ }
457
+
458
+ // Name
459
+ const name = await vscode.window.showInputBox({ prompt: "Connection name", value: conn.name });
460
+ if (name === undefined) return;
461
+
462
+ // Connection config (optional update)
463
+ let connectionConfig: Record<string, string> | undefined;
464
+ const updateConfig = await vscode.window.showQuickPick(["Keep current", "Update connection config"], {
465
+ placeHolder: "Connection config",
466
+ });
467
+ if (updateConfig === undefined) return;
468
+ if (updateConfig === "Update connection config") {
469
+ if (conn.type === "sqlite") {
470
+ const path = await vscode.window.showInputBox({ prompt: "SQLite file path", placeHolder: "/path/to/database.db" });
471
+ if (path === undefined) return;
472
+ if (path) connectionConfig = { type: "sqlite", path };
473
+ } else {
474
+ const connStr = await vscode.window.showInputBox({ prompt: "PostgreSQL connection string", placeHolder: "postgres://user:pass@host:5432/dbname" });
475
+ if (connStr === undefined) return;
476
+ if (connStr) connectionConfig = { type: "postgres", connectionString: connStr };
477
+ }
478
+ }
479
+
480
+ // Group, color, readonly
481
+ const groupName = await vscode.window.showInputBox({ prompt: "Group name", value: conn.group_name ?? "" });
482
+ if (groupName === undefined) return;
483
+
484
+ const COLOR_OPTIONS = ["None", "#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899"];
485
+ const color = await vscode.window.showQuickPick(COLOR_OPTIONS, { placeHolder: `Current color: ${conn.color ?? "None"}` });
486
+ if (color === undefined) return;
487
+
488
+ const readonlyChoice = await vscode.window.showQuickPick(["Yes (recommended)", "No"], {
489
+ placeHolder: `Readonly? (current: ${conn.readonly === 1 ? "Yes" : "No"})`,
490
+ });
491
+ if (readonlyChoice === undefined) return;
492
+
493
+ // Update via API
494
+ try {
495
+ const body: Record<string, unknown> = {
496
+ name: name || conn.name,
497
+ groupName: groupName || null,
498
+ color: color === "None" ? null : color,
499
+ readonly: readonlyChoice.startsWith("Yes") ? 1 : 0,
500
+ };
501
+ if (connectionConfig) body.connectionConfig = connectionConfig;
502
+
503
+ const res = await fetch(`${baseUrl}/api/db/connections/${connectionId}`, {
504
+ method: "PUT",
505
+ headers: { "Content-Type": "application/json" },
506
+ body: JSON.stringify(body),
507
+ });
508
+ const json = await res.json() as { ok: boolean; error?: string };
509
+ if (json.ok) {
510
+ await vscode.window.showInformationMessage(`Connection "${name || conn.name}" updated`);
511
+ treeProvider.refresh();
512
+ } else {
513
+ await vscode.window.showErrorMessage(json.error ?? "Failed to update connection");
514
+ }
515
+ } catch (e) {
516
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
517
+ }
518
+ }
519
+
520
+ // ---------------------------------------------------------------------------
521
+ // Export Connections — copy JSON to clipboard via showInformationMessage
522
+ // ---------------------------------------------------------------------------
523
+
524
+ async function exportConnections(vscode: VscodeApi): Promise<void> {
525
+ try {
526
+ const res = await fetch(`${baseUrl}/api/db/connections/export`);
527
+ const json = await res.json() as { ok: boolean; data?: unknown };
528
+ if (!json.ok || !json.data) {
529
+ await vscode.window.showErrorMessage("Failed to export connections");
530
+ return;
531
+ }
532
+ // In extension context, we can't write to clipboard directly — show data as info
533
+ const data = json.data as { connections: unknown[] };
534
+ await vscode.window.showInformationMessage(`Exported ${data.connections.length} connection(s). Use built-in UI for file export.`);
535
+ } catch (e) {
536
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
537
+ }
538
+ }
539
+
540
+ // ---------------------------------------------------------------------------
541
+ // Import Connections — paste JSON via InputBox
542
+ // ---------------------------------------------------------------------------
543
+
544
+ async function importConnections(
545
+ vscode: VscodeApi,
546
+ treeProvider: ConnectionTreeProvider,
547
+ ): Promise<void> {
548
+ const jsonStr = await vscode.window.showInputBox({
549
+ prompt: "Paste connections JSON (from export)",
550
+ placeHolder: '{"connections": [...]}',
551
+ });
552
+ if (!jsonStr) return;
553
+
554
+ try {
555
+ const data = JSON.parse(jsonStr);
556
+ const conns = data.connections ?? data;
557
+ if (!Array.isArray(conns)) {
558
+ await vscode.window.showErrorMessage("Invalid format: expected connections array");
559
+ return;
560
+ }
561
+ const res = await fetch(`${baseUrl}/api/db/connections/import`, {
562
+ method: "POST",
563
+ headers: { "Content-Type": "application/json" },
564
+ body: JSON.stringify({ connections: conns }),
565
+ });
566
+ const json = await res.json() as { ok: boolean; data?: { imported: number; skipped: number; errors: string[] } };
567
+ if (json.ok && json.data) {
568
+ let msg = `Imported ${json.data.imported} connection(s)`;
569
+ if (json.data.skipped > 0) msg += `, ${json.data.skipped} skipped`;
570
+ await vscode.window.showInformationMessage(msg);
571
+ treeProvider.refresh();
572
+ } else {
573
+ await vscode.window.showErrorMessage("Import failed");
574
+ }
575
+ } catch (e) {
576
+ await vscode.window.showErrorMessage(`Error: ${e instanceof Error ? e.message : String(e)}`);
577
+ }
578
+ }
@@ -15,25 +15,38 @@ export function getQueryPanelHtml(connectionName: string, tableName?: string): s
15
15
  <head>
16
16
  <meta charset="utf-8">
17
17
  <style>
18
- * { box-sizing: border-box; margin: 0; padding: 0; }
19
- body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #1e1e2e; color: #cdd6f4; padding: 12px; font-size: 13px; }
18
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
19
+ :root {
20
+ --bg: #ffffff; --surface: #f4f4f5; --text: #09090b; --subtext: #71717a; --subtle: #a1a1aa;
21
+ --border: #e4e4e7; --border2: #d4d4d8; --blue: #3b82f6; --red: #ef4444; --green: #22c55e;
22
+ --surface-hover: #f4f4f5;
23
+ }
24
+ @media (prefers-color-scheme: dark) {
25
+ :root {
26
+ --bg: #09090b; --surface: #18181b; --text: #fafafa; --subtext: #a1a1aa; --subtle: #52525b;
27
+ --border: #27272a; --border2: #3f3f46; --blue: #3b82f6; --red: #ef4444; --green: #22c55e;
28
+ --surface-hover: #27272a;
29
+ }
30
+ }
31
+ body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg); color: var(--text); padding: 12px; font-size: 13px; }
20
32
  .header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
21
33
  .header h3 { font-size: 14px; font-weight: 600; }
22
- .header .badge { background: #313244; padding: 2px 8px; border-radius: 4px; font-size: 11px; color: #a6adc8; }
23
- textarea { width: 100%; height: 80px; background: #313244; border: 1px solid #45475a; border-radius: 6px; color: #cdd6f4; padding: 8px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; resize: vertical; }
24
- textarea:focus { outline: none; border-color: #89b4fa; }
34
+ .header .badge { background: var(--surface); padding: 2px 8px; border-radius: 4px; font-size: 11px; color: var(--subtext); }
35
+ textarea { width: 100%; height: 80px; background: var(--surface); border: 1px solid var(--border2); border-radius: 6px; color: var(--text); padding: 8px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; resize: vertical; }
36
+ textarea:focus { outline: none; border-color: var(--blue); }
25
37
  .actions { display: flex; gap: 8px; margin: 8px 0; }
26
- button { background: #89b4fa; color: #1e1e2e; border: none; padding: 6px 16px; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer; }
27
- button:hover { background: #74c7ec; }
28
- button.secondary { background: #313244; color: #cdd6f4; }
29
- .status { font-size: 11px; color: #a6adc8; margin: 4px 0; }
30
- .error { color: #f38ba8; background: #45475a; padding: 8px; border-radius: 4px; margin: 8px 0; font-size: 12px; }
38
+ button { background: var(--blue); color: #fff; border: none; padding: 6px 16px; border-radius: 4px; font-size: 12px; font-weight: 600; cursor: pointer; }
39
+ button:hover { opacity: 0.9; }
40
+ button.secondary { background: var(--surface); color: var(--text); }
41
+ .status { font-size: 11px; color: var(--subtext); margin: 4px 0; }
42
+ .error { color: var(--red); background: var(--surface); padding: 8px; border-radius: 4px; margin: 8px 0; font-size: 12px; }
31
43
  table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 12px; }
32
- th { background: #313244; text-align: left; padding: 6px 8px; border-bottom: 1px solid #45475a; font-weight: 600; position: sticky; top: 0; }
33
- td { padding: 4px 8px; border-bottom: 1px solid #313244; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
34
- tr:hover td { background: #313244; }
35
- .results { max-height: 60vh; overflow: auto; border: 1px solid #45475a; border-radius: 6px; }
36
- .empty { text-align: center; padding: 24px; color: #6c7086; }
44
+ th { background: var(--surface); text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border2); font-weight: 600; position: sticky; top: 0; color: var(--subtext); }
45
+ td { padding: 4px 8px; border-bottom: 1px solid var(--border); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
46
+ tr:hover td { background: var(--surface-hover); }
47
+ .results { max-height: 60vh; overflow: auto; border: 1px solid var(--border2); border-radius: 6px; }
48
+ .empty { text-align: center; padding: 24px; color: var(--subtle); }
49
+ .null-val { color: var(--subtle); font-style: italic; }
37
50
  </style>
38
51
  </head>
39
52
  <body>
@@ -101,7 +114,7 @@ export function getQueryPanelHtml(connectionName: string, tableName?: string): s
101
114
  html += '<tr>';
102
115
  cols.forEach(c => {
103
116
  const v = row[c];
104
- html += '<td>' + (v === null ? '<span style="color:#6c7086">NULL</span>' : esc(v)) + '</td>';
117
+ html += v === null ? '<td class="null-val">NULL</td>' : '<td>' + esc(v) + '</td>';
105
118
  });
106
119
  html += '</tr>';
107
120
  });
@@ -27,12 +27,22 @@ export function getTableViewerHtml(opts: TableViewerOptions): string {
27
27
  <style>
28
28
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
29
29
  :root {
30
- --bg: #1e1e2e; --surface: #181825; --mantle: #11111b;
31
- --overlay0: #6c7086; --overlay1: #7f849c;
32
- --text: #cdd6f4; --subtext: #a6adc8; --subtle: #585b70;
33
- --border: #313244; --border2: #45475a;
34
- --blue: #89b4fa; --green: #a6e3a1; --red: #f38ba8; --yellow: #f9e2af;
35
- --surface-hover: #313244;
30
+ --bg: #ffffff; --surface: #f4f4f5; --mantle: #fafafa;
31
+ --overlay0: #71717a; --overlay1: #52525b;
32
+ --text: #09090b; --subtext: #71717a; --subtle: #a1a1aa;
33
+ --border: #e4e4e7; --border2: #d4d4d8;
34
+ --blue: #3b82f6; --green: #22c55e; --red: #ef4444; --yellow: #eab308;
35
+ --surface-hover: #f4f4f5;
36
+ }
37
+ @media (prefers-color-scheme: dark) {
38
+ :root {
39
+ --bg: #09090b; --surface: #18181b; --mantle: #09090b;
40
+ --overlay0: #71717a; --overlay1: #a1a1aa;
41
+ --text: #fafafa; --subtext: #a1a1aa; --subtle: #52525b;
42
+ --border: #27272a; --border2: #3f3f46;
43
+ --blue: #3b82f6; --green: #22c55e; --red: #ef4444; --yellow: #eab308;
44
+ --surface-hover: #27272a;
45
+ }
36
46
  }
37
47
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); font-size: 13px; display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
38
48
 
@@ -116,8 +116,8 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
116
116
  <div className="flex items-center h-12">
117
117
  {/* Fixed section: Menu + Project + Add — curved right edge */}
118
118
  <div className={cn(
119
- "flex items-center shrink-0 bg-background relative z-10 rounded-r-2xl border-r border-border transition-shadow duration-200",
120
- canScrollLeft && "shadow-[6px_0_12px_-4px_rgba(0,0,0,0.12)]",
119
+ "flex items-center shrink-0 bg-background relative z-10 transition-all duration-200",
120
+ canScrollLeft ? "rounded-r-2xl shadow-[6px_0_12px_-4px_rgba(0,0,0,0.12)]" : "border-r border-border",
121
121
  )}>
122
122
  <button onClick={onMenuPress} className="flex items-center justify-center size-12 shrink-0 text-text-secondary">
123
123
  <Menu className="size-5" />
@@ -156,9 +156,9 @@ export function MobileNav({ onMenuPress, onProjectsPress }: MobileNavProps) {
156
156
  </button>
157
157
  </div>
158
158
 
159
- {/* Tab list — scrollable */}
160
- <div className="flex-1 min-w-0 relative flex items-center h-12">
161
- <div ref={mobileScrollRef} className="flex-1 min-w-0 flex items-center h-12 overflow-x-auto scrollbar-none">
159
+ {/* Tab list — overlaps under curved edge so tabs slide beneath it */}
160
+ <div className="flex-1 min-w-0 relative flex items-center h-12 -ml-4">
161
+ <div ref={mobileScrollRef} className="flex-1 min-w-0 flex items-center h-12 overflow-x-auto scrollbar-none pl-4">
162
162
  {tabs.map((tab) => {
163
163
  const Icon = TAB_ICONS[tab.type];
164
164
  const isActive = tab.id === activeTabId;