sqlite-hub 0.5.0 → 0.7.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 (50) hide show
  1. package/README.md +109 -2
  2. package/bin/sqlite-hub.js +285 -4
  3. package/changelog.md +15 -0
  4. package/frontend/assets/mockups/connections.png +0 -0
  5. package/frontend/assets/mockups/data.png +0 -0
  6. package/frontend/assets/mockups/data_row_editor.png +0 -0
  7. package/frontend/assets/mockups/home.png +0 -0
  8. package/frontend/assets/mockups/sql_editor.png +0 -0
  9. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  10. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  11. package/frontend/assets/mockups/structure.png +0 -0
  12. package/frontend/assets/mockups/structure_inspector.png +0 -0
  13. package/frontend/index.html +1 -0
  14. package/frontend/js/api.js +49 -0
  15. package/frontend/js/app.js +377 -9
  16. package/frontend/js/components/modal.js +314 -1
  17. package/frontend/js/components/queryChartRenderer.js +125 -0
  18. package/frontend/js/components/queryEditor.js +37 -8
  19. package/frontend/js/components/queryHistoryPanel.js +16 -5
  20. package/frontend/js/components/sidebar.js +2 -0
  21. package/frontend/js/components/tableDesignerEditor.js +356 -0
  22. package/frontend/js/components/tableDesignerSidebar.js +129 -0
  23. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  24. package/frontend/js/lib/queryChartOptions.js +283 -0
  25. package/frontend/js/lib/queryCharts.js +560 -0
  26. package/frontend/js/router.js +18 -0
  27. package/frontend/js/store.js +914 -1
  28. package/frontend/js/utils/tableDesigner.js +1192 -0
  29. package/frontend/js/views/charts.js +471 -0
  30. package/frontend/js/views/data.js +281 -277
  31. package/frontend/js/views/editor.js +19 -13
  32. package/frontend/js/views/structure.js +81 -39
  33. package/frontend/js/views/tableDesigner.js +37 -0
  34. package/frontend/styles/base.css +87 -73
  35. package/frontend/styles/components.css +790 -251
  36. package/frontend/styles/views.css +316 -46
  37. package/package.json +2 -1
  38. package/server/routes/charts.js +153 -0
  39. package/server/routes/tableDesigner.js +60 -0
  40. package/server/server.js +10 -0
  41. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  42. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  43. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  44. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  45. package/server/services/sqlite/tableDesignerService.js +181 -0
  46. package/server/services/storage/appStateStore.js +400 -1
  47. package/server/services/storage/queryHistoryChartUtils.js +145 -0
  48. package/frontend/assets/mockups/data_edit.png +0 -0
  49. package/frontend/assets/mockups/graph_visualize.png +0 -0
  50. package/frontend/assets/mockups/overview.png +0 -0
@@ -1,5 +1,10 @@
1
1
  import { escapeHtml, truncateMiddle } from "../utils/format.js";
2
2
  import { renderConnectionLogo } from "./connectionLogo.js";
3
+ import {
4
+ analyzeQueryChartResult,
5
+ getQueryChartTypeLabel,
6
+ QUERY_CHART_TYPES,
7
+ } from "../lib/queryCharts.js";
3
8
 
4
9
  function renderField({ label, name, type = "text", placeholder = "", value = "" }) {
5
10
  return `
@@ -58,6 +63,31 @@ function renderFileField({
58
63
  `;
59
64
  }
60
65
 
66
+ function renderSelectField({ label, name, value = "", options = [], bind = "" }) {
67
+ return `
68
+ <label class="block space-y-2">
69
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
70
+ ${escapeHtml(label)}
71
+ </span>
72
+ <select
73
+ class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
74
+ ${bind ? `data-bind="${escapeHtml(bind)}"` : ""}
75
+ ${name ? `name="${escapeHtml(name)}"` : ""}
76
+ >
77
+ ${options
78
+ .map(
79
+ (option) => `
80
+ <option value="${escapeHtml(option.value)}" ${String(option.value) === String(value) ? "selected" : ""}>
81
+ ${escapeHtml(option.label)}
82
+ </option>
83
+ `
84
+ )
85
+ .join("")}
86
+ </select>
87
+ </label>
88
+ `;
89
+ }
90
+
61
91
  function renderError(error) {
62
92
  if (!error) {
63
93
  return "";
@@ -380,6 +410,279 @@ function renderDeleteRowConfirmForm(modal) {
380
410
  `;
381
411
  }
382
412
 
413
+ function renderChartColumnOptions(analysis, { allowEmpty = false, includeNumericHint = false } = {}) {
414
+ const options = allowEmpty ? [{ value: "", label: "None" }] : [];
415
+
416
+ return options.concat(
417
+ (analysis?.columns ?? []).map((column) => ({
418
+ value: column.name,
419
+ label: `${column.name} (${column.type}${includeNumericHint && column.type === "number" ? " numeric" : ""})`,
420
+ }))
421
+ );
422
+ }
423
+
424
+ function renderChartEditorForm(modal, state) {
425
+ const draft = modal.draft ?? {};
426
+ const analysis = state.charts.result ? analyzeQueryChartResult(state.charts.result) : null;
427
+ const chartTypeOptions = QUERY_CHART_TYPES.map((chartType) => ({
428
+ value: chartType,
429
+ label: getQueryChartTypeLabel(chartType),
430
+ }));
431
+ const columnOptions = renderChartColumnOptions(analysis);
432
+ const optionalColumnOptions = renderChartColumnOptions(analysis, { allowEmpty: true });
433
+ let chartSpecificFields = "";
434
+
435
+ if (draft.chartType === "bar") {
436
+ chartSpecificFields = `
437
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
438
+ ${renderSelectField({
439
+ label: "X Column",
440
+ value: draft.config?.x_column ?? "",
441
+ options: columnOptions,
442
+ bind: "query-chart-draft-config:x_column",
443
+ })}
444
+ ${renderSelectField({
445
+ label: "Y Column",
446
+ value: draft.config?.y_column ?? "",
447
+ options: columnOptions,
448
+ bind: "query-chart-draft-config:y_column",
449
+ })}
450
+ </div>
451
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
452
+ ${renderSelectField({
453
+ label: "Sort Direction",
454
+ value: draft.config?.sort_direction ?? "asc",
455
+ options: [
456
+ { value: "asc", label: "Ascending" },
457
+ { value: "desc", label: "Descending" },
458
+ ],
459
+ bind: "query-chart-draft-config:sort_direction",
460
+ })}
461
+ ${renderCheckboxField({
462
+ label: "Show legend",
463
+ name: "",
464
+ checked: Boolean(draft.config?.show_legend),
465
+ text: "Show legend",
466
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
467
+ ${renderCheckboxField({
468
+ label: "Show labels",
469
+ name: "",
470
+ checked: Boolean(draft.config?.show_labels),
471
+ text: "Show labels",
472
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
473
+ </div>
474
+ `;
475
+ } else if (draft.chartType === "line") {
476
+ chartSpecificFields = `
477
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
478
+ ${renderSelectField({
479
+ label: "X Column",
480
+ value: draft.config?.x_column ?? "",
481
+ options: columnOptions,
482
+ bind: "query-chart-draft-config:x_column",
483
+ })}
484
+ ${renderSelectField({
485
+ label: "Y Column",
486
+ value: draft.config?.y_column ?? "",
487
+ options: columnOptions,
488
+ bind: "query-chart-draft-config:y_column",
489
+ })}
490
+ </div>
491
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
492
+ ${renderSelectField({
493
+ label: "Sort Direction",
494
+ value: draft.config?.sort_direction ?? "asc",
495
+ options: [
496
+ { value: "asc", label: "Ascending" },
497
+ { value: "desc", label: "Descending" },
498
+ ],
499
+ bind: "query-chart-draft-config:sort_direction",
500
+ })}
501
+ ${renderCheckboxField({
502
+ label: "Smooth line",
503
+ name: "",
504
+ checked: Boolean(draft.config?.smooth),
505
+ text: "Smooth line",
506
+ }).replace("<input", '<input data-bind="query-chart-draft-config:smooth"')}
507
+ ${renderCheckboxField({
508
+ label: "Show legend",
509
+ name: "",
510
+ checked: Boolean(draft.config?.show_legend),
511
+ text: "Show legend",
512
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
513
+ ${renderCheckboxField({
514
+ label: "Show labels",
515
+ name: "",
516
+ checked: Boolean(draft.config?.show_labels),
517
+ text: "Show labels",
518
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
519
+ </div>
520
+ `;
521
+ } else if (draft.chartType === "pie") {
522
+ chartSpecificFields = `
523
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
524
+ ${renderSelectField({
525
+ label: "Label Column",
526
+ value: draft.config?.label_column ?? "",
527
+ options: columnOptions,
528
+ bind: "query-chart-draft-config:label_column",
529
+ })}
530
+ ${renderSelectField({
531
+ label: "Value Column",
532
+ value: draft.config?.value_column ?? "",
533
+ options: columnOptions,
534
+ bind: "query-chart-draft-config:value_column",
535
+ })}
536
+ </div>
537
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
538
+ ${renderCheckboxField({
539
+ label: "Donut",
540
+ name: "",
541
+ checked: Boolean(draft.config?.donut),
542
+ text: "Render as donut",
543
+ }).replace("<input", '<input data-bind="query-chart-draft-config:donut"')}
544
+ ${renderCheckboxField({
545
+ label: "Show legend",
546
+ name: "",
547
+ checked: Boolean(draft.config?.show_legend),
548
+ text: "Show legend",
549
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
550
+ ${renderCheckboxField({
551
+ label: "Show labels",
552
+ name: "",
553
+ checked: Boolean(draft.config?.show_labels),
554
+ text: "Show labels",
555
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
556
+ </div>
557
+ `;
558
+ } else if (draft.chartType === "scatter") {
559
+ chartSpecificFields = `
560
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
561
+ ${renderSelectField({
562
+ label: "X Column",
563
+ value: draft.config?.x_column ?? "",
564
+ options: columnOptions,
565
+ bind: "query-chart-draft-config:x_column",
566
+ })}
567
+ ${renderSelectField({
568
+ label: "Y Column",
569
+ value: draft.config?.y_column ?? "",
570
+ options: columnOptions,
571
+ bind: "query-chart-draft-config:y_column",
572
+ })}
573
+ </div>
574
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
575
+ ${renderSelectField({
576
+ label: "Size Column",
577
+ value: draft.config?.size_column ?? "",
578
+ options: optionalColumnOptions,
579
+ bind: "query-chart-draft-config:size_column",
580
+ })}
581
+ ${renderSelectField({
582
+ label: "Series Column",
583
+ value: draft.config?.series_column ?? "",
584
+ options: optionalColumnOptions,
585
+ bind: "query-chart-draft-config:series_column",
586
+ })}
587
+ ${renderCheckboxField({
588
+ label: "Show legend",
589
+ name: "",
590
+ checked: Boolean(draft.config?.show_legend),
591
+ text: "Show legend",
592
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
593
+ </div>
594
+ `;
595
+ }
596
+
597
+ return `
598
+ <form class="space-y-5" data-form="save-query-chart">
599
+ ${renderField({
600
+ label: "Chart Name",
601
+ name: "chartName",
602
+ value: draft.name ?? "",
603
+ }).replace("<input", '<input data-bind="query-chart-draft:name"')}
604
+ ${renderSelectField({
605
+ label: "Chart Type",
606
+ value: draft.chartType ?? "bar",
607
+ options: chartTypeOptions,
608
+ bind: "query-chart-draft:chartType",
609
+ })}
610
+ ${chartSpecificFields}
611
+ ${
612
+ analysis
613
+ ? `
614
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4">
615
+ <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
616
+ Result Columns
617
+ </div>
618
+ <div class="mt-3 flex flex-wrap gap-2">
619
+ ${analysis.columns
620
+ .map(
621
+ (column) => `
622
+ <span class="border border-outline-variant/15 bg-surface-container px-2 py-1 text-[10px] font-mono uppercase tracking-[0.12em] text-on-surface-variant/70">
623
+ ${escapeHtml(column.name)} • ${escapeHtml(column.type)}
624
+ </span>
625
+ `
626
+ )
627
+ .join("")}
628
+ </div>
629
+ </div>
630
+ `
631
+ : ""
632
+ }
633
+ ${renderError(modal.error)}
634
+ <div class="flex items-center justify-end gap-3 pt-2">
635
+ <button
636
+ class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
637
+ data-action="close-modal"
638
+ type="button"
639
+ >
640
+ Cancel
641
+ </button>
642
+ <button
643
+ class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
644
+ type="submit"
645
+ >
646
+ ${modal.submitting ? "Saving..." : draft.mode === "edit" ? "Save Chart" : "Create Chart"}
647
+ </button>
648
+ </div>
649
+ </form>
650
+ `;
651
+ }
652
+
653
+ function renderDeleteChartForm(modal) {
654
+ return `
655
+ <form class="space-y-5" data-form="delete-query-chart">
656
+ <div class="space-y-3">
657
+ <p class="text-sm leading-7 text-on-surface">
658
+ Delete chart <span class="font-bold text-primary-container">${escapeHtml(
659
+ modal.chartName ?? "Chart"
660
+ )}</span>?
661
+ </p>
662
+ <p class="text-sm leading-7 text-on-surface-variant/65">
663
+ The linked query-history entry stays intact. Only this chart definition is removed.
664
+ </p>
665
+ </div>
666
+ ${renderError(modal.error)}
667
+ <div class="flex items-center justify-end gap-3 pt-2">
668
+ <button
669
+ class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
670
+ data-action="close-modal"
671
+ type="button"
672
+ >
673
+ Cancel
674
+ </button>
675
+ <button
676
+ class="border border-error/25 bg-error-container/10 px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-error"
677
+ type="submit"
678
+ >
679
+ ${modal.submitting ? "Deleting..." : "Delete Chart"}
680
+ </button>
681
+ </div>
682
+ </form>
683
+ `;
684
+ }
685
+
383
686
  export function renderModal(state) {
384
687
  const modal = state.modal;
385
688
 
@@ -413,6 +716,16 @@ export function renderModal(state) {
413
716
  title: "Delete Row",
414
717
  body: renderDeleteRowConfirmForm(modal),
415
718
  },
719
+ "chart-editor": {
720
+ eyebrow: "Charts // Configure query-based ECharts panel",
721
+ title: modal.draft?.mode === "edit" ? "Edit Chart" : "New Chart",
722
+ body: renderChartEditorForm(modal, state),
723
+ },
724
+ "delete-chart": {
725
+ eyebrow: "Charts // Confirm chart deletion",
726
+ title: "Delete Chart",
727
+ body: renderDeleteChartForm(modal),
728
+ },
416
729
  };
417
730
 
418
731
  const config = contentByKind[modal.kind];
@@ -423,7 +736,7 @@ export function renderModal(state) {
423
736
 
424
737
  return `
425
738
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
426
- <div class="w-full max-w-xl border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
739
+ <div class="w-full ${modal.kind === "chart-editor" ? "max-w-3xl" : "max-w-xl"} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
427
740
  <div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
428
741
  <div>
429
742
  <div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">
@@ -0,0 +1,125 @@
1
+ import {
2
+ buildBarChartOption,
3
+ buildLineChartOption,
4
+ buildPieChartOption,
5
+ buildScatterChartOption,
6
+ } from "../lib/queryChartOptions.js";
7
+ import { analyzeQueryChartResult, validateQueryChartConfig } from "../lib/queryCharts.js";
8
+
9
+ const chartInstances = new Map();
10
+ const resizeObservers = new Map();
11
+
12
+ function getEchartsRuntime() {
13
+ return window.echarts ?? null;
14
+ }
15
+
16
+ function getOptionBuilder(chartType) {
17
+ if (chartType === "bar") {
18
+ return buildBarChartOption;
19
+ }
20
+
21
+ if (chartType === "line") {
22
+ return buildLineChartOption;
23
+ }
24
+
25
+ if (chartType === "pie") {
26
+ return buildPieChartOption;
27
+ }
28
+
29
+ if (chartType === "scatter") {
30
+ return buildScatterChartOption;
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ function disposeChart(chartId) {
37
+ const instance = chartInstances.get(chartId);
38
+
39
+ if (instance) {
40
+ instance.dispose();
41
+ chartInstances.delete(chartId);
42
+ }
43
+
44
+ const observer = resizeObservers.get(chartId);
45
+
46
+ if (observer) {
47
+ observer.disconnect();
48
+ resizeObservers.delete(chartId);
49
+ }
50
+ }
51
+
52
+ export function teardownQueryChartRenderer() {
53
+ Array.from(chartInstances.keys()).forEach((chartId) => disposeChart(chartId));
54
+ }
55
+
56
+ export function mountQueryChartRenderer(state) {
57
+ const echartsRuntime = getEchartsRuntime();
58
+ const charts = state.charts.detail?.charts ?? [];
59
+ const result = state.charts.result;
60
+
61
+ teardownQueryChartRenderer();
62
+
63
+ if (!echartsRuntime || !result || !charts.length) {
64
+ return;
65
+ }
66
+
67
+ const analysis = analyzeQueryChartResult(result);
68
+
69
+ charts.forEach((chart) => {
70
+ const host = document.querySelector(`[data-query-chart-id="${CSS.escape(String(chart.id))}"]`);
71
+
72
+ if (!(host instanceof HTMLElement)) {
73
+ return;
74
+ }
75
+
76
+ const validation = validateQueryChartConfig(chart.chartType, chart.config, analysis);
77
+
78
+ if (!validation.valid) {
79
+ return;
80
+ }
81
+
82
+ const buildOption = getOptionBuilder(chart.chartType);
83
+
84
+ if (!buildOption) {
85
+ return;
86
+ }
87
+
88
+ const instance = echartsRuntime.init(host);
89
+
90
+ instance.setOption(buildOption(chart, result.rows ?? [], analysis), true);
91
+ chartInstances.set(String(chart.id), instance);
92
+
93
+ const resizeObserver = new ResizeObserver(() => {
94
+ instance.resize();
95
+ });
96
+
97
+ resizeObserver.observe(host);
98
+ resizeObservers.set(String(chart.id), resizeObserver);
99
+ });
100
+ }
101
+
102
+ export function exportQueryChartAsPng(chartId) {
103
+ const instance = chartInstances.get(String(chartId));
104
+
105
+ if (!instance) {
106
+ return false;
107
+ }
108
+
109
+ const link = document.createElement("a");
110
+ const chartNode = document.querySelector(
111
+ `[data-query-chart-id="${CSS.escape(String(chartId))}"]`
112
+ );
113
+ const fileName = chartNode?.dataset.chartExportName ?? `chart-${chartId}`;
114
+
115
+ link.href = instance.getDataURL({
116
+ type: "png",
117
+ pixelRatio: 2,
118
+ backgroundColor: "#131313",
119
+ });
120
+ link.download = `${fileName}.png`;
121
+ document.body.appendChild(link);
122
+ link.click();
123
+ link.remove();
124
+ return true;
125
+ }
@@ -28,11 +28,11 @@ function renderEditorSurface({ query }) {
28
28
  <span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
29
29
  </div>
30
30
  <div class="query-editor-layer relative z-10 h-full min-h-[140px]">
31
- <pre
31
+ <div
32
32
  aria-hidden="true"
33
33
  class="query-editor-highlight"
34
34
  data-query-editor-highlight
35
- >${renderHighlightedQuery(query)}</pre>
35
+ >${renderHighlightedQuery(query)}</div>
36
36
  <textarea
37
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
38
  data-bind="current-query"
@@ -52,7 +52,11 @@ export function renderQueryEditor({
52
52
  exporting = false,
53
53
  historyLoading = false,
54
54
  historyTotal = 0,
55
+ editorVisible = true,
56
+ historyVisible = true,
55
57
  }) {
58
+ const secondaryButtonClass =
59
+ "toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-bold uppercase tracking-widest text-on-surface transition-colors hover:border-primary-container hover:text-primary-container";
56
60
  const left = `
57
61
  <div class="flex items-center gap-2 bg-surface-container-lowest px-3 py-1">
58
62
  <span class="material-symbols-outlined text-xs text-[#FCE300]">database</span>
@@ -68,22 +72,41 @@ export function renderQueryEditor({
68
72
 
69
73
  const right = `
70
74
  <button
71
- class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
75
+ class="${secondaryButtonClass}"
76
+ data-action="toggle-editor-panel"
77
+ data-next-value="${editorVisible ? "false" : "true"}"
78
+ type="button"
79
+ >
80
+ <span class="material-symbols-outlined text-sm">${editorVisible ? "keyboard_arrow_down" : "terminal"}</span>
81
+ ${editorVisible ? "Hide Editor" : "Show Editor"}
82
+ </button>
83
+ <button
84
+ class="${secondaryButtonClass}"
85
+ data-action="toggle-query-history-panel"
86
+ data-next-value="${historyVisible ? "false" : "true"}"
87
+ type="button"
88
+ >
89
+ <span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "history"}</span>
90
+ ${historyVisible ? "Hide History" : "Show History"}
91
+ </button>
92
+ <button
93
+ class="${secondaryButtonClass}"
72
94
  data-action="clear-query"
73
95
  type="button"
74
96
  >
75
97
  Clear
76
98
  </button>
77
99
  <button
78
- class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
100
+ class="${secondaryButtonClass}"
79
101
  data-action="export-query-csv"
80
102
  type="button"
81
103
  >
82
104
  ${exporting ? "Exporting..." : "Export CSV"}
83
105
  </button>
84
106
  <button
85
- class="bg-primary-container px-6 py-1.5 text-xs font-black uppercase tracking-tighter text-on-primary shadow-[0_0_15px_-5px_rgba(252,227,0,0.4)] transition-all hover:brightness-110"
107
+ class="toolbar-button toolbar-button--primary bg-primary-container px-4 py-2 font-headline text-xs font-bold uppercase tracking-widest text-on-primary clipped-corner"
86
108
  data-action="execute-query"
109
+ style="--clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);"
87
110
  type="button"
88
111
  >
89
112
  ${executing ? "RUNNING..." : "EXECUTE"}
@@ -99,9 +122,15 @@ export function renderQueryEditor({
99
122
  className: "flex-wrap",
100
123
  })}
101
124
  </div>
102
- <div class="flex min-h-0 flex-1 flex-col">
103
- ${renderEditorSurface({ query })}
104
- </div>
125
+ ${
126
+ editorVisible
127
+ ? `
128
+ <div class="flex min-h-0 flex-1 flex-col">
129
+ ${renderEditorSurface({ query })}
130
+ </div>
131
+ `
132
+ : ""
133
+ }
105
134
  </div>
106
135
  `;
107
136
  }
@@ -150,11 +150,22 @@ export function renderQueryHistoryPanel({
150
150
  return `
151
151
  <aside class="query-history-panel border-l border-outline-variant/10 bg-surface-container-lowest">
152
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>
153
+ <div class="flex items-center justify-between gap-3">
154
+ <div class="flex items-center gap-2">
155
+ <span class="material-symbols-outlined text-[18px] text-primary-container">history</span>
156
+ <span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
157
+ Query History
158
+ </span>
159
+ </div>
160
+ <button
161
+ class="query-history-icon-button"
162
+ data-action="toggle-query-history-panel"
163
+ data-next-value="false"
164
+ title="Hide query history"
165
+ type="button"
166
+ >
167
+ <span class="material-symbols-outlined text-[18px]">close</span>
168
+ </button>
158
169
  </div>
159
170
  <div class="mt-4">${renderQueryHistoryTabs(activeTab, total)}</div>
160
171
  <label class="mt-4 block">
@@ -5,8 +5,10 @@ const sidebarItems = [
5
5
  { label: "Connections", href: "#/connections", key: "connections", icon: "database" },
6
6
  { label: "Overview", href: "#/overview", key: "overview", icon: "dashboard" },
7
7
  { label: "Data", href: "#/data", key: "data", icon: "table_rows" },
8
+ { label: "Charts", href: "#/charts", key: "charts", icon: "bar_chart" },
8
9
  { label: "SQL Editor", href: "#/editor", key: "editor", icon: "terminal" },
9
10
  { label: "Structure", href: "#/structure", key: "structure", icon: "account_tree" },
11
+ { label: "Table Designer", href: "#/table-designer", key: "tableDesigner", icon: "table_chart" },
10
12
  { label: "Settings", href: "#/settings", key: "settings", icon: "settings" },
11
13
  ];
12
14