@quantumwake/terminal-ux-dashboard-components 0.1.12 → 0.1.15

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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useMemo, useState, useEffect, useRef, Suspense, useCallback } from 'react';
1
+ import { createContext, useContext, useMemo, useState, useEffect, useRef, useCallback, Suspense } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import { ResponsiveBar } from '@nivo/bar';
4
4
  import { ResponsivePie } from '@nivo/pie';
@@ -11,6 +11,7 @@ import { Loader2, Play, ChevronDown, AlertCircle, Rows3, BarChart3, PieChart, Tr
11
11
  import CodeMirror, { EditorView, Prec, keymap } from '@uiw/react-codemirror';
12
12
  import { sql, PostgreSQL, schemaCompletionSource, keywordCompletionSource } from '@codemirror/lang-sql';
13
13
  import { snippetCompletion, completeFromList, autocompletion, acceptCompletion } from '@codemirror/autocomplete';
14
+ import GridLayout, { WidthProvider } from 'react-grid-layout';
14
15
 
15
16
  // src/context/DashboardContext.tsx
16
17
  var DashboardContext = createContext(null);
@@ -39,7 +40,9 @@ function useCapabilities() {
39
40
  canAddPanel: !!c.addPanel,
40
41
  canEditPanels: !!c.removePanel,
41
42
  // Studio can recompute + persist a panel's precomputed result.
42
- canRefresh: !!c.persistPanelData
43
+ canRefresh: !!c.persistPanelData,
44
+ // Studio can drag/resize panels and persist the grid layout.
45
+ canEditLayout: !!c.persistLayout
43
46
  };
44
47
  }
45
48
 
@@ -135,8 +138,10 @@ var buildChartSQL = ({
135
138
  }
136
139
  case "scatter": {
137
140
  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}`;
141
+ const nx = `TRY_CAST(${qIdent(x)} AS DOUBLE)`, ny = `TRY_CAST(${qIdent(y)} AS DOUBLE)`;
142
+ const extra = `${nx} IS NOT NULL AND ${ny} IS NOT NULL`;
143
+ const w = where ? `${where} AND ${extra}` : ` WHERE ${extra}`;
144
+ return `SELECT ${nx} AS x, ${ny} AS y FROM ${TABLE}${w} LIMIT ${MAX_POINTS}`;
140
145
  }
141
146
  case "heatmap": {
142
147
  if (!x || !y) return null;
@@ -168,7 +173,10 @@ var shapeChartData = (chartType, rows, { yFields = [] } = {}) => {
168
173
  case "line":
169
174
  return [{ id: "series", data: rows.map((r) => ({ x: String(r.x), y: Number(r.y) || 0 })) }];
170
175
  case "scatter":
171
- return [{ id: "points", data: rows.map((r) => ({ x: Number(r.x) || 0, y: Number(r.y) || 0 })) }];
176
+ return [{
177
+ id: "points",
178
+ data: rows.map((r) => ({ x: Number(r.x), y: Number(r.y) })).filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y))
179
+ }];
172
180
  case "heatmap": {
173
181
  const rowKeys = [];
174
182
  const colKeys = [];
@@ -210,10 +218,25 @@ var DEFAULT_CHART_STYLE = {
210
218
  xLegendOffset: 46,
211
219
  yLegendPosition: "middle",
212
220
  yLegendOffset: -50,
221
+ // Custom axis title text + visibility.
222
+ xAxisLabel: "",
223
+ yAxisLabel: "",
224
+ showXLegend: true,
225
+ showYLegend: true,
226
+ // Axis title emphasis.
227
+ legendBold: false,
228
+ legendHighlight: "",
229
+ // Tick overflow handling.
230
+ xTickRotation: -35,
231
+ yTickRotation: 0,
232
+ tickTruncate: 0,
213
233
  // Series legend placement (charts that have one: pie, grouped bar).
214
234
  legendAnchor: "right",
215
- // Panel title alignment (rendered by DashboardRenderer's panel header).
216
- titleAlign: "left"
235
+ // Panel title (rendered by DashboardRenderer's panel header).
236
+ titleAlign: "left",
237
+ titleBold: false,
238
+ titleBackground: "",
239
+ titleColor: ""
217
240
  };
218
241
  var LEGEND_ANCHORS = ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"];
219
242
  var withStyleDefaults = (style) => ({
@@ -227,7 +250,17 @@ var buildNivoTheme = (style) => {
227
250
  text: { fill: s.textColor, fontSize: s.fontSize },
228
251
  axis: {
229
252
  ticks: { text: { fill: s.textColor, fontSize: s.fontSize } },
230
- legend: { text: { fill: s.legendColor, fontSize: s.fontSize + 2 } }
253
+ legend: {
254
+ text: {
255
+ fill: s.legendColor,
256
+ fontSize: s.fontSize + 2,
257
+ fontWeight: s.legendBold ? 700 : 400,
258
+ // A highlight is drawn as a text outline (halo) — the clean,
259
+ // SVG-native way to make an axis title stand out.
260
+ outlineWidth: s.legendHighlight ? 3 : 0,
261
+ outlineColor: s.legendHighlight || "transparent"
262
+ }
263
+ }
231
264
  },
232
265
  grid: { line: { stroke: s.gridColor } },
233
266
  crosshair: { line: { stroke: s.textColor, strokeDasharray: "6 6" } },
@@ -256,14 +289,25 @@ var legendConfig = (style) => {
256
289
  const ty = s.legendAnchor.includes("top") ? 10 : -10;
257
290
  return { ...base, translateX: tx, translateY: ty };
258
291
  };
259
- var axisLegend = (style, axis, legendText) => {
292
+ var truncate = (v, n) => {
293
+ const str = String(v);
294
+ return n > 0 && str.length > n ? `${str.slice(0, n)}\u2026` : str;
295
+ };
296
+ var axisLegend = (style, axis, columnName, opts2 = {}) => {
260
297
  const s = withStyleDefaults(style);
261
298
  const isX = axis === "x";
262
- return {
263
- legend: legendText,
264
- legendPosition: isX ? s.xLegendPosition : s.yLegendPosition,
265
- legendOffset: isX ? s.xLegendOffset : s.yLegendOffset
266
- };
299
+ const show = isX ? s.showXLegend : s.showYLegend;
300
+ const label = (isX ? s.xAxisLabel : s.yAxisLabel) || columnName;
301
+ const out = {};
302
+ if (show && label) {
303
+ out.legend = label;
304
+ out.legendPosition = isX ? s.xLegendPosition : s.yLegendPosition;
305
+ out.legendOffset = isX ? s.xLegendOffset : s.yLegendOffset;
306
+ }
307
+ out.tickRotation = isX ? s.xTickRotation : s.yTickRotation;
308
+ if (opts2.numeric) out.format = (v) => Number(v).toLocaleString();
309
+ else if (s.tickTruncate > 0) out.format = (v) => truncate(v, s.tickTruncate);
310
+ return out;
267
311
  };
268
312
 
269
313
  // src/dataShape.ts
@@ -327,8 +371,8 @@ function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: pre
327
371
  padding: 0.3,
328
372
  colors: ["rgba(74, 222, 128, 0.8)"],
329
373
  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() },
374
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", groupColumn) },
375
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", valueColumn, { numeric: true }) },
332
376
  labelSkipWidth: 12,
333
377
  labelSkipHeight: 12,
334
378
  labelTextColor: { from: "color", modifiers: [["darker", 3]] },
@@ -401,8 +445,8 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
401
445
  pointBorderWidth: 2,
402
446
  pointBorderColor: { from: "serieColor" },
403
447
  enableGridX: false,
404
- axisBottom: { tickSize: 5, tickPadding: 5, tickRotation: -35, ...axisLegend(style, "x", xColumn) },
405
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn) },
448
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn) },
449
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn, { numeric: true }) },
406
450
  useMesh: true,
407
451
  theme: buildNivoTheme(style)
408
452
  }
@@ -411,7 +455,7 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
411
455
  function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
412
456
  const computed = useMemo(() => {
413
457
  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);
458
+ 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
459
  return [{ id: `${xColumn} vs ${yColumn}`, data: points }];
416
460
  }, [records, xColumn, yColumn, presetData]);
417
461
  const data = presetData || computed;
@@ -424,8 +468,8 @@ function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
424
468
  yScale: { type: "linear", min: "auto", max: "auto" },
425
469
  colors: ["rgba(167, 139, 250, 0.7)"],
426
470
  nodeSize: 6,
427
- axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn) },
428
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn) },
471
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn, { numeric: true }) },
472
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn, { numeric: true }) },
429
473
  useMesh: true,
430
474
  theme: buildNivoTheme(style)
431
475
  }
@@ -524,8 +568,8 @@ function HeatmapView({
524
568
  {
525
569
  data,
526
570
  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 },
571
+ axisTop: { tickSize: 5, tickPadding: 5, tickRotation: s.xTickRotation, legend: s.showXLegend ? s.xAxisLabel || colColumn : "", legendPosition: s.xLegendPosition, legendOffset: -50 },
572
+ axisLeft: { tickSize: 5, tickPadding: 5, legend: s.showYLegend ? s.yAxisLabel || rowColumn : "", legendPosition: s.yLegendPosition, legendOffset: -80 },
529
573
  axisRight: showTotals ? { tickSize: 5, tickPadding: 5, format: (id) => fmtNum(rowTotals[id]), legend: `${marginAgg} \u25B8`, legendOffset: 60 } : null,
530
574
  axisBottom: showTotals ? { tickSize: 5, tickPadding: 5, tickRotation: -35, format: (id) => fmtNum(colTotals[id]) } : null,
531
575
  colors: { type: "sequential", scheme: "blue_green", minValue: 0 },
@@ -569,11 +613,17 @@ function PivotView({ records }) {
569
613
  )
570
614
  ] });
571
615
  }
572
- function Row({ label, children }) {
616
+ function Section({ title, children }) {
573
617
  const { theme } = useDashboard();
574
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
575
- /* @__PURE__ */ jsx("span", { className: `text-xs ${theme.font} text-midnight-text-muted`, children: label }),
576
- /* @__PURE__ */ jsx("div", { className: "flex items-center", children })
618
+ return /* @__PURE__ */ jsxs("div", { className: `py-3 border-t ${theme.border} first:border-t-0 first:pt-0`, children: [
619
+ /* @__PURE__ */ jsx("div", { className: "text-[10px] uppercase tracking-wider text-midnight-text-muted/70 font-mono mb-2", children: title }),
620
+ /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children })
621
+ ] });
622
+ }
623
+ function Row({ label, children }) {
624
+ return /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[88px_1fr] items-center gap-2 min-h-[30px]", children: [
625
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted truncate", title: label, children: label }),
626
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-end", children })
577
627
  ] });
578
628
  }
579
629
  function ColorControl({ value, onChange }) {
@@ -584,61 +634,112 @@ function ColorControl({ value, onChange }) {
584
634
  type: "color",
585
635
  value: /^#/.test(value) ? value : "#94a3b8",
586
636
  onChange: (e) => onChange(e.target.value),
587
- className: `w-9 h-6 bg-transparent border ${theme.border} cursor-pointer`
637
+ className: `w-9 h-6 bg-transparent border ${theme.border} cursor-pointer p-0`
588
638
  }
589
639
  );
590
640
  }
591
- function NumberControl({ value, onChange, min, max }) {
641
+ function SliderControl({ value, onChange, min, max, step = 1, unit = "" }) {
642
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 w-full", children: [
643
+ /* @__PURE__ */ jsx(
644
+ "input",
645
+ {
646
+ type: "range",
647
+ min,
648
+ max,
649
+ step,
650
+ value,
651
+ onChange: (e) => onChange(Number(e.target.value)),
652
+ className: "flex-1 h-1 accent-midnight-accent cursor-pointer"
653
+ }
654
+ ),
655
+ /* @__PURE__ */ jsxs("span", { className: "w-10 shrink-0 text-right text-[11px] font-mono tabular-nums text-midnight-text-body", children: [
656
+ value,
657
+ unit
658
+ ] })
659
+ ] });
660
+ }
661
+ function TextControl({ value, onChange, placeholder }) {
592
662
  const { theme } = useDashboard();
593
663
  return /* @__PURE__ */ jsx(
594
664
  "input",
595
665
  {
596
- type: "number",
666
+ type: "text",
597
667
  value,
598
- min,
599
- max,
600
- onChange: (e) => onChange(Number(e.target.value)),
601
- className: `w-20 px-2 py-1 text-xs ${theme.font} bg-midnight-surface border ${theme.border} text-midnight-text-body outline-none focus:border-midnight-accent`
668
+ placeholder,
669
+ onChange: (e) => onChange(e.target.value),
670
+ className: `w-full px-2 py-1 text-xs font-mono bg-midnight-surface border ${theme.border} text-midnight-text-body outline-none focus:border-midnight-accent transition-colors`
671
+ }
672
+ );
673
+ }
674
+ function Switch({ value, onChange }) {
675
+ return /* @__PURE__ */ jsx(
676
+ "button",
677
+ {
678
+ type: "button",
679
+ role: "switch",
680
+ "aria-checked": value,
681
+ onClick: () => onChange(!value),
682
+ className: `relative inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors ${value ? "bg-midnight-accent" : "bg-midnight-border"}`,
683
+ children: /* @__PURE__ */ jsx("span", { className: `inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${value ? "translate-x-3.5" : "translate-x-0.5"}` })
602
684
  }
603
685
  );
604
686
  }
687
+ function OptionalColor({ value, onChange, fallback = "#a78bfa" }) {
688
+ const on = !!value;
689
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
690
+ on && /* @__PURE__ */ jsx(ColorControl, { value, onChange }),
691
+ /* @__PURE__ */ jsx(Switch, { value: on, onChange: (v) => onChange(v ? fallback : "") })
692
+ ] });
693
+ }
605
694
  function Choice({ value, options, onChange }) {
606
695
  const { theme } = useDashboard();
607
- return /* @__PURE__ */ jsx("div", { className: "w-28", children: /* @__PURE__ */ jsx(
696
+ return /* @__PURE__ */ jsx(
608
697
  "select",
609
698
  {
610
699
  value,
611
700
  onChange: (e) => onChange(e.target.value),
612
- className: `w-full px-2 py-1 text-xs ${theme.font} bg-midnight-surface border ${theme.border} text-midnight-text-body outline-none focus:border-midnight-accent`,
701
+ className: `w-full px-2 py-1 text-xs font-mono bg-midnight-surface border ${theme.border} text-midnight-text-body outline-none focus:border-midnight-accent transition-colors`,
613
702
  children: options.map((o) => /* @__PURE__ */ jsx("option", { value: o, children: o }, o))
614
703
  }
615
- ) });
616
- }
617
- function Group({ children, last }) {
618
- const { theme } = useDashboard();
619
- return /* @__PURE__ */ jsx("div", { className: `space-y-2 pb-3 ${last ? "" : `mb-3 border-b ${theme.border}`}`, children });
704
+ );
620
705
  }
621
706
  function ChartStyleControls({ style, onChange }) {
622
707
  const s = withStyleDefaults(style);
623
708
  const set = (key, value) => onChange({ ...s, [key]: value });
624
709
  return /* @__PURE__ */ jsxs("div", { children: [
625
- /* @__PURE__ */ jsxs(Group, { children: [
710
+ /* @__PURE__ */ jsxs(Section, { title: "Canvas", children: [
626
711
  /* @__PURE__ */ jsx(Row, { label: "Background", children: /* @__PURE__ */ jsx(ColorControl, { value: s.background, onChange: (v) => set("background", v) }) }),
627
- /* @__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
- /* @__PURE__ */ jsx(Row, { label: "Grid color", children: /* @__PURE__ */ jsx(ColorControl, { value: s.gridColor, onChange: (v) => set("gridColor", v) }) }),
630
- /* @__PURE__ */ jsx(Row, { label: "Font size", children: /* @__PURE__ */ jsx(NumberControl, { value: s.fontSize, min: 6, max: 24, onChange: (v) => set("fontSize", v) }) })
712
+ /* @__PURE__ */ jsx(Row, { label: "Text", children: /* @__PURE__ */ jsx(ColorControl, { value: s.textColor, onChange: (v) => set("textColor", v) }) }),
713
+ /* @__PURE__ */ jsx(Row, { label: "Grid", children: /* @__PURE__ */ jsx(ColorControl, { value: s.gridColor, onChange: (v) => set("gridColor", v) }) }),
714
+ /* @__PURE__ */ jsx(Row, { label: "Font size", children: /* @__PURE__ */ jsx(SliderControl, { value: s.fontSize, min: 6, max: 24, unit: "px", onChange: (v) => set("fontSize", v) }) })
631
715
  ] }),
632
- /* @__PURE__ */ jsxs(Group, { children: [
633
- /* @__PURE__ */ jsx(Row, { label: "X title pos", children: /* @__PURE__ */ jsx(Choice, { value: s.xLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("xLegendPosition", v) }) }),
634
- /* @__PURE__ */ jsx(Row, { label: "X title offset", children: /* @__PURE__ */ jsx(NumberControl, { value: s.xLegendOffset, onChange: (v) => set("xLegendOffset", v) }) }),
635
- /* @__PURE__ */ jsx(Row, { label: "Y title pos", children: /* @__PURE__ */ jsx(Choice, { value: s.yLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("yLegendPosition", v) }) }),
636
- /* @__PURE__ */ jsx(Row, { label: "Y title offset", children: /* @__PURE__ */ jsx(NumberControl, { value: s.yLegendOffset, onChange: (v) => set("yLegendOffset", v) }) })
716
+ /* @__PURE__ */ jsxs(Section, { title: "Axis titles", children: [
717
+ /* @__PURE__ */ jsx(Row, { label: "X title", children: /* @__PURE__ */ jsx(TextControl, { value: s.xAxisLabel, placeholder: "(column)", onChange: (v) => set("xAxisLabel", v) }) }),
718
+ /* @__PURE__ */ jsx(Row, { label: "Show X", children: /* @__PURE__ */ jsx(Switch, { value: s.showXLegend, onChange: (v) => set("showXLegend", v) }) }),
719
+ /* @__PURE__ */ jsx(Row, { label: "Y title", children: /* @__PURE__ */ jsx(TextControl, { value: s.yAxisLabel, placeholder: "(column)", onChange: (v) => set("yAxisLabel", v) }) }),
720
+ /* @__PURE__ */ jsx(Row, { label: "Show Y", children: /* @__PURE__ */ jsx(Switch, { value: s.showYLegend, onChange: (v) => set("showYLegend", v) }) }),
721
+ /* @__PURE__ */ jsx(Row, { label: "Color", children: /* @__PURE__ */ jsx(ColorControl, { value: s.legendColor, onChange: (v) => set("legendColor", v) }) }),
722
+ /* @__PURE__ */ jsx(Row, { label: "Bold", children: /* @__PURE__ */ jsx(Switch, { value: s.legendBold, onChange: (v) => set("legendBold", v) }) }),
723
+ /* @__PURE__ */ jsx(Row, { label: "Highlight", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.legendHighlight, onChange: (v) => set("legendHighlight", v) }) })
637
724
  ] }),
638
- /* @__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) }) })
641
- ] })
725
+ /* @__PURE__ */ jsxs(Section, { title: "Axis placement", children: [
726
+ /* @__PURE__ */ jsx(Row, { label: "X pos", children: /* @__PURE__ */ jsx(Choice, { value: s.xLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("xLegendPosition", v) }) }),
727
+ /* @__PURE__ */ jsx(Row, { label: "X offset", children: /* @__PURE__ */ jsx(SliderControl, { value: s.xLegendOffset, min: -80, max: 80, onChange: (v) => set("xLegendOffset", v) }) }),
728
+ /* @__PURE__ */ jsx(Row, { label: "Y pos", children: /* @__PURE__ */ jsx(Choice, { value: s.yLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("yLegendPosition", v) }) }),
729
+ /* @__PURE__ */ jsx(Row, { label: "Y offset", children: /* @__PURE__ */ jsx(SliderControl, { value: s.yLegendOffset, min: -80, max: 80, onChange: (v) => set("yLegendOffset", v) }) })
730
+ ] }),
731
+ /* @__PURE__ */ jsxs(Section, { title: "Ticks", children: [
732
+ /* @__PURE__ */ jsx(Row, { label: "X angle", children: /* @__PURE__ */ jsx(SliderControl, { value: s.xTickRotation, min: -90, max: 90, unit: "\xB0", onChange: (v) => set("xTickRotation", v) }) }),
733
+ /* @__PURE__ */ jsx(Row, { label: "Y angle", children: /* @__PURE__ */ jsx(SliderControl, { value: s.yTickRotation, min: -90, max: 90, unit: "\xB0", onChange: (v) => set("yTickRotation", v) }) }),
734
+ /* @__PURE__ */ jsx(Row, { label: "Truncate", children: /* @__PURE__ */ jsx(SliderControl, { value: s.tickTruncate, min: 0, max: 40, onChange: (v) => set("tickTruncate", v) }) })
735
+ ] }),
736
+ /* @__PURE__ */ jsxs(Section, { title: "Panel title", children: [
737
+ /* @__PURE__ */ jsx(Row, { label: "Align", children: /* @__PURE__ */ jsx(Choice, { value: s.titleAlign, options: ["left", "center", "right"], onChange: (v) => set("titleAlign", v) }) }),
738
+ /* @__PURE__ */ jsx(Row, { label: "Bold", children: /* @__PURE__ */ jsx(Switch, { value: s.titleBold, onChange: (v) => set("titleBold", v) }) }),
739
+ /* @__PURE__ */ jsx(Row, { label: "Color", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.titleColor, onChange: (v) => set("titleColor", v), fallback: "#e2e8f0" }) }),
740
+ /* @__PURE__ */ jsx(Row, { label: "Background", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.titleBackground, onChange: (v) => set("titleBackground", v), fallback: "#1e293b" }) })
741
+ ] }),
742
+ /* @__PURE__ */ jsx(Section, { title: "Series legend", children: /* @__PURE__ */ jsx(Row, { label: "Position", children: /* @__PURE__ */ jsx(Choice, { value: s.legendAnchor, options: ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"], onChange: (v) => set("legendAnchor", v) }) }) })
642
743
  ] });
643
744
  }
644
745
  var AGGREGATES = [
@@ -1362,6 +1463,7 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1362
1463
  const [showTotals, setShowTotals] = useState(false);
1363
1464
  const [style, setStyle] = useState(DEFAULT_CHART_STYLE);
1364
1465
  const [showStyle, setShowStyle] = useState(false);
1466
+ const [showFields, setShowFields] = useState(false);
1365
1467
  const [sqlRows, setSqlRows] = useState(null);
1366
1468
  const [sqlLoading, setSqlLoading] = useState(false);
1367
1469
  const [sqlError, setSqlError] = useState(null);
@@ -1647,8 +1749,23 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1647
1749
  )
1648
1750
  ] }),
1649
1751
  /* @__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: [
1752
+ /* @__PURE__ */ jsxs(
1753
+ "button",
1754
+ {
1755
+ onClick: () => setShowFields((s) => !s),
1756
+ className: "flex items-center gap-1 text-xs uppercase text-midnight-text-muted font-mono hover:text-midnight-text-body transition-colors",
1757
+ children: [
1758
+ /* @__PURE__ */ jsx(ChevronDown, { className: `w-3 h-3 transition-transform ${showFields ? "" : "-rotate-90"}` }),
1759
+ "Available Fields",
1760
+ /* @__PURE__ */ jsxs("span", { className: "text-midnight-text-muted/60 normal-case", children: [
1761
+ "(",
1762
+ columns.length,
1763
+ ")"
1764
+ ] })
1765
+ ]
1766
+ }
1767
+ ),
1768
+ 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
1769
  /* @__PURE__ */ jsx("span", { className: "text-midnight-text-body", children: col.name }),
1653
1770
  /* @__PURE__ */ jsx("span", { className: "text-midnight-text-muted", children: col.type })
1654
1771
  ] }, col.name)) })
@@ -1678,6 +1795,15 @@ function ViewLoading() {
1678
1795
  return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Loading..." });
1679
1796
  }
1680
1797
  var SQL_CHART_TYPES = /* @__PURE__ */ new Set(["bar", "grouped-bar", "pie", "line", "scatter", "heatmap"]);
1798
+ var SQL_DATA_TYPES = /* @__PURE__ */ new Set(["table", "metric"]);
1799
+ var TABLE_PREVIEW_ROWS = 200;
1800
+ function tableSql(config) {
1801
+ const cols = config.columns?.length ? config.columns.map(qIdent).join(", ") : "*";
1802
+ return `SELECT ${cols} FROM data LIMIT ${TABLE_PREVIEW_ROWS}`;
1803
+ }
1804
+ function metricSql(config) {
1805
+ return `SELECT ${aggExpr(config.agg, config.column)} AS v FROM data`;
1806
+ }
1681
1807
  function panelToChartConfig(chartType, config) {
1682
1808
  const cfg = { chartType, xFields: [], yFields: [], filters: config.filters || [] };
1683
1809
  switch (chartType) {
@@ -1702,11 +1828,23 @@ function panelToChartConfig(chartType, config) {
1702
1828
  }
1703
1829
  return cfg;
1704
1830
  }
1705
- function panelSql(chartType, config) {
1831
+ function panelSql(type, config) {
1706
1832
  if (config.sql) return config.sql;
1707
- if (SQL_CHART_TYPES.has(chartType)) return buildChartSQL(panelToChartConfig(chartType, config)) || "";
1833
+ if (type === "table") return tableSql(config);
1834
+ if (type === "metric") return metricSql(config);
1835
+ if (SQL_CHART_TYPES.has(type)) return buildChartSQL(panelToChartConfig(type, config)) || "";
1708
1836
  return "";
1709
1837
  }
1838
+ function DataTable({ rows, columns }) {
1839
+ const colNames = columns?.length ? columns : rows[0] ? Object.keys(rows[0]) : [];
1840
+ return /* @__PURE__ */ jsx("div", { className: "overflow-auto h-full text-xs", children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
1841
+ /* @__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)) }) }),
1842
+ /* @__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) => {
1843
+ const cellValue = r[n];
1844
+ 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);
1845
+ }) }, i)) })
1846
+ ] }) });
1847
+ }
1710
1848
  function SqlPanel({ panel }) {
1711
1849
  const { runQuery, persistPanelData } = useDashboard();
1712
1850
  const { canRefresh } = useCapabilities();
@@ -1798,6 +1936,8 @@ function SqlPanel({ panel }) {
1798
1936
  const first = rows[0];
1799
1937
  const v = first ? Object.values(first)[0] : 0;
1800
1938
  chart = /* @__PURE__ */ jsx(MetricView, { value: Number(v) || 0, config: { column: config.column || "", agg: config.agg, label: config.label } });
1939
+ } else if (chartType === "table") {
1940
+ chart = /* @__PURE__ */ jsx(DataTable, { rows, columns: config.columns });
1801
1941
  } else {
1802
1942
  const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
1803
1943
  switch (chartType) {
@@ -1837,7 +1977,7 @@ function PanelContent({ panel, records, columns }) {
1837
1977
  const { type } = panel;
1838
1978
  const config = panel.config || {};
1839
1979
  const effType = config.chartType || type;
1840
- if (config.sql || SQL_CHART_TYPES.has(effType)) {
1980
+ if (config.sql || SQL_CHART_TYPES.has(effType) || SQL_DATA_TYPES.has(effType)) {
1841
1981
  return /* @__PURE__ */ jsx(SqlPanel, { panel });
1842
1982
  }
1843
1983
  if (!records?.length && type !== "insight") {
@@ -1860,17 +2000,8 @@ function PanelContent({ panel, records, columns }) {
1860
2000
  return /* @__PURE__ */ jsx(MetricView, { records, config: { column: config.column || "", agg: config.agg, label: config.label } });
1861
2001
  case "insight":
1862
2002
  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
- }
2003
+ case "table":
2004
+ return /* @__PURE__ */ jsx(DataTable, { rows: (records || []).slice(0, 50), columns: config.columns || columns?.map((c) => c.name) });
1874
2005
  default:
1875
2006
  return /* @__PURE__ */ jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
1876
2007
  "Unknown panel type: ",
@@ -1878,45 +2009,112 @@ function PanelContent({ panel, records, columns }) {
1878
2009
  ] });
1879
2010
  }
1880
2011
  }
1881
- var ROW_HEIGHT = 180;
2012
+ var GRID_COLS = 12;
2013
+ var ROW_HEIGHT = 80;
2014
+ var GRID_MARGIN = 10;
2015
+ var DRAG_HANDLE = "panel-drag-handle";
2016
+ var RGL_CSS = `
2017
+ .react-grid-layout { position: relative; transition: height 200ms ease; }
2018
+ .react-grid-item { transition: all 200ms ease; transition-property: left, top, width, height; box-sizing: border-box; }
2019
+ .react-grid-item.cssTransforms { transition-property: transform, width, height; }
2020
+ .react-grid-item.resizing { transition: none; z-index: 3; will-change: width, height; }
2021
+ .react-grid-item.react-draggable-dragging { transition: none; z-index: 3; will-change: transform; }
2022
+ .react-grid-item.react-grid-placeholder { background: rgba(99,102,241,0.18); border: 1px dashed #6366f1; border-radius: 2px; transition-duration: 100ms; z-index: 2; user-select: none; }
2023
+ .react-grid-item > .react-resizable-handle { position: absolute; width: 18px; height: 18px; bottom: 0; right: 0; cursor: se-resize; }
2024
+ .react-grid-item > .react-resizable-handle::after { content: ''; position: absolute; right: 4px; bottom: 4px; width: 6px; height: 6px; border-right: 2px solid rgba(148,163,184,0.7); border-bottom: 2px solid rgba(148,163,184,0.7); }
2025
+ .${DRAG_HANDLE} { cursor: grab; }
2026
+ .react-grid-item.react-draggable-dragging .${DRAG_HANDLE} { cursor: grabbing; }
2027
+ `;
2028
+ var rglCssInjected = false;
2029
+ function useInjectRglCss() {
2030
+ useEffect(() => {
2031
+ if (rglCssInjected || typeof document === "undefined") return;
2032
+ const el = document.createElement("style");
2033
+ el.setAttribute("data-rgl", "dashboard-renderer");
2034
+ el.textContent = RGL_CSS;
2035
+ document.head.appendChild(el);
2036
+ rglCssInjected = true;
2037
+ }, []);
2038
+ }
2039
+ var GridLayoutWithWidth = WidthProvider(GridLayout);
2040
+ function buildLayout(panels) {
2041
+ let cx = 0, cy = 0, rowH = 0;
2042
+ return panels.map((p) => {
2043
+ const w = Math.min(Math.max(p.width || 6, 1), GRID_COLS);
2044
+ const h = Math.max(p.height || 4, 1);
2045
+ if (typeof p.x === "number" && typeof p.y === "number") {
2046
+ return { i: p.id, x: p.x, y: p.y, w, h, minW: 2, minH: 2 };
2047
+ }
2048
+ if (cx + w > GRID_COLS) {
2049
+ cx = 0;
2050
+ cy += rowH;
2051
+ rowH = 0;
2052
+ }
2053
+ const item = { i: p.id, x: cx, y: cy, w, h, minW: 2, minH: 2 };
2054
+ cx += w;
2055
+ rowH = Math.max(rowH, h);
2056
+ return item;
2057
+ });
2058
+ }
1882
2059
  function DashboardRenderer({ dashboard, records, columns }) {
1883
- const { theme, removePanel } = useDashboard();
1884
- const { canEditPanels } = useCapabilities();
2060
+ const { theme, removePanel, persistLayout } = useDashboard();
2061
+ const { canEditPanels, canEditLayout } = useCapabilities();
2062
+ useInjectRglCss();
2063
+ const panels = dashboard?.panels ?? [];
2064
+ const layout = useMemo(() => buildLayout(panels), [panels]);
2065
+ const onLayoutChange = useCallback((next) => {
2066
+ if (!persistLayout) return;
2067
+ persistLayout(next.map((l) => ({ id: l.i, x: l.x, y: l.y, w: l.w, h: l.h })));
2068
+ }, [persistLayout]);
1885
2069
  if (!dashboard) return null;
1886
2070
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 p-2", children: [
1887
2071
  dashboard.insights && /* @__PURE__ */ jsxs("div", { className: `border ${theme.border} bg-midnight-elevated px-4 py-3 flex items-start gap-3`, children: [
1888
2072
  /* @__PURE__ */ jsx(Sparkles, { className: "w-5 h-5 text-midnight-accent shrink-0 mt-0.5" }),
1889
2073
  /* @__PURE__ */ jsx("p", { className: `text-sm ${theme.text} leading-relaxed`, children: dashboard.insights })
1890
2074
  ] }),
1891
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-12 gap-3", children: dashboard.panels?.map((panel) => {
1892
- const colSpan = Math.min(Math.max(panel.width || 6, 1), 12);
1893
- const rowSpan = Math.min(Math.max(panel.height || 2, 1), 4);
1894
- return /* @__PURE__ */ jsxs(
1895
- "div",
1896
- {
1897
- className: `border ${theme.border} bg-midnight-surface flex flex-col`,
1898
- style: {
1899
- gridColumn: `span ${colSpan}`,
1900
- minHeight: `${rowSpan * ROW_HEIGHT}px`
1901
- },
1902
- children: [
1903
- /* @__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 }),
1905
- canEditPanels && removePanel && /* @__PURE__ */ jsx(
1906
- "button",
1907
- {
1908
- onClick: () => removePanel(panel.id),
1909
- className: "p-0.5 hover:bg-midnight-raised text-midnight-text-muted hover:text-midnight-text-body transition-colors",
1910
- children: /* @__PURE__ */ jsx(X, { className: "w-3 h-3" })
1911
- }
1912
- )
1913
- ] }),
1914
- /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewLoading, {}), children: /* @__PURE__ */ jsx(PanelContent, { panel, records, columns }) }) })
1915
- ]
1916
- },
1917
- panel.id
1918
- );
1919
- }) })
2075
+ /* @__PURE__ */ jsx(
2076
+ GridLayoutWithWidth,
2077
+ {
2078
+ className: "layout",
2079
+ layout,
2080
+ cols: GRID_COLS,
2081
+ rowHeight: ROW_HEIGHT,
2082
+ margin: [GRID_MARGIN, GRID_MARGIN],
2083
+ isDraggable: canEditLayout,
2084
+ isResizable: canEditLayout,
2085
+ draggableHandle: `.${DRAG_HANDLE}`,
2086
+ onDragStop: onLayoutChange,
2087
+ onResizeStop: onLayoutChange,
2088
+ compactType: canEditLayout ? "vertical" : null,
2089
+ children: panels.map((panel) => /* @__PURE__ */ jsxs("div", { className: `border ${theme.border} bg-midnight-surface flex flex-col overflow-hidden`, children: [
2090
+ /* @__PURE__ */ jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 border-b ${theme.border} bg-midnight-elevated ${canEditLayout ? DRAG_HANDLE : ""}`, children: [
2091
+ /* @__PURE__ */ jsx(
2092
+ "span",
2093
+ {
2094
+ className: "flex-1 text-xs font-mono text-midnight-text-body truncate px-1",
2095
+ style: {
2096
+ textAlign: panel.config?.style?.titleAlign || "left",
2097
+ fontWeight: panel.config?.style?.titleBold ? 700 : void 0,
2098
+ background: panel.config?.style?.titleBackground || void 0,
2099
+ color: panel.config?.style?.titleColor || void 0
2100
+ },
2101
+ children: panel.title || panel.type
2102
+ }
2103
+ ),
2104
+ canEditPanels && removePanel && /* @__PURE__ */ jsx(
2105
+ "button",
2106
+ {
2107
+ onMouseDown: (e) => e.stopPropagation(),
2108
+ onClick: () => removePanel(panel.id),
2109
+ className: "p-0.5 hover:bg-midnight-raised text-midnight-text-muted hover:text-midnight-text-body transition-colors",
2110
+ children: /* @__PURE__ */ jsx(X, { className: "w-3 h-3" })
2111
+ }
2112
+ )
2113
+ ] }),
2114
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewLoading, {}), children: /* @__PURE__ */ jsx(PanelContent, { panel, records, columns }) }) })
2115
+ ] }, panel.id))
2116
+ }
2117
+ )
1920
2118
  ] });
1921
2119
  }
1922
2120
  function ProfileSummary({ profile, theme }) {