sqlite-hub 0.1.3

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 (76) hide show
  1. package/.npmingnore +4 -0
  2. package/README.md +46 -0
  3. package/assets/images/logo.webp +0 -0
  4. package/assets/images/logo_extrasmall.webp +0 -0
  5. package/assets/images/logo_raw.png +0 -0
  6. package/assets/images/logo_small.webp +0 -0
  7. package/assets/mockups/connections.png +0 -0
  8. package/assets/mockups/data.png +0 -0
  9. package/assets/mockups/data_edit.png +0 -0
  10. package/assets/mockups/home.png +0 -0
  11. package/assets/mockups/overview.png +0 -0
  12. package/assets/mockups/sql_editor.png +0 -0
  13. package/assets/mockups/structure.png +0 -0
  14. package/bin/sqlite-hub.js +116 -0
  15. package/changelog.md +3 -0
  16. package/data/.gitkeep +0 -0
  17. package/index.html +100 -0
  18. package/js/api.js +193 -0
  19. package/js/app.js +520 -0
  20. package/js/components/actionBar.js +8 -0
  21. package/js/components/appShell.js +17 -0
  22. package/js/components/badges.js +5 -0
  23. package/js/components/bottomTabs.js +37 -0
  24. package/js/components/connectionCard.js +106 -0
  25. package/js/components/dataGrid.js +47 -0
  26. package/js/components/emptyState.js +159 -0
  27. package/js/components/metricCard.js +32 -0
  28. package/js/components/modal.js +317 -0
  29. package/js/components/pageHeader.js +33 -0
  30. package/js/components/queryEditor.js +121 -0
  31. package/js/components/queryResults.js +107 -0
  32. package/js/components/rowEditorPanel.js +164 -0
  33. package/js/components/sidebar.js +57 -0
  34. package/js/components/statusBar.js +39 -0
  35. package/js/components/toast.js +39 -0
  36. package/js/components/topNav.js +27 -0
  37. package/js/router.js +66 -0
  38. package/js/store.js +1092 -0
  39. package/js/utils/format.js +179 -0
  40. package/js/views/connections.js +133 -0
  41. package/js/views/data.js +400 -0
  42. package/js/views/editor.js +259 -0
  43. package/js/views/landing.js +11 -0
  44. package/js/views/overview.js +220 -0
  45. package/js/views/settings.js +109 -0
  46. package/js/views/structure.js +242 -0
  47. package/package.json +18 -0
  48. package/publish_brew.sh +444 -0
  49. package/publish_npm.sh +241 -0
  50. package/server/routes/connections.js +146 -0
  51. package/server/routes/data.js +59 -0
  52. package/server/routes/export.js +25 -0
  53. package/server/routes/overview.js +39 -0
  54. package/server/routes/settings.js +50 -0
  55. package/server/routes/sql.js +50 -0
  56. package/server/routes/structure.js +38 -0
  57. package/server/server.js +136 -0
  58. package/server/services/sqlite/connectionManager.js +306 -0
  59. package/server/services/sqlite/dataBrowserService.js +255 -0
  60. package/server/services/sqlite/exportService.js +34 -0
  61. package/server/services/sqlite/importService.js +111 -0
  62. package/server/services/sqlite/introspection.js +302 -0
  63. package/server/services/sqlite/overviewService.js +109 -0
  64. package/server/services/sqlite/sqlExecutor.js +434 -0
  65. package/server/services/sqlite/structureService.js +60 -0
  66. package/server/services/storage/appStateStore.js +530 -0
  67. package/server/utils/csv.js +34 -0
  68. package/server/utils/errors.js +175 -0
  69. package/server/utils/fileValidation.js +135 -0
  70. package/server/utils/identifier.js +38 -0
  71. package/server/utils/sqliteTypes.js +112 -0
  72. package/styles/base.css +176 -0
  73. package/styles/components.css +323 -0
  74. package/styles/layout.css +101 -0
  75. package/styles/tokens.css +49 -0
  76. package/styles/views.css +84 -0
@@ -0,0 +1,179 @@
1
+ const KEYWORD_PATTERN =
2
+ /\b(WITH|SELECT|FROM|WHERE|AND|OR|ORDER|BY|ASC|DESC|AS|COUNT|FILTER|JOIN|LEFT|RIGHT|INNER|OUTER|ON|GROUP|LIMIT|OFFSET|AVG|SUM|MIN|MAX|OVER|PARTITION|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|PRAGMA|VALUES|INTO|SET|BEGIN|COMMIT|ROLLBACK)\b/gi;
3
+
4
+ const DATE_TIME_FORMATTER = new Intl.DateTimeFormat("en-GB", {
5
+ dateStyle: "medium",
6
+ timeStyle: "short",
7
+ });
8
+
9
+ export function escapeHtml(value = "") {
10
+ return String(value)
11
+ .replaceAll("&", "&")
12
+ .replaceAll("<", "&lt;")
13
+ .replaceAll(">", "&gt;")
14
+ .replaceAll('"', "&quot;")
15
+ .replaceAll("'", "&#39;");
16
+ }
17
+
18
+ export function formatNumber(value) {
19
+ const numeric = Number(value);
20
+ return Number.isFinite(numeric) ? numeric.toLocaleString("en-US") : "0";
21
+ }
22
+
23
+ export function formatBytes(value) {
24
+ const bytes = Number(value);
25
+
26
+ if (!Number.isFinite(bytes) || bytes < 0) {
27
+ return "0 B";
28
+ }
29
+
30
+ if (bytes < 1024) {
31
+ return `${bytes} B`;
32
+ }
33
+
34
+ const units = ["KB", "MB", "GB", "TB"];
35
+ let size = bytes / 1024;
36
+ let unitIndex = 0;
37
+
38
+ while (size >= 1024 && unitIndex < units.length - 1) {
39
+ size /= 1024;
40
+ unitIndex += 1;
41
+ }
42
+
43
+ return `${size.toFixed(size >= 10 ? 0 : 1)} ${units[unitIndex]}`;
44
+ }
45
+
46
+ export function formatDateTime(value) {
47
+ if (!value) {
48
+ return "N/A";
49
+ }
50
+
51
+ const date = new Date(value);
52
+ return Number.isNaN(date.getTime()) ? String(value) : DATE_TIME_FORMATTER.format(date);
53
+ }
54
+
55
+ export function formatRelativeBoolean(value) {
56
+ return value ? "ENABLED" : "DISABLED";
57
+ }
58
+
59
+ export function formatIdentifierLabel(value = "") {
60
+ return String(value || "unknown");
61
+ }
62
+
63
+ export function truncateMiddle(value = "", maxLength = 56) {
64
+ const text = String(value);
65
+
66
+ if (text.length <= maxLength) {
67
+ return text;
68
+ }
69
+
70
+ const sliceLength = Math.floor((maxLength - 3) / 2);
71
+ return `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}`;
72
+ }
73
+
74
+ export function highlightSql(query = "") {
75
+ const lines = String(query).split("\n");
76
+
77
+ return lines
78
+ .map((line) => {
79
+ if (line.trim().startsWith("--")) {
80
+ return `<span class="sql-comment">${escapeHtml(line)}</span>`;
81
+ }
82
+
83
+ const parts = [];
84
+ const tokenPattern = /'[^']*'|\b\d+(?:\.\d+)?\b/g;
85
+ let cursor = 0;
86
+ let match;
87
+
88
+ while ((match = tokenPattern.exec(line))) {
89
+ if (match.index > cursor) {
90
+ parts.push({
91
+ type: "plain",
92
+ value: line.slice(cursor, match.index),
93
+ });
94
+ }
95
+
96
+ parts.push({
97
+ type: match[0].startsWith("'") ? "string" : "value",
98
+ value: match[0],
99
+ });
100
+
101
+ cursor = match.index + match[0].length;
102
+ }
103
+
104
+ if (cursor < line.length) {
105
+ parts.push({
106
+ type: "plain",
107
+ value: line.slice(cursor),
108
+ });
109
+ }
110
+
111
+ return parts
112
+ .map((part) => {
113
+ if (part.type === "string") {
114
+ return `<span class="sql-string">${escapeHtml(part.value)}</span>`;
115
+ }
116
+
117
+ if (part.type === "value") {
118
+ return `<span class="sql-value">${escapeHtml(part.value)}</span>`;
119
+ }
120
+
121
+ return escapeHtml(part.value).replace(
122
+ KEYWORD_PATTERN,
123
+ '<span class="sql-keyword">$1</span>'
124
+ );
125
+ })
126
+ .join("");
127
+ })
128
+ .join("<br/>");
129
+ }
130
+
131
+ export function isBlobPreview(value) {
132
+ return Boolean(value && typeof value === "object" && value.__type === "blob");
133
+ }
134
+
135
+ export function formatBlobPreview(value) {
136
+ if (!isBlobPreview(value)) {
137
+ return "";
138
+ }
139
+
140
+ return `BLOB • ${formatBytes(value.sizeBytes)} • ${value.hexPreview || "binary"}`;
141
+ }
142
+
143
+ export function formatCellValue(value) {
144
+ if (value === null || value === undefined) {
145
+ return "NULL";
146
+ }
147
+
148
+ if (isBlobPreview(value)) {
149
+ return formatBlobPreview(value);
150
+ }
151
+
152
+ if (typeof value === "object") {
153
+ return JSON.stringify(value);
154
+ }
155
+
156
+ if (typeof value === "boolean") {
157
+ return value ? "true" : "false";
158
+ }
159
+
160
+ return String(value);
161
+ }
162
+
163
+ export function inferStatusTone(value) {
164
+ const normalized = String(value ?? "").toLowerCase();
165
+
166
+ if (["ok", "active", "enabled", "success", "primary", "on"].includes(normalized)) {
167
+ return "success";
168
+ }
169
+
170
+ if (["warning", "locked", "readonly", "read_only", "alert"].includes(normalized)) {
171
+ return "alert";
172
+ }
173
+
174
+ if (["critical", "error", "failed"].includes(normalized)) {
175
+ return "alert";
176
+ }
177
+
178
+ return "muted";
179
+ }
@@ -0,0 +1,133 @@
1
+ import { renderConnectionCard } from "../components/connectionCard.js";
2
+ import { renderPageHeader } from "../components/pageHeader.js";
3
+ import { escapeHtml } from "../utils/format.js";
4
+
5
+ function renderConnectionsActionButton({
6
+ label,
7
+ icon,
8
+ modal,
9
+ tone = "secondary",
10
+ className = "",
11
+ }) {
12
+ const clipPath = "polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%)";
13
+ const toneClassName =
14
+ tone === "primary"
15
+ ? "clipped-corner border border-primary-container bg-primary-container text-on-primary shadow-[0_0_18px_-10px_rgba(252,227,0,0.65)] hover:bg-primary-fixed"
16
+ : "clipped-corner border border-outline-variant/20 bg-surface-container-highest text-primary-container shadow-[inset_2px_0_0_0_rgba(252,227,0,0.95)] hover:bg-surface-bright";
17
+ const iconClassName =
18
+ tone === "primary"
19
+ ? "text-base text-on-primary"
20
+ : "text-base text-primary-container/90";
21
+ const clipStyle = `style="--clip-path: ${clipPath};"`;
22
+
23
+ return `
24
+ <button
25
+ class="flex h-11 items-center justify-between gap-6 px-5 font-headline text-xs font-bold uppercase tracking-[0.18em] transition-colors ${toneClassName} ${className}"
26
+ data-action="open-modal"
27
+ data-modal="${modal}"
28
+ ${clipStyle}
29
+ type="button"
30
+ >
31
+ <span>${label}</span>
32
+ <span class="material-symbols-outlined ${iconClassName}">${icon}</span>
33
+ </button>
34
+ `;
35
+ }
36
+
37
+ function renderConnectionsBody(state) {
38
+ if (state.connections.loading && !state.connections.recent.length) {
39
+ return `
40
+ <div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
41
+ <div class="text-center text-on-surface-variant/40">
42
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
43
+ <p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_CONNECTIONS</p>
44
+ </div>
45
+ </div>
46
+ `;
47
+ }
48
+
49
+ if (state.connections.error) {
50
+ return `
51
+ <div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
52
+ <div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
53
+ ${escapeHtml(state.connections.error.code)}
54
+ </div>
55
+ <div class="mt-2">${escapeHtml(state.connections.error.message)}</div>
56
+ </div>
57
+ `;
58
+ }
59
+
60
+ if (!state.connections.recent.length) {
61
+ return `
62
+ <div class="border border-dashed border-outline-variant/20 bg-surface-container-low px-8 py-10 text-center">
63
+ <span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">database_off</span>
64
+ <p class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
65
+ No Recorded SQLite Connections
66
+ </p>
67
+ <p class="mx-auto mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
68
+ Open an existing SQLite database or create a new one to add your first connection.
69
+ </p>
70
+ <div class="mt-6 flex flex-wrap items-center justify-center gap-3">
71
+ ${renderConnectionsActionButton({
72
+ label: "Open Database",
73
+ icon: "folder_open",
74
+ modal: "open-connection",
75
+ tone: "primary",
76
+ className: "min-w-[17rem] px-8 py-6 text-sm",
77
+ })}
78
+ ${renderConnectionsActionButton({
79
+ label: "Create Database",
80
+ icon: "note_add",
81
+ modal: "create-connection",
82
+ className: "min-w-[17rem] px-8 py-6 text-sm",
83
+ })}
84
+ </div>
85
+ </div>
86
+ `;
87
+ }
88
+
89
+ return `
90
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
91
+ ${state.connections.recent
92
+ .map((connection) =>
93
+ renderConnectionCard(connection, state.connections.active?.id)
94
+ )
95
+ .join("")}
96
+ </div>
97
+ `;
98
+ }
99
+
100
+ export function renderConnectionsView(state) {
101
+ const actions = `
102
+ ${renderConnectionsActionButton({
103
+ label: "Open Database",
104
+ icon: "folder_open",
105
+ modal: "open-connection",
106
+ tone: "primary",
107
+ className: "min-w-[13rem]",
108
+ })}
109
+ ${renderConnectionsActionButton({
110
+ label: "Create Database",
111
+ icon: "note_add",
112
+ modal: "create-connection",
113
+ className: "min-w-[13rem]",
114
+ })}
115
+ `;
116
+
117
+ return {
118
+ main: `
119
+ <section class="view-surface relative min-h-full overflow-hidden">
120
+ <div class="data-grid-texture pointer-events-none absolute inset-0"></div>
121
+ <div class="view-frame relative z-10">
122
+ ${renderPageHeader({
123
+ title: "Connections",
124
+ subtitle: `Registry // Recent_Targets: ${String(state.connections.recent.length).padStart(2, "0")}`,
125
+ actions,
126
+ })}
127
+ ${renderConnectionsBody(state)}
128
+ </div>
129
+ </section>
130
+ `,
131
+ panel: "",
132
+ };
133
+ }