sqlite-hub 0.7.0 → 0.8.7

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 (31) hide show
  1. package/changelog.md +14 -0
  2. package/docs/DESIGN_GUIDELINES.md +36 -0
  3. package/frontend/index.html +79 -0
  4. package/frontend/js/app.js +89 -8
  5. package/frontend/js/components/connectionCard.js +3 -4
  6. package/frontend/js/components/emptyState.js +4 -4
  7. package/frontend/js/components/modal.js +32 -28
  8. package/frontend/js/components/queryEditor.js +2 -4
  9. package/frontend/js/components/queryHistoryDetail.js +16 -7
  10. package/frontend/js/components/queryHistoryPanel.js +2 -2
  11. package/frontend/js/components/rowEditorPanel.js +59 -54
  12. package/frontend/js/components/sidebar.js +32 -33
  13. package/frontend/js/components/structureGraph.js +54 -122
  14. package/frontend/js/components/tableDesignerEditor.js +9 -8
  15. package/frontend/js/components/tableDesignerSidebar.js +11 -10
  16. package/frontend/js/components/tableDesignerSqlPreview.js +60 -28
  17. package/frontend/js/store.js +23 -0
  18. package/frontend/js/views/charts.js +66 -38
  19. package/frontend/js/views/connections.js +5 -17
  20. package/frontend/js/views/data.js +12 -13
  21. package/frontend/js/views/overview.js +149 -10
  22. package/frontend/js/views/settings.js +2 -2
  23. package/frontend/js/views/structure.js +74 -70
  24. package/frontend/js/views/tableDesigner.js +7 -2
  25. package/frontend/styles/base.css +62 -0
  26. package/frontend/styles/components.css +63 -105
  27. package/frontend/styles/structure-graph.css +19 -82
  28. package/frontend/styles/tokens.css +2 -0
  29. package/frontend/styles/views.css +33 -36
  30. package/package.json +1 -1
  31. package/server/services/sqlite/overviewService.js +68 -0
package/changelog.md CHANGED
@@ -1,3 +1,17 @@
1
+ # v0.8.7
2
+
3
+ - UX fixes
4
+ - fixing a lot of the vibe slop
5
+ - trying to build reuseable components
6
+
7
+ # v0.8.0
8
+
9
+ - DDL copy button
10
+ - open in charts button in query details
11
+ - clear makes editor active
12
+ - UI fixes
13
+ - Overview improvement
14
+
1
15
  # v0.7.0
2
16
 
3
17
  - hide query history, hide editor
@@ -0,0 +1,36 @@
1
+ # SQLite Hub UI Rules for Codex
2
+
3
+ Follow these rules exactly when creating or modifying UI controls.
4
+
5
+ 1. Reuse existing shared components before creating anything new.
6
+ 2. Use exactly one semantic main class per control.
7
+ 3. Never combine multiple button-style classes on the same element.
8
+ 4. View-specific classes may change layout, width, spacing, or position only.
9
+ 5. View-specific classes must not redefine the base style of shared components.
10
+ 6. The standard height for interactive controls is `36px`.
11
+ 7. The source of truth for control height is `--control-height` in `frontend/styles/tokens.css`.
12
+ 8. Controls in the same row must have the same height.
13
+ 9. All regular buttons must use one of these classes only: `standard-button`, `signature-button`, `delete-button`.
14
+ 10. Use `standard-button` for normal actions.
15
+ 11. Use `signature-button` only for the primary yellow CTA in a context.
16
+ 12. Use `delete-button` only for destructive actions.
17
+ 13. `signature-button` must keep its hover state and chamfered corner.
18
+ 14. All visible clickable buttons must have a hover state.
19
+ 15. Disabled states must come from the shared component, not from local view CSS.
20
+ 16. All reusable checkboxes must use `standard-checkbox`.
21
+ 17. Do not build feature-specific checkbox shells or checkbox base styles.
22
+ 18. Inputs and selects must follow the shared base rules in `frontend/styles/base.css`.
23
+ 19. Ghost or transparent inputs must not use a white background, including unfocused state.
24
+ 20. When a new control pattern appears in multiple places, promote it to a shared component first, then migrate existing usages.
25
+
26
+ ## Source of Truth
27
+
28
+ - `frontend/styles/tokens.css`
29
+ - `frontend/styles/base.css`
30
+ - `frontend/index.html`
31
+
32
+ ## Default Decision Rules
33
+
34
+ - Prefer reuse over invention.
35
+ - Prefer migration over local override.
36
+ - Prefer shared component updates over per-view fixes.
@@ -85,6 +85,85 @@
85
85
  },
86
86
  };
87
87
  </script>
88
+ <style type="text/tailwindcss">
89
+ @layer components {
90
+ .signature-button {
91
+ @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-primary-container bg-primary-container px-[var(--control-padding-inline)] font-headline text-xs font-bold uppercase tracking-[0.16em] text-on-primary transition-all;
92
+ box-shadow: 0 0 18px -10px rgba(252, 227, 0, 0.7);
93
+ clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
94
+ }
95
+
96
+ .signature-button:hover {
97
+ @apply -translate-y-px bg-primary-fixed text-on-primary;
98
+ border-color: rgba(252, 227, 0, 0.55);
99
+ box-shadow: 0 0 24px -8px rgba(252, 227, 0, 0.92);
100
+ filter: brightness(1.03);
101
+ }
102
+
103
+ .signature-button:disabled,
104
+ .signature-button[aria-disabled="true"] {
105
+ @apply cursor-not-allowed opacity-45;
106
+ box-shadow: none;
107
+ filter: none;
108
+ transform: none;
109
+ }
110
+
111
+ .signature-button .material-symbols-outlined {
112
+ @apply text-base;
113
+ }
114
+
115
+ .standard-button {
116
+ @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-outline-variant/20 bg-surface-container px-[var(--control-padding-inline)] font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-on-surface transition-colors;
117
+ }
118
+
119
+ .standard-button:hover {
120
+ @apply bg-surface-container-highest text-primary-container;
121
+ border-color: rgba(252, 227, 0, 0.24);
122
+ }
123
+
124
+ .standard-button.is-active {
125
+ @apply bg-surface-container-high text-primary-container;
126
+ border-color: rgba(252, 227, 0, 0.3);
127
+ }
128
+
129
+ .standard-button:disabled,
130
+ .standard-button[aria-disabled="true"] {
131
+ @apply cursor-not-allowed opacity-40;
132
+ }
133
+
134
+ .standard-button .material-symbols-outlined {
135
+ @apply text-base;
136
+ }
137
+
138
+ .standard-checkbox {
139
+ @apply inline-flex items-center gap-3 min-h-[var(--control-height)] border border-outline-variant/10 bg-surface-container-lowest px-[var(--control-padding-inline)] text-sm text-on-surface;
140
+ }
141
+
142
+ .standard-checkbox input[type="checkbox"] {
143
+ @apply m-0 rounded-none border-outline bg-surface-container-lowest text-primary-container focus:ring-primary-container;
144
+ accent-color: var(--color-primary-container);
145
+ }
146
+
147
+ .standard-checkbox.is-disabled,
148
+ .standard-checkbox:has(input:disabled) {
149
+ @apply opacity-45;
150
+ }
151
+
152
+ .delete-button {
153
+ @apply inline-flex items-center justify-center gap-2 h-[var(--control-height)] min-h-[var(--control-height)] border border-error/25 bg-error-container/10 px-[var(--control-padding-inline)] font-mono text-[10px] font-bold uppercase tracking-[0.16em] text-error transition-all;
154
+ }
155
+
156
+ .delete-button:hover {
157
+ @apply -translate-y-px border-error bg-error-container/20 text-error;
158
+ }
159
+
160
+ .delete-button:disabled,
161
+ .delete-button[aria-disabled="true"] {
162
+ @apply cursor-not-allowed opacity-45;
163
+ transform: none;
164
+ }
165
+ }
166
+ </style>
88
167
  <link href="styles/tokens.css" rel="stylesheet" />
89
168
  <link href="styles/base.css" rel="stylesheet" />
90
169
  <link href="styles/layout.css" rel="stylesheet" />
@@ -50,6 +50,8 @@ import {
50
50
  selectQueryHistoryItem,
51
51
  selectStructureEntry,
52
52
  setTableDesignerSearchQuery,
53
+ setTableDesignerSqlPreviewVisibility,
54
+ toggleStructureTablesPanel,
53
55
  setDataPage,
54
56
  setDataPageSize,
55
57
  setDataSearchColumn,
@@ -115,7 +117,21 @@ const shellRefs = {
115
117
  toast: document.querySelector("#toast-root"),
116
118
  };
117
119
  let lastRenderedRoutePath = null;
120
+ let lastRenderedTopNavMarkup = "";
121
+ let lastRenderedSidebarMarkup = "";
122
+ let lastRenderedStatusBarMarkup = "";
123
+ let lastRenderedMainMarkup = "";
124
+ let lastRenderedPanelMarkup = "";
125
+ let lastRenderedModalMarkup = "";
126
+ let lastRenderedToastMarkup = "";
127
+ let lastRenderedPanelOpen = false;
128
+ let lastRenderedLockedRoute = false;
118
129
  let pendingNewTableDesignerAutofocus = false;
130
+ let pendingQueryEditorFocus = false;
131
+
132
+ function invalidateMainRenderCache() {
133
+ lastRenderedMainMarkup = null;
134
+ }
119
135
 
120
136
  function resetStructureGraphForDatabaseChange() {
121
137
  resetPersistedStructureGraphState();
@@ -215,7 +231,7 @@ function renderNotFoundView() {
215
231
  404_SIGNAL
216
232
  </h1>
217
233
  <button
218
- class="mt-8 bg-primary-container px-6 py-3 font-headline text-sm font-bold uppercase tracking-[0.2em] text-on-primary"
234
+ class="standard-button mt-8 px-6 font-headline text-sm"
219
235
  data-action="navigate"
220
236
  data-to="/"
221
237
  type="button"
@@ -389,6 +405,18 @@ function focusTableDesignerColumnNameField(columnId) {
389
405
  return true;
390
406
  }
391
407
 
408
+ function focusQueryEditorInput() {
409
+ const input = document.querySelector('[data-bind="current-query"]');
410
+
411
+ if (!(input instanceof HTMLTextAreaElement)) {
412
+ return false;
413
+ }
414
+
415
+ input.focus({ preventScroll: true });
416
+ input.setSelectionRange(input.value.length, input.value.length);
417
+ return true;
418
+ }
419
+
392
420
  async function handleTableDesignerCsvImport(fileInput) {
393
421
  if (!(fileInput instanceof HTMLInputElement)) {
394
422
  return;
@@ -429,12 +457,39 @@ async function handleTableDesignerCsvImport(fileInput) {
429
457
  }
430
458
 
431
459
  function renderApp(state) {
432
- const focusedInput = captureFocusedInputState();
433
460
  const previousRoutePath = lastRenderedRoutePath;
434
461
  const { main, panel } = resolveView(state);
462
+ const topNavMarkup = renderTopNav(state);
463
+ const sidebarMarkup = renderSidebar(state);
464
+ const statusBarMarkup = renderStatusBar(state);
465
+ const modalMarkup = renderModal(state);
466
+ const toastMarkup = renderToasts(state.toasts);
435
467
  const isLockedRoute = ["editor", "editorResults", "data", "charts", "structure", "tableDesigner"].includes(
436
468
  state.route.name
437
469
  );
470
+ const panelOpen = Boolean(panel);
471
+ const shellMarkupUnchanged =
472
+ state.route.path === lastRenderedRoutePath &&
473
+ topNavMarkup === lastRenderedTopNavMarkup &&
474
+ sidebarMarkup === lastRenderedSidebarMarkup &&
475
+ statusBarMarkup === lastRenderedStatusBarMarkup &&
476
+ main === lastRenderedMainMarkup &&
477
+ panel === lastRenderedPanelMarkup &&
478
+ modalMarkup === lastRenderedModalMarkup &&
479
+ panelOpen === lastRenderedPanelOpen &&
480
+ isLockedRoute === lastRenderedLockedRoute;
481
+
482
+ if (shellMarkupUnchanged && toastMarkup !== lastRenderedToastMarkup) {
483
+ shellRefs.toast.innerHTML = toastMarkup;
484
+ lastRenderedToastMarkup = toastMarkup;
485
+ return;
486
+ }
487
+
488
+ if (shellMarkupUnchanged && toastMarkup === lastRenderedToastMarkup) {
489
+ return;
490
+ }
491
+
492
+ const focusedInput = captureFocusedInputState();
438
493
  const isEnteringNewTableDesignerRoute =
439
494
  state.route.name === "tableDesigner" &&
440
495
  state.route.params?.isNew &&
@@ -448,17 +503,24 @@ function renderApp(state) {
448
503
 
449
504
  teardownStructureGraph();
450
505
  teardownQueryChartRenderer();
451
- shellRefs.topNav.innerHTML = renderTopNav(state);
452
- shellRefs.sidebar.innerHTML = renderSidebar(state);
453
- shellRefs.statusBar.innerHTML = renderStatusBar(state);
506
+ shellRefs.topNav.innerHTML = topNavMarkup;
507
+ shellRefs.sidebar.innerHTML = sidebarMarkup;
508
+ shellRefs.statusBar.innerHTML = statusBarMarkup;
454
509
  shellRefs.view.innerHTML = main;
455
510
  shellRefs.view.classList.toggle("app-main-scroll--locked", isLockedRoute);
456
511
  shellRefs.panel.innerHTML = panel;
457
- shellRefs.modal.innerHTML = renderModal(state);
458
- shellRefs.toast.innerHTML = renderToasts(state.toasts);
459
- shellRefs.shell.classList.toggle("panel-open", Boolean(panel));
512
+ shellRefs.modal.innerHTML = modalMarkup;
513
+ shellRefs.toast.innerHTML = toastMarkup;
514
+ shellRefs.shell.classList.toggle("panel-open", panelOpen);
460
515
 
461
516
  if (
517
+ pendingQueryEditorFocus &&
518
+ (state.route.name === "editor" || state.route.name === "editorResults")
519
+ ) {
520
+ if (focusQueryEditorInput()) {
521
+ pendingQueryEditorFocus = false;
522
+ }
523
+ } else if (
462
524
  pendingNewTableDesignerAutofocus &&
463
525
  state.route.name === "tableDesigner" &&
464
526
  state.route.params?.isNew &&
@@ -472,6 +534,15 @@ function renderApp(state) {
472
534
  }
473
535
 
474
536
  lastRenderedRoutePath = state.route.path;
537
+ lastRenderedTopNavMarkup = topNavMarkup;
538
+ lastRenderedSidebarMarkup = sidebarMarkup;
539
+ lastRenderedStatusBarMarkup = statusBarMarkup;
540
+ lastRenderedMainMarkup = main;
541
+ lastRenderedPanelMarkup = panel;
542
+ lastRenderedModalMarkup = modalMarkup;
543
+ lastRenderedToastMarkup = toastMarkup;
544
+ lastRenderedPanelOpen = panelOpen;
545
+ lastRenderedLockedRoute = isLockedRoute;
475
546
 
476
547
  if (state.route.name === "structure") {
477
548
  mountStructureGraph(state).catch((error) => {
@@ -567,6 +638,7 @@ async function handleAction(actionNode) {
567
638
  openDeleteEditorRowModal(actionNode.dataset.rowIndex);
568
639
  return;
569
640
  case "clear-query":
641
+ pendingQueryEditorFocus = true;
570
642
  clearCurrentQuery();
571
643
  if (getState().route.name === "editorResults") {
572
644
  router.navigate("/editor");
@@ -663,6 +735,9 @@ async function handleAction(actionNode) {
663
735
  case "toggle-data-tables":
664
736
  toggleDataTablesPanel();
665
737
  return;
738
+ case "toggle-structure-tables":
739
+ toggleStructureTablesPanel();
740
+ return;
666
741
  case "select-structure-entry":
667
742
  if (actionNode.dataset.entryName) {
668
743
  await selectStructureEntry(actionNode.dataset.entryName);
@@ -704,6 +779,11 @@ async function handleAction(actionNode) {
704
779
  }
705
780
  return;
706
781
  }
782
+ case "toggle-table-designer-sql-preview":
783
+ setTableDesignerSqlPreviewVisibility(
784
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === "true" : undefined
785
+ );
786
+ return;
707
787
  case "import-table-designer-csv": {
708
788
  const fileInput = document.querySelector('[data-bind="table-designer-import-file"]');
709
789
 
@@ -822,6 +902,7 @@ document.addEventListener("input", (event) => {
822
902
  }
823
903
 
824
904
  if (bindNode.dataset.bind === "current-query") {
905
+ invalidateMainRenderCache();
825
906
  syncQueryEditorHighlight(bindNode);
826
907
  syncQueryEditorScroll(bindNode);
827
908
  setCurrentQuery(bindNode.value);
@@ -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"
@@ -13,7 +13,7 @@ function renderField({ label, name, type = "text", placeholder = "", value = ""
13
13
  ${escapeHtml(label)}
14
14
  </span>
15
15
  <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"
16
+ 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
17
  name="${escapeHtml(name)}"
18
18
  placeholder="${escapeHtml(placeholder)}"
19
19
  type="${escapeHtml(type)}"
@@ -25,14 +25,18 @@ function renderField({ label, name, type = "text", placeholder = "", value = ""
25
25
 
26
26
  function renderCheckboxField({ label, name, checked = false, text }) {
27
27
  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)}
28
+ <label class="block space-y-2">
29
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
30
+ ${escapeHtml(label)}
31
+ </span>
32
+ <span class="standard-checkbox">
33
+ <input
34
+ ${checked ? "checked" : ""}
35
+ name="${escapeHtml(name)}"
36
+ type="checkbox"
37
+ />
38
+ <span>${escapeHtml(text || label)}</span>
39
+ </span>
36
40
  </label>
37
41
  `;
38
42
  }
@@ -50,7 +54,7 @@ function renderFileField({
50
54
  </span>
51
55
  <input
52
56
  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"
57
+ 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
58
  name="${escapeHtml(name)}"
55
59
  type="file"
56
60
  />
@@ -70,7 +74,7 @@ function renderSelectField({ label, name, value = "", options = [], bind = "" })
70
74
  ${escapeHtml(label)}
71
75
  </span>
72
76
  <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"
77
+ 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
78
  ${bind ? `data-bind="${escapeHtml(bind)}"` : ""}
75
79
  ${name ? `name="${escapeHtml(name)}"` : ""}
76
80
  >
@@ -124,14 +128,14 @@ function renderOpenConnectionForm(modal) {
124
128
  ${renderError(modal.error)}
125
129
  <div class="flex items-center justify-end gap-3 pt-2">
126
130
  <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"
131
+ class="standard-button"
128
132
  data-action="close-modal"
129
133
  type="button"
130
134
  >
131
135
  Cancel
132
136
  </button>
133
137
  <button
134
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
138
+ class="standard-button"
135
139
  type="submit"
136
140
  >
137
141
  ${modal.submitting ? "Opening..." : "Open Database"}
@@ -198,14 +202,14 @@ function renderEditConnectionForm(modal) {
198
202
  ${renderError(modal.error)}
199
203
  <div class="flex items-center justify-end gap-3 pt-2">
200
204
  <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"
205
+ class="standard-button"
202
206
  data-action="close-modal"
203
207
  type="button"
204
208
  >
205
209
  Cancel
206
210
  </button>
207
211
  <button
208
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
212
+ class="standard-button"
209
213
  type="submit"
210
214
  >
211
215
  ${modal.submitting ? "Saving..." : "Save Changes"}
@@ -231,14 +235,14 @@ function renderCreateDatabaseForm(modal) {
231
235
  ${renderError(modal.error)}
232
236
  <div class="flex items-center justify-end gap-3 pt-2">
233
237
  <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"
238
+ class="standard-button"
235
239
  data-action="close-modal"
236
240
  type="button"
237
241
  >
238
242
  Cancel
239
243
  </button>
240
244
  <button
241
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
245
+ class="standard-button"
242
246
  type="submit"
243
247
  >
244
248
  ${modal.submitting ? "Creating..." : "Create Database"}
@@ -265,7 +269,7 @@ function renderImportTargetOptions(state) {
265
269
  Import Target
266
270
  </span>
267
271
  <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"
272
+ 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
273
  name="targetMode"
270
274
  >
271
275
  ${
@@ -286,7 +290,7 @@ function renderImportTargetOptions(state) {
286
290
  Recent Connection
287
291
  </span>
288
292
  <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"
293
+ 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
294
  name="targetConnectionId"
291
295
  >
292
296
  ${recentOptions}
@@ -324,14 +328,14 @@ function renderImportSqlForm(modal, state) {
324
328
  ${renderError(modal.error)}
325
329
  <div class="flex items-center justify-end gap-3 pt-2">
326
330
  <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"
331
+ class="standard-button"
328
332
  data-action="close-modal"
329
333
  type="button"
330
334
  >
331
335
  Cancel
332
336
  </button>
333
337
  <button
334
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
338
+ class="standard-button"
335
339
  type="submit"
336
340
  >
337
341
  ${modal.submitting ? "Importing..." : "Import SQL Dump"}
@@ -393,14 +397,14 @@ function renderDeleteRowConfirmForm(modal) {
393
397
  ${renderError(modal.error)}
394
398
  <div class="flex items-center justify-end gap-3 pt-2">
395
399
  <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"
400
+ class="standard-button"
397
401
  data-action="close-modal"
398
402
  type="button"
399
403
  >
400
404
  Cancel
401
405
  </button>
402
406
  <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"
407
+ class="delete-button"
404
408
  type="submit"
405
409
  >
406
410
  ${modal.submitting ? "Deleting..." : "Delete Row"}
@@ -633,14 +637,14 @@ function renderChartEditorForm(modal, state) {
633
637
  ${renderError(modal.error)}
634
638
  <div class="flex items-center justify-end gap-3 pt-2">
635
639
  <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"
640
+ class="standard-button"
637
641
  data-action="close-modal"
638
642
  type="button"
639
643
  >
640
644
  Cancel
641
645
  </button>
642
646
  <button
643
- class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
647
+ class="standard-button"
644
648
  type="submit"
645
649
  >
646
650
  ${modal.submitting ? "Saving..." : draft.mode === "edit" ? "Save Chart" : "Create Chart"}
@@ -666,14 +670,14 @@ function renderDeleteChartForm(modal) {
666
670
  ${renderError(modal.error)}
667
671
  <div class="flex items-center justify-end gap-3 pt-2">
668
672
  <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"
673
+ class="standard-button"
670
674
  data-action="close-modal"
671
675
  type="button"
672
676
  >
673
677
  Cancel
674
678
  </button>
675
679
  <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"
680
+ class="delete-button"
677
681
  type="submit"
678
682
  >
679
683
  ${modal.submitting ? "Deleting..." : "Delete Chart"}
@@ -747,7 +751,7 @@ export function renderModal(state) {
747
751
  </h2>
748
752
  </div>
749
753
  <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"
754
+ class="control-icon-button border border-outline-variant/20 text-on-surface-variant hover:bg-surface-container-highest hover:text-primary-container"
751
755
  data-action="close-modal"
752
756
  type="button"
753
757
  >
@@ -55,8 +55,7 @@ export function renderQueryEditor({
55
55
  editorVisible = true,
56
56
  historyVisible = true,
57
57
  }) {
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";
58
+ const secondaryButtonClass = "standard-button";
60
59
  const left = `
61
60
  <div class="flex items-center gap-2 bg-surface-container-lowest px-3 py-1">
62
61
  <span class="material-symbols-outlined text-xs text-[#FCE300]">database</span>
@@ -104,9 +103,8 @@ export function renderQueryEditor({
104
103
  ${exporting ? "Exporting..." : "Export CSV"}
105
104
  </button>
106
105
  <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"
106
+ class="signature-button"
108
107
  data-action="execute-query"
109
- style="--clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);"
110
108
  type="button"
111
109
  >
112
110
  ${executing ? "RUNNING..." : "EXECUTE"}