sqlite-hub 0.9.1 → 0.9.3

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.
package/changelog.md CHANGED
@@ -1,3 +1,10 @@
1
+ # v0.9.3
2
+
3
+ - improving rerendering
4
+ - fixed the sorting bug in charts
5
+ - data row size is stored in localstorage
6
+ - saved charts
7
+
1
8
  # v0.9.1
2
9
 
3
10
  - Improvements in tagging queue
@@ -22,7 +22,7 @@
22
22
  extend: {
23
23
  colors: {
24
24
  "on-secondary-fixed": "#1c1b1b",
25
- background: "#131313",
25
+ background: "rgb(var(--rgb-background) / <alpha-value>)",
26
26
  "inverse-primary": "#6a5f00",
27
27
  "primary-fixed": "#fde403",
28
28
  tertiary: "#fbfffe",
@@ -34,39 +34,39 @@
34
34
  "on-tertiary-fixed-variant": "#004f51",
35
35
  "secondary-fixed-dim": "#c8c6c5",
36
36
  "tertiary-fixed-dim": "#00dce1",
37
- "on-background": "#e5e2e1",
37
+ "on-background": "rgb(var(--rgb-on-surface) / <alpha-value>)",
38
38
  "surface-tint": "#dec800",
39
39
  "primary-fixed-dim": "#dec800",
40
- "on-surface": "#e5e2e1",
41
- "surface-container-low": "#1c1b1b",
40
+ "on-surface": "rgb(var(--rgb-on-surface) / <alpha-value>)",
41
+ "surface-container-low": "rgb(var(--rgb-surface-low) / <alpha-value>)",
42
42
  "on-tertiary": "#003738",
43
- "surface-container": "#201f1f",
44
- "on-surface-variant": "#cdc7ab",
43
+ "surface-container": "rgb(var(--rgb-surface-container) / <alpha-value>)",
44
+ "on-surface-variant": "rgb(var(--rgb-on-surface-variant) / <alpha-value>)",
45
45
  "on-primary-container": "#706400",
46
46
  "on-error": "#690005",
47
- "surface-container-high": "#2a2a2a",
48
- "surface-container-highest": "#353534",
49
- error: "#ffb4ab",
47
+ "surface-container-high": "rgb(var(--rgb-surface-high) / <alpha-value>)",
48
+ "surface-container-highest": "rgb(var(--rgb-surface-highest) / <alpha-value>)",
49
+ error: "rgb(var(--rgb-error) / <alpha-value>)",
50
50
  primary: "#fffeff",
51
- "outline-variant": "#4b4732",
51
+ "outline-variant": "rgb(var(--rgb-outline) / <alpha-value>)",
52
52
  "on-error-container": "#ffdad6",
53
53
  "on-secondary-fixed-variant": "#474746",
54
- "surface-container-lowest": "#0e0e0e",
54
+ "surface-container-lowest": "rgb(var(--rgb-surface-lowest) / <alpha-value>)",
55
55
  "inverse-surface": "#e5e2e1",
56
- surface: "#131313",
56
+ surface: "rgb(var(--rgb-surface) / <alpha-value>)",
57
57
  "tertiary-container": "#04faff",
58
58
  "on-tertiary-fixed": "#002021",
59
59
  "tertiary-fixed": "#2dfaff",
60
60
  "surface-variant": "#353534",
61
61
  "on-primary": "#373100",
62
- "primary-container": "#fce300",
62
+ "primary-container": "rgb(var(--rgb-primary) / <alpha-value>)",
63
63
  "inverse-on-surface": "#313030",
64
64
  secondary: "#c8c6c5",
65
65
  "on-secondary-container": "#b7b5b4",
66
66
  "on-primary-fixed": "#201c00",
67
67
  "secondary-container": "#474746",
68
- "surface-dim": "#131313",
69
- "surface-bright": "#3a3939",
68
+ "surface-dim": "rgb(var(--rgb-surface) / <alpha-value>)",
69
+ "surface-bright": "rgb(var(--rgb-surface-bright) / <alpha-value>)",
70
70
  "secondary-fixed": "#e5e2e1",
71
71
  },
72
72
  fontFamily: {
@@ -89,14 +89,14 @@
89
89
  @layer components {
90
90
  .signature-button {
91
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);
92
+ box-shadow: 0 0 18px -10px rgb(var(--rgb-primary) / 0.7);
93
93
  clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
94
94
  }
95
95
 
96
96
  .signature-button:hover {
97
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);
98
+ border-color: rgb(var(--rgb-primary) / 0.55);
99
+ box-shadow: 0 0 24px -8px rgb(var(--rgb-primary) / 0.92);
100
100
  filter: brightness(1.03);
101
101
  }
102
102
 
@@ -118,12 +118,12 @@
118
118
 
119
119
  .standard-button:hover {
120
120
  @apply bg-surface-container-highest text-primary-container;
121
- border-color: rgba(252, 227, 0, 0.24);
121
+ border-color: var(--primary-alpha-24);
122
122
  }
123
123
 
124
124
  .standard-button.is-active {
125
125
  @apply bg-surface-container-high text-primary-container;
126
- border-color: rgba(252, 227, 0, 0.3);
126
+ border-color: var(--primary-alpha-30);
127
127
  }
128
128
 
129
129
  .standard-button:disabled,
@@ -166,6 +166,7 @@
166
166
  </style>
167
167
  <link href="styles/tokens.css" rel="stylesheet" />
168
168
  <link href="styles/base.css" rel="stylesheet" />
169
+ <link href="styles/utilities.css" rel="stylesheet" />
169
170
  <link href="styles/layout.css" rel="stylesheet" />
170
171
  <link href="styles/components.css" rel="stylesheet" />
171
172
  <link href="styles/views.css" rel="stylesheet" />
@@ -69,6 +69,7 @@ import {
69
69
  toggleDataTablesPanel,
70
70
  setCurrentQuery,
71
71
  setChartsHeightPreset,
72
+ setChartsHistoryTab,
72
73
  setEditorPanelVisibility,
73
74
  setEditorTab,
74
75
  submitDeleteChartConfirmation,
@@ -111,7 +112,7 @@ import {
111
112
  applyCurrentMediaTaggingSelection,
112
113
  removeCurrentTableDesignerColumn,
113
114
  } from './store.js';
114
- import { renderChartsView } from './views/charts.js';
115
+ import { renderChartsDetail, renderChartsView } from './views/charts.js';
115
116
  import { renderConnectionsView } from './views/connections.js';
116
117
  import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
117
118
  import { renderEditorView } from './views/editor.js';
@@ -146,6 +147,7 @@ let lastRenderedMainMarkup = '';
146
147
  let lastRenderedPanelMarkup = '';
147
148
  let lastRenderedModalMarkup = '';
148
149
  let lastRenderedToastMarkup = '';
150
+ let lastRenderedChartsHistorySignature = '';
149
151
  let lastRenderedPanelOpen = false;
150
152
  let lastRenderedLockedRoute = false;
151
153
  let pendingNewTableDesignerAutofocus = false;
@@ -417,6 +419,188 @@ function syncQueryHistorySelectionUi(selectedHistoryId = null) {
417
419
  return true;
418
420
  }
419
421
 
422
+ function isEditorRouteName(routeName) {
423
+ return routeName === 'editor' || routeName === 'editorResults';
424
+ }
425
+
426
+ function captureQueryHistoryScrollState() {
427
+ if (!isEditorRouteName(lastRenderedRouteName)) {
428
+ return null;
429
+ }
430
+
431
+ const scrollNode = shellRefs.view.querySelector('[data-query-history-scroll]');
432
+
433
+ if (!(scrollNode instanceof HTMLElement)) {
434
+ return null;
435
+ }
436
+
437
+ return {
438
+ committedSearch: scrollNode.dataset.queryHistoryCommittedSearch ?? '',
439
+ historyTab: scrollNode.dataset.queryHistoryTab ?? '',
440
+ routeName: lastRenderedRouteName,
441
+ renderedItemCount: scrollNode.querySelectorAll('.query-history-item').length,
442
+ renderedLoadingMore: scrollNode.dataset.queryHistoryLoadingMore === 'true',
443
+ searchInput: scrollNode.dataset.queryHistorySearch ?? '',
444
+ scrollLeft: scrollNode.scrollLeft,
445
+ scrollTop: scrollNode.scrollTop,
446
+ };
447
+ }
448
+
449
+ function shouldRestoreQueryHistoryScroll(snapshot, state) {
450
+ if (!snapshot || !isEditorRouteName(state.route.name) || snapshot.routeName !== state.route.name) {
451
+ return false;
452
+ }
453
+
454
+ if (!state.editor.historyPanelVisible) {
455
+ return false;
456
+ }
457
+
458
+ if (
459
+ snapshot.historyTab !== state.editor.historyTab ||
460
+ snapshot.searchInput !== state.editor.historySearchInput ||
461
+ snapshot.committedSearch !== state.editor.historySearch
462
+ ) {
463
+ return false;
464
+ }
465
+
466
+ return (
467
+ state.editor.historyLoadingMore ||
468
+ snapshot.renderedLoadingMore ||
469
+ state.editor.history.length > snapshot.renderedItemCount
470
+ );
471
+ }
472
+
473
+ function restoreQueryHistoryScrollState(snapshot) {
474
+ const scrollNode = shellRefs.view.querySelector('[data-query-history-scroll]');
475
+
476
+ if (!(scrollNode instanceof HTMLElement)) {
477
+ return false;
478
+ }
479
+
480
+ scrollNode.scrollLeft = snapshot.scrollLeft;
481
+ scrollNode.scrollTop = snapshot.scrollTop;
482
+ return true;
483
+ }
484
+
485
+ function buildChartsHistorySignature(state) {
486
+ if (state.route.name !== 'charts') {
487
+ return '';
488
+ }
489
+
490
+ const historyVisible = state.editor.historyPanelVisible !== false || !state.charts.selectedHistoryId;
491
+
492
+ if (!historyVisible) {
493
+ return 'charts-history:hidden';
494
+ }
495
+
496
+ const queries = state.charts.queries ?? [];
497
+
498
+ if (!queries.length) {
499
+ if (state.charts.loading) {
500
+ return 'charts-history:loading-empty';
501
+ }
502
+
503
+ if (state.charts.error) {
504
+ return `charts-history:error:${state.charts.error.code}:${state.charts.error.message}`;
505
+ }
506
+
507
+ return 'charts-history:empty';
508
+ }
509
+
510
+ return JSON.stringify({
511
+ tab: state.charts.historyTab ?? 'recent',
512
+ queries: queries.map(item => [
513
+ item.id,
514
+ item.displayTitle,
515
+ item.previewSql,
516
+ Boolean(item.isSaved),
517
+ Array.isArray(item.chartTypes) ? item.chartTypes.join(',') : '',
518
+ ]),
519
+ });
520
+ }
521
+
522
+ function syncChartsHistorySelectionUi(state) {
523
+ const selectedHistoryId = String(state.charts.selectedHistoryId ?? '');
524
+ const historyButtons = shellRefs.view.querySelectorAll(
525
+ '.charts-view__sidebar [data-action="navigate"][data-history-id]',
526
+ );
527
+
528
+ for (const button of historyButtons) {
529
+ if (!(button instanceof HTMLElement)) {
530
+ continue;
531
+ }
532
+
533
+ const itemNode = button.closest('[data-charts-history-item]');
534
+ const isSelected = button.dataset.historyId === selectedHistoryId;
535
+ const titleNode = button.querySelector('[data-charts-history-title]');
536
+ const selectionNode = itemNode instanceof HTMLElement ? itemNode : button;
537
+
538
+ selectionNode.classList.toggle('border-primary-container/30', isSelected);
539
+ selectionNode.classList.toggle('bg-surface-container-high', isSelected);
540
+ selectionNode.classList.toggle('border-outline-variant/10', !isSelected);
541
+ selectionNode.classList.toggle('bg-surface-container-lowest', !isSelected);
542
+ selectionNode.classList.toggle('hover:bg-surface-container-high', !isSelected);
543
+
544
+ if (titleNode instanceof HTMLElement) {
545
+ titleNode.classList.toggle('text-primary-container', isSelected);
546
+ titleNode.classList.toggle('text-on-surface', !isSelected);
547
+ }
548
+ }
549
+
550
+ return true;
551
+ }
552
+
553
+ function syncChartsSavedToggleUi(actionNode, nextValue) {
554
+ const itemNode = actionNode.closest('[data-charts-history-item]');
555
+
556
+ actionNode.classList.toggle('is-active', nextValue);
557
+ actionNode.dataset.nextValue = nextValue ? 'false' : 'true';
558
+ actionNode.title = nextValue ? 'Remove from saved' : 'Save query';
559
+
560
+ const iconNode = actionNode.querySelector('.material-symbols-outlined');
561
+ if (iconNode instanceof HTMLElement) {
562
+ iconNode.textContent = nextValue ? 'bookmark' : 'bookmark_add';
563
+ }
564
+
565
+ if (itemNode instanceof HTMLElement) {
566
+ itemNode.querySelector('[data-charts-saved-badge]')?.toggleAttribute('hidden', !nextValue);
567
+
568
+ if (!nextValue && getState().charts.historyTab === 'saved') {
569
+ itemNode.remove();
570
+ }
571
+ }
572
+
573
+ const countNode = shellRefs.view.querySelector('[data-charts-history-count]');
574
+ if (countNode instanceof HTMLElement) {
575
+ const state = getState();
576
+ const count =
577
+ state.charts.historyTab === 'saved'
578
+ ? (state.charts.queries ?? []).filter(item => item.isSaved).length
579
+ : (state.charts.queries ?? []).length;
580
+ countNode.textContent = String(count);
581
+ }
582
+ }
583
+
584
+ function patchChartsDetailUi(state) {
585
+ const chartsView = shellRefs.view.querySelector('.charts-view');
586
+ const detailNode = chartsView?.querySelector('.charts-view__detail');
587
+
588
+ if (!(chartsView instanceof HTMLElement) || !(detailNode instanceof HTMLElement)) {
589
+ return false;
590
+ }
591
+
592
+ const historyVisible = state.editor.historyPanelVisible !== false || !state.charts.selectedHistoryId;
593
+ const sidebarNode = chartsView.querySelector('.charts-view__sidebar');
594
+
595
+ if (Boolean(sidebarNode) !== historyVisible) {
596
+ return false;
597
+ }
598
+
599
+ detailNode.innerHTML = renderChartsDetail(state);
600
+ syncChartsHistorySelectionUi(state);
601
+ return true;
602
+ }
603
+
420
604
  function readFileAsBase64(file) {
421
605
  return new Promise((resolve, reject) => {
422
606
  const reader = new FileReader();
@@ -755,6 +939,7 @@ function renderApp(state) {
755
939
  const statusBarMarkup = renderStatusBar(state);
756
940
  const modalMarkup = renderModal(state);
757
941
  const toastMarkup = renderToasts(state.toasts);
942
+ const chartsHistorySignature = buildChartsHistorySignature(state);
758
943
  const isLockedRoute = [
759
944
  'editor',
760
945
  'editorResults',
@@ -772,6 +957,7 @@ function renderApp(state) {
772
957
  const mainChanged = main !== lastRenderedMainMarkup;
773
958
  const panelChanged = panel !== lastRenderedPanelMarkup;
774
959
  const modalChanged = modalMarkup !== lastRenderedModalMarkup;
960
+ const chartsHistoryChanged = chartsHistorySignature !== lastRenderedChartsHistorySignature;
775
961
  const panelOpenChanged = panelOpen !== lastRenderedPanelOpen;
776
962
  const lockedRouteChanged = isLockedRoute !== lastRenderedLockedRoute;
777
963
  const shellMarkupUnchanged =
@@ -796,6 +982,7 @@ function renderApp(state) {
796
982
  }
797
983
 
798
984
  const focusedInput = captureFocusedInputState();
985
+ const queryHistoryScrollState = captureQueryHistoryScrollState();
799
986
  const isEnteringNewTableDesignerRoute =
800
987
  state.route.name === 'tableDesigner' && state.route.params?.isNew && previousRoutePath !== state.route.path;
801
988
 
@@ -805,9 +992,20 @@ function renderApp(state) {
805
992
  pendingNewTableDesignerAutofocus = false;
806
993
  }
807
994
 
808
- if (mainChanged) {
809
- teardownStructureGraph();
995
+ const canPatchChartsMain =
996
+ mainChanged && previousRouteName === 'charts' && state.route.name === 'charts' && !chartsHistoryChanged;
997
+ let mainPatched = false;
998
+
999
+ if (canPatchChartsMain) {
810
1000
  teardownQueryChartRenderer();
1001
+ mainPatched = patchChartsDetailUi(state);
1002
+ }
1003
+
1004
+ if (mainChanged) {
1005
+ if (!mainPatched) {
1006
+ teardownStructureGraph();
1007
+ teardownQueryChartRenderer();
1008
+ }
811
1009
  }
812
1010
 
813
1011
  if (topNavChanged) {
@@ -829,7 +1027,7 @@ function renderApp(state) {
829
1027
  shellRefs.statusBar.innerHTML = statusBarMarkup;
830
1028
  }
831
1029
 
832
- if (mainChanged) {
1030
+ if (mainChanged && !mainPatched) {
833
1031
  shellRefs.view.innerHTML = main;
834
1032
  }
835
1033
 
@@ -853,6 +1051,10 @@ function renderApp(state) {
853
1051
  shellRefs.shell.classList.toggle('panel-open', panelOpen);
854
1052
  }
855
1053
 
1054
+ if (shouldRestoreQueryHistoryScroll(queryHistoryScrollState, state)) {
1055
+ restoreQueryHistoryScrollState(queryHistoryScrollState);
1056
+ }
1057
+
856
1058
  if (pendingQueryEditorFocus && (state.route.name === 'editor' || state.route.name === 'editorResults')) {
857
1059
  if (focusQueryEditorInput()) {
858
1060
  pendingQueryEditorFocus = false;
@@ -883,6 +1085,7 @@ function renderApp(state) {
883
1085
  lastRenderedPanelMarkup = panel;
884
1086
  lastRenderedModalMarkup = modalMarkup;
885
1087
  lastRenderedToastMarkup = toastMarkup;
1088
+ lastRenderedChartsHistorySignature = chartsHistorySignature;
886
1089
  lastRenderedPanelOpen = panelOpen;
887
1090
  lastRenderedLockedRoute = isLockedRoute;
888
1091
 
@@ -1063,6 +1266,20 @@ async function handleAction(actionNode) {
1063
1266
  );
1064
1267
  }
1065
1268
  return;
1269
+ case 'toggle-charts-query-history-saved':
1270
+ if (actionNode.dataset.historyId) {
1271
+ const nextValue = actionNode.dataset.nextValue === 'true';
1272
+ const updated = await toggleQueryHistorySavedState(actionNode.dataset.historyId, nextValue, {
1273
+ notify: false,
1274
+ refresh: false,
1275
+ toast: false,
1276
+ });
1277
+
1278
+ if (updated) {
1279
+ syncChartsSavedToggleUi(actionNode, nextValue);
1280
+ }
1281
+ }
1282
+ return;
1066
1283
  case 'toggle-charts-sql-panel':
1067
1284
  toggleChartsSqlPanel();
1068
1285
  return;
@@ -1074,6 +1291,11 @@ async function handleAction(actionNode) {
1074
1291
  setChartsHeightPreset(actionNode.dataset.preset);
1075
1292
  }
1076
1293
  return;
1294
+ case 'set-charts-history-tab':
1295
+ if (actionNode.dataset.tab) {
1296
+ setChartsHistoryTab(actionNode.dataset.tab);
1297
+ }
1298
+ return;
1077
1299
  case 'export-query-chart-png':
1078
1300
  if (actionNode.dataset.chartId && !exportQueryChartAsPng(actionNode.dataset.chartId)) {
1079
1301
  showToast('The selected chart is not ready for PNG export.', 'alert');
@@ -481,13 +481,22 @@ function renderChartEditorForm(modal, state) {
481
481
  bind: "query-chart-draft-config:y_column",
482
482
  })}
483
483
  </div>
484
- <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
484
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
485
+ ${renderSelectField({
486
+ label: "Sort By",
487
+ value: draft.config?.sort_by ?? "y",
488
+ options: [
489
+ { value: "x", label: "X column" },
490
+ { value: "y", label: "Y value" },
491
+ ],
492
+ bind: "query-chart-draft-config:sort_by",
493
+ })}
485
494
  ${renderSelectField({
486
495
  label: "Sort Direction",
487
- value: draft.config?.sort_direction ?? "asc",
496
+ value: draft.config?.sort_direction ?? "desc",
488
497
  options: [
489
- { value: "asc", label: "Ascending" },
490
- { value: "desc", label: "Descending" },
498
+ { value: "asc", label: "Ascending / smallest first" },
499
+ { value: "desc", label: "Descending / largest first" },
491
500
  ],
492
501
  bind: "query-chart-draft-config:sort_direction",
493
502
  })}
@@ -139,6 +139,7 @@ export function renderQueryHistoryPanel({
139
139
  error = null,
140
140
  activeTab = "recent",
141
141
  search = "",
142
+ committedSearch = "",
142
143
  total = 0,
143
144
  hasMore = false,
144
145
  activeHistoryId = null,
@@ -176,7 +177,14 @@ export function renderQueryHistoryPanel({
176
177
  />
177
178
  </label>
178
179
  </div>
179
- <div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-3 py-3">
180
+ <div
181
+ class="custom-scrollbar min-h-0 flex-1 overflow-auto px-3 py-3"
182
+ data-query-history-committed-search="${escapeHtml(committedSearch)}"
183
+ data-query-history-loading-more="${loadingMore ? "true" : "false"}"
184
+ data-query-history-search="${escapeHtml(search)}"
185
+ data-query-history-scroll
186
+ data-query-history-tab="${escapeHtml(activeTab)}"
187
+ >
180
188
  ${
181
189
  error
182
190
  ? `
@@ -50,15 +50,7 @@ function renderReadonlyField(label, value) {
50
50
  <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
51
51
  <div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
52
52
  <span>${escapeHtml(displayLabel)}</span>
53
- ${badges
54
- .map(
55
- (badge) => `
56
- <span class="border border-outline-variant/20 bg-surface-container px-2 py-1 text-[9px] text-primary-container">
57
- ${escapeHtml(badge)}
58
- </span>
59
- `
60
- )
61
- .join("")}
53
+ ${badges.map((badge) => renderFieldBadge(badge)).join("")}
62
54
  </div>
63
55
  ${
64
56
  jsonPreview
@@ -72,37 +64,68 @@ function renderReadonlyField(label, value) {
72
64
  function renderEditableField(field) {
73
65
  const badges = Array.isArray(field.badges) ? field.badges : [];
74
66
  const jsonPreview = getJsonPreview(field.value);
67
+ const inputType = field.inputType === "number" ? "number" : "text";
68
+ const numberStep = field.numberStep === "1" ? "1" : "any";
75
69
 
76
70
  return `
77
71
  <label class="block space-y-2">
78
72
  <span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
79
73
  <span>${escapeHtml(field.label ?? field.name)}</span>
80
- ${badges
81
- .map(
82
- (badge) => `
83
- <span class="border border-outline-variant/20 bg-surface-container px-2 py-1 text-[9px] text-primary-container">
84
- ${escapeHtml(badge)}
85
- </span>
86
- `
87
- )
88
- .join("")}
74
+ ${badges.map((badge) => renderFieldBadge(badge)).join("")}
89
75
  </span>
90
76
  ${
91
77
  jsonPreview
92
78
  ? renderJsonViewer(jsonPreview, "JSON Preview")
93
79
  : ""
94
80
  }
95
- <textarea
96
- 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 ${
97
- jsonPreview ? "min-h-[14rem] font-mono leading-6" : "min-h-[56px]"
98
- }"
99
- name="field:${escapeHtml(field.name)}"
100
- spellcheck="false"
101
- >${escapeHtml(field.value ?? "")}</textarea>
81
+ ${
82
+ inputType === "number" && !jsonPreview
83
+ ? `
84
+ <input
85
+ 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"
86
+ name="field:${escapeHtml(field.name)}"
87
+ step="${escapeHtml(numberStep)}"
88
+ type="number"
89
+ value="${escapeHtml(field.value ?? "")}"
90
+ />
91
+ `
92
+ : `
93
+ <textarea
94
+ 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 ${
95
+ jsonPreview ? "min-h-[14rem] font-mono leading-6" : "min-h-[56px]"
96
+ }"
97
+ name="field:${escapeHtml(field.name)}"
98
+ spellcheck="false"
99
+ >${escapeHtml(field.value ?? "")}</textarea>
100
+ `
101
+ }
102
102
  </label>
103
103
  `;
104
104
  }
105
105
 
106
+ function getFieldBadgeClassName(tone) {
107
+ if (tone === "primary-key") {
108
+ return "border-primary-container/35 bg-primary-container/15 text-primary-container";
109
+ }
110
+
111
+ if (tone === "foreign-key") {
112
+ return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
113
+ }
114
+
115
+ return "border-outline-variant/20 bg-surface-container text-on-surface-variant";
116
+ }
117
+
118
+ function renderFieldBadge(badge) {
119
+ const label = typeof badge === "object" ? badge.label : badge;
120
+ const tone = typeof badge === "object" ? badge.tone : "";
121
+
122
+ return `
123
+ <span class="border px-2 py-1 text-[9px] ${getFieldBadgeClassName(tone)}">
124
+ ${escapeHtml(label)}
125
+ </span>
126
+ `;
127
+ }
128
+
106
129
  export function renderRowEditorPanel({
107
130
  title,
108
131
  sectionLabel = "Row Editor",
@@ -5,6 +5,7 @@ import {
5
5
  formatQueryChartAxisValue,
6
6
  getAnalysisColumn,
7
7
  sortQueryChartRows,
8
+ sortQueryChartRowsByNumericColumn,
8
9
  } from "./queryCharts.js";
9
10
 
10
11
  const CHART_PALETTE = ["#FCE300", "#2DFAFF", "#FFB4AB", "#CDC7AB", "#7DD3FC", "#86EFAC"];
@@ -55,7 +56,10 @@ function buildLineLabelConfig(enabled) {
55
56
  }
56
57
 
57
58
  export function buildBarChartOption(chart, rows) {
58
- const sortedRows = sortQueryChartRows(rows, chart.config.x_column, chart.config.sort_direction);
59
+ const sortedRows =
60
+ chart.config.sort_by === "y"
61
+ ? sortQueryChartRowsByNumericColumn(rows, chart.config.y_column, chart.config.sort_direction)
62
+ : sortQueryChartRows(rows, chart.config.x_column, chart.config.sort_direction);
59
63
 
60
64
  return {
61
65
  ...buildCommonOption(),
@@ -312,7 +312,43 @@ function getFirstColumn(columns, predicate) {
312
312
 
313
313
  function getNextNumericColumn(analysis, excludedNames = []) {
314
314
  const excluded = new Set(excludedNames);
315
- return getFirstColumn(analysis?.numberColumns, (column) => !excluded.has(column.name));
315
+ const candidates = (analysis?.numberColumns ?? []).filter((column) => !excluded.has(column.name));
316
+
317
+ if (!candidates.length) {
318
+ return null;
319
+ }
320
+
321
+ const scoredCandidates = candidates
322
+ .map((column, index) => ({
323
+ column,
324
+ index,
325
+ score: getNumericMeasureScore(column.name),
326
+ }))
327
+ .sort((left, right) => right.score - left.score || left.index - right.index);
328
+
329
+ return scoredCandidates[0]?.column ?? null;
330
+ }
331
+
332
+ function getNumericMeasureScore(columnName = "") {
333
+ const normalized = String(columnName ?? "")
334
+ .trim()
335
+ .toLowerCase();
336
+
337
+ let score = 0;
338
+
339
+ if (
340
+ /(count|total|sum|avg|average|amount|score|size|value|price|cost|rate|percent|pct|percentage|number)/.test(
341
+ normalized
342
+ )
343
+ ) {
344
+ score += 20;
345
+ }
346
+
347
+ if (/(^|_)(id|uuid|pk|key)$/.test(normalized) || /(^|_)id$/.test(normalized)) {
348
+ score -= 40;
349
+ }
350
+
351
+ return score;
316
352
  }
317
353
 
318
354
  function getPrimaryDimensionColumn(analysis) {
@@ -406,7 +442,8 @@ export function buildSuggestedChartConfig(chartType, analysis) {
406
442
  y_column: firstNumeric?.name ?? "",
407
443
  show_legend: true,
408
444
  show_labels: false,
409
- sort_direction: "asc",
445
+ sort_by: "y",
446
+ sort_direction: "desc",
410
447
  };
411
448
  }
412
449
  }
@@ -524,6 +561,17 @@ export function sortQueryChartRows(rows, columnName, direction = "asc") {
524
561
  );
525
562
  }
526
563
 
564
+ export function sortQueryChartRowsByNumericColumn(rows, columnName, direction = "asc") {
565
+ const multiplier = direction === "desc" ? -1 : 1;
566
+
567
+ return [...(rows ?? [])].sort((left, right) => {
568
+ const leftValue = coerceNumericChartValue(left?.[columnName]);
569
+ const rightValue = coerceNumericChartValue(right?.[columnName]);
570
+
571
+ return compareValues(leftValue, rightValue) * multiplier;
572
+ });
573
+ }
574
+
527
575
  export function formatQueryChartAxisValue(value) {
528
576
  if (value === null || value === undefined) {
529
577
  return "NULL";