sqlite-hub 0.1.3

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 (76) hide show
  1. package/.npmingnore +4 -0
  2. package/README.md +46 -0
  3. package/assets/images/logo.webp +0 -0
  4. package/assets/images/logo_extrasmall.webp +0 -0
  5. package/assets/images/logo_raw.png +0 -0
  6. package/assets/images/logo_small.webp +0 -0
  7. package/assets/mockups/connections.png +0 -0
  8. package/assets/mockups/data.png +0 -0
  9. package/assets/mockups/data_edit.png +0 -0
  10. package/assets/mockups/home.png +0 -0
  11. package/assets/mockups/overview.png +0 -0
  12. package/assets/mockups/sql_editor.png +0 -0
  13. package/assets/mockups/structure.png +0 -0
  14. package/bin/sqlite-hub.js +116 -0
  15. package/changelog.md +3 -0
  16. package/data/.gitkeep +0 -0
  17. package/index.html +100 -0
  18. package/js/api.js +193 -0
  19. package/js/app.js +520 -0
  20. package/js/components/actionBar.js +8 -0
  21. package/js/components/appShell.js +17 -0
  22. package/js/components/badges.js +5 -0
  23. package/js/components/bottomTabs.js +37 -0
  24. package/js/components/connectionCard.js +106 -0
  25. package/js/components/dataGrid.js +47 -0
  26. package/js/components/emptyState.js +159 -0
  27. package/js/components/metricCard.js +32 -0
  28. package/js/components/modal.js +317 -0
  29. package/js/components/pageHeader.js +33 -0
  30. package/js/components/queryEditor.js +121 -0
  31. package/js/components/queryResults.js +107 -0
  32. package/js/components/rowEditorPanel.js +164 -0
  33. package/js/components/sidebar.js +57 -0
  34. package/js/components/statusBar.js +39 -0
  35. package/js/components/toast.js +39 -0
  36. package/js/components/topNav.js +27 -0
  37. package/js/router.js +66 -0
  38. package/js/store.js +1092 -0
  39. package/js/utils/format.js +179 -0
  40. package/js/views/connections.js +133 -0
  41. package/js/views/data.js +400 -0
  42. package/js/views/editor.js +259 -0
  43. package/js/views/landing.js +11 -0
  44. package/js/views/overview.js +220 -0
  45. package/js/views/settings.js +109 -0
  46. package/js/views/structure.js +242 -0
  47. package/package.json +18 -0
  48. package/publish_brew.sh +444 -0
  49. package/publish_npm.sh +241 -0
  50. package/server/routes/connections.js +146 -0
  51. package/server/routes/data.js +59 -0
  52. package/server/routes/export.js +25 -0
  53. package/server/routes/overview.js +39 -0
  54. package/server/routes/settings.js +50 -0
  55. package/server/routes/sql.js +50 -0
  56. package/server/routes/structure.js +38 -0
  57. package/server/server.js +136 -0
  58. package/server/services/sqlite/connectionManager.js +306 -0
  59. package/server/services/sqlite/dataBrowserService.js +255 -0
  60. package/server/services/sqlite/exportService.js +34 -0
  61. package/server/services/sqlite/importService.js +111 -0
  62. package/server/services/sqlite/introspection.js +302 -0
  63. package/server/services/sqlite/overviewService.js +109 -0
  64. package/server/services/sqlite/sqlExecutor.js +434 -0
  65. package/server/services/sqlite/structureService.js +60 -0
  66. package/server/services/storage/appStateStore.js +530 -0
  67. package/server/utils/csv.js +34 -0
  68. package/server/utils/errors.js +175 -0
  69. package/server/utils/fileValidation.js +135 -0
  70. package/server/utils/identifier.js +38 -0
  71. package/server/utils/sqliteTypes.js +112 -0
  72. package/styles/base.css +176 -0
  73. package/styles/components.css +323 -0
  74. package/styles/layout.css +101 -0
  75. package/styles/tokens.css +49 -0
  76. package/styles/views.css +84 -0
@@ -0,0 +1,400 @@
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";
10
+
11
+ function getSelectedRow(state) {
12
+ const rowIndex = state.dataBrowser.selectedRowIndex;
13
+
14
+ if (typeof rowIndex !== "number") {
15
+ return null;
16
+ }
17
+
18
+ return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
19
+ }
20
+
21
+ function renderTableList(state) {
22
+ const tables = state.dataBrowser.tables ?? [];
23
+ const activeName = state.dataBrowser.selectedTable;
24
+
25
+ if (state.dataBrowser.loading && !tables.length) {
26
+ return `
27
+ <div class="flex flex-1 items-center justify-center px-6">
28
+ <div class="text-center text-on-surface-variant/40">
29
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
30
+ <p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_TABLES</p>
31
+ </div>
32
+ </div>
33
+ `;
34
+ }
35
+
36
+ if (!tables.length) {
37
+ return `
38
+ <div class="px-6 py-6 text-sm text-on-surface-variant/55">
39
+ No tables found in the active SQLite database.
40
+ </div>
41
+ `;
42
+ }
43
+
44
+ return `
45
+ <div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
46
+ <div class="space-y-2">
47
+ ${tables
48
+ .map(
49
+ (table) => `
50
+ <button
51
+ 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"
55
+ }"
56
+ data-action="navigate"
57
+ data-to="/data/${encodeURIComponent(table.name)}"
58
+ type="button"
59
+ >
60
+ <div class="truncate font-mono text-xs ${
61
+ table.name === activeName ? "text-primary-container" : "text-on-surface"
62
+ }">
63
+ ${escapeHtml(table.name)}
64
+ </div>
65
+ </button>
66
+ `
67
+ )
68
+ .join("")}
69
+ </div>
70
+ </div>
71
+ `;
72
+ }
73
+
74
+ function renderWorkspaceHeader(state) {
75
+ const table = state.dataBrowser.table;
76
+
77
+ return `
78
+ <header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
79
+ <div class="flex flex-wrap items-end justify-between gap-4">
80
+ <div>
81
+ <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
82
+ Data Browser
83
+ </div>
84
+ <h1 class="mt-2 font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
85
+ ${escapeHtml(table?.name ?? "Table Data")}
86
+ </h1>
87
+ <div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
88
+ ${
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))}`
94
+ }
95
+ </div>
96
+ </div>
97
+ <div class="flex items-center gap-3">
98
+ <button
99
+ 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"
100
+ data-action="refresh-view"
101
+ type="button"
102
+ >
103
+ Reload Data
104
+ </button>
105
+ ${
106
+ table
107
+ ? `
108
+ <button
109
+ 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"
110
+ data-action="navigate"
111
+ data-to="/structure"
112
+ type="button"
113
+ >
114
+ Open Structure
115
+ </button>
116
+ `
117
+ : ""
118
+ }
119
+ </div>
120
+ </div>
121
+ </header>
122
+ `;
123
+ }
124
+
125
+ function renderWorkspaceError(state) {
126
+ if (!state.dataBrowser.error) {
127
+ return "";
128
+ }
129
+
130
+ return `
131
+ <div class="border-b border-error/20 bg-error-container/10 px-6 py-4 text-sm text-on-surface">
132
+ <div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
133
+ ${escapeHtml(state.dataBrowser.error.code)}
134
+ </div>
135
+ <div class="mt-2">${escapeHtml(state.dataBrowser.error.message)}</div>
136
+ </div>
137
+ `;
138
+ }
139
+
140
+ function getCellWidthClass(columnName) {
141
+ const normalized = String(columnName ?? "").toLowerCase();
142
+
143
+ if (/(path|url|hash|sql|query|content|description|message|title|name)/.test(normalized)) {
144
+ return "max-w-[18rem]";
145
+ }
146
+
147
+ if (/(date|time|modified|created|updated|timestamp)/.test(normalized)) {
148
+ return "max-w-[11rem]";
149
+ }
150
+
151
+ if (/(id|uuid|token|key)/.test(normalized)) {
152
+ return "max-w-[10rem]";
153
+ }
154
+
155
+ return "max-w-[12rem]";
156
+ }
157
+
158
+ function renderTableSurface(state) {
159
+ const table = state.dataBrowser.table;
160
+
161
+ if (state.dataBrowser.tableLoading && !table) {
162
+ return `
163
+ <div class="flex flex-1 items-center justify-center">
164
+ <div class="text-center text-on-surface-variant/40">
165
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
166
+ <p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_TABLE_DATA</p>
167
+ </div>
168
+ </div>
169
+ `;
170
+ }
171
+
172
+ if (!table) {
173
+ return `
174
+ <div class="flex flex-1 items-center justify-center px-8 text-center">
175
+ <div>
176
+ <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
177
+ Full Table
178
+ </div>
179
+ <p class="mt-3 text-sm text-on-surface-variant/55">
180
+ Select a table to browse and edit its rows.
181
+ </p>
182
+ </div>
183
+ </div>
184
+ `;
185
+ }
186
+
187
+ const columns = (table.columns ?? []).map((columnName) => ({
188
+ label: escapeHtml(columnName),
189
+ headerClassName:
190
+ "border-b border-primary-container/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-primary-container",
191
+ cellClassName: "px-4 py-2 align-top text-[11px] text-on-surface",
192
+ render: (row) => {
193
+ const value = formatCellValue(row[columnName]);
194
+ const isNull = value === "NULL";
195
+ const widthClass = getCellWidthClass(columnName);
196
+ const displayValue = isNull ? value : truncateMiddle(value, 48);
197
+
198
+ return `<span class="block ${widthClass} overflow-hidden text-ellipsis whitespace-nowrap ${
199
+ isNull ? "text-on-surface-variant/45" : "text-on-surface"
200
+ }" title="${escapeHtml(value)}">${escapeHtml(displayValue)}</span>`;
201
+ },
202
+ }));
203
+ const totalRows = table.rowCount ?? 0;
204
+ const page = table.page ?? state.dataBrowser.page ?? 1;
205
+ const pageCount = table.pageCount ?? Math.max(1, Math.ceil(totalRows / (table.limit ?? 50)));
206
+ const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
207
+ const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
208
+ const pageSizes = [25, 50, 100];
209
+
210
+ return `
211
+ <div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">
212
+ <div class="custom-scrollbar flex-1 overflow-auto">
213
+ ${renderDataGrid({
214
+ columns,
215
+ rows: table.rows ?? [],
216
+ tableClass: "min-w-full border-collapse text-left font-mono text-xs",
217
+ theadClass: "sticky top-0 z-10 bg-surface-container-highest",
218
+ tbodyClass: "divide-y divide-outline-variant/5",
219
+ getRowClass: (_, index) =>
220
+ `${
221
+ state.dataBrowser.selectedRowIndex === index
222
+ ? "bg-surface-bright"
223
+ : index % 2 === 0
224
+ ? "bg-surface-container-low"
225
+ : "bg-surface-container-lowest"
226
+ } cursor-pointer transition-colors hover:bg-surface-container-high`,
227
+ getRowAttrs: (_, index) => `data-action="select-data-row" data-row-index="${index}"`,
228
+ })}
229
+ ${
230
+ !table.rows?.length
231
+ ? `
232
+ <div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">
233
+ <p class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/40">
234
+ TABLE_IS_EMPTY
235
+ </p>
236
+ </div>
237
+ `
238
+ : ""
239
+ }
240
+ </div>
241
+ <footer class="flex flex-wrap items-center justify-between gap-4 border-t border-outline-variant/10 bg-surface-container px-6 py-4">
242
+ <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
243
+ showing ${escapeHtml(formatNumber(fromRow))}-${escapeHtml(formatNumber(toRow))} of ${escapeHtml(
244
+ formatNumber(totalRows)
245
+ )} rows
246
+ </div>
247
+ <div class="flex flex-wrap items-center gap-4">
248
+ <div class="flex items-center gap-2">
249
+ <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
250
+ rows
251
+ </span>
252
+ <div class="flex items-center gap-2">
253
+ ${pageSizes
254
+ .map(
255
+ (pageSize) => `
256
+ <button
257
+ class="border px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] transition-colors ${
258
+ pageSize === (table.limit ?? state.dataBrowser.pageSize)
259
+ ? "border-primary-container/30 bg-surface-container-high text-primary-container"
260
+ : "border-outline-variant/20 text-on-surface hover:bg-surface-container-highest"
261
+ }"
262
+ data-action="set-data-page-size"
263
+ data-page-size="${pageSize}"
264
+ type="button"
265
+ >
266
+ ${pageSize}
267
+ </button>
268
+ `
269
+ )
270
+ .join("")}
271
+ </div>
272
+ </div>
273
+ <div class="flex items-center gap-2">
274
+ <button
275
+ class="border border-outline-variant/20 px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:bg-surface-container-highest disabled:cursor-default disabled:opacity-30"
276
+ data-action="set-data-page"
277
+ data-page="${page - 1}"
278
+ type="button"
279
+ ${page <= 1 ? "disabled" : ""}
280
+ >
281
+ Prev
282
+ </button>
283
+ <div class="min-w-[7rem] text-center text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
284
+ page ${escapeHtml(formatNumber(page))} / ${escapeHtml(formatNumber(pageCount))}
285
+ </div>
286
+ <button
287
+ class="border border-outline-variant/20 px-3 py-2 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:bg-surface-container-highest disabled:cursor-default disabled:opacity-30"
288
+ data-action="set-data-page"
289
+ data-page="${page + 1}"
290
+ type="button"
291
+ ${page >= pageCount ? "disabled" : ""}
292
+ >
293
+ Next
294
+ </button>
295
+ </div>
296
+ </div>
297
+ </footer>
298
+ </div>
299
+ `;
300
+ }
301
+
302
+ function renderDataRowEditorPanel(state) {
303
+ const table = state.dataBrowser.table;
304
+ const rowIndex = state.dataBrowser.selectedRowIndex;
305
+ const row = getSelectedRow(state);
306
+
307
+ if (!table || !row || typeof rowIndex !== "number") {
308
+ return "";
309
+ }
310
+
311
+ const identityColumns =
312
+ table.identityStrategy?.type === "primaryKey" ? table.identityStrategy.columns ?? [] : [];
313
+ const editableColumns = (table.columnMeta ?? []).filter((column) => {
314
+ if (!column.visible || column.generated) {
315
+ return false;
316
+ }
317
+
318
+ if (identityColumns.includes(column.name)) {
319
+ return false;
320
+ }
321
+
322
+ const value = row[column.name];
323
+ if (isBlobPreview(value) || (value && typeof value === "object")) {
324
+ return false;
325
+ }
326
+
327
+ return true;
328
+ });
329
+ const readonlyColumns = (table.columnMeta ?? []).filter((column) => {
330
+ if (!column.visible) {
331
+ return false;
332
+ }
333
+
334
+ if (identityColumns.includes(column.name) || column.generated) {
335
+ return true;
336
+ }
337
+
338
+ const value = row[column.name];
339
+ return isBlobPreview(value) || (value && typeof value === "object");
340
+ });
341
+
342
+ return renderRowEditorPanel({
343
+ title: table.name,
344
+ sectionLabel: "Row Editor",
345
+ subtitle: `row ${rowIndex + 1}`,
346
+ closeAction: "clear-data-row-selection",
347
+ formName: "save-data-row",
348
+ hiddenFields: [{ name: "rowIndex", value: String(rowIndex) }],
349
+ disabledMessage: state.connections.active?.readOnly
350
+ ? "The active database is opened read-only, so row editing is disabled."
351
+ : table.notSafelyUpdatable
352
+ ? "This table has no stable identity column, so SQLite Hub cannot safely update rows."
353
+ : "",
354
+ editableFields: editableColumns.map((column) => {
355
+ const value = row[column.name];
356
+
357
+ return {
358
+ name: column.name,
359
+ label: column.name,
360
+ value: value === null || value === undefined ? "" : String(value),
361
+ };
362
+ }),
363
+ readonlyFields: readonlyColumns.map((column) => ({
364
+ name: column.name,
365
+ label: column.name,
366
+ value: formatCellValue(row[column.name]),
367
+ })),
368
+ saveError: state.dataBrowser.saveError,
369
+ saving: state.dataBrowser.saving,
370
+ reloadAction: "reload-data-route",
371
+ });
372
+ }
373
+
374
+ export function renderDataView(state) {
375
+ return {
376
+ main: `
377
+ <section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden">
378
+ <div class="grid h-full min-h-0 grid-cols-1 md:grid-cols-[18rem_minmax(0,1fr)]">
379
+ <aside class="flex min-h-0 flex-col border-r border-outline-variant/10 bg-surface-low">
380
+ <div class="border-b border-outline-variant/10 px-6 py-5">
381
+ <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
382
+ Tables
383
+ </div>
384
+ <div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
385
+ total ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}
386
+ </div>
387
+ </div>
388
+ ${renderTableList(state)}
389
+ </aside>
390
+ <section class="flex min-h-0 flex-col overflow-hidden">
391
+ ${renderWorkspaceHeader(state)}
392
+ ${renderWorkspaceError(state)}
393
+ ${renderTableSurface(state)}
394
+ </section>
395
+ </div>
396
+ </section>
397
+ `,
398
+ panel: renderDataRowEditorPanel(state),
399
+ };
400
+ }
@@ -0,0 +1,259 @@
1
+ import { renderBottomTabs } from "../components/bottomTabs.js";
2
+ import { renderQueryEditor } from "../components/queryEditor.js";
3
+ import { renderRowEditorPanel } from "../components/rowEditorPanel.js";
4
+ import { renderQueryResultsPane } from "../components/queryResults.js";
5
+ import { getCurrentConnection, getQueryMessages, getQueryPerformance } from "../store.js";
6
+ import {
7
+ escapeHtml,
8
+ formatCellValue,
9
+ formatNumber,
10
+ isBlobPreview,
11
+ } from "../utils/format.js";
12
+
13
+ function renderMissingDatabase() {
14
+ return `
15
+ <div class="flex flex-1 flex-col items-center justify-center bg-surface-container-lowest px-8 text-center">
16
+ <span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
17
+ <p class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
18
+ No Active SQLite Database
19
+ </p>
20
+ <p class="mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
21
+ Connect a local SQLite database before executing statements or exporting query results.
22
+ </p>
23
+ </div>
24
+ `;
25
+ }
26
+
27
+ function renderMessagesPane(state) {
28
+ const items = getQueryMessages(state);
29
+
30
+ return `
31
+ <div class="custom-scrollbar h-full overflow-auto bg-surface-container-lowest px-6 py-6">
32
+ <div class="space-y-4">
33
+ ${items
34
+ .map(
35
+ (item) => `
36
+ <div class="border border-outline-variant/10 bg-surface-container-low px-4 py-4">
37
+ <div class="text-[10px] font-mono uppercase tracking-[0.2em] ${
38
+ item.tone === "alert" ? "text-error" : "text-primary-container"
39
+ }">
40
+ ${escapeHtml(item.label)}
41
+ </div>
42
+ <div class="mt-2 text-sm text-on-surface">${escapeHtml(item.value)}</div>
43
+ </div>
44
+ `
45
+ )
46
+ .join("")}
47
+ </div>
48
+ </div>
49
+ `;
50
+ }
51
+
52
+ function renderPerformancePane(state) {
53
+ const metrics = getQueryPerformance(state);
54
+
55
+ return `
56
+ <div class="grid flex-1 grid-cols-1 gap-4 bg-surface-container-lowest p-6 md:grid-cols-4">
57
+ <div class="metric-card">
58
+ <span class="text-[10px] font-mono uppercase text-on-surface/40">Exec_Time</span>
59
+ <span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
60
+ String(metrics.timingMs ?? 0)
61
+ )}ms</span>
62
+ <span class="text-[10px] text-primary-container">Measured backend execution time</span>
63
+ </div>
64
+ <div class="metric-card">
65
+ <span class="text-[10px] font-mono uppercase text-on-surface/40">Statements</span>
66
+ <span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
67
+ formatNumber(metrics.statementCount)
68
+ )}</span>
69
+ <span class="text-[10px] text-on-surface/40">Split and executed by SQLite</span>
70
+ </div>
71
+ <div class="metric-card">
72
+ <span class="text-[10px] font-mono uppercase text-on-surface/40">Rows_Returned</span>
73
+ <span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
74
+ formatNumber(metrics.rowCount)
75
+ )}</span>
76
+ <span class="text-[10px] text-on-surface/40">Visible result set size</span>
77
+ </div>
78
+ <div class="metric-card metric-card--accent">
79
+ <span class="text-[10px] font-mono uppercase text-on-surface/40">Rows_Affected</span>
80
+ <span class="font-headline text-3xl font-bold text-on-surface">${escapeHtml(
81
+ formatNumber(metrics.affectedRowCount)
82
+ )}</span>
83
+ <span class="text-[10px] text-primary-container">INSERT / UPDATE / DELETE impact</span>
84
+ </div>
85
+ </div>
86
+ `;
87
+ }
88
+
89
+ function getResultEditingState(state) {
90
+ const editing = state.editor.result?.editing;
91
+
92
+ if (!editing) {
93
+ return {
94
+ enabled: false,
95
+ message: "",
96
+ };
97
+ }
98
+
99
+ if (state.connections.active?.readOnly) {
100
+ return {
101
+ enabled: false,
102
+ message: "The active database is opened read-only, so query result editing is disabled.",
103
+ };
104
+ }
105
+
106
+ if (!editing.enabled) {
107
+ return {
108
+ enabled: false,
109
+ message: editing.reason || "Only direct single-table SELECT results can be edited here.",
110
+ };
111
+ }
112
+
113
+ return {
114
+ enabled: true,
115
+ message: `Click a row to edit it in ${editing.tableName}.`,
116
+ };
117
+ }
118
+
119
+ function getUniqueResultColumns(columns = []) {
120
+ const uniqueColumns = [];
121
+ const seen = new Set();
122
+
123
+ columns.forEach((column) => {
124
+ if (!column?.sourceColumn || seen.has(column.sourceColumn)) {
125
+ return;
126
+ }
127
+
128
+ seen.add(column.sourceColumn);
129
+ uniqueColumns.push(column);
130
+ });
131
+
132
+ return uniqueColumns;
133
+ }
134
+
135
+ function renderEditorRowPanel(state) {
136
+ const result = state.editor.result;
137
+ const rowIndex = state.editor.selectedRowIndex;
138
+ const row = typeof rowIndex === "number" ? result?.rows?.[rowIndex] ?? null : null;
139
+
140
+ if (!result || !row || typeof rowIndex !== "number") {
141
+ return "";
142
+ }
143
+
144
+ const uniqueColumns = getUniqueResultColumns(result.editing?.columns ?? []);
145
+ const editableColumns = uniqueColumns.filter((column) => {
146
+ if (column.identity || column.generated || !column.visible) {
147
+ return false;
148
+ }
149
+
150
+ const value = row[column.resultName];
151
+ if (isBlobPreview(value) || (value && typeof value === "object")) {
152
+ return false;
153
+ }
154
+
155
+ return true;
156
+ });
157
+ const readonlyColumns = uniqueColumns.filter((column) => {
158
+ if (!column.visible) {
159
+ return false;
160
+ }
161
+
162
+ if (column.identity || column.generated) {
163
+ return true;
164
+ }
165
+
166
+ const value = row[column.resultName];
167
+ return isBlobPreview(value) || (value && typeof value === "object");
168
+ });
169
+ const editingState = getResultEditingState(state);
170
+
171
+ return renderRowEditorPanel({
172
+ title: result.editing?.tableName ?? "Query Result",
173
+ sectionLabel: "Row Editor",
174
+ subtitle: `query row ${rowIndex + 1}`,
175
+ closeAction: "clear-editor-row-selection",
176
+ formName: "save-editor-row",
177
+ hiddenFields: [{ name: "rowIndex", value: String(rowIndex) }],
178
+ disabledMessage: editingState.enabled ? "" : editingState.message,
179
+ editableFields: editableColumns.map((column) => {
180
+ const value = row[column.resultName];
181
+
182
+ return {
183
+ name: column.sourceColumn,
184
+ label: column.sourceColumn,
185
+ value: value === null || value === undefined ? "" : String(value),
186
+ };
187
+ }),
188
+ readonlyFields: readonlyColumns.map((column) => ({
189
+ name: column.sourceColumn,
190
+ label: column.sourceColumn,
191
+ value: formatCellValue(row[column.resultName]),
192
+ })),
193
+ saveError: state.editor.saveError,
194
+ saving: state.editor.saving,
195
+ });
196
+ }
197
+
198
+ function renderResultsSurface(state, isResultsRoute) {
199
+ const activeTab = state.editor.activeTab;
200
+ const counts = {
201
+ resultRows: state.editor.result?.rows?.length ?? 0,
202
+ messages: getQueryMessages(state).length,
203
+ statementCount: state.editor.result?.statementCount ?? 0,
204
+ };
205
+
206
+ let content = renderMessagesPane(state);
207
+
208
+ if (activeTab === "performance") {
209
+ content = renderPerformancePane(state);
210
+ } else if (activeTab === "results") {
211
+ const editingState = getResultEditingState(state);
212
+
213
+ content = state.connections.active
214
+ ? renderQueryResultsPane(state.editor.result, {
215
+ exporting: state.editor.exportLoading,
216
+ selectedRowIndex: state.editor.selectedRowIndex,
217
+ editable: editingState.enabled,
218
+ editStatusMessage: editingState.message,
219
+ })
220
+ : renderMissingDatabase();
221
+ }
222
+
223
+ return `
224
+ <div class="flex h-full min-h-0 flex-col border-t border-outline-variant/10 bg-surface-container-lowest">
225
+ ${renderBottomTabs(activeTab, counts)}
226
+ <div class="min-h-0 flex-1">${content}</div>
227
+ </div>
228
+ `;
229
+ }
230
+
231
+ export function renderEditorView(state, { isResultsRoute = false } = {}) {
232
+ const connection = getCurrentConnection(state);
233
+ const editorSectionClass = isResultsRoute ? "h-1/4" : "min-h-[27.5%]";
234
+ const resultsSectionClass = isResultsRoute ? "h-3/4" : "flex-1";
235
+
236
+ return {
237
+ main: `
238
+ <section class="view-surface flex h-full min-h-0 flex-col overflow-hidden">
239
+ <div class="flex h-full min-h-0 flex-1 flex-col">
240
+ <section class="${editorSectionClass} flex min-h-0 flex-col ${
241
+ isResultsRoute ? "border-b-4 border-background" : ""
242
+ }">
243
+ ${renderQueryEditor({
244
+ query: state.editor.sqlText,
245
+ executing: state.editor.executing,
246
+ history: state.editor.history,
247
+ historyLoading: state.editor.historyLoading,
248
+ title: connection?.label ?? "SQLite Query Workspace",
249
+ })}
250
+ </section>
251
+ <section class="${resultsSectionClass} flex min-h-0 flex-col overflow-hidden">
252
+ ${renderResultsSurface(state, isResultsRoute)}
253
+ </section>
254
+ </div>
255
+ </section>
256
+ `,
257
+ panel: isResultsRoute ? renderEditorRowPanel(state) : "",
258
+ };
259
+ }
@@ -0,0 +1,11 @@
1
+ import { renderEmptyState } from "../components/emptyState.js";
2
+
3
+ export function renderLandingView(state) {
4
+ return {
5
+ main: renderEmptyState({
6
+ activeConnection: state.connections.active,
7
+ recentConnections: state.connections.recent,
8
+ }),
9
+ panel: "",
10
+ };
11
+ }