sqlite-hub 0.4.0 → 0.6.0

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 (56) hide show
  1. package/README.md +2 -2
  2. package/changelog.md +15 -0
  3. package/frontend/assets/mockups/connections.png +0 -0
  4. package/frontend/assets/mockups/data.png +0 -0
  5. package/frontend/assets/mockups/data_row_editor.png +0 -0
  6. package/frontend/assets/mockups/home.png +0 -0
  7. package/frontend/assets/mockups/sql_editor.png +0 -0
  8. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  9. package/frontend/assets/mockups/structure.png +0 -0
  10. package/frontend/assets/mockups/structure_inspector.png +0 -0
  11. package/frontend/js/api.js +114 -5
  12. package/frontend/js/app.js +368 -18
  13. package/frontend/js/components/bottomTabs.js +1 -1
  14. package/frontend/js/components/dataGrid.js +3 -3
  15. package/frontend/js/components/queryEditor.js +33 -55
  16. package/frontend/js/components/queryHistoryDetail.js +263 -0
  17. package/frontend/js/components/queryHistoryPanel.js +228 -0
  18. package/frontend/js/components/queryResults.js +32 -46
  19. package/frontend/js/components/rowEditorPanel.js +73 -14
  20. package/frontend/js/components/sidebar.js +1 -0
  21. package/frontend/js/components/tableDesignerEditor.js +356 -0
  22. package/frontend/js/components/tableDesignerSidebar.js +126 -0
  23. package/frontend/js/components/tableDesignerSqlPreview.js +40 -0
  24. package/frontend/js/router.js +10 -0
  25. package/frontend/js/store.js +841 -22
  26. package/frontend/js/utils/format.js +23 -0
  27. package/frontend/js/utils/tableDesigner.js +1192 -0
  28. package/frontend/js/views/data.js +273 -250
  29. package/frontend/js/views/editor.js +34 -10
  30. package/frontend/js/views/overview.js +15 -0
  31. package/frontend/js/views/tableDesigner.js +37 -0
  32. package/frontend/styles/base.css +87 -73
  33. package/frontend/styles/components.css +841 -188
  34. package/frontend/styles/views.css +40 -0
  35. package/package.json +1 -1
  36. package/server/routes/data.js +2 -0
  37. package/server/routes/export.js +4 -1
  38. package/server/routes/overview.js +12 -0
  39. package/server/routes/sql.js +163 -5
  40. package/server/routes/tableDesigner.js +60 -0
  41. package/server/server.js +5 -1
  42. package/server/services/sqlite/dataBrowserService.js +4 -16
  43. package/server/services/sqlite/exportService.js +4 -16
  44. package/server/services/sqlite/overviewService.js +34 -0
  45. package/server/services/sqlite/sqlExecutor.js +83 -63
  46. package/server/services/sqlite/tableDesigner/changeAnalysis.js +295 -0
  47. package/server/services/sqlite/tableDesigner/schemaMapping.js +233 -0
  48. package/server/services/sqlite/tableDesigner/sql.js +63 -0
  49. package/server/services/sqlite/tableDesigner/validation.js +245 -0
  50. package/server/services/sqlite/tableDesignerService.js +181 -0
  51. package/server/services/sqlite/tableSort.js +63 -0
  52. package/server/services/storage/appStateStore.js +674 -1
  53. package/server/services/storage/queryHistoryUtils.js +169 -0
  54. package/frontend/assets/mockups/data_edit.png +0 -0
  55. package/frontend/assets/mockups/graph_visualize.png +0 -0
  56. package/frontend/assets/mockups/overview.png +0 -0
@@ -1,9 +1,39 @@
1
1
  import { renderDataGrid } from "./dataGrid.js";
2
2
  import { escapeHtml, formatCellValue, formatNumber } from "../utils/format.js";
3
3
 
4
+ function getSortIcon(columnName, sortColumn, sortDirection) {
5
+ if (columnName !== sortColumn) {
6
+ return "unfold_more";
7
+ }
8
+
9
+ return sortDirection === "desc" ? "south" : "north";
10
+ }
11
+
12
+ function renderSortableHeader(columnName, sortColumn, sortDirection) {
13
+ const isActive = columnName === sortColumn;
14
+
15
+ return `
16
+ <button
17
+ class="flex w-full items-center justify-between gap-2 text-left transition-colors ${
18
+ isActive ? "text-primary-container" : "text-on-surface-variant hover:text-primary-container"
19
+ }"
20
+ data-action="sort-editor-results-column"
21
+ data-column-name="${escapeHtml(columnName)}"
22
+ type="button"
23
+ >
24
+ <span class="truncate">${escapeHtml(columnName)}</span>
25
+ <span class="material-symbols-outlined text-sm leading-none">${getSortIcon(
26
+ columnName,
27
+ sortColumn,
28
+ sortDirection
29
+ )}</span>
30
+ </button>
31
+ `;
32
+ }
33
+
4
34
  export function renderQueryResultsPane(
5
35
  result,
6
- { exporting = false, selectedRowIndex = null, editable = false, editStatusMessage = "" } = {}
36
+ { selectedRowIndex = null, editable = false, sortColumn = null, sortDirection = null } = {}
7
37
  ) {
8
38
  if (!result) {
9
39
  return `
@@ -17,9 +47,9 @@ export function renderQueryResultsPane(
17
47
  }
18
48
 
19
49
  const columns = (result.columns ?? []).map((columnName) => ({
20
- label: escapeHtml(columnName),
21
50
  headerClassName:
22
51
  "border-b-2 border-primary-container px-4 py-3 text-[10px] font-bold uppercase tracking-widest",
52
+ renderHeader: () => renderSortableHeader(columnName, sortColumn, sortDirection),
23
53
  cellClassName: "px-4 py-3 align-top text-on-surface",
24
54
  render: (row) => {
25
55
  const value = formatCellValue(row[columnName]);
@@ -32,50 +62,6 @@ export function renderQueryResultsPane(
32
62
 
33
63
  return `
34
64
  <div class="relative flex h-full min-h-0 flex-col overflow-hidden bg-surface-container">
35
- <div class="flex items-center justify-between gap-4 bg-surface-container-high px-4 py-2">
36
- <div class="flex items-center gap-4">
37
- <div class="flex items-center gap-2 text-primary-container">
38
- <span class="material-symbols-outlined text-sm">database</span>
39
- <span class="text-[10px] font-bold uppercase tracking-widest">Query Results</span>
40
- </div>
41
- <div class="h-3 w-px bg-outline-variant/30"></div>
42
- <div class="text-[10px] font-mono text-on-surface-variant/60">
43
- ROWS_RETURNED: ${escapeHtml(formatNumber(result.rows?.length ?? 0))}
44
- </div>
45
- <div class="text-[10px] font-mono text-on-surface-variant/60">
46
- AFFECTED: ${escapeHtml(formatNumber(result.affectedRowCount ?? 0))}
47
- </div>
48
- <div class="text-[10px] font-mono text-on-surface-variant/60">
49
- EXEC: ${escapeHtml(String(result.timingMs ?? 0))}ms
50
- </div>
51
- ${
52
- editStatusMessage
53
- ? `
54
- <div class="h-3 w-px bg-outline-variant/30"></div>
55
- <div class="text-[10px] font-mono text-on-surface-variant/60">
56
- ${escapeHtml(editStatusMessage)}
57
- </div>
58
- `
59
- : ""
60
- }
61
- </div>
62
- <div class="flex gap-4 text-[10px] font-mono uppercase">
63
- <button
64
- class="text-on-surface-variant transition-colors hover:text-primary-container"
65
- data-action="export-query-csv"
66
- type="button"
67
- >
68
- ${exporting ? "Exporting..." : "Export CSV"}
69
- </button>
70
- <button
71
- class="text-on-surface-variant transition-colors hover:text-primary-container"
72
- data-action="clear-results"
73
- type="button"
74
- >
75
- Clear Results
76
- </button>
77
- </div>
78
- </div>
79
65
  <div class="custom-scrollbar min-h-0 flex-1 overflow-auto bg-surface-container-lowest">
80
66
  ${
81
67
  result.columns?.length
@@ -1,16 +1,87 @@
1
1
  import { escapeHtml } from "../utils/format.js";
2
2
 
3
+ function getJsonPreview(value) {
4
+ if (value === null || value === undefined) {
5
+ return null;
6
+ }
7
+
8
+ if (typeof value === "object") {
9
+ return JSON.stringify(value, null, 2);
10
+ }
11
+
12
+ const text = String(value).trim();
13
+
14
+ if (!text || !["{", "["].includes(text[0])) {
15
+ return null;
16
+ }
17
+
18
+ try {
19
+ const parsed = JSON.parse(text);
20
+
21
+ if (!parsed || typeof parsed !== "object") {
22
+ return null;
23
+ }
24
+
25
+ return JSON.stringify(parsed, null, 2);
26
+ } catch (error) {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function renderJsonViewer(prettyJson, title = "JSON Viewer") {
32
+ return `
33
+ <div class="border border-outline-variant/10 bg-surface-container px-4 py-4">
34
+ <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/75">
35
+ ${escapeHtml(title)}
36
+ </div>
37
+ <pre class="custom-scrollbar mt-3 max-h-[18rem] overflow-auto whitespace-pre-wrap break-words border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 font-mono text-xs leading-6 text-on-surface">${escapeHtml(
38
+ prettyJson
39
+ )}</pre>
40
+ </div>
41
+ `;
42
+ }
43
+
3
44
  function renderReadonlyField(label, value) {
45
+ const jsonPreview = getJsonPreview(value);
46
+
4
47
  return `
5
48
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
6
49
  <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
7
50
  ${escapeHtml(label)}
8
51
  </div>
9
- <div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>
52
+ ${
53
+ jsonPreview
54
+ ? `<div class="mt-2">${renderJsonViewer(jsonPreview)}</div>`
55
+ : `<div class="mt-2 text-sm text-on-surface">${escapeHtml(value)}</div>`
56
+ }
10
57
  </div>
11
58
  `;
12
59
  }
13
60
 
61
+ function renderEditableField(field) {
62
+ const jsonPreview = getJsonPreview(field.value);
63
+
64
+ return `
65
+ <label class="block space-y-2">
66
+ <span class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
67
+ ${escapeHtml(field.label ?? field.name)}
68
+ </span>
69
+ ${
70
+ jsonPreview
71
+ ? renderJsonViewer(jsonPreview, "JSON Preview")
72
+ : ""
73
+ }
74
+ <textarea
75
+ class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container ${
76
+ jsonPreview ? "min-h-[14rem] font-mono leading-6" : "min-h-[56px]"
77
+ }"
78
+ name="field:${escapeHtml(field.name)}"
79
+ spellcheck="false"
80
+ >${escapeHtml(field.value ?? "")}</textarea>
81
+ </label>
82
+ `;
83
+ }
84
+
14
85
  export function renderRowEditorPanel({
15
86
  title,
16
87
  sectionLabel = "Row Editor",
@@ -137,19 +208,7 @@ export function renderRowEditorPanel({
137
208
  ? `
138
209
  <div class="space-y-4">
139
210
  ${editableFields
140
- .map(
141
- (field) => `
142
- <label class="block space-y-2">
143
- <span class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
144
- ${escapeHtml(field.label ?? field.name)}
145
- </span>
146
- <textarea
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"
148
- name="field:${escapeHtml(field.name)}"
149
- >${escapeHtml(field.value ?? "")}</textarea>
150
- </label>
151
- `
152
- )
211
+ .map((field) => renderEditableField(field))
153
212
  .join("")}
154
213
  </div>
155
214
  `
@@ -7,6 +7,7 @@ const sidebarItems = [
7
7
  { label: "Data", href: "#/data", key: "data", icon: "table_rows" },
8
8
  { label: "SQL Editor", href: "#/editor", key: "editor", icon: "terminal" },
9
9
  { label: "Structure", href: "#/structure", key: "structure", icon: "account_tree" },
10
+ { label: "Table Designer", href: "#/table-designer", key: "tableDesigner", icon: "table_chart" },
10
11
  { label: "Settings", href: "#/settings", key: "settings", icon: "settings" },
11
12
  ];
12
13
 
@@ -0,0 +1,356 @@
1
+ import { escapeHtml } from "../utils/format.js";
2
+
3
+ function renderWarningList(items = [], className, title, tone = "alert") {
4
+ if (!items.length) {
5
+ return "";
6
+ }
7
+
8
+ return `
9
+ <section class="${className}">
10
+ <div class="table-designer-banner__header">
11
+ <div class="table-designer-banner__title">${escapeHtml(title)}</div>
12
+ <div class="status-badge status-badge--${tone}">
13
+ ${escapeHtml(String(items.length))}
14
+ </div>
15
+ </div>
16
+ <div class="table-designer-banner__list">
17
+ ${items
18
+ .map(
19
+ (item) => `
20
+ <div class="table-designer-banner__item">
21
+ <div class="table-designer-banner__item-title">${escapeHtml(
22
+ item.title || item
23
+ )}</div>
24
+ ${
25
+ item.message
26
+ ? `<div class="table-designer-banner__item-text">${escapeHtml(item.message)}</div>`
27
+ : ""
28
+ }
29
+ </div>
30
+ `
31
+ )
32
+ .join("")}
33
+ </div>
34
+ </section>
35
+ `;
36
+ }
37
+
38
+ function getReferencedColumns(draft, catalogTables, referencesTable) {
39
+ const normalizedReference = String(referencesTable ?? "").trim().toLowerCase();
40
+ const normalizedDraftTable = String(draft.tableName ?? "").trim().toLowerCase();
41
+
42
+ if (!normalizedReference) {
43
+ return [];
44
+ }
45
+
46
+ if (normalizedReference === normalizedDraftTable) {
47
+ return draft.columns
48
+ .filter((column) => !column.deleted)
49
+ .map((column) => column.name)
50
+ .filter(Boolean);
51
+ }
52
+
53
+ return (
54
+ catalogTables.find((table) => table.name.toLowerCase() === normalizedReference)?.columns ?? []
55
+ );
56
+ }
57
+
58
+ function renderColumnRow(column, draft, catalogTables) {
59
+ const referenceColumns = getReferencedColumns(draft, catalogTables, column.referencesTable);
60
+
61
+ return `
62
+ <div class="table-designer-grid__row">
63
+ <input
64
+ class="table-designer-field"
65
+ data-bind="table-designer-column-field"
66
+ data-column-id="${escapeHtml(column.id)}"
67
+ data-field="name"
68
+ placeholder="column_name"
69
+ spellcheck="false"
70
+ type="text"
71
+ value="${escapeHtml(column.name)}"
72
+ />
73
+ <select
74
+ class="table-designer-field"
75
+ data-bind="table-designer-column-field"
76
+ data-column-id="${escapeHtml(column.id)}"
77
+ data-field="type"
78
+ >
79
+ ${draft.supportedTypes
80
+ .map(
81
+ (type) => `
82
+ <option value="${escapeHtml(type)}" ${type === column.type ? "selected" : ""}>
83
+ ${escapeHtml(type)}
84
+ </option>
85
+ `
86
+ )
87
+ .join("")}
88
+ </select>
89
+ <label class="table-designer-check">
90
+ <input
91
+ data-bind="table-designer-column-flag"
92
+ data-column-id="${escapeHtml(column.id)}"
93
+ data-field="notNull"
94
+ type="checkbox"
95
+ ${column.notNull ? "checked" : ""}
96
+ />
97
+ <span>Not null</span>
98
+ </label>
99
+ <label class="table-designer-check">
100
+ <input
101
+ data-bind="table-designer-column-flag"
102
+ data-column-id="${escapeHtml(column.id)}"
103
+ data-field="unique"
104
+ type="checkbox"
105
+ ${column.unique ? "checked" : ""}
106
+ />
107
+ <span>Unique</span>
108
+ </label>
109
+ <label class="table-designer-check">
110
+ <input
111
+ data-bind="table-designer-column-flag"
112
+ data-column-id="${escapeHtml(column.id)}"
113
+ data-field="primaryKey"
114
+ type="checkbox"
115
+ ${column.primaryKey ? "checked" : ""}
116
+ />
117
+ <span>PK</span>
118
+ </label>
119
+ <input
120
+ class="table-designer-field"
121
+ data-bind="table-designer-column-field"
122
+ data-column-id="${escapeHtml(column.id)}"
123
+ data-field="defaultValue"
124
+ placeholder="SQL default"
125
+ spellcheck="false"
126
+ type="text"
127
+ value="${escapeHtml(column.defaultValue)}"
128
+ />
129
+ <select
130
+ class="table-designer-field"
131
+ data-bind="table-designer-column-field"
132
+ data-column-id="${escapeHtml(column.id)}"
133
+ data-field="referencesTable"
134
+ >
135
+ <option value="">No FK table</option>
136
+ ${catalogTables
137
+ .map(
138
+ (table) => `
139
+ <option
140
+ value="${escapeHtml(table.name)}"
141
+ ${table.name === column.referencesTable ? "selected" : ""}
142
+ >
143
+ ${escapeHtml(table.name)}
144
+ </option>
145
+ `
146
+ )
147
+ .join("")}
148
+ </select>
149
+ <select
150
+ class="table-designer-field"
151
+ data-bind="table-designer-column-field"
152
+ data-column-id="${escapeHtml(column.id)}"
153
+ data-field="referencesColumn"
154
+ >
155
+ <option value="">No FK column</option>
156
+ ${referenceColumns
157
+ .map(
158
+ (name) => `
159
+ <option value="${escapeHtml(name)}" ${name === column.referencesColumn ? "selected" : ""}>
160
+ ${escapeHtml(name)}
161
+ </option>
162
+ `
163
+ )
164
+ .join("")}
165
+ </select>
166
+ <button
167
+ class="table-designer-delete"
168
+ data-action="remove-table-designer-column"
169
+ data-column-id="${escapeHtml(column.id)}"
170
+ type="button"
171
+ >
172
+ <span class="material-symbols-outlined text-base">delete</span>
173
+ </button>
174
+ </div>
175
+ `;
176
+ }
177
+
178
+ function renderColumnGrid(draft, catalogTables) {
179
+ const visibleColumns = draft.columns.filter((column) => !column.deleted);
180
+
181
+ return `
182
+ <div class="table-designer-grid custom-scrollbar">
183
+ <div class="table-designer-grid__header">
184
+ <div>Name</div>
185
+ <div>Type</div>
186
+ <div>Not Null</div>
187
+ <div>Unique</div>
188
+ <div>Primary Key</div>
189
+ <div>Default</div>
190
+ <div>FK Table</div>
191
+ <div>FK Column</div>
192
+ <div></div>
193
+ </div>
194
+ ${visibleColumns.map((column) => renderColumnRow(column, draft, catalogTables)).join("")}
195
+ </div>
196
+ `;
197
+ }
198
+
199
+ function renderFillToggle(draft) {
200
+ if (draft.mode !== "create") {
201
+ return "";
202
+ }
203
+
204
+ const hasImportedRows = (draft.importedCsvRows?.length ?? 0) > 0;
205
+
206
+ return `
207
+ <label class="table-designer-fill-toggle ${hasImportedRows ? "" : "is-disabled"}">
208
+ <input
209
+ data-bind="table-designer-field"
210
+ data-field="fillImportedRows"
211
+ type="checkbox"
212
+ ${draft.fillImportedRows ? "checked" : ""}
213
+ ${hasImportedRows ? "" : "disabled"}
214
+ />
215
+ <span>Fill</span>
216
+ <span class="table-designer-fill-toggle__meta">
217
+ ${
218
+ hasImportedRows
219
+ ? `${escapeHtml(String(draft.importedCsvRows.length))} imported row${
220
+ draft.importedCsvRows.length === 1 ? "" : "s"
221
+ }`
222
+ : "Available after CSV import"
223
+ }
224
+ </span>
225
+ </label>
226
+ `;
227
+ }
228
+
229
+ export function renderTableDesignerEditor(state) {
230
+ const draft = state.tableDesigner.draft;
231
+
232
+ if (state.tableDesigner.loading || state.tableDesigner.detailLoading) {
233
+ return `
234
+ <section class="table-designer-main__empty shell-section">
235
+ <span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
236
+ <div class="table-designer-main__empty-title">Loading Schema Draft</div>
237
+ <div class="table-designer-main__empty-text">
238
+ Reading the current SQLite schema and preparing the editable draft.
239
+ </div>
240
+ </section>
241
+ `;
242
+ }
243
+
244
+ if (!draft) {
245
+ return `
246
+ <section class="table-designer-main__empty shell-section">
247
+ <div class="table-designer-main__empty-title">No Table Selected</div>
248
+ <div class="table-designer-main__empty-text">
249
+ Open an existing table from the left or start a new draft with <strong>+ New Table</strong>.
250
+ </div>
251
+ </section>
252
+ `;
253
+ }
254
+
255
+ const validationItems = (draft.validationErrors ?? []).map((message) => ({
256
+ title: message,
257
+ }));
258
+ const warningItems = draft.warnings ?? [];
259
+ const catalogTables = state.tableDesigner.tables ?? [];
260
+ const visibleColumns = draft.columns.filter((column) => !column.deleted);
261
+ const saveLabel =
262
+ draft.mode === "create"
263
+ ? state.tableDesigner.saving
264
+ ? "Creating..."
265
+ : "Create Table"
266
+ : state.tableDesigner.saving
267
+ ? "Saving..."
268
+ : "Save Changes";
269
+
270
+ return `
271
+ <section class="table-designer-main shell-section">
272
+ <header class="table-designer-main__header">
273
+ <div>
274
+ <div class="table-designer-main__eyebrow">
275
+ ${draft.mode === "create" ? "New Table Draft" : "Editing Existing Table"}
276
+ </div>
277
+ <div class="table-designer-main__title-row">
278
+ <input
279
+ class="table-designer-main__name"
280
+ data-bind="table-designer-field"
281
+ data-field="tableName"
282
+ placeholder="table_name"
283
+ spellcheck="false"
284
+ type="text"
285
+ value="${escapeHtml(draft.tableName)}"
286
+ />
287
+ ${
288
+ draft.mode === "edit"
289
+ ? `<div class="status-badge status-badge--muted">FROM ${escapeHtml(
290
+ draft.originalTableName
291
+ )}</div>`
292
+ : `<div class="status-badge status-badge--primary">CREATE</div>`
293
+ }
294
+ </div>
295
+ <div class="table-designer-main__subtitle">
296
+ ${escapeHtml(String(visibleColumns.length))} visible column${
297
+ visibleColumns.length === 1 ? "" : "s"
298
+ } // SQLite-safe operations only
299
+ </div>
300
+ </div>
301
+ <div class="table-designer-main__actions">
302
+ ${renderFillToggle(draft)}
303
+ <button
304
+ class="table-designer-main__secondary"
305
+ data-action="refresh-view"
306
+ type="button"
307
+ >
308
+ Reload Schema
309
+ </button>
310
+ <button
311
+ class="table-designer-main__primary ${draft.canSave ? "" : "is-disabled"}"
312
+ data-action="save-table-designer"
313
+ ${draft.canSave ? "" : "disabled"}
314
+ type="button"
315
+ >
316
+ ${escapeHtml(saveLabel)}
317
+ </button>
318
+ </div>
319
+ </header>
320
+
321
+ ${
322
+ state.tableDesigner.saveError
323
+ ? `
324
+ <div class="table-designer-main__error">
325
+ <div class="table-designer-main__error-code">${escapeHtml(
326
+ state.tableDesigner.saveError.code
327
+ )}</div>
328
+ <div class="table-designer-main__error-text">${escapeHtml(
329
+ state.tableDesigner.saveError.message
330
+ )}</div>
331
+ </div>
332
+ `
333
+ : ""
334
+ }
335
+
336
+ ${renderWarningList(validationItems, "table-designer-banner is-validation", "Validation", "alert")}
337
+ ${renderWarningList(warningItems, "table-designer-banner is-warning", "Warnings", "alert")}
338
+
339
+ <section class="table-designer-main__section">
340
+ <div class="table-designer-main__section-header">
341
+ <div>
342
+ <div class="table-designer-main__section-title">Columns</div>
343
+ </div>
344
+ <button
345
+ class="table-designer-main__secondary"
346
+ data-action="add-table-designer-column"
347
+ type="button"
348
+ >
349
+ + Add Column
350
+ </button>
351
+ </div>
352
+ ${renderColumnGrid(draft, catalogTables)}
353
+ </section>
354
+ </section>
355
+ `;
356
+ }