pglens 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ tags:
7
+ - "v*"
8
+
9
+ jobs:
10
+ build:
11
+ strategy:
12
+ matrix:
13
+ os: [macos-latest, ubuntu-latest, windows-latest]
14
+ runs-on: ${{ matrix.os }}
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-node@v4
19
+ with:
20
+ node-version: 20
21
+ - run: npm ci
22
+ - run: npm run dist
23
+ - uses: softprops/action-gh-release@v1
24
+ with:
25
+ files: |
26
+ dist/*.dmg
27
+ dist/*.zip
28
+ dist/*.exe
29
+ dist/*.AppImage
30
+ dist/*.deb
package/CHANGELOG.md CHANGED
@@ -5,18 +5,35 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.2.0] - 2026-02-02
9
+
10
+ ### Added
11
+
12
+ - **Row Numbers**: Table rows now display row numbers for easier navigation and reference.
13
+ - **Table Schema Viewer**: View table structure and column definitions directly from the UI.
14
+ - **Spotlight Search**: Quick table search with `Cmd+K` / `Ctrl+K` keyboard shortcut for fast navigation.
15
+ - **Connection Persistence**: Desktop app now saves connections and restores them on restart.
16
+ - **Auto-Updates**: Desktop app automatically checks for updates and notifies when new versions are available.
17
+
18
+ ### Changed
19
+
20
+ - Cleaner codebase with reduced unnecessary comments.
21
+
8
22
  ## [2.1.0] - 2026-01-11
9
23
 
10
24
  ### Added
25
+
11
26
  - **Default Landing Page**: The app now defaults to an "All Connections" grid view on startup.
12
27
  - **Simplified Sidebar**: Connection management (Edit/Delete) is now centralized on the Landing Page.
13
28
  - **Smart View Switching**: Table list and search are hidden until a server is explicitly selected.
14
29
 
15
30
  ### Changed
31
+
16
32
  - **Navigation Flow**: "Add Connection" and Logo clicks efficiently return you to the Landing Page.
17
33
  - **Auto-Connect Disabled**: Adding a new connection returns to the grid instead of auto-opening the server.
18
34
 
19
35
  ### Fixed
36
+
20
37
  - **View Persistence**: Reconnecting to an active server no longer overwrites open tabs with a loading screen.
21
38
  - **Connection Updates**: Fixed issue where the connection grid would not update immediately after adding a server.
22
39
 
@@ -83,4 +100,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
83
100
 
84
101
  [1.1.0]: https://github.com/tsvillain/pglens/compare/v1.0.0...v1.1.0
85
102
  [1.0.0]: https://github.com/tsvillain/pglens/releases/tag/v1.0.0
86
-
package/README.md CHANGED
@@ -5,9 +5,13 @@ A simple PostgreSQL database viewer tool. Perfect to quickly view and explore yo
5
5
  ## Features
6
6
 
7
7
  - 🔌 **Connection Manager**: Manage multiple database connections from a single UI
8
+ - 💾 **Connection Persistence**: Saved connections are restored when you reopen the app
8
9
  - 🚀 **Background Service**: Runs as a daemon process for persistent access
9
10
  - 🗂️ **Table Browser**: View all tables in your database in a clean, searchable sidebar
11
+ - 🔎 **Spotlight Search**: Quick table search with `Cmd+K` / `Ctrl+K` for fast navigation
10
12
  - 📊 **Data Viewer**: Browse table rows with a modern, easy-to-read interface
13
+ - 🔢 **Row Numbers**: Row numbers displayed for easier navigation and reference
14
+ - 📋 **Table Schema**: View table structure and column definitions directly from the UI
11
15
  - 📝 **Cell Content Viewer**: Double-click any cell to view full content in a popup
12
16
  - 🎨 **JSON/JSONB Formatting**: Auto-formats JSON data with syntax highlighting
13
17
  - 🕒 **Timezone Support**: View timestamps in local, UTC, or other timezones
@@ -21,6 +25,7 @@ A simple PostgreSQL database viewer tool. Perfect to quickly view and explore yo
21
25
  - 🎨 **Theme Support**: Choose between light, dark, or system theme
22
26
  - ⚡ **Optimized Performance**: Uses cursor-based pagination for efficient large table navigation
23
27
  - 🔒 **SSL Support**: Configurable SSL modes (Disable, Require, Prefer, Verify CA/Full)
28
+ - 🔄 **Auto-Updates**: Desktop app automatically checks for and installs updates
24
29
  - 🚀 **Easy Setup**: Install globally and run with a single command
25
30
 
26
31
  ## Installation
@@ -54,8 +59,6 @@ pglens url
54
59
 
55
60
  ### Connect to a Database
56
61
 
57
- ### Connect to a Database
58
-
59
62
  1. Open `http://localhost:54321` to see the **All Connections** landing page.
60
63
  2. Click the **Add Connection** card or the **+** icon in the grid.
61
64
  3. Enter your connection details using one of the tabs:
@@ -77,7 +80,7 @@ pglens stop
77
80
 
78
81
  1. **Start**: Run `pglens start` to launch the background service
79
82
  2. **Connect**: Add one or more database connections via the Web UI
80
- 3. **Explore**:
83
+ 3. **Explore**:
81
84
  - Use the sidebar to browse tables across different connections
82
85
  - Double-click cells to view detailed content
83
86
  - Use the "Columns" menu to toggle visibility
@@ -92,10 +95,40 @@ To develop or modify pglens:
92
95
  git clone https://github.com/tsvillain/pglens.git
93
96
  cd pglens
94
97
 
98
+ # Install dependencies
95
99
  # Install dependencies
96
100
  npm install
101
+ ```
102
+
103
+ ### Run Desktop App
104
+
105
+ To run the application as a standalone desktop app during development:
106
+
107
+ ```bash
108
+ npm run electron:start
109
+ ```
110
+
111
+ ### Build Desktop App
112
+
113
+ To build the desktop application for your current platform:
97
114
 
98
- # Run locally (starts server in foreground for logs)
115
+ ```bash
116
+ npm run dist
117
+ ```
118
+
119
+ To build for specific platforms (requires supported environment):
120
+
121
+ ```bash
122
+ npm run dist:mac # Build for macOS
123
+ npm run dist:win # Build for Windows
124
+ npm run dist:linux # Build for Linux
125
+ ```
126
+
127
+ ### Run Server Locally
128
+
129
+ To run the server locally in foreground:
130
+
131
+ ```bash
99
132
  node bin/pglens serve
100
133
  ```
101
134
 
package/client/app.js CHANGED
@@ -60,6 +60,19 @@ const connectButton = document.getElementById('connectButton');
60
60
  const connectionError = document.getElementById('connectionError');
61
61
  const loadingOverlay = document.getElementById('loadingOverlay');
62
62
 
63
+ // Spotlight Elements
64
+ const spotlightOverlay = document.getElementById('spotlightOverlay');
65
+ const spotlightInput = document.getElementById('spotlightInput');
66
+ const spotlightResults = document.getElementById('spotlightResults');
67
+ let spotlightSelectedIndex = -1;
68
+ let spotlightMatches = [];
69
+
70
+ // Schema Dialog UI Elements
71
+ const schemaDialog = document.getElementById('schemaDialog');
72
+ const closeSchemaDialogBtn = document.getElementById('closeSchemaDialog');
73
+ const closeSchemaButton = document.getElementById('closeSchemaButton');
74
+ const schemaTableContainer = document.getElementById('schemaTableContainer');
75
+
63
76
  /**
64
77
  * Initialize the application when DOM is ready.
65
78
  * Sets up event listeners and loads initial data.
@@ -112,9 +125,8 @@ document.addEventListener('DOMContentLoaded', () => {
112
125
  });
113
126
 
114
127
  sidebarToggle.addEventListener('click', () => {
115
- if (tabs.length > 0) {
116
- sidebar.classList.toggle('minimized');
117
- }
128
+ sidebar.classList.toggle('minimized');
129
+ updateSidebarToggleState();
118
130
  });
119
131
 
120
132
  updateSidebarToggleState();
@@ -163,6 +175,66 @@ document.addEventListener('DOMContentLoaded', () => {
163
175
  if (newConnectionBtn) {
164
176
  newConnectionBtn.addEventListener('click', () => showConnectionDialog(true));
165
177
  }
178
+
179
+ // Schema Dialog Listeners
180
+ if (closeSchemaDialogBtn) {
181
+ closeSchemaDialogBtn.addEventListener('click', hideSchemaDialog);
182
+ }
183
+ if (closeSchemaButton) {
184
+ closeSchemaButton.addEventListener('click', hideSchemaDialog);
185
+ }
186
+
187
+ // Close schema dialog on outside click
188
+ if (schemaDialog) {
189
+ schemaDialog.addEventListener('click', (e) => {
190
+ if (e.target === schemaDialog) {
191
+ hideSchemaDialog();
192
+ }
193
+ });
194
+ }
195
+
196
+ // Spotlight Search Shortcut (Cmd+K / Ctrl+K)
197
+ document.addEventListener('keydown', (e) => {
198
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
199
+ e.preventDefault();
200
+ // Only toggle if tables are loaded
201
+ if (allTables && allTables.length > 0) {
202
+ toggleSpotlight();
203
+ }
204
+ }
205
+
206
+ // Spotlight Navigation
207
+ if (spotlightOverlay.style.display !== 'none') {
208
+ if (e.key === 'Escape') {
209
+ toggleSpotlight(false);
210
+ } else if (e.key === 'ArrowDown') {
211
+ e.preventDefault();
212
+ navigateSpotlight(1);
213
+ } else if (e.key === 'ArrowUp') {
214
+ e.preventDefault();
215
+ navigateSpotlight(-1);
216
+ } else if (e.key === 'Enter') {
217
+ e.preventDefault();
218
+ executeSpotlightSelection();
219
+ }
220
+ }
221
+ });
222
+
223
+ // Spotlight Input Listener
224
+ if (spotlightInput) {
225
+ spotlightInput.addEventListener('input', (e) => {
226
+ filterSpotlight(e.target.value);
227
+ });
228
+ }
229
+
230
+ // Close spotlight on overlay click
231
+ if (spotlightOverlay) {
232
+ spotlightOverlay.addEventListener('click', (e) => {
233
+ if (e.target === spotlightOverlay) {
234
+ toggleSpotlight(false);
235
+ }
236
+ });
237
+ }
166
238
  });
167
239
 
168
240
  /**
@@ -216,11 +288,7 @@ function updateThemeIcon() {
216
288
  }
217
289
 
218
290
  function updateSidebarToggleState() {
219
- if (tabs.length === 0 && connections.length === 0) {
220
- sidebarToggle.disabled = true;
221
- sidebarToggle.classList.add('disabled');
222
- sidebar.classList.remove('minimized');
223
- } else {
291
+ if (sidebarToggle) {
224
292
  sidebarToggle.disabled = false;
225
293
  sidebarToggle.classList.remove('disabled');
226
294
  }
@@ -286,6 +354,26 @@ function renderConnectionsList() {
286
354
  li.classList.add('active');
287
355
  }
288
356
 
357
+ // Generate Initials
358
+ let initials = '';
359
+ if (conn.name) {
360
+ const parts = conn.name.trim().split(/\s+/);
361
+ if (parts.length >= 2) {
362
+ initials = (parts[0][0] + parts[1][0]).toUpperCase();
363
+ } else if (conn.name.length >= 2) {
364
+ initials = conn.name.substring(0, 2).toUpperCase();
365
+ } else {
366
+ initials = conn.name.substring(0, 1).toUpperCase();
367
+ }
368
+ } else {
369
+ initials = 'DB';
370
+ }
371
+
372
+ const initialsDiv = document.createElement('div');
373
+ initialsDiv.className = 'connection-initials';
374
+ initialsDiv.textContent = initials;
375
+ li.appendChild(initialsDiv);
376
+
289
377
  const nameSpan = document.createElement('span');
290
378
  nameSpan.className = 'connection-name';
291
379
  nameSpan.textContent = conn.name;
@@ -301,6 +389,9 @@ function renderConnectionsList() {
301
389
  });
302
390
 
303
391
  connectionsList.appendChild(li);
392
+
393
+ // Add tooltip for minimized state
394
+ li.title = conn.name;
304
395
  });
305
396
  }
306
397
 
@@ -1299,6 +1390,7 @@ async function loadTableData() {
1299
1390
  tab.hasPrimaryKey = data.hasPrimaryKey || false;
1300
1391
  tab.isApproximate = data.isApproximate || false;
1301
1392
  tab.data = data; // Cache data for client-side sorting
1393
+ tab.columns = data.columns; // Store column metadata for schema view
1302
1394
 
1303
1395
  // Update cursor for next page navigation
1304
1396
  if (data.nextCursor) {
@@ -1387,6 +1479,14 @@ function renderTableHeader(tab, columns = [], isShimmer = false) {
1387
1479
  </svg>
1388
1480
  <span class="refresh-text">Refresh</span>
1389
1481
  </button>
1482
+ <button class="menu-button" id="schemaButton" title="View Schema">
1483
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1484
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
1485
+ <line x1="3" y1="9" x2="21" y2="9"></line>
1486
+ <line x1="9" y1="21" x2="9" y2="9"></line>
1487
+ </svg>
1488
+ <span>Schema</span>
1489
+ </button>
1390
1490
  <div class="limit-selector">
1391
1491
  <select id="limitSelect" class="limit-select" title="Rows per page">
1392
1492
  <option value="25" ${tab.limit === 25 ? 'selected' : ''}>25 rows</option>
@@ -1437,6 +1537,13 @@ function renderTableHeader(tab, columns = [], isShimmer = false) {
1437
1537
  refreshButton.addEventListener('click', handleRefresh);
1438
1538
  }
1439
1539
 
1540
+ const schemaButton = tableHeader.querySelector('#schemaButton');
1541
+ if (schemaButton) {
1542
+ schemaButton.addEventListener('click', () => {
1543
+ showSchemaModal(tab.tableName, tab.columns);
1544
+ });
1545
+ }
1546
+
1440
1547
  const limitSelect = tableHeader.querySelector('#limitSelect');
1441
1548
  if (limitSelect) {
1442
1549
  limitSelect.addEventListener('change', (e) => {
@@ -1479,6 +1586,14 @@ function renderShimmerTable(tab) {
1479
1586
  const headerRow = document.createElement('tr');
1480
1587
 
1481
1588
  // Render header placeholders
1589
+ const rowNumTh = document.createElement('th');
1590
+ rowNumTh.className = 'row-number-header';
1591
+ const rowNumSkeleton = document.createElement('div');
1592
+ rowNumSkeleton.className = 'skeleton';
1593
+ rowNumSkeleton.style.width = '30px';
1594
+ rowNumTh.appendChild(rowNumSkeleton);
1595
+ headerRow.appendChild(rowNumTh);
1596
+
1482
1597
  columns.forEach(col => {
1483
1598
  const th = document.createElement('th');
1484
1599
  th.className = 'resizable';
@@ -1497,6 +1612,15 @@ function renderShimmerTable(tab) {
1497
1612
  for (let i = 0; i < 10; i++) {
1498
1613
  const tr = document.createElement('tr');
1499
1614
  tr.className = 'shimmer-row';
1615
+
1616
+ // Row number shimmer cell
1617
+ const rowNumTd = document.createElement('td');
1618
+ const rowNumSkeleton = document.createElement('div');
1619
+ rowNumSkeleton.className = 'skeleton shimmer-cell';
1620
+ rowNumSkeleton.style.width = '20px';
1621
+ rowNumTd.appendChild(rowNumSkeleton);
1622
+ tr.appendChild(rowNumTd);
1623
+
1500
1624
  columns.forEach(() => {
1501
1625
  const td = document.createElement('td');
1502
1626
  const skeleton = document.createElement('div');
@@ -1542,6 +1666,12 @@ function renderTable(data) {
1542
1666
  const thead = document.createElement('thead');
1543
1667
  const headerRow = document.createElement('tr');
1544
1668
 
1669
+ // Add Row Number Header
1670
+ const rowNumTh = document.createElement('th');
1671
+ rowNumTh.className = 'row-number-header';
1672
+ rowNumTh.textContent = '#';
1673
+ headerRow.appendChild(rowNumTh);
1674
+
1545
1675
  visibleColumns.forEach((column, index) => {
1546
1676
  const th = document.createElement('th');
1547
1677
  th.className = 'sortable resizable';
@@ -1663,8 +1793,16 @@ function renderTable(data) {
1663
1793
  // Server-side sorting, so rows are already sorted
1664
1794
  const rows = data.rows || [];
1665
1795
 
1666
- rows.forEach(row => {
1796
+ rows.forEach((row, rowIndex) => {
1667
1797
  const tr = document.createElement('tr');
1798
+
1799
+ // Add Row Number Cell
1800
+ const rowNumTd = document.createElement('td');
1801
+ rowNumTd.className = 'row-number-cell';
1802
+ const rowNumber = ((tab.page - 1) * tab.limit) + rowIndex + 1;
1803
+ rowNumTd.textContent = rowNumber.toLocaleString();
1804
+ tr.appendChild(rowNumTd);
1805
+
1668
1806
  visibleColumns.forEach(column => {
1669
1807
  const td = document.createElement('td');
1670
1808
 
@@ -2460,3 +2598,177 @@ function hideLoading() {
2460
2598
  loadingOverlay.style.display = 'none';
2461
2599
  }
2462
2600
  }
2601
+
2602
+ /**
2603
+ * Show the schema modal for a table.
2604
+ * @param {string} tableName - Name of the table
2605
+ * @param {Object} columns - Column metadata
2606
+ */
2607
+ function showSchemaModal(tableName, columns) {
2608
+ const title = schemaDialog.querySelector('h2');
2609
+ title.textContent = `Schema: ${tableName}`;
2610
+
2611
+ renderSchemaTable(columns);
2612
+ schemaDialog.style.display = 'flex';
2613
+ }
2614
+
2615
+ function hideSchemaDialog() {
2616
+ schemaDialog.style.display = 'none';
2617
+ }
2618
+
2619
+ /**
2620
+ * Render the schema table inside the modal.
2621
+ * @param {Object} columns - Column metadata
2622
+ */
2623
+ function renderSchemaTable(columns) {
2624
+ schemaTableContainer.innerHTML = '';
2625
+
2626
+ const table = document.createElement('table');
2627
+ table.className = 'schema-table';
2628
+
2629
+ const thead = document.createElement('thead');
2630
+ thead.innerHTML = `
2631
+ <tr>
2632
+ <th>Column</th>
2633
+ <th>Type</th>
2634
+ <th>Key</th>
2635
+ </tr>
2636
+ `;
2637
+ table.appendChild(thead);
2638
+
2639
+ const tbody = document.createElement('tbody');
2640
+
2641
+ Object.entries(columns).forEach(([name, meta]) => {
2642
+ const tr = document.createElement('tr');
2643
+
2644
+ // Key Badges
2645
+ let keyBadges = '';
2646
+ if (meta.isPrimaryKey) {
2647
+ keyBadges += '<span class="schema-badge pk">PK</span> ';
2648
+ }
2649
+ if (meta.isForeignKey) {
2650
+ keyBadges += '<span class="schema-badge fk">FK</span> ';
2651
+ }
2652
+ if (meta.isUnique && !meta.isPrimaryKey) {
2653
+ keyBadges += '<span class="schema-badge unique">UQ</span> ';
2654
+ }
2655
+
2656
+ // FK Reference
2657
+ let fkRef = '';
2658
+ if (meta.isForeignKey && meta.foreignKeyRef) {
2659
+ fkRef = `<div class="fk-ref">→ ${meta.foreignKeyRef.table}(${meta.foreignKeyRef.column})</div>`;
2660
+ }
2661
+
2662
+ tr.innerHTML = `
2663
+ <td style="font-weight: 500">${name}</td>
2664
+ <td style="color: var(--text-secondary); font-family: monospace">${meta.dataType}</td>
2665
+ <td>
2666
+ ${keyBadges}
2667
+ ${fkRef}
2668
+ </td>
2669
+ `;
2670
+
2671
+ tbody.appendChild(tr);
2672
+ });
2673
+
2674
+ table.appendChild(tbody);
2675
+ schemaTableContainer.appendChild(table);
2676
+ }
2677
+
2678
+ // Spotlight Functions
2679
+ function toggleSpotlight(show) {
2680
+ if (show === undefined) {
2681
+ show = spotlightOverlay.style.display === 'none';
2682
+ }
2683
+
2684
+ if (show) {
2685
+ spotlightOverlay.style.display = 'flex';
2686
+ spotlightInput.value = '';
2687
+ spotlightInput.focus();
2688
+ filterSpotlight('');
2689
+ } else {
2690
+ spotlightOverlay.style.display = 'none';
2691
+ }
2692
+ }
2693
+
2694
+ function filterSpotlight(query) {
2695
+ spotlightResults.innerHTML = '';
2696
+ spotlightSelectedIndex = 0;
2697
+
2698
+ if (!allTables || allTables.length === 0) {
2699
+ spotlightResults.innerHTML = '<div class="spotlight-empty">No tables available</div>';
2700
+ spotlightMatches = [];
2701
+ return;
2702
+ }
2703
+
2704
+ const lowerQuery = query.toLowerCase().trim();
2705
+ spotlightMatches = allTables.filter(t => t.toLowerCase().includes(lowerQuery));
2706
+
2707
+ if (spotlightMatches.length === 0) {
2708
+ spotlightResults.innerHTML = '<div class="spotlight-empty">No tables found</div>';
2709
+ return;
2710
+ }
2711
+
2712
+ spotlightMatches.forEach((table, index) => {
2713
+ const div = document.createElement('div');
2714
+ div.className = `spotlight-result-item ${index === 0 ? 'selected' : ''}`;
2715
+ div.dataset.index = index;
2716
+
2717
+ div.innerHTML = `
2718
+ <div class="spotlight-result-icon">
2719
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2720
+ <line x1="3" y1="6" x2="21" y2="6"></line>
2721
+ <line x1="3" y1="12" x2="21" y2="12"></line>
2722
+ <line x1="3" y1="18" x2="21" y2="18"></line>
2723
+ </svg>
2724
+ </div>
2725
+ <div class="spotlight-result-info">
2726
+ <span class="spotlight-result-name">${table}</span>
2727
+ <span class="spotlight-result-schema">public</span>
2728
+ </div>
2729
+ `;
2730
+
2731
+ div.addEventListener('mousemove', () => {
2732
+ setSpotlightSelection(index);
2733
+ });
2734
+
2735
+ div.addEventListener('click', () => {
2736
+ openSpotlightTable(table);
2737
+ });
2738
+
2739
+ spotlightResults.appendChild(div);
2740
+ });
2741
+ }
2742
+
2743
+ function navigateSpotlight(direction) {
2744
+ if (spotlightMatches.length === 0) return;
2745
+
2746
+ let newIndex = spotlightSelectedIndex + direction;
2747
+ if (newIndex < 0) newIndex = spotlightMatches.length - 1;
2748
+ if (newIndex >= spotlightMatches.length) newIndex = 0;
2749
+
2750
+ setSpotlightSelection(newIndex);
2751
+ }
2752
+
2753
+ function setSpotlightSelection(index) {
2754
+ spotlightSelectedIndex = index;
2755
+ const items = spotlightResults.querySelectorAll('.spotlight-result-item');
2756
+ items.forEach(item => item.classList.remove('selected'));
2757
+
2758
+ const selectedItem = items[index];
2759
+ if (selectedItem) {
2760
+ selectedItem.classList.add('selected');
2761
+ selectedItem.scrollIntoView({ block: 'nearest' });
2762
+ }
2763
+ }
2764
+
2765
+ function executeSpotlightSelection() {
2766
+ if (spotlightSelectedIndex >= 0 && spotlightSelectedIndex < spotlightMatches.length) {
2767
+ openSpotlightTable(spotlightMatches[spotlightSelectedIndex]);
2768
+ }
2769
+ }
2770
+
2771
+ function openSpotlightTable(tableName) {
2772
+ toggleSpotlight(false);
2773
+ handleTableSelect(tableName);
2774
+ }
package/client/index.html CHANGED
@@ -86,6 +86,42 @@
86
86
  </div>
87
87
  </div>
88
88
 
89
+ <!-- Schema Dialog -->
90
+ <div class="connection-dialog-overlay" id="schemaDialog" style="display: none;">
91
+ <div class="connection-dialog schema-dialog-content">
92
+ <div class="connection-dialog-header">
93
+ <h2>Table Schema</h2>
94
+ <button class="close-dialog-button" id="closeSchemaDialog">×</button>
95
+ </div>
96
+ <div class="connection-dialog-body schema-body">
97
+ <div class="schema-table-container" id="schemaTableContainer">
98
+ <!-- Schema table rendered here -->
99
+ </div>
100
+ </div>
101
+ <div class="connection-dialog-footer">
102
+ <button class="button-primary" id="closeSchemaButton">Close</button>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- Spotlight Search Dialog -->
108
+ <div class="spotlight-overlay" id="spotlightOverlay" style="display: none;">
109
+ <div class="spotlight-modal">
110
+ <div class="spotlight-search-container">
111
+ <svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
112
+ stroke-width="2">
113
+ <circle cx="11" cy="11" r="8"></circle>
114
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
115
+ </svg>
116
+ <input type="text" id="spotlightInput" placeholder="Search tables..." autocomplete="off">
117
+ <div class="spotlight-shortcut-hint">Cmd + K</div>
118
+ </div>
119
+ <div class="spotlight-results" id="spotlightResults">
120
+ <!-- Results will be rendered here -->
121
+ </div>
122
+ </div>
123
+ </div>
124
+
89
125
  <div class="sidebar" id="sidebar">
90
126
  <div class="sidebar-header">
91
127
  <div class="sidebar-header-top">
@@ -118,7 +154,7 @@
118
154
  <h3>Tables <span class="table-count" id="tableCount">0</span></h3>
119
155
  </div>
120
156
  <div class="sidebar-search-container">
121
- <input type="text" class="sidebar-search" id="sidebarSearch" placeholder="Search tables..." />
157
+ <input type="text" class="sidebar-search" id="sidebarSearch" placeholder="Search tables (Cmd+K)..." />
122
158
  <div class="theme-selector">
123
159
  <button class="theme-button" id="themeButton" title="Theme">
124
160
  <span class="theme-icon">🌓</span>