sqlite-hub 0.6.0 → 0.8.7

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 (44) hide show
  1. package/README.md +107 -0
  2. package/bin/sqlite-hub.js +285 -4
  3. package/changelog.md +21 -0
  4. package/docs/DESIGN_GUIDELINES.md +36 -0
  5. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  6. package/frontend/index.html +80 -0
  7. package/frontend/js/api.js +34 -0
  8. package/frontend/js/app.js +188 -10
  9. package/frontend/js/components/connectionCard.js +3 -4
  10. package/frontend/js/components/emptyState.js +4 -4
  11. package/frontend/js/components/modal.js +341 -24
  12. package/frontend/js/components/queryChartRenderer.js +125 -0
  13. package/frontend/js/components/queryEditor.js +33 -6
  14. package/frontend/js/components/queryHistoryDetail.js +16 -7
  15. package/frontend/js/components/queryHistoryPanel.js +18 -7
  16. package/frontend/js/components/rowEditorPanel.js +59 -54
  17. package/frontend/js/components/sidebar.js +32 -32
  18. package/frontend/js/components/structureGraph.js +54 -122
  19. package/frontend/js/components/tableDesignerEditor.js +9 -8
  20. package/frontend/js/components/tableDesignerSidebar.js +52 -48
  21. package/frontend/js/components/tableDesignerSqlPreview.js +60 -28
  22. package/frontend/js/lib/queryChartOptions.js +283 -0
  23. package/frontend/js/lib/queryCharts.js +560 -0
  24. package/frontend/js/router.js +8 -0
  25. package/frontend/js/store.js +641 -0
  26. package/frontend/js/views/charts.js +499 -0
  27. package/frontend/js/views/connections.js +5 -17
  28. package/frontend/js/views/data.js +40 -27
  29. package/frontend/js/views/editor.js +19 -13
  30. package/frontend/js/views/overview.js +149 -10
  31. package/frontend/js/views/settings.js +2 -2
  32. package/frontend/js/views/structure.js +105 -59
  33. package/frontend/js/views/tableDesigner.js +7 -2
  34. package/frontend/styles/base.css +62 -0
  35. package/frontend/styles/components.css +68 -118
  36. package/frontend/styles/structure-graph.css +19 -82
  37. package/frontend/styles/tokens.css +2 -0
  38. package/frontend/styles/views.css +293 -66
  39. package/package.json +2 -1
  40. package/server/routes/charts.js +153 -0
  41. package/server/server.js +6 -0
  42. package/server/services/sqlite/overviewService.js +68 -0
  43. package/server/services/storage/appStateStore.js +400 -1
  44. package/server/services/storage/queryHistoryChartUtils.js +145 -0
@@ -1,3 +1,4 @@
1
+ import { showToast } from "../store.js";
1
2
  import { escapeHtml, formatNumber } from "../utils/format.js";
2
3
 
3
4
  let cytoscapeFactory = null;
@@ -98,6 +99,31 @@ function createColumnFlags(column, foreignKeyColumns) {
98
99
  return flags.join("");
99
100
  }
100
101
 
102
+ export function renderDdlSection(ddl, emptyLabel = "No DDL available.") {
103
+ const ddlText = typeof ddl === "string" ? ddl : "";
104
+ const hasDdl = Boolean(ddlText.trim());
105
+
106
+ return `
107
+ <section class="structure-graph__section">
108
+ <div class="structure-graph__section-header">
109
+ <div class="structure-graph__section-title">DDL</div>
110
+ <button
111
+ class="standard-button"
112
+ data-structure-graph-action="copy-ddl"
113
+ ${hasDdl ? "" : "disabled"}
114
+ type="button"
115
+ >
116
+ <span class="material-symbols-outlined text-sm">content_copy</span>
117
+ Copy
118
+ </button>
119
+ </div>
120
+ <pre class="structure-graph__ddl custom-scrollbar" data-structure-graph-ddl>${escapeHtml(
121
+ hasDdl ? ddlText : emptyLabel
122
+ )}</pre>
123
+ </section>
124
+ `;
125
+ }
126
+
101
127
  export function clearInspector() {
102
128
  return `
103
129
  <div class="structure-graph__panel is-empty">
@@ -165,12 +191,7 @@ export function renderInspector(tableData) {
165
191
  </div>
166
192
  </section>
167
193
 
168
- <section class="structure-graph__section">
169
- <div class="structure-graph__section-title">DDL</div>
170
- <pre class="structure-graph__ddl custom-scrollbar">${escapeHtml(
171
- tableData.ddl || "No DDL available."
172
- )}</pre>
173
- </section>
194
+ ${renderDdlSection(tableData.ddl)}
174
195
  </div>
175
196
  `;
176
197
  }
@@ -398,60 +419,6 @@ export function highlightConnectedElements(cy, element) {
398
419
  element.addClass("hovered");
399
420
  }
400
421
 
401
- function getBestMatchingNode(cy, name) {
402
- const normalizedQuery = String(name ?? "").trim().toLowerCase();
403
-
404
- if (!normalizedQuery) {
405
- return null;
406
- }
407
-
408
- const nodes = cy.nodes().toArray();
409
- const exactMatch = nodes.find(
410
- (node) => String(node.data("tableName")).toLowerCase() === normalizedQuery
411
- );
412
-
413
- if (exactMatch) {
414
- return exactMatch;
415
- }
416
-
417
- const startsWithMatch = nodes.find((node) =>
418
- String(node.data("tableName")).toLowerCase().startsWith(normalizedQuery)
419
- );
420
-
421
- if (startsWithMatch) {
422
- return startsWithMatch;
423
- }
424
-
425
- return (
426
- nodes.find((node) =>
427
- String(node.data("tableName")).toLowerCase().includes(normalizedQuery)
428
- ) ?? null
429
- );
430
- }
431
-
432
- export function focusTableByName(cy, name) {
433
- const node = getBestMatchingNode(cy, name);
434
-
435
- if (!node) {
436
- return null;
437
- }
438
-
439
- cy.animate(
440
- {
441
- fit: {
442
- eles: node.closedNeighborhood(),
443
- padding: 120,
444
- },
445
- duration: 240,
446
- },
447
- {
448
- queue: false,
449
- }
450
- );
451
-
452
- return node;
453
- }
454
-
455
422
  function destroyCurrentGraph() {
456
423
  if (!currentGraph) {
457
424
  return;
@@ -473,8 +440,6 @@ function destroyCurrentGraph() {
473
440
  zoom: currentGraph.cy.zoom(),
474
441
  inspectorHidden: currentGraph.inspectorHidden,
475
442
  selectedTableName: currentGraph.selectedTableName,
476
- searchValue: currentGraph.searchInput?.value ?? "",
477
- searchMiss: currentGraph.searchRoot?.classList.contains("is-miss") ?? false,
478
443
  };
479
444
  }
480
445
 
@@ -542,6 +507,25 @@ function updateOpenDataButton() {
542
507
  currentGraph.openDataButton.classList.toggle("is-disabled", !isEnabled);
543
508
  }
544
509
 
510
+ async function copyInspectorDdl(button) {
511
+ const ddlNode = button
512
+ ?.closest(".structure-graph__section")
513
+ ?.querySelector("[data-structure-graph-ddl]");
514
+ const ddl = ddlNode?.textContent ?? "";
515
+
516
+ if (!ddl.trim()) {
517
+ showToast("No DDL available to copy.", "alert");
518
+ return;
519
+ }
520
+
521
+ try {
522
+ await navigator.clipboard.writeText(ddl);
523
+ showToast("DDL copied.", "success");
524
+ } catch (error) {
525
+ showToast("Clipboard access failed.", "alert");
526
+ }
527
+ }
528
+
545
529
  function syncInspectorLayout() {
546
530
  if (!currentGraph?.root) {
547
531
  return;
@@ -565,17 +549,12 @@ function updateInspectorToggleButton() {
565
549
  `;
566
550
  }
567
551
 
568
- function clearSelection({ preserveSearch = true } = {}) {
552
+ function clearSelection() {
569
553
  if (!currentGraph) {
570
554
  return;
571
555
  }
572
556
 
573
557
  currentGraph.selectedTableName = null;
574
- if (!preserveSearch && currentGraph.searchInput) {
575
- currentGraph.searchInput.value = "";
576
- currentGraph.searchRoot.classList.remove("is-miss");
577
- }
578
-
579
558
  resetHighlights(currentGraph.cy);
580
559
  syncEntryHighlights();
581
560
  currentGraph.inspector.innerHTML = getDefaultInspectorMarkup();
@@ -677,61 +656,24 @@ function restorePersistedViewport(cy, persistedState) {
677
656
  return true;
678
657
  }
679
658
 
680
- export function setupToolbar(cy, schema) {
659
+ export function setupToolbar(cy) {
681
660
  if (!currentGraph) {
682
661
  return () => {};
683
662
  }
684
663
 
685
664
  const { root } = currentGraph;
686
- const searchRoot = root.querySelector("[data-structure-graph-search]");
687
- const searchInput = root.querySelector("[data-structure-graph-search-input]");
688
665
  const openDataButton = root.querySelector('[data-structure-graph-action="open-data"]');
689
666
  const inspectorToggleButton = root.querySelector(
690
667
  '[data-structure-graph-action="toggle-inspector"]'
691
668
  );
692
- let searchTimer = null;
693
669
 
694
- currentGraph.searchRoot = searchRoot;
695
- currentGraph.searchInput = searchInput;
696
670
  currentGraph.openDataButton = openDataButton;
697
671
  currentGraph.inspectorToggleButton = inspectorToggleButton;
698
672
  updateOpenDataButton();
699
673
  updateInspectorToggleButton();
700
674
  syncInspectorLayout();
701
675
 
702
- const handleSearch = () => {
703
- if (!searchInput || !searchRoot) {
704
- return;
705
- }
706
-
707
- const value = searchInput.value.trim();
708
-
709
- if (!value) {
710
- searchRoot.classList.remove("is-miss");
711
- restoreGraphState();
712
- if (!currentGraph.selectedTableName) {
713
- currentGraph.inspector.innerHTML = getDefaultInspectorMarkup();
714
- }
715
- return;
716
- }
717
-
718
- const matchedNode = focusTableByName(cy, value);
719
-
720
- if (!matchedNode) {
721
- searchRoot.classList.add("is-miss");
722
- return;
723
- }
724
-
725
- searchRoot.classList.remove("is-miss");
726
- applyTableSelection(matchedNode, { focus: true });
727
- };
728
-
729
- const onSearchInput = () => {
730
- window.clearTimeout(searchTimer);
731
- searchTimer = window.setTimeout(handleSearch, 140);
732
- };
733
-
734
- const onToolbarClick = (event) => {
676
+ const onToolbarClick = async (event) => {
735
677
  const button = event.target.closest("[data-structure-graph-action]");
736
678
 
737
679
  if (!button) {
@@ -755,7 +697,7 @@ export function setupToolbar(cy, schema) {
755
697
  });
756
698
  break;
757
699
  case "clear":
758
- clearSelection({ preserveSearch: false });
700
+ clearSelection();
759
701
  break;
760
702
  case "open-data":
761
703
  if (currentGraph?.selectedTableName) {
@@ -767,16 +709,16 @@ export function setupToolbar(cy, schema) {
767
709
  updateInspectorToggleButton();
768
710
  syncInspectorLayout();
769
711
  break;
712
+ case "copy-ddl":
713
+ await copyInspectorDdl(button);
714
+ break;
770
715
  default:
771
716
  }
772
717
  };
773
718
 
774
- searchInput?.addEventListener("input", onSearchInput);
775
719
  root.addEventListener("click", onToolbarClick);
776
720
 
777
721
  return () => {
778
- window.clearTimeout(searchTimer);
779
- searchInput?.removeEventListener("input", onSearchInput);
780
722
  root.removeEventListener("click", onToolbarClick);
781
723
  };
782
724
  }
@@ -837,8 +779,6 @@ export async function mountStructureGraph(snapshot) {
837
779
  cleanup: [],
838
780
  resizeObserver: null,
839
781
  resizeHandler: null,
840
- searchRoot: null,
841
- searchInput: null,
842
782
  openDataButton: null,
843
783
  inspectorToggleButton: null,
844
784
  inspectorHidden: cachedState?.inspectorHidden ?? false,
@@ -846,7 +786,7 @@ export async function mountStructureGraph(snapshot) {
846
786
  selectedTableName: null,
847
787
  };
848
788
 
849
- currentGraph.cleanup.push(setupToolbar(cy, schema));
789
+ currentGraph.cleanup.push(setupToolbar(cy));
850
790
 
851
791
  cy.on("tap", "node", (event) => {
852
792
  applyTableSelection(event.target, { focus: false });
@@ -877,14 +817,6 @@ export async function mountStructureGraph(snapshot) {
877
817
 
878
818
  currentGraph.initialSelectedTableName = selectedTableName;
879
819
 
880
- if (currentGraph.searchInput && cachedState?.searchValue) {
881
- currentGraph.searchInput.value = cachedState.searchValue;
882
- }
883
-
884
- if (currentGraph.searchRoot) {
885
- currentGraph.searchRoot.classList.toggle("is-miss", Boolean(cachedState?.searchMiss));
886
- }
887
-
888
820
  if (restorePersistedViewport(cy, cachedState)) {
889
821
  if (selectedTableName) {
890
822
  const selectedNode = cy.getElementById(getTableId(selectedTableName));
@@ -86,7 +86,7 @@ function renderColumnRow(column, draft, catalogTables) {
86
86
  )
87
87
  .join("")}
88
88
  </select>
89
- <label class="table-designer-check">
89
+ <label class="standard-checkbox table-designer-check table-designer-checkbox-override">
90
90
  <input
91
91
  data-bind="table-designer-column-flag"
92
92
  data-column-id="${escapeHtml(column.id)}"
@@ -96,7 +96,7 @@ function renderColumnRow(column, draft, catalogTables) {
96
96
  />
97
97
  <span>Not null</span>
98
98
  </label>
99
- <label class="table-designer-check">
99
+ <label class="standard-checkbox table-designer-check table-designer-checkbox-override">
100
100
  <input
101
101
  data-bind="table-designer-column-flag"
102
102
  data-column-id="${escapeHtml(column.id)}"
@@ -106,7 +106,7 @@ function renderColumnRow(column, draft, catalogTables) {
106
106
  />
107
107
  <span>Unique</span>
108
108
  </label>
109
- <label class="table-designer-check">
109
+ <label class="standard-checkbox table-designer-check table-designer-checkbox-override">
110
110
  <input
111
111
  data-bind="table-designer-column-flag"
112
112
  data-column-id="${escapeHtml(column.id)}"
@@ -164,12 +164,13 @@ function renderColumnRow(column, draft, catalogTables) {
164
164
  .join("")}
165
165
  </select>
166
166
  <button
167
- class="table-designer-delete"
167
+ class="delete-button"
168
168
  data-action="remove-table-designer-column"
169
169
  data-column-id="${escapeHtml(column.id)}"
170
170
  type="button"
171
171
  >
172
172
  <span class="material-symbols-outlined text-base">delete</span>
173
+ <span>Remove</span>
173
174
  </button>
174
175
  </div>
175
176
  `;
@@ -204,7 +205,7 @@ function renderFillToggle(draft) {
204
205
  const hasImportedRows = (draft.importedCsvRows?.length ?? 0) > 0;
205
206
 
206
207
  return `
207
- <label class="table-designer-fill-toggle ${hasImportedRows ? "" : "is-disabled"}">
208
+ <label class="standard-checkbox table-designer-fill-toggle table-designer-checkbox-override ${hasImportedRows ? "" : "is-disabled"}">
208
209
  <input
209
210
  data-bind="table-designer-field"
210
211
  data-field="fillImportedRows"
@@ -301,14 +302,14 @@ export function renderTableDesignerEditor(state) {
301
302
  <div class="table-designer-main__actions">
302
303
  ${renderFillToggle(draft)}
303
304
  <button
304
- class="table-designer-main__secondary"
305
+ class="standard-button"
305
306
  data-action="refresh-view"
306
307
  type="button"
307
308
  >
308
309
  Reload Schema
309
310
  </button>
310
311
  <button
311
- class="table-designer-main__primary ${draft.canSave ? "" : "is-disabled"}"
312
+ class="standard-button"
312
313
  data-action="save-table-designer"
313
314
  ${draft.canSave ? "" : "disabled"}
314
315
  type="button"
@@ -342,7 +343,7 @@ export function renderTableDesignerEditor(state) {
342
343
  <div class="table-designer-main__section-title">Columns</div>
343
344
  </div>
344
345
  <button
345
- class="table-designer-main__secondary"
346
+ class="standard-button"
346
347
  data-action="add-table-designer-column"
347
348
  type="button"
348
349
  >
@@ -1,41 +1,34 @@
1
- import { escapeHtml, formatNumber } from "../utils/format.js";
1
+ import { escapeHtml, formatNumber } from '../utils/format.js';
2
2
 
3
3
  function getFilteredTables(tables, searchQuery) {
4
- const normalizedSearch = String(searchQuery ?? "").trim().toLowerCase();
4
+ const normalizedSearch = String(searchQuery ?? '')
5
+ .trim()
6
+ .toLowerCase();
5
7
 
6
- if (!normalizedSearch) {
7
- return tables;
8
- }
8
+ if (!normalizedSearch) {
9
+ return tables;
10
+ }
9
11
 
10
- return tables.filter((table) => table.name.toLowerCase().includes(normalizedSearch));
12
+ return tables.filter(table => table.name.toLowerCase().includes(normalizedSearch));
11
13
  }
12
14
 
13
15
  export function renderTableDesignerSidebar(state) {
14
- const tables = state.tableDesigner.tables ?? [];
15
- const filteredTables = getFilteredTables(tables, state.tableDesigner.searchQuery);
16
- const isNewDraft = state.tableDesigner.draft?.mode === "create";
16
+ const tables = state.tableDesigner.tables ?? [];
17
+ const filteredTables = getFilteredTables(tables, state.tableDesigner.searchQuery);
18
+ const isNewDraft = state.tableDesigner.draft?.mode === 'create';
17
19
 
18
- return `
20
+ return `
19
21
  <aside class="table-designer-sidebar">
20
22
  <div class="table-designer-sidebar__header">
21
23
  <div>
22
- <div class="table-designer-sidebar__eyebrow">Schema Workspace</div>
23
- <div class="table-designer-sidebar__title">Table Designer</div>
24
+ <div class="table-designer-sidebar__eyebrow">Table Designer</div>
24
25
  <div class="table-designer-sidebar__meta">
25
- ${escapeHtml(formatNumber(tables.length))} table${tables.length === 1 ? "" : "s"}
26
+ ${escapeHtml(formatNumber(tables.length))} table${tables.length === 1 ? '' : 's'}
26
27
  </div>
27
28
  </div>
28
29
  <div class="table-designer-sidebar__header-actions">
29
30
  <button
30
- class="table-designer-sidebar__new-button"
31
- data-action="navigate"
32
- data-to="/table-designer/new"
33
- type="button"
34
- >
35
- + New Table
36
- </button>
37
- <button
38
- class="table-designer-sidebar__import-button"
31
+ class="standard-button"
39
32
  data-action="import-table-designer-csv"
40
33
  type="button"
41
34
  >
@@ -46,7 +39,16 @@ export function renderTableDesignerSidebar(state) {
46
39
  class="table-designer-sidebar__file-input"
47
40
  data-bind="table-designer-import-file"
48
41
  type="file"
49
- />
42
+ />
43
+ <button
44
+ class="signature-button"
45
+ data-action="navigate"
46
+ data-to="/table-designer/new"
47
+ type="button"
48
+ >
49
+ + New Table
50
+ </button>
51
+
50
52
  </div>
51
53
  </div>
52
54
 
@@ -58,23 +60,23 @@ export function renderTableDesignerSidebar(state) {
58
60
  placeholder="Search tables..."
59
61
  spellcheck="false"
60
62
  type="search"
61
- value="${escapeHtml(state.tableDesigner.searchQuery ?? "")}"
63
+ value="${escapeHtml(state.tableDesigner.searchQuery ?? '')}"
62
64
  />
63
65
  </label>
64
66
 
65
67
  <div class="table-designer-sidebar__list custom-scrollbar">
66
68
  ${
67
- state.tableDesigner.loading && !tables.length
68
- ? `
69
+ state.tableDesigner.loading && !tables.length
70
+ ? `
69
71
  <div class="table-designer-sidebar__empty">
70
72
  <span class="material-symbols-outlined mb-2 text-3xl">progress_activity</span>
71
73
  <div>Loading SQLite schema...</div>
72
74
  </div>
73
75
  `
74
- : isNewDraft
75
- ? `
76
+ : isNewDraft
77
+ ? `
76
78
  <button
77
- class="table-designer-sidebar__item is-active"
79
+ class="table-designer-sidebar__item is-active w-full border border-primary-container/30 bg-surface-container-high px-4 py-3 text-left transition-colors"
78
80
  data-action="navigate"
79
81
  data-to="/table-designer/new"
80
82
  type="button"
@@ -83,42 +85,44 @@ export function renderTableDesignerSidebar(state) {
83
85
  <div class="table-designer-sidebar__item-meta">unsaved schema</div>
84
86
  </button>
85
87
  `
86
- : ""
88
+ : ''
87
89
  }
88
90
  ${
89
- !filteredTables.length && !state.tableDesigner.loading
90
- ? `
91
+ !filteredTables.length && !state.tableDesigner.loading
92
+ ? `
91
93
  <div class="table-designer-sidebar__empty">
92
94
  ${
93
- tables.length
94
- ? "No tables match the current search."
95
- : "No tables found. Create the first table in this database."
95
+ tables.length
96
+ ? 'No tables match the current search.'
97
+ : 'No tables found. Create the first table in this database.'
96
98
  }
97
99
  </div>
98
100
  `
99
- : filteredTables
100
- .map(
101
- (table) => `
101
+ : filteredTables
102
+ .map(
103
+ table => `
102
104
  <button
103
- class="table-designer-sidebar__item ${
104
- !isNewDraft && table.name === state.tableDesigner.selectedTableName
105
- ? "is-active"
106
- : ""
105
+ class="table-designer-sidebar__item w-full border px-4 py-3 text-left transition-colors ${
106
+ !isNewDraft && table.name === state.tableDesigner.selectedTableName
107
+ ? 'is-active border-primary-container/30 bg-surface-container-high'
108
+ : 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
107
109
  }"
108
110
  data-action="navigate"
109
111
  data-to="/table-designer/${encodeURIComponent(table.name)}"
110
112
  type="button"
111
113
  >
112
- <div class="table-designer-sidebar__item-name">${escapeHtml(table.name)}</div>
113
- <div class="table-designer-sidebar__item-meta">
114
+ <div class="table-designer-sidebar__item-name ${
115
+ !isNewDraft && table.name === state.tableDesigner.selectedTableName ? 'is-active' : ''
116
+ }">${escapeHtml(table.name)}</div>
117
+ <div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
114
118
  ${escapeHtml(formatNumber(table.columnCount ?? 0))} column${
115
- Number(table.columnCount ?? 0) === 1 ? "" : "s"
119
+ Number(table.columnCount ?? 0) === 1 ? '' : 's'
116
120
  }
117
121
  </div>
118
122
  </button>
119
- `
120
- )
121
- .join("")
123
+ `,
124
+ )
125
+ .join('')
122
126
  }
123
127
  </div>
124
128
  </aside>
@@ -1,40 +1,72 @@
1
1
  import { escapeHtml } from "../utils/format.js";
2
2
 
3
- export function renderTableDesignerSqlPreview(draft) {
3
+ function renderPreviewHeader(draft, isVisible) {
4
+ return `
5
+ <div class="table-designer-preview__header">
6
+ <div>
7
+ <div class="table-designer-preview__eyebrow">SQL Preview</div>
8
+ <div class="table-designer-preview__title">${
9
+ draft ? "Live SQLite Output" : "No Draft Selected"
10
+ }</div>
11
+ </div>
12
+ <div class="table-designer-preview__actions">
13
+ ${
14
+ draft && isVisible
15
+ ? `
16
+ <button
17
+ class="standard-button"
18
+ data-action="copy-table-designer-sql"
19
+ type="button"
20
+ >
21
+ Copy SQL
22
+ </button>
23
+ `
24
+ : ""
25
+ }
26
+ <button
27
+ class="standard-button"
28
+ data-action="toggle-table-designer-sql-preview"
29
+ data-next-value="${isVisible ? "false" : "true"}"
30
+ type="button"
31
+ >
32
+ <span class="material-symbols-outlined">${isVisible ? "visibility_off" : "expand_less"}</span>
33
+ ${isVisible ? "Hide" : "Show"}
34
+ </button>
35
+ </div>
36
+ </div>
37
+ `;
38
+ }
39
+
40
+ export function renderTableDesignerSqlPreview(draft, isVisible = true) {
4
41
  if (!draft) {
5
42
  return `
6
- <section class="table-designer-preview shell-section">
7
- <div class="table-designer-preview__header">
8
- <div>
9
- <div class="table-designer-preview__eyebrow">SQL Preview</div>
10
- <div class="table-designer-preview__title">No Draft Selected</div>
11
- </div>
12
- </div>
13
- <div class="table-designer-preview__empty">
14
- Select a table or create a new one to inspect the generated SQLite statements.
15
- </div>
43
+ <section class="table-designer-preview shell-section${isVisible ? "" : " is-collapsed"}">
44
+ ${renderPreviewHeader(draft, isVisible)}
45
+ ${
46
+ isVisible
47
+ ? `
48
+ <div class="table-designer-preview__empty">
49
+ Select a table or create a new one to inspect the generated SQLite statements.
50
+ </div>
51
+ `
52
+ : ""
53
+ }
16
54
  </section>
17
55
  `;
18
56
  }
19
57
 
20
58
  return `
21
- <section class="table-designer-preview shell-section">
22
- <div class="table-designer-preview__header">
23
- <div>
24
- <div class="table-designer-preview__eyebrow">SQL Preview</div>
25
- <div class="table-designer-preview__title">Live SQLite Output</div>
26
- </div>
27
- <button
28
- class="table-designer-preview__copy"
29
- data-action="copy-table-designer-sql"
30
- type="button"
31
- >
32
- Copy SQL
33
- </button>
34
- </div>
35
- <pre class="table-designer-preview__body custom-scrollbar">${escapeHtml(
36
- draft.sqlPreview || "-- SQL preview unavailable."
37
- )}</pre>
59
+ <section class="table-designer-preview shell-section${isVisible ? "" : " is-collapsed"}">
60
+ ${renderPreviewHeader(draft, isVisible)}
61
+ ${
62
+ isVisible
63
+ ? `
64
+ <pre class="table-designer-preview__body custom-scrollbar">${escapeHtml(
65
+ draft.sqlPreview || "-- SQL preview unavailable."
66
+ )}</pre>
67
+ `
68
+ : ""
69
+ }
38
70
  </section>
39
71
  `;
40
72
  }