sqlite-hub 2.1.0 → 2.2.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.
@@ -2,6 +2,8 @@ import { escapeHtml } from './format.js';
2
2
 
3
3
  const FENCED_CODE_PATTERN = /^\s*(```|~~~)/;
4
4
  const TASK_LINE_PATTERN = /^(\s*)([-*+])\s+\[([ xX])\]\s+(.*?)(\r?)$/;
5
+ const SQLITE_HUB_MAGIC_COMMENT_PATTERN =
6
+ /^\s*<!--\s*\/?sqlite-hub:magic(?:\s+[^>]*)?-->\s*$/i;
5
7
 
6
8
  function decodeBasicHtmlEntities(value = '') {
7
9
  return String(value)
@@ -26,6 +28,15 @@ function sanitizeRenderedMarkdown(html = '') {
26
28
  });
27
29
  }
28
30
 
31
+ function openRenderedMarkdownLinksInNewTabs(html = '') {
32
+ return String(html).replace(/<a\b([^>]*)>/gi, (match, attributes = '') => {
33
+ const targetAttribute = /\starget\s*=/i.test(attributes) ? '' : ' target="_blank"';
34
+ const relAttribute = /\srel\s*=/i.test(attributes) ? '' : ' rel="noopener noreferrer"';
35
+
36
+ return `<a${attributes}${targetAttribute}${relAttribute}>`;
37
+ });
38
+ }
39
+
29
40
  function renderFallbackMarkdown(markdown = '') {
30
41
  const lines = String(markdown).split(/\r?\n/);
31
42
  const html = [];
@@ -108,13 +119,13 @@ function renderMarkdownBlock(markdown = '') {
108
119
  return renderFallbackMarkdown(markdown);
109
120
  }
110
121
 
111
- return sanitizeRenderedMarkdown(
122
+ return openRenderedMarkdownLinksInNewTabs(sanitizeRenderedMarkdown(
112
123
  parser(source, {
113
124
  async: false,
114
125
  breaks: false,
115
126
  gfm: true,
116
127
  }),
117
- );
128
+ ));
118
129
  }
119
130
 
120
131
  function renderMarkdownInline(markdown = '') {
@@ -127,13 +138,13 @@ function renderMarkdownInline(markdown = '') {
127
138
  return source;
128
139
  }
129
140
 
130
- return sanitizeRenderedMarkdown(
141
+ return openRenderedMarkdownLinksInNewTabs(sanitizeRenderedMarkdown(
131
142
  parser(source, {
132
143
  async: false,
133
144
  breaks: false,
134
145
  gfm: true,
135
146
  }),
136
- );
147
+ ));
137
148
  }
138
149
 
139
150
  function renderTaskList(items = []) {
@@ -193,6 +204,12 @@ export function renderMarkdownPreview(markdown = '') {
193
204
  };
194
205
 
195
206
  lines.forEach((line, lineIndex) => {
207
+ if (!inFence && SQLITE_HUB_MAGIC_COMMENT_PATTERN.test(line)) {
208
+ flushTasks();
209
+ flushMarkdown();
210
+ return;
211
+ }
212
+
196
213
  const fenceLine = FENCED_CODE_PATTERN.test(line);
197
214
 
198
215
  if (fenceLine) {
@@ -1,10 +1,7 @@
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
+ import { filterConnections, getConnectionTagCounts } from '../utils/connectionRegistry.js';
8
5
 
9
6
  function renderConnectionsActionButton({
10
7
  label,
@@ -175,9 +172,7 @@ function renderConnectionsBody(state, visibleConnections) {
175
172
 
176
173
  return `
177
174
  <div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
178
- ${visibleConnections
179
- .map(connection => renderConnectionCard(connection, state.connections.active?.id))
180
- .join('')}
175
+ ${visibleConnections.map(connection => renderConnectionCard(connection, state.connections.active?.id)).join('')}
181
176
  </div>
182
177
  `;
183
178
  }
@@ -191,7 +186,7 @@ export function renderConnectionsView(state) {
191
186
  ${
192
187
  state.connections.active
193
188
  ? renderConnectionsActionButton({
194
- label: state.connections.backupLoading ? 'Creating Backup...' : 'Create Backup',
189
+ label: state.connections.backupLoading ? 'Create Backup...' : 'CreateBackup',
195
190
  icon: 'inventory_2',
196
191
  action: 'create-backup',
197
192
  disabled: state.connections.backupLoading,
@@ -32,6 +32,33 @@ function getFilteredTables(tables, searchQuery) {
32
32
  return tables.filter(table => table.name.toLowerCase().includes(normalizedSearch));
33
33
  }
34
34
 
35
+ export function buildDataSidebarSignature(state) {
36
+ if (state?.route?.name !== 'data') {
37
+ return '';
38
+ }
39
+
40
+ const dataBrowser = state.dataBrowser ?? {};
41
+ const tablesVisible = dataBrowser.tablesVisible !== false;
42
+
43
+ if (!tablesVisible) {
44
+ return 'data-sidebar:hidden';
45
+ }
46
+
47
+ const tables = dataBrowser.tables ?? [];
48
+
49
+ return JSON.stringify({
50
+ loadingEmpty: Boolean(dataBrowser.loading && !tables.length),
51
+ tableSearchQuery: dataBrowser.tableSearchQuery ?? '',
52
+ tables: tables.map(table => [
53
+ table.name,
54
+ table.columnCount ?? 0,
55
+ Boolean(table.isVirtual),
56
+ Boolean(table.isShadow),
57
+ table.tableKind ?? '',
58
+ ]),
59
+ });
60
+ }
61
+
35
62
  function renderDataTableSearch(state) {
36
63
  return `
37
64
  <label class="table-designer-sidebar__search">
@@ -92,13 +119,15 @@ function renderTableList(state) {
92
119
  : 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
93
120
  }"
94
121
  data-action="navigate"
122
+ data-data-table-item
123
+ data-data-table-name="${escapeHtml(table.name)}"
95
124
  data-to="/data/${encodeURIComponent(table.name)}"
96
125
  type="button"
97
126
  >
98
127
  <div class="flex min-w-0 items-center gap-2">
99
128
  <div class="table-designer-sidebar__item-name min-w-0 flex-1 ${
100
129
  table.name === activeName ? 'is-active' : ''
101
- }">
130
+ }" data-data-table-name-label>
102
131
  ${escapeHtml(table.name)}
103
132
  </div>
104
133
  ${renderVirtualTableBadge(table)}
@@ -688,12 +717,12 @@ export function renderDataView(state) {
688
717
 
689
718
  return {
690
719
  main: `
691
- <section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden">
692
- <div class="data-view-grid ${tablesVisible ? 'data-view-grid--with-subnavi' : ''}">
720
+ <section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden" data-data-view>
721
+ <div class="data-view-grid ${tablesVisible ? 'data-view-grid--with-subnavi' : ''}" data-data-view-grid>
693
722
  ${
694
723
  tablesVisible
695
724
  ? `
696
- <aside class="data-view__sidebar subnavi-panel">
725
+ <aside class="data-view__sidebar subnavi-panel" data-data-sidebar>
697
726
  <div class="subnavi-header">
698
727
  <div>
699
728
  <div class="subnavi-header-title">Tables</div>
@@ -708,7 +737,7 @@ export function renderDataView(state) {
708
737
  `
709
738
  : ''
710
739
  }
711
- <section class="flex min-h-0 flex-col overflow-hidden">
740
+ <section class="flex min-h-0 flex-col overflow-hidden" data-data-workspace>
712
741
  ${renderWorkspaceHeader(state)}
713
742
  ${renderWorkspaceError(state)}
714
743
  ${renderTableSurface(state)}
@@ -35,7 +35,7 @@ function renderDocumentListItem(item, selectedId) {
35
35
  <span class="documents-list-item__title" title="${escapeHtml(item.filename)}">
36
36
  ${escapeHtml(item.filename)}
37
37
  </span>
38
- <span class="documents-list-item__meta">
38
+ <span class="documents-list-item__meta" data-document-list-meta>
39
39
  ${escapeHtml(formatCompactDateTime(item.updatedAt))} // ${formatNumber(item.contentLength ?? 0)} chars
40
40
  </span>
41
41
  </span>
@@ -77,7 +77,45 @@ function renderDocumentsSearch(documents) {
77
77
 
78
78
  function renderDocumentsSidebar(documents) {
79
79
  const items = documents.items ?? [];
80
+ const folders = documents.folders ?? [];
80
81
  const filteredItems = getFilteredDocuments(items, documents.searchQuery);
82
+ const searchActive = Boolean(String(documents.searchQuery ?? '').trim());
83
+ const rootItems = filteredItems.filter(item => !item.folderId);
84
+ const renderFolderGroup = (label, groupItems, options = {}) => {
85
+ if (searchActive && !groupItems.length) {
86
+ return '';
87
+ }
88
+
89
+ return `
90
+ <div class="documents-folder-group">
91
+ <div class="px-3 pb-1 pt-3 text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/55">
92
+ ${escapeHtml(label)}
93
+ </div>
94
+ ${
95
+ groupItems.length
96
+ ? groupItems.map(item => renderDocumentListItem(item, documents.selectedId)).join('')
97
+ : `<div class="documents-list-empty">${escapeHtml(options.emptyText ?? 'No documents')}</div>`
98
+ }
99
+ </div>
100
+ `;
101
+ };
102
+ const folderGroups = folders
103
+ .map(folder =>
104
+ renderFolderGroup(
105
+ folder.name,
106
+ filteredItems.filter(item => String(item.folderId ?? '') === String(folder.id)),
107
+ { emptyText: 'Empty folder' },
108
+ ),
109
+ )
110
+ .join('');
111
+ const rootGroup = renderFolderGroup('No Folder', rootItems, {
112
+ emptyText: folders.length ? 'No root documents' : 'No documents yet',
113
+ });
114
+ const listMarkup = rootGroup || folderGroups
115
+ ? `${rootGroup}${folderGroups}`
116
+ : `<div class="documents-list-empty">${
117
+ items.length ? 'No documents match the current search.' : 'No documents yet'
118
+ }</div>`;
81
119
 
82
120
  return `
83
121
  <aside class="documents-view__sidebar subnavi-panel">
@@ -89,13 +127,7 @@ function renderDocumentsSidebar(documents) {
89
127
  </div>
90
128
  ${renderDocumentsSearch(documents)}
91
129
  <div class="documents-view__sidebar-body subnavi-list custom-scrollbar">
92
- ${
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>`
98
- }
130
+ ${listMarkup}
99
131
  </div>
100
132
  </aside>
101
133
  `;
@@ -118,18 +150,18 @@ function renderNewDocumentDropdown(documents, className = '') {
118
150
  ${renderDropdownButton({
119
151
  disabled: documents.saving,
120
152
  icon: 'add',
121
- label: 'New Document',
122
- title: 'New document',
153
+ label: 'New',
154
+ title: 'New',
123
155
  items: [
124
156
  {
125
157
  action: 'create-document',
126
158
  icon: 'draft',
127
- label: 'Blank Page',
159
+ label: 'New Document',
128
160
  },
129
161
  {
130
- action: 'import-document-markdown',
131
- icon: 'upload_file',
132
- label: 'Import .md',
162
+ action: 'open-create-document-folder-modal',
163
+ icon: 'create_new_folder',
164
+ label: 'New Folder',
133
165
  },
134
166
  ],
135
167
  })}
@@ -137,40 +169,86 @@ function renderNewDocumentDropdown(documents, className = '') {
137
169
  `;
138
170
  }
139
171
 
140
- function renderDocumentsPanelToggle(documents) {
141
- const visible = documents.documentsVisible !== false;
172
+ function renderDocumentsViewDropdown(documents) {
173
+ const documentsVisible = documents.documentsVisible !== false;
174
+ const editorVisible = documents.editorVisible !== false;
175
+ const previewVisible = documents.previewVisible !== false;
142
176
 
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
- `;
177
+ return renderDropdownButton({
178
+ icon: 'visibility',
179
+ label: 'View',
180
+ title: 'View options',
181
+ items: [
182
+ {
183
+ action: 'toggle-document-pane',
184
+ dataAttributes: { pane: 'editor' },
185
+ icon: editorVisible ? 'visibility_off' : 'edit_note',
186
+ label: `${editorVisible ? 'Hide' : 'Show'} Editor`,
187
+ },
188
+ {
189
+ action: 'toggle-document-pane',
190
+ dataAttributes: { pane: 'preview' },
191
+ icon: previewVisible ? 'visibility_off' : 'visibility',
192
+ label: `${previewVisible ? 'Hide' : 'Show'} Preview`,
193
+ },
194
+ {
195
+ action: 'toggle-documents-panel',
196
+ icon: documentsVisible ? 'visibility_off' : 'description',
197
+ label: `${documentsVisible ? 'Hide' : 'Show'} Documents`,
198
+ },
199
+ ],
200
+ });
154
201
  }
155
202
 
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';
203
+ function renderDocumentsFileDropdown(documents) {
204
+ const exportDisabled = documents.saving || documents.detailLoading || !documents.selectedId;
161
205
 
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
- `;
206
+ return renderDropdownButton({
207
+ disabled: documents.saving,
208
+ icon: 'folder_open',
209
+ label: 'File',
210
+ title: 'File actions',
211
+ items: [
212
+ {
213
+ action: 'import-document-markdown',
214
+ icon: 'upload_file',
215
+ label: 'Import MD',
216
+ },
217
+ {
218
+ action: 'export-document-markdown',
219
+ disabled: exportDisabled,
220
+ icon: 'download',
221
+ label: 'Export MD',
222
+ },
223
+ ],
224
+ });
225
+ }
226
+
227
+ function renderDocumentsMoveDropdown(documents) {
228
+ const folders = documents.folders ?? [];
229
+ const selectedFolderId = String(documents.selected?.folderId ?? '');
230
+
231
+ return renderDropdownButton({
232
+ disabled: documents.saving || documents.detailLoading || !documents.selectedId,
233
+ icon: 'drive_file_move',
234
+ label: 'Move to',
235
+ title: 'Move document to folder',
236
+ items: [
237
+ {
238
+ action: 'move-document-to-folder',
239
+ disabled: !selectedFolderId,
240
+ icon: 'folder_off',
241
+ label: 'No Folder',
242
+ },
243
+ ...folders.map(folder => ({
244
+ action: 'move-document-to-folder',
245
+ dataAttributes: { folderId: folder.id },
246
+ disabled: selectedFolderId === String(folder.id),
247
+ icon: 'folder',
248
+ label: folder.name,
249
+ })),
250
+ ],
251
+ });
174
252
  }
175
253
 
176
254
  function renderDocumentsTitlebar(documents, options = {}) {
@@ -179,7 +257,6 @@ function renderDocumentsTitlebar(documents, options = {}) {
179
257
 
180
258
  return `
181
259
  <div class="documents-titlebar">
182
- ${renderDocumentsPanelToggle(documents)}
183
260
  ${
184
261
  showFilename
185
262
  ? `
@@ -216,19 +293,33 @@ function renderDocumentsTitlebar(documents, options = {}) {
216
293
  {
217
294
  action: 'open-document-insert-note-modal',
218
295
  icon: 'note_add',
219
- label: 'Insert Note',
296
+ label: 'Insert Query Note',
297
+ },
298
+ {
299
+ action: 'open-document-insert-table-definition-modal',
300
+ icon: 'schema',
301
+ label: 'Insert Table Definition',
302
+ },
303
+ {
304
+ action: 'insert-document-saved-queries',
305
+ icon: 'bookmark',
306
+ label: 'Insert Saved Queries',
307
+ },
308
+ {
309
+ action: 'insert-document-time-metadata',
310
+ icon: 'schedule',
311
+ label: 'Insert Time Metadata',
312
+ },
313
+ {
314
+ action: 'insert-document-database-info',
315
+ icon: 'database',
316
+ label: 'Insert Database Info',
220
317
  },
221
318
  ],
222
319
  })}
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>
320
+ ${renderDocumentsViewDropdown(documents)}
321
+ ${renderDocumentsFileDropdown(documents)}
322
+ ${renderDocumentsMoveDropdown(documents)}
232
323
  <button
233
324
  class="delete-button"
234
325
  data-action="delete-document"
@@ -239,7 +330,10 @@ function renderDocumentsTitlebar(documents, options = {}) {
239
330
  Delete
240
331
  </button>
241
332
  `
242
- : ''
333
+ : `
334
+ ${renderDocumentsViewDropdown(documents)}
335
+ ${renderDocumentsFileDropdown(documents)}
336
+ `
243
337
  }
244
338
  </div>
245
339
  `;
@@ -249,7 +343,6 @@ function renderDocumentEditor(documents) {
249
343
  return `
250
344
  <section class="documents-pane documents-pane--editor ${documents.editorVisible ? '' : 'documents-pane--collapsed'}">
251
345
  <div class="documents-pane__header">
252
- ${renderDocumentPaneToggle(documents, 'editor')}
253
346
  <span class="documents-pane__meta">${formatNumber(documents.draftContent.length)} chars</span>
254
347
  </div>
255
348
  ${
@@ -271,11 +364,12 @@ function renderDocumentEditor(documents) {
271
364
  }
272
365
 
273
366
  function renderDocumentPreview(documents) {
367
+ const saveState = documents.dirty ? 'unsaved' : 'saved';
368
+
274
369
  return `
275
370
  <section class="documents-pane documents-pane--preview ${documents.previewVisible ? '' : 'documents-pane--collapsed'}">
276
371
  <div class="documents-pane__header">
277
- ${renderDocumentPaneToggle(documents, 'preview')}
278
- <span class="documents-pane__meta">${escapeHtml(documents.dirty ? 'unsaved' : 'saved')}</span>
372
+ <span class="documents-pane__meta" data-document-save-state="${saveState}">${escapeHtml(saveState)}</span>
279
373
  </div>
280
374
  ${
281
375
  documents.previewVisible
@@ -70,6 +70,15 @@ textarea:disabled {
70
70
  padding: 0 var(--control-padding-inline);
71
71
  }
72
72
 
73
+ .control-input:focus,
74
+ .control-input:focus-visible,
75
+ .control-select:focus,
76
+ .control-select:focus-visible {
77
+ border-color: var(--color-primary-container);
78
+ box-shadow: none;
79
+ outline: none;
80
+ }
81
+
73
82
  input.control-input--ghost {
74
83
  appearance: none;
75
84
  background: transparent;