@quantumwake/terminal-ux-dashboard-components 0.1.12 → 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)) })
@@ -1678,6 +1778,15 @@ 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
  }
1680
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
+ }
1681
1790
  function panelToChartConfig(chartType, config) {
1682
1791
  const cfg = { chartType, xFields: [], yFields: [], filters: config.filters || [] };
1683
1792
  switch (chartType) {
@@ -1702,11 +1811,23 @@ function panelToChartConfig(chartType, config) {
1702
1811
  }
1703
1812
  return cfg;
1704
1813
  }
1705
- function panelSql(chartType, config) {
1814
+ function panelSql(type, config) {
1706
1815
  if (config.sql) return config.sql;
1707
- if (SQL_CHART_TYPES.has(chartType)) return buildChartSQL(panelToChartConfig(chartType, config)) || "";
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)) || "";
1708
1819
  return "";
1709
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
+ }
1710
1831
  function SqlPanel({ panel }) {
1711
1832
  const { runQuery, persistPanelData } = useDashboard();
1712
1833
  const { canRefresh } = useCapabilities();
@@ -1798,6 +1919,8 @@ function SqlPanel({ panel }) {
1798
1919
  const first = rows[0];
1799
1920
  const v = first ? Object.values(first)[0] : 0;
1800
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 });
1801
1924
  } else {
1802
1925
  const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
1803
1926
  switch (chartType) {
@@ -1837,7 +1960,7 @@ function PanelContent({ panel, records, columns }) {
1837
1960
  const { type } = panel;
1838
1961
  const config = panel.config || {};
1839
1962
  const effType = config.chartType || type;
1840
- if (config.sql || SQL_CHART_TYPES.has(effType)) {
1963
+ if (config.sql || SQL_CHART_TYPES.has(effType) || SQL_DATA_TYPES.has(effType)) {
1841
1964
  return /* @__PURE__ */ jsx(SqlPanel, { panel });
1842
1965
  }
1843
1966
  if (!records?.length && type !== "insight") {
@@ -1860,17 +1983,8 @@ function PanelContent({ panel, records, columns }) {
1860
1983
  return /* @__PURE__ */ jsx(MetricView, { records, config: { column: config.column || "", agg: config.agg, label: config.label } });
1861
1984
  case "insight":
1862
1985
  return /* @__PURE__ */ jsx(InsightView, { config: { text: config.text } });
1863
- case "table": {
1864
- const cols = config.columns ? config.columns.map((c) => ({ name: c })) : columns;
1865
- const colNames = cols?.map((c) => c.name) || (records && records[0] ? Object.keys(records[0]) : []);
1866
- return /* @__PURE__ */ jsx("div", { className: "overflow-auto h-full text-xs", children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
1867
- /* @__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)) }) }),
1868
- /* @__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) => {
1869
- const cellValue = r[n];
1870
- 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);
1871
- }) }, i)) })
1872
- ] }) });
1873
- }
1986
+ case "table":
1987
+ return /* @__PURE__ */ jsx(DataTable, { rows: (records || []).slice(0, 50), columns: config.columns || columns?.map((c) => c.name) });
1874
1988
  default:
1875
1989
  return /* @__PURE__ */ jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
1876
1990
  "Unknown panel type: ",
@@ -1901,7 +2015,19 @@ function DashboardRenderer({ dashboard, records, columns }) {
1901
2015
  },
1902
2016
  children: [
1903
2017
  /* @__PURE__ */ jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 border-b ${theme.border} bg-midnight-elevated`, children: [
1904
- /* @__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
+ ),
1905
2031
  canEditPanels && removePanel && /* @__PURE__ */ jsx(
1906
2032
  "button",
1907
2033
  {