sqlite-hub 0.7.0 → 0.9.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 (45) hide show
  1. package/changelog.md +25 -0
  2. package/docs/DESIGN_GUIDELINES.md +36 -0
  3. package/frontend/index.html +79 -0
  4. package/frontend/js/api.js +67 -0
  5. package/frontend/js/app.js +1401 -921
  6. package/frontend/js/components/connectionCard.js +3 -4
  7. package/frontend/js/components/emptyState.js +4 -4
  8. package/frontend/js/components/modal.js +239 -30
  9. package/frontend/js/components/queryEditor.js +11 -8
  10. package/frontend/js/components/queryHistoryDetail.js +27 -8
  11. package/frontend/js/components/queryHistoryPanel.js +6 -5
  12. package/frontend/js/components/rowEditorPanel.js +84 -58
  13. package/frontend/js/components/sidebar.js +76 -33
  14. package/frontend/js/components/structureGraph.js +629 -715
  15. package/frontend/js/components/tableDesignerEditor.js +9 -8
  16. package/frontend/js/components/tableDesignerSidebar.js +11 -10
  17. package/frontend/js/components/tableDesignerSqlPreview.js +61 -30
  18. package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
  19. package/frontend/js/router.js +81 -71
  20. package/frontend/js/store.js +3095 -2165
  21. package/frontend/js/views/charts.js +68 -40
  22. package/frontend/js/views/connections.js +5 -17
  23. package/frontend/js/views/data.js +40 -27
  24. package/frontend/js/views/editor.js +172 -177
  25. package/frontend/js/views/mediaTagging.js +861 -0
  26. package/frontend/js/views/overview.js +149 -10
  27. package/frontend/js/views/settings.js +2 -2
  28. package/frontend/js/views/structure.js +74 -70
  29. package/frontend/js/views/tableDesigner.js +7 -2
  30. package/frontend/styles/base.css +73 -1
  31. package/frontend/styles/components.css +105 -105
  32. package/frontend/styles/structure-graph.css +19 -82
  33. package/frontend/styles/tokens.css +2 -0
  34. package/frontend/styles/views.css +823 -30
  35. package/package.json +1 -1
  36. package/server/routes/charts.js +4 -1
  37. package/server/routes/data.js +14 -0
  38. package/server/routes/mediaTagging.js +166 -0
  39. package/server/routes/sql.js +1 -0
  40. package/server/server.js +4 -0
  41. package/server/services/sqlite/dataBrowserService.js +25 -0
  42. package/server/services/sqlite/exportService.js +31 -1
  43. package/server/services/sqlite/mediaTaggingService.js +1689 -0
  44. package/server/services/sqlite/overviewService.js +68 -0
  45. package/server/services/storage/appStateStore.js +321 -2
@@ -20,7 +20,7 @@ function getQueryTypeTone(queryType, isDestructive) {
20
20
  return "muted";
21
21
  }
22
22
 
23
- function renderHistoryItem(item, activeHistoryId, selectedHistoryId) {
23
+ export function renderQueryHistoryListItem(item, activeHistoryId, selectedHistoryId) {
24
24
  const isActive = Number(activeHistoryId) === Number(item.id);
25
25
  const isSelected = Number(selectedHistoryId) === Number(item.id);
26
26
  const visibleTables = (item.tablesDetected ?? []).slice(0, 3);
@@ -109,6 +109,7 @@ function renderQueryHistoryTabs(activeTab, historyTotal) {
109
109
  const tabs = [
110
110
  { id: "recent", label: "Recent" },
111
111
  { id: "saved", label: "Saved" },
112
+ { id: "unsaved", label: "Unsaved" },
112
113
  { id: "failed", label: "Failed" },
113
114
  ];
114
115
 
@@ -171,7 +172,7 @@ export function renderQueryHistoryPanel({
171
172
  <label class="mt-4 block">
172
173
  <span class="sr-only">Search query history</span>
173
174
  <input
174
- class="w-full border border-outline-variant/20 bg-surface-container px-3 py-2 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
175
+ 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"
175
176
  data-bind="query-history-search"
176
177
  placeholder="Search SQL, titles, notes..."
177
178
  type="search"
@@ -204,9 +205,9 @@ export function renderQueryHistoryPanel({
204
205
  `
205
206
  : ""
206
207
  }
207
- <div class="space-y-3">
208
+ <div class="space-y-3">
208
209
  ${items
209
- .map((item) => renderHistoryItem(item, activeHistoryId, selectedHistoryId))
210
+ .map((item) => renderQueryHistoryListItem(item, activeHistoryId, selectedHistoryId))
210
211
  .join("")}
211
212
  </div>
212
213
  ${
@@ -223,7 +224,7 @@ export function renderQueryHistoryPanel({
223
224
  ? `
224
225
  <div class="mt-4 flex justify-center">
225
226
  <button
226
- class="border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant transition-colors hover:border-primary-container hover:text-primary-container"
227
+ class="standard-button"
227
228
  data-action="load-more-query-history"
228
229
  type="button"
229
230
  >
@@ -42,12 +42,23 @@ function renderJsonViewer(prettyJson, title = "JSON Viewer") {
42
42
  }
43
43
 
44
44
  function renderReadonlyField(label, value) {
45
+ const badges = Array.isArray(label?.badges) ? label.badges : [];
46
+ const displayLabel = typeof label === "object" ? label.label : label;
45
47
  const jsonPreview = getJsonPreview(value);
46
48
 
47
49
  return `
48
50
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
49
- <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
50
- ${escapeHtml(label)}
51
+ <div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
52
+ <span>${escapeHtml(displayLabel)}</span>
53
+ ${badges
54
+ .map(
55
+ (badge) => `
56
+ <span class="border border-outline-variant/20 bg-surface-container px-2 py-1 text-[9px] text-primary-container">
57
+ ${escapeHtml(badge)}
58
+ </span>
59
+ `
60
+ )
61
+ .join("")}
51
62
  </div>
52
63
  ${
53
64
  jsonPreview
@@ -59,12 +70,22 @@ function renderReadonlyField(label, value) {
59
70
  }
60
71
 
61
72
  function renderEditableField(field) {
73
+ const badges = Array.isArray(field.badges) ? field.badges : [];
62
74
  const jsonPreview = getJsonPreview(field.value);
63
75
 
64
76
  return `
65
77
  <label class="block space-y-2">
66
- <span class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
67
- ${escapeHtml(field.label ?? field.name)}
78
+ <span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
79
+ <span>${escapeHtml(field.label ?? field.name)}</span>
80
+ ${badges
81
+ .map(
82
+ (badge) => `
83
+ <span class="border border-outline-variant/20 bg-surface-container px-2 py-1 text-[9px] text-primary-container">
84
+ ${escapeHtml(badge)}
85
+ </span>
86
+ `
87
+ )
88
+ .join("")}
68
89
  </span>
69
90
  ${
70
91
  jsonPreview
@@ -106,12 +127,52 @@ export function renderRowEditorPanel({
106
127
  const canSubmit = !disabledMessage && editableFields.length > 0;
107
128
  const canDelete = !disabledMessage && deleteEnabled;
108
129
  const formId = `${formName}-panel-form`;
130
+ const headerActions = [
131
+ reloadAction
132
+ ? `
133
+ <button
134
+ class="standard-button"
135
+ data-action="${escapeHtml(reloadAction)}"
136
+ type="button"
137
+ >
138
+ Reload
139
+ </button>
140
+ `
141
+ : "",
142
+ canSubmit
143
+ ? `
144
+ <button
145
+ class="standard-button"
146
+ form="${escapeHtml(formId)}"
147
+ type="submit"
148
+ ${saving || deleting ? "disabled" : ""}
149
+ >
150
+ ${escapeHtml(saving ? "Saving..." : submitLabel)}
151
+ </button>
152
+ `
153
+ : "",
154
+ canDelete
155
+ ? `
156
+ <button
157
+ class="delete-button"
158
+ data-action="${escapeHtml(deleteAction)}"
159
+ ${deleteRowIndex === null ? "" : `data-row-index="${escapeHtml(String(deleteRowIndex))}"`}
160
+ type="button"
161
+ ${saving || deleting ? "disabled" : ""}
162
+ >
163
+ ${escapeHtml(deleting ? "Deleting..." : deleteLabel)}
164
+ </button>
165
+ `
166
+ : "",
167
+ ]
168
+ .filter(Boolean)
169
+ .join("");
109
170
 
110
171
  return `
111
172
  <section class="flex h-full min-h-0 flex-col bg-surface-low">
112
173
  <header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
113
- <div class="space-y-4">
114
- <div>
174
+ <div class="flex items-start justify-between gap-4">
175
+ <div class="min-w-0 flex-1">
115
176
  <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
116
177
  ${escapeHtml(sectionLabel)}
117
178
  </div>
@@ -128,59 +189,24 @@ export function renderRowEditorPanel({
128
189
  : ""
129
190
  }
130
191
  </div>
131
- <div class="flex flex-wrap items-center justify-end gap-2">
132
- ${
133
- reloadAction
134
- ? `
135
- <button
136
- class="border border-outline-variant/20 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface hover:bg-surface-container-highest"
137
- data-action="${escapeHtml(reloadAction)}"
138
- type="button"
139
- >
140
- Reload
141
- </button>
142
- `
143
- : ""
144
- }
145
- ${
146
- canSubmit
147
- ? `
148
- <button
149
- class="bg-primary-container px-5 py-3 text-[10px] font-black uppercase tracking-[0.16em] text-on-primary disabled:cursor-default disabled:opacity-40"
150
- form="${escapeHtml(formId)}"
151
- type="submit"
152
- ${saving || deleting ? "disabled" : ""}
153
- >
154
- ${escapeHtml(saving ? "Saving..." : submitLabel)}
155
- </button>
156
- `
157
- : ""
158
- }
159
- ${
160
- canDelete
161
- ? `
162
- <button
163
- class="border border-error/25 bg-error-container/10 px-4 py-3 text-[10px] font-bold uppercase tracking-[0.16em] text-error transition-colors hover:bg-error-container/20 disabled:cursor-default disabled:opacity-40"
164
- data-action="${escapeHtml(deleteAction)}"
165
- data-row-index="${escapeHtml(String(deleteRowIndex ?? ""))}"
166
- type="button"
167
- ${saving || deleting ? "disabled" : ""}
168
- >
169
- ${escapeHtml(deleting ? "Deleting..." : deleteLabel)}
170
- </button>
171
- `
172
- : ""
173
- }
174
- <button
175
- aria-label="Close panel"
176
- class="flex h-11 w-11 items-center justify-center border border-outline-variant/20 text-on-surface hover:bg-surface-container-highest"
177
- data-action="${escapeHtml(closeAction)}"
178
- type="button"
179
- >
180
- <span class="material-symbols-outlined text-base">close</span>
181
- </button>
182
- </div>
192
+ <button
193
+ aria-label="Close panel"
194
+ class="query-history-icon-button shrink-0"
195
+ data-action="${escapeHtml(closeAction)}"
196
+ type="button"
197
+ >
198
+ <span class="material-symbols-outlined text-[18px]">close</span>
199
+ </button>
183
200
  </div>
201
+ ${
202
+ headerActions
203
+ ? `
204
+ <div class="mt-4 flex flex-wrap items-center justify-end gap-2">
205
+ ${headerActions}
206
+ </div>
207
+ `
208
+ : ""
209
+ }
184
210
  </header>
185
211
  <div class="custom-scrollbar flex-1 overflow-auto px-6 py-6">
186
212
  ${
@@ -1,61 +1,104 @@
1
- import { escapeHtml } from "../utils/format.js";
2
- import { renderConnectionLogo } from "./connectionLogo.js";
1
+ import { escapeHtml } from '../utils/format.js';
2
+ import { renderConnectionLogo } from './connectionLogo.js';
3
3
 
4
4
  const sidebarItems = [
5
- { label: "Connections", href: "#/connections", key: "connections", icon: "database" },
6
- { label: "Overview", href: "#/overview", key: "overview", icon: "dashboard" },
7
- { label: "Data", href: "#/data", key: "data", icon: "table_rows" },
8
- { label: "Charts", href: "#/charts", key: "charts", icon: "bar_chart" },
9
- { label: "SQL Editor", href: "#/editor", key: "editor", icon: "terminal" },
10
- { label: "Structure", href: "#/structure", key: "structure", icon: "account_tree" },
11
- { label: "Table Designer", href: "#/table-designer", key: "tableDesigner", icon: "table_chart" },
12
- { label: "Settings", href: "#/settings", key: "settings", icon: "settings" },
5
+ { label: 'Connections', href: '#/connections', key: 'connections', icon: 'database' },
6
+ { label: 'Overview', href: '#/overview', key: 'overview', icon: 'dashboard' },
7
+ { label: 'Data', href: '#/data', key: 'data', icon: 'table_rows' },
8
+ { label: 'Structure', href: '#/structure', key: 'structure', icon: 'account_tree' },
9
+ { label: 'SQL Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
10
+ { label: 'Charts', href: '#/charts', key: 'charts', icon: 'bar_chart' },
11
+ { label: 'Table Designer', href: '#/table-designer', key: 'tableDesigner', icon: 'table_chart' },
12
+ {
13
+ label: 'Media Tagging',
14
+ key: 'mediaTagging',
15
+ icon: 'sell',
16
+ children: [
17
+ { label: 'Setup', href: '#/media-tagging', key: 'mediaTaggingSetup' },
18
+ { label: 'Tagging Queue', href: '#/media-tagging/queue', key: 'mediaTaggingQueue' },
19
+ ],
20
+ },
21
+ { label: 'Settings', href: '#/settings', key: 'settings', icon: 'settings' },
13
22
  ];
14
23
 
15
24
  function getActiveSidebarKey(routeName) {
16
- if (routeName === "landing") {
17
- return "connections";
18
- }
25
+ if (routeName === 'landing') {
26
+ return 'connections';
27
+ }
19
28
 
20
- if (routeName === "editorResults") {
21
- return "editor";
22
- }
29
+ if (routeName === 'editorResults') {
30
+ return 'editor';
31
+ }
23
32
 
24
- return routeName;
33
+ if (routeName === 'mediaTaggingSetup' || routeName === 'mediaTaggingQueue') {
34
+ return 'mediaTagging';
35
+ }
36
+
37
+ return routeName;
25
38
  }
26
39
 
27
40
  export function renderSidebar(state) {
28
- const activeKey = getActiveSidebarKey(state.route.name);
29
- const activeConnection = state.connections.active;
41
+ const activeKey = getActiveSidebarKey(state.route.name);
42
+ const activeConnection = state.connections.active;
43
+ const expandedKey = activeKey === 'mediaTagging' ? 'mediaTagging' : null;
30
44
 
31
- return `
45
+ return `
32
46
  <nav class="sidebar-links">
33
47
  ${sidebarItems
34
- .map(
35
- (item) => `
36
- <a class="sidebar-link ${item.key === activeKey ? "is-active" : ""}" href="${item.href}">
48
+ .map(item => {
49
+ if (item.children) {
50
+ const isExpanded = expandedKey === item.key;
51
+ const isActive = activeKey === item.key;
52
+ return `
53
+ <div class="sidebar-group">
54
+ <a class="sidebar-link ${isActive ? 'is-active' : ''}" href="${item.children[0].href}" data-group="${item.key}">
55
+ <span class="material-symbols-outlined">${item.icon}</span>
56
+ <span>${item.label}</span>
57
+ <span class="material-symbols-outlined ml-auto text-[14px] ${isExpanded ? 'rotate-180' : ''}">expand_more</span>
58
+ </a>
59
+ ${
60
+ isExpanded
61
+ ? `
62
+ <div class="sidebar-sublinks">
63
+ ${item.children
64
+ .map(
65
+ child => `
66
+ <a class="sidebar-sublink ${state.route.name === child.key ? 'is-active' : ''}" href="${child.href}">
67
+ ${child.label}
68
+ </a>
69
+ `,
70
+ )
71
+ .join('')}
72
+ </div>
73
+ `
74
+ : ''
75
+ }
76
+ </div>
77
+ `;
78
+ }
79
+ return `
80
+ <a class="sidebar-link ${item.key === activeKey ? 'is-active' : ''}" href="${item.href}">
37
81
  <span class="material-symbols-outlined">${item.icon}</span>
38
82
  <span>${item.label}</span>
39
83
  </a>
40
- `
41
- )
42
- .join("")}
84
+ `;
85
+ })
86
+ .join('')}
43
87
  </nav>
44
88
  <div class="sidebar-footer">
45
89
  <div class="sidebar-footer-card">
46
90
  ${renderConnectionLogo(activeConnection, {
47
- containerClass:
48
- "sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary",
49
- imageClassName: "h-full w-full object-cover",
50
- iconClassName: "text-[15px]",
51
- icon: "memory",
91
+ containerClass: 'sidebar-footer-mark overflow-hidden bg-primary-container text-on-primary',
92
+ imageClassName: 'h-full w-full object-cover',
93
+ iconClassName: 'text-[15px]',
94
+ icon: 'memory',
52
95
  })}
53
96
  <div class="min-w-0">
54
97
  <p class="truncate text-[10px] font-bold text-on-surface">
55
- ${escapeHtml(activeConnection?.label ?? "NO_ACTIVE_DATABASE")}
98
+ ${escapeHtml(activeConnection?.label ?? 'NO_ACTIVE_DATABASE')}
56
99
  </p>
57
100
  <p class="text-[8px] text-on-surface-variant/60">
58
- ${activeConnection?.readOnly ? "READ_ONLY" : "READ_WRITE"}
101
+ ${activeConnection?.readOnly ? 'READ_ONLY' : 'READ_WRITE'}
59
102
  </p>
60
103
  </div>
61
104
  </div>