sqlite-hub 1.1.2 → 1.4.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 (157) hide show
  1. package/README.md +106 -73
  2. package/bin/sqlite-hub.js +176 -2
  3. package/docs/API.md +122 -0
  4. package/docs/CLI.md +227 -0
  5. package/docs/DESIGN_GUIDELINES.md +45 -0
  6. package/docs/changelog.md +141 -0
  7. package/docs/shortkeys.md +27 -0
  8. package/docs/todo.md +16 -0
  9. package/examples/api/generate-types.js +52 -0
  10. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  11. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  12. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  13. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  18. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  22. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  26. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  29. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  33. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  34. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  37. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  38. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  40. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  42. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  43. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  44. package/frontend/js/api.js +18 -0
  45. package/frontend/js/app.js +209 -13
  46. package/frontend/js/components/connectionCard.js +1 -1
  47. package/frontend/js/components/emptyState.js +4 -4
  48. package/frontend/js/components/generateTypesDropdown.js +33 -0
  49. package/frontend/js/components/metricCard.js +1 -1
  50. package/frontend/js/components/modal.js +211 -18
  51. package/frontend/js/components/pageHeader.js +1 -1
  52. package/frontend/js/components/queryHistoryDetail.js +3 -3
  53. package/frontend/js/components/queryHistoryHeader.js +1 -1
  54. package/frontend/js/components/queryHistoryPanel.js +1 -1
  55. package/frontend/js/components/queryResults.js +1 -1
  56. package/frontend/js/components/rowEditorPanel.js +65 -16
  57. package/frontend/js/components/topNav.js +1 -4
  58. package/frontend/js/components/workspaceOpenDropdown.js +52 -0
  59. package/frontend/js/router.js +28 -6
  60. package/frontend/js/store.js +489 -9
  61. package/frontend/js/utils/emailPreview.js +28 -0
  62. package/frontend/js/utils/markdownDocuments.js +17 -1
  63. package/frontend/js/views/backups.js +556 -36
  64. package/frontend/js/views/charts.js +9 -9
  65. package/frontend/js/views/connections.js +2 -2
  66. package/frontend/js/views/data.js +43 -17
  67. package/frontend/js/views/documents.js +3 -3
  68. package/frontend/js/views/editor.js +6 -6
  69. package/frontend/js/views/mediaTagging.js +7 -5
  70. package/frontend/js/views/overview.js +3 -3
  71. package/frontend/js/views/settings.js +4 -3
  72. package/frontend/js/views/structure.js +27 -10
  73. package/frontend/js/views/tableDesigner.js +23 -0
  74. package/frontend/styles/base.css +1 -40
  75. package/frontend/styles/components.css +114 -232
  76. package/frontend/styles/structure-graph.css +32 -68
  77. package/frontend/styles/tailwind.css +8 -4
  78. package/frontend/styles/tailwind.generated.css +1 -1
  79. package/frontend/styles/tokens.css +29 -21
  80. package/frontend/styles/views.css +94 -261
  81. package/package.json +18 -2
  82. package/server/routes/backups.js +18 -2
  83. package/server/routes/externalApi.js +36 -0
  84. package/server/routes/structure.js +22 -0
  85. package/server/services/databaseCommandService.js +14 -0
  86. package/server/services/sqlite/backupDiff.js +914 -0
  87. package/server/services/sqlite/backupService.js +77 -1
  88. package/server/services/sqlite/structureService.js +7 -0
  89. package/server/services/typeGenerationService.js +663 -0
  90. package/tailwind.config.cjs +0 -1
  91. package/.github/funding.yml +0 -2
  92. package/.github/workflows/ci.yml +0 -36
  93. package/database.sqlite +0 -0
  94. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  95. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  96. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  97. package/frontend/assets/mockups/connections_1200.webp +0 -0
  98. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  99. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  100. package/frontend/assets/mockups/documents_1200.webp +0 -0
  101. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  102. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  103. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  104. package/frontend/assets/mockups/overview_1200.webp +0 -0
  105. package/frontend/assets/mockups/settings_1200.webp +0 -0
  106. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  107. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  108. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  109. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  110. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  111. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  112. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  113. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  114. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  115. package/tests/api-token-auth.test.js +0 -279
  116. package/tests/backup-manager.test.js +0 -140
  117. package/tests/backups-view.test.js +0 -64
  118. package/tests/charts-height-preset-storage.test.js +0 -60
  119. package/tests/charts-route-state.test.js +0 -144
  120. package/tests/check-constraint-options.test.js +0 -90
  121. package/tests/cli-args.test.js +0 -113
  122. package/tests/cli-service-delegation.test.js +0 -140
  123. package/tests/connection-removal.test.js +0 -52
  124. package/tests/connections-file-dialog-route.test.js +0 -89
  125. package/tests/copy-column-modal.test.js +0 -131
  126. package/tests/database-command-service.test.js +0 -174
  127. package/tests/database-documents.test.js +0 -85
  128. package/tests/documents-view.test.js +0 -132
  129. package/tests/dropdown-button.test.js +0 -75
  130. package/tests/export-blob.test.js +0 -99
  131. package/tests/export-filenames.test.js +0 -38
  132. package/tests/file-path-preview.test.js +0 -165
  133. package/tests/form-controls.test.js +0 -34
  134. package/tests/json-preview.test.js +0 -49
  135. package/tests/local-request-security.test.js +0 -85
  136. package/tests/markdown-documents.test.js +0 -79
  137. package/tests/native-file-dialog.test.js +0 -105
  138. package/tests/query-editor.test.js +0 -28
  139. package/tests/query-history-detail.test.js +0 -37
  140. package/tests/query-history-header.test.js +0 -30
  141. package/tests/query-results-truncation.test.js +0 -20
  142. package/tests/risky-sql.test.js +0 -30
  143. package/tests/row-editor-json.test.js +0 -82
  144. package/tests/row-editor-null-values.test.js +0 -155
  145. package/tests/row-editor-timestamp-preview.test.js +0 -192
  146. package/tests/security-paths.test.js +0 -84
  147. package/tests/settings-api-tokens-route.test.js +0 -97
  148. package/tests/settings-metadata.test.js +0 -114
  149. package/tests/settings-view.test.js +0 -107
  150. package/tests/sql-formatter.test.js +0 -173
  151. package/tests/sql-highlight.test.js +0 -38
  152. package/tests/sql-identifier-safety.test.js +0 -171
  153. package/tests/sql-result-limit.test.js +0 -66
  154. package/tests/structure-view.test.js +0 -56
  155. package/tests/table-designer-v2-unique-constraints.test.js +0 -78
  156. package/tests/table-scroll-state.test.js +0 -70
  157. package/tests/text-cell-stats.test.js +0 -38
@@ -126,7 +126,7 @@ function renderError(error) {
126
126
 
127
127
  return `
128
128
  <div class="border border-error/20 bg-error-container/20 px-4 py-3 text-sm text-error">
129
- <div class="font-headline text-xs font-bold uppercase tracking-[0.18em]">${escapeHtml(
129
+ <div class="font-body text-xs font-bold uppercase tracking-[0.18em]">${escapeHtml(
130
130
  error.code || 'Request failed',
131
131
  )}</div>
132
132
  <div class="mt-1 text-on-surface">${escapeHtml(error.message)}</div>
@@ -449,15 +449,25 @@ function renderDeleteBackupForm(modal) {
449
449
  ].join('');
450
450
  }
451
451
 
452
- function renderEditBackupNotesForm(modal) {
452
+ function renderEditBackupForm(modal) {
453
453
  return `
454
- <form class="space-y-5" data-form="edit-backup-notes">
454
+ <form class="space-y-5" data-form="edit-backup">
455
455
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
456
- <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">Backup</div>
457
- <div class="mt-2 font-headline text-sm font-black uppercase text-on-surface">
458
- ${escapeHtml(modal.backupName ?? 'Backup')}
456
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">Primary Key</div>
457
+ <div class="mt-2 font-mono text-[10px] uppercase tracking-[0.12em] text-on-surface/80">
458
+ ${escapeHtml(modal.backupId ?? 'n/a')}
459
459
  </div>
460
460
  </div>
461
+ <label class="block space-y-2">
462
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Name</span>
463
+ <input
464
+ class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest px-3 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
465
+ name="name"
466
+ placeholder="Backup name"
467
+ required
468
+ value="${escapeHtml(modal.backupName ?? 'Backup')}"
469
+ />
470
+ </label>
461
471
  <label class="block space-y-2">
462
472
  <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Notes</span>
463
473
  <textarea
@@ -471,7 +481,7 @@ function renderEditBackupNotesForm(modal) {
471
481
  <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
472
482
  <button class="signature-button" type="submit" ${modal.submitting ? 'disabled' : ''}>
473
483
  <span class="material-symbols-outlined text-sm">save</span>
474
- ${modal.submitting ? 'Saving...' : 'Save notes'}
484
+ ${modal.submitting ? 'Saving...' : 'Save backup'}
475
485
  </button>
476
486
  </div>
477
487
  </form>
@@ -490,7 +500,7 @@ function renderBackupSafetyForm(modal) {
490
500
  </p>
491
501
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
492
502
  <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">Safety Backup</div>
493
- <div class="mt-2 font-headline text-sm font-black uppercase text-on-surface">${escapeHtml(
503
+ <div class="mt-2 font-body text-sm font-black uppercase text-on-surface">${escapeHtml(
494
504
  modal.backupNameBeforeOperation ?? modal.backupName ?? 'Before operation',
495
505
  )}</div>
496
506
  </div>
@@ -512,6 +522,180 @@ function renderBackupSafetyForm(modal) {
512
522
  `;
513
523
  }
514
524
 
525
+ function renderTypeGenerationSelect({ label, field, value, options = [] }) {
526
+ return renderSelectField({
527
+ label,
528
+ name: field,
529
+ value,
530
+ bind: 'type-generation-field',
531
+ options,
532
+ }).replace('<select ', `<select data-type-generation-field="${escapeHtml(field)}" `);
533
+ }
534
+
535
+ function renderTypeGenerationCheckbox({ label, field, checked }) {
536
+ return `
537
+ <label class="standard-checkbox">
538
+ <input
539
+ ${checked ? 'checked' : ''}
540
+ data-bind="type-generation-field"
541
+ data-type-generation-field="${escapeHtml(field)}"
542
+ type="checkbox"
543
+ />
544
+ <span>${escapeHtml(label)}</span>
545
+ </label>
546
+ `;
547
+ }
548
+
549
+ function renderTypeGenerationCodePreview(code) {
550
+ const text = String(code ?? '');
551
+ const normalized = text.includes('\n') || text.includes('\\n')
552
+ ? text.replace(/\\n/g, '\n')
553
+ : text
554
+ .replace(/\{\s*/g, '{\n ')
555
+ .replace(/;\s*/g, ';\n ')
556
+ .replace(/\s*\}\s*$/g, '\n}');
557
+
558
+ return normalized
559
+ .replace(/\r\n/g, '\n')
560
+ .split('\n')
561
+ .map(line => `<span class="block whitespace-pre">${line ? escapeHtml(line) : '&nbsp;'}</span>`)
562
+ .join('');
563
+ }
564
+
565
+ export function renderGenerateTypesForm(modal) {
566
+ const options = modal.options ?? {};
567
+ const result = modal.result ?? {};
568
+ const target = modal.target ?? 'typescript';
569
+ const isAllTables = modal.scope === 'all';
570
+ const fileCount = result.files?.length ?? modal.tableNames?.length ?? 0;
571
+ const subjectLabel = isAllTables
572
+ ? `all ${fileCount || ''} tables`.replace(/\s+/g, ' ').trim()
573
+ : `the schema of "${modal.tableName ?? ''}"`;
574
+ const downloadLabel = result.files?.length ? 'Download Files' : 'Download';
575
+
576
+ return `
577
+ <div class="space-y-5">
578
+ <p class="text-sm leading-6 text-on-surface-variant/70">
579
+ Generate application types from ${escapeHtml(subjectLabel)}.
580
+ </p>
581
+ <div class="grid gap-5 xl:grid-cols-[24rem_minmax(0,1fr)]">
582
+ <div class="min-w-0 space-y-4">
583
+ <div class="space-y-4">
584
+ ${renderTypeGenerationSelect({
585
+ label: 'Target',
586
+ field: 'target',
587
+ value: target,
588
+ options: [
589
+ { value: 'typescript', label: 'TypeScript' },
590
+ { value: 'rust', label: 'Rust' },
591
+ { value: 'kotlin', label: 'Kotlin' },
592
+ { value: 'swift', label: 'Swift' },
593
+ ],
594
+ })}
595
+ ${renderTypeGenerationSelect({
596
+ label: 'Property naming',
597
+ field: 'propertyNaming',
598
+ value: options.propertyNaming ?? 'camel',
599
+ options: [
600
+ { value: 'preserve', label: 'Preserve' },
601
+ { value: 'camel', label: 'camelCase' },
602
+ { value: 'pascal', label: 'PascalCase' },
603
+ { value: 'snake', label: 'snake_case' },
604
+ ],
605
+ })}
606
+ ${renderTypeGenerationSelect({
607
+ label: 'Nullable handling',
608
+ field: 'nullableMode',
609
+ value: options.nullableMode ?? 'native',
610
+ options: [
611
+ { value: 'native', label: 'Native' },
612
+ { value: 'optional', label: 'Optional (TypeScript)' },
613
+ ],
614
+ })}
615
+ ${
616
+ target === 'typescript'
617
+ ? renderTypeGenerationSelect({
618
+ label: 'JSON type',
619
+ field: 'jsonType',
620
+ value: options.jsonType ?? 'unknown',
621
+ options: [
622
+ { value: 'unknown', label: 'unknown' },
623
+ { value: 'record', label: 'Record<string, unknown>' },
624
+ { value: 'json-value', label: 'JsonValue' },
625
+ ],
626
+ })
627
+ : ''
628
+ }
629
+ </div>
630
+ <div class="grid gap-2">
631
+ ${renderTypeGenerationCheckbox({
632
+ label: 'Export declaration',
633
+ field: 'exportDeclaration',
634
+ checked: options.exportDeclaration !== false,
635
+ })}
636
+ ${renderTypeGenerationCheckbox({
637
+ label: 'Include default values as comments',
638
+ field: 'includeDefaultsAsComments',
639
+ checked: Boolean(options.includeDefaultsAsComments),
640
+ })}
641
+ ${renderTypeGenerationCheckbox({
642
+ label: 'Include generated columns',
643
+ field: 'includeGeneratedColumns',
644
+ checked: options.includeGeneratedColumns !== false,
645
+ })}
646
+ ${renderTypeGenerationCheckbox({
647
+ label: 'Include hidden columns',
648
+ field: 'includeHiddenColumns',
649
+ checked: Boolean(options.includeHiddenColumns),
650
+ })}
651
+ </div>
652
+ </div>
653
+ <div class="min-w-0 space-y-3">
654
+ <div class="flex items-center justify-between gap-3">
655
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container">Code Preview</div>
656
+ <div class="truncate font-mono text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
657
+ ${escapeHtml(result.fileName ?? '')}
658
+ </div>
659
+ </div>
660
+ ${
661
+ result.files?.length
662
+ ? `<div class="flex flex-wrap gap-2">${result.files
663
+ .map(
664
+ file =>
665
+ `<span class="border border-outline-variant/20 bg-surface-highest px-2 py-1 font-mono text-[10px] uppercase tracking-[0.14em] text-on-surface-variant/70">${escapeHtml(
666
+ file.fileName ?? file.tableName ?? 'types',
667
+ )}</span>`,
668
+ )
669
+ .join('')}</div>`
670
+ : ''
671
+ }
672
+ <pre class="type-generation-code-preview custom-scrollbar border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 font-mono text-xs leading-6 text-on-surface"><code>${renderTypeGenerationCodePreview(
673
+ modal.loading ? 'Generating...' : result.code ?? '',
674
+ )}</code></pre>
675
+ ${renderError(modal.error)}
676
+ </div>
677
+ </div>
678
+ <div class="flex items-center justify-between gap-3 pt-2">
679
+ <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
680
+ <div class="flex items-center gap-2">
681
+ <button class="standard-button" data-action="copy-generated-types" type="button" ${
682
+ result.code ? '' : 'disabled'
683
+ }>
684
+ <span class="material-symbols-outlined text-sm">content_copy</span>
685
+ Copy Code
686
+ </button>
687
+ <button class="signature-button" data-action="download-generated-types" type="button" ${
688
+ result.code ? '' : 'disabled'
689
+ }>
690
+ <span class="material-symbols-outlined text-sm">download</span>
691
+ ${escapeHtml(downloadLabel)}
692
+ </button>
693
+ </div>
694
+ </div>
695
+ </div>
696
+ `;
697
+ }
698
+
515
699
  function renderDeleteRowConfirmForm(modal) {
516
700
  const rowPreview = modal.rowPreview ?? [];
517
701
  const rowLabelMarkup = modal.rowLabel
@@ -1172,7 +1356,7 @@ function renderTextExportModal(modal, action) {
1172
1356
  <span class="material-symbols-outlined text-xl">${escapeHtml(option.icon)}</span>
1173
1357
  </span>
1174
1358
  <span class="min-w-0">
1175
- <span class="block truncate font-headline text-lg font-black uppercase tracking-normal text-primary-container">
1359
+ <span class="block truncate font-body text-lg font-black uppercase tracking-normal text-primary-container">
1176
1360
  ${escapeHtml(option.label)}
1177
1361
  </span>
1178
1362
  <span class="mt-1 block font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
@@ -1806,16 +1990,21 @@ export function renderModal(state) {
1806
1990
  title: 'Delete Backup',
1807
1991
  body: renderDeleteBackupForm(modal),
1808
1992
  },
1809
- 'edit-backup-notes': {
1810
- eyebrow: 'Backups // Notes',
1811
- title: 'Edit Backup Notes',
1812
- body: renderEditBackupNotesForm(modal),
1993
+ 'edit-backup': {
1994
+ eyebrow: 'Backups // Edit',
1995
+ title: 'Edit Backup',
1996
+ body: renderEditBackupForm(modal),
1813
1997
  },
1814
1998
  'backup-safety': {
1815
1999
  eyebrow: 'Backups // Safety check',
1816
2000
  title: 'Create a safety backup?',
1817
2001
  body: renderBackupSafetyForm(modal),
1818
2002
  },
2003
+ 'generate-types': {
2004
+ eyebrow: 'Structure // Type generation',
2005
+ title: 'Generate Types',
2006
+ body: renderGenerateTypesForm(modal),
2007
+ },
1819
2008
  'edit-connection': {
1820
2009
  eyebrow: 'Registry // Update saved SQLite target',
1821
2010
  title: 'Edit Connection',
@@ -1906,23 +2095,27 @@ export function renderModal(state) {
1906
2095
 
1907
2096
  return `
1908
2097
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
1909
- <div class="w-full ${
2098
+ <div class="app-modal-shell w-full ${
1910
2099
  modal.kind === 'chart-editor' ||
1911
2100
  modal.kind === 'document-insert-table' ||
1912
2101
  modal.kind === 'document-insert-note' ||
1913
2102
  modal.kind === 'row-update-preview' ||
1914
2103
  modal.kind === 'table-designer-constraints'
1915
2104
  ? 'max-w-3xl'
1916
- : modal.kind === 'query-export' || modal.kind === 'data-export' || modal.kind === 'backup-safety'
2105
+ : modal.kind === 'generate-types'
2106
+ ? 'max-w-6xl'
2107
+ : modal.kind === 'query-export' ||
2108
+ modal.kind === 'data-export' ||
2109
+ modal.kind === 'backup-safety'
1917
2110
  ? 'max-w-4xl'
1918
2111
  : 'max-w-xl'
1919
2112
  } border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
1920
- <div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
2113
+ <div class="shrink-0 flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
1921
2114
  <div>
1922
2115
  <div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">
1923
2116
  ${escapeHtml(config.eyebrow)}
1924
2117
  </div>
1925
- <h2 class="mt-2 font-headline text-3xl font-black uppercase tracking-tight text-primary-container">
2118
+ <h2 class="mt-2 font-body text-3xl font-black uppercase tracking-tight text-primary-container">
1926
2119
  ${escapeHtml(config.title)}
1927
2120
  </h2>
1928
2121
  </div>
@@ -1934,7 +2127,7 @@ export function renderModal(state) {
1934
2127
  <span class="material-symbols-outlined">close</span>
1935
2128
  </button>
1936
2129
  </div>
1937
- <div class="space-y-5 px-6 py-6">${config.body}</div>
2130
+ <div class="app-modal-body custom-scrollbar space-y-5 px-6 py-6">${config.body}</div>
1938
2131
  </div>
1939
2132
  </div>
1940
2133
  `;
@@ -16,7 +16,7 @@ export function renderPageHeader({ eyebrow = '', title, subtitle = '', actions =
16
16
  `
17
17
  : ''
18
18
  }
19
- <h1 class="text-5xl font-headline font-bold text-[#FCE300] tracking-tighter uppercase">${escapeHtml(title)}</h1>
19
+ <h1 class="text-5xl font-body font-bold text-[#FCE300] tracking-tighter uppercase">${escapeHtml(title)}</h1>
20
20
  ${
21
21
  subtitle
22
22
  ? `<p class="text-xs font-mono text-on-surface/40 mt-1 uppercase tracking-widest">${escapeHtml(
@@ -100,7 +100,7 @@ export function renderQueryHistoryDetail({
100
100
  <section class="flex h-full min-h-0 flex-col bg-surface-low">
101
101
  <div class="border-b border-outline-variant/10 px-5 py-4">
102
102
  <div class="flex items-center justify-between gap-3">
103
- <span class="font-headline text-sm font-black uppercase tracking-[0.18em] text-primary-container">
103
+ <span class="font-body text-sm font-black uppercase tracking-[0.18em] text-primary-container">
104
104
  Query Detail
105
105
  </span>
106
106
  <button
@@ -124,7 +124,7 @@ export function renderQueryHistoryDetail({
124
124
  <section class="flex h-full min-h-0 flex-col bg-surface-low">
125
125
  <div class="border-b border-outline-variant/10 px-5 py-4">
126
126
  <div class="flex items-center justify-between gap-3">
127
- <span class="font-headline text-sm font-black uppercase tracking-[0.18em] text-primary-container">
127
+ <span class="font-body text-sm font-black uppercase tracking-[0.18em] text-primary-container">
128
128
  Query Detail
129
129
  </span>
130
130
  <button
@@ -178,7 +178,7 @@ export function renderQueryHistoryDetail({
178
178
  '<div class="border-b border-outline-variant/10 px-5 py-4">',
179
179
  '<div class="flex items-center justify-between gap-3"><div>',
180
180
  '<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-primary-container/70">Query Detail</div>',
181
- '<h2 class="mt-1 font-headline text-lg font-black uppercase tracking-tight text-on-surface">',
181
+ '<h2 class="mt-1 font-body text-lg font-black uppercase tracking-tight text-on-surface">',
182
182
  escapeHtml(item.displayTitle),
183
183
  "</h2></div>",
184
184
  '<button class="query-history-icon-button" data-action="clear-query-history-selection" type="button"><span class="material-symbols-outlined text-[18px]">close</span></button>',
@@ -11,7 +11,7 @@ export function renderQueryHistoryHeader({
11
11
  <div class="flex items-center justify-between gap-3">
12
12
  <div class="flex items-center gap-2">
13
13
  <span class="material-symbols-outlined text-[18px] text-primary-container">${escapeHtml(icon)}</span>
14
- <span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
14
+ <span class="font-body text-xs font-black uppercase tracking-[0.18em] text-primary-container">
15
15
  ${escapeHtml(title)}
16
16
  </span>
17
17
  </div>
@@ -114,7 +114,7 @@ export function renderQueryHistoryPanel({
114
114
  ? `
115
115
  <div class="flex h-full min-h-[240px] flex-col items-center justify-center px-6 text-center">
116
116
  <span class="material-symbols-outlined text-4xl text-on-surface-variant/25">manage_search</span>
117
- <p class="mt-4 font-headline text-lg font-bold uppercase tracking-tight text-on-surface">
117
+ <p class="mt-4 font-body text-lg font-bold uppercase tracking-tight text-on-surface">
118
118
  No Matching Queries
119
119
  </p>
120
120
  <p class="mt-2 max-w-xs text-sm leading-6 text-on-surface-variant/65">
@@ -147,7 +147,7 @@ export function renderQueryResultsPane(
147
147
  ? renderDataGrid({
148
148
  columns,
149
149
  rows: result.rows ?? [],
150
- tableClass: "min-w-full border-collapse text-left font-mono text-xs",
150
+ tableClass: "data-table min-w-full border-collapse text-left font-mono text-xs",
151
151
  theadClass: "sticky top-0 z-10 bg-surface-container-highest text-on-surface",
152
152
  tbodyClass: "divide-y divide-outline-variant/5",
153
153
  getRowClass: (_, index) =>
@@ -1,4 +1,6 @@
1
1
  import { escapeHtml } from "../utils/format.js";
2
+ import { renderDropdownButton } from "./dropdownButton.js";
3
+ import { detectEmailValue } from "../utils/emailPreview.js";
2
4
  import {
3
5
  compactPathForDisplay,
4
6
  detectFilePathValue,
@@ -49,6 +51,19 @@ function withUrlBadge(badges = [], url) {
49
51
  return hasUrlBadge ? badges : [...badges, { label: "URL", tone: "url" }];
50
52
  }
51
53
 
54
+ function withEmailBadge(badges = [], email) {
55
+ if (!email) {
56
+ return badges;
57
+ }
58
+
59
+ const hasEmailBadge = badges.some((badge) => {
60
+ const label = typeof badge === "object" ? badge.label : badge;
61
+ return String(label ?? "").toUpperCase() === "EMAIL";
62
+ });
63
+
64
+ return hasEmailBadge ? badges : [...badges, { label: "EMAIL", tone: "email" }];
65
+ }
66
+
52
67
  function getAllowedValues(field) {
53
68
  const seen = new Set();
54
69
 
@@ -161,6 +176,14 @@ function getFieldTimestampPreview(field = {}, tableMeta = {}) {
161
176
  });
162
177
  }
163
178
 
179
+ function isNumberInputCompatibleValue(value) {
180
+ if (value === null || value === undefined || value === "") {
181
+ return true;
182
+ }
183
+
184
+ return /^[-+]?(?:\d+|\d*\.\d+)(?:e[-+]?\d+)?$/i.test(String(value).trim());
185
+ }
186
+
164
187
  function renderTimestampPreview(field = {}, tableMeta = {}) {
165
188
  const preview = getFieldTimestampPreview(field, tableMeta);
166
189
 
@@ -277,7 +300,11 @@ function renderReadonlyField(field = {}, tableMeta = {}) {
277
300
  const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
278
301
  const filePathPreview = getFieldFilePathPreview({ ...field, rawValue }, tableMeta);
279
302
  const url = getUrlValue(value);
280
- const badges = withFilePathBadge(withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url), filePathPreview);
303
+ const email = detectEmailValue(rawValue);
304
+ const badges = withEmailBadge(
305
+ withFilePathBadge(withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url), filePathPreview),
306
+ email
307
+ );
281
308
  const displayLabel = typeof label === "object" ? label.label : label;
282
309
  const jsonPreview = formatJsonPreview(value);
283
310
  const valueState = getRowEditorValueState(rawValue);
@@ -290,7 +317,9 @@ function renderReadonlyField(field = {}, tableMeta = {}) {
290
317
  data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
291
318
  ${
292
319
  url ? "data-row-editor-url-field" : ""
293
- }>
320
+ }
321
+ ${email ? "data-row-editor-email-field" : ""}
322
+ >
294
323
  <div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
295
324
  <span>${escapeHtml(displayLabel)}</span>
296
325
  ${renderFieldBadgesWithCharacterCount(badges, rawValue)}
@@ -334,12 +363,15 @@ function renderEditableField(field, tableMeta = {}) {
334
363
  const columnName = getFieldColumnName(field);
335
364
  const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
336
365
  const url = getUrlValue(field.value);
366
+ const email = detectEmailValue(getFieldRawValue(field));
337
367
  const allowedValues = getAllowedValues(field);
338
368
  const filePathPreview = getFieldFilePathPreview(field, tableMeta);
339
369
  const baseBadges = withCheckBadge(Array.isArray(field.badges) ? field.badges : [], allowedValues);
340
- const badges = withFilePathBadge(withUrlBadge(baseBadges, url), filePathPreview);
370
+ const badges = withEmailBadge(withFilePathBadge(withUrlBadge(baseBadges, url), filePathPreview), email);
341
371
  const jsonPreview = formatJsonPreview(field.value);
342
- const inputType = field.inputType === "number" ? "number" : "text";
372
+ const canUseNumberInput =
373
+ field.inputType === "number" && isNumberInputCompatibleValue(getFieldRawValue(field));
374
+ const inputType = canUseNumberInput ? "number" : "text";
343
375
  const numberStep = field.numberStep === "1" ? "1" : "any";
344
376
  const valueState = getRowEditorValueState(getFieldRawValue(field));
345
377
 
@@ -351,6 +383,7 @@ function renderEditableField(field, tableMeta = {}) {
351
383
  data-row-editor-initial-state="${escapeHtml(valueState)}"
352
384
  data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
353
385
  ${url ? "data-row-editor-url-field" : ""}
386
+ ${email ? "data-row-editor-email-field" : ""}
354
387
  >
355
388
  <span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
356
389
  <span>${escapeHtml(field.label ?? field.name)}</span>
@@ -427,6 +460,10 @@ function getFieldBadgeClassName(tone) {
427
460
  return "border-primary-container/35 bg-primary-container/15 text-primary-container";
428
461
  }
429
462
 
463
+ if (tone === "email") {
464
+ return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
465
+ }
466
+
430
467
  if (tone === "check") {
431
468
  return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
432
469
  }
@@ -488,16 +525,28 @@ export function renderRowEditorPanel({
488
525
  ].join("")
489
526
  : "",
490
527
  jsonActionsEnabled
491
- ? [
492
- '<button class="standard-button" data-action="copy-row-editor-json" type="button">',
493
- '<span class="material-symbols-outlined text-sm">content_copy</span>',
494
- "Copy as JSON",
495
- "</button>",
496
- '<button class="standard-button" data-action="export-row-editor-json" type="button">',
497
- '<span class="material-symbols-outlined text-sm">download</span>',
498
- "Export as JSON",
499
- "</button>",
500
- ].join("")
528
+ ? renderDropdownButton({
529
+ icon: "download",
530
+ label: "Export",
531
+ title: "Export row",
532
+ items: [
533
+ {
534
+ action: "copy-row-editor-json",
535
+ icon: "content_copy",
536
+ label: "Copy to clipboard",
537
+ },
538
+ {
539
+ action: "export-row-editor-json",
540
+ icon: "download",
541
+ label: "JSON file",
542
+ },
543
+ {
544
+ action: "insert-row-editor-json-into-document",
545
+ icon: "description",
546
+ label: "Markdown document",
547
+ },
548
+ ],
549
+ })
501
550
  : "",
502
551
  canSubmit
503
552
  ? [
@@ -538,7 +587,7 @@ export function renderRowEditorPanel({
538
587
  <div class="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-container">
539
588
  ${escapeHtml(sectionLabel)}
540
589
  </div>
541
- <h2 class="mt-2 font-headline text-3xl font-black uppercase tracking-tight text-primary-container">
590
+ <h2 class="mt-2 font-body text-3xl font-black uppercase tracking-tight text-primary-container">
542
591
  ${escapeHtml(title)}
543
592
  </h2>
544
593
  ${
@@ -608,7 +657,7 @@ export function renderRowEditorPanel({
608
657
  saveError
609
658
  ? `
610
659
  <div class="border border-error/20 bg-error-container/10 px-4 py-4 text-sm text-on-surface">
611
- <div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
660
+ <div class="font-body text-xs font-bold uppercase tracking-[0.18em] text-error">
612
661
  ${escapeHtml(saveError.code)}
613
662
  </div>
614
663
  <div class="mt-2">${escapeHtml(saveError.message)}</div>
@@ -1,11 +1,8 @@
1
1
  export function renderTopNav() {
2
- return `
2
+ return `
3
3
  <div class="top-nav-shell">
4
4
  <a class="top-nav-brand" href="#/">SQLite Hub</a>
5
5
  <div class="top-nav-actions">
6
- <button class="top-nav-icon" data-action="refresh-view" type="button" aria-label="Refresh">
7
- <span class="material-symbols-outlined">refresh</span>
8
- </button>
9
6
  <button
10
7
  class="top-nav-icon"
11
8
  data-action="open-modal"
@@ -0,0 +1,52 @@
1
+ import { renderDropdownButton } from "./dropdownButton.js";
2
+
3
+ function buildOpenItem({ disabled, icon, label, target, tableName }) {
4
+ return {
5
+ action: "navigate",
6
+ dataAttributes: {
7
+ to: target,
8
+ },
9
+ disabled,
10
+ icon,
11
+ label,
12
+ };
13
+ }
14
+
15
+ export function renderWorkspaceOpenDropdown({
16
+ align = "right",
17
+ destinations = [],
18
+ disabled = false,
19
+ tableName = "",
20
+ } = {}) {
21
+ const safeTableName = String(tableName ?? "").trim();
22
+ const items = destinations.map((destination) => {
23
+ if (destination.key === "sql-editor") {
24
+ return {
25
+ action: "open-table-in-sql-editor",
26
+ dataAttributes: {
27
+ tableName: safeTableName,
28
+ },
29
+ disabled: disabled || !safeTableName,
30
+ icon: "terminal",
31
+ label: "SQL Editor",
32
+ };
33
+ }
34
+
35
+ return buildOpenItem({
36
+ disabled: disabled || !safeTableName,
37
+ icon: destination.icon,
38
+ label: destination.label,
39
+ target: destination.target(safeTableName),
40
+ tableName: safeTableName,
41
+ });
42
+ });
43
+
44
+ return renderDropdownButton({
45
+ align,
46
+ disabled: disabled || !safeTableName,
47
+ icon: "open_in_new",
48
+ items,
49
+ label: "Open",
50
+ title: safeTableName ? `Open ${safeTableName}` : "Open table",
51
+ });
52
+ }