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.
- package/.npmingnore +4 -0
- package/README.md +46 -0
- package/assets/images/logo.webp +0 -0
- package/assets/images/logo_extrasmall.webp +0 -0
- package/assets/images/logo_raw.png +0 -0
- package/assets/images/logo_small.webp +0 -0
- package/assets/mockups/connections.png +0 -0
- package/assets/mockups/data.png +0 -0
- package/assets/mockups/data_edit.png +0 -0
- package/assets/mockups/home.png +0 -0
- package/assets/mockups/overview.png +0 -0
- package/assets/mockups/sql_editor.png +0 -0
- package/assets/mockups/structure.png +0 -0
- package/bin/sqlite-hub.js +116 -0
- package/changelog.md +3 -0
- package/data/.gitkeep +0 -0
- package/index.html +100 -0
- package/js/api.js +193 -0
- package/js/app.js +520 -0
- package/js/components/actionBar.js +8 -0
- package/js/components/appShell.js +17 -0
- package/js/components/badges.js +5 -0
- package/js/components/bottomTabs.js +37 -0
- package/js/components/connectionCard.js +106 -0
- package/js/components/dataGrid.js +47 -0
- package/js/components/emptyState.js +159 -0
- package/js/components/metricCard.js +32 -0
- package/js/components/modal.js +317 -0
- package/js/components/pageHeader.js +33 -0
- package/js/components/queryEditor.js +121 -0
- package/js/components/queryResults.js +107 -0
- package/js/components/rowEditorPanel.js +164 -0
- package/js/components/sidebar.js +57 -0
- package/js/components/statusBar.js +39 -0
- package/js/components/toast.js +39 -0
- package/js/components/topNav.js +27 -0
- package/js/router.js +66 -0
- package/js/store.js +1092 -0
- package/js/utils/format.js +179 -0
- package/js/views/connections.js +133 -0
- package/js/views/data.js +400 -0
- package/js/views/editor.js +259 -0
- package/js/views/landing.js +11 -0
- package/js/views/overview.js +220 -0
- package/js/views/settings.js +109 -0
- package/js/views/structure.js +242 -0
- package/package.json +18 -0
- package/publish_brew.sh +444 -0
- package/publish_npm.sh +241 -0
- package/server/routes/connections.js +146 -0
- package/server/routes/data.js +59 -0
- package/server/routes/export.js +25 -0
- package/server/routes/overview.js +39 -0
- package/server/routes/settings.js +50 -0
- package/server/routes/sql.js +50 -0
- package/server/routes/structure.js +38 -0
- package/server/server.js +136 -0
- package/server/services/sqlite/connectionManager.js +306 -0
- package/server/services/sqlite/dataBrowserService.js +255 -0
- package/server/services/sqlite/exportService.js +34 -0
- package/server/services/sqlite/importService.js +111 -0
- package/server/services/sqlite/introspection.js +302 -0
- package/server/services/sqlite/overviewService.js +109 -0
- package/server/services/sqlite/sqlExecutor.js +434 -0
- package/server/services/sqlite/structureService.js +60 -0
- package/server/services/storage/appStateStore.js +530 -0
- package/server/utils/csv.js +34 -0
- package/server/utils/errors.js +175 -0
- package/server/utils/fileValidation.js +135 -0
- package/server/utils/identifier.js +38 -0
- package/server/utils/sqliteTypes.js +112 -0
- package/styles/base.css +176 -0
- package/styles/components.css +323 -0
- package/styles/layout.css +101 -0
- package/styles/tokens.css +49 -0
- package/styles/views.css +84 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function renderDataGrid({
|
|
2
|
+
columns,
|
|
3
|
+
rows,
|
|
4
|
+
tableClass = "",
|
|
5
|
+
theadClass = "",
|
|
6
|
+
headerRowClass = "",
|
|
7
|
+
tbodyClass = "",
|
|
8
|
+
getRowClass = () => "",
|
|
9
|
+
getRowAttrs = () => "",
|
|
10
|
+
}) {
|
|
11
|
+
return `
|
|
12
|
+
<table class="${tableClass}">
|
|
13
|
+
<thead class="${theadClass}">
|
|
14
|
+
<tr class="${headerRowClass}">
|
|
15
|
+
${columns
|
|
16
|
+
.map(
|
|
17
|
+
(column) => `
|
|
18
|
+
<th class="${column.headerClassName ?? ""}">
|
|
19
|
+
${column.label}
|
|
20
|
+
</th>
|
|
21
|
+
`
|
|
22
|
+
)
|
|
23
|
+
.join("")}
|
|
24
|
+
</tr>
|
|
25
|
+
</thead>
|
|
26
|
+
<tbody class="${tbodyClass}">
|
|
27
|
+
${rows
|
|
28
|
+
.map(
|
|
29
|
+
(row, index) => `
|
|
30
|
+
<tr class="${getRowClass(row, index)}" ${getRowAttrs(row, index)}>
|
|
31
|
+
${columns
|
|
32
|
+
.map(
|
|
33
|
+
(column) => `
|
|
34
|
+
<td class="${column.cellClassName ?? ""}">
|
|
35
|
+
${column.render ? column.render(row, index) : row[column.key]}
|
|
36
|
+
</td>
|
|
37
|
+
`
|
|
38
|
+
)
|
|
39
|
+
.join("")}
|
|
40
|
+
</tr>
|
|
41
|
+
`
|
|
42
|
+
)
|
|
43
|
+
.join("")}
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHtml,
|
|
3
|
+
formatBytes,
|
|
4
|
+
formatDateTime,
|
|
5
|
+
truncateMiddle,
|
|
6
|
+
} from "../utils/format.js";
|
|
7
|
+
|
|
8
|
+
function renderRecentConnections(recentConnections = []) {
|
|
9
|
+
if (!recentConnections.length) {
|
|
10
|
+
return `
|
|
11
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/40">
|
|
12
|
+
No recent SQLite databases recorded yet.
|
|
13
|
+
</div>
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return `
|
|
18
|
+
<div class="flex flex-wrap justify-center gap-4">
|
|
19
|
+
${recentConnections
|
|
20
|
+
.slice(0, 4)
|
|
21
|
+
.map(
|
|
22
|
+
(connection) => `
|
|
23
|
+
<button
|
|
24
|
+
class="flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 py-3 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high"
|
|
25
|
+
data-action="select-connection"
|
|
26
|
+
data-connection-id="${escapeHtml(connection.id)}"
|
|
27
|
+
type="button"
|
|
28
|
+
>
|
|
29
|
+
<span class="material-symbols-outlined text-sm text-primary-container">database</span>
|
|
30
|
+
<span class="min-w-0">
|
|
31
|
+
<span class="block truncate font-mono text-xs">${escapeHtml(connection.label)}</span>
|
|
32
|
+
<span class="block truncate text-[10px] text-on-surface-variant/45">${escapeHtml(
|
|
33
|
+
truncateMiddle(connection.path, 34)
|
|
34
|
+
)}</span>
|
|
35
|
+
</span>
|
|
36
|
+
</button>
|
|
37
|
+
`
|
|
38
|
+
)
|
|
39
|
+
.join("")}
|
|
40
|
+
</div>
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderActiveConnection(activeConnection) {
|
|
45
|
+
if (!activeConnection) {
|
|
46
|
+
return `
|
|
47
|
+
<p class="font-light text-lg uppercase tracking-wide text-on-surface-variant">
|
|
48
|
+
No database connected
|
|
49
|
+
</p>
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return `
|
|
54
|
+
<div class="mx-auto max-w-2xl border border-outline-variant/15 bg-surface-container-low px-6 py-5 text-left">
|
|
55
|
+
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
56
|
+
<div>
|
|
57
|
+
<p class="text-[10px] font-mono uppercase tracking-[0.24em] text-primary-container/70">
|
|
58
|
+
ACTIVE_DATABASE
|
|
59
|
+
</p>
|
|
60
|
+
<h2 class="mt-2 font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
|
|
61
|
+
${escapeHtml(activeConnection.label)}
|
|
62
|
+
</h2>
|
|
63
|
+
<p class="mt-2 font-mono text-[10px] text-on-surface-variant/55">${escapeHtml(
|
|
64
|
+
truncateMiddle(activeConnection.path, 72)
|
|
65
|
+
)}</p>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="text-right text-xs text-on-surface-variant/65">
|
|
68
|
+
<div>${escapeHtml(formatBytes(activeConnection.sizeBytes))}</div>
|
|
69
|
+
<div class="mt-1">${escapeHtml(formatDateTime(activeConnection.lastModifiedAt))}</div>
|
|
70
|
+
<div class="mt-1">${activeConnection.readOnly ? "READ_ONLY" : "READ_WRITE"}</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function renderEmptyState({ activeConnection, recentConnections = [] }) {
|
|
78
|
+
const hasActive = Boolean(activeConnection);
|
|
79
|
+
|
|
80
|
+
return `
|
|
81
|
+
<section class="landing-view machined-grid px-6">
|
|
82
|
+
<div class="landing-accent landing-accent--a"></div>
|
|
83
|
+
<div class="landing-accent landing-accent--b"></div>
|
|
84
|
+
<div class="landing-accent--c absolute"></div>
|
|
85
|
+
<div class="empty-state-shell z-10 text-center">
|
|
86
|
+
<div class="mb-2">
|
|
87
|
+
<span class="font-mono text-[10px] tracking-[0.3em] text-primary-container/40">
|
|
88
|
+
SYSTEM_READY // ${hasActive ? "ACTIVE_CONTEXT" : "IDLE_STATE"}
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
<h1 class="mb-4 font-headline text-7xl font-black tracking-tighter text-primary-container opacity-90 md:text-9xl">
|
|
92
|
+
SQLite Hub
|
|
93
|
+
</h1>
|
|
94
|
+
<div class="mx-auto mb-12 max-w-3xl space-y-4">
|
|
95
|
+
${renderActiveConnection(activeConnection)}
|
|
96
|
+
<div class="h-[2px] w-12 bg-primary-container mx-auto"></div>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="mx-auto grid w-full max-w-3xl grid-cols-1 gap-4 px-6 md:grid-cols-2">
|
|
99
|
+
<button
|
|
100
|
+
class="clipped-btn group flex items-center justify-between bg-primary-container px-8 py-6 font-headline text-lg font-bold text-on-primary transition-all duration-300 hover:shadow-[0_0_20px_rgba(252,227,0,0.3)]"
|
|
101
|
+
data-action="open-modal"
|
|
102
|
+
data-modal="open-connection"
|
|
103
|
+
style="--clip-path: polygon(0 0, 90% 0, 100% 25%, 100% 100%, 0 100%);"
|
|
104
|
+
type="button"
|
|
105
|
+
>
|
|
106
|
+
<span>CONNECT DATABASE</span>
|
|
107
|
+
<span class="material-symbols-outlined transition-transform group-hover:translate-x-1">add_circle</span>
|
|
108
|
+
</button>
|
|
109
|
+
<button
|
|
110
|
+
class="flex items-center justify-between border-l-2 border-primary-container bg-surface-container-highest px-8 py-6 font-headline text-lg font-bold text-primary-container transition-colors duration-150 hover:bg-surface-bright"
|
|
111
|
+
data-action="open-modal"
|
|
112
|
+
data-modal="create-connection"
|
|
113
|
+
type="button"
|
|
114
|
+
>
|
|
115
|
+
<span>CREATE DATABASE</span>
|
|
116
|
+
<span class="material-symbols-outlined">note_add</span>
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
${
|
|
120
|
+
hasActive
|
|
121
|
+
? `
|
|
122
|
+
<div class="mx-auto mt-8 grid w-full max-w-3xl grid-cols-1 gap-4 px-6 md:grid-cols-3">
|
|
123
|
+
<button
|
|
124
|
+
class="border border-outline-variant/20 bg-surface-container-low px-5 py-4 font-headline text-sm font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container/30 hover:text-primary-container"
|
|
125
|
+
data-action="navigate"
|
|
126
|
+
data-to="/overview"
|
|
127
|
+
type="button"
|
|
128
|
+
>
|
|
129
|
+
Overview
|
|
130
|
+
</button>
|
|
131
|
+
<button
|
|
132
|
+
class="border border-outline-variant/20 bg-surface-container-low px-5 py-4 font-headline text-sm font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container/30 hover:text-primary-container"
|
|
133
|
+
data-action="navigate"
|
|
134
|
+
data-to="/structure"
|
|
135
|
+
type="button"
|
|
136
|
+
>
|
|
137
|
+
Structure
|
|
138
|
+
</button>
|
|
139
|
+
<button
|
|
140
|
+
class="border border-outline-variant/20 bg-surface-container-low px-5 py-4 font-headline text-sm font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container/30 hover:text-primary-container"
|
|
141
|
+
data-action="navigate"
|
|
142
|
+
data-to="/editor"
|
|
143
|
+
type="button"
|
|
144
|
+
>
|
|
145
|
+
SQL Editor
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
`
|
|
149
|
+
: ""
|
|
150
|
+
}
|
|
151
|
+
<div class="mt-16 flex flex-col items-center gap-4 opacity-70 transition-opacity hover:opacity-100">
|
|
152
|
+
<p class="font-mono text-[10px] tracking-widest text-on-surface-variant">RECENT_TARGETS</p>
|
|
153
|
+
${renderRecentConnections(recentConnections)}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="pointer-events-none absolute bottom-0 left-0 h-1/3 w-full bg-gradient-to-t from-surface-container-lowest to-transparent"></div>
|
|
157
|
+
</section>
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
export function renderMetricCard({
|
|
4
|
+
label,
|
|
5
|
+
value,
|
|
6
|
+
subtext = "",
|
|
7
|
+
accent = false,
|
|
8
|
+
progress = "",
|
|
9
|
+
}) {
|
|
10
|
+
return `
|
|
11
|
+
<div class="metric-card ${accent ? "metric-card--accent" : ""}">
|
|
12
|
+
<span class="text-[10px] font-mono text-on-surface/40 uppercase">${escapeHtml(label)}</span>
|
|
13
|
+
<span class="text-3xl font-headline font-bold text-on-surface">${escapeHtml(value)}</span>
|
|
14
|
+
${
|
|
15
|
+
progress
|
|
16
|
+
? `
|
|
17
|
+
<div class="w-full bg-surface-container-highest h-1 mt-2">
|
|
18
|
+
<div class="bg-primary-container h-full" style="width: ${escapeHtml(progress)}"></div>
|
|
19
|
+
</div>
|
|
20
|
+
`
|
|
21
|
+
: ""
|
|
22
|
+
}
|
|
23
|
+
${
|
|
24
|
+
subtext
|
|
25
|
+
? `<span class="text-[10px] ${accent ? "text-primary-container" : "text-on-surface/40"}">${escapeHtml(
|
|
26
|
+
subtext
|
|
27
|
+
)}</span>`
|
|
28
|
+
: ""
|
|
29
|
+
}
|
|
30
|
+
</div>
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { escapeHtml, truncateMiddle } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
function renderField({ label, name, type = "text", placeholder = "", value = "" }) {
|
|
4
|
+
return `
|
|
5
|
+
<label class="block space-y-2">
|
|
6
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
7
|
+
${escapeHtml(label)}
|
|
8
|
+
</span>
|
|
9
|
+
<input
|
|
10
|
+
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"
|
|
11
|
+
name="${escapeHtml(name)}"
|
|
12
|
+
placeholder="${escapeHtml(placeholder)}"
|
|
13
|
+
type="${escapeHtml(type)}"
|
|
14
|
+
value="${escapeHtml(value)}"
|
|
15
|
+
/>
|
|
16
|
+
</label>
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderCheckboxField({ label, name, checked = false, text }) {
|
|
21
|
+
return `
|
|
22
|
+
<label class="flex items-center gap-3 border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface">
|
|
23
|
+
<input
|
|
24
|
+
class="rounded-none border-outline bg-surface-container-lowest text-primary-container focus:ring-primary-container"
|
|
25
|
+
${checked ? "checked" : ""}
|
|
26
|
+
name="${escapeHtml(name)}"
|
|
27
|
+
type="checkbox"
|
|
28
|
+
/>
|
|
29
|
+
${escapeHtml(text || label)}
|
|
30
|
+
</label>
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderError(error) {
|
|
35
|
+
if (!error) {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return `
|
|
40
|
+
<div class="border border-error/20 bg-error-container/20 px-4 py-3 text-sm text-error">
|
|
41
|
+
<div class="font-headline text-xs font-bold uppercase tracking-[0.18em]">${escapeHtml(
|
|
42
|
+
error.code || "Request failed"
|
|
43
|
+
)}</div>
|
|
44
|
+
<div class="mt-1 text-on-surface">${escapeHtml(error.message)}</div>
|
|
45
|
+
</div>
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderOpenConnectionForm(modal) {
|
|
50
|
+
return `
|
|
51
|
+
<form class="space-y-5" data-form="open-connection">
|
|
52
|
+
${renderField({
|
|
53
|
+
label: "SQLite File Path",
|
|
54
|
+
name: "path",
|
|
55
|
+
placeholder: "/absolute/path/to/database.sqlite",
|
|
56
|
+
})}
|
|
57
|
+
${renderField({
|
|
58
|
+
label: "Label",
|
|
59
|
+
name: "label",
|
|
60
|
+
placeholder: "Optional display name",
|
|
61
|
+
})}
|
|
62
|
+
${renderCheckboxField({
|
|
63
|
+
label: "Open read-only",
|
|
64
|
+
name: "readOnly",
|
|
65
|
+
text: "Open read-only",
|
|
66
|
+
})}
|
|
67
|
+
${renderError(modal.error)}
|
|
68
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
69
|
+
<button
|
|
70
|
+
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"
|
|
71
|
+
data-action="close-modal"
|
|
72
|
+
type="button"
|
|
73
|
+
>
|
|
74
|
+
Cancel
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
|
|
78
|
+
type="submit"
|
|
79
|
+
>
|
|
80
|
+
${modal.submitting ? "Opening..." : "Open Database"}
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
</form>
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderEditConnectionForm(modal) {
|
|
88
|
+
const connection = modal.connection ?? {};
|
|
89
|
+
|
|
90
|
+
return `
|
|
91
|
+
<form class="space-y-5" data-form="edit-connection">
|
|
92
|
+
<input name="connectionId" type="hidden" value="${escapeHtml(connection.id ?? "")}" />
|
|
93
|
+
${renderField({
|
|
94
|
+
label: "SQLite File Path",
|
|
95
|
+
name: "path",
|
|
96
|
+
placeholder: "/absolute/path/to/database.sqlite",
|
|
97
|
+
value: connection.path ?? "",
|
|
98
|
+
})}
|
|
99
|
+
${renderField({
|
|
100
|
+
label: "Label",
|
|
101
|
+
name: "label",
|
|
102
|
+
placeholder: "Optional display name",
|
|
103
|
+
value: connection.label ?? "",
|
|
104
|
+
})}
|
|
105
|
+
${renderCheckboxField({
|
|
106
|
+
label: "Open read-only",
|
|
107
|
+
name: "readOnly",
|
|
108
|
+
checked: Boolean(connection.readOnly),
|
|
109
|
+
text: "Open read-only",
|
|
110
|
+
})}
|
|
111
|
+
${renderError(modal.error)}
|
|
112
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
113
|
+
<button
|
|
114
|
+
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"
|
|
115
|
+
data-action="close-modal"
|
|
116
|
+
type="button"
|
|
117
|
+
>
|
|
118
|
+
Cancel
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
|
|
122
|
+
type="submit"
|
|
123
|
+
>
|
|
124
|
+
${modal.submitting ? "Saving..." : "Save Changes"}
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</form>
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderCreateDatabaseForm(modal) {
|
|
132
|
+
return `
|
|
133
|
+
<form class="space-y-5" data-form="create-connection">
|
|
134
|
+
${renderField({
|
|
135
|
+
label: "New SQLite File Path",
|
|
136
|
+
name: "path",
|
|
137
|
+
placeholder: "/absolute/path/to/new-database.sqlite",
|
|
138
|
+
})}
|
|
139
|
+
${renderField({
|
|
140
|
+
label: "Label",
|
|
141
|
+
name: "label",
|
|
142
|
+
placeholder: "Optional display name",
|
|
143
|
+
})}
|
|
144
|
+
${renderError(modal.error)}
|
|
145
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
146
|
+
<button
|
|
147
|
+
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"
|
|
148
|
+
data-action="close-modal"
|
|
149
|
+
type="button"
|
|
150
|
+
>
|
|
151
|
+
Cancel
|
|
152
|
+
</button>
|
|
153
|
+
<button
|
|
154
|
+
class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
|
|
155
|
+
type="submit"
|
|
156
|
+
>
|
|
157
|
+
${modal.submitting ? "Creating..." : "Create Database"}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
</form>
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function renderImportTargetOptions(state) {
|
|
165
|
+
const recentOptions = state.connections.recent
|
|
166
|
+
.map(
|
|
167
|
+
(connection) => `
|
|
168
|
+
<option value="${escapeHtml(connection.id)}">
|
|
169
|
+
${escapeHtml(connection.label)} • ${escapeHtml(truncateMiddle(connection.path, 42))}
|
|
170
|
+
</option>
|
|
171
|
+
`
|
|
172
|
+
)
|
|
173
|
+
.join("");
|
|
174
|
+
|
|
175
|
+
return `
|
|
176
|
+
<label class="block space-y-2">
|
|
177
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
178
|
+
Import Target
|
|
179
|
+
</span>
|
|
180
|
+
<select
|
|
181
|
+
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"
|
|
182
|
+
name="targetMode"
|
|
183
|
+
>
|
|
184
|
+
${
|
|
185
|
+
state.connections.active
|
|
186
|
+
? '<option value="active">Use active database</option>'
|
|
187
|
+
: ""
|
|
188
|
+
}
|
|
189
|
+
${state.connections.recent.length ? '<option value="recent">Use recent connection</option>' : ""}
|
|
190
|
+
<option value="create">Create new database from dump</option>
|
|
191
|
+
<option value="path">Open explicit target path</option>
|
|
192
|
+
</select>
|
|
193
|
+
</label>
|
|
194
|
+
${
|
|
195
|
+
state.connections.recent.length
|
|
196
|
+
? `
|
|
197
|
+
<label class="block space-y-2">
|
|
198
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
199
|
+
Recent Connection
|
|
200
|
+
</span>
|
|
201
|
+
<select
|
|
202
|
+
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"
|
|
203
|
+
name="targetConnectionId"
|
|
204
|
+
>
|
|
205
|
+
${recentOptions}
|
|
206
|
+
</select>
|
|
207
|
+
</label>
|
|
208
|
+
`
|
|
209
|
+
: ""
|
|
210
|
+
}
|
|
211
|
+
${renderField({
|
|
212
|
+
label: "Target Path",
|
|
213
|
+
name: "targetPath",
|
|
214
|
+
placeholder: "/absolute/path/to/target.sqlite",
|
|
215
|
+
})}
|
|
216
|
+
${renderField({
|
|
217
|
+
label: "Target Label",
|
|
218
|
+
name: "label",
|
|
219
|
+
placeholder: "Optional display name",
|
|
220
|
+
})}
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function renderImportSqlForm(modal, state) {
|
|
225
|
+
return `
|
|
226
|
+
<form class="space-y-5" data-form="import-sql">
|
|
227
|
+
${renderField({
|
|
228
|
+
label: "SQL Dump Path",
|
|
229
|
+
name: "sqlFilePath",
|
|
230
|
+
placeholder: "/absolute/path/to/dump.sql",
|
|
231
|
+
})}
|
|
232
|
+
${renderImportTargetOptions(state)}
|
|
233
|
+
<p class="text-[11px] leading-6 text-on-surface-variant/60">
|
|
234
|
+
Use an absolute filesystem path. Browsers do not expose local file paths, so SQLite Hub imports by
|
|
235
|
+
explicit path instead of file upload.
|
|
236
|
+
</p>
|
|
237
|
+
${renderError(modal.error)}
|
|
238
|
+
<div class="flex items-center justify-end gap-3 pt-2">
|
|
239
|
+
<button
|
|
240
|
+
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"
|
|
241
|
+
data-action="close-modal"
|
|
242
|
+
type="button"
|
|
243
|
+
>
|
|
244
|
+
Cancel
|
|
245
|
+
</button>
|
|
246
|
+
<button
|
|
247
|
+
class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
|
|
248
|
+
type="submit"
|
|
249
|
+
>
|
|
250
|
+
${modal.submitting ? "Importing..." : "Import SQL Dump"}
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
</form>
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function renderModal(state) {
|
|
258
|
+
const modal = state.modal;
|
|
259
|
+
|
|
260
|
+
if (!modal) {
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const contentByKind = {
|
|
265
|
+
"open-connection": {
|
|
266
|
+
eyebrow: "Filesystem // Open existing SQLite database",
|
|
267
|
+
title: "Connect Database",
|
|
268
|
+
body: renderOpenConnectionForm(modal),
|
|
269
|
+
},
|
|
270
|
+
"create-connection": {
|
|
271
|
+
eyebrow: "Filesystem // Create a new SQLite database",
|
|
272
|
+
title: "Create Database",
|
|
273
|
+
body: renderCreateDatabaseForm(modal),
|
|
274
|
+
},
|
|
275
|
+
"import-sql": {
|
|
276
|
+
eyebrow: "Import // Execute SQL dump into SQLite",
|
|
277
|
+
title: "Import SQL Dump",
|
|
278
|
+
body: renderImportSqlForm(modal, state),
|
|
279
|
+
},
|
|
280
|
+
"edit-connection": {
|
|
281
|
+
eyebrow: "Registry // Update saved SQLite target",
|
|
282
|
+
title: "Edit Connection",
|
|
283
|
+
body: renderEditConnectionForm(modal),
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const config = contentByKind[modal.kind];
|
|
288
|
+
|
|
289
|
+
if (!config) {
|
|
290
|
+
return "";
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return `
|
|
294
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
|
|
295
|
+
<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)]">
|
|
296
|
+
<div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
|
|
297
|
+
<div>
|
|
298
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">
|
|
299
|
+
${escapeHtml(config.eyebrow)}
|
|
300
|
+
</div>
|
|
301
|
+
<h2 class="mt-2 font-headline text-3xl font-black uppercase tracking-tight text-primary-container">
|
|
302
|
+
${escapeHtml(config.title)}
|
|
303
|
+
</h2>
|
|
304
|
+
</div>
|
|
305
|
+
<button
|
|
306
|
+
class="flex h-10 w-10 items-center justify-center border border-outline-variant/20 text-on-surface-variant hover:bg-surface-container-highest hover:text-primary-container"
|
|
307
|
+
data-action="close-modal"
|
|
308
|
+
type="button"
|
|
309
|
+
>
|
|
310
|
+
<span class="material-symbols-outlined">close</span>
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="space-y-5 px-6 py-6">${config.body}</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
|
|
3
|
+
export function renderPageHeader({ eyebrow = "", title, subtitle = "", actions = "" }) {
|
|
4
|
+
return `
|
|
5
|
+
<div class="mb-10 flex flex-wrap items-end justify-between gap-6">
|
|
6
|
+
<div>
|
|
7
|
+
${
|
|
8
|
+
eyebrow
|
|
9
|
+
? `
|
|
10
|
+
<div class="page-eyebrow">
|
|
11
|
+
<span class="text-[#FCE300] text-xs font-mono font-bold tracking-widest uppercase">${escapeHtml(
|
|
12
|
+
eyebrow
|
|
13
|
+
)}</span>
|
|
14
|
+
<div class="page-eyebrow-line"></div>
|
|
15
|
+
</div>
|
|
16
|
+
`
|
|
17
|
+
: ""
|
|
18
|
+
}
|
|
19
|
+
<h1 class="text-5xl font-['Space_Grotesk'] font-bold text-[#FCE300] tracking-tighter uppercase">${escapeHtml(
|
|
20
|
+
title
|
|
21
|
+
)}</h1>
|
|
22
|
+
${
|
|
23
|
+
subtitle
|
|
24
|
+
? `<p class="text-xs font-mono text-on-surface/40 mt-1 uppercase tracking-widest">${escapeHtml(
|
|
25
|
+
subtitle
|
|
26
|
+
)}</p>`
|
|
27
|
+
: ""
|
|
28
|
+
}
|
|
29
|
+
</div>
|
|
30
|
+
${actions ? `<div class="flex gap-3">${actions}</div>` : ""}
|
|
31
|
+
</div>
|
|
32
|
+
`;
|
|
33
|
+
}
|