sqlite-hub 1.0.0 → 1.1.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 (40) 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 +199 -34
  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 +20 -24
  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 +699 -89
  13. package/frontend/js/components/tableDesignerEditor.js +3 -5
  14. package/frontend/js/store.js +73 -7
  15. package/frontend/js/utils/exportFilenames.js +3 -1
  16. package/frontend/js/views/charts.js +320 -169
  17. package/frontend/js/views/connections.js +59 -64
  18. package/frontend/js/views/data.js +12 -20
  19. package/frontend/js/views/editor.js +0 -2
  20. package/frontend/js/views/settings.js +78 -0
  21. package/frontend/js/views/structure.js +27 -13
  22. package/frontend/styles/components.css +155 -0
  23. package/frontend/styles/structure-graph.css +140 -35
  24. package/frontend/styles/tailwind.generated.css +1 -1
  25. package/frontend/styles/views.css +12 -6
  26. package/package.json +3 -2
  27. package/server/routes/export.js +89 -22
  28. package/server/routes/externalApi.js +84 -2
  29. package/server/routes/settings.js +37 -28
  30. package/server/services/appInfoService.js +215 -0
  31. package/server/services/databaseCommandService.js +50 -6
  32. package/server/services/sqlite/exportService.js +307 -22
  33. package/tests/api-token-auth.test.js +110 -1
  34. package/tests/cli-args.test.js +16 -3
  35. package/tests/database-command-service.test.js +39 -2
  36. package/tests/export-blob.test.js +54 -1
  37. package/tests/export-filenames.test.js +4 -0
  38. package/tests/settings-api-tokens-route.test.js +22 -1
  39. package/tests/settings-metadata.test.js +99 -1
  40. package/tests/settings-view.test.js +28 -0
@@ -35,6 +35,7 @@ import {
35
35
  clearEditorResults,
36
36
  clearQueryHistorySelection,
37
37
  closeModal,
38
+ checkSettingsAppVersion,
38
39
  dismissMediaTaggingIssue,
39
40
  dismissToast,
40
41
  executeCurrentQuery,
@@ -95,7 +96,9 @@ import {
95
96
  toggleDataTablesPanel,
96
97
  setCurrentQuery,
97
98
  setChartsHeightPreset,
99
+ setChartsDetailPanelVisibility,
98
100
  setChartsHistoryTab,
101
+ setChartsHistorySearchInput,
99
102
  setChartsHistoryPanelVisibility,
100
103
  setEditorPanelVisibility,
101
104
  setEditorTab,
@@ -222,6 +225,7 @@ let lastRenderedPanelMarkup = '';
222
225
  let lastRenderedModalMarkup = '';
223
226
  let lastRenderedToastMarkup = '';
224
227
  let lastRenderedChartsHistorySignature = '';
228
+ let lastRenderedChartsCardSignature = '';
225
229
  let lastRenderedPanelOpen = false;
226
230
  let lastRenderedLockedRoute = false;
227
231
  let pendingNewTableDesignerAutofocus = false;
@@ -714,7 +718,11 @@ function buildChartsHistorySignature(state) {
714
718
  }
715
719
 
716
720
  return JSON.stringify({
721
+ detailPanelVisible: Boolean(state.charts.detailPanelVisible),
722
+ detailPanelHistoryId: state.charts.detailPanelVisible ? state.charts.selectedHistoryId : null,
717
723
  tab: state.charts.historyTab ?? 'recent',
724
+ searchInput: state.charts.historySearchInput ?? '',
725
+ search: state.charts.historySearch ?? '',
718
726
  queries: queries.map(item => [
719
727
  item.id,
720
728
  item.displayTitle,
@@ -725,6 +733,46 @@ function buildChartsHistorySignature(state) {
725
733
  });
726
734
  }
727
735
 
736
+ const chartSignatureObjectIds = new WeakMap();
737
+ let nextChartSignatureObjectId = 1;
738
+
739
+ function getChartSignatureObjectId(value) {
740
+ if (!value || typeof value !== 'object') {
741
+ return null;
742
+ }
743
+
744
+ if (!chartSignatureObjectIds.has(value)) {
745
+ chartSignatureObjectIds.set(value, nextChartSignatureObjectId);
746
+ nextChartSignatureObjectId += 1;
747
+ }
748
+
749
+ return chartSignatureObjectIds.get(value);
750
+ }
751
+
752
+ function buildChartsCardSignature(state) {
753
+ if (state.route.name !== 'charts' || !state.charts.selectedHistoryId || !state.charts.detail?.item) {
754
+ return '';
755
+ }
756
+
757
+ const charts = state.charts.detail?.charts ?? [];
758
+
759
+ return JSON.stringify({
760
+ selectedHistoryId: state.charts.selectedHistoryId,
761
+ chartHeightPreset: state.charts.chartHeightPreset ?? 'medium',
762
+ resultObjectId: getChartSignatureObjectId(state.charts.result),
763
+ resultLoading: Boolean(state.charts.resultLoading),
764
+ resultError: state.charts.resultError
765
+ ? [state.charts.resultError.code ?? '', state.charts.resultError.message ?? '']
766
+ : null,
767
+ charts: charts.map(chart => [
768
+ chart.id,
769
+ chart.name,
770
+ chart.chartType,
771
+ JSON.stringify(chart.config ?? {}),
772
+ ]),
773
+ });
774
+ }
775
+
728
776
  function syncChartsHistorySelectionUi(state) {
729
777
  const selectedHistoryId = String(state.charts.selectedHistoryId ?? '');
730
778
  const historyButtons = shellRefs.view.querySelectorAll(
@@ -738,56 +786,123 @@ function syncChartsHistorySelectionUi(state) {
738
786
 
739
787
  const itemNode = button.closest('[data-charts-history-item]');
740
788
  const isSelected = button.dataset.historyId === selectedHistoryId;
741
- const titleNode = button.querySelector('[data-charts-history-title]');
742
789
  const selectionNode = itemNode instanceof HTMLElement ? itemNode : button;
743
790
 
744
- selectionNode.classList.toggle('border-primary-container/30', isSelected);
745
- selectionNode.classList.toggle('bg-surface-container-high', isSelected);
746
- selectionNode.classList.toggle('border-outline-variant/10', !isSelected);
747
- selectionNode.classList.toggle('bg-surface-container-lowest', !isSelected);
748
- selectionNode.classList.toggle('hover:bg-surface-container-high', !isSelected);
749
-
750
- if (titleNode instanceof HTMLElement) {
751
- titleNode.classList.toggle('text-primary-container', isSelected);
752
- titleNode.classList.toggle('text-on-surface', !isSelected);
753
- }
791
+ selectionNode.classList.toggle('is-active', isSelected);
792
+ button.classList.toggle('is-active', isSelected);
754
793
  }
755
794
 
756
795
  return true;
757
796
  }
758
797
 
759
- function syncChartsSavedToggleUi(actionNode, nextValue) {
760
- const itemNode = actionNode.closest('[data-charts-history-item]');
761
-
762
- actionNode.classList.toggle('is-active', nextValue);
763
- actionNode.dataset.nextValue = nextValue ? 'false' : 'true';
764
- actionNode.title = nextValue ? 'Remove from saved' : 'Save query';
798
+ function syncChartsSavedToggleButtonUi(button, nextValue) {
799
+ button.classList.toggle('is-active', nextValue);
800
+ button.dataset.nextValue = nextValue ? 'false' : 'true';
801
+ button.title = nextValue ? 'Remove from saved' : 'Save query';
765
802
 
766
- const iconNode = actionNode.querySelector('.material-symbols-outlined');
803
+ const iconNode = button.querySelector('.material-symbols-outlined');
767
804
  if (iconNode instanceof HTMLElement) {
768
805
  iconNode.textContent = nextValue ? 'bookmark' : 'bookmark_add';
769
806
  }
770
807
 
771
- if (itemNode instanceof HTMLElement) {
772
- itemNode.querySelector('[data-charts-saved-badge]')?.toggleAttribute('hidden', !nextValue);
808
+ const labelNode = button.querySelector('[data-charts-saved-label]');
809
+ if (labelNode instanceof HTMLElement) {
810
+ labelNode.textContent = nextValue ? 'Unsave' : 'Save';
811
+ }
812
+ }
813
+
814
+ function syncChartsSavedToggleUi(actionNode, nextValue) {
815
+ const historyId = actionNode.dataset.historyId;
816
+ const relatedButtons = [
817
+ actionNode,
818
+ ...shellRefs.view.querySelectorAll('[data-action="toggle-charts-query-history-saved"][data-history-id]'),
819
+ ...shellRefs.panel.querySelectorAll('[data-action="toggle-charts-query-history-saved"][data-history-id]'),
820
+ ].filter((button, index, buttons) => {
821
+ return (
822
+ button instanceof HTMLElement &&
823
+ button.dataset.historyId === historyId &&
824
+ buttons.indexOf(button) === index
825
+ );
826
+ });
773
827
 
774
- if (!nextValue && getState().charts.historyTab === 'saved') {
828
+ for (const button of relatedButtons) {
829
+ syncChartsSavedToggleButtonUi(button, nextValue);
830
+
831
+ const itemNode = button.closest('[data-charts-history-item]');
832
+ const historyTab = getState().charts.historyTab;
833
+ if (
834
+ itemNode instanceof HTMLElement &&
835
+ ((historyTab === 'saved' && !nextValue) || (historyTab === 'unsaved' && nextValue))
836
+ ) {
775
837
  itemNode.remove();
776
838
  }
777
839
  }
778
840
 
841
+ for (const badgeNode of [
842
+ ...shellRefs.view.querySelectorAll('[data-charts-saved-badge][data-history-id]'),
843
+ ...shellRefs.panel.querySelectorAll('[data-charts-saved-badge][data-history-id]'),
844
+ ]) {
845
+ if (badgeNode instanceof HTMLElement && badgeNode.dataset.historyId === historyId) {
846
+ badgeNode.toggleAttribute('hidden', !nextValue);
847
+ }
848
+ }
849
+
779
850
  const countNode = shellRefs.view.querySelector('[data-charts-history-count]');
780
851
  if (countNode instanceof HTMLElement) {
781
- const state = getState();
782
- const count =
783
- state.charts.historyTab === 'saved'
784
- ? (state.charts.queries ?? []).filter(item => item.isSaved).length
785
- : (state.charts.queries ?? []).length;
786
- countNode.textContent = String(count);
852
+ countNode.textContent = String(shellRefs.view.querySelectorAll('[data-charts-history-item]').length);
853
+ }
854
+ }
855
+
856
+ function renderChartsMainIntoScratch(state) {
857
+ const scratch = document.createElement('div');
858
+
859
+ replaceChildrenFromRenderedMarkup(scratch, renderChartsView(state).main);
860
+ return scratch;
861
+ }
862
+
863
+ function patchChartsHistoryUi(state) {
864
+ const chartsView = shellRefs.view.querySelector('.charts-view');
865
+
866
+ if (!(chartsView instanceof HTMLElement)) {
867
+ return false;
868
+ }
869
+
870
+ const currentSidebar = chartsView.querySelector('.charts-view__sidebar');
871
+ const scratch = renderChartsMainIntoScratch(state);
872
+ const nextSidebar = scratch.querySelector('.charts-view__sidebar');
873
+
874
+ if (!currentSidebar && !nextSidebar) {
875
+ return true;
787
876
  }
877
+
878
+ if (!(currentSidebar instanceof HTMLElement) || !(nextSidebar instanceof HTMLElement)) {
879
+ return false;
880
+ }
881
+
882
+ currentSidebar.replaceWith(nextSidebar);
883
+ return true;
884
+ }
885
+
886
+ function renderChartsDetailIntoScratch(state) {
887
+ const scratch = document.createElement('div');
888
+
889
+ replaceChildrenFromRenderedMarkup(scratch, renderChartsDetail(state));
890
+ return scratch;
891
+ }
892
+
893
+ function replaceChartsDetailSection(detailNode, scratchNode, selector) {
894
+ const currentNode = detailNode.querySelector(selector);
895
+ const nextNode = scratchNode.querySelector(selector);
896
+
897
+ if (!(currentNode instanceof HTMLElement) || !(nextNode instanceof HTMLElement)) {
898
+ return false;
899
+ }
900
+
901
+ currentNode.replaceWith(nextNode);
902
+ return true;
788
903
  }
789
904
 
790
- function patchChartsDetailUi(state) {
905
+ function patchChartsDetailUi(state, { preserveCharts = false } = {}) {
791
906
  const chartsView = shellRefs.view.querySelector('.charts-view');
792
907
  const detailNode = chartsView?.querySelector('.charts-view__detail');
793
908
 
@@ -802,7 +917,17 @@ function patchChartsDetailUi(state) {
802
917
  return false;
803
918
  }
804
919
 
805
- replaceChildrenFromRenderedMarkup(detailNode, renderChartsDetail(state));
920
+ if (preserveCharts) {
921
+ const scratch = renderChartsDetailIntoScratch(state);
922
+ const patched = replaceChartsDetailSection(detailNode, scratch, '[data-charts-detail-header]');
923
+
924
+ if (!patched) {
925
+ return false;
926
+ }
927
+ } else {
928
+ replaceChildrenFromRenderedMarkup(detailNode, renderChartsDetail(state));
929
+ }
930
+
806
931
  syncChartsHistorySelectionUi(state);
807
932
  return true;
808
933
  }
@@ -1228,6 +1353,7 @@ function renderApp(state) {
1228
1353
  const modalMarkup = renderModal(state);
1229
1354
  const toastMarkup = renderToasts(state.toasts);
1230
1355
  const chartsHistorySignature = buildChartsHistorySignature(state);
1356
+ const chartsCardSignature = buildChartsCardSignature(state);
1231
1357
  const isLockedRoute = [
1232
1358
  'editor',
1233
1359
  'editorResults',
@@ -1247,6 +1373,7 @@ function renderApp(state) {
1247
1373
  const panelChanged = panel !== lastRenderedPanelMarkup;
1248
1374
  const modalChanged = modalMarkup !== lastRenderedModalMarkup;
1249
1375
  const chartsHistoryChanged = chartsHistorySignature !== lastRenderedChartsHistorySignature;
1376
+ const chartsCardsChanged = chartsCardSignature !== lastRenderedChartsCardSignature;
1250
1377
  const panelOpenChanged = panelOpen !== lastRenderedPanelOpen;
1251
1378
  const lockedRouteChanged = isLockedRoute !== lastRenderedLockedRoute;
1252
1379
  const shellMarkupUnchanged =
@@ -1286,12 +1413,25 @@ function renderApp(state) {
1286
1413
  }
1287
1414
 
1288
1415
  const canPatchChartsMain =
1289
- mainChanged && previousRouteName === 'charts' && state.route.name === 'charts' && !chartsHistoryChanged;
1416
+ mainChanged && previousRouteName === 'charts' && state.route.name === 'charts';
1290
1417
  let mainPatched = false;
1418
+ let preservedChartsDom = false;
1291
1419
 
1292
1420
  if (canPatchChartsMain) {
1293
- teardownQueryChartRenderer();
1294
- mainPatched = patchChartsDetailUi(state);
1421
+ const historyPatched = !chartsHistoryChanged || patchChartsHistoryUi(state);
1422
+
1423
+ if (historyPatched) {
1424
+ if (chartsCardsChanged) {
1425
+ teardownQueryChartRenderer();
1426
+ }
1427
+
1428
+ preservedChartsDom = !chartsCardsChanged;
1429
+ mainPatched = patchChartsDetailUi(state, { preserveCharts: preservedChartsDom });
1430
+ }
1431
+
1432
+ if (!mainPatched) {
1433
+ preservedChartsDom = false;
1434
+ }
1295
1435
  }
1296
1436
 
1297
1437
  if (mainChanged) {
@@ -1387,6 +1527,7 @@ function renderApp(state) {
1387
1527
  lastRenderedModalMarkup = modalMarkup;
1388
1528
  lastRenderedToastMarkup = toastMarkup;
1389
1529
  lastRenderedChartsHistorySignature = chartsHistorySignature;
1530
+ lastRenderedChartsCardSignature = chartsCardSignature;
1390
1531
  lastRenderedPanelOpen = panelOpen;
1391
1532
  lastRenderedLockedRoute = isLockedRoute;
1392
1533
 
@@ -1396,7 +1537,7 @@ function renderApp(state) {
1396
1537
  });
1397
1538
  }
1398
1539
 
1399
- if (state.route.name === 'charts') {
1540
+ if (state.route.name === 'charts' && !preservedChartsDom) {
1400
1541
  mountQueryChartRenderer(state);
1401
1542
  }
1402
1543
  }
@@ -2099,6 +2240,9 @@ async function handleAction(actionNode) {
2099
2240
  case 'set-settings-section':
2100
2241
  setSettingsSection(actionNode.dataset.section);
2101
2242
  return;
2243
+ case 'check-app-version':
2244
+ await checkSettingsAppVersion();
2245
+ return;
2102
2246
  case 'open-delete-api-token-modal':
2103
2247
  openDeleteSettingsApiTokenModal(actionNode.dataset.tokenId);
2104
2248
  return;
@@ -2387,7 +2531,12 @@ async function handleAction(actionNode) {
2387
2531
  await loadMoreQueryHistory();
2388
2532
  return;
2389
2533
  case 'open-query-history':
2390
- if (actionNode.dataset.historyId && openQueryHistoryInEditor(actionNode.dataset.historyId)) {
2534
+ if (
2535
+ actionNode.dataset.historyId &&
2536
+ openQueryHistoryInEditor(actionNode.dataset.historyId, {
2537
+ notify: getState().route.name !== 'charts',
2538
+ })
2539
+ ) {
2391
2540
  router.navigate('/editor');
2392
2541
  }
2393
2542
  return;
@@ -2420,6 +2569,17 @@ async function handleAction(actionNode) {
2420
2569
  }
2421
2570
  }
2422
2571
  return;
2572
+ case 'open-charts-query-detail':
2573
+ if (actionNode.dataset.historyId) {
2574
+ setChartsDetailPanelVisibility(true);
2575
+ if (getState().route.params?.historyId !== actionNode.dataset.historyId) {
2576
+ router.navigate(`/charts/${encodeURIComponent(actionNode.dataset.historyId)}`);
2577
+ }
2578
+ }
2579
+ return;
2580
+ case 'close-charts-query-detail':
2581
+ setChartsDetailPanelVisibility(false);
2582
+ return;
2423
2583
  case 'toggle-charts-sql-panel':
2424
2584
  toggleChartsSqlPanel();
2425
2585
  return;
@@ -2982,6 +3142,11 @@ document.addEventListener('input', event => {
2982
3142
  return;
2983
3143
  }
2984
3144
 
3145
+ if (bindNode.dataset.bind === 'charts-history-search') {
3146
+ setChartsHistorySearchInput(bindNode.value);
3147
+ return;
3148
+ }
3149
+
2985
3150
  if (bindNode.dataset.bind === 'query-chart-draft:name') {
2986
3151
  updateCurrentQueryChartDraftField('name', bindNode.value);
2987
3152
  }
@@ -1018,6 +1018,20 @@ function getExportOptions() {
1018
1018
  meta: ".md",
1019
1019
  preview: ["| id | name |", "| --- | --- |"],
1020
1020
  },
1021
+ {
1022
+ label: "JSON",
1023
+ format: "json",
1024
+ icon: "data_object",
1025
+ meta: ".json",
1026
+ preview: ['[', ' {"id":1,"name":"Acme"}', ']'],
1027
+ },
1028
+ {
1029
+ label: "Parquet",
1030
+ format: "parquet",
1031
+ icon: "dataset",
1032
+ meta: ".parquet",
1033
+ preview: ["columnar binary", "typed row groups"],
1034
+ },
1021
1035
  {
1022
1036
  label: "Duplicate",
1023
1037
  format: "table",
@@ -1047,7 +1061,7 @@ function renderTextExportModal(modal, action) {
1047
1061
  ${disabledAttribute}
1048
1062
  />
1049
1063
  </label>
1050
- <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
1064
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
1051
1065
  ${getExportOptions()
1052
1066
  .map(
1053
1067
  (option) => `
@@ -1784,6 +1798,8 @@ export function renderModal(state) {
1784
1798
  modal.kind === "row-update-preview" ||
1785
1799
  modal.kind === "table-designer-constraints"
1786
1800
  ? "max-w-3xl"
1801
+ : modal.kind === "query-export" || modal.kind === "data-export"
1802
+ ? "max-w-4xl"
1787
1803
  : "max-w-xl"
1788
1804
  } border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
1789
1805
  <div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
@@ -7,6 +7,7 @@ import {
7
7
  import { analyzeQueryChartResult, validateQueryChartConfig } from "../lib/queryCharts.js";
8
8
 
9
9
  const chartInstances = new Map();
10
+ const chartHosts = new Map();
10
11
  const resizeObservers = new Map();
11
12
 
12
13
  function getEchartsRuntime() {
@@ -41,6 +42,8 @@ function disposeChart(chartId) {
41
42
  chartInstances.delete(chartId);
42
43
  }
43
44
 
45
+ chartHosts.delete(chartId);
46
+
44
47
  const observer = resizeObservers.get(chartId);
45
48
 
46
49
  if (observer) {
@@ -58,7 +61,15 @@ export function mountQueryChartRenderer(state) {
58
61
  const charts = state.charts.detail?.charts ?? [];
59
62
  const result = state.charts.result;
60
63
 
61
- teardownQueryChartRenderer();
64
+ const activeChartIds = new Set(charts.map((chart) => String(chart.id)));
65
+
66
+ Array.from(chartInstances.keys()).forEach((chartId) => {
67
+ const host = chartHosts.get(chartId);
68
+
69
+ if (!activeChartIds.has(chartId) || !(host instanceof HTMLElement) || !host.isConnected) {
70
+ disposeChart(chartId);
71
+ }
72
+ });
62
73
 
63
74
  if (!echartsRuntime || !result || !charts.length) {
64
75
  return;
@@ -85,17 +96,31 @@ export function mountQueryChartRenderer(state) {
85
96
  return;
86
97
  }
87
98
 
99
+ const chartId = String(chart.id);
100
+ const existingInstance = chartInstances.get(chartId);
101
+ const existingHost = chartHosts.get(chartId);
102
+
103
+ if (existingInstance && existingHost === host) {
104
+ existingInstance.resize();
105
+ return;
106
+ }
107
+
108
+ if (existingInstance) {
109
+ disposeChart(chartId);
110
+ }
111
+
88
112
  const instance = echartsRuntime.init(host);
89
113
 
90
114
  instance.setOption(buildOption(chart, result.rows ?? [], analysis), true);
91
- chartInstances.set(String(chart.id), instance);
115
+ chartInstances.set(chartId, instance);
116
+ chartHosts.set(chartId, host);
92
117
 
93
118
  const resizeObserver = new ResizeObserver(() => {
94
119
  instance.resize();
95
120
  });
96
121
 
97
122
  resizeObserver.observe(host);
98
- resizeObservers.set(String(chart.id), resizeObserver);
123
+ resizeObservers.set(chartId, resizeObserver);
99
124
  });
100
125
  }
101
126
 
@@ -1,5 +1,4 @@
1
1
  import { escapeHtml, highlightSql } from "../utils/format.js";
2
- import { renderActionBar } from "./actionBar.js";
3
2
 
4
3
  function renderLineNumbers(query) {
5
4
  const lineCount = Math.max(1, String(query || "").split("\n").length);
@@ -40,20 +39,11 @@ export function renderQueryEditor({
40
39
  query,
41
40
  executing = false,
42
41
  exporting = false,
43
- historyLoading = false,
44
- historyTotal = 0,
45
42
  editorVisible = true,
46
43
  historyVisible = true,
47
44
  }) {
48
45
  const secondaryButtonClass = "standard-button";
49
46
  const left = `
50
- <div class="flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40">
51
- <span class="material-symbols-outlined text-xs">history</span>
52
- ${historyLoading ? "Loading history..." : `${historyTotal} queries tracked`}
53
- </div>
54
- `;
55
-
56
- const right = `
57
47
  <button
58
48
  class="${secondaryButtonClass}"
59
49
  data-action="toggle-editor-panel"
@@ -63,15 +53,9 @@ export function renderQueryEditor({
63
53
  <span class="material-symbols-outlined text-sm">${editorVisible ? "keyboard_arrow_down" : "terminal"}</span>
64
54
  ${editorVisible ? "Hide Editor" : "Show Editor"}
65
55
  </button>
66
- <button
67
- class="${secondaryButtonClass}"
68
- data-action="toggle-query-history-panel"
69
- data-next-value="${historyVisible ? "false" : "true"}"
70
- type="button"
71
- >
72
- <span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "visibility"}</span>
73
- ${historyVisible ? "Hide History" : "Show History"}
74
- </button>
56
+ `;
57
+
58
+ const center = `
75
59
  <button
76
60
  class="${secondaryButtonClass}"
77
61
  data-action="format-current-query"
@@ -104,14 +88,26 @@ export function renderQueryEditor({
104
88
  </button>
105
89
  `;
106
90
 
91
+ const right = `
92
+ <button
93
+ class="${secondaryButtonClass}"
94
+ data-action="toggle-query-history-panel"
95
+ data-next-value="${historyVisible ? "false" : "true"}"
96
+ type="button"
97
+ >
98
+ <span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "visibility"}</span>
99
+ ${historyVisible ? "Hide History" : "Show History"}
100
+ </button>
101
+ `;
102
+
107
103
  return `
108
104
  <div class="flex h-full min-h-0 flex-col">
109
105
  <div class="bg-surface-container-low px-6 py-3">
110
- ${renderActionBar({
111
- left,
112
- right,
113
- className: "flex-wrap",
114
- })}
106
+ <div class="query-editor-toolbar">
107
+ <div class="query-editor-toolbar__left">${left}</div>
108
+ <div class="query-editor-toolbar__center">${center}</div>
109
+ <div class="query-editor-toolbar__right">${right}</div>
110
+ </div>
115
111
  </div>
116
112
  ${
117
113
  editorVisible
@@ -163,7 +163,7 @@ export function renderQueryHistoryDetail({
163
163
  escapeHtml(item.displayTitle),
164
164
  "</h2></div>",
165
165
  '<button class="query-history-icon-button" data-action="clear-query-history-selection" type="button"><span class="material-symbols-outlined text-[18px]">close</span></button>',
166
- '</div><div class="mt-4 flex flex-wrap gap-2">',
166
+ '</div><div class="query-history-badge-row mt-4 flex flex-wrap gap-2">',
167
167
  statusMarkup,
168
168
  "</div></div>",
169
169
  '<div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-5 py-5">',
@@ -0,0 +1,48 @@
1
+ import { escapeHtml } from '../utils/format.js';
2
+
3
+ export function renderQueryHistoryHeader({
4
+ title = 'Query History',
5
+ icon = 'history',
6
+ closeAction = 'toggle-query-history-panel',
7
+ closeTitle = 'Hide query history',
8
+ nextValue = 'false',
9
+ } = {}) {
10
+ return `
11
+ <div class="flex items-center justify-between gap-3">
12
+ <div class="flex items-center gap-2">
13
+ <span class="material-symbols-outlined text-[18px] text-primary-container">${escapeHtml(icon)}</span>
14
+ <span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
15
+ ${escapeHtml(title)}
16
+ </span>
17
+ </div>
18
+ <button
19
+ class="query-history-icon-button"
20
+ data-action="${escapeHtml(closeAction)}"
21
+ data-next-value="${escapeHtml(nextValue)}"
22
+ title="${escapeHtml(closeTitle)}"
23
+ type="button"
24
+ >
25
+ <span class="material-symbols-outlined text-[18px]">close</span>
26
+ </button>
27
+ </div>
28
+ `;
29
+ }
30
+
31
+ export function renderQueryHistorySearch({
32
+ bind = 'query-history-search',
33
+ value = '',
34
+ placeholder = 'Search SQL, titles, notes...',
35
+ } = {}) {
36
+ return `
37
+ <label class="mt-4 block">
38
+ <span class="sr-only">Search query history</span>
39
+ <input
40
+ class="control-input w-full border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
41
+ data-bind="${escapeHtml(bind)}"
42
+ placeholder="${escapeHtml(placeholder)}"
43
+ type="search"
44
+ value="${escapeHtml(value)}"
45
+ />
46
+ </label>
47
+ `;
48
+ }