sqlite-hub 0.6.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.
@@ -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,6 +5,7 @@ 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" },
10
11
  { label: "Table Designer", href: "#/table-designer", key: "tableDesigner", icon: "table_chart" },
@@ -1,28 +1,29 @@
1
- import { escapeHtml, formatNumber } from "../utils/format.js";
1
+ import { escapeHtml, formatNumber } from '../utils/format.js';
2
2
 
3
3
  function getFilteredTables(tables, searchQuery) {
4
- const normalizedSearch = String(searchQuery ?? "").trim().toLowerCase();
4
+ const normalizedSearch = String(searchQuery ?? '')
5
+ .trim()
6
+ .toLowerCase();
5
7
 
6
- if (!normalizedSearch) {
7
- return tables;
8
- }
8
+ if (!normalizedSearch) {
9
+ return tables;
10
+ }
9
11
 
10
- return tables.filter((table) => table.name.toLowerCase().includes(normalizedSearch));
12
+ return tables.filter(table => table.name.toLowerCase().includes(normalizedSearch));
11
13
  }
12
14
 
13
15
  export function renderTableDesignerSidebar(state) {
14
- const tables = state.tableDesigner.tables ?? [];
15
- const filteredTables = getFilteredTables(tables, state.tableDesigner.searchQuery);
16
- const isNewDraft = state.tableDesigner.draft?.mode === "create";
16
+ const tables = state.tableDesigner.tables ?? [];
17
+ const filteredTables = getFilteredTables(tables, state.tableDesigner.searchQuery);
18
+ const isNewDraft = state.tableDesigner.draft?.mode === 'create';
17
19
 
18
- return `
20
+ return `
19
21
  <aside class="table-designer-sidebar">
20
22
  <div class="table-designer-sidebar__header">
21
23
  <div>
22
- <div class="table-designer-sidebar__eyebrow">Schema Workspace</div>
23
- <div class="table-designer-sidebar__title">Table Designer</div>
24
+ <div class="table-designer-sidebar__eyebrow">Table Designer</div>
24
25
  <div class="table-designer-sidebar__meta">
25
- ${escapeHtml(formatNumber(tables.length))} table${tables.length === 1 ? "" : "s"}
26
+ ${escapeHtml(formatNumber(tables.length))} table${tables.length === 1 ? '' : 's'}
26
27
  </div>
27
28
  </div>
28
29
  <div class="table-designer-sidebar__header-actions">
@@ -58,23 +59,23 @@ export function renderTableDesignerSidebar(state) {
58
59
  placeholder="Search tables..."
59
60
  spellcheck="false"
60
61
  type="search"
61
- value="${escapeHtml(state.tableDesigner.searchQuery ?? "")}"
62
+ value="${escapeHtml(state.tableDesigner.searchQuery ?? '')}"
62
63
  />
63
64
  </label>
64
65
 
65
66
  <div class="table-designer-sidebar__list custom-scrollbar">
66
67
  ${
67
- state.tableDesigner.loading && !tables.length
68
- ? `
68
+ state.tableDesigner.loading && !tables.length
69
+ ? `
69
70
  <div class="table-designer-sidebar__empty">
70
71
  <span class="material-symbols-outlined mb-2 text-3xl">progress_activity</span>
71
72
  <div>Loading SQLite schema...</div>
72
73
  </div>
73
74
  `
74
- : isNewDraft
75
- ? `
75
+ : isNewDraft
76
+ ? `
76
77
  <button
77
- class="table-designer-sidebar__item is-active"
78
+ class="table-designer-sidebar__item is-active w-full border border-primary-container/30 bg-surface-container-high px-4 py-3 text-left transition-colors"
78
79
  data-action="navigate"
79
80
  data-to="/table-designer/new"
80
81
  type="button"
@@ -83,42 +84,44 @@ export function renderTableDesignerSidebar(state) {
83
84
  <div class="table-designer-sidebar__item-meta">unsaved schema</div>
84
85
  </button>
85
86
  `
86
- : ""
87
+ : ''
87
88
  }
88
89
  ${
89
- !filteredTables.length && !state.tableDesigner.loading
90
- ? `
90
+ !filteredTables.length && !state.tableDesigner.loading
91
+ ? `
91
92
  <div class="table-designer-sidebar__empty">
92
93
  ${
93
- tables.length
94
- ? "No tables match the current search."
95
- : "No tables found. Create the first table in this database."
94
+ tables.length
95
+ ? 'No tables match the current search.'
96
+ : 'No tables found. Create the first table in this database.'
96
97
  }
97
98
  </div>
98
99
  `
99
- : filteredTables
100
- .map(
101
- (table) => `
100
+ : filteredTables
101
+ .map(
102
+ table => `
102
103
  <button
103
- class="table-designer-sidebar__item ${
104
- !isNewDraft && table.name === state.tableDesigner.selectedTableName
105
- ? "is-active"
106
- : ""
104
+ class="table-designer-sidebar__item w-full border px-4 py-3 text-left transition-colors ${
105
+ !isNewDraft && table.name === state.tableDesigner.selectedTableName
106
+ ? 'is-active border-primary-container/30 bg-surface-container-high'
107
+ : 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
107
108
  }"
108
109
  data-action="navigate"
109
110
  data-to="/table-designer/${encodeURIComponent(table.name)}"
110
111
  type="button"
111
112
  >
112
- <div class="table-designer-sidebar__item-name">${escapeHtml(table.name)}</div>
113
- <div class="table-designer-sidebar__item-meta">
113
+ <div class="table-designer-sidebar__item-name ${
114
+ !isNewDraft && table.name === state.tableDesigner.selectedTableName ? 'is-active' : ''
115
+ }">${escapeHtml(table.name)}</div>
116
+ <div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
114
117
  ${escapeHtml(formatNumber(table.columnCount ?? 0))} column${
115
- Number(table.columnCount ?? 0) === 1 ? "" : "s"
118
+ Number(table.columnCount ?? 0) === 1 ? '' : 's'
116
119
  }
117
120
  </div>
118
121
  </button>
119
- `
120
- )
121
- .join("")
122
+ `,
123
+ )
124
+ .join('')
122
125
  }
123
126
  </div>
124
127
  </aside>
@@ -0,0 +1,283 @@
1
+ import {
2
+ coerceDatetimeChartValue,
3
+ coerceNumericChartValue,
4
+ formatTimeOfDayAxisValue,
5
+ formatQueryChartAxisValue,
6
+ getAnalysisColumn,
7
+ sortQueryChartRows,
8
+ } from "./queryCharts.js";
9
+
10
+ const CHART_PALETTE = ["#FCE300", "#2DFAFF", "#FFB4AB", "#CDC7AB", "#7DD3FC", "#86EFAC"];
11
+
12
+ function buildCommonOption() {
13
+ return {
14
+ animationDuration: 220,
15
+ backgroundColor: "transparent",
16
+ color: CHART_PALETTE,
17
+ textStyle: {
18
+ color: "#e5e2e1",
19
+ fontFamily: "Inter, sans-serif",
20
+ },
21
+ tooltip: {
22
+ backgroundColor: "#201f1f",
23
+ borderColor: "rgba(252, 227, 0, 0.14)",
24
+ textStyle: {
25
+ color: "#e5e2e1",
26
+ },
27
+ },
28
+ grid: {
29
+ left: 48,
30
+ right: 24,
31
+ top: 36,
32
+ bottom: 48,
33
+ containLabel: true,
34
+ },
35
+ };
36
+ }
37
+
38
+ function buildAxisLabel(color = "#cdc7ab") {
39
+ return {
40
+ color,
41
+ fontFamily: "Roboto Mono, monospace",
42
+ fontSize: 11,
43
+ };
44
+ }
45
+
46
+ function buildLineLabelConfig(enabled) {
47
+ return enabled
48
+ ? {
49
+ show: true,
50
+ color: "#e5e2e1",
51
+ fontFamily: "Roboto Mono, monospace",
52
+ fontSize: 10,
53
+ }
54
+ : { show: false };
55
+ }
56
+
57
+ export function buildBarChartOption(chart, rows) {
58
+ const sortedRows = sortQueryChartRows(rows, chart.config.x_column, chart.config.sort_direction);
59
+
60
+ return {
61
+ ...buildCommonOption(),
62
+ legend: {
63
+ show: chart.config.show_legend,
64
+ textStyle: { color: "#cdc7ab" },
65
+ },
66
+ xAxis: {
67
+ type: "category",
68
+ data: sortedRows.map((row) => formatQueryChartAxisValue(row[chart.config.x_column])),
69
+ axisLabel: buildAxisLabel(),
70
+ axisLine: { lineStyle: { color: "rgba(205, 199, 171, 0.28)" } },
71
+ },
72
+ yAxis: {
73
+ type: "value",
74
+ axisLabel: buildAxisLabel(),
75
+ splitLine: { lineStyle: { color: "rgba(205, 199, 171, 0.12)" } },
76
+ },
77
+ series: [
78
+ {
79
+ name: chart.name,
80
+ type: "bar",
81
+ label: buildLineLabelConfig(chart.config.show_labels),
82
+ emphasis: { focus: "series" },
83
+ data: sortedRows.map((row) => coerceNumericChartValue(row[chart.config.y_column]) ?? 0),
84
+ },
85
+ ],
86
+ };
87
+ }
88
+
89
+ export function buildLineChartOption(chart, rows) {
90
+ const sortedRows = sortQueryChartRows(rows, chart.config.x_column, chart.config.sort_direction);
91
+
92
+ return {
93
+ ...buildCommonOption(),
94
+ legend: {
95
+ show: chart.config.show_legend,
96
+ textStyle: { color: "#cdc7ab" },
97
+ },
98
+ xAxis: {
99
+ type: "category",
100
+ data: sortedRows.map((row) => formatQueryChartAxisValue(row[chart.config.x_column])),
101
+ axisLabel: buildAxisLabel(),
102
+ axisLine: { lineStyle: { color: "rgba(205, 199, 171, 0.28)" } },
103
+ },
104
+ yAxis: {
105
+ type: "value",
106
+ axisLabel: buildAxisLabel(),
107
+ splitLine: { lineStyle: { color: "rgba(205, 199, 171, 0.12)" } },
108
+ },
109
+ series: [
110
+ {
111
+ name: chart.name,
112
+ type: "line",
113
+ smooth: chart.config.smooth,
114
+ showSymbol: sortedRows.length < 60,
115
+ symbolSize: 8,
116
+ lineStyle: {
117
+ width: 3,
118
+ },
119
+ label: buildLineLabelConfig(chart.config.show_labels),
120
+ data: sortedRows.map((row) => coerceNumericChartValue(row[chart.config.y_column]) ?? 0),
121
+ },
122
+ ],
123
+ };
124
+ }
125
+
126
+ export function buildPieChartOption(chart, rows) {
127
+ return {
128
+ ...buildCommonOption(),
129
+ legend: {
130
+ show: chart.config.show_legend,
131
+ orient: "vertical",
132
+ right: 0,
133
+ top: "middle",
134
+ textStyle: { color: "#cdc7ab" },
135
+ },
136
+ series: [
137
+ {
138
+ name: chart.name,
139
+ type: "pie",
140
+ radius: chart.config.donut ? ["42%", "70%"] : "70%",
141
+ center: chart.config.show_legend ? ["36%", "50%"] : ["50%", "50%"],
142
+ label: {
143
+ show: chart.config.show_labels,
144
+ color: "#e5e2e1",
145
+ formatter: "{b}: {c}",
146
+ fontSize: 11,
147
+ },
148
+ data: rows.map((row) => ({
149
+ name: formatQueryChartAxisValue(row[chart.config.label_column]),
150
+ value: coerceNumericChartValue(row[chart.config.value_column]) ?? 0,
151
+ })),
152
+ },
153
+ ],
154
+ };
155
+ }
156
+
157
+ function resolveScatterAxisType(column) {
158
+ if (column?.type === "datetime") {
159
+ return column.datetimeKind === "time" ? "time-of-day" : "time";
160
+ }
161
+
162
+ return "value";
163
+ }
164
+
165
+ function buildScatterAxisConfig(column, columnName) {
166
+ const axisType = resolveScatterAxisType(column);
167
+ const baseConfig = {
168
+ axisLabel: buildAxisLabel(),
169
+ splitLine: { lineStyle: { color: "rgba(205, 199, 171, 0.12)" } },
170
+ name: columnName,
171
+ nameTextStyle: buildAxisLabel("#FCE300"),
172
+ };
173
+
174
+ if (axisType === "time") {
175
+ return {
176
+ ...baseConfig,
177
+ type: "time",
178
+ };
179
+ }
180
+
181
+ if (axisType === "time-of-day") {
182
+ return {
183
+ ...baseConfig,
184
+ type: "value",
185
+ axisLabel: {
186
+ ...buildAxisLabel(),
187
+ formatter: (value) => formatTimeOfDayAxisValue(value),
188
+ },
189
+ };
190
+ }
191
+
192
+ return {
193
+ ...baseConfig,
194
+ type: "value",
195
+ };
196
+ }
197
+
198
+ function coerceScatterAxisValue(value, axisType) {
199
+ if (axisType === "time" || axisType === "time-of-day") {
200
+ return coerceDatetimeChartValue(value);
201
+ }
202
+
203
+ return coerceNumericChartValue(value);
204
+ }
205
+
206
+ function buildScatterSeries(chart, rows, analysis) {
207
+ const groups = new Map();
208
+ const xColumn = getAnalysisColumn(analysis, chart.config.x_column);
209
+ const yColumn = getAnalysisColumn(analysis, chart.config.y_column);
210
+ const xAxisType = resolveScatterAxisType(xColumn);
211
+ const yAxisType = resolveScatterAxisType(yColumn);
212
+ const sizeValues = rows
213
+ .map((row) => coerceNumericChartValue(row[chart.config.size_column]))
214
+ .filter((value) => value !== null);
215
+ const minSize = sizeValues.length ? Math.min(...sizeValues) : 0;
216
+ const maxSize = sizeValues.length ? Math.max(...sizeValues) : 0;
217
+
218
+ function resolveSymbolSize(sizeValue) {
219
+ if (sizeValue === null || sizeValue === undefined) {
220
+ return 14;
221
+ }
222
+
223
+ if (maxSize <= minSize) {
224
+ return 20;
225
+ }
226
+
227
+ return 10 + ((sizeValue - minSize) / (maxSize - minSize)) * 24;
228
+ }
229
+
230
+ rows.forEach((row) => {
231
+ const seriesName = chart.config.series_column
232
+ ? formatQueryChartAxisValue(row[chart.config.series_column])
233
+ : chart.name;
234
+ const x = coerceScatterAxisValue(row[chart.config.x_column], xAxisType);
235
+ const y = coerceScatterAxisValue(row[chart.config.y_column], yAxisType);
236
+
237
+ if (x === null || y === null) {
238
+ return;
239
+ }
240
+
241
+ const point = {
242
+ value: [
243
+ x,
244
+ y,
245
+ chart.config.size_column ? coerceNumericChartValue(row[chart.config.size_column]) : null,
246
+ ],
247
+ symbolSize: resolveSymbolSize(
248
+ chart.config.size_column ? coerceNumericChartValue(row[chart.config.size_column]) : null
249
+ ),
250
+ };
251
+
252
+ if (!groups.has(seriesName)) {
253
+ groups.set(seriesName, []);
254
+ }
255
+
256
+ groups.get(seriesName).push(point);
257
+ });
258
+
259
+ return Array.from(groups.entries()).map(([seriesName, data]) => ({
260
+ name: seriesName,
261
+ type: "scatter",
262
+ data,
263
+ symbolSize(value, params) {
264
+ return params?.data?.symbolSize ?? resolveSymbolSize(value?.[2] ?? null);
265
+ },
266
+ }));
267
+ }
268
+
269
+ export function buildScatterChartOption(chart, rows, analysis = null) {
270
+ const xColumn = getAnalysisColumn(analysis, chart.config.x_column);
271
+ const yColumn = getAnalysisColumn(analysis, chart.config.y_column);
272
+
273
+ return {
274
+ ...buildCommonOption(),
275
+ legend: {
276
+ show: chart.config.show_legend,
277
+ textStyle: { color: "#cdc7ab" },
278
+ },
279
+ xAxis: buildScatterAxisConfig(xColumn, chart.config.x_column),
280
+ yAxis: buildScatterAxisConfig(yColumn, chart.config.y_column),
281
+ series: buildScatterSeries(chart, rows, analysis),
282
+ };
283
+ }