sqlite-hub 1.1.0 → 1.1.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.
Files changed (58) hide show
  1. package/README.md +17 -196
  2. package/bin/sqlite-hub.js +7 -2
  3. package/frontend/js/api.js +60 -0
  4. package/frontend/js/app.js +140 -10
  5. package/frontend/js/components/dropdownButton.js +92 -0
  6. package/frontend/js/components/modal.js +991 -876
  7. package/frontend/js/components/queryEditor.js +29 -18
  8. package/frontend/js/components/queryHistoryDetail.js +20 -1
  9. package/frontend/js/components/queryHistoryHeader.js +3 -2
  10. package/frontend/js/components/rowEditorPanel.js +1 -0
  11. package/frontend/js/components/sidebar.js +1 -0
  12. package/frontend/js/components/structureGraph.js +1 -0
  13. package/frontend/js/components/tableDesignerEditor.js +23 -42
  14. package/frontend/js/components/tableDesignerSidebar.js +7 -31
  15. package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
  16. package/frontend/js/router.js +2 -0
  17. package/frontend/js/store.js +407 -38
  18. package/frontend/js/utils/riskySql.js +165 -0
  19. package/frontend/js/views/backups.js +176 -0
  20. package/frontend/js/views/charts.js +12 -11
  21. package/frontend/js/views/data.js +37 -35
  22. package/frontend/js/views/documents.js +231 -162
  23. package/frontend/js/views/mediaTagging.js +6 -5
  24. package/frontend/js/views/structure.js +47 -43
  25. package/frontend/js/views/tableDesigner.js +45 -1
  26. package/frontend/styles/components.css +237 -59
  27. package/frontend/styles/structure-graph.css +10 -2
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/tokens.css +2 -0
  30. package/frontend/styles/views.css +84 -41
  31. package/package.json +2 -1
  32. package/server/routes/backups.js +120 -0
  33. package/server/routes/connections.js +26 -3
  34. package/server/routes/externalApi.js +6 -2
  35. package/server/server.js +3 -1
  36. package/server/services/databaseCommandService.js +4 -3
  37. package/server/services/sqlite/backupService.js +497 -66
  38. package/server/services/sqlite/connectionManager.js +2 -2
  39. package/server/services/sqlite/dataBrowserService.js +11 -3
  40. package/server/services/sqlite/importService.js +25 -0
  41. package/server/services/sqlite/sqlExecutor.js +2 -0
  42. package/server/services/storage/appStateStore.js +379 -88
  43. package/tests/api-token-auth.test.js +45 -2
  44. package/tests/backup-manager.test.js +140 -0
  45. package/tests/backups-view.test.js +64 -0
  46. package/tests/charts-height-preset-storage.test.js +60 -0
  47. package/tests/charts-route-state.test.js +144 -0
  48. package/tests/cli-service-delegation.test.js +97 -0
  49. package/tests/connection-removal.test.js +52 -0
  50. package/tests/database-command-service.test.js +38 -3
  51. package/tests/documents-view.test.js +132 -0
  52. package/tests/dropdown-button.test.js +75 -0
  53. package/tests/query-editor.test.js +28 -0
  54. package/tests/query-history-detail.test.js +37 -0
  55. package/tests/query-history-header.test.js +30 -0
  56. package/tests/risky-sql.test.js +30 -0
  57. package/tests/row-editor-null-values.test.js +24 -0
  58. package/tests/structure-view.test.js +56 -0
@@ -1,3 +1,4 @@
1
+ import { renderDropdownButton } from '../components/dropdownButton.js';
1
2
  import { escapeHtml, formatCompactDateTime, formatNumber } from '../utils/format.js';
2
3
  import { renderMarkdownPreview } from '../utils/markdownDocuments.js';
3
4
 
@@ -24,7 +25,7 @@ function renderDocumentListItem(item, selectedId) {
24
25
 
25
26
  return `
26
27
  <button
27
- class="documents-list-item ${isSelected ? 'is-selected' : ''}"
28
+ class="documents-list-item subnavi-item ${isSelected ? 'is-selected' : ''}"
28
29
  data-action="select-document"
29
30
  data-document-id="${escapeHtml(item.id)}"
30
31
  type="button"
@@ -42,183 +43,249 @@ function renderDocumentListItem(item, selectedId) {
42
43
  `;
43
44
  }
44
45
 
46
+ function getFilteredDocuments(items, searchQuery) {
47
+ const normalizedSearch = String(searchQuery ?? '')
48
+ .trim()
49
+ .toLowerCase();
50
+
51
+ if (!normalizedSearch) {
52
+ return items;
53
+ }
54
+
55
+ return items.filter(item =>
56
+ [item.filename, item.title]
57
+ .filter(Boolean)
58
+ .some(value => String(value).toLowerCase().includes(normalizedSearch)),
59
+ );
60
+ }
61
+
62
+ function renderDocumentsSearch(documents) {
63
+ return `
64
+ <label class="table-designer-sidebar__search">
65
+ <span class="material-symbols-outlined text-sm text-on-surface-variant/55">search</span>
66
+ <input
67
+ class="table-designer-sidebar__search-input"
68
+ data-bind="documents-search"
69
+ placeholder="Search documents..."
70
+ spellcheck="false"
71
+ type="search"
72
+ value="${escapeHtml(documents.searchQuery ?? '')}"
73
+ />
74
+ </label>
75
+ `;
76
+ }
77
+
45
78
  function renderDocumentsSidebar(documents) {
46
79
  const items = documents.items ?? [];
80
+ const filteredItems = getFilteredDocuments(items, documents.searchQuery);
47
81
 
48
82
  return `
49
- <aside class="documents-view__sidebar">
50
- <div class="documents-view__sidebar-header">
83
+ <aside class="documents-view__sidebar subnavi-panel">
84
+ <div class="documents-view__sidebar-header subnavi-header">
51
85
  <div>
52
- <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/50">
53
- Documents
54
- </div>
55
- <div class="mt-1 font-mono text-xs text-primary-container">${formatNumber(items.length)} files</div>
86
+ <div class="subnavi-header-title">Documents</div>
87
+ <div class="subnavi-header-details">${formatNumber(items.length)} files</div>
56
88
  </div>
57
- <button
58
- class="icon-button"
59
- data-action="create-document"
60
- title="New document"
61
- type="button"
62
- ${documents.saving ? 'disabled aria-disabled="true"' : ''}
63
- >
64
- <span class="material-symbols-outlined">add</span>
65
- </button>
66
89
  </div>
67
- <div class="documents-view__sidebar-body custom-scrollbar">
90
+ ${renderDocumentsSearch(documents)}
91
+ <div class="documents-view__sidebar-body subnavi-list custom-scrollbar">
68
92
  ${
69
- items.length
70
- ? items.map(item => renderDocumentListItem(item, documents.selectedId)).join('')
71
- : `<div class="documents-list-empty">No documents yet</div>`
93
+ filteredItems.length
94
+ ? filteredItems.map(item => renderDocumentListItem(item, documents.selectedId)).join('')
95
+ : `<div class="documents-list-empty">${
96
+ items.length ? 'No documents match the current search.' : 'No documents yet'
97
+ }</div>`
72
98
  }
73
99
  </div>
74
100
  </aside>
75
101
  `;
76
102
  }
77
103
 
78
- function renderDocumentToolbar(documents) {
104
+ function renderDocumentImportFileInput() {
105
+ return `
106
+ <input
107
+ accept=".md,.markdown,text/markdown,text/plain"
108
+ data-bind="document-import-file"
109
+ hidden
110
+ type="file"
111
+ />
112
+ `;
113
+ }
114
+
115
+ function renderNewDocumentDropdown(documents, className = '') {
116
+ return `
117
+ <div class="${escapeHtml(className)}">
118
+ ${renderDropdownButton({
119
+ disabled: documents.saving,
120
+ icon: 'add',
121
+ label: 'New Document',
122
+ title: 'New document',
123
+ items: [
124
+ {
125
+ action: 'create-document',
126
+ icon: 'draft',
127
+ label: 'Blank Page',
128
+ },
129
+ {
130
+ action: 'import-document-markdown',
131
+ icon: 'upload_file',
132
+ label: 'Import .md',
133
+ },
134
+ ],
135
+ })}
136
+ </div>
137
+ `;
138
+ }
139
+
140
+ function renderDocumentsPanelToggle(documents) {
141
+ const visible = documents.documentsVisible !== false;
142
+
143
+ return `
144
+ <button
145
+ class="standard-button panel-toggle-button ${visible ? '' : 'is-active'}"
146
+ aria-pressed="${visible ? 'false' : 'true'}"
147
+ data-action="toggle-documents-panel"
148
+ type="button"
149
+ >
150
+ <span class="material-symbols-outlined">${visible ? 'visibility_off' : 'visibility'}</span>
151
+ ${visible ? 'Hide Documents' : 'Show Documents'}
152
+ </button>
153
+ `;
154
+ }
155
+
156
+ function renderDocumentPaneToggle(documents, pane) {
157
+ const isEditor = pane === 'editor';
158
+ const visible = isEditor ? documents.editorVisible : documents.previewVisible;
159
+ const label = `${visible ? 'Hide' : 'Show'} ${isEditor ? 'Editor' : 'Preview'}`;
160
+ const icon = visible ? 'visibility_off' : isEditor ? 'edit_note' : 'visibility';
161
+
162
+ return `
163
+ <button
164
+ class="standard-button panel-toggle-button documents-pane__toggle ${visible ? '' : 'is-active'}"
165
+ aria-pressed="${visible ? 'false' : 'true'}"
166
+ data-action="toggle-document-pane"
167
+ data-pane="${escapeHtml(pane)}"
168
+ type="button"
169
+ >
170
+ <span class="material-symbols-outlined">${icon}</span>
171
+ ${label}
172
+ </button>
173
+ `;
174
+ }
175
+
176
+ function renderDocumentsTitlebar(documents, options = {}) {
177
+ const { showDocumentActions = false, showFilename = true } = options;
79
178
  const disabled = documents.saving || documents.detailLoading || !documents.selectedId;
80
179
 
81
- return ` <label class="documents-filename-field">
82
- <input
83
- class="control-input"
84
- data-bind="document-field"
85
- data-field="filename"
86
- name="filename"
87
- spellcheck="false"
88
- type="text"
89
- value="${escapeHtml(documents.draftFilename)}"
90
- ${!documents.selectedId ? 'disabled' : ''}
91
- />
92
- </label>
93
- <div class="documents-toolbar">
94
-
95
-
96
-
97
- <div class="documents-toolbar__actions">
98
- <button
99
- class="standard-button"
100
- data-action="toggle-document-pane"
101
- data-pane="editor"
102
- type="button"
103
- >
104
- <span class="material-symbols-outlined">${documents.editorVisible ? 'visibility_off' : 'edit_note'}</span>
105
- ${documents.editorVisible ? 'Hide Editor' : 'Show Editor'}
106
- </button>
107
- <button
108
- class="standard-button"
109
- data-action="toggle-document-pane"
110
- data-pane="preview"
111
- type="button"
112
- >
113
- <span class="material-symbols-outlined">${documents.previewVisible ? 'visibility_off' : 'visibility'}</span>
114
- ${documents.previewVisible ? 'Hide Preview' : 'Show Preview'}
115
- </button>
116
- <input
117
- accept=".md,.markdown,text/markdown,text/plain"
118
- data-bind="document-import-file"
119
- hidden
120
- type="file"
121
- />
122
- </div>
123
- <div class="documents-toolbar__actions">
124
- <button
125
- class="standard-button"
126
- data-action="open-document-insert-table-modal"
127
- type="button"
128
- ${disabled ? 'disabled aria-disabled="true"' : ''}
129
- >
130
- <span class="material-symbols-outlined">table_chart</span>
131
- Insert Table
132
- </button>
133
- <button
134
- class="standard-button"
135
- data-action="open-document-insert-note-modal"
136
- type="button"
137
- ${disabled ? 'disabled aria-disabled="true"' : ''}
138
- >
139
- <span class="material-symbols-outlined">note_add</span>
140
- Insert Note
141
- </button>
142
- <button
143
- class="standard-button"
144
- data-action="export-document-markdown"
145
- type="button"
146
- ${disabled ? 'disabled aria-disabled="true"' : ''}
147
- >
148
- <span class="material-symbols-outlined">download</span>
149
- Export .md
150
- </button>
151
- <button
152
- class="standard-button"
153
- data-action="import-document-markdown"
154
- type="button"
155
- ${documents.saving ? 'disabled aria-disabled="true"' : ''}
156
- >
157
- <span class="material-symbols-outlined">upload_file</span>
158
- Import .md
159
- </button>
160
- </div>
161
- <div class="documents-toolbar__actions">
162
- <button
163
- class="standard-button"
164
- data-action="delete-document"
165
- type="button"
166
- ${disabled || documents.deleting ? 'disabled aria-disabled="true"' : ''}
167
- >
168
- <span class="material-symbols-outlined">delete</span>
169
- Delete
170
- </button>
171
- <button
172
- class="signature-button"
173
- data-action="save-document"
174
- type="button"
175
- ${disabled || !documents.dirty ? 'disabled aria-disabled="true"' : ''}
176
- >
177
- <span class="material-symbols-outlined">save</span>
178
- ${documents.saving ? 'Saving...' : 'Save'}
179
- </button>
180
- </div>
180
+ return `
181
+ <div class="documents-titlebar">
182
+ ${renderDocumentsPanelToggle(documents)}
183
+ ${
184
+ showFilename
185
+ ? `
186
+ <label class="documents-filename-field">
187
+ <input
188
+ class="control-input"
189
+ data-bind="document-field"
190
+ data-field="filename"
191
+ name="filename"
192
+ spellcheck="false"
193
+ type="text"
194
+ value="${escapeHtml(documents.draftFilename)}"
195
+ ${!documents.selectedId ? 'disabled' : ''}
196
+ />
197
+ </label>
198
+ `
199
+ : ''
200
+ }
201
+ ${renderNewDocumentDropdown(documents, 'documents-create-button')}
202
+ ${
203
+ showDocumentActions
204
+ ? `
205
+ ${renderDropdownButton({
206
+ disabled,
207
+ icon: 'add_box',
208
+ label: 'Insert',
209
+ title: 'Insert content',
210
+ items: [
211
+ {
212
+ action: 'open-document-insert-table-modal',
213
+ icon: 'table_chart',
214
+ label: 'Insert Table',
215
+ },
216
+ {
217
+ action: 'open-document-insert-note-modal',
218
+ icon: 'note_add',
219
+ label: 'Insert Note',
220
+ },
221
+ ],
222
+ })}
223
+ <button
224
+ class="standard-button"
225
+ data-action="export-document-markdown"
226
+ type="button"
227
+ ${disabled ? 'disabled aria-disabled="true"' : ''}
228
+ >
229
+ <span class="material-symbols-outlined">download</span>
230
+ Export .md
231
+ </button>
232
+ <button
233
+ class="delete-button"
234
+ data-action="delete-document"
235
+ type="button"
236
+ ${disabled || documents.deleting ? 'disabled aria-disabled="true"' : ''}
237
+ >
238
+ <span class="material-symbols-outlined">delete</span>
239
+ Delete
240
+ </button>
241
+ `
242
+ : ''
243
+ }
181
244
  </div>
182
245
  `;
183
246
  }
184
247
 
185
248
  function renderDocumentEditor(documents) {
186
- if (!documents.editorVisible) {
187
- return '';
188
- }
189
-
190
249
  return `
191
- <section class="documents-pane documents-pane--editor">
250
+ <section class="documents-pane documents-pane--editor ${documents.editorVisible ? '' : 'documents-pane--collapsed'}">
192
251
  <div class="documents-pane__header">
193
- <span>Editor</span>
194
- <span>${formatNumber(documents.draftContent.length)} chars</span>
252
+ ${renderDocumentPaneToggle(documents, 'editor')}
253
+ <span class="documents-pane__meta">${formatNumber(documents.draftContent.length)} chars</span>
195
254
  </div>
196
- <textarea
197
- class="documents-editor-input custom-scrollbar"
198
- data-bind="document-field"
199
- data-field="content"
200
- name="content"
201
- spellcheck="true"
202
- ${!documents.selectedId ? 'disabled' : ''}
203
- >${escapeHtml(documents.draftContent)}</textarea>
255
+ ${
256
+ documents.editorVisible
257
+ ? `
258
+ <textarea
259
+ class="documents-editor-input custom-scrollbar"
260
+ data-bind="document-field"
261
+ data-field="content"
262
+ name="content"
263
+ spellcheck="true"
264
+ ${!documents.selectedId ? 'disabled' : ''}
265
+ >${escapeHtml(documents.draftContent)}</textarea>
266
+ `
267
+ : ''
268
+ }
204
269
  </section>
205
270
  `;
206
271
  }
207
272
 
208
273
  function renderDocumentPreview(documents) {
209
- if (!documents.previewVisible) {
210
- return '';
211
- }
212
-
213
274
  return `
214
- <section class="documents-pane documents-pane--preview">
275
+ <section class="documents-pane documents-pane--preview ${documents.previewVisible ? '' : 'documents-pane--collapsed'}">
215
276
  <div class="documents-pane__header">
216
- <span>Preview</span>
217
- <span>${escapeHtml(documents.dirty ? 'unsaved' : 'saved')}</span>
218
- </div>
219
- <div class="document-markdown-preview custom-scrollbar" data-document-preview>
220
- ${renderMarkdownPreview(documents.draftContent)}
277
+ ${renderDocumentPaneToggle(documents, 'preview')}
278
+ <span class="documents-pane__meta">${escapeHtml(documents.dirty ? 'unsaved' : 'saved')}</span>
221
279
  </div>
280
+ ${
281
+ documents.previewVisible
282
+ ? `
283
+ <div class="document-markdown-preview custom-scrollbar" data-document-preview>
284
+ ${renderMarkdownPreview(documents.draftContent)}
285
+ </div>
286
+ `
287
+ : ''
288
+ }
222
289
  </section>
223
290
  `;
224
291
  }
@@ -230,15 +297,7 @@ function renderEmptyDocumentsState(documents) {
230
297
  <p class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
231
298
  No Documents
232
299
  </p>
233
- <button
234
- class="signature-button mt-4"
235
- data-action="create-document"
236
- type="button"
237
- ${documents.saving ? 'disabled aria-disabled="true"' : ''}
238
- >
239
- <span class="material-symbols-outlined">add</span>
240
- New Document
241
- </button>
300
+ ${renderNewDocumentDropdown(documents, 'mt-4')}
242
301
  </div>
243
302
  `;
244
303
  }
@@ -247,6 +306,7 @@ function renderDocumentDetail(documents) {
247
306
  if (documents.loading && !documents.items.length) {
248
307
  return `
249
308
  <main class="documents-view__detail">
309
+ ${renderDocumentsTitlebar(documents, { showFilename: false })}
250
310
  <div class="documents-empty-state">
251
311
  <span class="material-symbols-outlined">sync</span>
252
312
  <p class="font-headline text-2xl font-black uppercase tracking-tight text-primary-container">
@@ -260,18 +320,26 @@ function renderDocumentDetail(documents) {
260
320
  if (!documents.selectedId) {
261
321
  return `
262
322
  <main class="documents-view__detail">
323
+ ${renderDocumentsTitlebar(documents, { showFilename: false })}
263
324
  ${renderEmptyDocumentsState(documents)}
264
325
  </main>
265
326
  `;
266
327
  }
267
328
 
268
- const paneCount = Number(documents.editorVisible) + Number(documents.previewVisible);
329
+ const workspaceClasses = [
330
+ 'documents-workspace',
331
+ documents.editorVisible && documents.previewVisible ? 'documents-workspace--split' : '',
332
+ !documents.editorVisible ? 'documents-workspace--editor-collapsed' : '',
333
+ !documents.previewVisible ? 'documents-workspace--preview-collapsed' : '',
334
+ ]
335
+ .filter(Boolean)
336
+ .join(' ');
269
337
 
270
338
  return `
271
339
  <main class="documents-view__detail">
272
- ${renderDocumentToolbar(documents)}
340
+ ${renderDocumentsTitlebar(documents, { showDocumentActions: true })}
273
341
  ${documents.saveError ? `<div class="documents-error">${escapeHtml(documents.saveError.message)}</div>` : ''}
274
- <div class="documents-workspace ${paneCount > 1 ? 'documents-workspace--split' : ''}">
342
+ <div class="${workspaceClasses}">
275
343
  ${renderDocumentEditor(documents)}
276
344
  ${renderDocumentPreview(documents)}
277
345
  </div>
@@ -288,13 +356,14 @@ export function renderDocumentsView(state) {
288
356
  }
289
357
 
290
358
  const documents = state.documents;
359
+ const documentsVisible = documents.documentsVisible !== false;
291
360
 
292
361
  return {
293
- main: `<div class="documents-view">
294
- ${renderDocumentsSidebar(documents)}
362
+ main: `<div class="documents-view ${documentsVisible ? 'documents-view--with-subnavi' : ''}">
363
+ ${renderDocumentImportFileInput()}
364
+ ${documentsVisible ? renderDocumentsSidebar(documents) : ''}
295
365
  ${renderDocumentDetail(documents)}
296
- </div>
297
- </div>`,
366
+ </div>`,
298
367
  panel: '',
299
368
  };
300
369
  }
@@ -396,8 +396,8 @@ function renderPreviewMedia(
396
396
 
397
397
  const metadata = getWorkflowMetadataEntries(currentItem);
398
398
  const toggleLabel = detailsVisible
399
- ? '<span class="material-symbols-outlined">visibility_off</span> Shrink Media Viewer'
400
- : 'Show Media Viewer';
399
+ ? '<span class="material-symbols-outlined">visibility_off</span> Hide Viewer'
400
+ : '<span class="material-symbols-outlined">visibility</span> Show Viewer';
401
401
 
402
402
  return `
403
403
  <div class="media-tagging-preview ${detailsVisible ? '' : 'media-tagging-preview--meta-hidden'}">
@@ -450,11 +450,12 @@ function renderPreviewMedia(
450
450
  : ''
451
451
  }
452
452
  <button
453
- class="standard-button media-tagging-preview__toggle"
453
+ class="standard-button panel-toggle-button media-tagging-preview__toggle ${detailsVisible ? '' : 'is-active'}"
454
+ aria-pressed="${detailsVisible ? 'false' : 'true'}"
454
455
  data-action="toggle-media-tagging-current-media"
455
456
  data-next-value="${detailsVisible ? 'false' : 'true'}"
456
- data-expanded-label="Shrink Media Viewer"
457
- data-collapsed-label="Show Media Viewer"
457
+ data-expanded-label="Hide Viewer"
458
+ data-collapsed-label="Show Viewer"
458
459
  aria-expanded="${detailsVisible ? 'true' : 'false'}"
459
460
  type="button"
460
461
  >
@@ -1,4 +1,5 @@
1
1
  import { clearInspector, renderDdlSection, renderInspector } from '../components/structureGraph.js';
2
+ import { renderDropdownButton } from '../components/dropdownButton.js';
2
3
  import { escapeHtml, formatNumber } from '../utils/format.js';
3
4
 
4
5
  function renderEntryGroup(title, entries, activeName, options = {}) {
@@ -22,7 +23,7 @@ function renderEntryGroup(title, entries, activeName, options = {}) {
22
23
  : '';
23
24
 
24
25
  return [
25
- '<button class="w-full border px-3 ',
26
+ '<button class="subnavi-item border px-3 ',
26
27
  compact ? 'py-2.5' : 'py-3',
27
28
  ' text-left transition-colors ',
28
29
  isActive
@@ -217,10 +218,11 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
217
218
 
218
219
  return `
219
220
  <section class="structure-graph" data-structure-graph-root>
220
- <div class="structure-graph__toolbar">
221
+ <div class="structure-graph__toolbar workspace-header">
221
222
  <div class="structure-graph__toolbar-main">
222
223
  <button
223
- class="${toolbarButtonClass}"
224
+ class="${toolbarButtonClass} panel-toggle-button ${tablesVisible ? '' : 'is-active'}"
225
+ aria-pressed="${tablesVisible ? 'false' : 'true'}"
224
226
  data-action="toggle-structure-tables"
225
227
  type="button"
226
228
  >
@@ -229,30 +231,31 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
229
231
  </button>
230
232
  </div>
231
233
  <div class="structure-graph__toolbar-actions">
232
- <button
233
- class="${toolbarButtonClass}"
234
- data-structure-graph-action="fit"
235
- type="button"
236
- >
237
- <span class="material-symbols-outlined text-sm">fit_screen</span>
238
- Fit Graph
239
- </button>
240
- <button
241
- class="${toolbarButtonClass}"
242
- data-structure-graph-action="relayout"
243
- type="button"
244
- >
245
- <span class="material-symbols-outlined text-sm">device_hub</span>
246
- Recalculate Layout
247
- </button>
248
- <button
249
- class="${toolbarButtonClass}"
250
- data-structure-graph-action="clear"
251
- type="button"
252
- >
253
- <span class="material-symbols-outlined text-sm">close</span>
254
- Clear Selection
255
- </button>
234
+ ${renderDropdownButton({
235
+ icon: 'format_shapes',
236
+ label: 'Format',
237
+ title: 'Format graph',
238
+ items: [
239
+ {
240
+ action: 'fit',
241
+ actionAttribute: 'data-structure-graph-action',
242
+ icon: 'fit_screen',
243
+ label: 'Fit Graph',
244
+ },
245
+ {
246
+ action: 'relayout',
247
+ actionAttribute: 'data-structure-graph-action',
248
+ icon: 'device_hub',
249
+ label: 'Recalculate Layout',
250
+ },
251
+ {
252
+ action: 'clear',
253
+ actionAttribute: 'data-structure-graph-action',
254
+ icon: 'close',
255
+ label: 'Clear Selection',
256
+ },
257
+ ],
258
+ })}
256
259
  <button
257
260
  class="${toolbarButtonClass}"
258
261
  data-structure-graph-action="open-data"
@@ -265,7 +268,8 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
265
268
  </div>
266
269
  <div class="structure-graph__toolbar-inspector">
267
270
  <button
268
- class="${toolbarButtonClass}"
271
+ class="${toolbarButtonClass} panel-toggle-button"
272
+ aria-pressed="false"
269
273
  data-structure-graph-action="toggle-inspector"
270
274
  type="button"
271
275
  >
@@ -321,24 +325,24 @@ export function renderStructureView(state) {
321
325
  ${
322
326
  tablesVisible
323
327
  ? `
324
- <aside class="structure-view__sidebar">
325
- <div class="structure-view__sidebar-header">
326
- <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
327
- Objects
328
- </div>
329
- <div class="mt-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
330
- total ${escapeHtml(
331
- formatNumber(
332
- (structure?.grouped?.tables?.length ?? 0) +
333
- (structure?.grouped?.views?.length ?? 0) +
334
- (structure?.grouped?.indexes?.length ?? 0) +
335
- (structure?.grouped?.triggers?.length ?? 0),
336
- ),
337
- )}
328
+ <aside class="structure-view__sidebar subnavi-panel">
329
+ <div class="structure-view__sidebar-header subnavi-header">
330
+ <div>
331
+ <div class="subnavi-header-title">Objects</div>
332
+ <div class="subnavi-header-details">
333
+ total ${escapeHtml(
334
+ formatNumber(
335
+ (structure?.grouped?.tables?.length ?? 0) +
336
+ (structure?.grouped?.views?.length ?? 0) +
337
+ (structure?.grouped?.indexes?.length ?? 0) +
338
+ (structure?.grouped?.triggers?.length ?? 0),
339
+ ),
340
+ )}
341
+ </div>
338
342
  </div>
339
343
  </div>
340
344
  ${renderStructureTableSearch(state)}
341
- <div class="structure-view__sidebar-body custom-scrollbar">
345
+ <div class="structure-view__sidebar-body subnavi-list custom-scrollbar">
342
346
  ${
343
347
  structure
344
348
  ? `