@quantumwake/terminal-ux-dashboard-components 0.1.10 → 0.1.13

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/dist/index.d.cts CHANGED
@@ -98,8 +98,19 @@ interface ChartStyle {
98
98
  xLegendOffset: number;
99
99
  yLegendPosition: LegendPosition;
100
100
  yLegendOffset: number;
101
+ xAxisLabel: string;
102
+ yAxisLabel: string;
103
+ showXLegend: boolean;
104
+ showYLegend: boolean;
105
+ legendBold: boolean;
106
+ legendHighlight: string;
107
+ xTickRotation: number;
108
+ tickTruncate: number;
101
109
  legendAnchor: LegendAnchor;
102
110
  titleAlign: TitleAlign;
111
+ titleBold: boolean;
112
+ titleBackground: string;
113
+ titleColor: string;
103
114
  }
104
115
  declare const DEFAULT_CHART_STYLE: ChartStyle;
105
116
  declare const LEGEND_ANCHORS: LegendAnchor[];
@@ -121,6 +132,9 @@ declare const buildNivoTheme: (style?: Partial<ChartStyle> | null) => {
121
132
  text: {
122
133
  fill: string;
123
134
  fontSize: number;
135
+ fontWeight: number;
136
+ outlineWidth: number;
137
+ outlineColor: string;
124
138
  };
125
139
  };
126
140
  };
@@ -176,11 +190,9 @@ declare const legendConfig: (style?: Partial<ChartStyle> | null) => {
176
190
  symbolShape: "circle";
177
191
  itemTextColor: string;
178
192
  } | null;
179
- declare const axisLegend: (style: Partial<ChartStyle> | null | undefined, axis: "x" | "y", legendText: string) => {
180
- legend: string;
181
- legendPosition: LegendPosition;
182
- legendOffset: number;
183
- };
193
+ declare const axisLegend: (style: Partial<ChartStyle> | null | undefined, axis: "x" | "y", columnName: string, opts?: {
194
+ numeric?: boolean;
195
+ }) => Record<string, unknown>;
184
196
 
185
197
  type AggFn = 'count' | 'distinct' | 'sum' | 'avg' | 'min' | 'max';
186
198
  declare const groupBy: (records: Row[], column: string) => Record<string, Row[]>;
package/dist/index.d.ts CHANGED
@@ -98,8 +98,19 @@ interface ChartStyle {
98
98
  xLegendOffset: number;
99
99
  yLegendPosition: LegendPosition;
100
100
  yLegendOffset: number;
101
+ xAxisLabel: string;
102
+ yAxisLabel: string;
103
+ showXLegend: boolean;
104
+ showYLegend: boolean;
105
+ legendBold: boolean;
106
+ legendHighlight: string;
107
+ xTickRotation: number;
108
+ tickTruncate: number;
101
109
  legendAnchor: LegendAnchor;
102
110
  titleAlign: TitleAlign;
111
+ titleBold: boolean;
112
+ titleBackground: string;
113
+ titleColor: string;
103
114
  }
104
115
  declare const DEFAULT_CHART_STYLE: ChartStyle;
105
116
  declare const LEGEND_ANCHORS: LegendAnchor[];
@@ -121,6 +132,9 @@ declare const buildNivoTheme: (style?: Partial<ChartStyle> | null) => {
121
132
  text: {
122
133
  fill: string;
123
134
  fontSize: number;
135
+ fontWeight: number;
136
+ outlineWidth: number;
137
+ outlineColor: string;
124
138
  };
125
139
  };
126
140
  };
@@ -176,11 +190,9 @@ declare const legendConfig: (style?: Partial<ChartStyle> | null) => {
176
190
  symbolShape: "circle";
177
191
  itemTextColor: string;
178
192
  } | null;
179
- declare const axisLegend: (style: Partial<ChartStyle> | null | undefined, axis: "x" | "y", legendText: string) => {
180
- legend: string;
181
- legendPosition: LegendPosition;
182
- legendOffset: number;
183
- };
193
+ declare const axisLegend: (style: Partial<ChartStyle> | null | undefined, axis: "x" | "y", columnName: string, opts?: {
194
+ numeric?: boolean;
195
+ }) => Record<string, unknown>;
184
196
 
185
197
  type AggFn = 'count' | 'distinct' | 'sum' | 'avg' | 'min' | 'max';
186
198
  declare const groupBy: (records: Row[], column: string) => Record<string, Row[]>;
package/dist/index.js CHANGED
@@ -135,8 +135,10 @@ var buildChartSQL = ({
135
135
  }
136
136
  case "scatter": {
137
137
  if (!x || !y) return null;
138
- const w = where ? `${where} AND ${qIdent(x)} IS NOT NULL AND ${qIdent(y)} IS NOT NULL` : ` WHERE ${qIdent(x)} IS NOT NULL AND ${qIdent(y)} IS NOT NULL`;
139
- return `SELECT ${qIdent(x)} AS x, ${qIdent(y)} AS y FROM ${TABLE}${w} LIMIT ${MAX_POINTS}`;
138
+ const nx = `TRY_CAST(${qIdent(x)} AS DOUBLE)`, ny = `TRY_CAST(${qIdent(y)} AS DOUBLE)`;
139
+ const extra = `${nx} IS NOT NULL AND ${ny} IS NOT NULL`;
140
+ const w = where ? `${where} AND ${extra}` : ` WHERE ${extra}`;
141
+ return `SELECT ${nx} AS x, ${ny} AS y FROM ${TABLE}${w} LIMIT ${MAX_POINTS}`;
140
142
  }
141
143
  case "heatmap": {
142
144
  if (!x || !y) return null;
@@ -168,7 +170,10 @@ var shapeChartData = (chartType, rows, { yFields = [] } = {}) => {
168
170
  case "line":
169
171
  return [{ id: "series", data: rows.map((r) => ({ x: String(r.x), y: Number(r.y) || 0 })) }];
170
172
  case "scatter":
171
- return [{ id: "points", data: rows.map((r) => ({ x: Number(r.x) || 0, y: Number(r.y) || 0 })) }];
173
+ return [{
174
+ id: "points",
175
+ data: rows.map((r) => ({ x: Number(r.x), y: Number(r.y) })).filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y))
176
+ }];
172
177
  case "heatmap": {
173
178
  const rowKeys = [];
174
179
  const colKeys = [];
@@ -210,10 +215,24 @@ var DEFAULT_CHART_STYLE = {
210
215
  xLegendOffset: 46,
211
216
  yLegendPosition: "middle",
212
217
  yLegendOffset: -50,
218
+ // Custom axis title text + visibility.
219
+ xAxisLabel: "",
220
+ yAxisLabel: "",
221
+ showXLegend: true,
222
+ showYLegend: true,
223
+ // Axis title emphasis.
224
+ legendBold: false,
225
+ legendHighlight: "",
226
+ // Tick overflow handling.
227
+ xTickRotation: -35,
228
+ tickTruncate: 0,
213
229
  // Series legend placement (charts that have one: pie, grouped bar).
214
230
  legendAnchor: "right",
215
- // Panel title alignment (rendered by DashboardRenderer's panel header).
216
- titleAlign: "left"
231
+ // Panel title (rendered by DashboardRenderer's panel header).
232
+ titleAlign: "left",
233
+ titleBold: false,
234
+ titleBackground: "",
235
+ titleColor: ""
217
236
  };
218
237
  var LEGEND_ANCHORS = ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"];
219
238
  var withStyleDefaults = (style) => ({
@@ -227,7 +246,17 @@ var buildNivoTheme = (style) => {
227
246
  text: { fill: s.textColor, fontSize: s.fontSize },
228
247
  axis: {
229
248
  ticks: { text: { fill: s.textColor, fontSize: s.fontSize } },
230
- legend: { text: { fill: s.legendColor, fontSize: s.fontSize + 2 } }
249
+ legend: {
250
+ text: {
251
+ fill: s.legendColor,
252
+ fontSize: s.fontSize + 2,
253
+ fontWeight: s.legendBold ? 700 : 400,
254
+ // A highlight is drawn as a text outline (halo) — the clean,
255
+ // SVG-native way to make an axis title stand out.
256
+ outlineWidth: s.legendHighlight ? 3 : 0,
257
+ outlineColor: s.legendHighlight || "transparent"
258
+ }
259
+ }
231
260
  },
232
261
  grid: { line: { stroke: s.gridColor } },
233
262
  crosshair: { line: { stroke: s.textColor, strokeDasharray: "6 6" } },
@@ -256,14 +285,25 @@ var legendConfig = (style) => {
256
285
  const ty = s.legendAnchor.includes("top") ? 10 : -10;
257
286
  return { ...base, translateX: tx, translateY: ty };
258
287
  };
259
- var axisLegend = (style, axis, legendText) => {
288
+ var truncate = (v, n) => {
289
+ const str = String(v);
290
+ return n > 0 && str.length > n ? `${str.slice(0, n)}\u2026` : str;
291
+ };
292
+ var axisLegend = (style, axis, columnName, opts2 = {}) => {
260
293
  const s = withStyleDefaults(style);
261
294
  const isX = axis === "x";
262
- return {
263
- legend: legendText,
264
- legendPosition: isX ? s.xLegendPosition : s.yLegendPosition,
265
- legendOffset: isX ? s.xLegendOffset : s.yLegendOffset
266
- };
295
+ const show = isX ? s.showXLegend : s.showYLegend;
296
+ const label = (isX ? s.xAxisLabel : s.yAxisLabel) || columnName;
297
+ const out = {};
298
+ if (show && label) {
299
+ out.legend = label;
300
+ out.legendPosition = isX ? s.xLegendPosition : s.yLegendPosition;
301
+ out.legendOffset = isX ? s.xLegendOffset : s.yLegendOffset;
302
+ }
303
+ if (isX) out.tickRotation = s.xTickRotation;
304
+ if (opts2.numeric) out.format = (v) => Number(v).toLocaleString();
305
+ else if (s.tickTruncate > 0) out.format = (v) => truncate(v, s.tickTruncate);
306
+ return out;
267
307
  };
268
308
 
269
309
  // src/dataShape.ts
@@ -327,8 +367,8 @@ function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: pre
327
367
  padding: 0.3,
328
368
  colors: ["rgba(74, 222, 128, 0.8)"],
329
369
  borderColor: { from: "color", modifiers: [["darker", 1.6]] },
330
- axisBottom: { tickSize: 5, tickPadding: 5, tickRotation: -35 },
331
- axisLeft: { tickSize: 5, tickPadding: 5, format: (v) => Number(v).toLocaleString() },
370
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", groupColumn) },
371
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", valueColumn, { numeric: true }) },
332
372
  labelSkipWidth: 12,
333
373
  labelSkipHeight: 12,
334
374
  labelTextColor: { from: "color", modifiers: [["darker", 3]] },
@@ -401,8 +441,8 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
401
441
  pointBorderWidth: 2,
402
442
  pointBorderColor: { from: "serieColor" },
403
443
  enableGridX: false,
404
- axisBottom: { tickSize: 5, tickPadding: 5, tickRotation: -35, ...axisLegend(style, "x", xColumn) },
405
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn) },
444
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn) },
445
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn, { numeric: true }) },
406
446
  useMesh: true,
407
447
  theme: buildNivoTheme(style)
408
448
  }
@@ -411,7 +451,7 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
411
451
  function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
412
452
  const computed = useMemo(() => {
413
453
  if (presetData || !records) return [];
414
- const points = records.filter((r) => r[xColumn] != null && r[yColumn] != null).map((r) => ({ x: Number(r[xColumn]) || 0, y: Number(r[yColumn]) || 0 })).slice(0, 1e3);
454
+ const points = records.map((r) => ({ x: Number(r[xColumn]), y: Number(r[yColumn]) })).filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y)).slice(0, 1e3);
415
455
  return [{ id: `${xColumn} vs ${yColumn}`, data: points }];
416
456
  }, [records, xColumn, yColumn, presetData]);
417
457
  const data = presetData || computed;
@@ -424,8 +464,8 @@ function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
424
464
  yScale: { type: "linear", min: "auto", max: "auto" },
425
465
  colors: ["rgba(167, 139, 250, 0.7)"],
426
466
  nodeSize: 6,
427
- axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn) },
428
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn) },
467
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn, { numeric: true }) },
468
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn, { numeric: true }) },
429
469
  useMesh: true,
430
470
  theme: buildNivoTheme(style)
431
471
  }
@@ -524,8 +564,8 @@ function HeatmapView({
524
564
  {
525
565
  data,
526
566
  margin,
527
- axisTop: { tickSize: 5, tickPadding: 5, tickRotation: -35, legend: colColumn, legendPosition: s.xLegendPosition, legendOffset: -50 },
528
- axisLeft: { tickSize: 5, tickPadding: 5, legend: rowColumn, legendPosition: s.yLegendPosition, legendOffset: -80 },
567
+ axisTop: { tickSize: 5, tickPadding: 5, tickRotation: s.xTickRotation, legend: s.showXLegend ? s.xAxisLabel || colColumn : "", legendPosition: s.xLegendPosition, legendOffset: -50 },
568
+ axisLeft: { tickSize: 5, tickPadding: 5, legend: s.showYLegend ? s.yAxisLabel || rowColumn : "", legendPosition: s.yLegendPosition, legendOffset: -80 },
529
569
  axisRight: showTotals ? { tickSize: 5, tickPadding: 5, format: (id) => fmtNum(rowTotals[id]), legend: `${marginAgg} \u25B8`, legendOffset: 60 } : null,
530
570
  axisBottom: showTotals ? { tickSize: 5, tickPadding: 5, tickRotation: -35, format: (id) => fmtNum(colTotals[id]) } : null,
531
571
  colors: { type: "sequential", scheme: "blue_green", minValue: 0 },
@@ -602,6 +642,37 @@ function NumberControl({ value, onChange, min, max }) {
602
642
  }
603
643
  );
604
644
  }
645
+ function TextControl({ value, onChange, placeholder }) {
646
+ const { theme } = useDashboard();
647
+ return /* @__PURE__ */ jsx(
648
+ "input",
649
+ {
650
+ type: "text",
651
+ value,
652
+ placeholder,
653
+ onChange: (e) => onChange(e.target.value),
654
+ className: `w-28 px-2 py-1 text-xs ${theme.font} bg-midnight-surface border ${theme.border} text-midnight-text-body outline-none focus:border-midnight-accent`
655
+ }
656
+ );
657
+ }
658
+ function Toggle({ value, onChange }) {
659
+ return /* @__PURE__ */ jsx(
660
+ "input",
661
+ {
662
+ type: "checkbox",
663
+ checked: value,
664
+ onChange: (e) => onChange(e.target.checked),
665
+ className: "w-4 h-4 accent-midnight-accent cursor-pointer"
666
+ }
667
+ );
668
+ }
669
+ function OptionalColor({ value, onChange, fallback = "#a78bfa" }) {
670
+ const on = !!value;
671
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
672
+ /* @__PURE__ */ jsx(Toggle, { value: on, onChange: (v) => onChange(v ? fallback : "") }),
673
+ on && /* @__PURE__ */ jsx(ColorControl, { value, onChange })
674
+ ] });
675
+ }
605
676
  function Choice({ value, options, onChange }) {
606
677
  const { theme } = useDashboard();
607
678
  return /* @__PURE__ */ jsx("div", { className: "w-28", children: /* @__PURE__ */ jsx(
@@ -625,19 +696,32 @@ function ChartStyleControls({ style, onChange }) {
625
696
  /* @__PURE__ */ jsxs(Group, { children: [
626
697
  /* @__PURE__ */ jsx(Row, { label: "Background", children: /* @__PURE__ */ jsx(ColorControl, { value: s.background, onChange: (v) => set("background", v) }) }),
627
698
  /* @__PURE__ */ jsx(Row, { label: "Text color", children: /* @__PURE__ */ jsx(ColorControl, { value: s.textColor, onChange: (v) => set("textColor", v) }) }),
628
- /* @__PURE__ */ jsx(Row, { label: "Title color", children: /* @__PURE__ */ jsx(ColorControl, { value: s.legendColor, onChange: (v) => set("legendColor", v) }) }),
629
699
  /* @__PURE__ */ jsx(Row, { label: "Grid color", children: /* @__PURE__ */ jsx(ColorControl, { value: s.gridColor, onChange: (v) => set("gridColor", v) }) }),
630
700
  /* @__PURE__ */ jsx(Row, { label: "Font size", children: /* @__PURE__ */ jsx(NumberControl, { value: s.fontSize, min: 6, max: 24, onChange: (v) => set("fontSize", v) }) })
631
701
  ] }),
632
702
  /* @__PURE__ */ jsxs(Group, { children: [
703
+ /* @__PURE__ */ jsx(Row, { label: "X axis title", children: /* @__PURE__ */ jsx(TextControl, { value: s.xAxisLabel, placeholder: "(column)", onChange: (v) => set("xAxisLabel", v) }) }),
704
+ /* @__PURE__ */ jsx(Row, { label: "Show X title", children: /* @__PURE__ */ jsx(Toggle, { value: s.showXLegend, onChange: (v) => set("showXLegend", v) }) }),
705
+ /* @__PURE__ */ jsx(Row, { label: "Y axis title", children: /* @__PURE__ */ jsx(TextControl, { value: s.yAxisLabel, placeholder: "(column)", onChange: (v) => set("yAxisLabel", v) }) }),
706
+ /* @__PURE__ */ jsx(Row, { label: "Show Y title", children: /* @__PURE__ */ jsx(Toggle, { value: s.showYLegend, onChange: (v) => set("showYLegend", v) }) }),
707
+ /* @__PURE__ */ jsx(Row, { label: "Title color", children: /* @__PURE__ */ jsx(ColorControl, { value: s.legendColor, onChange: (v) => set("legendColor", v) }) }),
708
+ /* @__PURE__ */ jsx(Row, { label: "Title bold", children: /* @__PURE__ */ jsx(Toggle, { value: s.legendBold, onChange: (v) => set("legendBold", v) }) }),
709
+ /* @__PURE__ */ jsx(Row, { label: "Title highlight", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.legendHighlight, onChange: (v) => set("legendHighlight", v) }) }),
633
710
  /* @__PURE__ */ jsx(Row, { label: "X title pos", children: /* @__PURE__ */ jsx(Choice, { value: s.xLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("xLegendPosition", v) }) }),
634
711
  /* @__PURE__ */ jsx(Row, { label: "X title offset", children: /* @__PURE__ */ jsx(NumberControl, { value: s.xLegendOffset, onChange: (v) => set("xLegendOffset", v) }) }),
635
712
  /* @__PURE__ */ jsx(Row, { label: "Y title pos", children: /* @__PURE__ */ jsx(Choice, { value: s.yLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("yLegendPosition", v) }) }),
636
713
  /* @__PURE__ */ jsx(Row, { label: "Y title offset", children: /* @__PURE__ */ jsx(NumberControl, { value: s.yLegendOffset, onChange: (v) => set("yLegendOffset", v) }) })
637
714
  ] }),
715
+ /* @__PURE__ */ jsxs(Group, { children: [
716
+ /* @__PURE__ */ jsx(Row, { label: "X tick angle", children: /* @__PURE__ */ jsx(NumberControl, { value: s.xTickRotation, min: -90, max: 90, onChange: (v) => set("xTickRotation", v) }) }),
717
+ /* @__PURE__ */ jsx(Row, { label: "Truncate ticks", children: /* @__PURE__ */ jsx(NumberControl, { value: s.tickTruncate, min: 0, max: 40, onChange: (v) => set("tickTruncate", v) }) })
718
+ ] }),
638
719
  /* @__PURE__ */ jsxs(Group, { last: true, children: [
639
- /* @__PURE__ */ jsx(Row, { label: "Legend", children: /* @__PURE__ */ jsx(Choice, { value: s.legendAnchor, options: ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"], onChange: (v) => set("legendAnchor", v) }) }),
640
- /* @__PURE__ */ jsx(Row, { label: "Title align", children: /* @__PURE__ */ jsx(Choice, { value: s.titleAlign, options: ["left", "center", "right"], onChange: (v) => set("titleAlign", v) }) })
720
+ /* @__PURE__ */ jsx(Row, { label: "Series legend", children: /* @__PURE__ */ jsx(Choice, { value: s.legendAnchor, options: ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"], onChange: (v) => set("legendAnchor", v) }) }),
721
+ /* @__PURE__ */ jsx(Row, { label: "Panel title align", children: /* @__PURE__ */ jsx(Choice, { value: s.titleAlign, options: ["left", "center", "right"], onChange: (v) => set("titleAlign", v) }) }),
722
+ /* @__PURE__ */ jsx(Row, { label: "Panel title bold", children: /* @__PURE__ */ jsx(Toggle, { value: s.titleBold, onChange: (v) => set("titleBold", v) }) }),
723
+ /* @__PURE__ */ jsx(Row, { label: "Panel title color", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.titleColor, onChange: (v) => set("titleColor", v), fallback: "#e2e8f0" }) }),
724
+ /* @__PURE__ */ jsx(Row, { label: "Panel title bg", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.titleBackground, onChange: (v) => set("titleBackground", v), fallback: "#1e293b" }) })
641
725
  ] })
642
726
  ] });
643
727
  }
@@ -1362,6 +1446,7 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1362
1446
  const [showTotals, setShowTotals] = useState(false);
1363
1447
  const [style, setStyle] = useState(DEFAULT_CHART_STYLE);
1364
1448
  const [showStyle, setShowStyle] = useState(false);
1449
+ const [showFields, setShowFields] = useState(false);
1365
1450
  const [sqlRows, setSqlRows] = useState(null);
1366
1451
  const [sqlLoading, setSqlLoading] = useState(false);
1367
1452
  const [sqlError, setSqlError] = useState(null);
@@ -1647,8 +1732,23 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1647
1732
  )
1648
1733
  ] }),
1649
1734
  /* @__PURE__ */ jsxs("div", { className: "pt-4 border-t border-midnight-border", children: [
1650
- /* @__PURE__ */ jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Available Fields" }),
1651
- /* @__PURE__ */ jsx("div", { className: "space-y-1", children: columns.map((col) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs font-mono px-1 py-0.5", children: [
1735
+ /* @__PURE__ */ jsxs(
1736
+ "button",
1737
+ {
1738
+ onClick: () => setShowFields((s) => !s),
1739
+ className: "flex items-center gap-1 text-xs uppercase text-midnight-text-muted font-mono hover:text-midnight-text-body transition-colors",
1740
+ children: [
1741
+ /* @__PURE__ */ jsx(ChevronDown, { className: `w-3 h-3 transition-transform ${showFields ? "" : "-rotate-90"}` }),
1742
+ "Available Fields",
1743
+ /* @__PURE__ */ jsxs("span", { className: "text-midnight-text-muted/60 normal-case", children: [
1744
+ "(",
1745
+ columns.length,
1746
+ ")"
1747
+ ] })
1748
+ ]
1749
+ }
1750
+ ),
1751
+ showFields && /* @__PURE__ */ jsx("div", { className: "space-y-1 mt-2", children: columns.map((col) => /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs font-mono px-1 py-0.5", children: [
1652
1752
  /* @__PURE__ */ jsx("span", { className: "text-midnight-text-body", children: col.name }),
1653
1753
  /* @__PURE__ */ jsx("span", { className: "text-midnight-text-muted", children: col.type })
1654
1754
  ] }, col.name)) })
@@ -1677,6 +1777,57 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1677
1777
  function ViewLoading() {
1678
1778
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Loading..." });
1679
1779
  }
1780
+ var SQL_CHART_TYPES = /* @__PURE__ */ new Set(["bar", "grouped-bar", "pie", "line", "scatter", "heatmap"]);
1781
+ var SQL_DATA_TYPES = /* @__PURE__ */ new Set(["table", "metric"]);
1782
+ var TABLE_PREVIEW_ROWS = 200;
1783
+ function tableSql(config) {
1784
+ const cols = config.columns?.length ? config.columns.map(qIdent).join(", ") : "*";
1785
+ return `SELECT ${cols} FROM data LIMIT ${TABLE_PREVIEW_ROWS}`;
1786
+ }
1787
+ function metricSql(config) {
1788
+ return `SELECT ${aggExpr(config.agg, config.column)} AS v FROM data`;
1789
+ }
1790
+ function panelToChartConfig(chartType, config) {
1791
+ const cfg = { chartType, xFields: [], yFields: [], filters: config.filters || [] };
1792
+ switch (chartType) {
1793
+ case "bar":
1794
+ case "grouped-bar":
1795
+ if (config.group) cfg.xFields = [{ name: config.group }];
1796
+ cfg.yFields = config.yFields?.length ? config.yFields : config.value ? [{ name: config.value, agg: config.agg }] : [];
1797
+ break;
1798
+ case "pie":
1799
+ if (config.group) cfg.xFields = [{ name: config.group }];
1800
+ break;
1801
+ case "line":
1802
+ case "scatter":
1803
+ if (config.x) cfg.xFields = [{ name: config.x }];
1804
+ if (config.y) cfg.yFields = [{ name: config.y }];
1805
+ break;
1806
+ case "heatmap":
1807
+ if (config.row) cfg.xFields = [{ name: config.row }];
1808
+ if (config.col) cfg.yFields = [{ name: config.col }];
1809
+ if (config.value) cfg.valueField = { name: config.value, agg: config.agg };
1810
+ break;
1811
+ }
1812
+ return cfg;
1813
+ }
1814
+ function panelSql(type, config) {
1815
+ if (config.sql) return config.sql;
1816
+ if (type === "table") return tableSql(config);
1817
+ if (type === "metric") return metricSql(config);
1818
+ if (SQL_CHART_TYPES.has(type)) return buildChartSQL(panelToChartConfig(type, config)) || "";
1819
+ return "";
1820
+ }
1821
+ function DataTable({ rows, columns }) {
1822
+ const colNames = columns?.length ? columns : rows[0] ? Object.keys(rows[0]) : [];
1823
+ return /* @__PURE__ */ jsx("div", { className: "overflow-auto h-full text-xs", children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
1824
+ /* @__PURE__ */ jsx("thead", { className: "sticky top-0 bg-midnight-elevated", children: /* @__PURE__ */ jsx("tr", { children: colNames.map((n) => /* @__PURE__ */ jsx("th", { className: "px-2 py-1 text-left text-midnight-text-muted font-mono border-b border-midnight-border", children: n }, n)) }) }),
1825
+ /* @__PURE__ */ jsx("tbody", { children: rows.map((r, i) => /* @__PURE__ */ jsx("tr", { className: "border-b border-dashed border-midnight-border hover:bg-midnight-raised", children: colNames.map((n) => {
1826
+ const cellValue = r[n];
1827
+ return /* @__PURE__ */ jsx("td", { className: "px-2 py-1 text-midnight-text-body truncate max-w-[200px]", children: cellValue == null ? "" : typeof cellValue === "object" ? JSON.stringify(cellValue) : String(cellValue) }, n);
1828
+ }) }, i)) })
1829
+ ] }) });
1830
+ }
1680
1831
  function SqlPanel({ panel }) {
1681
1832
  const { runQuery, persistPanelData } = useDashboard();
1682
1833
  const { canRefresh } = useCapabilities();
@@ -1684,6 +1835,7 @@ function SqlPanel({ panel }) {
1684
1835
  runQueryRef.current = runQuery;
1685
1836
  const config = panel.config || {};
1686
1837
  const chartType = config.chartType || panel.type;
1838
+ const sql = panelSql(chartType, config);
1687
1839
  const precomputed = config.data;
1688
1840
  const [rows, setRows] = useState(precomputed ?? null);
1689
1841
  const [error, setError] = useState(null);
@@ -1702,7 +1854,7 @@ function SqlPanel({ panel }) {
1702
1854
  let cancelled = false;
1703
1855
  setLoading(true);
1704
1856
  setError(null);
1705
- runQueryRef.current(config.sql || "").then(
1857
+ runQueryRef.current(sql).then(
1706
1858
  (res) => {
1707
1859
  if (!cancelled) {
1708
1860
  setLoading(false);
@@ -1720,12 +1872,12 @@ function SqlPanel({ panel }) {
1720
1872
  return () => {
1721
1873
  cancelled = true;
1722
1874
  };
1723
- }, [config.sql, canRefresh]);
1875
+ }, [sql, canRefresh]);
1724
1876
  const refresh = useCallback(async () => {
1725
1877
  setRefreshing(true);
1726
1878
  setError(null);
1727
1879
  try {
1728
- const res = await runQueryRef.current(config.sql || "");
1880
+ const res = await runQueryRef.current(sql);
1729
1881
  const r = res?.rows || [];
1730
1882
  setRows(r);
1731
1883
  persistPanelData?.(panel.id, r, (/* @__PURE__ */ new Date()).toISOString());
@@ -1734,7 +1886,7 @@ function SqlPanel({ panel }) {
1734
1886
  } finally {
1735
1887
  setRefreshing(false);
1736
1888
  }
1737
- }, [config.sql, panel.id, persistPanelData]);
1889
+ }, [sql, panel.id, persistPanelData]);
1738
1890
  const refreshBtn = canRefresh ? /* @__PURE__ */ jsx(
1739
1891
  "button",
1740
1892
  {
@@ -1767,6 +1919,8 @@ function SqlPanel({ panel }) {
1767
1919
  const first = rows[0];
1768
1920
  const v = first ? Object.values(first)[0] : 0;
1769
1921
  chart = /* @__PURE__ */ jsx(MetricView, { value: Number(v) || 0, config: { column: config.column || "", agg: config.agg, label: config.label } });
1922
+ } else if (chartType === "table") {
1923
+ chart = /* @__PURE__ */ jsx(DataTable, { rows, columns: config.columns });
1770
1924
  } else {
1771
1925
  const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
1772
1926
  switch (chartType) {
@@ -1805,7 +1959,8 @@ function SqlPanel({ panel }) {
1805
1959
  function PanelContent({ panel, records, columns }) {
1806
1960
  const { type } = panel;
1807
1961
  const config = panel.config || {};
1808
- if (config.sql) {
1962
+ const effType = config.chartType || type;
1963
+ if (config.sql || SQL_CHART_TYPES.has(effType) || SQL_DATA_TYPES.has(effType)) {
1809
1964
  return /* @__PURE__ */ jsx(SqlPanel, { panel });
1810
1965
  }
1811
1966
  if (!records?.length && type !== "insight") {
@@ -1828,17 +1983,8 @@ function PanelContent({ panel, records, columns }) {
1828
1983
  return /* @__PURE__ */ jsx(MetricView, { records, config: { column: config.column || "", agg: config.agg, label: config.label } });
1829
1984
  case "insight":
1830
1985
  return /* @__PURE__ */ jsx(InsightView, { config: { text: config.text } });
1831
- case "table": {
1832
- const cols = config.columns ? config.columns.map((c) => ({ name: c })) : columns;
1833
- const colNames = cols?.map((c) => c.name) || (records && records[0] ? Object.keys(records[0]) : []);
1834
- return /* @__PURE__ */ jsx("div", { className: "overflow-auto h-full text-xs", children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
1835
- /* @__PURE__ */ jsx("thead", { className: "sticky top-0 bg-midnight-elevated", children: /* @__PURE__ */ jsx("tr", { children: colNames.map((n) => /* @__PURE__ */ jsx("th", { className: "px-2 py-1 text-left text-midnight-text-muted font-mono border-b border-midnight-border", children: n }, n)) }) }),
1836
- /* @__PURE__ */ jsx("tbody", { children: (records || []).slice(0, 50).map((r, i) => /* @__PURE__ */ jsx("tr", { className: "border-b border-dashed border-midnight-border hover:bg-midnight-raised", children: colNames.map((n) => {
1837
- const cellValue = r[n];
1838
- return /* @__PURE__ */ jsx("td", { className: "px-2 py-1 text-midnight-text-body truncate max-w-[200px]", children: cellValue == null ? "" : typeof cellValue === "object" ? JSON.stringify(cellValue) : String(cellValue) }, n);
1839
- }) }, i)) })
1840
- ] }) });
1841
- }
1986
+ case "table":
1987
+ return /* @__PURE__ */ jsx(DataTable, { rows: (records || []).slice(0, 50), columns: config.columns || columns?.map((c) => c.name) });
1842
1988
  default:
1843
1989
  return /* @__PURE__ */ jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
1844
1990
  "Unknown panel type: ",
@@ -1869,7 +2015,19 @@ function DashboardRenderer({ dashboard, records, columns }) {
1869
2015
  },
1870
2016
  children: [
1871
2017
  /* @__PURE__ */ jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 border-b ${theme.border} bg-midnight-elevated`, children: [
1872
- /* @__PURE__ */ jsx("span", { className: "flex-1 text-xs font-mono text-midnight-text-body truncate", style: { textAlign: panel.config?.style?.titleAlign || "left" }, children: panel.title || panel.type }),
2018
+ /* @__PURE__ */ jsx(
2019
+ "span",
2020
+ {
2021
+ className: "flex-1 text-xs font-mono text-midnight-text-body truncate px-1",
2022
+ style: {
2023
+ textAlign: panel.config?.style?.titleAlign || "left",
2024
+ fontWeight: panel.config?.style?.titleBold ? 700 : void 0,
2025
+ background: panel.config?.style?.titleBackground || void 0,
2026
+ color: panel.config?.style?.titleColor || void 0
2027
+ },
2028
+ children: panel.title || panel.type
2029
+ }
2030
+ ),
1873
2031
  canEditPanels && removePanel && /* @__PURE__ */ jsx(
1874
2032
  "button",
1875
2033
  {