sqlite-hub 0.7.0 → 0.9.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 (45) hide show
  1. package/changelog.md +25 -0
  2. package/docs/DESIGN_GUIDELINES.md +36 -0
  3. package/frontend/index.html +79 -0
  4. package/frontend/js/api.js +67 -0
  5. package/frontend/js/app.js +1401 -921
  6. package/frontend/js/components/connectionCard.js +3 -4
  7. package/frontend/js/components/emptyState.js +4 -4
  8. package/frontend/js/components/modal.js +239 -30
  9. package/frontend/js/components/queryEditor.js +11 -8
  10. package/frontend/js/components/queryHistoryDetail.js +27 -8
  11. package/frontend/js/components/queryHistoryPanel.js +6 -5
  12. package/frontend/js/components/rowEditorPanel.js +84 -58
  13. package/frontend/js/components/sidebar.js +76 -33
  14. package/frontend/js/components/structureGraph.js +629 -715
  15. package/frontend/js/components/tableDesignerEditor.js +9 -8
  16. package/frontend/js/components/tableDesignerSidebar.js +11 -10
  17. package/frontend/js/components/tableDesignerSqlPreview.js +61 -30
  18. package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
  19. package/frontend/js/router.js +81 -71
  20. package/frontend/js/store.js +3095 -2165
  21. package/frontend/js/views/charts.js +68 -40
  22. package/frontend/js/views/connections.js +5 -17
  23. package/frontend/js/views/data.js +40 -27
  24. package/frontend/js/views/editor.js +172 -177
  25. package/frontend/js/views/mediaTagging.js +861 -0
  26. package/frontend/js/views/overview.js +149 -10
  27. package/frontend/js/views/settings.js +2 -2
  28. package/frontend/js/views/structure.js +74 -70
  29. package/frontend/js/views/tableDesigner.js +7 -2
  30. package/frontend/styles/base.css +73 -1
  31. package/frontend/styles/components.css +105 -105
  32. package/frontend/styles/structure-graph.css +19 -82
  33. package/frontend/styles/tokens.css +2 -0
  34. package/frontend/styles/views.css +823 -30
  35. package/package.json +1 -1
  36. package/server/routes/charts.js +4 -1
  37. package/server/routes/data.js +14 -0
  38. package/server/routes/mediaTagging.js +166 -0
  39. package/server/routes/sql.js +1 -0
  40. package/server/server.js +4 -0
  41. package/server/services/sqlite/dataBrowserService.js +25 -0
  42. package/server/services/sqlite/exportService.js +31 -1
  43. package/server/services/sqlite/mediaTaggingService.js +1689 -0
  44. package/server/services/sqlite/overviewService.js +68 -0
  45. package/server/services/storage/appStateStore.js +321 -2
@@ -73,17 +73,16 @@ export function renderConnectionCard(connection, activeConnectionId) {
73
73
  <div class="border-t border-outline-variant/10 bg-surface-container-low px-4 py-3">
74
74
  <div class="grid grid-cols-[minmax(0,1fr)_5.1rem_5.8rem] gap-2">
75
75
  <button
76
- class="clipped-corner min-w-0 bg-primary-container px-3 py-2.5 text-[10px] font-black uppercase tracking-[0.18em] text-on-primary shadow-[0_0_18px_-10px_rgba(252,227,0,0.7)] transition-[filter,box-shadow] hover:brightness-105"
76
+ class="signature-button"
77
77
  data-action="select-connection"
78
78
  data-connection-id="${escapeHtml(connection.id)}"
79
- style="--clip-path: ${clipPath};"
80
79
  type="button"
81
80
  title="${primaryActionLabel}"
82
81
  >
83
82
  ${primaryActionLabel}
84
83
  </button>
85
84
  <button
86
- class="border border-outline-variant/20 bg-surface-container px-3 py-2.5 text-[10px] font-bold uppercase tracking-[0.18em] text-on-surface-variant transition-colors hover:bg-surface-container-highest"
85
+ class="standard-button"
87
86
  data-action="edit-connection"
88
87
  data-connection-id="${escapeHtml(connection.id)}"
89
88
  type="button"
@@ -91,7 +90,7 @@ export function renderConnectionCard(connection, activeConnectionId) {
91
90
  Edit
92
91
  </button>
93
92
  <button
94
- class="border border-outline-variant/20 bg-surface-container px-3 py-2.5 text-[10px] font-bold uppercase tracking-[0.18em] text-on-surface-variant transition-colors hover:bg-surface-container-highest"
93
+ class="delete-button"
95
94
  data-action="remove-connection"
96
95
  data-connection-id="${escapeHtml(connection.id)}"
97
96
  type="button"
@@ -22,7 +22,7 @@ function renderRecentConnections(recentConnections = []) {
22
22
  .map(
23
23
  (connection) => `
24
24
  <button
25
- class="flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 py-3 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high"
25
+ class="control-button flex items-center gap-2 border border-outline-variant/15 bg-surface-container-low px-4 text-left text-on-surface transition-colors hover:border-primary-container/30 hover:bg-surface-container-high"
26
26
  data-action="select-connection"
27
27
  data-connection-id="${escapeHtml(connection.id)}"
28
28
  type="button"
@@ -135,7 +135,7 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
135
135
  ? `
136
136
  <div class="mx-auto mt-8 grid w-full max-w-3xl grid-cols-1 gap-4 px-6 md:grid-cols-3">
137
137
  <button
138
- class="border border-outline-variant/20 bg-surface-container-low px-5 py-4 font-headline text-sm font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container/30 hover:text-primary-container"
138
+ class="standard-button"
139
139
  data-action="navigate"
140
140
  data-to="/overview"
141
141
  type="button"
@@ -143,7 +143,7 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
143
143
  Overview
144
144
  </button>
145
145
  <button
146
- class="border border-outline-variant/20 bg-surface-container-low px-5 py-4 font-headline text-sm font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container/30 hover:text-primary-container"
146
+ class="standard-button"
147
147
  data-action="navigate"
148
148
  data-to="/structure"
149
149
  type="button"
@@ -151,7 +151,7 @@ export function renderEmptyState({ activeConnection, recentConnections = [] }) {
151
151
  Structure
152
152
  </button>
153
153
  <button
154
- class="border border-outline-variant/20 bg-surface-container-low px-5 py-4 font-headline text-sm font-bold uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container/30 hover:text-primary-container"
154
+ class="standard-button"
155
155
  data-action="navigate"
156
156
  data-to="/editor"
157
157
  type="button"
@@ -1,10 +1,18 @@
1
- import { escapeHtml, truncateMiddle } from "../utils/format.js";
1
+ import { escapeHtml, highlightSql, truncateMiddle } from "../utils/format.js";
2
2
  import { renderConnectionLogo } from "./connectionLogo.js";
3
3
  import {
4
4
  analyzeQueryChartResult,
5
5
  getQueryChartTypeLabel,
6
6
  QUERY_CHART_TYPES,
7
7
  } from "../lib/queryCharts.js";
8
+ import {
9
+ hasDefaultMediaTaggingTagTable,
10
+ hasDefaultMediaTaggingMappingTable,
11
+ MEDIA_TAGGING_DEFAULT_MAPPING_TABLE,
12
+ MEDIA_TAGGING_DEFAULT_MAPPING_TABLE_SQL,
13
+ MEDIA_TAGGING_DEFAULT_TAG_TABLE,
14
+ MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL,
15
+ } from "../lib/mediaTaggingDefaults.js";
8
16
 
9
17
  function renderField({ label, name, type = "text", placeholder = "", value = "" }) {
10
18
  return `
@@ -13,7 +21,7 @@ function renderField({ label, name, type = "text", placeholder = "", value = ""
13
21
  ${escapeHtml(label)}
14
22
  </span>
15
23
  <input
16
- class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
24
+ 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"
17
25
  name="${escapeHtml(name)}"
18
26
  placeholder="${escapeHtml(placeholder)}"
19
27
  type="${escapeHtml(type)}"
@@ -25,18 +33,43 @@ function renderField({ label, name, type = "text", placeholder = "", value = ""
25
33
 
26
34
  function renderCheckboxField({ label, name, checked = false, text }) {
27
35
  return `
28
- <label class="flex items-center gap-3 border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface">
29
- <input
30
- class="rounded-none border-outline bg-surface-container-lowest text-primary-container focus:ring-primary-container"
31
- ${checked ? "checked" : ""}
32
- name="${escapeHtml(name)}"
33
- type="checkbox"
34
- />
35
- ${escapeHtml(text || label)}
36
+ <label class="flex flex-col gap-2">
37
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
38
+ ${escapeHtml(label)}
39
+ </span>
40
+ <span class="standard-checkbox">
41
+ <input
42
+ ${checked ? "checked" : ""}
43
+ name="${escapeHtml(name)}"
44
+ type="checkbox"
45
+ />
46
+ <span>${escapeHtml(text || label)}</span>
47
+ </span>
36
48
  </label>
37
49
  `;
38
50
  }
39
51
 
52
+ function renderSqlPreviewField(value, minHeightClass = "sql-highlight-shell--tall") {
53
+ return `
54
+ <div class="sql-highlight-shell ${minHeightClass}">
55
+ <div class="query-editor-layer sql-highlight-layer">
56
+ <div
57
+ aria-hidden="true"
58
+ class="query-editor-highlight sql-highlight-content"
59
+ data-query-editor-highlight
60
+ >${value ? highlightSql(value) : ""}</div>
61
+ <textarea
62
+ class="query-editor-input sql-highlight-input custom-scrollbar"
63
+ data-sql-highlight="true"
64
+ readonly
65
+ spellcheck="false"
66
+ wrap="off"
67
+ >${escapeHtml(value)}</textarea>
68
+ </div>
69
+ </div>
70
+ `;
71
+ }
72
+
40
73
  function renderFileField({
41
74
  label,
42
75
  name,
@@ -50,7 +83,7 @@ function renderFileField({
50
83
  </span>
51
84
  <input
52
85
  accept="${escapeHtml(accept)}"
53
- class="block w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface file:mr-4 file:border-0 file:bg-primary-container file:px-3 file:py-2 file:text-xs file:font-bold file:text-on-primary"
86
+ class="control-input block w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface file:mr-4 file:border-0 file:bg-primary-container file:px-3 file:py-2 file:text-xs file:font-bold file:text-on-primary"
54
87
  name="${escapeHtml(name)}"
55
88
  type="file"
56
89
  />
@@ -66,11 +99,11 @@ function renderFileField({
66
99
  function renderSelectField({ label, name, value = "", options = [], bind = "" }) {
67
100
  return `
68
101
  <label class="block space-y-2">
69
- <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
102
+ <span class="block text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
70
103
  ${escapeHtml(label)}
71
104
  </span>
72
105
  <select
73
- class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
106
+ 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"
74
107
  ${bind ? `data-bind="${escapeHtml(bind)}"` : ""}
75
108
  ${name ? `name="${escapeHtml(name)}"` : ""}
76
109
  >
@@ -124,14 +157,14 @@ function renderOpenConnectionForm(modal) {
124
157
  ${renderError(modal.error)}
125
158
  <div class="flex items-center justify-end gap-3 pt-2">
126
159
  <button
127
- class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
160
+ class="standard-button"
128
161
  data-action="close-modal"
129
162
  type="button"
130
163
  >
131
164
  Cancel
132
165
  </button>
133
166
  <button
134
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
167
+ class="standard-button"
135
168
  type="submit"
136
169
  >
137
170
  ${modal.submitting ? "Opening..." : "Open Database"}
@@ -198,14 +231,14 @@ function renderEditConnectionForm(modal) {
198
231
  ${renderError(modal.error)}
199
232
  <div class="flex items-center justify-end gap-3 pt-2">
200
233
  <button
201
- class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
234
+ class="standard-button"
202
235
  data-action="close-modal"
203
236
  type="button"
204
237
  >
205
238
  Cancel
206
239
  </button>
207
240
  <button
208
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
241
+ class="standard-button"
209
242
  type="submit"
210
243
  >
211
244
  ${modal.submitting ? "Saving..." : "Save Changes"}
@@ -231,14 +264,14 @@ function renderCreateDatabaseForm(modal) {
231
264
  ${renderError(modal.error)}
232
265
  <div class="flex items-center justify-end gap-3 pt-2">
233
266
  <button
234
- class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
267
+ class="standard-button"
235
268
  data-action="close-modal"
236
269
  type="button"
237
270
  >
238
271
  Cancel
239
272
  </button>
240
273
  <button
241
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
274
+ class="standard-button"
242
275
  type="submit"
243
276
  >
244
277
  ${modal.submitting ? "Creating..." : "Create Database"}
@@ -265,7 +298,7 @@ function renderImportTargetOptions(state) {
265
298
  Import Target
266
299
  </span>
267
300
  <select
268
- class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
301
+ 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"
269
302
  name="targetMode"
270
303
  >
271
304
  ${
@@ -286,7 +319,7 @@ function renderImportTargetOptions(state) {
286
319
  Recent Connection
287
320
  </span>
288
321
  <select
289
- class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
322
+ 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"
290
323
  name="targetConnectionId"
291
324
  >
292
325
  ${recentOptions}
@@ -324,14 +357,14 @@ function renderImportSqlForm(modal, state) {
324
357
  ${renderError(modal.error)}
325
358
  <div class="flex items-center justify-end gap-3 pt-2">
326
359
  <button
327
- class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
360
+ class="standard-button"
328
361
  data-action="close-modal"
329
362
  type="button"
330
363
  >
331
364
  Cancel
332
365
  </button>
333
366
  <button
334
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
367
+ class="standard-button"
335
368
  type="submit"
336
369
  >
337
370
  ${modal.submitting ? "Importing..." : "Import SQL Dump"}
@@ -393,14 +426,14 @@ function renderDeleteRowConfirmForm(modal) {
393
426
  ${renderError(modal.error)}
394
427
  <div class="flex items-center justify-end gap-3 pt-2">
395
428
  <button
396
- class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
429
+ class="standard-button"
397
430
  data-action="close-modal"
398
431
  type="button"
399
432
  >
400
433
  Cancel
401
434
  </button>
402
435
  <button
403
- class="border border-error/25 bg-error-container/10 px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-error"
436
+ class="delete-button"
404
437
  type="submit"
405
438
  >
406
439
  ${modal.submitting ? "Deleting..." : "Delete Row"}
@@ -633,14 +666,14 @@ function renderChartEditorForm(modal, state) {
633
666
  ${renderError(modal.error)}
634
667
  <div class="flex items-center justify-end gap-3 pt-2">
635
668
  <button
636
- class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
669
+ class="standard-button"
637
670
  data-action="close-modal"
638
671
  type="button"
639
672
  >
640
673
  Cancel
641
674
  </button>
642
675
  <button
643
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
676
+ class="standard-button"
644
677
  type="submit"
645
678
  >
646
679
  ${modal.submitting ? "Saving..." : draft.mode === "edit" ? "Save Chart" : "Create Chart"}
@@ -666,14 +699,14 @@ function renderDeleteChartForm(modal) {
666
699
  ${renderError(modal.error)}
667
700
  <div class="flex items-center justify-end gap-3 pt-2">
668
701
  <button
669
- class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
702
+ class="standard-button"
670
703
  data-action="close-modal"
671
704
  type="button"
672
705
  >
673
706
  Cancel
674
707
  </button>
675
708
  <button
676
- class="border border-error/25 bg-error-container/10 px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-error"
709
+ class="delete-button"
677
710
  type="submit"
678
711
  >
679
712
  ${modal.submitting ? "Deleting..." : "Delete Chart"}
@@ -683,6 +716,167 @@ function renderDeleteChartForm(modal) {
683
716
  `;
684
717
  }
685
718
 
719
+ function renderDeleteQueryHistoryForm(modal) {
720
+ return `
721
+ <form class="space-y-5" data-form="delete-query-history-confirm">
722
+ <div class="space-y-3">
723
+ <p class="text-sm leading-7 text-on-surface">
724
+ Delete query <span class="font-bold text-primary-container">${escapeHtml(
725
+ modal.queryTitle ?? "SQL query"
726
+ )}</span>?
727
+ </p>
728
+ <p class="text-sm leading-7 text-on-surface-variant/65">
729
+ This removes the query-history entry and all recorded runs linked to it.
730
+ </p>
731
+ </div>
732
+ ${renderError(modal.error)}
733
+ <div class="flex items-center justify-end gap-3 pt-2">
734
+ <button
735
+ class="standard-button"
736
+ data-action="close-modal"
737
+ type="button"
738
+ >
739
+ Cancel
740
+ </button>
741
+ <button
742
+ class="delete-button"
743
+ type="submit"
744
+ >
745
+ ${modal.submitting ? "Deleting..." : "Delete Query"}
746
+ </button>
747
+ </div>
748
+ </form>
749
+ `;
750
+ }
751
+
752
+ function renderCreateMediaTaggingMappingTableForm(modal, state) {
753
+ const mappingExists = hasDefaultMediaTaggingMappingTable(state.mediaTagging.schemaTables ?? []);
754
+ const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
755
+
756
+ return `
757
+ <form class="space-y-5" data-form="create-media-tagging-mapping-table">
758
+ <div class="space-y-3">
759
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
760
+ Mapping Table
761
+ </div>
762
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 font-mono text-sm text-on-surface">
763
+ ${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)}
764
+ </div>
765
+ </div>
766
+ <div class="space-y-3">
767
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
768
+ Status
769
+ </div>
770
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface">
771
+ ${
772
+ mappingExists
773
+ ? `${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)} already exists in the active database.`
774
+ : `${escapeHtml(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE)} does not exist yet.`
775
+ }
776
+ ${
777
+ readOnly && !mappingExists
778
+ ? `<div class="mt-2 text-on-surface-variant/60">The active connection is read-only, so the table cannot be created here.</div>`
779
+ : ""
780
+ }
781
+ </div>
782
+ </div>
783
+ <div class="space-y-3">
784
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
785
+ SQL
786
+ </div>
787
+ ${renderSqlPreviewField(MEDIA_TAGGING_DEFAULT_MAPPING_TABLE_SQL)}
788
+ </div>
789
+ ${renderError(modal.error)}
790
+ <div class="flex items-center justify-end gap-3 pt-2">
791
+ <button
792
+ class="standard-button"
793
+ data-action="close-modal"
794
+ type="button"
795
+ >
796
+ Close
797
+ </button>
798
+ ${
799
+ !mappingExists
800
+ ? `
801
+ <button
802
+ class="standard-button"
803
+ type="submit"
804
+ ${readOnly ? "disabled" : ""}
805
+ >
806
+ ${modal.submitting ? "Creating..." : "Create Table"}
807
+ </button>
808
+ `
809
+ : ""
810
+ }
811
+ </div>
812
+ </form>
813
+ `;
814
+ }
815
+
816
+ function renderCreateMediaTaggingTagTableForm(modal, state) {
817
+ const tagTableExists = hasDefaultMediaTaggingTagTable(state.mediaTagging.schemaTables ?? []);
818
+ const readOnly = Boolean(state.mediaTagging.connection?.readOnly);
819
+
820
+ return `
821
+ <form class="space-y-5" data-form="create-media-tagging-tag-table">
822
+ <div class="space-y-3">
823
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
824
+ Tag Table
825
+ </div>
826
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 font-mono text-sm text-on-surface">
827
+ ${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)}
828
+ </div>
829
+ </div>
830
+ <div class="space-y-3">
831
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
832
+ Status
833
+ </div>
834
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface">
835
+ ${
836
+ tagTableExists
837
+ ? `${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)} already exists in the active database.`
838
+ : `${escapeHtml(MEDIA_TAGGING_DEFAULT_TAG_TABLE)} does not exist yet.`
839
+ }
840
+ ${
841
+ readOnly && !tagTableExists
842
+ ? `<div class="mt-2 text-on-surface-variant/60">The active connection is read-only, so the table cannot be created here.</div>`
843
+ : ""
844
+ }
845
+ </div>
846
+ </div>
847
+ <div class="space-y-3">
848
+ <div class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
849
+ SQL
850
+ </div>
851
+ ${renderSqlPreviewField(MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL)}
852
+ </div>
853
+ ${renderError(modal.error)}
854
+ <div class="flex items-center justify-end gap-3 pt-2">
855
+ <button
856
+ class="standard-button"
857
+ data-action="close-modal"
858
+ type="button"
859
+ >
860
+ Close
861
+ </button>
862
+ ${
863
+ !tagTableExists
864
+ ? `
865
+ <button
866
+ class="standard-button"
867
+ type="submit"
868
+ ${readOnly ? "disabled" : ""}
869
+ >
870
+ ${modal.submitting ? "Creating..." : "Create Table"}
871
+ </button>
872
+ `
873
+ : ""
874
+ }
875
+ </div>
876
+ </form>
877
+ `;
878
+ }
879
+
686
880
  export function renderModal(state) {
687
881
  const modal = state.modal;
688
882
 
@@ -726,6 +920,21 @@ export function renderModal(state) {
726
920
  title: "Delete Chart",
727
921
  body: renderDeleteChartForm(modal),
728
922
  },
923
+ "delete-query-history": {
924
+ eyebrow: "History // Confirm query deletion",
925
+ title: "Delete Query",
926
+ body: renderDeleteQueryHistoryForm(modal),
927
+ },
928
+ "create-media-tagging-tag-table": {
929
+ eyebrow: "Media Tagging // Create default tag table",
930
+ title: "Create Tag Table",
931
+ body: renderCreateMediaTaggingTagTableForm(modal, state),
932
+ },
933
+ "create-media-tagging-mapping-table": {
934
+ eyebrow: "Media Tagging // Create default join table",
935
+ title: "Create Mapping Table",
936
+ body: renderCreateMediaTaggingMappingTableForm(modal, state),
937
+ },
729
938
  };
730
939
 
731
940
  const config = contentByKind[modal.kind];
@@ -747,7 +956,7 @@ export function renderModal(state) {
747
956
  </h2>
748
957
  </div>
749
958
  <button
750
- class="flex h-10 w-10 items-center justify-center border border-outline-variant/20 text-on-surface-variant hover:bg-surface-container-highest hover:text-primary-container"
959
+ class="control-icon-button border border-outline-variant/20 text-on-surface-variant hover:bg-surface-container-highest hover:text-primary-container"
751
960
  data-action="close-modal"
752
961
  type="button"
753
962
  >
@@ -19,11 +19,16 @@ function renderHighlightedQuery(query) {
19
19
 
20
20
  function renderEditorSurface({ query }) {
21
21
  return `
22
- <div class="flex min-h-0 flex-1 overflow-hidden">
23
- <div class="flex w-12 flex-col items-center bg-surface-container-lowest py-4 font-mono text-xs select-none text-outline-variant/30">
24
- ${renderLineNumbers(query)}
22
+ <div class="query-editor-shell flex min-h-0 flex-1 overflow-hidden">
23
+ <div class="flex w-12 min-h-0 overflow-hidden bg-surface-container-lowest py-4 font-mono text-xs select-none text-outline-variant/30">
24
+ <div
25
+ class="query-editor-gutter-track flex w-full flex-col items-center"
26
+ data-query-editor-gutter
27
+ >
28
+ ${renderLineNumbers(query)}
29
+ </div>
25
30
  </div>
26
- <div class="relative flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
31
+ <div class="relative min-h-0 flex-1 overflow-hidden bg-surface-container p-6 font-mono text-sm leading-relaxed">
27
32
  <div class="pointer-events-none absolute right-0 top-0 p-4 opacity-5">
28
33
  <span class="material-symbols-outlined text-[120px] font-thin">terminal</span>
29
34
  </div>
@@ -55,8 +60,7 @@ export function renderQueryEditor({
55
60
  editorVisible = true,
56
61
  historyVisible = true,
57
62
  }) {
58
- const secondaryButtonClass =
59
- "toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-bold uppercase tracking-widest text-on-surface transition-colors hover:border-primary-container hover:text-primary-container";
63
+ const secondaryButtonClass = "standard-button";
60
64
  const left = `
61
65
  <div class="flex items-center gap-2 bg-surface-container-lowest px-3 py-1">
62
66
  <span class="material-symbols-outlined text-xs text-[#FCE300]">database</span>
@@ -104,9 +108,8 @@ export function renderQueryEditor({
104
108
  ${exporting ? "Exporting..." : "Export CSV"}
105
109
  </button>
106
110
  <button
107
- class="toolbar-button toolbar-button--primary bg-primary-container px-4 py-2 font-headline text-xs font-bold uppercase tracking-widest text-on-primary clipped-corner"
111
+ class="signature-button"
108
112
  data-action="execute-query"
109
- style="--clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);"
110
113
  type="button"
111
114
  >
112
115
  ${executing ? "RUNNING..." : "EXECUTE"}
@@ -19,6 +19,10 @@ function renderDetailMetaItem(label, value) {
19
19
  `;
20
20
  }
21
21
 
22
+ function canOpenQueryHistoryInCharts(item) {
23
+ return Boolean(item?.chartsEligible);
24
+ }
25
+
22
26
  function renderRunItem(run) {
23
27
  return `
24
28
  <div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
@@ -155,14 +159,14 @@ export function renderQueryHistoryDetail({
155
159
  </label>
156
160
  <div class="mt-2 flex gap-2">
157
161
  <input
158
- class="flex-1 border border-outline-variant/20 bg-surface-container px-3 py-2 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
162
+ class="control-input flex-1 border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
159
163
  name="title"
160
164
  placeholder="${escapeHtml(item.displayTitle)}"
161
165
  type="text"
162
166
  value="${escapeHtml(item.title ?? "")}"
163
167
  />
164
168
  <button
165
- class="border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
169
+ class="standard-button"
166
170
  type="submit"
167
171
  >
168
172
  Save
@@ -175,15 +179,30 @@ export function renderQueryHistoryDetail({
175
179
 
176
180
  <div class="mt-5 flex flex-wrap gap-2">
177
181
  <button
178
- class="toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
182
+ class="standard-button"
179
183
  data-action="open-query-history"
180
184
  data-history-id="${escapeHtml(item.id)}"
181
185
  type="button"
182
186
  >
183
187
  Open In Editor
184
188
  </button>
189
+ ${
190
+ canOpenQueryHistoryInCharts(item)
191
+ ? `
192
+ <button
193
+ class="standard-button"
194
+ data-action="navigate"
195
+ data-to="/charts/${encodeURIComponent(item.id)}"
196
+ type="button"
197
+ >
198
+ <span class="material-symbols-outlined text-sm">bar_chart</span>
199
+ Open In Charts
200
+ </button>
201
+ `
202
+ : ""
203
+ }
185
204
  <button
186
- class="toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
205
+ class="standard-button"
187
206
  data-action="run-query-history"
188
207
  data-history-id="${escapeHtml(item.id)}"
189
208
  type="button"
@@ -191,7 +210,7 @@ export function renderQueryHistoryDetail({
191
210
  Run Now
192
211
  </button>
193
212
  <button
194
- class="toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
213
+ class="standard-button"
195
214
  data-action="toggle-query-history-saved"
196
215
  data-history-id="${escapeHtml(item.id)}"
197
216
  data-next-value="${item.isSaved ? "false" : "true"}"
@@ -200,8 +219,8 @@ export function renderQueryHistoryDetail({
200
219
  ${item.isSaved ? "Unsave" : "Save"}
201
220
  </button>
202
221
  <button
203
- class="toolbar-button border border-error/25 bg-error-container/20 px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-error transition-colors hover:border-error hover:bg-error-container/30"
204
- data-action="delete-query-history"
222
+ class="delete-button"
223
+ data-action="open-delete-query-history-modal"
205
224
  data-history-id="${escapeHtml(item.id)}"
206
225
  type="button"
207
226
  >
@@ -228,7 +247,7 @@ export function renderQueryHistoryDetail({
228
247
  )}</textarea>
229
248
  <div class="mt-2 flex justify-end">
230
249
  <button
231
- class="border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface transition-colors hover:border-primary-container hover:text-primary-container"
250
+ class="standard-button"
232
251
  type="submit"
233
252
  >
234
253
  Save Notes