sqlite-hub 0.3.1 → 0.4.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 (65) hide show
  1. package/README.md +12 -6
  2. package/changelog.md +12 -0
  3. package/{js → frontend/js}/api.js +7 -0
  4. package/{js → frontend/js}/app.js +87 -3
  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/emptyState.js +25 -11
  8. package/{js → frontend/js}/components/modal.js +131 -0
  9. package/{js → frontend/js}/components/rowEditorPanel.js +63 -33
  10. package/{js → frontend/js}/components/sidebar.js +8 -3
  11. package/{js → frontend/js}/store.js +236 -1
  12. package/{js → frontend/js}/views/data.js +106 -9
  13. package/{js → frontend/js}/views/editor.js +4 -0
  14. package/{js → frontend/js}/views/structure.js +10 -12
  15. package/{styles → frontend/styles}/structure-graph.css +5 -10
  16. package/package.json +3 -3
  17. package/{publish_brew.sh → scripts/publish_brew.sh} +2 -2
  18. package/{publish_npm.sh → scripts/publish_npm.sh} +2 -2
  19. package/server/data/db_logos/.gitkeep +0 -0
  20. package/server/routes/connections.js +2 -0
  21. package/server/routes/data.js +14 -0
  22. package/server/server.js +29 -6
  23. package/server/services/sqlite/connectionManager.js +68 -33
  24. package/server/services/sqlite/dataBrowserService.js +30 -0
  25. package/server/services/storage/appStateStore.js +159 -20
  26. package/server/utils/appPaths.js +42 -18
  27. /package/{assets → frontend/assets}/images/logo.webp +0 -0
  28. /package/{assets → frontend/assets}/images/logo_extrasmall.webp +0 -0
  29. /package/{assets → frontend/assets}/images/logo_raw.png +0 -0
  30. /package/{assets → frontend/assets}/images/logo_small.webp +0 -0
  31. /package/{assets → frontend/assets}/mockups/connections.png +0 -0
  32. /package/{assets → frontend/assets}/mockups/data.png +0 -0
  33. /package/{assets → frontend/assets}/mockups/data_edit.png +0 -0
  34. /package/{assets → frontend/assets}/mockups/graph_visualize.png +0 -0
  35. /package/{assets → frontend/assets}/mockups/home.png +0 -0
  36. /package/{assets → frontend/assets}/mockups/overview.png +0 -0
  37. /package/{assets → frontend/assets}/mockups/sql_editor.png +0 -0
  38. /package/{assets → frontend/assets}/mockups/structure.png +0 -0
  39. /package/{index.html → frontend/index.html} +0 -0
  40. /package/{js → frontend/js}/components/actionBar.js +0 -0
  41. /package/{js → frontend/js}/components/appShell.js +0 -0
  42. /package/{js → frontend/js}/components/badges.js +0 -0
  43. /package/{js → frontend/js}/components/bottomTabs.js +0 -0
  44. /package/{js → frontend/js}/components/dataGrid.js +0 -0
  45. /package/{js → frontend/js}/components/metricCard.js +0 -0
  46. /package/{js → frontend/js}/components/pageHeader.js +0 -0
  47. /package/{js → frontend/js}/components/queryEditor.js +0 -0
  48. /package/{js → frontend/js}/components/queryResults.js +0 -0
  49. /package/{js → frontend/js}/components/statusBar.js +0 -0
  50. /package/{js → frontend/js}/components/structureGraph.js +0 -0
  51. /package/{js → frontend/js}/components/toast.js +0 -0
  52. /package/{js → frontend/js}/components/topNav.js +0 -0
  53. /package/{js → frontend/js}/lib/cytoscapeRuntime.js +0 -0
  54. /package/{js → frontend/js}/router.js +0 -0
  55. /package/{js → frontend/js}/utils/format.js +0 -0
  56. /package/{js → frontend/js}/views/connections.js +0 -0
  57. /package/{js → frontend/js}/views/landing.js +0 -0
  58. /package/{js → frontend/js}/views/overview.js +0 -0
  59. /package/{js → frontend/js}/views/settings.js +0 -0
  60. /package/{styles → frontend/styles}/base.css +0 -0
  61. /package/{styles → frontend/styles}/components.css +0 -0
  62. /package/{styles → frontend/styles}/layout.css +0 -0
  63. /package/{styles → frontend/styles}/tokens.css +0 -0
  64. /package/{styles → frontend/styles}/views.css +0 -0
  65. /package/{data → server/data}/.gitkeep +0 -0
@@ -1,4 +1,5 @@
1
1
  import { escapeHtml } from "../utils/format.js";
2
+ import { renderConnectionLogo } from "./connectionLogo.js";
2
3
 
3
4
  const sidebarItems = [
4
5
  { label: "Connections", href: "#/connections", key: "connections", icon: "database" },
@@ -40,9 +41,13 @@ export function renderSidebar(state) {
40
41
  </nav>
41
42
  <div class="sidebar-footer">
42
43
  <div class="sidebar-footer-card">
43
- <div class="sidebar-footer-mark">
44
- <span class="material-symbols-outlined text-[15px]">memory</span>
45
- </div>
44
+ ${renderConnectionLogo(activeConnection, {
45
+ containerClass:
46
+ "sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary",
47
+ imageClassName: "h-full w-full object-cover",
48
+ iconClassName: "text-[15px]",
49
+ icon: "memory",
50
+ })}
46
51
  <div class="min-w-0">
47
52
  <p class="truncate text-[10px] font-bold text-on-surface">
48
53
  ${escapeHtml(activeConnection?.label ?? "NO_ACTIVE_DATABASE")}
@@ -1,5 +1,5 @@
1
1
  import * as api from "./api.js";
2
- import { inferStatusTone } from "./utils/format.js";
2
+ import { formatCellValue, inferStatusTone, truncateMiddle } from "./utils/format.js";
3
3
 
4
4
  const listeners = new Set();
5
5
  const DEFAULT_SETTINGS = {
@@ -45,8 +45,11 @@ const state = {
45
45
  loading: false,
46
46
  tableLoading: false,
47
47
  saving: false,
48
+ deleting: false,
48
49
  page: 1,
49
50
  pageSize: 50,
51
+ searchQuery: "",
52
+ searchColumn: "",
50
53
  selectedRowIndex: null,
51
54
  exportLoading: false,
52
55
  error: null,
@@ -64,6 +67,7 @@ const state = {
64
67
  exportLoading: false,
65
68
  selectedRowIndex: null,
66
69
  saving: false,
70
+ deleting: false,
67
71
  saveError: null,
68
72
  },
69
73
  structure: {
@@ -116,6 +120,11 @@ function canEditQueryResult(snapshot = state) {
116
120
  return Boolean(snapshot.editor.result?.editing?.enabled) && !snapshot.connections.active?.readOnly;
117
121
  }
118
122
 
123
+ function resetDataBrowserSearch() {
124
+ state.dataBrowser.searchQuery = "";
125
+ state.dataBrowser.searchColumn = "";
126
+ }
127
+
119
128
  function buildUpdatedEditorResultRow(existingRow, updatedSourceRow, editableColumns) {
120
129
  const nextRow = {
121
130
  ...existingRow,
@@ -140,6 +149,32 @@ function getCurrentStructureEntry(snapshot = state) {
140
149
  return entries.find((entry) => entry.name === snapshot.structure.selectedName) ?? null;
141
150
  }
142
151
 
152
+ function buildDeleteRowPreview(fields = []) {
153
+ return fields
154
+ .filter((field) => field && field.label)
155
+ .slice(0, 8)
156
+ .map((field) => {
157
+ const fullValue = formatCellValue(field.value);
158
+
159
+ return {
160
+ label: String(field.label),
161
+ value: truncateMiddle(fullValue, 96),
162
+ fullValue,
163
+ };
164
+ });
165
+ }
166
+
167
+ function buildFallbackDeleteRowPreview(row) {
168
+ return buildDeleteRowPreview(
169
+ Object.entries(row ?? {})
170
+ .filter(([key]) => key !== "__identity")
171
+ .map(([key, value]) => ({
172
+ label: key,
173
+ value,
174
+ }))
175
+ );
176
+ }
177
+
143
178
  function clearRouteSlices() {
144
179
  state.overview.error = null;
145
180
  state.dataBrowser.error = null;
@@ -160,6 +195,7 @@ function setMissingDatabaseState() {
160
195
  state.dataBrowser.selectedTable = null;
161
196
  state.dataBrowser.table = null;
162
197
  state.dataBrowser.page = 1;
198
+ resetDataBrowserSearch();
163
199
  state.dataBrowser.selectedRowIndex = null;
164
200
  state.dataBrowser.exportLoading = false;
165
201
  state.dataBrowser.error = error;
@@ -193,6 +229,9 @@ function syncRouteContext() {
193
229
  if (route.name !== "data" || route.params?.tableName !== state.dataBrowser.selectedTable) {
194
230
  state.dataBrowser.page = 1;
195
231
  }
232
+ if (route.params?.tableName !== state.dataBrowser.selectedTable) {
233
+ resetDataBrowserSearch();
234
+ }
196
235
  state.dataBrowser.selectedRowIndex = null;
197
236
  state.dataBrowser.saveError = null;
198
237
  }
@@ -318,6 +357,9 @@ async function loadDataTable(version) {
318
357
  state.dataBrowser.table = response.data ?? null;
319
358
  state.dataBrowser.pageSize = pageSize;
320
359
  state.dataBrowser.page = response.data?.page ?? page;
360
+ state.dataBrowser.searchColumn = response.data?.columns?.includes(state.dataBrowser.searchColumn)
361
+ ? state.dataBrowser.searchColumn
362
+ : (response.data?.columns?.[0] ?? "");
321
363
  state.dataBrowser.selectedRowIndex = null;
322
364
  } catch (error) {
323
365
  if (version !== routeLoadVersion) {
@@ -355,6 +397,7 @@ async function loadData(version, route) {
355
397
  if (requestedTableName && tables.some((table) => table.name === requestedTableName)) {
356
398
  if (requestedTableName !== state.dataBrowser.selectedTable) {
357
399
  state.dataBrowser.page = 1;
400
+ resetDataBrowserSearch();
358
401
  }
359
402
  state.dataBrowser.selectedTable = requestedTableName;
360
403
  } else if (
@@ -363,10 +406,12 @@ async function loadData(version, route) {
363
406
  ) {
364
407
  state.dataBrowser.selectedTable = tables[0]?.name ?? null;
365
408
  state.dataBrowser.page = 1;
409
+ resetDataBrowserSearch();
366
410
  }
367
411
 
368
412
  if (!state.dataBrowser.selectedTable) {
369
413
  state.dataBrowser.table = null;
414
+ resetDataBrowserSearch();
370
415
  state.dataBrowser.selectedRowIndex = null;
371
416
  return;
372
417
  }
@@ -482,6 +527,7 @@ function invalidateDatabaseCaches() {
482
527
  state.dataBrowser.selectedTable = null;
483
528
  state.dataBrowser.table = null;
484
529
  state.dataBrowser.page = 1;
530
+ resetDataBrowserSearch();
485
531
  state.dataBrowser.selectedRowIndex = null;
486
532
  state.dataBrowser.exportLoading = false;
487
533
  state.dataBrowser.error = null;
@@ -621,6 +667,69 @@ export function openEditConnectionModal(id) {
621
667
  emitChange();
622
668
  }
623
669
 
670
+ export function openDeleteDataRowModal(rowIndex) {
671
+ const numericIndex = Number(rowIndex);
672
+ const tableName = state.dataBrowser.selectedTable;
673
+ const row = state.dataBrowser.table?.rows?.[numericIndex];
674
+ const rowPreview = buildDeleteRowPreview(
675
+ (state.dataBrowser.table?.columnMeta ?? [])
676
+ .filter((column) => column.visible)
677
+ .map((column) => ({
678
+ label: column.name,
679
+ value: row[column.name],
680
+ }))
681
+ );
682
+
683
+ if (!tableName || !row?.__identity) {
684
+ pushToast("The selected row could not be loaded.", "alert");
685
+ return;
686
+ }
687
+
688
+ state.modal = {
689
+ kind: "delete-row",
690
+ target: "data",
691
+ rowIndex: numericIndex,
692
+ tableName,
693
+ rowLabel: `row ${numericIndex + 1}`,
694
+ rowPreview: rowPreview.length ? rowPreview : buildFallbackDeleteRowPreview(row),
695
+ error: null,
696
+ submitting: false,
697
+ };
698
+ emitChange();
699
+ }
700
+
701
+ export function openDeleteEditorRowModal(rowIndex) {
702
+ const numericIndex = Number(rowIndex);
703
+ const tableName = state.editor.result?.editing?.tableName ?? null;
704
+ const row = state.editor.result?.rows?.[numericIndex];
705
+ const columns = state.editor.result?.editing?.columns ?? [];
706
+ const rowPreview = buildDeleteRowPreview(
707
+ columns
708
+ .filter((column) => column.visible !== false)
709
+ .map((column) => ({
710
+ label: column.sourceColumn || column.resultName,
711
+ value: row[column.resultName],
712
+ }))
713
+ );
714
+
715
+ if (!tableName || !row?.__identity || !canEditQueryResult()) {
716
+ pushToast("The selected query result row could not be loaded.", "alert");
717
+ return;
718
+ }
719
+
720
+ state.modal = {
721
+ kind: "delete-row",
722
+ target: "editor",
723
+ rowIndex: numericIndex,
724
+ tableName,
725
+ rowLabel: `query row ${numericIndex + 1}`,
726
+ rowPreview: rowPreview.length ? rowPreview : buildFallbackDeleteRowPreview(row),
727
+ error: null,
728
+ submitting: false,
729
+ };
730
+ emitChange();
731
+ }
732
+
624
733
  export function closeModal() {
625
734
  closeModalInternal();
626
735
  }
@@ -919,6 +1028,20 @@ export function clearDataRowSelection() {
919
1028
  emitChange();
920
1029
  }
921
1030
 
1031
+ export function setDataSearchQuery(query) {
1032
+ state.dataBrowser.searchQuery = String(query ?? "");
1033
+ state.dataBrowser.selectedRowIndex = null;
1034
+ state.dataBrowser.saveError = null;
1035
+ emitChange();
1036
+ }
1037
+
1038
+ export function setDataSearchColumn(columnName) {
1039
+ state.dataBrowser.searchColumn = String(columnName ?? "");
1040
+ state.dataBrowser.selectedRowIndex = null;
1041
+ state.dataBrowser.saveError = null;
1042
+ emitChange();
1043
+ }
1044
+
922
1045
  export async function setDataPage(page) {
923
1046
  const numericPage = Number(page);
924
1047
 
@@ -992,6 +1115,51 @@ export async function submitDataRowUpdate(rowIndex, values) {
992
1115
  }
993
1116
  }
994
1117
 
1118
+ export async function submitDataRowDelete(rowIndex, options = {}) {
1119
+ const numericIndex = Number(rowIndex);
1120
+ const tableName = state.dataBrowser.selectedTable;
1121
+ const row = state.dataBrowser.table?.rows?.[numericIndex];
1122
+ const reportErrorToModal = Boolean(options.reportErrorToModal);
1123
+
1124
+ if (!tableName || !row?.__identity) {
1125
+ pushToast("The selected row could not be loaded.", "alert");
1126
+ return null;
1127
+ }
1128
+
1129
+ const shouldStepBackPage =
1130
+ (state.dataBrowser.table?.rows?.length ?? 0) <= 1 && state.dataBrowser.page > 1;
1131
+
1132
+ state.dataBrowser.deleting = true;
1133
+ state.dataBrowser.saveError = null;
1134
+ emitChange();
1135
+
1136
+ try {
1137
+ const response = await api.deleteDataTableRow(tableName, {
1138
+ identity: row.__identity,
1139
+ });
1140
+
1141
+ if (shouldStepBackPage) {
1142
+ state.dataBrowser.page -= 1;
1143
+ }
1144
+
1145
+ pushToast(response.message || "Table row deleted.", "success");
1146
+ await loadDataTable(++routeLoadVersion);
1147
+ state.dataBrowser.selectedRowIndex = null;
1148
+ return response.data;
1149
+ } catch (error) {
1150
+ if (reportErrorToModal) {
1151
+ withModalError(error);
1152
+ } else {
1153
+ state.dataBrowser.saveError = normalizeError(error);
1154
+ emitChange();
1155
+ }
1156
+ return null;
1157
+ } finally {
1158
+ state.dataBrowser.deleting = false;
1159
+ emitChange();
1160
+ }
1161
+ }
1162
+
995
1163
  export async function submitEditorRowUpdate(rowIndex, values) {
996
1164
  const numericIndex = Number(rowIndex);
997
1165
  const result = state.editor.result;
@@ -1039,6 +1207,73 @@ export async function submitEditorRowUpdate(rowIndex, values) {
1039
1207
  }
1040
1208
  }
1041
1209
 
1210
+ export async function submitEditorRowDelete(rowIndex, options = {}) {
1211
+ const numericIndex = Number(rowIndex);
1212
+ const result = state.editor.result;
1213
+ const row = result?.rows?.[numericIndex];
1214
+ const tableName = result?.editing?.tableName ?? null;
1215
+ const reportErrorToModal = Boolean(options.reportErrorToModal);
1216
+
1217
+ if (!tableName || !row?.__identity || !canEditQueryResult()) {
1218
+ pushToast("The selected query result row could not be loaded.", "alert");
1219
+ return null;
1220
+ }
1221
+
1222
+ state.editor.deleting = true;
1223
+ state.editor.saveError = null;
1224
+ emitChange();
1225
+
1226
+ try {
1227
+ const response = await api.deleteDataTableRow(tableName, {
1228
+ identity: row.__identity,
1229
+ });
1230
+ const nextRows = [...(result.rows ?? [])];
1231
+
1232
+ nextRows.splice(numericIndex, 1);
1233
+ state.editor.result = {
1234
+ ...result,
1235
+ rows: nextRows,
1236
+ };
1237
+ state.editor.selectedRowIndex = null;
1238
+ invalidateDatabaseCaches();
1239
+ pushToast(response.message || "Query result row deleted.", "success");
1240
+ emitChange();
1241
+ return response.data;
1242
+ } catch (error) {
1243
+ if (reportErrorToModal) {
1244
+ withModalError(error);
1245
+ } else {
1246
+ state.editor.saveError = normalizeError(error);
1247
+ emitChange();
1248
+ }
1249
+ return null;
1250
+ } finally {
1251
+ state.editor.deleting = false;
1252
+ emitChange();
1253
+ }
1254
+ }
1255
+
1256
+ export async function submitDeleteRowConfirmation() {
1257
+ const modal = state.modal;
1258
+
1259
+ if (modal?.kind !== "delete-row") {
1260
+ return null;
1261
+ }
1262
+
1263
+ startModalSubmission();
1264
+
1265
+ const result =
1266
+ modal.target === "editor"
1267
+ ? await submitEditorRowDelete(modal.rowIndex, { reportErrorToModal: true })
1268
+ : await submitDataRowDelete(modal.rowIndex, { reportErrorToModal: true });
1269
+
1270
+ if (result) {
1271
+ closeModalInternal();
1272
+ }
1273
+
1274
+ return result;
1275
+ }
1276
+
1042
1277
  export async function exportCurrentQueryCsv() {
1043
1278
  state.editor.exportLoading = true;
1044
1279
  emitChange();
@@ -168,6 +168,80 @@ function getCellWidthClass(columnName) {
168
168
  return "max-w-[12rem]";
169
169
  }
170
170
 
171
+ function getFilteredTableRows(table, state) {
172
+ const allRows = table?.rows ?? [];
173
+ const availableColumns = table?.columns ?? [];
174
+ const searchQuery = String(state.dataBrowser.searchQuery ?? "").trim().toLowerCase();
175
+ const activeColumn = availableColumns.includes(state.dataBrowser.searchColumn)
176
+ ? state.dataBrowser.searchColumn
177
+ : (availableColumns[0] ?? "");
178
+
179
+ const indexedRows = allRows.map((row, index) => ({
180
+ row,
181
+ index,
182
+ }));
183
+
184
+ if (!searchQuery || !activeColumn) {
185
+ return {
186
+ activeColumn,
187
+ filteredRows: indexedRows,
188
+ searchQuery,
189
+ };
190
+ }
191
+
192
+ return {
193
+ activeColumn,
194
+ searchQuery,
195
+ filteredRows: indexedRows.filter(({ row }) =>
196
+ formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery)
197
+ ),
198
+ };
199
+ }
200
+
201
+ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
202
+ const columns = table?.columns ?? [];
203
+
204
+ if (!table || !columns.length) {
205
+ return "";
206
+ }
207
+
208
+ return `
209
+ <div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">
210
+ <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">
211
+ <span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>
212
+ <input
213
+ class="min-w-0 flex-1 bg-transparent text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40"
214
+ data-bind="data-search-query"
215
+ placeholder="Filter current page..."
216
+ type="search"
217
+ value="${escapeHtml(state.dataBrowser.searchQuery ?? "")}"
218
+ />
219
+ </label>
220
+ <select
221
+ 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"
222
+ data-bind="data-search-column"
223
+ >
224
+ ${columns
225
+ .map(
226
+ (columnName) => `
227
+ <option value="${escapeHtml(columnName)}" ${
228
+ columnName === activeColumn ? "selected" : ""
229
+ }>
230
+ ${escapeHtml(columnName)}
231
+ </option>
232
+ `
233
+ )
234
+ .join("")}
235
+ </select>
236
+ <div class="text-[10px] font-mono tracking-[0.14em] text-on-surface-variant/55">
237
+ ${escapeHtml(formatNumber(filteredRowCount))} match${
238
+ filteredRowCount === 1 ? "" : "es"
239
+ } on this page
240
+ </div>
241
+ </div>
242
+ `;
243
+ }
244
+
171
245
  function renderTableSurface(state) {
172
246
  const table = state.dataBrowser.table;
173
247
 
@@ -197,10 +271,11 @@ function renderTableSurface(state) {
197
271
  `;
198
272
  }
199
273
 
274
+ const { activeColumn, filteredRows, searchQuery } = getFilteredTableRows(table, state);
200
275
  const columns = (table.columns ?? []).map((columnName) => ({
201
276
  label: escapeHtml(columnName),
202
277
  headerClassName:
203
- "border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-primary-container",
278
+ "border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold tracking-[0.08em] text-primary-container",
204
279
  cellClassName: "px-4 py-2 align-top text-[11px] text-on-surface",
205
280
  render: (row) => {
206
281
  const value = formatCellValue(row[columnName]);
@@ -219,25 +294,35 @@ function renderTableSurface(state) {
219
294
  const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
220
295
  const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
221
296
  const pageSizes = [25, 50, 100];
297
+ const filteredRowCount = filteredRows.length;
298
+ const hasActiveSearch = Boolean(searchQuery);
222
299
 
223
300
  return `
224
301
  <div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">
302
+ ${renderTableSearchBar(table, state, activeColumn, filteredRowCount)}
225
303
  <div class="custom-scrollbar flex-1 overflow-auto">
226
304
  ${renderDataGrid({
227
305
  columns,
228
- rows: table.rows ?? [],
306
+ rows: filteredRows.map(({ row }) => row),
229
307
  tableClass: "min-w-full border-collapse text-left font-mono text-xs",
230
308
  theadClass: "sticky top-0 z-10 bg-surface-container-highest",
231
309
  tbodyClass: "divide-y divide-outline-variant/5",
232
- getRowClass: (_, index) =>
233
- `${
234
- state.dataBrowser.selectedRowIndex === index
310
+ getRowClass: (_, filteredIndex) => {
311
+ const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
312
+
313
+ return `${
314
+ state.dataBrowser.selectedRowIndex === rowIndex
235
315
  ? "bg-surface-bright"
236
- : index % 2 === 0
316
+ : filteredIndex % 2 === 0
237
317
  ? "bg-surface-container-low"
238
318
  : "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}"`,
319
+ } cursor-pointer transition-colors hover:bg-surface-container-high`;
320
+ },
321
+ getRowAttrs: (_, filteredIndex) => {
322
+ const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
323
+
324
+ return `data-action="select-data-row" data-row-index="${rowIndex}"`;
325
+ },
241
326
  })}
242
327
  ${
243
328
  !table.rows?.length
@@ -248,6 +333,14 @@ function renderTableSurface(state) {
248
333
  </p>
249
334
  </div>
250
335
  `
336
+ : !filteredRowCount
337
+ ? `
338
+ <div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
339
+ <p class="font-mono text-[10px] tracking-[0.18em] text-on-surface-variant/40">
340
+ ${hasActiveSearch ? "No matching rows on this page." : "No rows available."}
341
+ </p>
342
+ </div>
343
+ `
251
344
  : ""
252
345
  }
253
346
  </div>
@@ -255,7 +348,7 @@ function renderTableSurface(state) {
255
348
  <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
256
349
  showing ${escapeHtml(formatNumber(fromRow))}-${escapeHtml(formatNumber(toRow))} of ${escapeHtml(
257
350
  formatNumber(totalRows)
258
- )} rows
351
+ )} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` : ""}
259
352
  </div>
260
353
  <div class="flex flex-wrap items-center gap-4">
261
354
  <div class="flex items-center gap-2">
@@ -380,6 +473,10 @@ function renderDataRowEditorPanel(state) {
380
473
  })),
381
474
  saveError: state.dataBrowser.saveError,
382
475
  saving: state.dataBrowser.saving,
476
+ deleting: state.dataBrowser.deleting,
477
+ deleteAction: "delete-data-row",
478
+ deleteRowIndex: rowIndex,
479
+ deleteEnabled: Boolean(row.__identity),
383
480
  reloadAction: "reload-data-route",
384
481
  });
385
482
  }
@@ -192,6 +192,10 @@ function renderEditorRowPanel(state) {
192
192
  })),
193
193
  saveError: state.editor.saveError,
194
194
  saving: state.editor.saving,
195
+ deleting: state.editor.deleting,
196
+ deleteAction: "delete-editor-row",
197
+ deleteRowIndex: rowIndex,
198
+ deleteEnabled: editingState.enabled && Boolean(row.__identity),
195
199
  });
196
200
  }
197
201
 
@@ -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,
@@ -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.1",
3
+ "version": "0.4.0",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "main": "server/server.js",
6
6
  "bin": {
@@ -8,8 +8,8 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/sqlite-hub.js",
11
- "dev": "node --watch server/server.js",
12
- "publish": "bash publish_brew.sh && bash publish_npm.sh"
11
+ "dev": "node --watch server/server.js --port:4180",
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.