@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.cjs CHANGED
@@ -142,8 +142,10 @@ var buildChartSQL = ({
142
142
  }
143
143
  case "scatter": {
144
144
  if (!x || !y) return null;
145
- 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`;
146
- return `SELECT ${qIdent(x)} AS x, ${qIdent(y)} AS y FROM ${TABLE}${w} LIMIT ${MAX_POINTS}`;
145
+ const nx = `TRY_CAST(${qIdent(x)} AS DOUBLE)`, ny = `TRY_CAST(${qIdent(y)} AS DOUBLE)`;
146
+ const extra = `${nx} IS NOT NULL AND ${ny} IS NOT NULL`;
147
+ const w = where ? `${where} AND ${extra}` : ` WHERE ${extra}`;
148
+ return `SELECT ${nx} AS x, ${ny} AS y FROM ${TABLE}${w} LIMIT ${MAX_POINTS}`;
147
149
  }
148
150
  case "heatmap": {
149
151
  if (!x || !y) return null;
@@ -175,7 +177,10 @@ var shapeChartData = (chartType, rows, { yFields = [] } = {}) => {
175
177
  case "line":
176
178
  return [{ id: "series", data: rows.map((r) => ({ x: String(r.x), y: Number(r.y) || 0 })) }];
177
179
  case "scatter":
178
- return [{ id: "points", data: rows.map((r) => ({ x: Number(r.x) || 0, y: Number(r.y) || 0 })) }];
180
+ return [{
181
+ id: "points",
182
+ data: rows.map((r) => ({ x: Number(r.x), y: Number(r.y) })).filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y))
183
+ }];
179
184
  case "heatmap": {
180
185
  const rowKeys = [];
181
186
  const colKeys = [];
@@ -217,10 +222,24 @@ var DEFAULT_CHART_STYLE = {
217
222
  xLegendOffset: 46,
218
223
  yLegendPosition: "middle",
219
224
  yLegendOffset: -50,
225
+ // Custom axis title text + visibility.
226
+ xAxisLabel: "",
227
+ yAxisLabel: "",
228
+ showXLegend: true,
229
+ showYLegend: true,
230
+ // Axis title emphasis.
231
+ legendBold: false,
232
+ legendHighlight: "",
233
+ // Tick overflow handling.
234
+ xTickRotation: -35,
235
+ tickTruncate: 0,
220
236
  // Series legend placement (charts that have one: pie, grouped bar).
221
237
  legendAnchor: "right",
222
- // Panel title alignment (rendered by DashboardRenderer's panel header).
223
- titleAlign: "left"
238
+ // Panel title (rendered by DashboardRenderer's panel header).
239
+ titleAlign: "left",
240
+ titleBold: false,
241
+ titleBackground: "",
242
+ titleColor: ""
224
243
  };
225
244
  var LEGEND_ANCHORS = ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"];
226
245
  var withStyleDefaults = (style) => ({
@@ -234,7 +253,17 @@ var buildNivoTheme = (style) => {
234
253
  text: { fill: s.textColor, fontSize: s.fontSize },
235
254
  axis: {
236
255
  ticks: { text: { fill: s.textColor, fontSize: s.fontSize } },
237
- legend: { text: { fill: s.legendColor, fontSize: s.fontSize + 2 } }
256
+ legend: {
257
+ text: {
258
+ fill: s.legendColor,
259
+ fontSize: s.fontSize + 2,
260
+ fontWeight: s.legendBold ? 700 : 400,
261
+ // A highlight is drawn as a text outline (halo) — the clean,
262
+ // SVG-native way to make an axis title stand out.
263
+ outlineWidth: s.legendHighlight ? 3 : 0,
264
+ outlineColor: s.legendHighlight || "transparent"
265
+ }
266
+ }
238
267
  },
239
268
  grid: { line: { stroke: s.gridColor } },
240
269
  crosshair: { line: { stroke: s.textColor, strokeDasharray: "6 6" } },
@@ -263,14 +292,25 @@ var legendConfig = (style) => {
263
292
  const ty = s.legendAnchor.includes("top") ? 10 : -10;
264
293
  return { ...base, translateX: tx, translateY: ty };
265
294
  };
266
- var axisLegend = (style, axis, legendText) => {
295
+ var truncate = (v, n) => {
296
+ const str = String(v);
297
+ return n > 0 && str.length > n ? `${str.slice(0, n)}\u2026` : str;
298
+ };
299
+ var axisLegend = (style, axis, columnName, opts2 = {}) => {
267
300
  const s = withStyleDefaults(style);
268
301
  const isX = axis === "x";
269
- return {
270
- legend: legendText,
271
- legendPosition: isX ? s.xLegendPosition : s.yLegendPosition,
272
- legendOffset: isX ? s.xLegendOffset : s.yLegendOffset
273
- };
302
+ const show = isX ? s.showXLegend : s.showYLegend;
303
+ const label = (isX ? s.xAxisLabel : s.yAxisLabel) || columnName;
304
+ const out = {};
305
+ if (show && label) {
306
+ out.legend = label;
307
+ out.legendPosition = isX ? s.xLegendPosition : s.yLegendPosition;
308
+ out.legendOffset = isX ? s.xLegendOffset : s.yLegendOffset;
309
+ }
310
+ if (isX) out.tickRotation = s.xTickRotation;
311
+ if (opts2.numeric) out.format = (v) => Number(v).toLocaleString();
312
+ else if (s.tickTruncate > 0) out.format = (v) => truncate(v, s.tickTruncate);
313
+ return out;
274
314
  };
275
315
 
276
316
  // src/dataShape.ts
@@ -334,8 +374,8 @@ function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: pre
334
374
  padding: 0.3,
335
375
  colors: ["rgba(74, 222, 128, 0.8)"],
336
376
  borderColor: { from: "color", modifiers: [["darker", 1.6]] },
337
- axisBottom: { tickSize: 5, tickPadding: 5, tickRotation: -35 },
338
- axisLeft: { tickSize: 5, tickPadding: 5, format: (v) => Number(v).toLocaleString() },
377
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", groupColumn) },
378
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", valueColumn, { numeric: true }) },
339
379
  labelSkipWidth: 12,
340
380
  labelSkipHeight: 12,
341
381
  labelTextColor: { from: "color", modifiers: [["darker", 3]] },
@@ -408,8 +448,8 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
408
448
  pointBorderWidth: 2,
409
449
  pointBorderColor: { from: "serieColor" },
410
450
  enableGridX: false,
411
- axisBottom: { tickSize: 5, tickPadding: 5, tickRotation: -35, ...axisLegend(style, "x", xColumn) },
412
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn) },
451
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn) },
452
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn, { numeric: true }) },
413
453
  useMesh: true,
414
454
  theme: buildNivoTheme(style)
415
455
  }
@@ -418,7 +458,7 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
418
458
  function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
419
459
  const computed = react.useMemo(() => {
420
460
  if (presetData || !records) return [];
421
- 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);
461
+ 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);
422
462
  return [{ id: `${xColumn} vs ${yColumn}`, data: points }];
423
463
  }, [records, xColumn, yColumn, presetData]);
424
464
  const data = presetData || computed;
@@ -431,8 +471,8 @@ function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
431
471
  yScale: { type: "linear", min: "auto", max: "auto" },
432
472
  colors: ["rgba(167, 139, 250, 0.7)"],
433
473
  nodeSize: 6,
434
- axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn) },
435
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn) },
474
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn, { numeric: true }) },
475
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn, { numeric: true }) },
436
476
  useMesh: true,
437
477
  theme: buildNivoTheme(style)
438
478
  }
@@ -531,8 +571,8 @@ function HeatmapView({
531
571
  {
532
572
  data,
533
573
  margin,
534
- axisTop: { tickSize: 5, tickPadding: 5, tickRotation: -35, legend: colColumn, legendPosition: s.xLegendPosition, legendOffset: -50 },
535
- axisLeft: { tickSize: 5, tickPadding: 5, legend: rowColumn, legendPosition: s.yLegendPosition, legendOffset: -80 },
574
+ axisTop: { tickSize: 5, tickPadding: 5, tickRotation: s.xTickRotation, legend: s.showXLegend ? s.xAxisLabel || colColumn : "", legendPosition: s.xLegendPosition, legendOffset: -50 },
575
+ axisLeft: { tickSize: 5, tickPadding: 5, legend: s.showYLegend ? s.yAxisLabel || rowColumn : "", legendPosition: s.yLegendPosition, legendOffset: -80 },
536
576
  axisRight: showTotals ? { tickSize: 5, tickPadding: 5, format: (id) => fmtNum(rowTotals[id]), legend: `${marginAgg} \u25B8`, legendOffset: 60 } : null,
537
577
  axisBottom: showTotals ? { tickSize: 5, tickPadding: 5, tickRotation: -35, format: (id) => fmtNum(colTotals[id]) } : null,
538
578
  colors: { type: "sequential", scheme: "blue_green", minValue: 0 },
@@ -609,6 +649,37 @@ function NumberControl({ value, onChange, min, max }) {
609
649
  }
610
650
  );
611
651
  }
652
+ function TextControl({ value, onChange, placeholder }) {
653
+ const { theme } = useDashboard();
654
+ return /* @__PURE__ */ jsxRuntime.jsx(
655
+ "input",
656
+ {
657
+ type: "text",
658
+ value,
659
+ placeholder,
660
+ onChange: (e) => onChange(e.target.value),
661
+ 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`
662
+ }
663
+ );
664
+ }
665
+ function Toggle({ value, onChange }) {
666
+ return /* @__PURE__ */ jsxRuntime.jsx(
667
+ "input",
668
+ {
669
+ type: "checkbox",
670
+ checked: value,
671
+ onChange: (e) => onChange(e.target.checked),
672
+ className: "w-4 h-4 accent-midnight-accent cursor-pointer"
673
+ }
674
+ );
675
+ }
676
+ function OptionalColor({ value, onChange, fallback = "#a78bfa" }) {
677
+ const on = !!value;
678
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
679
+ /* @__PURE__ */ jsxRuntime.jsx(Toggle, { value: on, onChange: (v) => onChange(v ? fallback : "") }),
680
+ on && /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value, onChange })
681
+ ] });
682
+ }
612
683
  function Choice({ value, options, onChange }) {
613
684
  const { theme } = useDashboard();
614
685
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-28", children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -632,19 +703,32 @@ function ChartStyleControls({ style, onChange }) {
632
703
  /* @__PURE__ */ jsxRuntime.jsxs(Group, { children: [
633
704
  /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Background", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.background, onChange: (v) => set("background", v) }) }),
634
705
  /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Text color", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.textColor, onChange: (v) => set("textColor", v) }) }),
635
- /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Title color", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.legendColor, onChange: (v) => set("legendColor", v) }) }),
636
706
  /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Grid color", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.gridColor, onChange: (v) => set("gridColor", v) }) }),
637
707
  /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Font size", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.fontSize, min: 6, max: 24, onChange: (v) => set("fontSize", v) }) })
638
708
  ] }),
639
709
  /* @__PURE__ */ jsxRuntime.jsxs(Group, { children: [
710
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X axis title", children: /* @__PURE__ */ jsxRuntime.jsx(TextControl, { value: s.xAxisLabel, placeholder: "(column)", onChange: (v) => set("xAxisLabel", v) }) }),
711
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Show X title", children: /* @__PURE__ */ jsxRuntime.jsx(Toggle, { value: s.showXLegend, onChange: (v) => set("showXLegend", v) }) }),
712
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Y axis title", children: /* @__PURE__ */ jsxRuntime.jsx(TextControl, { value: s.yAxisLabel, placeholder: "(column)", onChange: (v) => set("yAxisLabel", v) }) }),
713
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Show Y title", children: /* @__PURE__ */ jsxRuntime.jsx(Toggle, { value: s.showYLegend, onChange: (v) => set("showYLegend", v) }) }),
714
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Title color", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.legendColor, onChange: (v) => set("legendColor", v) }) }),
715
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Title bold", children: /* @__PURE__ */ jsxRuntime.jsx(Toggle, { value: s.legendBold, onChange: (v) => set("legendBold", v) }) }),
716
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Title highlight", children: /* @__PURE__ */ jsxRuntime.jsx(OptionalColor, { value: s.legendHighlight, onChange: (v) => set("legendHighlight", v) }) }),
640
717
  /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X title pos", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.xLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("xLegendPosition", v) }) }),
641
718
  /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X title offset", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.xLegendOffset, onChange: (v) => set("xLegendOffset", v) }) }),
642
719
  /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Y title pos", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.yLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("yLegendPosition", v) }) }),
643
720
  /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Y title offset", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.yLegendOffset, onChange: (v) => set("yLegendOffset", v) }) })
644
721
  ] }),
722
+ /* @__PURE__ */ jsxRuntime.jsxs(Group, { children: [
723
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X tick angle", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.xTickRotation, min: -90, max: 90, onChange: (v) => set("xTickRotation", v) }) }),
724
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Truncate ticks", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.tickTruncate, min: 0, max: 40, onChange: (v) => set("tickTruncate", v) }) })
725
+ ] }),
645
726
  /* @__PURE__ */ jsxRuntime.jsxs(Group, { last: true, children: [
646
- /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Legend", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.legendAnchor, options: ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"], onChange: (v) => set("legendAnchor", v) }) }),
647
- /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Title align", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.titleAlign, options: ["left", "center", "right"], onChange: (v) => set("titleAlign", v) }) })
727
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Series legend", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.legendAnchor, options: ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"], onChange: (v) => set("legendAnchor", v) }) }),
728
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Panel title align", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.titleAlign, options: ["left", "center", "right"], onChange: (v) => set("titleAlign", v) }) }),
729
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Panel title bold", children: /* @__PURE__ */ jsxRuntime.jsx(Toggle, { value: s.titleBold, onChange: (v) => set("titleBold", v) }) }),
730
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Panel title color", children: /* @__PURE__ */ jsxRuntime.jsx(OptionalColor, { value: s.titleColor, onChange: (v) => set("titleColor", v), fallback: "#e2e8f0" }) }),
731
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Panel title bg", children: /* @__PURE__ */ jsxRuntime.jsx(OptionalColor, { value: s.titleBackground, onChange: (v) => set("titleBackground", v), fallback: "#1e293b" }) })
648
732
  ] })
649
733
  ] });
650
734
  }
@@ -1369,6 +1453,7 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1369
1453
  const [showTotals, setShowTotals] = react.useState(false);
1370
1454
  const [style, setStyle] = react.useState(DEFAULT_CHART_STYLE);
1371
1455
  const [showStyle, setShowStyle] = react.useState(false);
1456
+ const [showFields, setShowFields] = react.useState(false);
1372
1457
  const [sqlRows, setSqlRows] = react.useState(null);
1373
1458
  const [sqlLoading, setSqlLoading] = react.useState(false);
1374
1459
  const [sqlError, setSqlError] = react.useState(null);
@@ -1654,8 +1739,23 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1654
1739
  )
1655
1740
  ] }),
1656
1741
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-4 border-t border-midnight-border", children: [
1657
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs uppercase text-midnight-text-muted mb-2 font-mono", children: "Available Fields" }),
1658
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1", children: columns.map((col) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs font-mono px-1 py-0.5", children: [
1742
+ /* @__PURE__ */ jsxRuntime.jsxs(
1743
+ "button",
1744
+ {
1745
+ onClick: () => setShowFields((s) => !s),
1746
+ className: "flex items-center gap-1 text-xs uppercase text-midnight-text-muted font-mono hover:text-midnight-text-body transition-colors",
1747
+ children: [
1748
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: `w-3 h-3 transition-transform ${showFields ? "" : "-rotate-90"}` }),
1749
+ "Available Fields",
1750
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-midnight-text-muted/60 normal-case", children: [
1751
+ "(",
1752
+ columns.length,
1753
+ ")"
1754
+ ] })
1755
+ ]
1756
+ }
1757
+ ),
1758
+ showFields && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1 mt-2", children: columns.map((col) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs font-mono px-1 py-0.5", children: [
1659
1759
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-midnight-text-body", children: col.name }),
1660
1760
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-midnight-text-muted", children: col.type })
1661
1761
  ] }, col.name)) })
@@ -1684,6 +1784,57 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1684
1784
  function ViewLoading() {
1685
1785
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Loading..." });
1686
1786
  }
1787
+ var SQL_CHART_TYPES = /* @__PURE__ */ new Set(["bar", "grouped-bar", "pie", "line", "scatter", "heatmap"]);
1788
+ var SQL_DATA_TYPES = /* @__PURE__ */ new Set(["table", "metric"]);
1789
+ var TABLE_PREVIEW_ROWS = 200;
1790
+ function tableSql(config) {
1791
+ const cols = config.columns?.length ? config.columns.map(qIdent).join(", ") : "*";
1792
+ return `SELECT ${cols} FROM data LIMIT ${TABLE_PREVIEW_ROWS}`;
1793
+ }
1794
+ function metricSql(config) {
1795
+ return `SELECT ${aggExpr(config.agg, config.column)} AS v FROM data`;
1796
+ }
1797
+ function panelToChartConfig(chartType, config) {
1798
+ const cfg = { chartType, xFields: [], yFields: [], filters: config.filters || [] };
1799
+ switch (chartType) {
1800
+ case "bar":
1801
+ case "grouped-bar":
1802
+ if (config.group) cfg.xFields = [{ name: config.group }];
1803
+ cfg.yFields = config.yFields?.length ? config.yFields : config.value ? [{ name: config.value, agg: config.agg }] : [];
1804
+ break;
1805
+ case "pie":
1806
+ if (config.group) cfg.xFields = [{ name: config.group }];
1807
+ break;
1808
+ case "line":
1809
+ case "scatter":
1810
+ if (config.x) cfg.xFields = [{ name: config.x }];
1811
+ if (config.y) cfg.yFields = [{ name: config.y }];
1812
+ break;
1813
+ case "heatmap":
1814
+ if (config.row) cfg.xFields = [{ name: config.row }];
1815
+ if (config.col) cfg.yFields = [{ name: config.col }];
1816
+ if (config.value) cfg.valueField = { name: config.value, agg: config.agg };
1817
+ break;
1818
+ }
1819
+ return cfg;
1820
+ }
1821
+ function panelSql(type, config) {
1822
+ if (config.sql) return config.sql;
1823
+ if (type === "table") return tableSql(config);
1824
+ if (type === "metric") return metricSql(config);
1825
+ if (SQL_CHART_TYPES.has(type)) return buildChartSQL(panelToChartConfig(type, config)) || "";
1826
+ return "";
1827
+ }
1828
+ function DataTable({ rows, columns }) {
1829
+ const colNames = columns?.length ? columns : rows[0] ? Object.keys(rows[0]) : [];
1830
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto h-full text-xs", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full", children: [
1831
+ /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "sticky top-0 bg-midnight-elevated", children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: colNames.map((n) => /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-2 py-1 text-left text-midnight-text-muted font-mono border-b border-midnight-border", children: n }, n)) }) }),
1832
+ /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: rows.map((r, i) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-b border-dashed border-midnight-border hover:bg-midnight-raised", children: colNames.map((n) => {
1833
+ const cellValue = r[n];
1834
+ return /* @__PURE__ */ jsxRuntime.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);
1835
+ }) }, i)) })
1836
+ ] }) });
1837
+ }
1687
1838
  function SqlPanel({ panel }) {
1688
1839
  const { runQuery, persistPanelData } = useDashboard();
1689
1840
  const { canRefresh } = useCapabilities();
@@ -1691,6 +1842,7 @@ function SqlPanel({ panel }) {
1691
1842
  runQueryRef.current = runQuery;
1692
1843
  const config = panel.config || {};
1693
1844
  const chartType = config.chartType || panel.type;
1845
+ const sql = panelSql(chartType, config);
1694
1846
  const precomputed = config.data;
1695
1847
  const [rows, setRows] = react.useState(precomputed ?? null);
1696
1848
  const [error, setError] = react.useState(null);
@@ -1709,7 +1861,7 @@ function SqlPanel({ panel }) {
1709
1861
  let cancelled = false;
1710
1862
  setLoading(true);
1711
1863
  setError(null);
1712
- runQueryRef.current(config.sql || "").then(
1864
+ runQueryRef.current(sql).then(
1713
1865
  (res) => {
1714
1866
  if (!cancelled) {
1715
1867
  setLoading(false);
@@ -1727,12 +1879,12 @@ function SqlPanel({ panel }) {
1727
1879
  return () => {
1728
1880
  cancelled = true;
1729
1881
  };
1730
- }, [config.sql, canRefresh]);
1882
+ }, [sql, canRefresh]);
1731
1883
  const refresh = react.useCallback(async () => {
1732
1884
  setRefreshing(true);
1733
1885
  setError(null);
1734
1886
  try {
1735
- const res = await runQueryRef.current(config.sql || "");
1887
+ const res = await runQueryRef.current(sql);
1736
1888
  const r = res?.rows || [];
1737
1889
  setRows(r);
1738
1890
  persistPanelData?.(panel.id, r, (/* @__PURE__ */ new Date()).toISOString());
@@ -1741,7 +1893,7 @@ function SqlPanel({ panel }) {
1741
1893
  } finally {
1742
1894
  setRefreshing(false);
1743
1895
  }
1744
- }, [config.sql, panel.id, persistPanelData]);
1896
+ }, [sql, panel.id, persistPanelData]);
1745
1897
  const refreshBtn = canRefresh ? /* @__PURE__ */ jsxRuntime.jsx(
1746
1898
  "button",
1747
1899
  {
@@ -1774,6 +1926,8 @@ function SqlPanel({ panel }) {
1774
1926
  const first = rows[0];
1775
1927
  const v = first ? Object.values(first)[0] : 0;
1776
1928
  chart = /* @__PURE__ */ jsxRuntime.jsx(MetricView, { value: Number(v) || 0, config: { column: config.column || "", agg: config.agg, label: config.label } });
1929
+ } else if (chartType === "table") {
1930
+ chart = /* @__PURE__ */ jsxRuntime.jsx(DataTable, { rows, columns: config.columns });
1777
1931
  } else {
1778
1932
  const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
1779
1933
  switch (chartType) {
@@ -1812,7 +1966,8 @@ function SqlPanel({ panel }) {
1812
1966
  function PanelContent({ panel, records, columns }) {
1813
1967
  const { type } = panel;
1814
1968
  const config = panel.config || {};
1815
- if (config.sql) {
1969
+ const effType = config.chartType || type;
1970
+ if (config.sql || SQL_CHART_TYPES.has(effType) || SQL_DATA_TYPES.has(effType)) {
1816
1971
  return /* @__PURE__ */ jsxRuntime.jsx(SqlPanel, { panel });
1817
1972
  }
1818
1973
  if (!records?.length && type !== "insight") {
@@ -1835,17 +1990,8 @@ function PanelContent({ panel, records, columns }) {
1835
1990
  return /* @__PURE__ */ jsxRuntime.jsx(MetricView, { records, config: { column: config.column || "", agg: config.agg, label: config.label } });
1836
1991
  case "insight":
1837
1992
  return /* @__PURE__ */ jsxRuntime.jsx(InsightView, { config: { text: config.text } });
1838
- case "table": {
1839
- const cols = config.columns ? config.columns.map((c) => ({ name: c })) : columns;
1840
- const colNames = cols?.map((c) => c.name) || (records && records[0] ? Object.keys(records[0]) : []);
1841
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto h-full text-xs", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full", children: [
1842
- /* @__PURE__ */ jsxRuntime.jsx("thead", { className: "sticky top-0 bg-midnight-elevated", children: /* @__PURE__ */ jsxRuntime.jsx("tr", { children: colNames.map((n) => /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-2 py-1 text-left text-midnight-text-muted font-mono border-b border-midnight-border", children: n }, n)) }) }),
1843
- /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: (records || []).slice(0, 50).map((r, i) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "border-b border-dashed border-midnight-border hover:bg-midnight-raised", children: colNames.map((n) => {
1844
- const cellValue = r[n];
1845
- return /* @__PURE__ */ jsxRuntime.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);
1846
- }) }, i)) })
1847
- ] }) });
1848
- }
1993
+ case "table":
1994
+ return /* @__PURE__ */ jsxRuntime.jsx(DataTable, { rows: (records || []).slice(0, 50), columns: config.columns || columns?.map((c) => c.name) });
1849
1995
  default:
1850
1996
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
1851
1997
  "Unknown panel type: ",
@@ -1876,7 +2022,19 @@ function DashboardRenderer({ dashboard, records, columns }) {
1876
2022
  },
1877
2023
  children: [
1878
2024
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 border-b ${theme.border} bg-midnight-elevated`, children: [
1879
- /* @__PURE__ */ jsxRuntime.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 }),
2025
+ /* @__PURE__ */ jsxRuntime.jsx(
2026
+ "span",
2027
+ {
2028
+ className: "flex-1 text-xs font-mono text-midnight-text-body truncate px-1",
2029
+ style: {
2030
+ textAlign: panel.config?.style?.titleAlign || "left",
2031
+ fontWeight: panel.config?.style?.titleBold ? 700 : void 0,
2032
+ background: panel.config?.style?.titleBackground || void 0,
2033
+ color: panel.config?.style?.titleColor || void 0
2034
+ },
2035
+ children: panel.title || panel.type
2036
+ }
2037
+ ),
1880
2038
  canEditPanels && removePanel && /* @__PURE__ */ jsxRuntime.jsx(
1881
2039
  "button",
1882
2040
  {