sqlite-hub 0.2.0 → 0.3.1

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,16 @@
1
+ import cytoscape from "/vendor/cytoscape/dist/cytoscape.esm.min.mjs";
2
+
3
+ let elkRegistered = false;
4
+
5
+ export function getCytoscape() {
6
+ if (!elkRegistered) {
7
+ if (typeof window.cytoscapeElk !== "function") {
8
+ throw new Error("cytoscape-elk is not available on window.");
9
+ }
10
+
11
+ cytoscape.use(window.cytoscapeElk);
12
+ elkRegistered = true;
13
+ }
14
+
15
+ return cytoscape;
16
+ }
package/js/store.js CHANGED
@@ -24,6 +24,7 @@ const state = {
24
24
  recent: [],
25
25
  active: null,
26
26
  loading: false,
27
+ backupLoading: false,
27
28
  error: null,
28
29
  },
29
30
  settings: {
@@ -47,6 +48,7 @@ const state = {
47
48
  page: 1,
48
49
  pageSize: 50,
49
50
  selectedRowIndex: null,
51
+ exportLoading: false,
50
52
  error: null,
51
53
  saveError: null,
52
54
  },
@@ -159,6 +161,7 @@ function setMissingDatabaseState() {
159
161
  state.dataBrowser.table = null;
160
162
  state.dataBrowser.page = 1;
161
163
  state.dataBrowser.selectedRowIndex = null;
164
+ state.dataBrowser.exportLoading = false;
162
165
  state.dataBrowser.error = error;
163
166
  state.dataBrowser.saveError = null;
164
167
 
@@ -480,6 +483,7 @@ function invalidateDatabaseCaches() {
480
483
  state.dataBrowser.table = null;
481
484
  state.dataBrowser.page = 1;
482
485
  state.dataBrowser.selectedRowIndex = null;
486
+ state.dataBrowser.exportLoading = false;
483
487
  state.dataBrowser.error = null;
484
488
  state.dataBrowser.saveError = null;
485
489
  state.structure.data = null;
@@ -743,6 +747,32 @@ export async function removeConnection(id) {
743
747
  }
744
748
  }
745
749
 
750
+ export async function createActiveConnectionBackup() {
751
+ if (!state.connections.active) {
752
+ pushToast("No active SQLite database selected for backup.", "alert");
753
+ return null;
754
+ }
755
+
756
+ state.connections.backupLoading = true;
757
+ emitChange();
758
+
759
+ try {
760
+ const response = await api.createActiveConnectionBackup();
761
+ await refreshConnectionsState();
762
+ pushToast(response.message || "Backup created.", "success");
763
+ return response.data;
764
+ } catch (error) {
765
+ pushToast(
766
+ normalizeError(error)?.message || "Backup could not be created.",
767
+ "alert"
768
+ );
769
+ return null;
770
+ } finally {
771
+ state.connections.backupLoading = false;
772
+ emitChange();
773
+ }
774
+ }
775
+
746
776
  export function setCurrentQuery(query) {
747
777
  const nextQuery = String(query ?? "");
748
778
  const previousLineCount = Math.max(1, String(state.editor.sqlText || "").split("\n").length);
@@ -1027,6 +1057,31 @@ export async function exportCurrentQueryCsv() {
1027
1057
  }
1028
1058
  }
1029
1059
 
1060
+ export async function exportCurrentDataTableCsv() {
1061
+ const tableName = state.dataBrowser.selectedTable;
1062
+
1063
+ if (!tableName) {
1064
+ pushToast("No table selected for export.", "alert");
1065
+ return false;
1066
+ }
1067
+
1068
+ state.dataBrowser.exportLoading = true;
1069
+ emitChange();
1070
+
1071
+ try {
1072
+ await api.downloadTableCsv(tableName);
1073
+ pushToast(`CSV export started for ${tableName}.`, "success");
1074
+ return true;
1075
+ } catch (error) {
1076
+ state.dataBrowser.error = normalizeError(error);
1077
+ emitChange();
1078
+ return false;
1079
+ } finally {
1080
+ state.dataBrowser.exportLoading = false;
1081
+ emitChange();
1082
+ }
1083
+ }
1084
+
1030
1085
  export async function refreshCurrentRoute() {
1031
1086
  await loadRouteData(state.route);
1032
1087
  }
@@ -5,9 +5,11 @@ import { escapeHtml } from "../utils/format.js";
5
5
  function renderConnectionsActionButton({
6
6
  label,
7
7
  icon,
8
+ action = "open-modal",
8
9
  modal,
9
10
  tone = "secondary",
10
11
  className = "",
12
+ disabled = false,
11
13
  }) {
12
14
  const clipPath = "polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%)";
13
15
  const toneClassName =
@@ -19,14 +21,16 @@ function renderConnectionsActionButton({
19
21
  ? "text-base text-on-primary"
20
22
  : "text-base text-primary-container/90";
21
23
  const clipStyle = `style="--clip-path: ${clipPath};"`;
24
+ const modalAttribute = modal ? `data-modal="${modal}"` : "";
22
25
 
23
26
  return `
24
27
  <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
+ class="flex h-11 items-center justify-between gap-6 px-5 font-headline text-xs font-bold uppercase tracking-[0.18em] transition-colors disabled:cursor-default disabled:opacity-40 ${toneClassName} ${className}"
29
+ data-action="${escapeHtml(action)}"
30
+ ${modalAttribute}
28
31
  ${clipStyle}
29
32
  type="button"
33
+ ${disabled ? "disabled" : ""}
30
34
  >
31
35
  <span>${label}</span>
32
36
  <span class="material-symbols-outlined ${iconClassName}">${icon}</span>
@@ -112,6 +116,17 @@ export function renderConnectionsView(state) {
112
116
  modal: "create-connection",
113
117
  className: "min-w-[13rem]",
114
118
  })}
119
+ ${
120
+ state.connections.active
121
+ ? renderConnectionsActionButton({
122
+ label: state.connections.backupLoading ? "Creating Backup..." : "Create Backup",
123
+ icon: "inventory_2",
124
+ action: "create-backup",
125
+ className: "min-w-[13rem]",
126
+ disabled: state.connections.backupLoading,
127
+ })
128
+ : ""
129
+ }
115
130
  `;
116
131
 
117
132
  return {
package/js/views/data.js CHANGED
@@ -95,6 +95,19 @@ function renderWorkspaceHeader(state) {
95
95
  </div>
96
96
  </div>
97
97
  <div class="flex items-center gap-3">
98
+ ${
99
+ table
100
+ ? `
101
+ <button
102
+ 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"
103
+ data-action="export-data-csv"
104
+ type="button"
105
+ >
106
+ ${state.dataBrowser.exportLoading ? "Exporting..." : "Export CSV"}
107
+ </button>
108
+ `
109
+ : ""
110
+ }
98
111
  <button
99
112
  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"
100
113
  data-action="refresh-view"
@@ -243,6 +243,7 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
243
243
  ${renderQueryEditor({
244
244
  query: state.editor.sqlText,
245
245
  executing: state.editor.executing,
246
+ exporting: state.editor.exportLoading,
246
247
  history: state.editor.history,
247
248
  historyLoading: state.editor.historyLoading,
248
249
  title: connection?.label ?? "SQLite Query Workspace",
@@ -1,9 +1,10 @@
1
- import { renderMetricCard } from "../components/metricCard.js";
2
1
  import { renderPageHeader } from "../components/pageHeader.js";
3
- import { getCurrentStructureEntryDetail } from "../store.js";
2
+ import { clearInspector, renderInspector } from "../components/structureGraph.js";
4
3
  import { escapeHtml, formatNumber } from "../utils/format.js";
5
4
 
6
- function renderEntryGroup(title, entries, activeName) {
5
+ function renderEntryGroup(title, entries, activeName, options = {}) {
6
+ const { compact = false, showMeta = true } = options;
7
+
7
8
  return `
8
9
  <section class="shell-section p-5">
9
10
  <div class="mb-4 text-[10px] font-bold uppercase tracking-[0.25em] text-primary-container">
@@ -17,13 +18,18 @@ function renderEntryGroup(title, entries, activeName) {
17
18
  .map(
18
19
  (entry) => `
19
20
  <button
20
- class="w-full border px-3 py-3 text-left transition-colors ${
21
+ class="w-full border px-3 ${compact ? "py-2.5" : "py-3"} text-left transition-colors ${
21
22
  entry.name === activeName
22
23
  ? "border-primary-container/30 bg-surface-container-high"
23
24
  : "border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high"
24
25
  }"
25
26
  data-action="select-structure-entry"
26
27
  data-entry-name="${escapeHtml(entry.name)}"
28
+ ${
29
+ entry.type === "table"
30
+ ? `data-structure-entry-name="${escapeHtml(entry.name)}"`
31
+ : ""
32
+ }
27
33
  type="button"
28
34
  >
29
35
  <div class="font-mono text-xs ${
@@ -33,9 +39,15 @@ function renderEntryGroup(title, entries, activeName) {
33
39
  }">
34
40
  ${escapeHtml(entry.name)}
35
41
  </div>
36
- <div class="mt-1 text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
37
- ${escapeHtml(entry.tableName || entry.type)}
38
- </div>
42
+ ${
43
+ showMeta
44
+ ? `
45
+ <div class="mt-1 text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
46
+ ${escapeHtml(entry.tableName || entry.type)}
47
+ </div>
48
+ `
49
+ : ""
50
+ }
39
51
  </button>
40
52
  `
41
53
  )
@@ -50,136 +62,242 @@ function renderEntryGroup(title, entries, activeName) {
50
62
  `;
51
63
  }
52
64
 
53
- function renderDetail(detail) {
65
+ function renderLoadingInspector() {
66
+ return `
67
+ <div class="structure-graph__panel is-empty">
68
+ <span class="material-symbols-outlined structure-graph__empty-icon">progress_activity</span>
69
+ <div class="structure-graph__title">Loading</div>
70
+ <p class="text-sm leading-7 text-on-surface-variant/55">
71
+ Loading structure metadata for the selected object.
72
+ </p>
73
+ </div>
74
+ `;
75
+ }
76
+
77
+ function renderObjectInspector(detail) {
54
78
  if (!detail) {
55
- return `
56
- <div class="shell-section p-8">
57
- <p class="text-sm text-on-surface-variant/55">Select a structure object to inspect metadata and relational detail.</p>
58
- </div>
59
- `;
79
+ return clearInspector();
60
80
  }
61
81
 
82
+ const visibleColumns = (detail.columns ?? []).filter((column) => column.visible !== false);
83
+ const foreignKeyLinks = (detail.foreignKeys ?? []).reduce(
84
+ (count, foreignKey) => count + (foreignKey.mappings?.length ?? 0),
85
+ 0
86
+ );
87
+
62
88
  return `
63
- <section class="shell-section p-8">
64
- <div class="mb-6 flex items-center justify-between border-b border-outline-variant/10 pb-4">
65
- <div>
66
- <h2 class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
67
- ${escapeHtml(detail.name)}
68
- </h2>
69
- <p class="mt-2 font-mono text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/50">
70
- ${escapeHtml(detail.type ?? "table")}
71
- </p>
72
- </div>
73
- <div class="flex items-center gap-3">
89
+ <div class="structure-graph__panel">
90
+ <div class="space-y-3">
91
+ <div class="structure-graph__eyebrow">Object Inspector</div>
92
+ <div class="structure-graph__title">${escapeHtml(detail.name)}</div>
93
+ <div class="flex flex-wrap items-center gap-3">
94
+ <div class="structure-graph__subtitle">${escapeHtml(detail.type ?? "object")}</div>
74
95
  ${
75
- detail.type === "table"
96
+ detail.tableName
76
97
  ? `
77
- <button
78
- 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"
79
- data-action="navigate"
80
- data-to="/data/${encodeURIComponent(detail.name)}"
81
- type="button"
82
- >
83
- Open Data
84
- </button>
98
+ <div class="structure-graph__subtitle">
99
+ TABLE ${escapeHtml(detail.tableName)}
100
+ </div>
85
101
  `
86
102
  : ""
87
103
  }
88
- <div class="flex h-12 w-12 items-center justify-center bg-primary-container/10">
89
- <span class="material-symbols-outlined text-2xl text-primary-container">account_tree</span>
90
- </div>
91
104
  </div>
92
105
  </div>
93
- <div class="grid grid-cols-1 gap-6 xl:grid-cols-[0.95fr_1.05fr]">
94
- <div class="space-y-6">
95
- <div class="bg-surface-container-lowest p-4">
96
- <div class="mb-3 text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">Columns</div>
97
- ${
98
- detail.columns?.length
99
- ? `
100
- <div class="space-y-2 font-mono text-[11px] text-on-surface/65">
101
- ${detail.columns
102
- .map(
103
- (column) => `
104
- <div class="flex justify-between gap-3">
105
- <span>${escapeHtml(column.name)}</span>
106
- <span class="text-primary-container">${escapeHtml(
107
- column.declaredType || column.affinity
108
- )}</span>
109
- </div>
110
- `
111
- )
112
- .join("")}
113
- </div>
114
- `
115
- : '<div class="text-sm text-on-surface-variant/45">No column metadata for this object.</div>'
116
- }
117
- </div>
118
- <div class="bg-surface-container-lowest p-4">
119
- <div class="mb-3 text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">Relational Detail</div>
120
- <div class="space-y-2 font-mono text-[11px] text-on-surface/65">
121
- <div class="flex justify-between">
122
- <span>Foreign Keys</span>
123
- <span class="text-primary-container">${escapeHtml(
124
- formatNumber(detail.foreignKeys?.length ?? 0)
125
- )}</span>
126
- </div>
127
- <div class="flex justify-between">
128
- <span>Indexes</span>
129
- <span class="text-primary-container">${escapeHtml(
130
- formatNumber(detail.indexes?.length ?? 0)
131
- )}</span>
132
- </div>
133
- <div class="flex justify-between">
134
- <span>Triggers</span>
135
- <span class="text-primary-container">${escapeHtml(
136
- formatNumber(detail.triggers?.length ?? 0)
137
- )}</span>
138
- </div>
139
- <div class="flex justify-between">
140
- <span>Identity</span>
141
- <span class="text-primary-container">${escapeHtml(
142
- detail.identityStrategy?.type ?? "n/a"
143
- )}</span>
144
- </div>
145
- </div>
106
+
107
+ <div class="structure-graph__summary">
108
+ <div class="structure-graph__summary-card">
109
+ <div class="structure-graph__summary-label">Columns</div>
110
+ <div class="structure-graph__summary-value">${escapeHtml(
111
+ formatNumber(visibleColumns.length)
112
+ )}</div>
113
+ </div>
114
+ <div class="structure-graph__summary-card">
115
+ <div class="structure-graph__summary-label">FK Links</div>
116
+ <div class="structure-graph__summary-value">${escapeHtml(
117
+ formatNumber(foreignKeyLinks)
118
+ )}</div>
119
+ </div>
120
+ <div class="structure-graph__summary-card">
121
+ <div class="structure-graph__summary-label">Indexes</div>
122
+ <div class="structure-graph__summary-value">${escapeHtml(
123
+ formatNumber(detail.indexes?.length ?? 0)
124
+ )}</div>
125
+ </div>
126
+ <div class="structure-graph__summary-card">
127
+ <div class="structure-graph__summary-label">Triggers</div>
128
+ <div class="structure-graph__summary-value">${escapeHtml(
129
+ formatNumber(detail.triggers?.length ?? 0)
130
+ )}</div>
131
+ </div>
132
+ </div>
133
+
134
+ ${
135
+ visibleColumns.length
136
+ ? `
137
+ <section class="structure-graph__section">
138
+ <div class="structure-graph__section-title">Columns</div>
139
+ <div class="structure-graph__column-list">
140
+ ${visibleColumns
141
+ .map(
142
+ (column) => `
143
+ <div class="structure-graph__column-row">
144
+ <div class="min-w-0 space-y-1">
145
+ <div class="structure-graph__column-name">${escapeHtml(
146
+ column.name
147
+ )}</div>
148
+ <div class="structure-graph__column-type">${escapeHtml(
149
+ column.declaredType || column.affinity || "BLOB"
150
+ )}</div>
151
+ </div>
152
+ <div class="structure-graph__column-flags">
153
+ ${
154
+ column.primaryKeyPosition > 0
155
+ ? `<span class="structure-graph__flag is-key">PK${
156
+ column.primaryKeyPosition > 1
157
+ ? ` ${escapeHtml(String(column.primaryKeyPosition))}`
158
+ : ""
159
+ }</span>`
160
+ : ""
161
+ }
162
+ <span class="structure-graph__flag is-nullable">${
163
+ column.notNull ? "NOT NULL" : "NULLABLE"
164
+ }</span>
165
+ </div>
166
+ </div>
167
+ `
168
+ )
169
+ .join("")}
170
+ </div>
171
+ </section>
172
+ `
173
+ : ""
174
+ }
175
+
176
+ <section class="structure-graph__section">
177
+ <div class="structure-graph__section-title">DDL</div>
178
+ <pre class="structure-graph__ddl custom-scrollbar">${escapeHtml(
179
+ detail.ddl || "No DDL available."
180
+ )}</pre>
181
+ </section>
182
+ </div>
183
+ `;
184
+ }
185
+
186
+ function renderGraphSurface(structure, selectedName, detail, detailLoading) {
187
+ const graph = structure?.graph ?? { tables: [], relationshipCount: 0 };
188
+ const selectedGraphTable =
189
+ graph.tables?.find((table) => table.name === selectedName && table.type === "table") ?? null;
190
+ const inspectorMarkup = selectedGraphTable
191
+ ? renderInspector(selectedGraphTable)
192
+ : detailLoading
193
+ ? renderLoadingInspector()
194
+ : detail
195
+ ? renderObjectInspector(detail)
196
+ : clearInspector();
197
+
198
+ return `
199
+ <section class="structure-graph" data-structure-graph-root>
200
+ <div class="structure-graph__toolbar">
201
+ <div class="structure-graph__toolbar-main">
202
+ <label class="structure-graph__search" data-structure-graph-search>
203
+ <span class="material-symbols-outlined structure-graph__search-icon">search</span>
204
+ <input
205
+ class="structure-graph__search-input"
206
+ data-structure-graph-search-input
207
+ placeholder="Find table"
208
+ spellcheck="false"
209
+ type="search"
210
+ />
211
+ </label>
212
+ <div class="structure-graph__toolbar-meta">
213
+ ${escapeHtml(formatNumber(graph.tables?.length ?? 0))} TABLES //
214
+ ${escapeHtml(formatNumber(graph.relationshipCount ?? 0))} RELATIONSHIPS
146
215
  </div>
147
216
  </div>
148
- <div class="bg-surface-container-lowest p-4">
149
- <div class="mb-3 text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">DDL</div>
150
- <pre class="custom-scrollbar max-h-[520px] overflow-auto whitespace-pre-wrap font-mono text-[11px] leading-6 text-on-surface-variant/75">${escapeHtml(
151
- detail.ddl || "No DDL available."
152
- )}</pre>
217
+ <div class="structure-graph__toolbar-actions">
218
+ <button
219
+ class="structure-graph__button"
220
+ data-structure-graph-action="fit"
221
+ type="button"
222
+ >
223
+ <span class="material-symbols-outlined text-sm">fit_screen</span>
224
+ Fit Graph
225
+ </button>
226
+ <button
227
+ class="structure-graph__button"
228
+ data-structure-graph-action="relayout"
229
+ type="button"
230
+ >
231
+ <span class="material-symbols-outlined text-sm">device_hub</span>
232
+ Recalculate Layout
233
+ </button>
234
+ <button
235
+ class="structure-graph__button"
236
+ data-structure-graph-action="clear"
237
+ type="button"
238
+ >
239
+ <span class="material-symbols-outlined text-sm">close</span>
240
+ Clear Selection
241
+ </button>
242
+ <button
243
+ class="structure-graph__button is-disabled"
244
+ data-structure-graph-action="open-data"
245
+ disabled
246
+ type="button"
247
+ >
248
+ <span class="material-symbols-outlined text-sm">table_rows</span>
249
+ Open Data
250
+ </button>
251
+ <button
252
+ class="structure-graph__button"
253
+ data-structure-graph-action="toggle-inspector"
254
+ type="button"
255
+ >
256
+ <span class="material-symbols-outlined text-sm">right_panel_close</span>
257
+ Hide Inspector
258
+ </button>
153
259
  </div>
154
260
  </div>
261
+
262
+ <div class="structure-graph__workspace">
263
+ <div class="structure-graph__canvas-shell">
264
+ <div class="structure-graph__canvas" data-structure-graph-canvas></div>
265
+ <div
266
+ class="structure-graph__empty"
267
+ data-structure-graph-empty
268
+ ${graph.tables?.length ? "hidden" : ""}
269
+ >
270
+ <span class="material-symbols-outlined structure-graph__empty-icon">account_tree</span>
271
+ <div class="structure-graph__title">No Tables</div>
272
+ <p class="text-sm leading-7 text-on-surface-variant/55">
273
+ The active database does not expose table metadata for the schema graph.
274
+ </p>
275
+ </div>
276
+ </div>
277
+
278
+ <aside
279
+ class="structure-graph__inspector custom-scrollbar"
280
+ data-structure-graph-inspector
281
+ >
282
+ ${inspectorMarkup}
283
+ </aside>
284
+ </div>
155
285
  </section>
156
286
  `;
157
287
  }
158
288
 
159
289
  export function renderStructureView(state) {
160
290
  const structure = state.structure.data;
161
- const detail = getCurrentStructureEntryDetail(state);
162
- const counts = structure
163
- ? [
164
- { label: "Tables", value: formatNumber(structure.grouped.tables.length) },
165
- { label: "Views", value: formatNumber(structure.grouped.views.length) },
166
- { label: "Indexes", value: formatNumber(structure.grouped.indexes.length) },
167
- {
168
- label: "Triggers",
169
- value: formatNumber(structure.grouped.triggers.length),
170
- accent: true,
171
- },
172
- ]
173
- : [];
291
+ const detail =
292
+ state.structure.detail?.name === state.structure.selectedName ? state.structure.detail : null;
174
293
 
175
294
  return {
176
295
  main: `
177
296
  <section class="view-surface min-h-full bg-surface-container">
178
- <div class="view-frame mx-auto max-w-7xl space-y-8">
297
+ <div class="view-frame space-y-8">
179
298
  ${renderPageHeader({
180
- eyebrow: "Structure // sqlite_master + PRAGMA",
181
299
  title: "Structure",
182
- subtitle: "Raw DDL, table identity, foreign keys, and index metadata",
300
+ subtitle: "Schema graph, foreign-key paths, raw DDL, and object metadata",
183
301
  })}
184
302
 
185
303
  ${
@@ -203,15 +321,13 @@ export function renderStructureView(state) {
203
321
  `
204
322
  : structure
205
323
  ? `
206
- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
207
- ${counts.map((metric) => renderMetricCard(metric)).join("")}
208
- </div>
209
- <section class="grid grid-cols-1 gap-6 xl:grid-cols-[0.45fr_1.55fr]">
324
+ <section class="grid grid-cols-1 gap-6 xl:grid-cols-[18.5rem_minmax(0,1fr)] 2xl:grid-cols-[19.5rem_minmax(0,1fr)]">
210
325
  <div class="space-y-6">
211
326
  ${renderEntryGroup(
212
327
  "Tables",
213
328
  structure.grouped.tables,
214
- state.structure.selectedName
329
+ state.structure.selectedName,
330
+ { compact: true, showMeta: false }
215
331
  )}
216
332
  ${renderEntryGroup(
217
333
  "Views",
@@ -221,7 +337,8 @@ export function renderStructureView(state) {
221
337
  ${renderEntryGroup(
222
338
  "Indexes",
223
339
  structure.grouped.indexes,
224
- state.structure.selectedName
340
+ state.structure.selectedName,
341
+ { compact: true, showMeta: false }
225
342
  )}
226
343
  ${renderEntryGroup(
227
344
  "Triggers",
@@ -229,7 +346,12 @@ export function renderStructureView(state) {
229
346
  state.structure.selectedName
230
347
  )}
231
348
  </div>
232
- ${renderDetail(detail)}
349
+ ${renderGraphSurface(
350
+ structure,
351
+ state.structure.selectedName,
352
+ detail,
353
+ state.structure.detailLoading
354
+ )}
233
355
  </section>
234
356
  `
235
357
  : ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-hub",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "main": "server/server.js",
6
6
  "bin": {
@@ -13,6 +13,9 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "better-sqlite3": "^11.8.1",
16
+ "cytoscape": "^3.33.2",
17
+ "cytoscape-elk": "^2.3.0",
18
+ "elkjs": "^0.11.1",
16
19
  "express": "^4.21.2"
17
20
  }
18
21
  }