sqlite-hub 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +5 -5
  2. package/changelog.md +14 -0
  3. package/{js → frontend/js}/api.js +99 -5
  4. package/{js → frontend/js}/app.js +162 -11
  5. package/{js → frontend/js}/components/connectionCard.js +8 -9
  6. package/frontend/js/components/connectionLogo.js +33 -0
  7. package/{js → frontend/js}/components/dataGrid.js +3 -3
  8. package/{js → frontend/js}/components/emptyState.js +25 -11
  9. package/{js → frontend/js}/components/modal.js +57 -0
  10. package/{js → frontend/js}/components/queryEditor.js +33 -55
  11. package/frontend/js/components/queryHistoryDetail.js +263 -0
  12. package/frontend/js/components/queryHistoryPanel.js +228 -0
  13. package/{js → frontend/js}/components/queryResults.js +32 -46
  14. package/{js → frontend/js}/components/rowEditorPanel.js +73 -14
  15. package/{js → frontend/js}/components/sidebar.js +8 -3
  16. package/{js → frontend/js}/store.js +577 -21
  17. package/{js → frontend/js}/utils/format.js +23 -0
  18. package/{js → frontend/js}/views/data.js +136 -10
  19. package/{js → frontend/js}/views/editor.js +34 -10
  20. package/{js → frontend/js}/views/overview.js +15 -0
  21. package/{js → frontend/js}/views/structure.js +10 -12
  22. package/{styles → frontend/styles}/components.css +106 -0
  23. package/{styles → frontend/styles}/structure-graph.css +5 -10
  24. package/package.json +2 -2
  25. package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
  26. package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
  27. package/server/data/db_logos/.gitkeep +0 -0
  28. package/server/routes/connections.js +2 -0
  29. package/server/routes/data.js +2 -0
  30. package/server/routes/export.js +4 -1
  31. package/server/routes/overview.js +12 -0
  32. package/server/routes/sql.js +163 -5
  33. package/server/server.js +8 -6
  34. package/server/services/sqlite/connectionManager.js +68 -33
  35. package/server/services/sqlite/dataBrowserService.js +4 -16
  36. package/server/services/sqlite/exportService.js +4 -16
  37. package/server/services/sqlite/overviewService.js +34 -0
  38. package/server/services/sqlite/sqlExecutor.js +83 -63
  39. package/server/services/sqlite/tableSort.js +63 -0
  40. package/server/services/storage/appStateStore.js +832 -20
  41. package/server/services/storage/queryHistoryUtils.js +169 -0
  42. package/server/utils/appPaths.js +42 -18
  43. package/{assets → frontend/assets}/images/logo.webp +0 -0
  44. package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
  45. package/{assets → frontend/assets}/images/logo_raw.png +0 -0
  46. package/{assets → frontend/assets}/images/logo_small.webp +0 -0
  47. package/{assets → frontend/assets}/mockups/connections.png +0 -0
  48. package/{assets → frontend/assets}/mockups/data.png +0 -0
  49. package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
  50. package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
  51. package/{assets → frontend/assets}/mockups/home.png +0 -0
  52. package/{assets → frontend/assets}/mockups/overview.png +0 -0
  53. package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
  54. package/{assets → frontend/assets}/mockups/structure.png +0 -0
  55. package/{index.html → frontend/index.html} +0 -0
  56. package/{js → frontend/js}/components/actionBar.js +0 -0
  57. package/{js → frontend/js}/components/appShell.js +0 -0
  58. package/{js → frontend/js}/components/badges.js +0 -0
  59. package/{js → frontend/js}/components/bottomTabs.js +1 -1
  60. /package/{js → frontend/js}/components/metricCard.js +0 -0
  61. /package/{js → frontend/js}/components/pageHeader.js +0 -0
  62. /package/{js → frontend/js}/components/statusBar.js +0 -0
  63. /package/{js → frontend/js}/components/structureGraph.js +0 -0
  64. /package/{js → frontend/js}/components/toast.js +0 -0
  65. /package/{js → frontend/js}/components/topNav.js +0 -0
  66. /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
  67. /package/{js → frontend/js}/router.js +0 -0
  68. /package/{js → frontend/js}/views/connections.js +0 -0
  69. /package/{js → frontend/js}/views/landing.js +0 -0
  70. /package/{js → frontend/js}/views/settings.js +0 -0
  71. /package/{styles → frontend/styles}/base.css +0 -0
  72. /package/{styles → frontend/styles}/layout.css +0 -0
  73. /package/{styles → frontend/styles}/tokens.css +0 -0
  74. /package/{styles → frontend/styles}/views.css +0 -0
  75. /package/{data → server/data}/.gitkeep +0 -0
@@ -5,6 +5,10 @@ const DATE_TIME_FORMATTER = new Intl.DateTimeFormat("en-GB", {
5
5
  dateStyle: "medium",
6
6
  timeStyle: "short",
7
7
  });
8
+ const COMPACT_DATE_TIME_FORMATTER = new Intl.DateTimeFormat("en-GB", {
9
+ dateStyle: "short",
10
+ timeStyle: "short",
11
+ });
8
12
 
9
13
  export function escapeHtml(value = "") {
10
14
  return String(value)
@@ -52,6 +56,25 @@ export function formatDateTime(value) {
52
56
  return Number.isNaN(date.getTime()) ? String(value) : DATE_TIME_FORMATTER.format(date);
53
57
  }
54
58
 
59
+ export function formatCompactDateTime(value) {
60
+ if (!value) {
61
+ return "N/A";
62
+ }
63
+
64
+ const date = new Date(value);
65
+ return Number.isNaN(date.getTime()) ? String(value) : COMPACT_DATE_TIME_FORMATTER.format(date);
66
+ }
67
+
68
+ export function formatDurationMs(value) {
69
+ const numericValue = Number(value);
70
+
71
+ if (!Number.isFinite(numericValue) || numericValue < 0) {
72
+ return "N/A";
73
+ }
74
+
75
+ return `${numericValue.toLocaleString("en-US")} ms`;
76
+ }
77
+
55
78
  export function formatRelativeBoolean(value) {
56
79
  return value ? "ENABLED" : "DISABLED";
57
80
  }
@@ -168,6 +168,110 @@ function getCellWidthClass(columnName) {
168
168
  return "max-w-[12rem]";
169
169
  }
170
170
 
171
+ function getSortIcon(columnName, sortColumn, sortDirection) {
172
+ if (columnName !== sortColumn) {
173
+ return "unfold_more";
174
+ }
175
+
176
+ return sortDirection === "desc" ? "south" : "north";
177
+ }
178
+
179
+ function renderSortableHeader(columnName, sortColumn, sortDirection, action) {
180
+ const isActive = columnName === sortColumn;
181
+
182
+ return `
183
+ <button
184
+ class="flex w-full items-center justify-between gap-2 text-left transition-colors ${
185
+ isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
186
+ }"
187
+ data-action="${action}"
188
+ data-column-name="${escapeHtml(columnName)}"
189
+ type="button"
190
+ >
191
+ <span class="truncate">${escapeHtml(columnName)}</span>
192
+ <span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
193
+ columnName,
194
+ sortColumn,
195
+ sortDirection
196
+ )}</span>
197
+ </button>
198
+ `;
199
+ }
200
+
201
+ function getFilteredTableRows(table, state) {
202
+ const allRows = table?.rows ?? [];
203
+ const availableColumns = table?.columns ?? [];
204
+ const searchQuery = String(state.dataBrowser.searchQuery ?? "").trim().toLowerCase();
205
+ const activeColumn = availableColumns.includes(state.dataBrowser.searchColumn)
206
+ ? state.dataBrowser.searchColumn
207
+ : (availableColumns[0] ?? "");
208
+
209
+ const indexedRows = allRows.map((row, index) => ({
210
+ row,
211
+ index,
212
+ }));
213
+
214
+ if (!searchQuery || !activeColumn) {
215
+ return {
216
+ activeColumn,
217
+ filteredRows: indexedRows,
218
+ searchQuery,
219
+ };
220
+ }
221
+
222
+ return {
223
+ activeColumn,
224
+ searchQuery,
225
+ filteredRows: indexedRows.filter(({ row }) =>
226
+ formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery)
227
+ ),
228
+ };
229
+ }
230
+
231
+ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
232
+ const columns = table?.columns ?? [];
233
+
234
+ if (!table || !columns.length) {
235
+ return "";
236
+ }
237
+
238
+ return `
239
+ <div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">
240
+ <label class="flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-4 py-3">
241
+ <span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>
242
+ <input
243
+ class="min-w-0 flex-1 bg-transparent text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40"
244
+ data-bind="data-search-query"
245
+ placeholder="Filter current page..."
246
+ type="search"
247
+ value="${escapeHtml(state.dataBrowser.searchQuery ?? "")}"
248
+ />
249
+ </label>
250
+ <select
251
+ class="min-w-[14rem] border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 font-mono text-xs tracking-[0.04em] text-on-surface outline-none"
252
+ data-bind="data-search-column"
253
+ >
254
+ ${columns
255
+ .map(
256
+ (columnName) => `
257
+ <option value="${escapeHtml(columnName)}" ${
258
+ columnName === activeColumn ? "selected" : ""
259
+ }>
260
+ ${escapeHtml(columnName)}
261
+ </option>
262
+ `
263
+ )
264
+ .join("")}
265
+ </select>
266
+ <div class="text-[10px] font-mono tracking-[0.14em] text-on-surface-variant/55">
267
+ ${escapeHtml(formatNumber(filteredRowCount))} match${
268
+ filteredRowCount === 1 ? "" : "es"
269
+ } on this page
270
+ </div>
271
+ </div>
272
+ `;
273
+ }
274
+
171
275
  function renderTableSurface(state) {
172
276
  const table = state.dataBrowser.table;
173
277
 
@@ -197,10 +301,14 @@ function renderTableSurface(state) {
197
301
  `;
198
302
  }
199
303
 
304
+ const { activeColumn, filteredRows, searchQuery } = getFilteredTableRows(table, state);
305
+ const sortColumn = state.dataBrowser.sortColumn;
306
+ const sortDirection = state.dataBrowser.sortDirection;
200
307
  const columns = (table.columns ?? []).map((columnName) => ({
201
- label: escapeHtml(columnName),
202
308
  headerClassName:
203
- "border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-primary-container",
309
+ "border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold tracking-[0.08em] text-primary-container",
310
+ renderHeader: () =>
311
+ renderSortableHeader(columnName, sortColumn, sortDirection, "sort-data-column"),
204
312
  cellClassName: "px-4 py-2 align-top text-[11px] text-on-surface",
205
313
  render: (row) => {
206
314
  const value = formatCellValue(row[columnName]);
@@ -219,25 +327,35 @@ function renderTableSurface(state) {
219
327
  const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
220
328
  const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
221
329
  const pageSizes = [25, 50, 100];
330
+ const filteredRowCount = filteredRows.length;
331
+ const hasActiveSearch = Boolean(searchQuery);
222
332
 
223
333
  return `
224
334
  <div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">
335
+ ${renderTableSearchBar(table, state, activeColumn, filteredRowCount)}
225
336
  <div class="custom-scrollbar flex-1 overflow-auto">
226
337
  ${renderDataGrid({
227
338
  columns,
228
- rows: table.rows ?? [],
339
+ rows: filteredRows.map(({ row }) => row),
229
340
  tableClass: "min-w-full border-collapse text-left font-mono text-xs",
230
341
  theadClass: "sticky top-0 z-10 bg-surface-container-highest",
231
342
  tbodyClass: "divide-y divide-outline-variant/5",
232
- getRowClass: (_, index) =>
233
- `${
234
- state.dataBrowser.selectedRowIndex === index
343
+ getRowClass: (_, filteredIndex) => {
344
+ const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
345
+
346
+ return `${
347
+ state.dataBrowser.selectedRowIndex === rowIndex
235
348
  ? "bg-surface-bright"
236
- : index % 2 === 0
349
+ : filteredIndex % 2 === 0
237
350
  ? "bg-surface-container-low"
238
351
  : "bg-surface-container-lowest"
239
- } cursor-pointer transition-colors hover:bg-surface-container-high`,
240
- getRowAttrs: (_, index) => `data-action="select-data-row" data-row-index="${index}"`,
352
+ } cursor-pointer transition-colors hover:bg-surface-container-high`;
353
+ },
354
+ getRowAttrs: (_, filteredIndex) => {
355
+ const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
356
+
357
+ return `data-action="select-data-row" data-row-index="${rowIndex}"`;
358
+ },
241
359
  })}
242
360
  ${
243
361
  !table.rows?.length
@@ -248,6 +366,14 @@ function renderTableSurface(state) {
248
366
  </p>
249
367
  </div>
250
368
  `
369
+ : !filteredRowCount
370
+ ? `
371
+ <div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
372
+ <p class="font-mono text-[10px] tracking-[0.18em] text-on-surface-variant/40">
373
+ ${hasActiveSearch ? "No matching rows on this page." : "No rows available."}
374
+ </p>
375
+ </div>
376
+ `
251
377
  : ""
252
378
  }
253
379
  </div>
@@ -255,7 +381,7 @@ function renderTableSurface(state) {
255
381
  <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
256
382
  showing ${escapeHtml(formatNumber(fromRow))}-${escapeHtml(formatNumber(toRow))} of ${escapeHtml(
257
383
  formatNumber(totalRows)
258
- )} rows
384
+ )} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` : ""}
259
385
  </div>
260
386
  <div class="flex flex-wrap items-center gap-4">
261
387
  <div class="flex items-center gap-2">
@@ -1,5 +1,7 @@
1
1
  import { renderBottomTabs } from "../components/bottomTabs.js";
2
2
  import { renderQueryEditor } from "../components/queryEditor.js";
3
+ import { renderQueryHistoryDetail } from "../components/queryHistoryDetail.js";
4
+ import { renderQueryHistoryPanel } from "../components/queryHistoryPanel.js";
3
5
  import { renderRowEditorPanel } from "../components/rowEditorPanel.js";
4
6
  import { renderQueryResultsPane } from "../components/queryResults.js";
5
7
  import { getCurrentConnection, getQueryMessages, getQueryPerformance } from "../store.js";
@@ -216,17 +218,17 @@ function renderResultsSurface(state, isResultsRoute) {
216
218
 
217
219
  content = state.connections.active
218
220
  ? renderQueryResultsPane(state.editor.result, {
219
- exporting: state.editor.exportLoading,
220
221
  selectedRowIndex: state.editor.selectedRowIndex,
221
222
  editable: editingState.enabled,
222
- editStatusMessage: editingState.message,
223
+ sortColumn: state.editor.resultSortColumn,
224
+ sortDirection: state.editor.resultSortDirection,
223
225
  })
224
226
  : renderMissingDatabase();
225
227
  }
226
228
 
227
229
  return `
228
230
  <div class="flex h-full min-h-0 flex-col border-t border-outline-variant/10 bg-surface-container-lowest">
229
- ${renderBottomTabs(activeTab, counts)}
231
+ ${renderBottomTabs(state.editor.activeTab, counts)}
230
232
  <div class="min-h-0 flex-1">${content}</div>
231
233
  </div>
232
234
  `;
@@ -234,13 +236,13 @@ function renderResultsSurface(state, isResultsRoute) {
234
236
 
235
237
  export function renderEditorView(state, { isResultsRoute = false } = {}) {
236
238
  const connection = getCurrentConnection(state);
237
- const editorSectionClass = isResultsRoute ? "h-1/4" : "min-h-[27.5%]";
238
- const resultsSectionClass = isResultsRoute ? "h-3/4" : "flex-1";
239
+ const editorSectionClass = "min-h-[27.5%]";
240
+ const resultsSectionClass = "flex-1";
239
241
 
240
242
  return {
241
243
  main: `
242
- <section class="view-surface flex h-full min-h-0 flex-col overflow-hidden">
243
- <div class="flex h-full min-h-0 flex-1 flex-col">
244
+ <section class="view-surface flex h-full min-h-0 flex-col overflow-hidden xl:flex-row">
245
+ <div class="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
244
246
  <section class="${editorSectionClass} flex min-h-0 flex-col ${
245
247
  isResultsRoute ? "border-b-4 border-background" : ""
246
248
  }">
@@ -248,17 +250,39 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
248
250
  query: state.editor.sqlText,
249
251
  executing: state.editor.executing,
250
252
  exporting: state.editor.exportLoading,
251
- history: state.editor.history,
252
253
  historyLoading: state.editor.historyLoading,
254
+ historyTotal: state.editor.historyTotal,
253
255
  title: connection?.label ?? "SQLite Query Workspace",
254
256
  })}
255
257
  </section>
256
- <section class="${resultsSectionClass} flex min-h-0 flex-col overflow-hidden">
258
+ <section class="${resultsSectionClass} flex min-h-0 min-w-0 flex-col overflow-hidden">
257
259
  ${renderResultsSurface(state, isResultsRoute)}
258
260
  </section>
259
261
  </div>
262
+ ${renderQueryHistoryPanel({
263
+ items: state.editor.history,
264
+ loading: state.editor.historyLoading,
265
+ loadingMore: state.editor.historyLoadingMore,
266
+ error: state.editor.historyError,
267
+ activeTab: state.editor.historyTab,
268
+ search: state.editor.historySearchInput,
269
+ total: state.editor.historyTotal,
270
+ hasMore: state.editor.historyHasMore,
271
+ activeHistoryId: state.editor.historyActiveId,
272
+ selectedHistoryId: state.editor.historySelectedId,
273
+ })}
260
274
  </section>
261
275
  `,
262
- panel: isResultsRoute ? renderEditorRowPanel(state) : "",
276
+ panel:
277
+ isResultsRoute && state.editor.selectedRowIndex !== null
278
+ ? renderEditorRowPanel(state)
279
+ : state.editor.historySelectedId
280
+ ? renderQueryHistoryDetail({
281
+ item: state.editor.historyDetail,
282
+ runs: state.editor.historyRuns,
283
+ loading: state.editor.historyDetailLoading,
284
+ error: state.editor.historyDetailError,
285
+ })
286
+ : "",
263
287
  };
264
288
  }
@@ -126,6 +126,21 @@ function renderOperationalSurface(overview) {
126
126
  )
127
127
  .join("")}
128
128
  </div>
129
+ ${
130
+ overview.file?.path
131
+ ? `
132
+ <div class="border-t border-outline-variant/10 px-4 py-3">
133
+ <button
134
+ class="toolbar-button border border-outline-variant/20 bg-surface-container px-3 py-1.5 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
135
+ data-action="open-overview-in-finder"
136
+ type="button"
137
+ >
138
+ Open In Finder
139
+ </button>
140
+ </div>
141
+ `
142
+ : ""
143
+ }
129
144
  </section>
130
145
  `;
131
146
  }
@@ -6,8 +6,8 @@ function renderEntryGroup(title, entries, activeName, options = {}) {
6
6
  const { compact = false, showMeta = true } = options;
7
7
 
8
8
  return `
9
- <section class="shell-section p-5">
10
- <div class="mb-4 text-[10px] font-bold uppercase tracking-[0.25em] text-primary-container">
9
+ <section class="shell-section flex flex-col p-5">
10
+ <div class="mb-4 shrink-0 text-[10px] font-bold uppercase tracking-[0.25em] text-primary-container">
11
11
  ${escapeHtml(title)}
12
12
  </div>
13
13
  ${
@@ -209,10 +209,6 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading) {
209
209
  type="search"
210
210
  />
211
211
  </label>
212
- <div class="structure-graph__toolbar-meta">
213
- ${escapeHtml(formatNumber(graph.tables?.length ?? 0))} TABLES //
214
- ${escapeHtml(formatNumber(graph.relationshipCount ?? 0))} RELATIONSHIPS
215
- </div>
216
212
  </div>
217
213
  <div class="structure-graph__toolbar-actions">
218
214
  <button
@@ -293,8 +289,8 @@ export function renderStructureView(state) {
293
289
 
294
290
  return {
295
291
  main: `
296
- <section class="view-surface min-h-full bg-surface-container">
297
- <div class="view-frame space-y-8">
292
+ <section class="view-surface flex h-full min-h-0 flex-col bg-surface-container">
293
+ <div class="view-frame flex h-full min-h-0 flex-col">
298
294
  ${renderPageHeader({
299
295
  title: "Structure",
300
296
  subtitle: "Schema graph, foreign-key paths, raw DDL, and object metadata",
@@ -303,7 +299,7 @@ export function renderStructureView(state) {
303
299
  ${
304
300
  state.structure.loading && !structure
305
301
  ? `
306
- <div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
302
+ <div class="flex min-h-0 flex-1 items-center justify-center border border-outline-variant/10 bg-surface-container-low">
307
303
  <div class="text-center text-on-surface-variant/40">
308
304
  <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
309
305
  <p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_STRUCTURE</p>
@@ -312,7 +308,7 @@ export function renderStructureView(state) {
312
308
  `
313
309
  : state.structure.error
314
310
  ? `
315
- <div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
311
+ <div class="min-h-0 flex-1 border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
316
312
  <div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
317
313
  ${escapeHtml(state.structure.error.code)}
318
314
  </div>
@@ -321,8 +317,9 @@ export function renderStructureView(state) {
321
317
  `
322
318
  : structure
323
319
  ? `
324
- <section class="grid grid-cols-1 gap-6 xl:grid-cols-[18.5rem_minmax(0,1fr)] 2xl:grid-cols-[19.5rem_minmax(0,1fr)]">
325
- <div class="space-y-6">
320
+ <section class="grid min-h-0 flex-1 grid-cols-1 gap-6 xl:grid-cols-[18.5rem_minmax(0,1fr)] 2xl:grid-cols-[19.5rem_minmax(0,1fr)]">
321
+ <div class="custom-scrollbar min-h-0 overflow-y-auto pr-1">
322
+ <div class="space-y-6">
326
323
  ${renderEntryGroup(
327
324
  "Tables",
328
325
  structure.grouped.tables,
@@ -345,6 +342,7 @@ export function renderStructureView(state) {
345
342
  structure.grouped.triggers,
346
343
  state.structure.selectedName
347
344
  )}
345
+ </div>
348
346
  </div>
349
347
  ${renderGraphSurface(
350
348
  structure,
@@ -316,6 +316,112 @@
316
316
  padding: var(--spacing-3) var(--spacing-4);
317
317
  }
318
318
 
319
+ .query-history-panel {
320
+ display: flex;
321
+ flex: 0 0 360px;
322
+ flex-direction: column;
323
+ min-height: 0;
324
+ width: 360px;
325
+ }
326
+
327
+ .query-history-item {
328
+ align-items: stretch;
329
+ background: var(--color-surface-low);
330
+ border: 1px solid rgba(75, 71, 50, 0.16);
331
+ border-left: 2px solid transparent;
332
+ display: flex;
333
+ flex-direction: column;
334
+ min-width: 0;
335
+ transition: background-color var(--transition-fast), border-color var(--transition-fast),
336
+ box-shadow var(--transition-fast);
337
+ }
338
+
339
+ .query-history-item:hover {
340
+ background: var(--color-surface-high);
341
+ border-color: rgba(252, 227, 0, 0.18);
342
+ }
343
+
344
+ .query-history-item.is-active {
345
+ border-left-color: var(--color-primary-container);
346
+ box-shadow: 0 0 24px rgba(252, 227, 0, 0.06);
347
+ }
348
+
349
+ .query-history-item.is-error {
350
+ border-left-color: var(--color-error);
351
+ }
352
+
353
+ .query-history-item-hit {
354
+ background: transparent;
355
+ border: 0;
356
+ color: inherit;
357
+ display: block;
358
+ min-width: 0;
359
+ padding: var(--spacing-3);
360
+ text-align: left;
361
+ transition: background-color var(--transition-fast);
362
+ }
363
+
364
+ .query-history-item-hit.is-active {
365
+ background: rgba(252, 227, 0, 0.08);
366
+ }
367
+
368
+ .query-history-icon-button {
369
+ align-items: center;
370
+ background: transparent;
371
+ border: 0;
372
+ color: rgba(229, 226, 225, 0.6);
373
+ display: inline-flex;
374
+ height: 2rem;
375
+ justify-content: center;
376
+ transition: color var(--transition-fast), background-color var(--transition-fast);
377
+ width: 2rem;
378
+ }
379
+
380
+ .query-history-icon-button:hover,
381
+ .query-history-icon-button.is-active {
382
+ background: var(--color-surface-highest);
383
+ color: var(--color-primary-container);
384
+ }
385
+
386
+ .query-history-tab {
387
+ background: transparent;
388
+ border: 0;
389
+ border-bottom: 1px solid transparent;
390
+ color: rgba(205, 199, 171, 0.45);
391
+ font-family: var(--font-family-mono);
392
+ font-size: var(--font-size-status);
393
+ letter-spacing: 0.16em;
394
+ padding: 0.35rem 0;
395
+ text-transform: uppercase;
396
+ transition: border-color var(--transition-fast), color var(--transition-fast);
397
+ }
398
+
399
+ .query-history-tab.is-active {
400
+ border-color: var(--color-primary-container);
401
+ color: var(--color-primary-container);
402
+ }
403
+
404
+ .query-history-sql-preview {
405
+ display: -webkit-box;
406
+ -webkit-box-orient: vertical;
407
+ -webkit-line-clamp: 2;
408
+ overflow: hidden;
409
+ }
410
+
411
+ .query-history-detail-sql {
412
+ max-height: 240px;
413
+ white-space: pre-wrap;
414
+ }
415
+
416
+ @media (max-width: 1279px) {
417
+ .query-history-panel {
418
+ border-left: 0;
419
+ border-top: 1px solid rgba(75, 71, 50, 0.16);
420
+ flex: 0 0 auto;
421
+ width: 100%;
422
+ }
423
+ }
424
+
319
425
  @media (min-width: 768px) {
320
426
  .top-nav-links {
321
427
  display: flex;
@@ -3,7 +3,8 @@
3
3
  border: 1px solid rgba(138, 123, 52, 0.26);
4
4
  display: flex;
5
5
  flex-direction: column;
6
- min-height: 760px;
6
+ height: 100%;
7
+ min-height: 0;
7
8
  overflow: hidden;
8
9
  }
9
10
 
@@ -100,14 +101,6 @@
100
101
  appearance: none;
101
102
  }
102
103
 
103
- .structure-graph__toolbar-meta {
104
- color: rgba(231, 223, 189, 0.64);
105
- font-family: var(--font-family-mono);
106
- font-size: 0.625rem;
107
- letter-spacing: 0.18em;
108
- text-transform: uppercase;
109
- }
110
-
111
104
  .structure-graph__button {
112
105
  align-items: center;
113
106
  background: rgba(17, 17, 15, 0.94);
@@ -193,8 +186,10 @@
193
186
  .structure-graph__inspector {
194
187
  background: linear-gradient(to bottom, rgba(26, 25, 23, 0.98), rgba(11, 11, 10, 0.99));
195
188
  border-left: 1px solid rgba(138, 123, 52, 0.24);
189
+ height: 100%;
196
190
  min-height: 0;
197
- overflow: auto;
191
+ overflow-y: auto;
192
+ overflow-x: hidden;
198
193
  }
199
194
 
200
195
  .structure-graph.is-inspector-hidden .structure-graph__inspector {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-hub",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "main": "server/server.js",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "start": "node bin/sqlite-hub.js",
11
11
  "dev": "node --watch server/server.js --port:4180",
12
- "publish": "bash publish_brew.sh && bash publish_npm.sh"
12
+ "publish": "bash scripts/publish_brew.sh && sleep 10 && npm login --auth-type=web && bash scripts/publish_npm.sh"
13
13
  },
14
14
  "dependencies": {
15
15
  "better-sqlite3": "^11.8.1",
@@ -2,7 +2,7 @@
2
2
 
3
3
  set -euo pipefail
4
4
 
5
- ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
6
  cd "$ROOT_DIR"
7
7
 
8
8
  DRY_RUN=0
@@ -17,7 +17,7 @@ GH_REMOTE="origin"
17
17
  usage() {
18
18
  cat <<'EOF'
19
19
  Usage:
20
- ./publish.sh [options]
20
+ ./scripts/publish_brew.sh [options]
21
21
 
22
22
  Options:
23
23
  --version X.Y.Z Override the package.json version for this publish run.
@@ -2,7 +2,7 @@
2
2
 
3
3
  set -euo pipefail
4
4
 
5
- ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
6
  cd "$ROOT_DIR"
7
7
 
8
8
  DRY_RUN=0
@@ -15,7 +15,7 @@ GH_REMOTE="origin"
15
15
  usage() {
16
16
  cat <<'EOF'
17
17
  Usage:
18
- ./publish_npm.sh [options]
18
+ ./scripts/publish_npm.sh [options]
19
19
 
20
20
  Options:
21
21
  --tag NAME Publish to a custom npm dist-tag instead of `latest`.
File without changes
@@ -113,6 +113,8 @@ function createConnectionsRouter({ connectionManager, importService, backupServi
113
113
  filePath: req.body.path,
114
114
  label: req.body.label,
115
115
  readOnly: Boolean(req.body.readOnly),
116
+ logoUpload: req.body.logoUpload ?? null,
117
+ clearLogo: Boolean(req.body.clearLogo),
116
118
  });
117
119
 
118
120
  res.json(
@@ -26,6 +26,8 @@ function createDataRouter({ dataBrowserService }) {
26
26
  const data = dataBrowserService.getTableData(req.params.tableName, {
27
27
  limit: req.query.limit,
28
28
  offset: req.query.offset,
29
+ sortColumn: req.query.sortColumn,
30
+ sortDirection: req.query.sortDirection,
29
31
  });
30
32
 
31
33
  res.json(
@@ -24,7 +24,10 @@ function createExportRouter({ exportService }) {
24
24
  router.post(
25
25
  "/table.csv",
26
26
  route((req, res) => {
27
- const result = exportService.exportTable(req.body?.tableName);
27
+ const result = exportService.exportTable(req.body?.tableName, {
28
+ sortColumn: req.body?.sortColumn,
29
+ sortDirection: req.body?.sortDirection,
30
+ });
28
31
  sendCsv(res, result);
29
32
  })
30
33
  );
@@ -31,6 +31,18 @@ function createOverviewRouter({ overviewService }) {
31
31
  })
32
32
  );
33
33
 
34
+ router.post(
35
+ "/overview/open-in-finder",
36
+ route(async (req, res) => {
37
+ await overviewService.revealActiveDatabaseInFinder();
38
+ res.json(
39
+ successResponse({
40
+ message: "Database file revealed in Finder.",
41
+ })
42
+ );
43
+ })
44
+ );
45
+
34
46
  return router;
35
47
  }
36
48