sqlite-hub 0.4.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 (56) hide show
  1. package/README.md +2 -2
  2. package/changelog.md +15 -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 +114 -5
  12. package/frontend/js/app.js +368 -18
  13. package/frontend/js/components/bottomTabs.js +1 -1
  14. package/frontend/js/components/dataGrid.js +3 -3
  15. package/frontend/js/components/queryEditor.js +33 -55
  16. package/frontend/js/components/queryHistoryDetail.js +263 -0
  17. package/frontend/js/components/queryHistoryPanel.js +228 -0
  18. package/frontend/js/components/queryResults.js +32 -46
  19. package/frontend/js/components/rowEditorPanel.js +73 -14
  20. package/frontend/js/components/sidebar.js +1 -0
  21. package/frontend/js/components/tableDesignerEditor.js +356 -0
  22. package/frontend/js/components/tableDesignerSidebar.js +126 -0
  23. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  24. package/frontend/js/router.js +10 -0
  25. package/frontend/js/store.js +841 -22
  26. package/frontend/js/utils/format.js +23 -0
  27. package/frontend/js/utils/tableDesigner.js +1192 -0
  28. package/frontend/js/views/data.js +273 -250
  29. package/frontend/js/views/editor.js +34 -10
  30. package/frontend/js/views/overview.js +15 -0
  31. package/frontend/js/views/tableDesigner.js +37 -0
  32. package/frontend/styles/base.css +87 -73
  33. package/frontend/styles/components.css +841 -188
  34. package/frontend/styles/views.css +40 -0
  35. package/package.json +1 -1
  36. package/server/routes/data.js +2 -0
  37. package/server/routes/export.js +4 -1
  38. package/server/routes/overview.js +12 -0
  39. package/server/routes/sql.js +163 -5
  40. package/server/routes/tableDesigner.js +60 -0
  41. package/server/server.js +5 -1
  42. package/server/services/sqlite/dataBrowserService.js +4 -16
  43. package/server/services/sqlite/exportService.js +4 -16
  44. package/server/services/sqlite/overviewService.js +34 -0
  45. package/server/services/sqlite/sqlExecutor.js +83 -63
  46. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  47. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  48. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  49. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  50. package/server/services/sqlite/tableDesignerService.js +181 -0
  51. package/server/services/sqlite/tableSort.js +63 -0
  52. package/server/services/storage/appStateStore.js +674 -1
  53. package/server/services/storage/queryHistoryUtils.js +169 -0
  54. package/frontend/assets/mockups/data_edit.png +0 -0
  55. package/frontend/assets/mockups/graph_visualize.png +0 -0
  56. package/frontend/assets/mockups/overview.png +0 -0
@@ -9,23 +9,6 @@ function renderLineNumbers(query) {
9
9
  .join("");
10
10
  }
11
11
 
12
- function renderHistoryOptions(history) {
13
- if (!history.length) {
14
- return '<option value="">No recent statements</option>';
15
- }
16
-
17
- return [
18
- '<option value="">Load recent statement...</option>',
19
- ...history.map(
20
- (entry) => `
21
- <option value="${escapeHtml(entry.id)}">
22
- ${escapeHtml(entry.sql.replace(/\s+/g, " ").slice(0, 96))}
23
- </option>
24
- `
25
- ),
26
- ].join("");
27
- }
28
-
29
12
  function renderHighlightedQuery(query) {
30
13
  if (query) {
31
14
  return highlightSql(query);
@@ -34,13 +17,41 @@ function renderHighlightedQuery(query) {
34
17
  return '<span class="text-on-surface-variant/35">SELECT name FROM sqlite_master WHERE type = \'table\';</span>';
35
18
  }
36
19
 
20
+ function renderEditorSurface({ query }) {
21
+ return `
22
+ <div class="flex min-h-0 flex-1 overflow-hidden">
23
+ <div class="flex w-12 flex-col items-center bg-surface-container-lowest py-4 font-mono text-xs select-none text-outline-variant/30">
24
+ ${renderLineNumbers(query)}
25
+ </div>
26
+ <div class="relative flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
27
+ <div class="pointer-events-none absolute right-0 top-0 p-4 opacity-5">
28
+ <span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
29
+ </div>
30
+ <div class="query-editor-layer relative z-10 h-full min-h-[140px]">
31
+ <div
32
+ aria-hidden="true"
33
+ class="query-editor-highlight"
34
+ data-query-editor-highlight
35
+ >${renderHighlightedQuery(query)}</div>
36
+ <textarea
37
+ class="query-editor-input custom-scrollbar relative z-10 h-full min-h-[140px] w-full resize-none border-none focus:ring-0"
38
+ data-bind="current-query"
39
+ placeholder="SELECT name FROM sqlite_master WHERE type = 'table';"
40
+ spellcheck="false"
41
+ >${escapeHtml(query)}</textarea>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ `;
46
+ }
47
+
37
48
  export function renderQueryEditor({
38
49
  query,
39
50
  title,
40
51
  executing = false,
41
52
  exporting = false,
42
- history = [],
43
53
  historyLoading = false,
54
+ historyTotal = 0,
44
55
  }) {
45
56
  const left = `
46
57
  <div class="flex items-center gap-2 bg-surface-container-lowest px-3 py-1">
@@ -51,24 +62,11 @@ export function renderQueryEditor({
51
62
  </div>
52
63
  <div class="hidden items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40 md:flex">
53
64
  <span class="material-symbols-outlined text-xs">history</span>
54
- ${historyLoading ? "Loading history..." : `${history.length} statements in history`}
65
+ ${historyLoading ? "Loading history..." : `${historyTotal} queries tracked`}
55
66
  </div>
56
67
  `;
57
68
 
58
69
  const right = `
59
- <select
60
- class="min-w-[220px] border border-outline-variant/20 bg-surface-container-lowest px-3 py-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant outline-none"
61
- data-bind="history-entry"
62
- >
63
- ${renderHistoryOptions(history)}
64
- </select>
65
- <button
66
- class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
67
- data-action="clear-sql-history"
68
- type="button"
69
- >
70
- Clear History
71
- </button>
72
70
  <button
73
71
  class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
74
72
  data-action="clear-query"
@@ -93,7 +91,7 @@ export function renderQueryEditor({
93
91
  `;
94
92
 
95
93
  return `
96
- <div class="flex h-full flex-col">
94
+ <div class="flex h-full min-h-0 flex-col">
97
95
  <div class="bg-surface-container-low px-6 py-3">
98
96
  ${renderActionBar({
99
97
  left,
@@ -101,28 +99,8 @@ export function renderQueryEditor({
101
99
  className: "flex-wrap",
102
100
  })}
103
101
  </div>
104
- <div class="flex flex-1 overflow-hidden">
105
- <div class="flex w-12 flex-col items-center bg-surface-container-lowest py-4 font-mono text-xs select-none text-outline-variant/30">
106
- ${renderLineNumbers(query)}
107
- </div>
108
- <div class="relative flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
109
- <div class="pointer-events-none absolute right-0 top-0 p-4 opacity-5">
110
- <span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
111
- </div>
112
- <div class="query-editor-layer relative z-10 h-full min-h-[140px]">
113
- <pre
114
- aria-hidden="true"
115
- class="query-editor-highlight"
116
- data-query-editor-highlight
117
- >${renderHighlightedQuery(query)}</pre>
118
- <textarea
119
- class="query-editor-input custom-scrollbar relative z-10 h-full min-h-[140px] w-full resize-none border-none focus:ring-0"
120
- data-bind="current-query"
121
- placeholder="SELECT name FROM sqlite_master WHERE type = 'table';"
122
- spellcheck="false"
123
- >${escapeHtml(query)}</textarea>
124
- </div>
125
- </div>
102
+ <div class="flex min-h-0 flex-1 flex-col">
103
+ ${renderEditorSurface({ query })}
126
104
  </div>
127
105
  </div>
128
106
  `;
@@ -0,0 +1,263 @@
1
+ import {
2
+ escapeHtml,
3
+ formatCompactDateTime,
4
+ formatDateTime,
5
+ formatDurationMs,
6
+ formatNumber,
7
+ highlightSql,
8
+ } from "../utils/format.js";
9
+ import { renderStatusBadge } from "./badges.js";
10
+
11
+ function renderDetailMetaItem(label, value) {
12
+ return `
13
+ <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
14
+ <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
15
+ ${escapeHtml(label)}
16
+ </div>
17
+ <div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>
18
+ </div>
19
+ `;
20
+ }
21
+
22
+ function renderRunItem(run) {
23
+ return `
24
+ <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
25
+ <div class="flex flex-wrap items-center gap-2">
26
+ ${renderStatusBadge(run.status, run.status === "error" ? "alert" : "success")}
27
+ <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
28
+ ${escapeHtml(formatCompactDateTime(run.executedAt))}
29
+ </span>
30
+ <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
31
+ ${escapeHtml(formatDurationMs(run.durationMs))}
32
+ </span>
33
+ <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
34
+ rows ${escapeHtml(formatNumber(run.rowCount ?? 0))}
35
+ </span>
36
+ <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
37
+ affected ${escapeHtml(formatNumber(run.affectedRows ?? 0))}
38
+ </span>
39
+ </div>
40
+ ${
41
+ run.errorMessage
42
+ ? `
43
+ <div class="mt-3 border border-error/20 bg-error-container/20 px-3 py-3 text-sm leading-6 text-error">
44
+ ${escapeHtml(run.errorMessage)}
45
+ </div>
46
+ `
47
+ : ""
48
+ }
49
+ </div>
50
+ `;
51
+ }
52
+
53
+ export function renderQueryHistoryDetail({
54
+ item = null,
55
+ runs = [],
56
+ loading = false,
57
+ error = null,
58
+ }) {
59
+ if (!item && !loading && !error) {
60
+ return "";
61
+ }
62
+
63
+ if (loading) {
64
+ return `
65
+ <section class="flex h-full min-h-0 flex-col bg-surface-low">
66
+ <div class="border-b border-outline-variant/10 px-5 py-4">
67
+ <div class="flex items-center justify-between gap-3">
68
+ <span class="font-headline text-sm font-black uppercase tracking-[0.18em] text-primary-container">
69
+ Query Detail
70
+ </span>
71
+ <button
72
+ class="query-history-icon-button"
73
+ data-action="clear-query-history-selection"
74
+ type="button"
75
+ >
76
+ <span class="material-symbols-outlined text-[18px]">close</span>
77
+ </button>
78
+ </div>
79
+ </div>
80
+ <div class="flex flex-1 items-center justify-center px-6 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
81
+ Loading query detail...
82
+ </div>
83
+ </section>
84
+ `;
85
+ }
86
+
87
+ if (error) {
88
+ return `
89
+ <section class="flex h-full min-h-0 flex-col bg-surface-low">
90
+ <div class="border-b border-outline-variant/10 px-5 py-4">
91
+ <div class="flex items-center justify-between gap-3">
92
+ <span class="font-headline text-sm font-black uppercase tracking-[0.18em] text-primary-container">
93
+ Query Detail
94
+ </span>
95
+ <button
96
+ class="query-history-icon-button"
97
+ data-action="clear-query-history-selection"
98
+ type="button"
99
+ >
100
+ <span class="material-symbols-outlined text-[18px]">close</span>
101
+ </button>
102
+ </div>
103
+ </div>
104
+ <div class="p-5">
105
+ <div class="border border-error/30 bg-error-container/20 px-4 py-4 text-sm text-error">
106
+ ${escapeHtml(error.message)}
107
+ </div>
108
+ </div>
109
+ </section>
110
+ `;
111
+ }
112
+
113
+ return `
114
+ <section class="flex h-full min-h-0 flex-col bg-surface-low">
115
+ <div class="border-b border-outline-variant/10 px-5 py-4">
116
+ <div class="flex items-center justify-between gap-3">
117
+ <div>
118
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
119
+ Query Detail
120
+ </div>
121
+ <h2 class="mt-1 font-headline text-lg font-black uppercase tracking-tight text-on-surface">
122
+ ${escapeHtml(item.displayTitle)}
123
+ </h2>
124
+ </div>
125
+ <button
126
+ class="query-history-icon-button"
127
+ data-action="clear-query-history-selection"
128
+ type="button"
129
+ >
130
+ <span class="material-symbols-outlined text-[18px]">close</span>
131
+ </button>
132
+ </div>
133
+ <div class="mt-4 flex flex-wrap gap-2">
134
+ ${renderStatusBadge(item.queryType, item.isDestructive ? "alert" : "primary")}
135
+ ${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
136
+ ${item.isDestructive ? renderStatusBadge("destructive", "alert") : ""}
137
+ ${item.lastRun ? renderStatusBadge(item.lastRun.status, item.lastRun.status === "error" ? "alert" : "success") : ""}
138
+ </div>
139
+ </div>
140
+ <div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-5 py-5">
141
+ <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
142
+ ${renderDetailMetaItem("Last Used", formatDateTime(item.lastUsedAt))}
143
+ ${renderDetailMetaItem("First Executed", formatDateTime(item.firstExecutedAt))}
144
+ ${renderDetailMetaItem("Use Count", formatNumber(item.useCount))}
145
+ ${renderDetailMetaItem(
146
+ "Tables",
147
+ item.tablesDetected?.length ? item.tablesDetected.join(", ") : "None detected"
148
+ )}
149
+ </div>
150
+
151
+ <form class="mt-5" data-form="save-query-history-title">
152
+ <input name="historyId" type="hidden" value="${escapeHtml(item.id)}" />
153
+ <label class="block text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
154
+ Custom Title
155
+ </label>
156
+ <div class="mt-2 flex gap-2">
157
+ <input
158
+ class="flex-1 border border-outline-variant/20 bg-surface-container px-3 py-2 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
159
+ name="title"
160
+ placeholder="${escapeHtml(item.displayTitle)}"
161
+ type="text"
162
+ value="${escapeHtml(item.title ?? "")}"
163
+ />
164
+ <button
165
+ class="border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
166
+ type="submit"
167
+ >
168
+ Save
169
+ </button>
170
+ </div>
171
+ <p class="mt-2 text-xs text-on-surface-variant/60">
172
+ Leave empty to fall back to the auto title generated from the query.
173
+ </p>
174
+ </form>
175
+
176
+ <div class="mt-5 flex flex-wrap gap-2">
177
+ <button
178
+ class="toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
179
+ data-action="open-query-history"
180
+ data-history-id="${escapeHtml(item.id)}"
181
+ type="button"
182
+ >
183
+ Open In Editor
184
+ </button>
185
+ <button
186
+ class="toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
187
+ data-action="run-query-history"
188
+ data-history-id="${escapeHtml(item.id)}"
189
+ type="button"
190
+ >
191
+ Run Now
192
+ </button>
193
+ <button
194
+ class="toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
195
+ data-action="toggle-query-history-saved"
196
+ data-history-id="${escapeHtml(item.id)}"
197
+ data-next-value="${item.isSaved ? "false" : "true"}"
198
+ type="button"
199
+ >
200
+ ${item.isSaved ? "Unsave" : "Save"}
201
+ </button>
202
+ <button
203
+ class="toolbar-button border border-error/25 bg-error-container/20 px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-error transition-colors hover:border-error hover:bg-error-container/30"
204
+ data-action="delete-query-history"
205
+ data-history-id="${escapeHtml(item.id)}"
206
+ type="button"
207
+ >
208
+ Delete
209
+ </button>
210
+ </div>
211
+
212
+ <div class="mt-6">
213
+ <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
214
+ SQL
215
+ </div>
216
+ <pre class="query-history-detail-sql custom-scrollbar mt-2 overflow-auto border border-outline-variant/10 bg-surface-container-lowest p-4 font-mono text-sm leading-6 text-on-surface"><code>${highlightSql(
217
+ item.rawSql
218
+ )}</code></pre>
219
+ </div>
220
+
221
+ <form class="mt-6" data-form="save-query-history-notes">
222
+ <input name="historyId" type="hidden" value="${escapeHtml(item.id)}" />
223
+ <label class="block text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
224
+ Notes
225
+ </label>
226
+ <textarea class="custom-scrollbar mt-2 min-h-[120px] w-full resize-y border border-outline-variant/20 bg-surface-container px-3 py-3 text-sm leading-6 text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container" name="notes" placeholder="Add context, caveats, or why this query matters...">${escapeHtml(
227
+ item.notes ?? ""
228
+ )}</textarea>
229
+ <div class="mt-2 flex justify-end">
230
+ <button
231
+ class="border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
232
+ type="submit"
233
+ >
234
+ Save Notes
235
+ </button>
236
+ </div>
237
+ </form>
238
+
239
+ <div class="mt-6">
240
+ <div class="flex items-center justify-between gap-3">
241
+ <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
242
+ Latest Runs
243
+ </div>
244
+ <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/40">
245
+ ${escapeHtml(formatNumber(runs.length))}
246
+ </div>
247
+ </div>
248
+ <div class="mt-3 space-y-3">
249
+ ${
250
+ runs.length
251
+ ? runs.map((run) => renderRunItem(run)).join("")
252
+ : `
253
+ <div class="border border-outline-variant/10 bg-surface-container px-3 py-4 text-sm text-on-surface-variant/65">
254
+ No execution runs recorded yet.
255
+ </div>
256
+ `
257
+ }
258
+ </div>
259
+ </div>
260
+ </div>
261
+ </section>
262
+ `;
263
+ }
@@ -0,0 +1,228 @@
1
+ import {
2
+ escapeHtml,
3
+ formatNumber,
4
+ } from "../utils/format.js";
5
+ import { renderStatusBadge } from "./badges.js";
6
+
7
+ function getQueryTypeTone(queryType, isDestructive) {
8
+ if (isDestructive) {
9
+ return "alert";
10
+ }
11
+
12
+ if (queryType === "select") {
13
+ return "success";
14
+ }
15
+
16
+ if (queryType === "pragma") {
17
+ return "primary";
18
+ }
19
+
20
+ return "muted";
21
+ }
22
+
23
+ function renderHistoryItem(item, activeHistoryId, selectedHistoryId) {
24
+ const isActive = Number(activeHistoryId) === Number(item.id);
25
+ const isSelected = Number(selectedHistoryId) === Number(item.id);
26
+ const visibleTables = (item.tablesDetected ?? []).slice(0, 3);
27
+
28
+ return `
29
+ <article class="query-history-item ${isActive ? "is-active" : ""} ${
30
+ item.lastRun?.status === "error" ? "is-error" : ""
31
+ }">
32
+ <button
33
+ class="query-history-item-hit ${isActive ? "is-active" : ""}"
34
+ data-action="select-query-history-item"
35
+ data-history-id="${escapeHtml(item.id)}"
36
+ type="button"
37
+ >
38
+ <div class="flex flex-wrap items-center gap-2">
39
+ <span class="truncate font-headline text-sm font-bold uppercase tracking-tight text-on-surface">
40
+ ${escapeHtml(item.displayTitle)}
41
+ </span>
42
+ ${renderStatusBadge(item.queryType, getQueryTypeTone(item.queryType, item.isDestructive))}
43
+ ${item.isSaved ? renderStatusBadge("saved", "primary") : ""}
44
+ ${item.isDestructive ? renderStatusBadge("destructive", "alert") : ""}
45
+ </div>
46
+ <p class="query-history-sql-preview mt-2 text-left font-mono text-xs leading-5 text-on-surface-variant/75">
47
+ ${escapeHtml(item.previewSql)}
48
+ </p>
49
+ </button>
50
+ <div class="flex items-center justify-between gap-3 border-t border-outline-variant/10 px-3 pb-3 pt-2">
51
+ <div class="min-w-0 flex flex-wrap gap-2 text-[10px] font-mono uppercase tracking-[0.14em] text-on-surface-variant/55">
52
+ ${visibleTables
53
+ .map(
54
+ (tableName) => `
55
+ <span class="border border-outline-variant/20 bg-surface-highest px-2 py-1">
56
+ ${escapeHtml(tableName)}
57
+ </span>
58
+ `
59
+ )
60
+ .join("")}
61
+ </div>
62
+ <div class="flex items-center gap-1">
63
+ <button
64
+ class="query-history-icon-button"
65
+ data-action="open-query-history"
66
+ data-history-id="${escapeHtml(item.id)}"
67
+ title="Open in editor"
68
+ type="button"
69
+ >
70
+ <span class="material-symbols-outlined text-[18px]">edit_note</span>
71
+ </button>
72
+ <button
73
+ class="query-history-icon-button"
74
+ data-action="run-query-history"
75
+ data-history-id="${escapeHtml(item.id)}"
76
+ title="Run query"
77
+ type="button"
78
+ >
79
+ <span class="material-symbols-outlined text-[18px]">play_arrow</span>
80
+ </button>
81
+ <button
82
+ class="query-history-icon-button ${item.isSaved ? "is-active" : ""}"
83
+ data-action="toggle-query-history-saved"
84
+ data-history-id="${escapeHtml(item.id)}"
85
+ data-next-value="${item.isSaved ? "false" : "true"}"
86
+ title="${item.isSaved ? "Remove from saved" : "Save query"}"
87
+ type="button"
88
+ >
89
+ <span class="material-symbols-outlined text-[18px]">
90
+ ${item.isSaved ? "bookmark" : "bookmark_add"}
91
+ </span>
92
+ </button>
93
+ <button
94
+ class="query-history-icon-button ${isSelected ? "is-active" : ""}"
95
+ data-action="select-query-history-item"
96
+ data-history-id="${escapeHtml(item.id)}"
97
+ title="Open query detail"
98
+ type="button"
99
+ >
100
+ <span class="material-symbols-outlined text-[18px]">info</span>
101
+ </button>
102
+ </div>
103
+ </div>
104
+ </article>
105
+ `;
106
+ }
107
+
108
+ function renderQueryHistoryTabs(activeTab, historyTotal) {
109
+ const tabs = [
110
+ { id: "recent", label: "Recent" },
111
+ { id: "saved", label: "Saved" },
112
+ { id: "failed", label: "Failed" },
113
+ ];
114
+
115
+ return `
116
+ <div class="flex items-center gap-2">
117
+ ${tabs
118
+ .map(
119
+ (tab) => `
120
+ <button
121
+ class="query-history-tab ${activeTab === tab.id ? "is-active" : ""}"
122
+ data-action="set-query-history-tab"
123
+ data-tab="${tab.id}"
124
+ type="button"
125
+ >
126
+ ${escapeHtml(tab.label)}
127
+ </button>
128
+ `
129
+ )
130
+ .join("")}
131
+ <span class="ml-auto text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/50">
132
+ ${escapeHtml(formatNumber(historyTotal))}
133
+ </span>
134
+ </div>
135
+ `;
136
+ }
137
+
138
+ export function renderQueryHistoryPanel({
139
+ items = [],
140
+ loading = false,
141
+ loadingMore = false,
142
+ error = null,
143
+ activeTab = "recent",
144
+ search = "",
145
+ total = 0,
146
+ hasMore = false,
147
+ activeHistoryId = null,
148
+ selectedHistoryId = null,
149
+ }) {
150
+ return `
151
+ <aside class="query-history-panel border-l border-outline-variant/10 bg-surface-container-lowest">
152
+ <div class="border-b border-outline-variant/10 px-4 py-4">
153
+ <div class="flex items-center gap-2">
154
+ <span class="material-symbols-outlined text-[18px] text-primary-container">history</span>
155
+ <span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
156
+ Query History
157
+ </span>
158
+ </div>
159
+ <div class="mt-4">${renderQueryHistoryTabs(activeTab, total)}</div>
160
+ <label class="mt-4 block">
161
+ <span class="sr-only">Search query history</span>
162
+ <input
163
+ class="w-full border border-outline-variant/20 bg-surface-container px-3 py-2 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
164
+ data-bind="query-history-search"
165
+ placeholder="Search SQL, titles, notes..."
166
+ type="search"
167
+ value="${escapeHtml(search)}"
168
+ />
169
+ </label>
170
+ </div>
171
+ <div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-3 py-3">
172
+ ${
173
+ error
174
+ ? `
175
+ <div class="border border-error/30 bg-error-container/20 px-4 py-3 text-sm text-error">
176
+ ${escapeHtml(error.message)}
177
+ </div>
178
+ `
179
+ : ""
180
+ }
181
+ ${
182
+ !loading && !items.length
183
+ ? `
184
+ <div class="flex h-full min-h-[240px] flex-col items-center justify-center px-6 text-center">
185
+ <span class="material-symbols-outlined text-4xl text-on-surface-variant/25">manage_search</span>
186
+ <p class="mt-4 font-headline text-lg font-bold uppercase tracking-tight text-on-surface">
187
+ No Matching Queries
188
+ </p>
189
+ <p class="mt-2 max-w-xs text-sm leading-6 text-on-surface-variant/65">
190
+ Executed statements will appear here once they run against the active database.
191
+ </p>
192
+ </div>
193
+ `
194
+ : ""
195
+ }
196
+ <div class="space-y-3">
197
+ ${items
198
+ .map((item) => renderHistoryItem(item, activeHistoryId, selectedHistoryId))
199
+ .join("")}
200
+ </div>
201
+ ${
202
+ loading
203
+ ? `
204
+ <div class="mt-4 text-center text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/50">
205
+ Loading query history...
206
+ </div>
207
+ `
208
+ : ""
209
+ }
210
+ ${
211
+ hasMore
212
+ ? `
213
+ <div class="mt-4 flex justify-center">
214
+ <button
215
+ class="border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant transition-colors hover:border-primary-container hover:text-primary-container"
216
+ data-action="load-more-query-history"
217
+ type="button"
218
+ >
219
+ ${loadingMore ? "Loading..." : "Load More"}
220
+ </button>
221
+ </div>
222
+ `
223
+ : ""
224
+ }
225
+ </div>
226
+ </aside>
227
+ `;
228
+ }