sqlite-hub 0.12.0 → 0.17.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 (64) hide show
  1. package/README.md +118 -23
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +32 -2
  5. package/frontend/js/app.js +989 -13
  6. package/frontend/js/components/modal.js +644 -15
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryEditor.js +9 -1
  9. package/frontend/js/components/queryHistoryPanel.js +126 -131
  10. package/frontend/js/components/queryResults.js +79 -17
  11. package/frontend/js/components/rowEditorPanel.js +204 -10
  12. package/frontend/js/components/sidebar.js +102 -18
  13. package/frontend/js/components/tableDesignerEditor.js +69 -11
  14. package/frontend/js/router.js +8 -0
  15. package/frontend/js/store.js +868 -9
  16. package/frontend/js/utils/copyColumnExport.js +117 -0
  17. package/frontend/js/utils/exportFilenames.js +32 -0
  18. package/frontend/js/utils/filePathPreview.js +315 -0
  19. package/frontend/js/utils/format.js +1 -1
  20. package/frontend/js/utils/markdownDocuments.js +248 -0
  21. package/frontend/js/utils/rowEditorJson.js +65 -0
  22. package/frontend/js/utils/sqlFormatter.js +691 -0
  23. package/frontend/js/utils/tableDesigner.js +178 -6
  24. package/frontend/js/utils/textCellStats.js +20 -0
  25. package/frontend/js/utils/timestampPreview.js +264 -0
  26. package/frontend/js/views/charts.js +3 -1
  27. package/frontend/js/views/data.js +34 -2
  28. package/frontend/js/views/documents.js +300 -0
  29. package/frontend/js/views/editor.js +48 -1
  30. package/frontend/js/views/settings.js +39 -6
  31. package/frontend/js/views/structure.js +154 -212
  32. package/frontend/styles/base.css +6 -0
  33. package/frontend/styles/components.css +476 -2
  34. package/frontend/styles/structure-graph.css +0 -3
  35. package/frontend/styles/tailwind.generated.css +1 -1
  36. package/frontend/styles/tokens.css +1 -1
  37. package/frontend/styles/views.css +422 -0
  38. package/package.json +3 -3
  39. package/server/routes/documents.js +163 -0
  40. package/server/routes/settings.js +22 -6
  41. package/server/server.js +6 -0
  42. package/server/services/sqlite/introspection.js +10 -0
  43. package/server/services/sqlite/sqlExecutor.js +29 -0
  44. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  45. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  46. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  47. package/server/services/sqlite/tableDesignerService.js +1 -1
  48. package/server/services/storage/appStateStore.js +313 -0
  49. package/tests/check-constraint-options.test.js +14 -0
  50. package/tests/cli-args.test.js +100 -0
  51. package/tests/copy-column-modal.test.js +83 -0
  52. package/tests/database-documents.test.js +85 -0
  53. package/tests/export-filenames.test.js +34 -0
  54. package/tests/file-path-preview.test.js +165 -0
  55. package/tests/markdown-documents.test.js +79 -0
  56. package/tests/row-editor-json.test.js +82 -0
  57. package/tests/row-editor-timestamp-preview.test.js +192 -0
  58. package/tests/settings-metadata.test.js +16 -0
  59. package/tests/settings-view.test.js +32 -0
  60. package/tests/sql-formatter.test.js +173 -0
  61. package/tests/sql-highlight.test.js +38 -0
  62. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  63. package/tests/text-cell-stats.test.js +38 -0
  64. package/fill.js +0 -526
@@ -1,4 +1,11 @@
1
- import { escapeHtml, highlightSql, truncateMiddle } from "../utils/format.js";
1
+ import { escapeHtml, formatNumber, highlightSql, truncateMiddle } from "../utils/format.js";
2
+ import {
3
+ buildCopyColumnText,
4
+ buildCopyColumnPreviewText,
5
+ getCopyColumnActionLabel,
6
+ getCopyColumnExportMetadata,
7
+ isMarkdownTodoCopyColumnMode,
8
+ } from "../utils/copyColumnExport.js";
2
9
  import { renderConnectionLogo } from "./connectionLogo.js";
3
10
  import {
4
11
  analyzeQueryChartResult,
@@ -164,7 +171,7 @@ function renderOpenConnectionForm(modal) {
164
171
  text: "Open read-only",
165
172
  })}
166
173
  ${renderError(modal.error)}
167
- <div class="flex items-center justify-end gap-3 pt-2">
174
+ <div class="flex items-center justify-between gap-3 pt-2">
168
175
  <button
169
176
  class="standard-button"
170
177
  data-action="close-modal"
@@ -238,7 +245,7 @@ function renderEditConnectionForm(modal) {
238
245
  text: "Open read-only",
239
246
  })}
240
247
  ${renderError(modal.error)}
241
- <div class="flex items-center justify-end gap-3 pt-2">
248
+ <div class="flex items-center justify-between gap-3 pt-2">
242
249
  <button
243
250
  class="standard-button"
244
251
  data-action="close-modal"
@@ -271,7 +278,7 @@ function renderCreateDatabaseForm(modal) {
271
278
  placeholder: "Optional display name",
272
279
  })}
273
280
  ${renderError(modal.error)}
274
- <div class="flex items-center justify-end gap-3 pt-2">
281
+ <div class="flex items-center justify-between gap-3 pt-2">
275
282
  <button
276
283
  class="standard-button"
277
284
  data-action="close-modal"
@@ -357,7 +364,7 @@ function renderImportSqlForm(modal, state) {
357
364
  explicit path instead of file upload.
358
365
  </p>
359
366
  ${renderError(modal.error)}
360
- <div class="flex items-center justify-end gap-3 pt-2">
367
+ <div class="flex items-center justify-between gap-3 pt-2">
361
368
  <button
362
369
  class="standard-button"
363
370
  data-action="close-modal"
@@ -417,7 +424,7 @@ function renderDeleteRowConfirmForm(modal) {
417
424
  rowPreviewMarkup,
418
425
  "</div>",
419
426
  renderError(modal.error),
420
- '<div class="flex items-center justify-end gap-3 pt-2">',
427
+ '<div class="flex items-center justify-between gap-3 pt-2">',
421
428
  '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
422
429
  '<button class="delete-button" type="submit">',
423
430
  modal.submitting ? "Deleting..." : "Delete Row",
@@ -507,7 +514,7 @@ function renderRowUpdatePreviewForm(modal) {
507
514
  </div>
508
515
  ${paramsMarkup}
509
516
  ${renderError(modal.error)}
510
- <div class="flex items-center justify-end gap-3 pt-2">
517
+ <div class="flex items-center justify-between gap-3 pt-2">
511
518
  <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
512
519
  <button class="signature-button" type="submit">
513
520
  ${modal.submitting ? 'Applying...' : 'Apply Changes'}
@@ -747,7 +754,7 @@ function renderChartEditorForm(modal, state) {
747
754
  : ""
748
755
  }
749
756
  ${renderError(modal.error)}
750
- <div class="flex items-center justify-end gap-3 pt-2">
757
+ <div class="flex items-center justify-between gap-3 pt-2">
751
758
  <button
752
759
  class="standard-button"
753
760
  data-action="close-modal"
@@ -775,7 +782,7 @@ function renderDeleteChartForm(modal) {
775
782
  '<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>',
776
783
  "</div>",
777
784
  renderError(modal.error),
778
- '<div class="flex items-center justify-end gap-3 pt-2">',
785
+ '<div class="flex items-center justify-between gap-3 pt-2">',
779
786
  '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
780
787
  '<button class="delete-button" type="submit">',
781
788
  modal.submitting ? "Deleting..." : "Delete Chart",
@@ -792,7 +799,7 @@ function renderDeleteQueryHistoryForm(modal) {
792
799
  '<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>',
793
800
  "</div>",
794
801
  renderError(modal.error),
795
- '<div class="flex items-center justify-end gap-3 pt-2">',
802
+ '<div class="flex items-center justify-between gap-3 pt-2">',
796
803
  '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
797
804
  '<button class="delete-button" type="submit">',
798
805
  modal.submitting ? "Deleting..." : "Delete Query",
@@ -800,6 +807,147 @@ function renderDeleteQueryHistoryForm(modal) {
800
807
  ].join("");
801
808
  }
802
809
 
810
+ function renderDeleteDocumentForm(modal) {
811
+ return [
812
+ '<form class="space-y-5" data-form="delete-document-confirm"><div class="space-y-3">',
813
+ '<p class="text-sm leading-7 text-on-surface">Delete document <span class="font-bold text-primary-container">',
814
+ escapeHtml(modal.filename ?? "document"),
815
+ "</span>?</p>",
816
+ '<p class="text-sm leading-7 text-on-surface-variant/65">This removes the Markdown document from the active database document folder.</p>',
817
+ '<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">',
818
+ formatNumber(modal.contentLength ?? 0),
819
+ " chars</div>",
820
+ "</div>",
821
+ renderError(modal.error),
822
+ '<div class="flex items-center justify-between gap-3 pt-2">',
823
+ '<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
824
+ '<button class="delete-button" type="submit">',
825
+ modal.submitting ? "Deleting..." : "Delete Document",
826
+ "</button></div></form>",
827
+ ].join("");
828
+ }
829
+
830
+ function getDocumentInsertQueryTitle(query) {
831
+ return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || "Saved query";
832
+ }
833
+
834
+ function getSelectedDocumentInsertQuery(modal) {
835
+ const selectedHistoryId = String(modal.selectedHistoryId ?? "");
836
+
837
+ return (modal.queries ?? []).find((query) => String(query.id) === selectedHistoryId) ?? null;
838
+ }
839
+
840
+ function renderDocumentInsertQuerySelect(modal, emptyText) {
841
+ const queries = modal.queries ?? [];
842
+
843
+ if (modal.loading) {
844
+ 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>';
845
+ }
846
+
847
+ if (!queries.length) {
848
+ 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>`;
849
+ }
850
+
851
+ return `
852
+ <label class="block space-y-2">
853
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
854
+ Saved Query
855
+ </span>
856
+ <select
857
+ 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"
858
+ data-bind="document-insert-query-select"
859
+ name="historyId"
860
+ >
861
+ ${queries
862
+ .map(
863
+ (query) => `
864
+ <option
865
+ value="${escapeHtml(query.id)}"
866
+ ${String(query.id) === String(modal.selectedHistoryId) ? "selected" : ""}
867
+ >
868
+ ${escapeHtml(getDocumentInsertQueryTitle(query))}
869
+ </option>
870
+ `
871
+ )
872
+ .join("")}
873
+ </select>
874
+ </label>
875
+ `;
876
+ }
877
+
878
+ function renderDocumentInsertQueryPreview(query) {
879
+ if (!query) {
880
+ return "";
881
+ }
882
+
883
+ return `
884
+ <div class="space-y-2">
885
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
886
+ Query Preview
887
+ </div>
888
+ <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(
889
+ query.rawSql || query.previewSql || ""
890
+ )}</pre>
891
+ </div>
892
+ `;
893
+ }
894
+
895
+ function renderDocumentInsertTableForm(modal) {
896
+ const selectedQuery = getSelectedDocumentInsertQuery(modal);
897
+ const disabledAttribute = modal.loading || modal.submitting || !(modal.queries ?? []).length
898
+ ? 'disabled aria-disabled="true"'
899
+ : "";
900
+
901
+ return `
902
+ <form class="space-y-5" data-form="document-insert-table">
903
+ ${renderDocumentInsertQuerySelect(modal, "No saved queries are available for this database.")}
904
+ ${renderDocumentInsertQueryPreview(selectedQuery)}
905
+ ${renderError(modal.error)}
906
+ <div class="flex items-center justify-between gap-3 pt-2">
907
+ <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
908
+ <button class="signature-button" type="submit" ${disabledAttribute}>
909
+ ${modal.submitting ? "Inserting..." : "Insert Table"}
910
+ </button>
911
+ </div>
912
+ </form>
913
+ `;
914
+ }
915
+
916
+ function renderDocumentInsertNoteForm(modal) {
917
+ const selectedQuery = getSelectedDocumentInsertQuery(modal);
918
+ const note = String(selectedQuery?.notes ?? "").trim();
919
+ const disabledAttribute = modal.loading || modal.submitting || !(modal.queries ?? []).length
920
+ ? 'disabled aria-disabled="true"'
921
+ : "";
922
+
923
+ return `
924
+ <form class="space-y-5" data-form="document-insert-note">
925
+ ${renderDocumentInsertQuerySelect(modal, "No saved queries with notes are available for this database.")}
926
+ ${
927
+ note
928
+ ? `
929
+ <div class="space-y-2">
930
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
931
+ Note Preview
932
+ </div>
933
+ <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(
934
+ note
935
+ )}</pre>
936
+ </div>
937
+ `
938
+ : ""
939
+ }
940
+ ${renderError(modal.error)}
941
+ <div class="flex items-center justify-between gap-3 pt-2">
942
+ <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
943
+ <button class="signature-button" type="submit" ${disabledAttribute}>
944
+ ${modal.submitting ? "Inserting..." : "Insert Note"}
945
+ </button>
946
+ </div>
947
+ </form>
948
+ `;
949
+ }
950
+
803
951
  function renderQueryExportPreview(lines = []) {
804
952
  return lines.map((line) => `<span class="block whitespace-pre">${escapeHtml(line)}</span>`).join("");
805
953
  }
@@ -829,7 +977,21 @@ function renderTextExportModal(modal, action) {
829
977
  const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : "";
830
978
 
831
979
  return `
832
- <div class="space-y-5">
980
+ <div class="space-y-5" data-export-modal>
981
+ <label class="block space-y-2">
982
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
983
+ Filename
984
+ </span>
985
+ <input
986
+ autocomplete="off"
987
+ class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
988
+ name="filename"
989
+ spellcheck="false"
990
+ type="text"
991
+ value="${escapeHtml(modal.filename ?? "")}"
992
+ ${disabledAttribute}
993
+ />
994
+ </label>
833
995
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
834
996
  ${getExportOptions()
835
997
  .map(
@@ -868,7 +1030,7 @@ function renderTextExportModal(modal, action) {
868
1030
  .join("")}
869
1031
  </div>
870
1032
  ${renderError(modal.error)}
871
- <div class="flex justify-end">
1033
+ <div class="flex justify-start">
872
1034
  <button class="standard-button" data-action="close-modal" type="button">
873
1035
  Cancel
874
1036
  </button>
@@ -885,6 +1047,440 @@ function renderDataExportModal(modal) {
885
1047
  return renderTextExportModal(modal, "export-data-format");
886
1048
  }
887
1049
 
1050
+ function getCopyColumnResult(state, modal) {
1051
+ return modal.scope === "charts" ? state.charts.result : state.editor.result;
1052
+ }
1053
+
1054
+ function renderCopyColumnPreview(modal, state) {
1055
+ const result = getCopyColumnResult(state, modal);
1056
+ const isMarkdownTodo = isMarkdownTodoCopyColumnMode(modal.copyMode);
1057
+ const separator = Boolean(modal.lineBreaks) && !isMarkdownTodoCopyColumnMode(modal.copyMode)
1058
+ ? "\n"
1059
+ : String(modal.separator ?? ",");
1060
+ const wrapper = String(modal.wrapper ?? '"');
1061
+ const preview = isMarkdownTodo
1062
+ ? modal.editedText ?? buildCopyColumnText({
1063
+ result,
1064
+ columnName: modal.columnName,
1065
+ copyMode: modal.copyMode,
1066
+ separator: "\n",
1067
+ wrapper,
1068
+ }).text
1069
+ : buildCopyColumnPreviewText({
1070
+ result,
1071
+ columnName: modal.columnName,
1072
+ copyMode: modal.copyMode,
1073
+ separator,
1074
+ wrapper,
1075
+ maxRows: 4,
1076
+ });
1077
+
1078
+ if (!preview && !isMarkdownTodo) {
1079
+ return "";
1080
+ }
1081
+
1082
+ if (isMarkdownTodo) {
1083
+ return `
1084
+ <label class="block space-y-2">
1085
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1086
+ Editable Preview
1087
+ </span>
1088
+ <textarea
1089
+ class="copy-column-preview copy-column-preview--editable custom-scrollbar"
1090
+ name="editedText"
1091
+ spellcheck="true"
1092
+ >${escapeHtml(preview)}</textarea>
1093
+ </label>
1094
+ `;
1095
+ }
1096
+
1097
+ return `
1098
+ <div class="space-y-2">
1099
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1100
+ Preview
1101
+ </div>
1102
+ <pre class="copy-column-preview custom-scrollbar">${escapeHtml(preview)}</pre>
1103
+ </div>
1104
+ `;
1105
+ }
1106
+
1107
+ function renderCopyColumnLineBreaksField({ checked = false, disabled = false } = {}) {
1108
+ return `
1109
+ <label class="block space-y-2">
1110
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1111
+ Format
1112
+ </span>
1113
+ <span class="standard-checkbox ${disabled ? "is-disabled" : ""}">
1114
+ <input
1115
+ ${checked ? "checked" : ""}
1116
+ ${disabled ? "disabled" : ""}
1117
+ data-bind="copy-column-format-field"
1118
+ data-field="lineBreaks"
1119
+ name="lineBreaks"
1120
+ type="checkbox"
1121
+ />
1122
+ <span>Line breaks</span>
1123
+ </span>
1124
+ </label>
1125
+ `;
1126
+ }
1127
+
1128
+ function renderCopyColumnFormatField({ label, name, value = "" }) {
1129
+ return `
1130
+ <label class="block space-y-2">
1131
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1132
+ ${escapeHtml(label)}
1133
+ </span>
1134
+ <input
1135
+ class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
1136
+ data-bind="copy-column-format-field"
1137
+ data-field="${escapeHtml(name)}"
1138
+ name="${escapeHtml(name)}"
1139
+ type="text"
1140
+ value="${escapeHtml(value)}"
1141
+ />
1142
+ </label>
1143
+ `;
1144
+ }
1145
+
1146
+ export function renderCopyColumnModal(modal, state) {
1147
+ const result = getCopyColumnResult(state, modal);
1148
+ const rows = result?.rows ?? [];
1149
+ const valueCount = modal.copyMode === "first-10" ? Math.min(rows.length, 10) : rows.length;
1150
+ const disabledAttribute = modal.submitting ? 'disabled aria-disabled="true"' : "";
1151
+ const exportMetadata = getCopyColumnExportMetadata(modal.copyMode);
1152
+ const isMarkdownTodo = isMarkdownTodoCopyColumnMode(modal.copyMode);
1153
+ const lineBreaks = isMarkdownTodo || Boolean(modal.lineBreaks);
1154
+ const formatFieldsMarkup = isMarkdownTodo
1155
+ ? renderCopyColumnLineBreaksField({
1156
+ checked: true,
1157
+ disabled: true,
1158
+ })
1159
+ : `
1160
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
1161
+ ${renderCopyColumnFormatField({
1162
+ label: "Separator",
1163
+ name: "separator",
1164
+ value: modal.separator ?? ",",
1165
+ })}
1166
+ ${renderCopyColumnFormatField({
1167
+ label: "Wrapper",
1168
+ name: "wrapper",
1169
+ value: modal.wrapper ?? '"',
1170
+ })}
1171
+ ${renderCopyColumnLineBreaksField({
1172
+ checked: lineBreaks,
1173
+ })}
1174
+ </div>
1175
+ `;
1176
+
1177
+ return `
1178
+ <form class="space-y-5" data-form="copy-column">
1179
+ <input name="scope" type="hidden" value="${escapeHtml(modal.scope ?? "editor")}" />
1180
+ <input name="columnName" type="hidden" value="${escapeHtml(modal.columnName ?? "")}" />
1181
+ <input name="copyMode" type="hidden" value="${escapeHtml(modal.copyMode ?? "column")}" />
1182
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
1183
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
1184
+ Column
1185
+ </div>
1186
+ <div class="mt-2 flex min-w-0 items-center justify-between gap-4">
1187
+ <code class="min-w-0 truncate font-mono text-sm text-primary-container" title="${escapeHtml(
1188
+ modal.columnName ?? ""
1189
+ )}">${escapeHtml(modal.columnName ?? "")}</code>
1190
+ <span class="shrink-0 font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/50">
1191
+ ${escapeHtml(getCopyColumnActionLabel(modal.copyMode))} · ${formatNumber(valueCount)}
1192
+ </span>
1193
+ </div>
1194
+ </div>
1195
+ ${formatFieldsMarkup}
1196
+ ${renderCopyColumnPreview(modal, state)}
1197
+ ${renderError(modal.error)}
1198
+ <div class="flex flex-wrap items-center justify-between gap-3 pt-2">
1199
+ <button
1200
+ class="standard-button"
1201
+ data-action="close-modal"
1202
+ type="button"
1203
+ ${disabledAttribute}
1204
+ >
1205
+ Cancel
1206
+ </button>
1207
+ <div class="flex flex-wrap items-center justify-end gap-3">
1208
+ <button
1209
+ class="standard-button"
1210
+ name="intent"
1211
+ type="submit"
1212
+ value="export"
1213
+ ${disabledAttribute}
1214
+ >
1215
+ ${modal.submitting ? "Working..." : `Export as ${exportMetadata.extension.toUpperCase()}`}
1216
+ </button>
1217
+ ${
1218
+ isMarkdownTodo
1219
+ ? `
1220
+ <button
1221
+ class="standard-button"
1222
+ name="intent"
1223
+ type="submit"
1224
+ value="document"
1225
+ ${disabledAttribute}
1226
+ >
1227
+ ${modal.submitting ? "Working..." : "Export to document folder"}
1228
+ </button>
1229
+ `
1230
+ : ""
1231
+ }
1232
+ <button
1233
+ class="signature-button"
1234
+ name="intent"
1235
+ type="submit"
1236
+ value="copy"
1237
+ ${disabledAttribute}
1238
+ >
1239
+ ${modal.submitting ? "Working..." : "Copy"}
1240
+ </button>
1241
+ </div>
1242
+ </div>
1243
+ </form>
1244
+ `;
1245
+ }
1246
+
1247
+ function getTableDesignerUniqueConstraintTypeLabel(constraint) {
1248
+ if (constraint.partial) {
1249
+ return "Partial unique index";
1250
+ }
1251
+
1252
+ if ((constraint.columns?.length ?? 0) > 1) {
1253
+ return "Multi-column unique";
1254
+ }
1255
+
1256
+ return "Unique constraint";
1257
+ }
1258
+
1259
+ function renderTableDesignerUniqueConstraintExpression(constraint) {
1260
+ const expression = String(constraint.expression || constraint.sql || "").trim();
1261
+
1262
+ if (expression) {
1263
+ return expression;
1264
+ }
1265
+
1266
+ const columns = (constraint.columns ?? []).map((column) => column.name).filter(Boolean);
1267
+ return columns.length ? `UNIQUE (${columns.join(", ")})` : "UNIQUE constraint";
1268
+ }
1269
+
1270
+ function renderTableDesignerCheckConstraintExpression(constraint) {
1271
+ return String(constraint.expression || "").trim() || "CHECK constraint";
1272
+ }
1273
+
1274
+ function normalizeTableDesignerConstraintColumnName(name) {
1275
+ return String(name ?? "").trim().toLowerCase();
1276
+ }
1277
+
1278
+ function tableDesignerConstraintIncludesColumn(constraint, columnName) {
1279
+ const normalizedColumn = normalizeTableDesignerConstraintColumnName(columnName);
1280
+
1281
+ if (!normalizedColumn) {
1282
+ return false;
1283
+ }
1284
+
1285
+ return (constraint.columns ?? []).some(
1286
+ (constraintColumn) =>
1287
+ normalizeTableDesignerConstraintColumnName(constraintColumn.name) === normalizedColumn
1288
+ );
1289
+ }
1290
+
1291
+ function renderTableDesignerUniqueConstraintEditor(constraint) {
1292
+ const columns = (constraint.columns ?? []).map((column) => column.name).filter(Boolean);
1293
+
1294
+ return `
1295
+ <article class="table-designer-constraint-editor">
1296
+ <div class="table-designer-constraint-editor__header">
1297
+ <div class="min-w-0">
1298
+ <div class="table-designer-constraint-editor__type">
1299
+ ${escapeHtml(getTableDesignerUniqueConstraintTypeLabel(constraint))}
1300
+ ${constraint.origin ? ` // origin ${escapeHtml(constraint.origin)}` : ""}
1301
+ </div>
1302
+ <input
1303
+ class="table-designer-field table-designer-constraint-editor__name"
1304
+ data-bind="table-designer-constraint-field"
1305
+ data-constraint-id="${escapeHtml(constraint.id)}"
1306
+ data-constraint-kind="unique"
1307
+ data-field="name"
1308
+ spellcheck="false"
1309
+ type="text"
1310
+ value="${escapeHtml(constraint.name || "UNIQUE constraint")}"
1311
+ />
1312
+ </div>
1313
+ <div class="status-badge status-badge--muted">Rebuild</div>
1314
+ </div>
1315
+ ${
1316
+ columns.length
1317
+ ? `
1318
+ <div class="table-designer-constraint__columns">
1319
+ ${columns.map((column) => `<span>${escapeHtml(column)}</span>`).join("")}
1320
+ </div>
1321
+ `
1322
+ : ""
1323
+ }
1324
+ <textarea
1325
+ class="table-designer-constraint-editor__sql custom-scrollbar"
1326
+ data-bind="table-designer-constraint-field"
1327
+ data-constraint-id="${escapeHtml(constraint.id)}"
1328
+ data-constraint-kind="unique"
1329
+ data-field="expression"
1330
+ spellcheck="false"
1331
+ >${escapeHtml(renderTableDesignerUniqueConstraintExpression(constraint))}</textarea>
1332
+ </article>
1333
+ `;
1334
+ }
1335
+
1336
+ function renderTableDesignerCheckConstraintEditor(constraint) {
1337
+ const columns = constraint.columns ?? [];
1338
+ const allowedValues = columns.flatMap((column) =>
1339
+ (column.allowedValues ?? []).map((value) => ({
1340
+ columnName: column.name,
1341
+ value,
1342
+ }))
1343
+ );
1344
+
1345
+ return `
1346
+ <article class="table-designer-constraint-editor">
1347
+ <div class="table-designer-constraint-editor__header">
1348
+ <div class="min-w-0">
1349
+ <div class="table-designer-constraint-editor__type">
1350
+ ${columns.length ? escapeHtml(columns.map((column) => column.name).join(", ")) : "Table check"}
1351
+ </div>
1352
+ <input
1353
+ class="table-designer-field table-designer-constraint-editor__name"
1354
+ data-bind="table-designer-constraint-field"
1355
+ data-constraint-id="${escapeHtml(constraint.id)}"
1356
+ data-constraint-kind="check"
1357
+ data-field="name"
1358
+ spellcheck="false"
1359
+ type="text"
1360
+ value="${escapeHtml(constraint.name || "CHECK constraint")}"
1361
+ />
1362
+ </div>
1363
+ <div class="status-badge status-badge--muted">Rebuild</div>
1364
+ </div>
1365
+ ${
1366
+ allowedValues.length
1367
+ ? `
1368
+ <div class="table-designer-constraint__values custom-scrollbar">
1369
+ ${allowedValues
1370
+ .map(
1371
+ (entry) => `
1372
+ <span title="${escapeHtml(entry.columnName)}">${escapeHtml(entry.value)}</span>
1373
+ `
1374
+ )
1375
+ .join("")}
1376
+ </div>
1377
+ `
1378
+ : ""
1379
+ }
1380
+ <textarea
1381
+ class="table-designer-constraint-editor__sql custom-scrollbar"
1382
+ data-bind="table-designer-constraint-field"
1383
+ data-constraint-id="${escapeHtml(constraint.id)}"
1384
+ data-constraint-kind="check"
1385
+ data-field="expression"
1386
+ spellcheck="false"
1387
+ >${escapeHtml(renderTableDesignerCheckConstraintExpression(constraint))}</textarea>
1388
+ </article>
1389
+ `;
1390
+ }
1391
+
1392
+ function renderTableDesignerConstraintSection({ title, count, emptyText, body }) {
1393
+ return `
1394
+ <section class="table-designer-constraints-modal__section">
1395
+ <div class="table-designer-constraints-modal__section-header">
1396
+ <div class="table-designer-constraints-modal__section-title">${escapeHtml(title)}</div>
1397
+ <div class="status-badge status-badge--muted">${formatNumber(count)}</div>
1398
+ </div>
1399
+ ${
1400
+ count
1401
+ ? `<div class="table-designer-constraints__list">${body}</div>`
1402
+ : `<div class="table-designer-constraints-modal__empty">${escapeHtml(emptyText)}</div>`
1403
+ }
1404
+ </section>
1405
+ `;
1406
+ }
1407
+
1408
+ function renderTableDesignerConstraintsModal(modal, state) {
1409
+ const draft = state.tableDesigner?.draft;
1410
+
1411
+ if (!draft) {
1412
+ return `
1413
+ <div class="table-designer-constraints-modal__empty">
1414
+ No active table designer draft.
1415
+ </div>
1416
+ <div class="flex items-center justify-between gap-3 pt-2">
1417
+ <button class="standard-button" data-action="close-modal" type="button">Close</button>
1418
+ </div>
1419
+ `;
1420
+ }
1421
+
1422
+ const selectedColumn = modal.columnId
1423
+ ? (draft.columns ?? []).find((column) => column.id === modal.columnId)
1424
+ : null;
1425
+ const selectedColumnName = String(modal.columnName ?? selectedColumn?.name ?? "").trim();
1426
+ const hasSelectedColumn = Boolean(modal.columnId || selectedColumnName);
1427
+ const selectedColumnLabel = selectedColumnName || "Unnamed column";
1428
+ const allUniqueConstraints = draft.mode === "edit" ? draft.uniqueConstraints ?? [] : [];
1429
+ const allCheckConstraints = draft.mode === "edit" ? draft.checkConstraints ?? [] : [];
1430
+ const uniqueConstraints = hasSelectedColumn
1431
+ ? allUniqueConstraints.filter((constraint) =>
1432
+ tableDesignerConstraintIncludesColumn(constraint, selectedColumnName)
1433
+ )
1434
+ : allUniqueConstraints;
1435
+ const checkConstraints = hasSelectedColumn
1436
+ ? allCheckConstraints.filter((constraint) =>
1437
+ tableDesignerConstraintIncludesColumn(constraint, selectedColumnName)
1438
+ )
1439
+ : allCheckConstraints;
1440
+ const totalCount = uniqueConstraints.length + checkConstraints.length;
1441
+
1442
+ return `
1443
+ <div class="table-designer-constraints-modal">
1444
+ <div class="table-designer-constraints-modal__summary">
1445
+ <div class="min-w-0">
1446
+ <div class="table-designer-constraints-modal__label">Table</div>
1447
+ <code title="${escapeHtml(draft.tableName ?? "")}">${escapeHtml(draft.tableName ?? "")}</code>
1448
+ </div>
1449
+ ${
1450
+ hasSelectedColumn
1451
+ ? `
1452
+ <div class="min-w-0">
1453
+ <div class="table-designer-constraints-modal__label">Column</div>
1454
+ <code title="${escapeHtml(selectedColumnLabel)}">${escapeHtml(selectedColumnLabel)}</code>
1455
+ </div>
1456
+ `
1457
+ : ""
1458
+ }
1459
+ <div class="status-badge status-badge--primary">V2 · ${formatNumber(totalCount)}</div>
1460
+ </div>
1461
+ ${renderTableDesignerConstraintSection({
1462
+ title: "Check constraints",
1463
+ count: checkConstraints.length,
1464
+ emptyText: hasSelectedColumn
1465
+ ? "No check constraints detected for this column."
1466
+ : "No check constraints detected.",
1467
+ body: checkConstraints.map(renderTableDesignerCheckConstraintEditor).join(""),
1468
+ })}
1469
+ ${renderTableDesignerConstraintSection({
1470
+ title: hasSelectedColumn ? "Related unique constraints" : "Unique constraints",
1471
+ count: uniqueConstraints.length,
1472
+ emptyText: hasSelectedColumn
1473
+ ? "No related multi-column or partial unique constraints detected."
1474
+ : "No multi-column or partial unique constraints detected.",
1475
+ body: uniqueConstraints.map(renderTableDesignerUniqueConstraintEditor).join(""),
1476
+ })}
1477
+ <div class="flex items-center justify-between gap-3 pt-2">
1478
+ <button class="standard-button" data-action="close-modal" type="button">Close</button>
1479
+ </div>
1480
+ </div>
1481
+ `;
1482
+ }
1483
+
888
1484
  function renderCreateMediaTaggingMappingTableForm(modal, state) {
889
1485
  const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
890
1486
  const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
@@ -923,7 +1519,7 @@ function renderCreateMediaTaggingMappingTableForm(modal, state) {
923
1519
  ${renderSqlPreviewField(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE_SQL)}
924
1520
  </div>
925
1521
  ${renderError(modal.error)}
926
- <div class="flex items-center justify-end gap-3 pt-2">
1522
+ <div class="flex items-center justify-between gap-3 pt-2">
927
1523
  <button
928
1524
  class="standard-button"
929
1525
  data-action="close-modal"
@@ -987,7 +1583,7 @@ function renderCreateMediaTaggingTagTableForm(modal, state) {
987
1583
  ${renderSqlPreviewField(MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL)}
988
1584
  </div>
989
1585
  ${renderError(modal.error)}
990
- <div class="flex items-center justify-end gap-3 pt-2">
1586
+ <div class="flex items-center justify-between gap-3 pt-2">
991
1587
  <button
992
1588
  class="standard-button"
993
1589
  data-action="close-modal"
@@ -1066,6 +1662,21 @@ export function renderModal(state) {
1066
1662
  title: "Delete Query",
1067
1663
  body: renderDeleteQueryHistoryForm(modal),
1068
1664
  },
1665
+ "delete-document": {
1666
+ eyebrow: "Documents // Confirm deletion",
1667
+ title: "Delete Document",
1668
+ body: renderDeleteDocumentForm(modal),
1669
+ },
1670
+ "document-insert-table": {
1671
+ eyebrow: "Documents // Saved query output",
1672
+ title: "Insert Table",
1673
+ body: renderDocumentInsertTableForm(modal),
1674
+ },
1675
+ "document-insert-note": {
1676
+ eyebrow: "Documents // Saved query notes",
1677
+ title: "Insert Note",
1678
+ body: renderDocumentInsertNoteForm(modal),
1679
+ },
1069
1680
  "query-export": {
1070
1681
  eyebrow: "SQL Editor // Export query result",
1071
1682
  title: "Export Query",
@@ -1076,6 +1687,16 @@ export function renderModal(state) {
1076
1687
  title: "Export Table",
1077
1688
  body: renderDataExportModal(modal),
1078
1689
  },
1690
+ "copy-column": {
1691
+ eyebrow: "Results // Copy or export column values",
1692
+ title: isMarkdownTodoCopyColumnMode(modal.copyMode) ? "Export Markdown Todo" : "Copy column",
1693
+ body: renderCopyColumnModal(modal, state),
1694
+ },
1695
+ "table-designer-constraints": {
1696
+ eyebrow: "Table Designer // Constraints",
1697
+ title: "Checks",
1698
+ body: renderTableDesignerConstraintsModal(modal, state),
1699
+ },
1079
1700
  "create-media-tagging-tag-table": {
1080
1701
  eyebrow: "Media Tagging // Create default tag table",
1081
1702
  title: "Create Tag Table",
@@ -1096,7 +1717,15 @@ export function renderModal(state) {
1096
1717
 
1097
1718
  return `
1098
1719
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
1099
- <div class="w-full ${modal.kind === "chart-editor" || modal.kind === "row-update-preview" ? "max-w-3xl" : "max-w-xl"} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
1720
+ <div class="w-full ${
1721
+ modal.kind === "chart-editor" ||
1722
+ modal.kind === "document-insert-table" ||
1723
+ modal.kind === "document-insert-note" ||
1724
+ modal.kind === "row-update-preview" ||
1725
+ modal.kind === "table-designer-constraints"
1726
+ ? "max-w-3xl"
1727
+ : "max-w-xl"
1728
+ } border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
1100
1729
  <div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
1101
1730
  <div>
1102
1731
  <div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">