sqlite-hub 2.0.1 → 2.1.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 (85) hide show
  1. package/README.md +14 -9
  2. package/docs/MCP.md +8 -0
  3. package/docs/changelog.md +10 -0
  4. package/docs/guidelines/DESIGN.md +64 -0
  5. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  6. package/frontend/assets/mockups/backups_2_compare_drawer_1920.webp +0 -0
  7. package/frontend/assets/mockups/backups_3_create_backup_modal_1920.webp +0 -0
  8. package/frontend/assets/mockups/backups_4_edit_backup_modal_1920.webp +0 -0
  9. package/frontend/assets/mockups/backups_5_restore_backup_modal_1920.webp +0 -0
  10. package/frontend/assets/mockups/backups_6_delete_backup_modal_1920.webp +0 -0
  11. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  12. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  13. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  21. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  22. package/frontend/assets/mockups/data_3_generate_data_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/data_4_data_export_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/logs_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  29. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  37. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  40. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  41. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  42. package/frontend/js/api.js +11 -0
  43. package/frontend/js/app.js +238 -8
  44. package/frontend/js/components/badges.js +16 -0
  45. package/frontend/js/components/connectionCard.js +35 -0
  46. package/frontend/js/components/modal.js +116 -245
  47. package/frontend/js/components/queryHistoryDetail.js +25 -45
  48. package/frontend/js/components/rowEditorPanel.js +7 -60
  49. package/frontend/js/components/structureGraph.js +292 -13
  50. package/frontend/js/components/tableDesignerConstraintsDrawer.js +400 -0
  51. package/frontend/js/components/tableDesignerEditor.js +51 -16
  52. package/frontend/js/components/tableDesignerSidebar.js +9 -5
  53. package/frontend/js/store.js +551 -7
  54. package/frontend/js/utils/connectionRegistry.js +76 -0
  55. package/frontend/js/utils/tableDesigner.js +431 -62
  56. package/frontend/js/views/backups.js +2 -5
  57. package/frontend/js/views/charts.js +20 -10
  58. package/frontend/js/views/connections.js +103 -3
  59. package/frontend/js/views/data.js +19 -5
  60. package/frontend/js/views/structure.js +28 -5
  61. package/frontend/js/views/tableAdvisor.js +14 -6
  62. package/frontend/js/views/tableDesigner.js +44 -9
  63. package/frontend/styles/components.css +676 -79
  64. package/frontend/styles/structure-graph.css +49 -0
  65. package/frontend/styles/tailwind.generated.css +1 -1
  66. package/package.json +1 -1
  67. package/server/routes/connections.js +12 -0
  68. package/server/routes/data.js +1 -1
  69. package/server/routes/tableDesigner.js +15 -1
  70. package/server/services/mcpToolService.js +28 -0
  71. package/server/services/sqlite/connectionManager.js +23 -2
  72. package/server/services/sqlite/dataBrowserService.js +21 -0
  73. package/server/services/sqlite/introspection.js +39 -1
  74. package/server/services/sqlite/structureService.js +66 -11
  75. package/server/services/sqlite/tableAdvisor.js +6 -0
  76. package/server/services/sqlite/tableDesigner/schemaMapping.js +7 -1
  77. package/server/services/sqlite/tableDesigner/sql.js +31 -2
  78. package/server/services/sqlite/tableDesigner/validation.js +69 -9
  79. package/server/services/sqlite/tableDesignerService.js +110 -5
  80. package/server/services/storage/appStateStore.js +303 -4
  81. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  82. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  83. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  84. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  85. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
@@ -687,14 +687,11 @@ export function renderBackupDiffDrawer(state) {
687
687
  <div class="flex items-center justify-between gap-3">
688
688
  <div class="min-w-0">
689
689
  <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container/70">
690
- Backup Compare
690
+ Backup Compare // ${escapeHtml(currentLabel)}
691
691
  </div>
692
692
  <h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">
693
693
  ${escapeHtml(title)}
694
694
  </h2>
695
- <div class="mt-1 truncate text-xs text-on-surface-variant/60">
696
- Current // ${escapeHtml(currentLabel)}
697
- </div>
698
695
  </div>
699
696
  <button
700
697
  class="query-history-icon-button"
@@ -708,7 +705,7 @@ export function renderBackupDiffDrawer(state) {
708
705
  <div class="mt-4 flex flex-wrap gap-2">
709
706
  ${renderStatusBadge('read only', 'muted')}
710
707
  ${diffState.loading ? renderStatusBadge('loading', 'muted') : ''}
711
- ${diffState.error ? renderStatusBadge('error', 'alert') : ''}
708
+ ${diffState.error ? renderStatusBadge('error', 'muted') : ''}
712
709
  </div>
713
710
  </div>
714
711
  ${renderBackupDiffDrawerBody(diffState)}
@@ -339,9 +339,14 @@ export function renderChartsDetailDrawer(state) {
339
339
  <section class="flex h-full min-h-0 flex-col bg-surface-low">
340
340
  <div class="border-b border-outline-variant/10 px-5 py-4">
341
341
  <div class="flex items-center justify-between gap-3">
342
- <span class="font-body text-sm font-black uppercase tracking-[0.18em] text-primary-container">
343
- Chart Detail
344
- </span>
342
+ <div class="min-w-0">
343
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container/70">
344
+ Charts // Query Detail
345
+ </div>
346
+ <h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">
347
+ Loading
348
+ </h2>
349
+ </div>
345
350
  <button class="query-history-icon-button" data-action="close-charts-query-detail" type="button">
346
351
  <span class="material-symbols-outlined text-[18px]">close</span>
347
352
  </button>
@@ -359,9 +364,14 @@ export function renderChartsDetailDrawer(state) {
359
364
  <section class="flex h-full min-h-0 flex-col bg-surface-low">
360
365
  <div class="border-b border-outline-variant/10 px-5 py-4">
361
366
  <div class="flex items-center justify-between gap-3">
362
- <span class="font-body text-sm font-black uppercase tracking-[0.18em] text-primary-container">
363
- Chart Detail
364
- </span>
367
+ <div class="min-w-0">
368
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container/70">
369
+ Charts // Query Detail
370
+ </div>
371
+ <h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">
372
+ Error
373
+ </h2>
374
+ </div>
365
375
  <button class="query-history-icon-button" data-action="close-charts-query-detail" type="button">
366
376
  <span class="material-symbols-outlined text-[18px]">close</span>
367
377
  </button>
@@ -383,7 +393,7 @@ export function renderChartsDetailDrawer(state) {
383
393
  const charts = detail.charts ?? [];
384
394
  const result = state.charts.result;
385
395
  const chartTypeBadges = charts.length
386
- ? charts.map(chart => renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary')).join('')
396
+ ? charts.map(chart => renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'muted')).join('')
387
397
  : renderStatusBadge('no charts', 'muted');
388
398
  const savedBadgeMarkup = `
389
399
  <span
@@ -391,12 +401,12 @@ export function renderChartsDetailDrawer(state) {
391
401
  data-history-id="${escapeHtml(detail.item.id)}"
392
402
  ${detail.item.isSaved ? '' : 'hidden'}
393
403
  >
394
- ${renderStatusBadge('saved', 'primary')}
404
+ ${renderStatusBadge('saved', 'muted')}
395
405
  </span>
396
406
  `;
397
407
  const statusMarkup = [
398
408
  savedBadgeMarkup,
399
- state.charts.resultError ? renderStatusBadge('result error', 'alert') : '',
409
+ state.charts.resultError ? renderStatusBadge('result error', 'muted') : '',
400
410
  state.charts.resultLoading ? renderStatusBadge('loading result', 'muted') : '',
401
411
  chartTypeBadges,
402
412
  ].join('');
@@ -413,7 +423,7 @@ export function renderChartsDetailDrawer(state) {
413
423
  <div class="flex items-center justify-between gap-3">
414
424
  <div class="min-w-0">
415
425
  <div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">
416
- Chart Detail
426
+ Charts // Query Detail
417
427
  </div>
418
428
  <h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">
419
429
  ${escapeHtml(detail.item.displayTitle)}
@@ -1,6 +1,10 @@
1
1
  import { renderConnectionCard } from '../components/connectionCard.js';
2
2
  import { renderPageHeader } from '../components/pageHeader.js';
3
3
  import { escapeHtml } from '../utils/format.js';
4
+ import {
5
+ filterConnections,
6
+ getConnectionTagCounts,
7
+ } from '../utils/connectionRegistry.js';
4
8
 
5
9
  function renderConnectionsActionButton({
6
10
  label,
@@ -28,7 +32,88 @@ function renderConnectionsActionButton({
28
32
  `;
29
33
  }
30
34
 
31
- function renderConnectionsBody(state) {
35
+ function renderConnectionsToolbar(state, visibleCount) {
36
+ if (!state.connections.recent.length) {
37
+ return '';
38
+ }
39
+
40
+ const searchQuery = state.connections.searchQuery ?? '';
41
+ const selectedTagIds = new Set((state.connections.selectedTagIds ?? []).map(tagId => String(tagId)));
42
+ const selectedTags = (state.connections.tags ?? []).filter(tag => selectedTagIds.has(String(tag.id)));
43
+ const tagCounts = getConnectionTagCounts(state.connections.recent);
44
+ const filterLabel =
45
+ selectedTags.length === 0
46
+ ? 'TAGS: ALL'
47
+ : selectedTags.length === 1
48
+ ? `TAGS: ${selectedTags[0].name}`
49
+ : `TAGS: ${selectedTags.length}`;
50
+ const tagRows = (state.connections.tags ?? [])
51
+ .map(tag => {
52
+ const tagId = String(tag.id);
53
+ const isSelected = selectedTagIds.has(tagId);
54
+
55
+ return `
56
+ <label class="connection-tag-filter-row">
57
+ <input
58
+ ${isSelected ? 'checked' : ''}
59
+ data-bind="connection-tag-filter"
60
+ data-tag-id="${escapeHtml(tagId)}"
61
+ type="checkbox"
62
+ />
63
+ <span class="connection-tag-filter-row__name">${escapeHtml(tag.name)}</span>
64
+ <span class="connection-tag-filter-row__count">${escapeHtml(String(tagCounts.get(tagId) ?? tag.connectionCount ?? 0))}</span>
65
+ </label>
66
+ `;
67
+ })
68
+ .join('');
69
+
70
+ return `
71
+ <div class="connection-registry-toolbar" data-connection-registry-toolbar>
72
+ <label class="connection-registry-search">
73
+ <span class="sr-only">Search connections</span>
74
+ <span class="material-symbols-outlined connection-registry-search__icon" aria-hidden="true">search</span>
75
+ <input
76
+ autocomplete="off"
77
+ class="connection-registry-search__input"
78
+ data-bind="connections-search"
79
+ data-connections-search-input
80
+ placeholder="SEARCH_CONNECTIONS..."
81
+ spellcheck="false"
82
+ type="search"
83
+ value="${escapeHtml(searchQuery)}"
84
+ />
85
+ <span class="connection-registry-search__shortcut" aria-hidden="true">/</span>
86
+ </label>
87
+ <details class="dropdown-button dropdown-button--align-right connection-tag-filter" data-dropdown-button>
88
+ <summary class="standard-button dropdown-button__toggle connection-tag-filter__toggle" title="${escapeHtml(filterLabel)}">
89
+ <span class="connection-tag-filter__toggle-label">${escapeHtml(filterLabel)}</span>
90
+ <span class="material-symbols-outlined dropdown-button__chevron" aria-hidden="true">expand_more</span>
91
+ </summary>
92
+ <div class="dropdown-button__panel connection-tag-filter__panel">
93
+ <div class="connection-tag-filter__header">
94
+ <span>FILTER_BY_TAG</span>
95
+ <span>${escapeHtml(String(visibleCount))}</span>
96
+ </div>
97
+ ${
98
+ tagRows
99
+ ? `<div class="connection-tag-filter__list custom-scrollbar">${tagRows}</div>`
100
+ : '<div class="connection-tag-filter__empty">NO_TAGS_AVAILABLE</div>'
101
+ }
102
+ <button
103
+ class="standard-button connection-tag-filter__reset"
104
+ data-action="clear-connection-tag-filters"
105
+ type="button"
106
+ ${selectedTagIds.size ? '' : 'disabled'}
107
+ >
108
+ RESET_FILTER
109
+ </button>
110
+ </div>
111
+ </details>
112
+ </div>
113
+ `;
114
+ }
115
+
116
+ function renderConnectionsBody(state, visibleConnections) {
32
117
  if (state.connections.loading && !state.connections.recent.length) {
33
118
  return `
34
119
  <div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
@@ -78,9 +163,19 @@ function renderConnectionsBody(state) {
78
163
  `;
79
164
  }
80
165
 
166
+ if (!visibleConnections.length) {
167
+ return `
168
+ <div class="connection-filter-empty">
169
+ <span class="material-symbols-outlined connection-filter-empty__icon">search_off</span>
170
+ <p class="connection-filter-empty__title">NO_CONNECTIONS_FOUND</p>
171
+ <p class="connection-filter-empty__hint">TRY_ANOTHER_SEARCH_OR_FILTER</p>
172
+ </div>
173
+ `;
174
+ }
175
+
81
176
  return `
82
177
  <div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
83
- ${state.connections.recent
178
+ ${visibleConnections
84
179
  .map(connection => renderConnectionCard(connection, state.connections.active?.id))
85
180
  .join('')}
86
181
  </div>
@@ -88,6 +183,10 @@ function renderConnectionsBody(state) {
88
183
  }
89
184
 
90
185
  export function renderConnectionsView(state) {
186
+ const visibleConnections = filterConnections(state.connections.recent, {
187
+ searchQuery: state.connections.searchQuery,
188
+ selectedTagIds: state.connections.selectedTagIds,
189
+ });
91
190
  const actions = `
92
191
  ${
93
192
  state.connections.active
@@ -122,7 +221,8 @@ export function renderConnectionsView(state) {
122
221
  subtitle: `Registry // Recent_Targets: ${String(state.connections.recent.length).padStart(2, '0')}`,
123
222
  actions,
124
223
  })}
125
- ${renderConnectionsBody(state)}
224
+ ${renderConnectionsToolbar(state, visibleConnections.length)}
225
+ ${renderConnectionsBody(state, visibleConnections)}
126
226
  </div>
127
227
  </section>
128
228
  `,
@@ -1,5 +1,6 @@
1
1
  import { renderDataGrid } from '../components/dataGrid.js';
2
2
  import { renderRowEditorPanel } from '../components/rowEditorPanel.js';
3
+ import { renderVirtualTableBadge } from '../components/badges.js';
3
4
  import { renderWorkspaceOpenDropdown } from '../components/workspaceOpenDropdown.js';
4
5
  import { escapeHtml, formatCellValue, formatNumber, isBlobPreview, truncateMiddle } from '../utils/format.js';
5
6
  import { detectEmailValue } from '../utils/emailPreview.js';
@@ -94,8 +95,13 @@ function renderTableList(state) {
94
95
  data-to="/data/${encodeURIComponent(table.name)}"
95
96
  type="button"
96
97
  >
97
- <div class="table-designer-sidebar__item-name ${table.name === activeName ? 'is-active' : ''}">
98
- ${escapeHtml(table.name)}
98
+ <div class="flex min-w-0 items-center gap-2">
99
+ <div class="table-designer-sidebar__item-name min-w-0 flex-1 ${
100
+ table.name === activeName ? 'is-active' : ''
101
+ }">
102
+ ${escapeHtml(table.name)}
103
+ </div>
104
+ ${renderVirtualTableBadge(table)}
99
105
  </div>
100
106
  <div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
101
107
  ${escapeHtml(formatNumber(table.columnCount ?? 0))} column${
@@ -113,6 +119,12 @@ function renderTableList(state) {
113
119
  function renderWorkspaceHeader(state) {
114
120
  const table = state.dataBrowser.table;
115
121
  const tablesVisible = state.dataBrowser.tablesVisible !== false;
122
+ const generateDisabled = !table || table.isShadow;
123
+ const generateTitle = !table
124
+ ? 'Select a table before generating rows'
125
+ : table.isShadow
126
+ ? 'Shadow tables are read-only in Data'
127
+ : 'Generate synthetic test rows for this table';
116
128
 
117
129
  return `
118
130
  <header class="workspace-header">
@@ -140,9 +152,9 @@ function renderWorkspaceHeader(state) {
140
152
  <button
141
153
  class="standard-button"
142
154
  data-action="open-generate-data-modal"
143
- title="${table ? 'Generate synthetic test rows for this table' : 'Select a table before generating rows'}"
155
+ title="${escapeHtml(generateTitle)}"
144
156
  type="button"
145
- ${table ? '' : 'disabled aria-disabled="true"'}
157
+ ${generateDisabled ? 'disabled aria-disabled="true"' : ''}
146
158
  >
147
159
  <span class="material-symbols-outlined text-sm">auto_awesome</span>
148
160
  Generate
@@ -628,6 +640,8 @@ export function renderDataRowEditorPanel(state) {
628
640
  : [{ name: 'rowIdentity', value: JSON.stringify(row.__identity ?? null) }],
629
641
  disabledMessage: state.connections.active?.readOnly
630
642
  ? 'The active database is opened read-only, so row editing is disabled.'
643
+ : table.isShadow
644
+ ? 'Shadow tables are read-only in Data.'
631
645
  : table.notSafelyUpdatable
632
646
  ? 'This table has no stable identity column, so SQLite Hub cannot safely update rows.'
633
647
  : '',
@@ -663,7 +677,7 @@ export function renderDataRowEditorPanel(state) {
663
677
  deleting: state.dataBrowser.deleting,
664
678
  deleteAction: 'delete-data-row',
665
679
  deleteRowIndex: isIndexedRow ? rowIndex : null,
666
- deleteEnabled: Boolean(row.__identity),
680
+ deleteEnabled: Boolean(row.__identity) && !table.isShadow,
667
681
  reloadAction: 'reload-data-route',
668
682
  jsonActionsEnabled: true,
669
683
  });
@@ -1,4 +1,5 @@
1
1
  import { clearInspector, renderDdlSection, renderInspector } from '../components/structureGraph.js';
2
+ import { renderVirtualTableBadge } from '../components/badges.js';
2
3
  import { renderDropdownButton } from '../components/dropdownButton.js';
3
4
  import { renderGenerateTypesDropdown } from '../components/generateTypesDropdown.js';
4
5
  import { renderWorkspaceOpenDropdown } from '../components/workspaceOpenDropdown.js';
@@ -36,11 +37,14 @@ function renderEntryGroup(title, entries, activeName, options = {}) {
36
37
  '" ',
37
38
  entryAttributes,
38
39
  ' type="button">',
39
- '<div class="font-mono text-xs ',
40
+ '<div class="flex min-w-0 items-center gap-2">',
41
+ '<div class="min-w-0 flex-1 truncate font-mono text-xs ',
40
42
  isActive ? 'text-primary-container' : 'text-on-surface',
41
43
  '">',
42
44
  escapeHtml(entry.name),
43
45
  '</div>',
46
+ renderVirtualTableBadge(entry),
47
+ '</div>',
44
48
  metaMarkup,
45
49
  '</button>',
46
50
  ].join('');
@@ -76,6 +80,10 @@ function getFilteredTables(tables, searchQuery) {
76
80
  return tables.filter(table => table.name.toLowerCase().includes(normalizedSearch));
77
81
  }
78
82
 
83
+ function isShadowTable(table) {
84
+ return Boolean(table?.isShadow);
85
+ }
86
+
79
87
  function renderStructureTableSearch(state) {
80
88
  return `
81
89
  <label class="table-designer-sidebar__search">
@@ -129,7 +137,10 @@ function renderObjectInspector(detail) {
129
137
  <span class="material-symbols-outlined text-[18px]">close</span>
130
138
  </button>
131
139
  </div>
132
- <div class="structure-graph__title">${escapeHtml(detail.name)}</div>
140
+ <div class="flex min-w-0 flex-wrap items-center gap-2">
141
+ <div class="structure-graph__title min-w-0">${escapeHtml(detail.name)}</div>
142
+ ${renderVirtualTableBadge(detail)}
143
+ </div>
133
144
  ${
134
145
  detail.tableName
135
146
  ? `
@@ -336,9 +347,10 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
336
347
 
337
348
  export function renderStructureView(state) {
338
349
  const structure = state.structure.data;
339
- const filteredTables = structure
340
- ? getFilteredTables(structure.grouped.tables ?? [], state.structure.tableSearchQuery)
341
- : [];
350
+ const regularTables = (structure?.grouped.tables ?? []).filter(table => !isShadowTable(table));
351
+ const shadowTables = (structure?.grouped.tables ?? []).filter(isShadowTable);
352
+ const filteredTables = structure ? getFilteredTables(regularTables, state.structure.tableSearchQuery) : [];
353
+ const filteredShadowTables = structure ? getFilteredTables(shadowTables, state.structure.tableSearchQuery) : [];
342
354
  const detail = state.structure.detail?.name === state.structure.selectedName ? state.structure.detail : null;
343
355
  const tablesVisible = state.structure.tablesVisible !== false;
344
356
 
@@ -376,6 +388,17 @@ export function renderStructureView(state) {
376
388
  ? 'tables match the current search.'
377
389
  : null,
378
390
  })}
391
+ ${
392
+ shadowTables.length
393
+ ? renderEntryGroup('Shadow Tables', filteredShadowTables, state.structure.selectedName, {
394
+ compact: true,
395
+ showMeta: false,
396
+ emptyMessage: state.structure.tableSearchQuery
397
+ ? 'shadow tables match the current search.'
398
+ : null,
399
+ })
400
+ : ''
401
+ }
379
402
  ${renderEntryGroup('Views', structure.grouped.views, state.structure.selectedName)}
380
403
  ${renderEntryGroup('Indexes', structure.grouped.indexes, state.structure.selectedName, {
381
404
  compact: true,
@@ -5,6 +5,7 @@ import {
5
5
  highlightSql,
6
6
  truncateMiddle,
7
7
  } from '../utils/format.js';
8
+ import { renderVirtualTableBadge } from '../components/badges.js';
8
9
  import { renderWorkspaceOpenDropdown } from '../components/workspaceOpenDropdown.js';
9
10
 
10
11
  const CATEGORY_LABELS = {
@@ -73,7 +74,8 @@ function renderRiskBadge(risk = 'low') {
73
74
  }
74
75
 
75
76
  function renderTableList(state) {
76
- const { tables = [], selectedTableName } = state.tableAdvisor;
77
+ const { selectedTableName } = state.tableAdvisor;
78
+ const tables = (state.tableAdvisor.tables ?? []).filter(table => !table?.isShadow);
77
79
 
78
80
  if (state.tableAdvisor.loading && !tables.length) {
79
81
  return `
@@ -107,8 +109,11 @@ function renderTableList(state) {
107
109
  href="#/table-advisor/${encodeURIComponent(table.name)}"
108
110
  title="${escapeHtml(table.name)}"
109
111
  >
110
- <div class="table-designer-sidebar__item-name ${isActive ? 'is-active' : ''}">
111
- ${escapeHtml(table.name)}
112
+ <div class="flex min-w-0 items-center gap-2">
113
+ <div class="table-designer-sidebar__item-name min-w-0 flex-1 ${isActive ? 'is-active' : ''}">
114
+ ${escapeHtml(table.name)}
115
+ </div>
116
+ ${renderVirtualTableBadge(table)}
112
117
  </div>
113
118
  <div class="table-designer-sidebar__item-meta">
114
119
  ${escapeHtml(formatNumber(table.columnCount ?? 0))} columns
@@ -128,9 +133,12 @@ function renderSummary(result = {}) {
128
133
  <div class="grid grid-cols-1 gap-3 md:grid-cols-4">
129
134
  <div class="border border-outline-variant/10 bg-surface-container-low px-4 py-3">
130
135
  <div class="font-mono text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/55">Table</div>
131
- <div class="mt-2 min-w-0 truncate text-lg font-black text-on-surface" title="${escapeHtml(
132
- result.tableName ?? '',
133
- )}">${escapeHtml(result.tableName ?? 'Unknown')}</div>
136
+ <div class="mt-2 flex min-w-0 items-center gap-2">
137
+ <div class="min-w-0 truncate text-lg font-black text-on-surface" title="${escapeHtml(
138
+ result.tableName ?? '',
139
+ )}">${escapeHtml(result.tableName ?? 'Unknown')}</div>
140
+ ${renderVirtualTableBadge(result)}
141
+ </div>
134
142
  </div>
135
143
  <div class="border border-outline-variant/10 bg-surface-container-low px-4 py-3">
136
144
  <div class="font-mono text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/55">Score</div>
@@ -1,6 +1,8 @@
1
1
  import { renderTableDesignerEditor } from "../components/tableDesignerEditor.js";
2
+ import { renderTableDesignerConstraintsDrawer } from "../components/tableDesignerConstraintsDrawer.js";
2
3
  import { renderTableDesignerSidebar } from "../components/tableDesignerSidebar.js";
3
4
  import { renderTableDesignerSqlPreview } from "../components/tableDesignerSqlPreview.js";
5
+ import { renderDropdownButton } from "../components/dropdownButton.js";
4
6
  import { renderWorkspaceOpenDropdown } from "../components/workspaceOpenDropdown.js";
5
7
  import { escapeHtml } from "../utils/format.js";
6
8
 
@@ -17,6 +19,44 @@ function renderRouteError(error) {
17
19
  `;
18
20
  }
19
21
 
22
+ function renderImportDropdown() {
23
+ return renderDropdownButton({
24
+ align: "right",
25
+ icon: "upload_file",
26
+ label: "Import",
27
+ title: "Import data",
28
+ items: [
29
+ {
30
+ action: "import-table-designer-data",
31
+ dataAttributes: {
32
+ importFormat: "csv",
33
+ importAccept: ".csv,text/csv",
34
+ },
35
+ icon: "table_chart",
36
+ label: "CSV",
37
+ },
38
+ {
39
+ action: "import-table-designer-data",
40
+ dataAttributes: {
41
+ importFormat: "tsv",
42
+ importAccept: ".tsv,text/tab-separated-values",
43
+ },
44
+ icon: "table_rows",
45
+ label: "TSV",
46
+ },
47
+ {
48
+ action: "import-table-designer-data",
49
+ dataAttributes: {
50
+ importFormat: "json",
51
+ importAccept: ".json,application/json",
52
+ },
53
+ icon: "data_object",
54
+ label: "JSON",
55
+ },
56
+ ],
57
+ });
58
+ }
59
+
20
60
  function renderWorkspaceToolbar(state) {
21
61
  const tablesVisible = state.tableDesigner.tablesVisible !== false;
22
62
  const tableName = state.tableDesigner.selectedTableName ?? state.tableDesigner.draft?.tableName ?? "";
@@ -62,17 +102,12 @@ function renderWorkspaceToolbar(state) {
62
102
  },
63
103
  ],
64
104
  })}
65
- <button
66
- class="standard-button"
67
- data-action="import-table-designer-csv"
68
- type="button"
69
- >
70
- Import CSV
71
- </button>
105
+ ${renderImportDropdown()}
72
106
  <input
73
- accept=".csv,text/csv"
107
+ accept=".csv,text/csv,.tsv,text/tab-separated-values,.json,application/json"
74
108
  class="table-designer-workspace__file-input"
75
109
  data-bind="table-designer-import-file"
110
+ data-import-format="csv"
76
111
  type="file"
77
112
  />
78
113
  <button
@@ -110,6 +145,6 @@ export function renderTableDesignerView(state) {
110
145
  </div>
111
146
  </section>
112
147
  `,
113
- panel: "",
148
+ panel: renderTableDesignerConstraintsDrawer(state),
114
149
  };
115
150
  }