sqlite-hub 0.16.0 → 0.17.2

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 (53) hide show
  1. package/README.md +106 -19
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +34 -0
  5. package/frontend/js/app.js +481 -32
  6. package/frontend/js/components/modal.js +270 -50
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryHistoryPanel.js +126 -131
  9. package/frontend/js/components/queryResults.js +18 -1
  10. package/frontend/js/components/rowEditorPanel.js +42 -34
  11. package/frontend/js/components/sidebar.js +1 -0
  12. package/frontend/js/router.js +8 -0
  13. package/frontend/js/store.js +655 -2
  14. package/frontend/js/utils/jsonPreview.js +31 -0
  15. package/frontend/js/utils/markdownDocuments.js +248 -0
  16. package/frontend/js/utils/rowEditorValues.js +41 -0
  17. package/frontend/js/utils/tableScrollState.js +39 -0
  18. package/frontend/js/views/charts.js +8 -0
  19. package/frontend/js/views/data.js +4 -1
  20. package/frontend/js/views/documents.js +300 -0
  21. package/frontend/js/views/editor.js +2 -0
  22. package/frontend/js/views/settings.js +39 -3
  23. package/frontend/styles/components.css +19 -0
  24. package/frontend/styles/tailwind.generated.css +1 -1
  25. package/frontend/styles/views.css +422 -0
  26. package/package.json +2 -1
  27. package/server/middleware/localRequestSecurity.js +84 -0
  28. package/server/routes/connections.js +18 -1
  29. package/server/routes/documents.js +163 -0
  30. package/server/routes/settings.js +22 -6
  31. package/server/server.js +18 -1
  32. package/server/services/nativeFileDialogService.js +168 -0
  33. package/server/services/sqlite/dataBrowserService.js +0 -4
  34. package/server/services/sqlite/exportService.js +5 -1
  35. package/server/services/sqlite/sqlExecutor.js +51 -2
  36. package/server/services/storage/appStateStore.js +313 -0
  37. package/server/utils/sqliteTypes.js +17 -8
  38. package/tests/cli-args.test.js +100 -0
  39. package/tests/connections-file-dialog-route.test.js +46 -0
  40. package/tests/copy-column-modal.test.js +97 -0
  41. package/tests/database-documents.test.js +85 -0
  42. package/tests/export-blob.test.js +46 -0
  43. package/tests/json-preview.test.js +49 -0
  44. package/tests/local-request-security.test.js +85 -0
  45. package/tests/markdown-documents.test.js +79 -0
  46. package/tests/native-file-dialog.test.js +78 -0
  47. package/tests/query-results-truncation.test.js +20 -0
  48. package/tests/row-editor-null-values.test.js +131 -0
  49. package/tests/settings-metadata.test.js +16 -0
  50. package/tests/settings-view.test.js +32 -0
  51. package/tests/sql-identifier-safety.test.js +9 -3
  52. package/tests/sql-result-limit.test.js +66 -0
  53. package/tests/table-scroll-state.test.js +70 -0
@@ -1,5 +1,6 @@
1
1
  import { escapeHtml, formatNumber, highlightSql, truncateMiddle } from "../utils/format.js";
2
2
  import {
3
+ buildCopyColumnText,
3
4
  buildCopyColumnPreviewText,
4
5
  getCopyColumnActionLabel,
5
6
  getCopyColumnExportMetadata,
@@ -170,7 +171,7 @@ function renderOpenConnectionForm(modal) {
170
171
  text: "Open read-only",
171
172
  })}
172
173
  ${renderError(modal.error)}
173
- <div class="flex items-center justify-end gap-3 pt-2">
174
+ <div class="flex items-center justify-between gap-3 pt-2">
174
175
  <button
175
176
  class="standard-button"
176
177
  data-action="close-modal"
@@ -244,7 +245,7 @@ function renderEditConnectionForm(modal) {
244
245
  text: "Open read-only",
245
246
  })}
246
247
  ${renderError(modal.error)}
247
- <div class="flex items-center justify-end gap-3 pt-2">
248
+ <div class="flex items-center justify-between gap-3 pt-2">
248
249
  <button
249
250
  class="standard-button"
250
251
  data-action="close-modal"
@@ -263,21 +264,41 @@ function renderEditConnectionForm(modal) {
263
264
  `;
264
265
  }
265
266
 
266
- function renderCreateDatabaseForm(modal) {
267
+ export function renderCreateDatabaseForm(modal) {
267
268
  return `
268
269
  <form class="space-y-5" data-form="create-connection">
269
- ${renderField({
270
- label: "New SQLite File Path",
271
- name: "path",
272
- placeholder: "/absolute/path/to/new-database.sqlite",
273
- })}
270
+ <label class="block space-y-2">
271
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
272
+ New SQLite File Path
273
+ </span>
274
+ <span class="flex items-stretch gap-2">
275
+ <input
276
+ class="control-input min-w-0 flex-1 border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
277
+ data-create-database-path
278
+ name="path"
279
+ placeholder="/absolute/path/to/new-database.sqlite"
280
+ type="text"
281
+ />
282
+ <button
283
+ class="standard-button flex-none"
284
+ data-action="choose-create-database-path"
285
+ type="button"
286
+ >
287
+ <span class="material-symbols-outlined text-sm">folder_open</span>
288
+ <span data-create-database-path-button-label>Browse...</span>
289
+ </button>
290
+ </span>
291
+ <span class="block text-[11px] leading-5 text-on-surface-variant/60">
292
+ Choose a folder and filename, or enter an absolute path manually.
293
+ </span>
294
+ </label>
274
295
  ${renderField({
275
296
  label: "Label",
276
297
  name: "label",
277
298
  placeholder: "Optional display name",
278
299
  })}
279
300
  ${renderError(modal.error)}
280
- <div class="flex items-center justify-end gap-3 pt-2">
301
+ <div class="flex items-center justify-between gap-3 pt-2">
281
302
  <button
282
303
  class="standard-button"
283
304
  data-action="close-modal"
@@ -363,7 +384,7 @@ function renderImportSqlForm(modal, state) {
363
384
  explicit path instead of file upload.
364
385
  </p>
365
386
  ${renderError(modal.error)}
366
- <div class="flex items-center justify-end gap-3 pt-2">
387
+ <div class="flex items-center justify-between gap-3 pt-2">
367
388
  <button
368
389
  class="standard-button"
369
390
  data-action="close-modal"
@@ -423,7 +444,7 @@ function renderDeleteRowConfirmForm(modal) {
423
444
  rowPreviewMarkup,
424
445
  "</div>",
425
446
  renderError(modal.error),
426
- '<div class="flex items-center justify-end gap-3 pt-2">',
447
+ '<div class="flex items-center justify-between gap-3 pt-2">',
427
448
  '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
428
449
  '<button class="delete-button" type="submit">',
429
450
  modal.submitting ? "Deleting..." : "Delete Row",
@@ -513,7 +534,7 @@ function renderRowUpdatePreviewForm(modal) {
513
534
  </div>
514
535
  ${paramsMarkup}
515
536
  ${renderError(modal.error)}
516
- <div class="flex items-center justify-end gap-3 pt-2">
537
+ <div class="flex items-center justify-between gap-3 pt-2">
517
538
  <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
518
539
  <button class="signature-button" type="submit">
519
540
  ${modal.submitting ? 'Applying...' : 'Apply Changes'}
@@ -753,7 +774,7 @@ function renderChartEditorForm(modal, state) {
753
774
  : ""
754
775
  }
755
776
  ${renderError(modal.error)}
756
- <div class="flex items-center justify-end gap-3 pt-2">
777
+ <div class="flex items-center justify-between gap-3 pt-2">
757
778
  <button
758
779
  class="standard-button"
759
780
  data-action="close-modal"
@@ -781,7 +802,7 @@ function renderDeleteChartForm(modal) {
781
802
  '<p class="text-sm leading-7 text-on-surface-variant/65">The linked query-history entry stays intact. Only this chart definition is removed.</p>',
782
803
  "</div>",
783
804
  renderError(modal.error),
784
- '<div class="flex items-center justify-end gap-3 pt-2">',
805
+ '<div class="flex items-center justify-between gap-3 pt-2">',
785
806
  '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
786
807
  '<button class="delete-button" type="submit">',
787
808
  modal.submitting ? "Deleting..." : "Delete Chart",
@@ -798,7 +819,7 @@ function renderDeleteQueryHistoryForm(modal) {
798
819
  '<p class="text-sm leading-7 text-on-surface-variant/65">This removes the query-history entry and all recorded runs linked to it.</p>',
799
820
  "</div>",
800
821
  renderError(modal.error),
801
- '<div class="flex items-center justify-end gap-3 pt-2">',
822
+ '<div class="flex items-center justify-between gap-3 pt-2">',
802
823
  '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
803
824
  '<button class="delete-button" type="submit">',
804
825
  modal.submitting ? "Deleting..." : "Delete Query",
@@ -806,6 +827,147 @@ function renderDeleteQueryHistoryForm(modal) {
806
827
  ].join("");
807
828
  }
808
829
 
830
+ function renderDeleteDocumentForm(modal) {
831
+ return [
832
+ '<form class="space-y-5" data-form="delete-document-confirm"><div class="space-y-3">',
833
+ '<p class="text-sm leading-7 text-on-surface">Delete document <span class="font-bold text-primary-container">',
834
+ escapeHtml(modal.filename ?? "document"),
835
+ "</span>?</p>",
836
+ '<p class="text-sm leading-7 text-on-surface-variant/65">This removes the Markdown document from the active database document folder.</p>',
837
+ '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">',
838
+ formatNumber(modal.contentLength ?? 0),
839
+ " chars</div>",
840
+ "</div>",
841
+ renderError(modal.error),
842
+ '<div class="flex items-center justify-between gap-3 pt-2">',
843
+ '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
844
+ '<button class="delete-button" type="submit">',
845
+ modal.submitting ? "Deleting..." : "Delete Document",
846
+ "</button></div></form>",
847
+ ].join("");
848
+ }
849
+
850
+ function getDocumentInsertQueryTitle(query) {
851
+ return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || "Saved query";
852
+ }
853
+
854
+ function getSelectedDocumentInsertQuery(modal) {
855
+ const selectedHistoryId = String(modal.selectedHistoryId ?? "");
856
+
857
+ return (modal.queries ?? []).find((query) => String(query.id) === selectedHistoryId) ?? null;
858
+ }
859
+
860
+ function renderDocumentInsertQuerySelect(modal, emptyText) {
861
+ const queries = modal.queries ?? [];
862
+
863
+ if (modal.loading) {
864
+ return '<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface-variant/65">Loading saved queries...</div>';
865
+ }
866
+
867
+ if (!queries.length) {
868
+ return `<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface-variant/65">${escapeHtml(emptyText)}</div>`;
869
+ }
870
+
871
+ return `
872
+ <label class="block space-y-2">
873
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
874
+ Saved Query
875
+ </span>
876
+ <select
877
+ class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
878
+ data-bind="document-insert-query-select"
879
+ name="historyId"
880
+ >
881
+ ${queries
882
+ .map(
883
+ (query) => `
884
+ <option
885
+ value="${escapeHtml(query.id)}"
886
+ ${String(query.id) === String(modal.selectedHistoryId) ? "selected" : ""}
887
+ >
888
+ ${escapeHtml(getDocumentInsertQueryTitle(query))}
889
+ </option>
890
+ `
891
+ )
892
+ .join("")}
893
+ </select>
894
+ </label>
895
+ `;
896
+ }
897
+
898
+ function renderDocumentInsertQueryPreview(query) {
899
+ if (!query) {
900
+ return "";
901
+ }
902
+
903
+ return `
904
+ <div class="space-y-2">
905
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
906
+ Query Preview
907
+ </div>
908
+ <pre class="max-h-44 overflow-auto border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 font-mono text-xs leading-6 text-on-surface-variant/75 custom-scrollbar">${escapeHtml(
909
+ query.rawSql || query.previewSql || ""
910
+ )}</pre>
911
+ </div>
912
+ `;
913
+ }
914
+
915
+ function renderDocumentInsertTableForm(modal) {
916
+ const selectedQuery = getSelectedDocumentInsertQuery(modal);
917
+ const disabledAttribute = modal.loading || modal.submitting || !(modal.queries ?? []).length
918
+ ? 'disabled aria-disabled="true"'
919
+ : "";
920
+
921
+ return `
922
+ <form class="space-y-5" data-form="document-insert-table">
923
+ ${renderDocumentInsertQuerySelect(modal, "No saved queries are available for this database.")}
924
+ ${renderDocumentInsertQueryPreview(selectedQuery)}
925
+ ${renderError(modal.error)}
926
+ <div class="flex items-center justify-between gap-3 pt-2">
927
+ <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
928
+ <button class="signature-button" type="submit" ${disabledAttribute}>
929
+ ${modal.submitting ? "Inserting..." : "Insert Table"}
930
+ </button>
931
+ </div>
932
+ </form>
933
+ `;
934
+ }
935
+
936
+ function renderDocumentInsertNoteForm(modal) {
937
+ const selectedQuery = getSelectedDocumentInsertQuery(modal);
938
+ const note = String(selectedQuery?.notes ?? "").trim();
939
+ const disabledAttribute = modal.loading || modal.submitting || !(modal.queries ?? []).length
940
+ ? 'disabled aria-disabled="true"'
941
+ : "";
942
+
943
+ return `
944
+ <form class="space-y-5" data-form="document-insert-note">
945
+ ${renderDocumentInsertQuerySelect(modal, "No saved queries with notes are available for this database.")}
946
+ ${
947
+ note
948
+ ? `
949
+ <div class="space-y-2">
950
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
951
+ Note Preview
952
+ </div>
953
+ <pre class="max-h-64 overflow-auto whitespace-pre-wrap border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm leading-6 text-on-surface custom-scrollbar">${escapeHtml(
954
+ note
955
+ )}</pre>
956
+ </div>
957
+ `
958
+ : ""
959
+ }
960
+ ${renderError(modal.error)}
961
+ <div class="flex items-center justify-between gap-3 pt-2">
962
+ <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
963
+ <button class="signature-button" type="submit" ${disabledAttribute}>
964
+ ${modal.submitting ? "Inserting..." : "Insert Note"}
965
+ </button>
966
+ </div>
967
+ </form>
968
+ `;
969
+ }
970
+
809
971
  function renderQueryExportPreview(lines = []) {
810
972
  return lines.map((line) => `<span class="block whitespace-pre">${escapeHtml(line)}</span>`).join("");
811
973
  }
@@ -888,7 +1050,7 @@ function renderTextExportModal(modal, action) {
888
1050
  .join("")}
889
1051
  </div>
890
1052
  ${renderError(modal.error)}
891
- <div class="flex justify-end">
1053
+ <div class="flex justify-start">
892
1054
  <button class="standard-button" data-action="close-modal" type="button">
893
1055
  Cancel
894
1056
  </button>
@@ -911,23 +1073,47 @@ function getCopyColumnResult(state, modal) {
911
1073
 
912
1074
  function renderCopyColumnPreview(modal, state) {
913
1075
  const result = getCopyColumnResult(state, modal);
1076
+ const isMarkdownTodo = isMarkdownTodoCopyColumnMode(modal.copyMode);
914
1077
  const separator = Boolean(modal.lineBreaks) && !isMarkdownTodoCopyColumnMode(modal.copyMode)
915
1078
  ? "\n"
916
1079
  : String(modal.separator ?? ",");
917
1080
  const wrapper = String(modal.wrapper ?? '"');
918
- const preview = buildCopyColumnPreviewText({
919
- result,
920
- columnName: modal.columnName,
921
- copyMode: modal.copyMode,
922
- separator,
923
- wrapper,
924
- maxRows: isMarkdownTodoCopyColumnMode(modal.copyMode) ? 10 : 4,
925
- });
926
-
927
- if (!preview) {
1081
+ const preview = isMarkdownTodo
1082
+ ? modal.editedText ?? buildCopyColumnText({
1083
+ result,
1084
+ columnName: modal.columnName,
1085
+ copyMode: modal.copyMode,
1086
+ separator: "\n",
1087
+ wrapper,
1088
+ }).text
1089
+ : buildCopyColumnPreviewText({
1090
+ result,
1091
+ columnName: modal.columnName,
1092
+ copyMode: modal.copyMode,
1093
+ separator,
1094
+ wrapper,
1095
+ maxRows: 4,
1096
+ });
1097
+
1098
+ if (!preview && !isMarkdownTodo) {
928
1099
  return "";
929
1100
  }
930
1101
 
1102
+ if (isMarkdownTodo) {
1103
+ return `
1104
+ <label class="block space-y-2">
1105
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1106
+ Editable Preview
1107
+ </span>
1108
+ <textarea
1109
+ class="copy-column-preview copy-column-preview--editable custom-scrollbar"
1110
+ name="editedText"
1111
+ spellcheck="true"
1112
+ >${escapeHtml(preview)}</textarea>
1113
+ </label>
1114
+ `;
1115
+ }
1116
+
931
1117
  return `
932
1118
  <div class="space-y-2">
933
1119
  <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
@@ -977,7 +1163,7 @@ function renderCopyColumnFormatField({ label, name, value = "" }) {
977
1163
  `;
978
1164
  }
979
1165
 
980
- function renderCopyColumnModal(modal, state) {
1166
+ export function renderCopyColumnModal(modal, state) {
981
1167
  const result = getCopyColumnResult(state, modal);
982
1168
  const rows = result?.rows ?? [];
983
1169
  const valueCount = modal.copyMode === "first-10" ? Math.min(rows.length, 10) : rows.length;
@@ -1029,7 +1215,7 @@ function renderCopyColumnModal(modal, state) {
1029
1215
  ${formatFieldsMarkup}
1030
1216
  ${renderCopyColumnPreview(modal, state)}
1031
1217
  ${renderError(modal.error)}
1032
- <div class="flex flex-wrap items-center justify-end gap-3 pt-2">
1218
+ <div class="flex flex-wrap items-center justify-between gap-3 pt-2">
1033
1219
  <button
1034
1220
  class="standard-button"
1035
1221
  data-action="close-modal"
@@ -1038,24 +1224,41 @@ function renderCopyColumnModal(modal, state) {
1038
1224
  >
1039
1225
  Cancel
1040
1226
  </button>
1041
- <button
1042
- class="standard-button"
1043
- name="intent"
1044
- type="submit"
1045
- value="export"
1046
- ${disabledAttribute}
1047
- >
1048
- ${modal.submitting ? "Working..." : `Export as ${exportMetadata.extension.toUpperCase()}`}
1049
- </button>
1050
- <button
1051
- class="signature-button"
1052
- name="intent"
1053
- type="submit"
1054
- value="copy"
1055
- ${disabledAttribute}
1056
- >
1057
- ${modal.submitting ? "Working..." : "Copy"}
1058
- </button>
1227
+ <div class="flex flex-wrap items-center justify-end gap-3">
1228
+ <button
1229
+ class="standard-button"
1230
+ name="intent"
1231
+ type="submit"
1232
+ value="export"
1233
+ ${disabledAttribute}
1234
+ >
1235
+ ${modal.submitting ? "Working..." : `Export as ${exportMetadata.extension.toUpperCase()}`}
1236
+ </button>
1237
+ ${
1238
+ isMarkdownTodo
1239
+ ? `
1240
+ <button
1241
+ class="standard-button"
1242
+ name="intent"
1243
+ type="submit"
1244
+ value="document"
1245
+ ${disabledAttribute}
1246
+ >
1247
+ ${modal.submitting ? "Working..." : "Export to document folder"}
1248
+ </button>
1249
+ `
1250
+ : ""
1251
+ }
1252
+ <button
1253
+ class="signature-button"
1254
+ name="intent"
1255
+ type="submit"
1256
+ value="copy"
1257
+ ${disabledAttribute}
1258
+ >
1259
+ ${modal.submitting ? "Working..." : "Copy"}
1260
+ </button>
1261
+ </div>
1059
1262
  </div>
1060
1263
  </form>
1061
1264
  `;
@@ -1230,7 +1433,7 @@ function renderTableDesignerConstraintsModal(modal, state) {
1230
1433
  <div class="table-designer-constraints-modal__empty">
1231
1434
  No active table designer draft.
1232
1435
  </div>
1233
- <div class="flex items-center justify-end gap-3 pt-2">
1436
+ <div class="flex items-center justify-between gap-3 pt-2">
1234
1437
  <button class="standard-button" data-action="close-modal" type="button">Close</button>
1235
1438
  </div>
1236
1439
  `;
@@ -1291,7 +1494,7 @@ function renderTableDesignerConstraintsModal(modal, state) {
1291
1494
  : "No multi-column or partial unique constraints detected.",
1292
1495
  body: uniqueConstraints.map(renderTableDesignerUniqueConstraintEditor).join(""),
1293
1496
  })}
1294
- <div class="flex items-center justify-end gap-3 pt-2">
1497
+ <div class="flex items-center justify-between gap-3 pt-2">
1295
1498
  <button class="standard-button" data-action="close-modal" type="button">Close</button>
1296
1499
  </div>
1297
1500
  </div>
@@ -1336,7 +1539,7 @@ function renderCreateMediaTaggingMappingTableForm(modal, state) {
1336
1539
  ${renderSqlPreviewField(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE_SQL)}
1337
1540
  </div>
1338
1541
  ${renderError(modal.error)}
1339
- <div class="flex items-center justify-end gap-3 pt-2">
1542
+ <div class="flex items-center justify-between gap-3 pt-2">
1340
1543
  <button
1341
1544
  class="standard-button"
1342
1545
  data-action="close-modal"
@@ -1400,7 +1603,7 @@ function renderCreateMediaTaggingTagTableForm(modal, state) {
1400
1603
  ${renderSqlPreviewField(MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL)}
1401
1604
  </div>
1402
1605
  ${renderError(modal.error)}
1403
- <div class="flex items-center justify-end gap-3 pt-2">
1606
+ <div class="flex items-center justify-between gap-3 pt-2">
1404
1607
  <button
1405
1608
  class="standard-button"
1406
1609
  data-action="close-modal"
@@ -1479,6 +1682,21 @@ export function renderModal(state) {
1479
1682
  title: "Delete Query",
1480
1683
  body: renderDeleteQueryHistoryForm(modal),
1481
1684
  },
1685
+ "delete-document": {
1686
+ eyebrow: "Documents // Confirm deletion",
1687
+ title: "Delete Document",
1688
+ body: renderDeleteDocumentForm(modal),
1689
+ },
1690
+ "document-insert-table": {
1691
+ eyebrow: "Documents // Saved query output",
1692
+ title: "Insert Table",
1693
+ body: renderDocumentInsertTableForm(modal),
1694
+ },
1695
+ "document-insert-note": {
1696
+ eyebrow: "Documents // Saved query notes",
1697
+ title: "Insert Note",
1698
+ body: renderDocumentInsertNoteForm(modal),
1699
+ },
1482
1700
  "query-export": {
1483
1701
  eyebrow: "SQL Editor // Export query result",
1484
1702
  title: "Export Query",
@@ -1521,6 +1739,8 @@ export function renderModal(state) {
1521
1739
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
1522
1740
  <div class="w-full ${
1523
1741
  modal.kind === "chart-editor" ||
1742
+ modal.kind === "document-insert-table" ||
1743
+ modal.kind === "document-insert-note" ||
1524
1744
  modal.kind === "row-update-preview" ||
1525
1745
  modal.kind === "table-designer-constraints"
1526
1746
  ? "max-w-3xl"
@@ -1,33 +1,31 @@
1
- import { escapeHtml } from "../utils/format.js";
1
+ import { escapeHtml } from '../utils/format.js';
2
2
 
3
- export function renderPageHeader({ eyebrow = "", title, subtitle = "", actions = "" }) {
4
- return `
3
+ export function renderPageHeader({ eyebrow = '', title, subtitle = '', actions = '' }) {
4
+ return `
5
5
  <div class="mb-10 flex flex-wrap items-end justify-between gap-6">
6
6
  <div>
7
7
  ${
8
- eyebrow
9
- ? `
8
+ eyebrow
9
+ ? `
10
10
  <div class="page-eyebrow">
11
11
  <span class="text-[#FCE300] text-xs font-mono font-bold tracking-widest uppercase">${escapeHtml(
12
- eyebrow
12
+ eyebrow,
13
13
  )}</span>
14
14
  <div class="page-eyebrow-line"></div>
15
15
  </div>
16
16
  `
17
- : ""
17
+ : ''
18
18
  }
19
- <h1 class="text-5xl font-headline font-bold text-[#FCE300] tracking-tighter uppercase">${escapeHtml(
20
- title
21
- )}</h1>
19
+ <h1 class="text-5xl font-headline font-bold text-[#FCE300] tracking-tighter uppercase">${escapeHtml(title)}</h1>
22
20
  ${
23
- subtitle
24
- ? `<p class="text-xs font-mono text-on-surface/40 mt-1 uppercase tracking-widest">${escapeHtml(
25
- subtitle
26
- )}</p>`
27
- : ""
21
+ subtitle
22
+ ? `<p class="text-xs font-mono text-on-surface/40 mt-1 uppercase tracking-widest">${escapeHtml(
23
+ subtitle,
24
+ )}</p>`
25
+ : ''
28
26
  }
29
27
  </div>
30
- ${actions ? `<div class="flex gap-3">${actions}</div>` : ""}
28
+ ${actions ? `<div class="flex gap-3">${actions}</div>` : ''}
31
29
  </div>
32
30
  `;
33
31
  }