sqlite-hub 0.5.0 → 0.6.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 (36) hide show
  1. package/README.md +2 -2
  2. package/changelog.md +8 -0
  3. package/frontend/assets/mockups/connections.png +0 -0
  4. package/frontend/assets/mockups/data.png +0 -0
  5. package/frontend/assets/mockups/data_row_editor.png +0 -0
  6. package/frontend/assets/mockups/home.png +0 -0
  7. package/frontend/assets/mockups/sql_editor.png +0 -0
  8. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  9. package/frontend/assets/mockups/structure.png +0 -0
  10. package/frontend/assets/mockups/structure_inspector.png +0 -0
  11. package/frontend/js/api.js +15 -0
  12. package/frontend/js/app.js +279 -8
  13. package/frontend/js/components/queryEditor.js +2 -2
  14. package/frontend/js/components/sidebar.js +1 -0
  15. package/frontend/js/components/tableDesignerEditor.js +356 -0
  16. package/frontend/js/components/tableDesignerSidebar.js +126 -0
  17. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  18. package/frontend/js/router.js +10 -0
  19. package/frontend/js/store.js +296 -1
  20. package/frontend/js/utils/tableDesigner.js +1192 -0
  21. package/frontend/js/views/data.js +253 -263
  22. package/frontend/js/views/tableDesigner.js +37 -0
  23. package/frontend/styles/base.css +87 -73
  24. package/frontend/styles/components.css +798 -251
  25. package/frontend/styles/views.css +40 -0
  26. package/package.json +1 -1
  27. package/server/routes/tableDesigner.js +60 -0
  28. package/server/server.js +4 -0
  29. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  30. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  31. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  32. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  33. package/server/services/sqlite/tableDesignerService.js +181 -0
  34. package/frontend/assets/mockups/data_edit.png +0 -0
  35. package/frontend/assets/mockups/graph_visualize.png +0 -0
  36. package/frontend/assets/mockups/overview.png +0 -0
@@ -1,29 +1,23 @@
1
- import { renderDataGrid } from "../components/dataGrid.js";
2
- import { renderRowEditorPanel } from "../components/rowEditorPanel.js";
3
- import {
4
- escapeHtml,
5
- formatCellValue,
6
- formatNumber,
7
- isBlobPreview,
8
- truncateMiddle,
9
- } from "../utils/format.js";
1
+ import { renderDataGrid } from '../components/dataGrid.js';
2
+ import { renderRowEditorPanel } from '../components/rowEditorPanel.js';
3
+ import { escapeHtml, formatCellValue, formatNumber, isBlobPreview, truncateMiddle } from '../utils/format.js';
10
4
 
11
5
  function getSelectedRow(state) {
12
- const rowIndex = state.dataBrowser.selectedRowIndex;
6
+ const rowIndex = state.dataBrowser.selectedRowIndex;
13
7
 
14
- if (typeof rowIndex !== "number") {
15
- return null;
16
- }
8
+ if (typeof rowIndex !== 'number') {
9
+ return null;
10
+ }
17
11
 
18
- return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
12
+ return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
19
13
  }
20
14
 
21
15
  function renderTableList(state) {
22
- const tables = state.dataBrowser.tables ?? [];
23
- const activeName = state.dataBrowser.selectedTable;
16
+ const tables = state.dataBrowser.tables ?? [];
17
+ const activeName = state.dataBrowser.selectedTable;
24
18
 
25
- if (state.dataBrowser.loading && !tables.length) {
26
- return `
19
+ if (state.dataBrowser.loading && !tables.length) {
20
+ return `
27
21
  <div class="flex flex-1 items-center justify-center px-6">
28
22
  <div class="text-center text-on-surface-variant/40">
29
23
  <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
@@ -31,82 +25,82 @@ function renderTableList(state) {
31
25
  </div>
32
26
  </div>
33
27
  `;
34
- }
28
+ }
35
29
 
36
- if (!tables.length) {
37
- return `
30
+ if (!tables.length) {
31
+ return `
38
32
  <div class="px-6 py-6 text-sm text-on-surface-variant/55">
39
33
  No tables found in the active SQLite database.
40
34
  </div>
41
35
  `;
42
- }
36
+ }
43
37
 
44
- return `
38
+ return `
45
39
  <div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
46
40
  <div class="space-y-2">
47
41
  ${tables
48
- .map(
49
- (table) => `
42
+ .map(
43
+ table => `
50
44
  <button
51
45
  class="w-full border px-4 py-3 text-left transition-colors ${
52
- table.name === activeName
53
- ? "border-primary-container/30 bg-surface-container-high"
54
- : "border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high"
46
+ table.name === activeName
47
+ ? 'border-primary-container/30 bg-surface-container-high'
48
+ : 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
55
49
  }"
56
50
  data-action="navigate"
57
51
  data-to="/data/${encodeURIComponent(table.name)}"
58
52
  type="button"
59
53
  >
60
54
  <div class="truncate font-mono text-xs ${
61
- table.name === activeName ? "text-primary-container" : "text-on-surface"
55
+ table.name === activeName ? 'text-primary-container' : 'text-on-surface'
62
56
  }">
63
57
  ${escapeHtml(table.name)}
64
58
  </div>
65
59
  </button>
66
- `
67
- )
68
- .join("")}
60
+ `,
61
+ )
62
+ .join('')}
69
63
  </div>
70
64
  </div>
71
65
  `;
72
66
  }
73
67
 
74
68
  function renderWorkspaceHeader(state) {
75
- const table = state.dataBrowser.table;
69
+ const table = state.dataBrowser.table;
76
70
 
77
- return `
71
+ return `
78
72
  <header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
79
73
  <div class="flex flex-wrap items-end justify-between gap-4">
80
- <div>
74
+ <div class="data-headline-container">
81
75
  <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
82
76
  Data Browser
83
77
  </div>
84
78
  <h1 class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
85
- ${escapeHtml(table?.name ?? "Table Data")}
79
+ ${escapeHtml(table?.name ?? 'Table Data')}
86
80
  </h1>
87
81
  <div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
88
82
  ${
89
- table
90
- ? `rows ${escapeHtml(formatNumber(table.rowCount ?? 0))} // columns ${escapeHtml(
91
- formatNumber(table.columns?.length ?? 0)
92
- )}`
93
- : `tables ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}`
83
+ table
84
+ ? `rows ${escapeHtml(formatNumber(table.rowCount ?? 0))} // columns ${escapeHtml(
85
+ formatNumber(table.columns?.length ?? 0),
86
+ )}`
87
+ : `tables ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}`
94
88
  }
95
89
  </div>
96
90
  </div>
97
91
  <div class="flex items-center gap-3">
98
92
  ${
99
- table
100
- ? `
93
+ table
94
+ ? `
101
95
  <button
102
96
  class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
103
97
  data-action="export-data-csv"
104
98
  type="button"
105
99
  >
106
- ${state.dataBrowser.exportLoading ? "Exporting..." : "Export CSV"}
100
+ ${state.dataBrowser.exportLoading ? 'Exporting...' : 'Export CSV'}
107
101
  </button>
108
102
  `
109
- : ""
103
+ : ''
110
104
  }
111
105
  <button
112
106
  class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
@@ -116,8 +110,8 @@ function renderWorkspaceHeader(state) {
116
110
  Reload Data
117
111
  </button>
118
112
  ${
119
- table
120
- ? `
113
+ table
114
+ ? `
121
115
  <button
122
116
  class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
123
117
  data-action="navigate"
@@ -127,7 +121,7 @@ function renderWorkspaceHeader(state) {
127
121
  Open Structure
128
122
  </button>
129
123
  `
130
- : ""
124
+ : ''
131
125
  }
132
126
  </div>
133
127
  </div>
@@ -136,11 +130,11 @@ function renderWorkspaceHeader(state) {
136
130
  }
137
131
 
138
132
  function renderWorkspaceError(state) {
139
- if (!state.dataBrowser.error) {
140
- return "";
141
- }
133
+ if (!state.dataBrowser.error) {
134
+ return '';
135
+ }
142
136
 
143
- return `
137
+ return `
144
138
  <div class="border-b border-error/20 bg-error-container/10 px-6 py-4 text-sm text-on-surface">
145
139
  <div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
146
140
  ${escapeHtml(state.dataBrowser.error.code)}
@@ -151,38 +145,38 @@ function renderWorkspaceError(state) {
151
145
  }
152
146
 
153
147
  function getCellWidthClass(columnName) {
154
- const normalized = String(columnName ?? "").toLowerCase();
148
+ const normalized = String(columnName ?? '').toLowerCase();
155
149
 
156
- if (/(path|url|hash|sql|query|content|description|message|title|name)/.test(normalized)) {
157
- return "max-w-[18rem]";
158
- }
150
+ if (/(path|url|hash|sql|query|content|description|message|title|name)/.test(normalized)) {
151
+ return 'max-w-[18rem]';
152
+ }
159
153
 
160
- if (/(date|time|modified|created|updated|timestamp)/.test(normalized)) {
161
- return "max-w-[11rem]";
162
- }
154
+ if (/(date|time|modified|created|updated|timestamp)/.test(normalized)) {
155
+ return 'max-w-[11rem]';
156
+ }
163
157
 
164
- if (/(id|uuid|token|key)/.test(normalized)) {
165
- return "max-w-[10rem]";
166
- }
158
+ if (/(id|uuid|token|key)/.test(normalized)) {
159
+ return 'max-w-[10rem]';
160
+ }
167
161
 
168
- return "max-w-[12rem]";
162
+ return 'max-w-[12rem]';
169
163
  }
170
164
 
171
165
  function getSortIcon(columnName, sortColumn, sortDirection) {
172
- if (columnName !== sortColumn) {
173
- return "unfold_more";
174
- }
166
+ if (columnName !== sortColumn) {
167
+ return 'unfold_more';
168
+ }
175
169
 
176
- return sortDirection === "desc" ? "south" : "north";
170
+ return sortDirection === 'desc' ? 'south' : 'north';
177
171
  }
178
172
 
179
173
  function renderSortableHeader(columnName, sortColumn, sortDirection, action) {
180
- const isActive = columnName === sortColumn;
174
+ const isActive = columnName === sortColumn;
181
175
 
182
- return `
176
+ return `
183
177
  <button
184
178
  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"
179
+ isActive ? 'text-primary-container' : 'text-on-surface-variant hover:text-primary-container'
186
180
  }"
187
181
  data-action="${action}"
188
182
  data-column-name="${escapeHtml(columnName)}"
@@ -190,52 +184,54 @@ function renderSortableHeader(columnName, sortColumn, sortDirection, action) {
190
184
  >
191
185
  <span class="truncate">${escapeHtml(columnName)}</span>
192
186
  <span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
193
- columnName,
194
- sortColumn,
195
- sortDirection
187
+ columnName,
188
+ sortColumn,
189
+ sortDirection,
196
190
  )}</span>
197
191
  </button>
198
192
  `;
199
193
  }
200
194
 
201
195
  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) {
196
+ const allRows = table?.rows ?? [];
197
+ const availableColumns = table?.columns ?? [];
198
+ const searchQuery = String(state.dataBrowser.searchQuery ?? '')
199
+ .trim()
200
+ .toLowerCase();
201
+ const activeColumn = availableColumns.includes(state.dataBrowser.searchColumn)
202
+ ? state.dataBrowser.searchColumn
203
+ : (availableColumns[0] ?? '');
204
+
205
+ const indexedRows = allRows.map((row, index) => ({
206
+ row,
207
+ index,
208
+ }));
209
+
210
+ if (!searchQuery || !activeColumn) {
211
+ return {
212
+ activeColumn,
213
+ filteredRows: indexedRows,
214
+ searchQuery,
215
+ };
216
+ }
217
+
215
218
  return {
216
- activeColumn,
217
- filteredRows: indexedRows,
218
- searchQuery,
219
+ activeColumn,
220
+ searchQuery,
221
+ filteredRows: indexedRows.filter(({ row }) =>
222
+ formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery),
223
+ ),
219
224
  };
220
- }
221
-
222
- return {
223
- activeColumn,
224
- searchQuery,
225
- filteredRows: indexedRows.filter(({ row }) =>
226
- formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery)
227
- ),
228
- };
229
225
  }
230
226
 
231
227
  function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
232
- const columns = table?.columns ?? [];
228
+ const columns = table?.columns ?? [];
233
229
 
234
- if (!table || !columns.length) {
235
- return "";
236
- }
230
+ if (!table || !columns.length) {
231
+ return '';
232
+ }
237
233
 
238
- return `
234
+ return `
239
235
  <div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">
240
236
  <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
237
  <span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>
@@ -244,7 +240,7 @@ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
244
240
  data-bind="data-search-query"
245
241
  placeholder="Filter current page..."
246
242
  type="search"
247
- value="${escapeHtml(state.dataBrowser.searchQuery ?? "")}"
243
+ value="${escapeHtml(state.dataBrowser.searchQuery ?? '')}"
248
244
  />
249
245
  </label>
250
246
  <select
@@ -252,31 +248,27 @@ function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
252
248
  data-bind="data-search-column"
253
249
  >
254
250
  ${columns
255
- .map(
256
- (columnName) => `
257
- <option value="${escapeHtml(columnName)}" ${
258
- columnName === activeColumn ? "selected" : ""
259
- }>
251
+ .map(
252
+ columnName => `
253
+ <option value="${escapeHtml(columnName)}" ${columnName === activeColumn ? 'selected' : ''}>
260
254
  ${escapeHtml(columnName)}
261
255
  </option>
262
- `
263
- )
264
- .join("")}
256
+ `,
257
+ )
258
+ .join('')}
265
259
  </select>
266
260
  <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
261
+ ${escapeHtml(formatNumber(filteredRowCount))} match${filteredRowCount === 1 ? '' : 'es'} on this page
270
262
  </div>
271
263
  </div>
272
264
  `;
273
265
  }
274
266
 
275
267
  function renderTableSurface(state) {
276
- const table = state.dataBrowser.table;
268
+ const table = state.dataBrowser.table;
277
269
 
278
- if (state.dataBrowser.tableLoading && !table) {
279
- return `
270
+ if (state.dataBrowser.tableLoading && !table) {
271
+ return `
280
272
  <div class="flex flex-1 items-center justify-center">
281
273
  <div class="text-center text-on-surface-variant/40">
282
274
  <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
@@ -284,10 +276,10 @@ function renderTableSurface(state) {
284
276
  </div>
285
277
  </div>
286
278
  `;
287
- }
279
+ }
288
280
 
289
- if (!table) {
290
- return `
281
+ if (!table) {
282
+ return `
291
283
  <div class="flex flex-1 items-center justify-center px-8 text-center">
292
284
  <div>
293
285
  <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
@@ -299,89 +291,88 @@ function renderTableSurface(state) {
299
291
  </div>
300
292
  </div>
301
293
  `;
302
- }
303
-
304
- const { activeColumn, filteredRows, searchQuery } = getFilteredTableRows(table, state);
305
- const sortColumn = state.dataBrowser.sortColumn;
306
- const sortDirection = state.dataBrowser.sortDirection;
307
- const columns = (table.columns ?? []).map((columnName) => ({
308
- headerClassName:
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"),
312
- cellClassName: "px-4 py-2 align-top text-[11px] text-on-surface",
313
- render: (row) => {
314
- const value = formatCellValue(row[columnName]);
315
- const isNull = value === "NULL";
316
- const widthClass = getCellWidthClass(columnName);
317
- const displayValue = isNull ? value : truncateMiddle(value, 48);
318
-
319
- return `<span class="block ${widthClass} overflow-hidden text-ellipsis whitespace-nowrap ${
320
- isNull ? "text-on-surface-variant/45" : "text-on-surface"
321
- }" title="${escapeHtml(value)}">${escapeHtml(displayValue)}</span>`;
322
- },
323
- }));
324
- const totalRows = table.rowCount ?? 0;
325
- const page = table.page ?? state.dataBrowser.page ?? 1;
326
- const pageCount = table.pageCount ?? Math.max(1, Math.ceil(totalRows / (table.limit ?? 50)));
327
- const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
328
- const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
329
- const pageSizes = [25, 50, 100];
330
- const filteredRowCount = filteredRows.length;
331
- const hasActiveSearch = Boolean(searchQuery);
332
-
333
- return `
294
+ }
295
+
296
+ const { activeColumn, filteredRows, searchQuery } = getFilteredTableRows(table, state);
297
+ const sortColumn = state.dataBrowser.sortColumn;
298
+ const sortDirection = state.dataBrowser.sortDirection;
299
+ const columns = (table.columns ?? []).map(columnName => ({
300
+ headerClassName:
301
+ 'border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold tracking-[0.08em] text-primary-container',
302
+ renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection, 'sort-data-column'),
303
+ cellClassName: 'px-4 py-2 align-top text-[11px] text-on-surface',
304
+ render: row => {
305
+ const value = formatCellValue(row[columnName]);
306
+ const isNull = value === 'NULL';
307
+ const widthClass = getCellWidthClass(columnName);
308
+ const displayValue = isNull ? value : truncateMiddle(value, 48);
309
+
310
+ return `<span class="block ${widthClass} overflow-hidden text-ellipsis whitespace-nowrap ${
311
+ isNull ? 'text-on-surface-variant/45' : 'text-on-surface'
312
+ }" title="${escapeHtml(value)}">${escapeHtml(displayValue)}</span>`;
313
+ },
314
+ }));
315
+ const totalRows = table.rowCount ?? 0;
316
+ const page = table.page ?? state.dataBrowser.page ?? 1;
317
+ const pageCount = table.pageCount ?? Math.max(1, Math.ceil(totalRows / (table.limit ?? 50)));
318
+ const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
319
+ const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
320
+ const pageSizes = [25, 50, 100];
321
+ const filteredRowCount = filteredRows.length;
322
+ const hasActiveSearch = Boolean(searchQuery);
323
+
324
+ return `
334
325
  <div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">
335
326
  ${renderTableSearchBar(table, state, activeColumn, filteredRowCount)}
336
327
  <div class="custom-scrollbar flex-1 overflow-auto">
337
328
  ${renderDataGrid({
338
- columns,
339
- rows: filteredRows.map(({ row }) => row),
340
- tableClass: "min-w-full border-collapse text-left font-mono text-xs",
341
- theadClass: "sticky top-0 z-10 bg-surface-container-highest",
342
- tbodyClass: "divide-y divide-outline-variant/5",
343
- getRowClass: (_, filteredIndex) => {
344
- const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
345
-
346
- return `${
347
- state.dataBrowser.selectedRowIndex === rowIndex
348
- ? "bg-surface-bright"
349
- : filteredIndex % 2 === 0
350
- ? "bg-surface-container-low"
351
- : "bg-surface-container-lowest"
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
- },
329
+ columns,
330
+ rows: filteredRows.map(({ row }) => row),
331
+ tableClass: 'min-w-full border-collapse text-left font-mono text-xs',
332
+ theadClass: 'sticky top-0 z-10 bg-surface-container-highest',
333
+ tbodyClass: 'divide-y divide-outline-variant/5',
334
+ getRowClass: (_, filteredIndex) => {
335
+ const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
336
+
337
+ return `${
338
+ state.dataBrowser.selectedRowIndex === rowIndex
339
+ ? 'bg-surface-bright'
340
+ : filteredIndex % 2 === 0
341
+ ? 'bg-surface-container-low'
342
+ : 'bg-surface-container-lowest'
343
+ } cursor-pointer transition-colors hover:bg-surface-container-high`;
344
+ },
345
+ getRowAttrs: (_, filteredIndex) => {
346
+ const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
347
+
348
+ return `data-action="select-data-row" data-row-index="${rowIndex}"`;
349
+ },
359
350
  })}
360
351
  ${
361
- !table.rows?.length
362
- ? `
352
+ !table.rows?.length
353
+ ? `
363
354
  <div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
364
355
  <p class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/40">
365
356
  TABLE_IS_EMPTY
366
357
  </p>
367
358
  </div>
368
359
  `
369
- : !filteredRowCount
370
- ? `
360
+ : !filteredRowCount
361
+ ? `
371
362
  <div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
372
363
  <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."}
364
+ ${hasActiveSearch ? 'No matching rows on this page.' : 'No rows available.'}
374
365
  </p>
375
366
  </div>
376
367
  `
377
- : ""
368
+ : ''
378
369
  }
379
370
  </div>
380
371
  <footer class="flex flex-wrap items-center justify-between gap-4 border-t border-outline-variant/10 bg-surface-container px-6 py-4">
381
372
  <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
382
373
  showing ${escapeHtml(formatNumber(fromRow))}-${escapeHtml(formatNumber(toRow))} of ${escapeHtml(
383
- formatNumber(totalRows)
384
- )} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` : ""}
374
+ formatNumber(totalRows),
375
+ )} rows${hasActiveSearch ? ` // ${escapeHtml(formatNumber(filteredRowCount))} visible on this page` : ''}
385
376
  </div>
386
377
  <div class="flex flex-wrap items-center gap-4">
387
378
  <div class="flex items-center gap-2">
@@ -390,13 +381,13 @@ function renderTableSurface(state) {
390
381
  </span>
391
382
  <div class="flex items-center gap-2">
392
383
  ${pageSizes
393
- .map(
394
- (pageSize) => `
384
+ .map(
385
+ pageSize => `
395
386
  <button
396
387
  class="border px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] transition-colors ${
397
- pageSize === (table.limit ?? state.dataBrowser.pageSize)
398
- ? "border-primary-container/30 bg-surface-container-high text-primary-container"
399
- : "border-outline-variant/20 text-on-surface hover:bg-surface-container-highest"
388
+ pageSize === (table.limit ?? state.dataBrowser.pageSize)
389
+ ? 'border-primary-container/30 bg-surface-container-high text-primary-container'
390
+ : 'border-outline-variant/20 text-on-surface hover:bg-surface-container-highest'
400
391
  }"
401
392
  data-action="set-data-page-size"
402
393
  data-page-size="${pageSize}"
@@ -404,9 +395,9 @@ function renderTableSurface(state) {
404
395
  >
405
396
  ${pageSize}
406
397
  </button>
407
- `
408
- )
409
- .join("")}
398
+ `,
399
+ )
400
+ .join('')}
410
401
  </div>
411
402
  </div>
412
403
  <div class="flex items-center gap-2">
@@ -415,7 +406,7 @@ function renderTableSurface(state) {
415
406
  data-action="set-data-page"
416
407
  data-page="${page - 1}"
417
408
  type="button"
418
- ${page <= 1 ? "disabled" : ""}
409
+ ${page <= 1 ? 'disabled' : ''}
419
410
  >
420
411
  Prev
421
412
  </button>
@@ -427,7 +418,7 @@ function renderTableSurface(state) {
427
418
  data-action="set-data-page"
428
419
  data-page="${page + 1}"
429
420
  type="button"
430
- ${page >= pageCount ? "disabled" : ""}
421
+ ${page >= pageCount ? 'disabled' : ''}
431
422
  >
432
423
  Next
433
424
  </button>
@@ -439,84 +430,83 @@ function renderTableSurface(state) {
439
430
  }
440
431
 
441
432
  function renderDataRowEditorPanel(state) {
442
- const table = state.dataBrowser.table;
443
- const rowIndex = state.dataBrowser.selectedRowIndex;
444
- const row = getSelectedRow(state);
445
-
446
- if (!table || !row || typeof rowIndex !== "number") {
447
- return "";
448
- }
449
-
450
- const identityColumns =
451
- table.identityStrategy?.type === "primaryKey" ? table.identityStrategy.columns ?? [] : [];
452
- const editableColumns = (table.columnMeta ?? []).filter((column) => {
453
- if (!column.visible || column.generated) {
454
- return false;
455
- }
433
+ const table = state.dataBrowser.table;
434
+ const rowIndex = state.dataBrowser.selectedRowIndex;
435
+ const row = getSelectedRow(state);
456
436
 
457
- if (identityColumns.includes(column.name)) {
458
- return false;
437
+ if (!table || !row || typeof rowIndex !== 'number') {
438
+ return '';
459
439
  }
460
440
 
461
- const value = row[column.name];
462
- if (isBlobPreview(value) || (value && typeof value === "object")) {
463
- return false;
464
- }
441
+ const identityColumns = table.identityStrategy?.type === 'primaryKey' ? (table.identityStrategy.columns ?? []) : [];
442
+ const editableColumns = (table.columnMeta ?? []).filter(column => {
443
+ if (!column.visible || column.generated) {
444
+ return false;
445
+ }
465
446
 
466
- return true;
467
- });
468
- const readonlyColumns = (table.columnMeta ?? []).filter((column) => {
469
- if (!column.visible) {
470
- return false;
471
- }
447
+ if (identityColumns.includes(column.name)) {
448
+ return false;
449
+ }
472
450
 
473
- if (identityColumns.includes(column.name) || column.generated) {
474
- return true;
475
- }
451
+ const value = row[column.name];
452
+ if (isBlobPreview(value) || (value && typeof value === 'object')) {
453
+ return false;
454
+ }
455
+
456
+ return true;
457
+ });
458
+ const readonlyColumns = (table.columnMeta ?? []).filter(column => {
459
+ if (!column.visible) {
460
+ return false;
461
+ }
462
+
463
+ if (identityColumns.includes(column.name) || column.generated) {
464
+ return true;
465
+ }
476
466
 
477
- const value = row[column.name];
478
- return isBlobPreview(value) || (value && typeof value === "object");
479
- });
480
-
481
- return renderRowEditorPanel({
482
- title: table.name,
483
- sectionLabel: "Row Editor",
484
- subtitle: `row ${rowIndex + 1}`,
485
- closeAction: "clear-data-row-selection",
486
- formName: "save-data-row",
487
- hiddenFields: [{ name: "rowIndex", value: String(rowIndex) }],
488
- disabledMessage: state.connections.active?.readOnly
489
- ? "The active database is opened read-only, so row editing is disabled."
490
- : table.notSafelyUpdatable
491
- ? "This table has no stable identity column, so SQLite Hub cannot safely update rows."
492
- : "",
493
- editableFields: editableColumns.map((column) => {
494
- const value = row[column.name];
495
-
496
- return {
497
- name: column.name,
498
- label: column.name,
499
- value: value === null || value === undefined ? "" : String(value),
500
- };
501
- }),
502
- readonlyFields: readonlyColumns.map((column) => ({
503
- name: column.name,
504
- label: column.name,
505
- value: formatCellValue(row[column.name]),
506
- })),
507
- saveError: state.dataBrowser.saveError,
508
- saving: state.dataBrowser.saving,
509
- deleting: state.dataBrowser.deleting,
510
- deleteAction: "delete-data-row",
511
- deleteRowIndex: rowIndex,
512
- deleteEnabled: Boolean(row.__identity),
513
- reloadAction: "reload-data-route",
514
- });
467
+ const value = row[column.name];
468
+ return isBlobPreview(value) || (value && typeof value === 'object');
469
+ });
470
+
471
+ return renderRowEditorPanel({
472
+ title: table.name,
473
+ sectionLabel: 'Row Editor',
474
+ subtitle: `row ${rowIndex + 1}`,
475
+ closeAction: 'clear-data-row-selection',
476
+ formName: 'save-data-row',
477
+ hiddenFields: [{ name: 'rowIndex', value: String(rowIndex) }],
478
+ disabledMessage: state.connections.active?.readOnly
479
+ ? 'The active database is opened read-only, so row editing is disabled.'
480
+ : table.notSafelyUpdatable
481
+ ? 'This table has no stable identity column, so SQLite Hub cannot safely update rows.'
482
+ : '',
483
+ editableFields: editableColumns.map(column => {
484
+ const value = row[column.name];
485
+
486
+ return {
487
+ name: column.name,
488
+ label: column.name,
489
+ value: value === null || value === undefined ? '' : String(value),
490
+ };
491
+ }),
492
+ readonlyFields: readonlyColumns.map(column => ({
493
+ name: column.name,
494
+ label: column.name,
495
+ value: formatCellValue(row[column.name]),
496
+ })),
497
+ saveError: state.dataBrowser.saveError,
498
+ saving: state.dataBrowser.saving,
499
+ deleting: state.dataBrowser.deleting,
500
+ deleteAction: 'delete-data-row',
501
+ deleteRowIndex: rowIndex,
502
+ deleteEnabled: Boolean(row.__identity),
503
+ reloadAction: 'reload-data-route',
504
+ });
515
505
  }
516
506
 
517
507
  export function renderDataView(state) {
518
- return {
519
- main: `
508
+ return {
509
+ main: `
520
510
  <section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden">
521
511
  <div class="grid h-full min-h-0 grid-cols-1 md:grid-cols-[18rem_minmax(0,1fr)]">
522
512
  <aside class="flex min-h-0 flex-col border-r border-outline-variant/10 bg-surface-low">
@@ -538,6 +528,6 @@ export function renderDataView(state) {
538
528
  </div>
539
529
  </section>
540
530
  `,
541
- panel: renderDataRowEditorPanel(state),
542
- };
531
+ panel: renderDataRowEditorPanel(state),
532
+ };
543
533
  }