sqlite-hub 1.0.0 → 1.1.1

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 (47) hide show
  1. package/README.md +47 -33
  2. package/bin/sqlite-hub.js +127 -47
  3. package/frontend/js/api.js +6 -0
  4. package/frontend/js/app.js +229 -44
  5. package/frontend/js/components/modal.js +17 -1
  6. package/frontend/js/components/queryChartRenderer.js +28 -3
  7. package/frontend/js/components/queryEditor.js +24 -26
  8. package/frontend/js/components/queryHistoryDetail.js +1 -1
  9. package/frontend/js/components/queryHistoryHeader.js +48 -0
  10. package/frontend/js/components/queryHistoryList.js +132 -0
  11. package/frontend/js/components/queryHistoryPanel.js +72 -136
  12. package/frontend/js/components/structureGraph.js +700 -89
  13. package/frontend/js/components/tableDesignerEditor.js +26 -47
  14. package/frontend/js/components/tableDesignerSidebar.js +7 -31
  15. package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
  16. package/frontend/js/store.js +97 -8
  17. package/frontend/js/utils/exportFilenames.js +3 -1
  18. package/frontend/js/views/charts.js +326 -174
  19. package/frontend/js/views/connections.js +59 -64
  20. package/frontend/js/views/data.js +48 -54
  21. package/frontend/js/views/documents.js +62 -23
  22. package/frontend/js/views/editor.js +0 -2
  23. package/frontend/js/views/mediaTagging.js +6 -5
  24. package/frontend/js/views/settings.js +78 -0
  25. package/frontend/js/views/structure.js +48 -32
  26. package/frontend/js/views/tableDesigner.js +45 -1
  27. package/frontend/styles/components.css +259 -54
  28. package/frontend/styles/structure-graph.css +150 -37
  29. package/frontend/styles/tailwind.generated.css +1 -1
  30. package/frontend/styles/tokens.css +2 -0
  31. package/frontend/styles/views.css +75 -38
  32. package/package.json +3 -2
  33. package/server/routes/export.js +89 -22
  34. package/server/routes/externalApi.js +84 -2
  35. package/server/routes/settings.js +37 -28
  36. package/server/services/appInfoService.js +215 -0
  37. package/server/services/databaseCommandService.js +50 -6
  38. package/server/services/sqlite/dataBrowserService.js +11 -3
  39. package/server/services/sqlite/exportService.js +307 -22
  40. package/tests/api-token-auth.test.js +110 -1
  41. package/tests/cli-args.test.js +16 -3
  42. package/tests/database-command-service.test.js +40 -3
  43. package/tests/export-blob.test.js +54 -1
  44. package/tests/export-filenames.test.js +4 -0
  45. package/tests/settings-api-tokens-route.test.js +22 -1
  46. package/tests/settings-metadata.test.js +99 -1
  47. package/tests/settings-view.test.js +28 -0
@@ -1,7 +1,15 @@
1
1
  import { renderQueryResultsPane } from '../components/queryResults.js';
2
2
  import { analyzeQueryChartResult, getQueryChartTypeLabel, validateQueryChartConfig } from '../lib/queryCharts.js';
3
- import { escapeHtml, highlightSql } from '../utils/format.js';
3
+ import { escapeHtml, formatNumber, highlightSql } from '../utils/format.js';
4
4
  import { renderStatusBadge } from '../components/badges.js';
5
+ import { renderQueryHistoryHeader, renderQueryHistorySearch } from '../components/queryHistoryHeader.js';
6
+ import {
7
+ renderQueryHistoryActionGroup,
8
+ renderQueryHistoryBadgeRow,
9
+ renderQueryHistoryIconButton,
10
+ renderQueryHistoryListItem,
11
+ renderQueryHistoryTabs,
12
+ } from '../components/queryHistoryList.js';
5
13
 
6
14
  function renderMissingDatabase() {
7
15
  return `
@@ -19,11 +27,101 @@ function renderMissingDatabase() {
19
27
  `;
20
28
  }
21
29
 
22
- function renderChartsList(state) {
23
- const activeTab = ['recent', 'saved'].includes(state.charts.historyTab) ? state.charts.historyTab : 'recent';
30
+ function normalizeChartsHistorySearchText(value) {
31
+ return String(value ?? '')
32
+ .trim()
33
+ .toLowerCase();
34
+ }
35
+
36
+ function getChartsHistorySearchHaystack(item) {
37
+ return [
38
+ item.displayTitle,
39
+ item.title,
40
+ item.previewSql,
41
+ item.rawSql,
42
+ item.chartTypes?.join(' '),
43
+ item.isSaved ? 'saved' : '',
44
+ ]
45
+ .filter(Boolean)
46
+ .join(' ')
47
+ .toLowerCase();
48
+ }
49
+
50
+ function getFilteredChartsQueries(state) {
51
+ const activeTab = ['recent', 'saved', 'unsaved'].includes(state.charts.historyTab)
52
+ ? state.charts.historyTab
53
+ : 'recent';
54
+ const search = normalizeChartsHistorySearchText(state.charts.historySearch);
24
55
  const allQueries = state.charts.queries ?? [];
25
- const queries = activeTab === 'saved' ? allQueries.filter(item => item.isSaved) : allQueries;
56
+ const tabQueries =
57
+ activeTab === 'saved'
58
+ ? allQueries.filter(item => item.isSaved)
59
+ : activeTab === 'unsaved'
60
+ ? allQueries.filter(item => !item.isSaved)
61
+ : allQueries;
62
+
63
+ if (!search) {
64
+ return tabQueries;
65
+ }
66
+
67
+ return tabQueries.filter(item => getChartsHistorySearchHaystack(item).includes(search));
68
+ }
69
+
70
+ function renderChartsHistoryListItem(item, state) {
26
71
  const selectedHistoryId = Number(state.charts.selectedHistoryId);
72
+ const isActive = selectedHistoryId === Number(item.id);
73
+ const savedBadgeMarkup = `
74
+ <span
75
+ data-charts-saved-badge
76
+ data-history-id="${escapeHtml(item.id)}"
77
+ ${item.isSaved ? '' : 'hidden'}
78
+ >
79
+ ${renderStatusBadge('saved', 'primary')}
80
+ </span>
81
+ `;
82
+ const chartBadgesMarkup = item.chartTypes?.length
83
+ ? item.chartTypes.map(chartType => renderStatusBadge(getQueryChartTypeLabel(chartType), 'primary')).join('')
84
+ : renderStatusBadge('None', 'muted');
85
+ const badgesMarkup = renderQueryHistoryBadgeRow([savedBadgeMarkup, chartBadgesMarkup].join(''));
86
+ const actionsMarkup = renderQueryHistoryActionGroup([
87
+ renderQueryHistoryIconButton({
88
+ action: 'open-charts-query-detail',
89
+ historyId: item.id,
90
+ icon: 'info',
91
+ title: 'Open chart detail',
92
+ active: Boolean(state.charts.detailPanelVisible && isActive),
93
+ }),
94
+ renderQueryHistoryIconButton({
95
+ action: 'toggle-charts-query-history-saved',
96
+ historyId: item.id,
97
+ icon: item.isSaved ? 'bookmark' : 'bookmark_add',
98
+ title: item.isSaved ? 'Remove from saved' : 'Save query',
99
+ active: item.isSaved,
100
+ nextValue: item.isSaved ? 'false' : 'true',
101
+ }),
102
+ ]);
103
+
104
+ return renderQueryHistoryListItem({
105
+ title: item.displayTitle,
106
+ preview: item.previewSql,
107
+ historyId: item.id,
108
+ active: isActive,
109
+ hitAction: 'navigate',
110
+ hitAttrs: `data-to="/charts/${encodeURIComponent(item.id)}"`,
111
+ itemAttrs: 'data-charts-history-item',
112
+ titleAttrs: 'data-charts-history-title',
113
+ badgesMarkup,
114
+ actionsMarkup,
115
+ });
116
+ }
117
+
118
+ function renderChartsList(state) {
119
+ const activeTab = ['recent', 'saved', 'unsaved'].includes(state.charts.historyTab)
120
+ ? state.charts.historyTab
121
+ : 'recent';
122
+ const allQueries = state.charts.queries ?? [];
123
+ const queries = getFilteredChartsQueries(state);
124
+ const hasSearch = Boolean(normalizeChartsHistorySearchText(state.charts.historySearch));
27
125
 
28
126
  if (state.charts.loading && !allQueries.length) {
29
127
  return `
@@ -48,21 +146,34 @@ function renderChartsList(state) {
48
146
 
49
147
  if (!queries.length) {
50
148
  const isSavedTab = activeTab === 'saved';
149
+ const isUnsavedTab = activeTab === 'unsaved';
51
150
 
52
151
  return `
53
152
  <div class="flex h-full items-center justify-center px-6 text-center">
54
153
  <div>
55
154
  <span class="material-symbols-outlined mb-3 text-4xl text-on-surface-variant/25">${
56
- isSavedTab ? 'bookmark' : 'query_stats'
155
+ hasSearch ? 'manage_search' : isSavedTab ? 'bookmark' : isUnsavedTab ? 'bookmark_border' : 'query_stats'
57
156
  }</span>
58
157
  <p class="font-headline text-lg font-black uppercase tracking-tight text-on-surface">
59
- ${isSavedTab ? 'No Saved Charts Queries' : 'No Chartable Queries'}
158
+ ${
159
+ hasSearch
160
+ ? 'No Matching Queries'
161
+ : isSavedTab
162
+ ? 'No Saved Charts Queries'
163
+ : isUnsavedTab
164
+ ? 'No Unsaved Charts Queries'
165
+ : 'No Chartable Queries'
166
+ }
60
167
  </p>
61
168
  <p class="mt-2 max-w-xs text-sm leading-6 text-on-surface-variant/60">
62
169
  ${
63
- isSavedTab
64
- ? 'Save chartable queries from this list or from the SQL Editor to keep them here.'
65
- : 'Run SELECT queries in the SQL Editor first. They will appear here automatically.'
170
+ hasSearch
171
+ ? 'Try another title, SQL fragment, chart type, or saved status.'
172
+ : isSavedTab
173
+ ? 'Save chartable queries from this list or from the SQL Editor to keep them here.'
174
+ : isUnsavedTab
175
+ ? 'Unsaved chartable queries will appear here until you bookmark them.'
176
+ : 'Run SELECT queries in the SQL Editor first. They will appear here automatically.'
66
177
  }
67
178
  </p>
68
179
  </div>
@@ -71,109 +182,37 @@ function renderChartsList(state) {
71
182
  }
72
183
 
73
184
  return `
74
- <div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
75
- <div class="space-y-2">
76
- ${queries
77
- .map(
78
- item => `
79
- <article
80
- class="group w-full border transition-colors ${
81
- selectedHistoryId === Number(item.id)
82
- ? 'border-primary-container/30 bg-surface-container-high'
83
- : 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
84
- }"
85
- data-charts-history-item
86
- >
87
- <button
88
- class="w-full px-4 py-3 text-left transition-colors group-hover:bg-surface-container-high"
89
- data-action="navigate"
90
- data-history-id="${escapeHtml(item.id)}"
91
- data-to="/charts/${encodeURIComponent(item.id)}"
92
- type="button"
93
- >
94
- <div class="flex items-start justify-between gap-3">
95
- <div class="min-w-0 flex-1 truncate font-mono text-xs ${
96
- selectedHistoryId === Number(item.id) ? 'text-primary-container' : 'text-on-surface'
97
- }" data-charts-history-title>
98
- ${escapeHtml(item.displayTitle)}
99
- </div>
100
- <div class="flex shrink-0 flex-wrap justify-end gap-1">
101
- <span class="inline-flex" data-charts-saved-badge ${item.isSaved ? '' : 'hidden'}>
102
- ${renderStatusBadge('saved', 'primary')}
103
- </span>
104
- ${
105
- item.chartTypes?.length
106
- ? item.chartTypes
107
- .map(chartType => renderStatusBadge(getQueryChartTypeLabel(chartType), 'primary'))
108
- .join('')
109
- : renderStatusBadge('None', 'muted')
110
- }
111
- </div>
112
- </div>
113
- <div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
114
- ${escapeHtml(item.previewSql)}
115
- </div>
116
- </button>
117
- <div class="flex items-center justify-end border-t border-outline-variant/10 px-3 py-2 transition-colors group-hover:bg-surface-container-high">
118
- <button
119
- aria-label="Open chart query"
120
- class="min-h-[var(--control-height)] flex-1 self-stretch"
121
- data-action="navigate"
122
- data-history-id="${escapeHtml(item.id)}"
123
- data-to="/charts/${encodeURIComponent(item.id)}"
124
- type="button"
125
- ></button>
126
- <button
127
- class="query-history-icon-button ${item.isSaved ? 'is-active' : ''}"
128
- data-action="toggle-charts-query-history-saved"
129
- data-history-id="${escapeHtml(item.id)}"
130
- data-next-value="${item.isSaved ? 'false' : 'true'}"
131
- title="${item.isSaved ? 'Remove from saved' : 'Save query'}"
132
- type="button"
133
- >
134
- <span class="material-symbols-outlined text-[18px]">
135
- ${item.isSaved ? 'bookmark' : 'bookmark_add'}
136
- </span>
137
- </button>
138
- </div>
139
- </article>
140
- `,
141
- )
142
- .join('')}
185
+ <div class="query-history-list-scroll custom-scrollbar">
186
+ <div class="query-history-list">
187
+ ${queries.map(item => renderChartsHistoryListItem(item, state)).join('')}
143
188
  </div>
144
189
  </div>
145
190
  `;
146
191
  }
147
192
 
148
193
  function renderChartsHistoryTabs(state) {
149
- const activeTab = ['recent', 'saved'].includes(state.charts.historyTab) ? state.charts.historyTab : 'recent';
150
- const savedCount = (state.charts.queries ?? []).filter(item => item.isSaved).length;
194
+ const activeTab = ['recent', 'saved', 'unsaved'].includes(state.charts.historyTab)
195
+ ? state.charts.historyTab
196
+ : 'recent';
197
+ const search = normalizeChartsHistorySearchText(state.charts.historySearch);
198
+ const countableQueries = search
199
+ ? (state.charts.queries ?? []).filter(item => getChartsHistorySearchHaystack(item).includes(search))
200
+ : (state.charts.queries ?? []);
201
+ const savedCount = countableQueries.filter(item => item.isSaved).length;
202
+ const unsavedCount = countableQueries.filter(item => !item.isSaved).length;
151
203
  const tabs = [
152
- { id: 'recent', label: 'Recent', count: state.charts.queries?.length ?? 0 },
204
+ { id: 'recent', label: 'Recent', count: countableQueries.length },
153
205
  { id: 'saved', label: 'Saved', count: savedCount },
206
+ { id: 'unsaved', label: 'Unsaved', count: unsavedCount },
154
207
  ];
155
208
 
156
- return `
157
- <div class="mt-4 flex items-center gap-2">
158
- ${tabs
159
- .map(
160
- tab => `
161
- <button
162
- class="query-history-tab ${activeTab === tab.id ? 'is-active' : ''}"
163
- data-action="set-charts-history-tab"
164
- data-tab="${escapeHtml(tab.id)}"
165
- type="button"
166
- >
167
- ${escapeHtml(tab.label)}
168
- </button>
169
- `,
170
- )
171
- .join('')}
172
- <span class="ml-auto text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/50">
173
- <span data-charts-history-count>${escapeHtml(String(tabs.find(tab => tab.id === activeTab)?.count ?? 0))}</span>
174
- </span>
175
- </div>
176
- `;
209
+ return renderQueryHistoryTabs({
210
+ tabs,
211
+ activeTab,
212
+ action: 'set-charts-history-tab',
213
+ count: tabs.find(tab => tab.id === activeTab)?.count ?? 0,
214
+ countAttr: 'data-charts-history-count',
215
+ });
177
216
  }
178
217
 
179
218
  function renderEmptyChartDetail() {
@@ -185,7 +224,7 @@ function renderEmptyChartDetail() {
185
224
  Select A Query
186
225
  </h2>
187
226
  <p class="mt-3 text-sm leading-7 text-on-surface-variant/65">
188
- Choose a query-history entry on the left to load its charts, render the live result set, and manage chart definitions.
227
+ Choose a query-history entry on the right to load its charts, render the live result set, and manage chart definitions.
189
228
  </p>
190
229
  </div>
191
230
  </div>
@@ -231,60 +270,45 @@ function renderQueryResultState(state, result) {
231
270
  return '';
232
271
  }
233
272
 
234
- function renderQuerySqlSection(state, rawSql) {
235
- const isExpanded = Boolean(state.charts.sqlExpanded);
273
+ function renderChartsDetailDrawerMetaItem(label, value) {
274
+ return `
275
+ <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
276
+ <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
277
+ ${escapeHtml(label)}
278
+ </div>
279
+ <div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>
280
+ </div>
281
+ `;
282
+ }
236
283
 
284
+ function renderChartsDetailDrawerQuerySection(rawSql) {
237
285
  return `
238
286
  <section class="mt-5 border border-outline-variant/10 bg-surface-container-lowest">
239
- <button
240
- class="standard-button flex w-full justify-between border-b border-outline-variant/10 px-4 text-left"
241
- data-action="toggle-charts-sql-panel"
242
- type="button"
243
- >
287
+ <div class="border-b border-outline-variant/10 px-4 py-3">
244
288
  <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
245
- Query SQL
289
+ Query
246
290
  </span>
247
- <span class="material-symbols-outlined text-on-surface-variant/55">
248
- ${isExpanded ? 'expand_less' : 'expand_more'}
249
- </span>
250
- </button>
251
- ${
252
- isExpanded
253
- ? `
254
- <pre class="custom-scrollbar overflow-auto p-4 font-mono text-sm leading-6 text-on-surface"><code>${highlightSql(
255
- rawSql,
256
- )}</code></pre>
257
- `
258
- : ''
259
- }
291
+ </div>
292
+ <pre class="query-history-detail-sql custom-scrollbar max-h-[22rem] overflow-auto p-4 font-mono text-sm leading-6 text-on-surface"><code>${highlightSql(
293
+ rawSql ?? '',
294
+ )}</code></pre>
260
295
  </section>
261
296
  `;
262
297
  }
263
298
 
264
- function renderQueryResultsSection(state) {
265
- const isVisible = Boolean(state.charts.resultsVisible);
299
+ function renderChartsDetailDrawerResultsSection(state) {
266
300
  const statusMarkup = renderQueryResultState(state, state.charts.result);
267
301
 
268
302
  return `
269
- <section class="mt-6 border border-outline-variant/10 bg-surface-container-lowest">
270
- <div class="flex items-center justify-between gap-3 border-b border-outline-variant/10 px-4 py-3">
303
+ <section class="mt-5 border border-outline-variant/10 bg-surface-container-lowest">
304
+ <div class="border-b border-outline-variant/10 px-4 py-3">
271
305
  <span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
272
306
  Results
273
307
  </span>
274
- <button
275
- class="standard-button"
276
- data-action="toggle-charts-results-panel"
277
- type="button"
278
- >
279
- <span class="material-symbols-outlined text-sm">
280
- ${isVisible ? 'table_rows_narrow' : 'table_rows'}
281
- </span>
282
- ${isVisible ? 'Hide Results' : 'Show Results'}
283
- </button>
284
308
  </div>
285
309
  ${statusMarkup}
286
310
  ${
287
- isVisible && state.charts.result && !state.charts.resultLoading && !state.charts.resultError
311
+ state.charts.result && !state.charts.resultLoading && !state.charts.resultError
288
312
  ? `
289
313
  <div class="h-[18rem] overflow-hidden">
290
314
  ${renderQueryResultsPane(state.charts.result, {
@@ -303,6 +327,140 @@ function renderQueryResultsSection(state) {
303
327
  `;
304
328
  }
305
329
 
330
+ export function renderChartsDetailDrawer(state) {
331
+ if (!state.charts.detailPanelVisible) {
332
+ return '';
333
+ }
334
+
335
+ const detail = state.charts.detail;
336
+
337
+ if (state.charts.detailLoading && !detail) {
338
+ return `
339
+ <section class="flex h-full min-h-0 flex-col bg-surface-low">
340
+ <div class="border-b border-outline-variant/10 px-5 py-4">
341
+ <div class="flex items-center justify-between gap-3">
342
+ <span class="font-headline text-sm font-black uppercase tracking-[0.18em] text-primary-container">
343
+ Chart Detail
344
+ </span>
345
+ <button class="query-history-icon-button" data-action="close-charts-query-detail" type="button">
346
+ <span class="material-symbols-outlined text-[18px]">close</span>
347
+ </button>
348
+ </div>
349
+ </div>
350
+ <div class="flex flex-1 items-center justify-center px-6 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
351
+ Loading chart detail...
352
+ </div>
353
+ </section>
354
+ `;
355
+ }
356
+
357
+ if (state.charts.detailError && !detail) {
358
+ return `
359
+ <section class="flex h-full min-h-0 flex-col bg-surface-low">
360
+ <div class="border-b border-outline-variant/10 px-5 py-4">
361
+ <div class="flex items-center justify-between gap-3">
362
+ <span class="font-headline text-sm font-black uppercase tracking-[0.18em] text-primary-container">
363
+ Chart Detail
364
+ </span>
365
+ <button class="query-history-icon-button" data-action="close-charts-query-detail" type="button">
366
+ <span class="material-symbols-outlined text-[18px]">close</span>
367
+ </button>
368
+ </div>
369
+ </div>
370
+ <div class="p-5">
371
+ <div class="border border-error/30 bg-error-container/20 px-4 py-4 text-sm text-error">
372
+ ${escapeHtml(state.charts.detailError.message)}
373
+ </div>
374
+ </div>
375
+ </section>
376
+ `;
377
+ }
378
+
379
+ if (!detail?.item) {
380
+ return '';
381
+ }
382
+
383
+ const charts = detail.charts ?? [];
384
+ const result = state.charts.result;
385
+ const chartTypeBadges = charts.length
386
+ ? charts.map(chart => renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary')).join('')
387
+ : renderStatusBadge('no charts', 'muted');
388
+ const savedBadgeMarkup = `
389
+ <span
390
+ data-charts-saved-badge
391
+ data-history-id="${escapeHtml(detail.item.id)}"
392
+ ${detail.item.isSaved ? '' : 'hidden'}
393
+ >
394
+ ${renderStatusBadge('saved', 'primary')}
395
+ </span>
396
+ `;
397
+ const statusMarkup = [
398
+ savedBadgeMarkup,
399
+ state.charts.resultError ? renderStatusBadge('result error', 'alert') : '',
400
+ state.charts.resultLoading ? renderStatusBadge('loading result', 'muted') : '',
401
+ chartTypeBadges,
402
+ ].join('');
403
+ const metaMarkup = [
404
+ renderChartsDetailDrawerMetaItem('Charts', formatNumber(charts.length)),
405
+ renderChartsDetailDrawerMetaItem('Rows', formatNumber(result?.rows?.length ?? 0)),
406
+ renderChartsDetailDrawerMetaItem('Columns', formatNumber(result?.columns?.length ?? 0)),
407
+ renderChartsDetailDrawerMetaItem('Query ID', String(detail.item.id)),
408
+ ].join('');
409
+
410
+ return `
411
+ <section class="flex h-full min-h-0 flex-col bg-surface-low">
412
+ <div class="border-b border-outline-variant/10 px-5 py-4">
413
+ <div class="flex items-center justify-between gap-3">
414
+ <div class="min-w-0">
415
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
416
+ Chart Detail
417
+ </div>
418
+ <h2 class="mt-1 truncate font-headline text-lg font-black uppercase tracking-tight text-on-surface">
419
+ ${escapeHtml(detail.item.displayTitle)}
420
+ </h2>
421
+ </div>
422
+ <button
423
+ class="query-history-icon-button"
424
+ data-action="close-charts-query-detail"
425
+ title="Close chart detail"
426
+ type="button"
427
+ >
428
+ <span class="material-symbols-outlined text-[18px]">close</span>
429
+ </button>
430
+ </div>
431
+ ${renderQueryHistoryBadgeRow(statusMarkup, { compact: false, className: 'mt-4' })}
432
+ </div>
433
+ <div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-5 py-5">
434
+ <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
435
+ ${metaMarkup}
436
+ </div>
437
+ <div class="mt-5 flex flex-wrap gap-2">
438
+ <button
439
+ class="standard-button"
440
+ data-action="open-query-history"
441
+ data-history-id="${escapeHtml(detail.item.id)}"
442
+ type="button"
443
+ >
444
+ Open In Editor
445
+ </button>
446
+ <button
447
+ class="standard-button"
448
+ data-action="toggle-charts-query-history-saved"
449
+ data-history-id="${escapeHtml(detail.item.id)}"
450
+ data-next-value="${detail.item.isSaved ? 'false' : 'true'}"
451
+ title="${detail.item.isSaved ? 'Remove from saved' : 'Save query'}"
452
+ type="button"
453
+ >
454
+ <span data-charts-saved-label>${detail.item.isSaved ? 'Unsave' : 'Save'}</span>
455
+ </button>
456
+ </div>
457
+ ${renderChartsDetailDrawerQuerySection(detail.item.rawSql)}
458
+ ${renderChartsDetailDrawerResultsSection(state)}
459
+ </div>
460
+ </section>
461
+ `;
462
+ }
463
+
306
464
  function resolveChartCardSizeClass(state) {
307
465
  return `query-chart-card--${String(state.charts.chartHeightPreset ?? 'medium')
308
466
  .trim()
@@ -391,7 +549,7 @@ function renderChartCard(chart, state, analysis) {
391
549
  '<div class="flex flex-wrap items-center gap-2">',
392
550
  '<h3 class="truncate font-headline text-xl font-black uppercase tracking-tight text-on-surface">',
393
551
  escapeHtml(chart.name),
394
- "</h3>",
552
+ '</h3>',
395
553
  renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary'),
396
554
  '</div></div><div class="flex flex-wrap items-center gap-2">',
397
555
  '<button class="standard-button" data-action="open-edit-query-chart-modal" data-chart-id="',
@@ -405,8 +563,8 @@ function renderChartCard(chart, state, analysis) {
405
563
  '" type="button">Export PNG</button>',
406
564
  '</div></header><div class="query-chart-card__body">',
407
565
  renderChartSurface(chart, state, analysis),
408
- "</div></article>",
409
- ].join("");
566
+ '</div></article>',
567
+ ].join('');
410
568
  }
411
569
 
412
570
  export function renderChartsDetail(state) {
@@ -447,12 +605,9 @@ export function renderChartsDetail(state) {
447
605
  const charts = detail.charts ?? [];
448
606
 
449
607
  return `
450
- <div class="custom-scrollbar flex-1 overflow-auto">
608
+ <div class="charts-detail-scroll custom-scrollbar flex-1 overflow-auto">
451
609
  <div class="charts-detail-shell">
452
- <header class="charts-detail-shell__header">
453
- <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
454
- Charts
455
- </div>
610
+ <header class="charts-detail-shell__header" data-charts-detail-header>
456
611
  <div class="charts-detail-shell__title">
457
612
  <h1 class="mt-2 truncate font-headline text-4xl font-black uppercase tracking-tight text-primary-container">
458
613
  ${escapeHtml(detail.item.displayTitle)}
@@ -461,19 +616,6 @@ export function renderChartsDetail(state) {
461
616
  <div class="charts-detail-shell__controls">
462
617
  <div class="charts-detail-shell__controls-group">
463
618
  ${renderChartHeightPresetToggle(state)}
464
- <button
465
- class="standard-button"
466
- data-action="toggle-query-history-panel"
467
- data-next-value="${historyVisible ? 'false' : 'true'}"
468
- type="button"
469
- >
470
- <span class="material-symbols-outlined text-sm">${
471
- historyVisible ? 'visibility_off' : 'visibility'
472
- }</span>
473
- ${historyVisible ? 'Hide Query History' : 'Show Query History'}
474
- </button>
475
- </div>
476
- <div class="charts-detail-shell__controls-group charts-detail-shell__controls-group--end">
477
619
  <button
478
620
  class="standard-button"
479
621
  data-action="open-query-history"
@@ -492,12 +634,24 @@ export function renderChartsDetail(state) {
492
634
  New Chart
493
635
  </button>
494
636
  </div>
637
+ <div class="charts-detail-shell__controls-group charts-detail-shell__controls-group--end">
638
+ <button
639
+ class="standard-button panel-toggle-button ${historyVisible ? '' : 'is-active'}"
640
+ aria-pressed="${historyVisible ? 'false' : 'true'}"
641
+ data-action="toggle-query-history-panel"
642
+ data-next-value="${historyVisible ? 'false' : 'true'}"
643
+ type="button"
644
+ >
645
+ <span class="material-symbols-outlined text-sm">${
646
+ historyVisible ? 'visibility_off' : 'visibility'
647
+ }</span>
648
+ ${historyVisible ? 'Hide History' : 'Show History'}
649
+ </button>
650
+ </div>
495
651
  </div>
496
652
  </header>
497
653
 
498
- ${renderQuerySqlSection(state, detail.item.rawSql)}
499
-
500
- <section class="mt-6 space-y-5">
654
+ <section class="mt-6 space-y-5" data-charts-card-list>
501
655
  ${
502
656
  charts.length
503
657
  ? charts.map(chart => renderChartCard(chart, state, analysis)).join('')
@@ -517,7 +671,6 @@ export function renderChartsDetail(state) {
517
671
  }
518
672
  </section>
519
673
 
520
- ${renderQueryResultsSection(state)}
521
674
  </div>
522
675
  </div>
523
676
  `;
@@ -536,27 +689,26 @@ export function renderChartsView(state) {
536
689
  return {
537
690
  main: `
538
691
  <section class="charts-view">
692
+ <div class="charts-view__detail">${renderChartsDetail(state)}</div>
539
693
  ${
540
694
  historyVisible
541
695
  ? `
542
- <aside class="charts-view__sidebar">
543
- <div class="charts-view__sidebar-header">
544
- <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
545
- Query History
546
- </div>
547
- <h2 class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
548
- Charts
549
- </h2>
696
+ <aside class="query-history-panel charts-view__sidebar">
697
+ <div class="query-history-panel__header charts-view__sidebar-header">
698
+ ${renderQueryHistoryHeader()}
550
699
  ${renderChartsHistoryTabs(state)}
700
+ ${renderQueryHistorySearch({
701
+ bind: 'charts-history-search',
702
+ value: state.charts.historySearchInput,
703
+ })}
551
704
  </div>
552
705
  ${renderChartsList(state)}
553
706
  </aside>
554
707
  `
555
708
  : ''
556
709
  }
557
- <div class="charts-view__detail">${renderChartsDetail(state)}</div>
558
710
  </section>
559
711
  `,
560
- panel: '',
712
+ panel: renderChartsDetailDrawer(state),
561
713
  };
562
714
  }