sqlite-hub 0.2.0 → 0.3.2

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.
@@ -0,0 +1,2 @@
1
+ github: oliverjessner
2
+ buy_me_a_coffee: oliverjessner
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 oliverjessner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,48 +1,80 @@
1
- # sqlite-hub
2
-
3
- A cyberpunk inspired management app for sqlite
1
+ # sqlite-hub ⚡️
4
2
 
5
3
  ![](/assets/mockups/home.png)
6
4
 
7
- ## Install
5
+ A focused local-first app for browsing, editing, and querying SQLite databases.
8
6
 
9
- ### Homebrew
7
+ SQLite Hub is built for developers and technical users who want a clean SQLite workflow without heavy database clients, cloud layers, or dashboard noise.
10
8
 
11
- ```bash
12
- brew install oliverjessner/tap/sqlite-hub
13
- ```
9
+ ## Why SQLite Hub?
14
10
 
15
- Or tap first and then install:
11
+ Many database tools are powerful, but feel oversized when all you want is to inspect a local SQLite file, edit a few rows, and run a query fast.
16
12
 
17
- ```bash
18
- brew tap oliverjessner/tap
19
- brew install sqlite-hub
20
- ```
13
+ SQLite Hub keeps that workflow sharp:
21
14
 
22
- ### NPM
15
+ - browse tables and rows
16
+ - inspect schema and structure
17
+ - edit records in place
18
+ - export tables and query results as CSV
19
+ - create simple local backups of the active database
20
+ - run SQL in a syntax-highlighted editor
21
+ - stay local and move fast
23
22
 
24
- ```bash
25
- npm install sqlite-hub
26
- ```
23
+ ## Features
24
+
25
+ ### Structure view
26
+
27
+ ![](/assets/mockups/graph_visualize.png)
28
+
29
+ Inspect tables, columns, types, and schema details without losing pace. Visualized in a graph.
30
+
31
+ ### Data browser
32
+
33
+ ![](/assets/mockups/data.png)
27
34
 
28
- ## Run
35
+ Scan rows, sort fast, move through local data quickly, and export full tables as CSV.
29
36
 
30
- Start the app and open it automatically in the default browser:
37
+ ### Row editing
38
+
39
+ ![](/assets/mockups/data_edit.png)
40
+
41
+ Open one record, edit it in place, commit, continue.
42
+
43
+ ### SQL editor
44
+
45
+ ![](/assets/mockups/sql_editor.png)
46
+
47
+ Write queries in a syntax-highlighted editor, inspect results in the same workflow, and export result sets as CSV.
48
+
49
+ ### Simple backups
50
+
51
+ Create timestamped local backups of the active SQLite database in one click. Backups are stored as plain file copies in a local `backups` folder next to the database.
52
+
53
+ ### Local-first
54
+
55
+ Built around local SQLite files, not hosted dashboards or team complexity.
56
+
57
+ ## Install
58
+
59
+ ### Homebrew
31
60
 
32
61
  ```bash
33
- npm start
62
+ brew tap oliverjessner/tap
63
+ brew install sqlite-hub
34
64
  ```
35
65
 
36
- Use a custom port via CLI. If omitted, `4173` is used:
66
+ ### NPM
37
67
 
38
68
  ```bash
39
- npm start -- --port:1203
69
+ npm install -g sqlit-hub
40
70
  ```
41
71
 
42
- After linking or installing globally, the binary can also be called directly:
72
+ ## alternative port
43
73
 
44
74
  ```bash
45
- sqlite-hub --port:1203
75
+ sqlit-hub --port:4174
46
76
  ```
47
77
 
48
- App state such as recent connections, SQL history, and local settings is stored in the user profile instead of the install directory. On macOS this lives under `~/Library/Application Support/sqlite-hub/`, so Homebrew upgrades keep the internal state across versions.
78
+ ## Changelog
79
+
80
+ [Changelog](/changelog.md)
package/changelog.md CHANGED
@@ -1,7 +1,18 @@
1
+ # v0.3.2
2
+
3
+ - delete rows
4
+ - modal windows for rows
5
+
6
+ # v0.3.1
7
+
8
+ - visualize tables
9
+ - export csv from data and sql_editor
10
+ - Backup mode
11
+
1
12
  # v0.2.0
2
13
 
3
14
  - db fix
4
15
 
5
16
  # v0.1.3
6
17
 
7
- - edit in sql editro
18
+ - edit in sql editor
package/index.html CHANGED
@@ -90,11 +90,14 @@
90
90
  <link href="styles/layout.css" rel="stylesheet" />
91
91
  <link href="styles/components.css" rel="stylesheet" />
92
92
  <link href="styles/views.css" rel="stylesheet" />
93
+ <link href="styles/structure-graph.css" rel="stylesheet" />
93
94
  </head>
94
95
  <body
95
96
  class="bg-background text-on-surface font-body selection:bg-primary-container selection:text-on-primary overflow-hidden"
96
97
  >
97
98
  <div id="app"></div>
99
+ <script src="/vendor/elkjs/lib/elk.bundled.js"></script>
100
+ <script src="/vendor/cytoscape-elk/dist/cytoscape-elk.js"></script>
98
101
  <script src="js/app.js" type="module"></script>
99
102
  </body>
100
103
  </html>
package/js/api.js CHANGED
@@ -113,6 +113,12 @@ export function updateRecentConnection(id, payload) {
113
113
  });
114
114
  }
115
115
 
116
+ export function createActiveConnectionBackup() {
117
+ return request("/api/connections/backup-active", {
118
+ method: "POST",
119
+ });
120
+ }
121
+
116
122
  export function getOverview() {
117
123
  return request("/api/db/overview");
118
124
  }
@@ -173,6 +179,13 @@ export function updateDataTableRow(tableName, payload) {
173
179
  });
174
180
  }
175
181
 
182
+ export function deleteDataTableRow(tableName, payload) {
183
+ return request(`/api/data/${encodeURIComponent(tableName)}/rows`, {
184
+ method: "DELETE",
185
+ body: payload,
186
+ });
187
+ }
188
+
176
189
  export function getSettings() {
177
190
  return request("/api/settings");
178
191
  }
@@ -191,3 +204,11 @@ export function downloadQueryCsv(sql) {
191
204
  fallbackFilename: "query-results.csv",
192
205
  });
193
206
  }
207
+
208
+ export function downloadTableCsv(tableName) {
209
+ return download("/api/export/table.csv", {
210
+ method: "POST",
211
+ body: { tableName },
212
+ fallbackFilename: `${tableName || "table"}.csv`,
213
+ });
214
+ }
package/js/app.js CHANGED
@@ -2,10 +2,16 @@ import { renderAppShell } from "./components/appShell.js";
2
2
  import { renderModal } from "./components/modal.js";
3
3
  import { renderSidebar } from "./components/sidebar.js";
4
4
  import { renderStatusBar } from "./components/statusBar.js";
5
+ import {
6
+ mountStructureGraph,
7
+ resetPersistedStructureGraphState,
8
+ teardownStructureGraph,
9
+ } from "./components/structureGraph.js";
5
10
  import { renderToasts } from "./components/toast.js";
6
11
  import { renderTopNav } from "./components/topNav.js";
7
12
  import { createRouter } from "./router.js";
8
13
  import {
14
+ createActiveConnectionBackup,
9
15
  clearCurrentQuery,
10
16
  clearDataRowSelection,
11
17
  clearEditorRowSelection,
@@ -14,11 +20,14 @@ import {
14
20
  closeModal,
15
21
  dismissToast,
16
22
  executeCurrentQuery,
23
+ exportCurrentDataTableCsv,
17
24
  exportCurrentQueryCsv,
18
25
  getState,
19
26
  initializeApp,
20
27
  loadQueryFromHistory,
21
28
  openModal,
29
+ openDeleteDataRowModal,
30
+ openDeleteEditorRowModal,
22
31
  openEditConnectionModal,
23
32
  refreshCurrentRoute,
24
33
  removeConnection,
@@ -32,6 +41,7 @@ import {
32
41
  setEditorTab,
33
42
  setRoute,
34
43
  submitCreateConnection,
44
+ submitDeleteRowConfirmation,
35
45
  submitDataRowUpdate,
36
46
  submitEditorRowUpdate,
37
47
  submitEditConnection,
@@ -63,6 +73,10 @@ const shellRefs = {
63
73
  toast: document.querySelector("#toast-root"),
64
74
  };
65
75
 
76
+ function resetStructureGraphForDatabaseChange() {
77
+ resetPersistedStructureGraphState();
78
+ }
79
+
66
80
  function renderQueryHighlightMarkup(query) {
67
81
  if (query) {
68
82
  return highlightSql(query);
@@ -212,6 +226,7 @@ function renderApp(state) {
212
226
  const { main, panel } = resolveView(state);
213
227
  const isLockedRoute = ["editor", "editorResults", "data"].includes(state.route.name);
214
228
 
229
+ teardownStructureGraph();
215
230
  shellRefs.topNav.innerHTML = renderTopNav(state);
216
231
  shellRefs.sidebar.innerHTML = renderSidebar(state);
217
232
  shellRefs.statusBar.innerHTML = renderStatusBar(state);
@@ -222,6 +237,12 @@ function renderApp(state) {
222
237
  shellRefs.toast.innerHTML = renderToasts(state.toasts);
223
238
  shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
224
239
  restoreFocusedInputState(focusedInput);
240
+
241
+ if (state.route.name === "structure") {
242
+ mountStructureGraph(state).catch((error) => {
243
+ console.error("Failed to mount structure graph.", error);
244
+ });
245
+ }
225
246
  }
226
247
 
227
248
  const router = createRouter((route) => {
@@ -251,6 +272,7 @@ async function handleAction(actionNode) {
251
272
  dismissToast(actionNode.dataset.toastId);
252
273
  return;
253
274
  case "select-connection": {
275
+ resetStructureGraphForDatabaseChange();
254
276
  const next = await selectConnection(actionNode.dataset.connectionId);
255
277
  if (next) {
256
278
  router.navigate("/overview");
@@ -258,6 +280,12 @@ async function handleAction(actionNode) {
258
280
  return;
259
281
  }
260
282
  case "remove-connection": {
283
+ const isActiveConnection = getState().connections.active?.id === actionNode.dataset.connectionId;
284
+
285
+ if (isActiveConnection) {
286
+ resetStructureGraphForDatabaseChange();
287
+ }
288
+
261
289
  const removed = await removeConnection(actionNode.dataset.connectionId);
262
290
  if (removed) {
263
291
  const nextState = getState();
@@ -269,11 +297,20 @@ async function handleAction(actionNode) {
269
297
  }
270
298
  return;
271
299
  }
300
+ case "create-backup":
301
+ await createActiveConnectionBackup();
302
+ return;
272
303
  case "execute-query": {
273
304
  const success = await executeCurrentQuery();
274
305
  router.navigate(success ? "/editor/results" : "/editor");
275
306
  return;
276
307
  }
308
+ case "delete-data-row":
309
+ openDeleteDataRowModal(actionNode.dataset.rowIndex);
310
+ return;
311
+ case "delete-editor-row":
312
+ openDeleteEditorRowModal(actionNode.dataset.rowIndex);
313
+ return;
277
314
  case "clear-query":
278
315
  clearCurrentQuery();
279
316
  return;
@@ -296,6 +333,9 @@ async function handleAction(actionNode) {
296
333
  case "export-query-csv":
297
334
  await exportCurrentQueryCsv();
298
335
  return;
336
+ case "export-data-csv":
337
+ await exportCurrentDataTableCsv();
338
+ return;
299
339
  case "select-structure-entry":
300
340
  if (actionNode.dataset.entryName) {
301
341
  await selectStructureEntry(actionNode.dataset.entryName);
@@ -422,6 +462,7 @@ document.addEventListener("submit", async (event) => {
422
462
 
423
463
  switch (form.dataset.form) {
424
464
  case "open-connection": {
465
+ resetStructureGraphForDatabaseChange();
425
466
  const connection = await submitOpenConnection({
426
467
  path: String(formData.get("path") ?? ""),
427
468
  label: String(formData.get("label") ?? ""),
@@ -434,6 +475,7 @@ document.addEventListener("submit", async (event) => {
434
475
  return;
435
476
  }
436
477
  case "create-connection": {
478
+ resetStructureGraphForDatabaseChange();
437
479
  const connection = await submitCreateConnection({
438
480
  path: String(formData.get("path") ?? ""),
439
481
  label: String(formData.get("label") ?? ""),
@@ -445,6 +487,7 @@ document.addEventListener("submit", async (event) => {
445
487
  return;
446
488
  }
447
489
  case "import-sql": {
490
+ resetStructureGraphForDatabaseChange();
448
491
  const targetMode = String(formData.get("targetMode") ?? "active");
449
492
  const payload = {
450
493
  sqlFilePath: String(formData.get("sqlFilePath") ?? ""),
@@ -467,7 +510,14 @@ document.addEventListener("submit", async (event) => {
467
510
  return;
468
511
  }
469
512
  case "edit-connection": {
470
- await submitEditConnection(String(formData.get("connectionId") ?? ""), {
513
+ const connectionId = String(formData.get("connectionId") ?? "");
514
+ const isActiveConnection = getState().connections.active?.id === connectionId;
515
+
516
+ if (isActiveConnection) {
517
+ resetStructureGraphForDatabaseChange();
518
+ }
519
+
520
+ await submitEditConnection(connectionId, {
471
521
  path: String(formData.get("path") ?? ""),
472
522
  label: String(formData.get("label") ?? ""),
473
523
  readOnly: formData.get("readOnly") === "on",
@@ -475,6 +525,9 @@ document.addEventListener("submit", async (event) => {
475
525
 
476
526
  return;
477
527
  }
528
+ case "delete-row-confirm":
529
+ await submitDeleteRowConfirmation();
530
+ return;
478
531
  case "save-data-row": {
479
532
  const values = {};
480
533
 
@@ -254,6 +254,75 @@ function renderImportSqlForm(modal, state) {
254
254
  `;
255
255
  }
256
256
 
257
+ function renderDeleteRowConfirmForm(modal) {
258
+ const rowPreview = modal.rowPreview ?? [];
259
+
260
+ return `
261
+ <form class="space-y-5" data-form="delete-row-confirm">
262
+ <div class="space-y-3">
263
+ <p class="text-sm leading-7 text-on-surface">
264
+ Delete this row from <span class="font-bold text-primary-container">${escapeHtml(
265
+ modal.tableName ?? "the current table"
266
+ )}</span>?
267
+ </p>
268
+ <p class="text-sm leading-7 text-on-surface-variant/65">
269
+ This action cannot be undone.
270
+ </p>
271
+ ${
272
+ modal.rowLabel
273
+ ? `
274
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
275
+ ${escapeHtml(modal.rowLabel)}
276
+ </div>
277
+ `
278
+ : ""
279
+ }
280
+ ${
281
+ rowPreview.length
282
+ ? `
283
+ <div class="grid grid-cols-1 gap-3 md:grid-cols-2">
284
+ ${rowPreview
285
+ .map(
286
+ (field) => `
287
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
288
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
289
+ ${escapeHtml(field.label)}
290
+ </div>
291
+ <div
292
+ class="mt-2 text-sm text-on-surface"
293
+ title="${escapeHtml(field.fullValue ?? field.value ?? "")}"
294
+ >
295
+ ${escapeHtml(field.value ?? "")}
296
+ </div>
297
+ </div>
298
+ `
299
+ )
300
+ .join("")}
301
+ </div>
302
+ `
303
+ : ""
304
+ }
305
+ </div>
306
+ ${renderError(modal.error)}
307
+ <div class="flex items-center justify-end gap-3 pt-2">
308
+ <button
309
+ 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"
310
+ data-action="close-modal"
311
+ type="button"
312
+ >
313
+ Cancel
314
+ </button>
315
+ <button
316
+ class="border border-error/25 bg-error-container/10 px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-error"
317
+ type="submit"
318
+ >
319
+ ${modal.submitting ? "Deleting..." : "Delete Row"}
320
+ </button>
321
+ </div>
322
+ </form>
323
+ `;
324
+ }
325
+
257
326
  export function renderModal(state) {
258
327
  const modal = state.modal;
259
328
 
@@ -282,6 +351,11 @@ export function renderModal(state) {
282
351
  title: "Edit Connection",
283
352
  body: renderEditConnectionForm(modal),
284
353
  },
354
+ "delete-row": {
355
+ eyebrow: "Mutation // Confirm row deletion",
356
+ title: "Delete Row",
357
+ body: renderDeleteRowConfirmForm(modal),
358
+ },
285
359
  };
286
360
 
287
361
  const config = contentByKind[modal.kind];
@@ -38,6 +38,7 @@ export function renderQueryEditor({
38
38
  query,
39
39
  title,
40
40
  executing = false,
41
+ exporting = false,
41
42
  history = [],
42
43
  historyLoading = false,
43
44
  }) {
@@ -75,6 +76,13 @@ export function renderQueryEditor({
75
76
  >
76
77
  Clear
77
78
  </button>
79
+ <button
80
+ class="px-4 py-1.5 text-[10px] font-bold uppercase tracking-widest text-on-surface hover:bg-surface-container-highest transition-colors"
81
+ data-action="export-query-csv"
82
+ type="button"
83
+ >
84
+ ${exporting ? "Exporting..." : "Export CSV"}
85
+ </button>
78
86
  <button
79
87
  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"
80
88
  data-action="execute-query"
@@ -23,16 +23,23 @@ export function renderRowEditorPanel({
23
23
  disabledMessage = "",
24
24
  saveError = null,
25
25
  saving = false,
26
+ deleting = false,
26
27
  reloadAction = "",
27
- submitLabel = "Save Row",
28
+ submitLabel = "Save",
29
+ deleteAction = "",
30
+ deleteRowIndex = null,
31
+ deleteLabel = "Delete Row",
32
+ deleteEnabled = false,
28
33
  emptyEditableMessage = "This row has no editable scalar columns.",
29
34
  }) {
30
35
  const canSubmit = !disabledMessage && editableFields.length > 0;
36
+ const canDelete = !disabledMessage && deleteEnabled;
37
+ const formId = `${formName}-panel-form`;
31
38
 
32
39
  return `
33
40
  <section class="flex h-full min-h-0 flex-col bg-surface-low">
34
41
  <header class="border-b border-outline-variant/10 bg-surface-container px-6 py-5">
35
- <div class="flex items-start justify-between gap-4">
42
+ <div class="space-y-4">
36
43
  <div>
37
44
  <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
38
45
  ${escapeHtml(sectionLabel)}
@@ -50,13 +57,58 @@ export function renderRowEditorPanel({
50
57
  : ""
51
58
  }
52
59
  </div>
53
- <button
54
- 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"
55
- data-action="${escapeHtml(closeAction)}"
56
- type="button"
57
- >
58
- Close
59
- </button>
60
+ <div class="flex flex-wrap items-center justify-end gap-2">
61
+ ${
62
+ reloadAction
63
+ ? `
64
+ <button
65
+ 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"
66
+ data-action="${escapeHtml(reloadAction)}"
67
+ type="button"
68
+ >
69
+ Reload
70
+ </button>
71
+ `
72
+ : ""
73
+ }
74
+ ${
75
+ canSubmit
76
+ ? `
77
+ <button
78
+ 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"
79
+ form="${escapeHtml(formId)}"
80
+ type="submit"
81
+ ${saving || deleting ? "disabled" : ""}
82
+ >
83
+ ${escapeHtml(saving ? "Saving..." : submitLabel)}
84
+ </button>
85
+ `
86
+ : ""
87
+ }
88
+ ${
89
+ canDelete
90
+ ? `
91
+ <button
92
+ 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"
93
+ data-action="${escapeHtml(deleteAction)}"
94
+ data-row-index="${escapeHtml(String(deleteRowIndex ?? ""))}"
95
+ type="button"
96
+ ${saving || deleting ? "disabled" : ""}
97
+ >
98
+ ${escapeHtml(deleting ? "Deleting..." : deleteLabel)}
99
+ </button>
100
+ `
101
+ : ""
102
+ }
103
+ <button
104
+ aria-label="Close panel"
105
+ class="flex h-11 w-11 items-center justify-center border border-outline-variant/20 text-on-surface hover:bg-surface-container-highest"
106
+ data-action="${escapeHtml(closeAction)}"
107
+ type="button"
108
+ >
109
+ <span class="material-symbols-outlined text-base">close</span>
110
+ </button>
111
+ </div>
60
112
  </div>
61
113
  </header>
62
114
  <div class="custom-scrollbar flex-1 overflow-auto px-6 py-6">
@@ -68,7 +120,7 @@ export function renderRowEditorPanel({
68
120
  </div>
69
121
  `
70
122
  : `
71
- <form class="space-y-6" data-form="${escapeHtml(formName)}">
123
+ <form class="space-y-6" data-form="${escapeHtml(formName)}" id="${escapeHtml(formId)}">
72
124
  ${hiddenFields
73
125
  .map(
74
126
  (field) => `
@@ -92,7 +144,7 @@ export function renderRowEditorPanel({
92
144
  ${escapeHtml(field.label ?? field.name)}
93
145
  </span>
94
146
  <textarea
95
- class="min-h-[112px] 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"
147
+ class="min-h-[56px] 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"
96
148
  name="field:${escapeHtml(field.name)}"
97
149
  >${escapeHtml(field.value ?? "")}</textarea>
98
150
  </label>
@@ -117,28 +169,6 @@ export function renderRowEditorPanel({
117
169
  `
118
170
  : ""
119
171
  }
120
- <div class="flex items-center justify-end gap-3 border-t border-outline-variant/10 pt-6">
121
- ${
122
- reloadAction
123
- ? `
124
- <button
125
- class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface hover:bg-surface-container-highest"
126
- data-action="${escapeHtml(reloadAction)}"
127
- type="button"
128
- >
129
- Reload
130
- </button>
131
- `
132
- : ""
133
- }
134
- <button
135
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary disabled:cursor-default disabled:opacity-40"
136
- type="submit"
137
- ${canSubmit ? "" : "disabled"}
138
- >
139
- ${escapeHtml(saving ? "Saving..." : submitLabel)}
140
- </button>
141
- </div>
142
172
  </form>
143
173
  `
144
174
  }