@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.cjs CHANGED
@@ -13,11 +13,13 @@ var lucideReact = require('lucide-react');
13
13
  var CodeMirror = require('@uiw/react-codemirror');
14
14
  var langSql = require('@codemirror/lang-sql');
15
15
  var autocomplete = require('@codemirror/autocomplete');
16
+ var GridLayout = require('react-grid-layout');
16
17
 
17
18
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
19
 
19
20
  var PivotTableUI__default = /*#__PURE__*/_interopDefault(PivotTableUI);
20
21
  var CodeMirror__default = /*#__PURE__*/_interopDefault(CodeMirror);
22
+ var GridLayout__default = /*#__PURE__*/_interopDefault(GridLayout);
21
23
 
22
24
  // src/context/DashboardContext.tsx
23
25
  var DashboardContext = react.createContext(null);
@@ -46,7 +48,9 @@ function useCapabilities() {
46
48
  canAddPanel: !!c.addPanel,
47
49
  canEditPanels: !!c.removePanel,
48
50
  // Studio can recompute + persist a panel's precomputed result.
49
- canRefresh: !!c.persistPanelData
51
+ canRefresh: !!c.persistPanelData,
52
+ // Studio can drag/resize panels and persist the grid layout.
53
+ canEditLayout: !!c.persistLayout
50
54
  };
51
55
  }
52
56
 
@@ -142,8 +146,10 @@ var buildChartSQL = ({
142
146
  }
143
147
  case "scatter": {
144
148
  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}`;
149
+ const nx = `TRY_CAST(${qIdent(x)} AS DOUBLE)`, ny = `TRY_CAST(${qIdent(y)} AS DOUBLE)`;
150
+ const extra = `${nx} IS NOT NULL AND ${ny} IS NOT NULL`;
151
+ const w = where ? `${where} AND ${extra}` : ` WHERE ${extra}`;
152
+ return `SELECT ${nx} AS x, ${ny} AS y FROM ${TABLE}${w} LIMIT ${MAX_POINTS}`;
147
153
  }
148
154
  case "heatmap": {
149
155
  if (!x || !y) return null;
@@ -175,7 +181,10 @@ var shapeChartData = (chartType, rows, { yFields = [] } = {}) => {
175
181
  case "line":
176
182
  return [{ id: "series", data: rows.map((r) => ({ x: String(r.x), y: Number(r.y) || 0 })) }];
177
183
  case "scatter":
178
- return [{ id: "points", data: rows.map((r) => ({ x: Number(r.x) || 0, y: Number(r.y) || 0 })) }];
184
+ return [{
185
+ id: "points",
186
+ data: rows.map((r) => ({ x: Number(r.x), y: Number(r.y) })).filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y))
187
+ }];
179
188
  case "heatmap": {
180
189
  const rowKeys = [];
181
190
  const colKeys = [];
@@ -217,10 +226,25 @@ var DEFAULT_CHART_STYLE = {
217
226
  xLegendOffset: 46,
218
227
  yLegendPosition: "middle",
219
228
  yLegendOffset: -50,
229
+ // Custom axis title text + visibility.
230
+ xAxisLabel: "",
231
+ yAxisLabel: "",
232
+ showXLegend: true,
233
+ showYLegend: true,
234
+ // Axis title emphasis.
235
+ legendBold: false,
236
+ legendHighlight: "",
237
+ // Tick overflow handling.
238
+ xTickRotation: -35,
239
+ yTickRotation: 0,
240
+ tickTruncate: 0,
220
241
  // Series legend placement (charts that have one: pie, grouped bar).
221
242
  legendAnchor: "right",
222
- // Panel title alignment (rendered by DashboardRenderer's panel header).
223
- titleAlign: "left"
243
+ // Panel title (rendered by DashboardRenderer's panel header).
244
+ titleAlign: "left",
245
+ titleBold: false,
246
+ titleBackground: "",
247
+ titleColor: ""
224
248
  };
225
249
  var LEGEND_ANCHORS = ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"];
226
250
  var withStyleDefaults = (style) => ({
@@ -234,7 +258,17 @@ var buildNivoTheme = (style) => {
234
258
  text: { fill: s.textColor, fontSize: s.fontSize },
235
259
  axis: {
236
260
  ticks: { text: { fill: s.textColor, fontSize: s.fontSize } },
237
- legend: { text: { fill: s.legendColor, fontSize: s.fontSize + 2 } }
261
+ legend: {
262
+ text: {
263
+ fill: s.legendColor,
264
+ fontSize: s.fontSize + 2,
265
+ fontWeight: s.legendBold ? 700 : 400,
266
+ // A highlight is drawn as a text outline (halo) — the clean,
267
+ // SVG-native way to make an axis title stand out.
268
+ outlineWidth: s.legendHighlight ? 3 : 0,
269
+ outlineColor: s.legendHighlight || "transparent"
270
+ }
271
+ }
238
272
  },
239
273
  grid: { line: { stroke: s.gridColor } },
240
274
  crosshair: { line: { stroke: s.textColor, strokeDasharray: "6 6" } },
@@ -263,14 +297,25 @@ var legendConfig = (style) => {
263
297
  const ty = s.legendAnchor.includes("top") ? 10 : -10;
264
298
  return { ...base, translateX: tx, translateY: ty };
265
299
  };
266
- var axisLegend = (style, axis, legendText) => {
300
+ var truncate = (v, n) => {
301
+ const str = String(v);
302
+ return n > 0 && str.length > n ? `${str.slice(0, n)}\u2026` : str;
303
+ };
304
+ var axisLegend = (style, axis, columnName, opts2 = {}) => {
267
305
  const s = withStyleDefaults(style);
268
306
  const isX = axis === "x";
269
- return {
270
- legend: legendText,
271
- legendPosition: isX ? s.xLegendPosition : s.yLegendPosition,
272
- legendOffset: isX ? s.xLegendOffset : s.yLegendOffset
273
- };
307
+ const show = isX ? s.showXLegend : s.showYLegend;
308
+ const label = (isX ? s.xAxisLabel : s.yAxisLabel) || columnName;
309
+ const out = {};
310
+ if (show && label) {
311
+ out.legend = label;
312
+ out.legendPosition = isX ? s.xLegendPosition : s.yLegendPosition;
313
+ out.legendOffset = isX ? s.xLegendOffset : s.yLegendOffset;
314
+ }
315
+ out.tickRotation = isX ? s.xTickRotation : s.yTickRotation;
316
+ if (opts2.numeric) out.format = (v) => Number(v).toLocaleString();
317
+ else if (s.tickTruncate > 0) out.format = (v) => truncate(v, s.tickTruncate);
318
+ return out;
274
319
  };
275
320
 
276
321
  // src/dataShape.ts
@@ -334,8 +379,8 @@ function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: pre
334
379
  padding: 0.3,
335
380
  colors: ["rgba(74, 222, 128, 0.8)"],
336
381
  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() },
382
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", groupColumn) },
383
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", valueColumn, { numeric: true }) },
339
384
  labelSkipWidth: 12,
340
385
  labelSkipHeight: 12,
341
386
  labelTextColor: { from: "color", modifiers: [["darker", 3]] },
@@ -408,8 +453,8 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
408
453
  pointBorderWidth: 2,
409
454
  pointBorderColor: { from: "serieColor" },
410
455
  enableGridX: false,
411
- axisBottom: { tickSize: 5, tickPadding: 5, tickRotation: -35, ...axisLegend(style, "x", xColumn) },
412
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn) },
456
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn) },
457
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn, { numeric: true }) },
413
458
  useMesh: true,
414
459
  theme: buildNivoTheme(style)
415
460
  }
@@ -418,7 +463,7 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
418
463
  function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
419
464
  const computed = react.useMemo(() => {
420
465
  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);
466
+ 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
467
  return [{ id: `${xColumn} vs ${yColumn}`, data: points }];
423
468
  }, [records, xColumn, yColumn, presetData]);
424
469
  const data = presetData || computed;
@@ -431,8 +476,8 @@ function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
431
476
  yScale: { type: "linear", min: "auto", max: "auto" },
432
477
  colors: ["rgba(167, 139, 250, 0.7)"],
433
478
  nodeSize: 6,
434
- axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn) },
435
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn) },
479
+ axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn, { numeric: true }) },
480
+ axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn, { numeric: true }) },
436
481
  useMesh: true,
437
482
  theme: buildNivoTheme(style)
438
483
  }
@@ -531,8 +576,8 @@ function HeatmapView({
531
576
  {
532
577
  data,
533
578
  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 },
579
+ axisTop: { tickSize: 5, tickPadding: 5, tickRotation: s.xTickRotation, legend: s.showXLegend ? s.xAxisLabel || colColumn : "", legendPosition: s.xLegendPosition, legendOffset: -50 },
580
+ axisLeft: { tickSize: 5, tickPadding: 5, legend: s.showYLegend ? s.yAxisLabel || rowColumn : "", legendPosition: s.yLegendPosition, legendOffset: -80 },
536
581
  axisRight: showTotals ? { tickSize: 5, tickPadding: 5, format: (id) => fmtNum(rowTotals[id]), legend: `${marginAgg} \u25B8`, legendOffset: 60 } : null,
537
582
  axisBottom: showTotals ? { tickSize: 5, tickPadding: 5, tickRotation: -35, format: (id) => fmtNum(colTotals[id]) } : null,
538
583
  colors: { type: "sequential", scheme: "blue_green", minValue: 0 },
@@ -576,11 +621,17 @@ function PivotView({ records }) {
576
621
  )
577
622
  ] });
578
623
  }
579
- function Row({ label, children }) {
624
+ function Section({ title, children }) {
580
625
  const { theme } = useDashboard();
581
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
582
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-xs ${theme.font} text-midnight-text-muted`, children: label }),
583
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", children })
626
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `py-3 border-t ${theme.border} first:border-t-0 first:pt-0`, children: [
627
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-[10px] uppercase tracking-wider text-midnight-text-muted/70 font-mono mb-2", children: title }),
628
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1.5", children })
629
+ ] });
630
+ }
631
+ function Row({ label, children }) {
632
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[88px_1fr] items-center gap-2 min-h-[30px]", children: [
633
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-mono text-midnight-text-muted truncate", title: label, children: label }),
634
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-end", children })
584
635
  ] });
585
636
  }
586
637
  function ColorControl({ value, onChange }) {
@@ -591,61 +642,112 @@ function ColorControl({ value, onChange }) {
591
642
  type: "color",
592
643
  value: /^#/.test(value) ? value : "#94a3b8",
593
644
  onChange: (e) => onChange(e.target.value),
594
- className: `w-9 h-6 bg-transparent border ${theme.border} cursor-pointer`
645
+ className: `w-9 h-6 bg-transparent border ${theme.border} cursor-pointer p-0`
595
646
  }
596
647
  );
597
648
  }
598
- function NumberControl({ value, onChange, min, max }) {
649
+ function SliderControl({ value, onChange, min, max, step = 1, unit = "" }) {
650
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 w-full", children: [
651
+ /* @__PURE__ */ jsxRuntime.jsx(
652
+ "input",
653
+ {
654
+ type: "range",
655
+ min,
656
+ max,
657
+ step,
658
+ value,
659
+ onChange: (e) => onChange(Number(e.target.value)),
660
+ className: "flex-1 h-1 accent-midnight-accent cursor-pointer"
661
+ }
662
+ ),
663
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "w-10 shrink-0 text-right text-[11px] font-mono tabular-nums text-midnight-text-body", children: [
664
+ value,
665
+ unit
666
+ ] })
667
+ ] });
668
+ }
669
+ function TextControl({ value, onChange, placeholder }) {
599
670
  const { theme } = useDashboard();
600
671
  return /* @__PURE__ */ jsxRuntime.jsx(
601
672
  "input",
602
673
  {
603
- type: "number",
674
+ type: "text",
604
675
  value,
605
- min,
606
- max,
607
- onChange: (e) => onChange(Number(e.target.value)),
608
- 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`
676
+ placeholder,
677
+ onChange: (e) => onChange(e.target.value),
678
+ 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`
679
+ }
680
+ );
681
+ }
682
+ function Switch({ value, onChange }) {
683
+ return /* @__PURE__ */ jsxRuntime.jsx(
684
+ "button",
685
+ {
686
+ type: "button",
687
+ role: "switch",
688
+ "aria-checked": value,
689
+ onClick: () => onChange(!value),
690
+ className: `relative inline-flex h-4 w-7 shrink-0 items-center rounded-full transition-colors ${value ? "bg-midnight-accent" : "bg-midnight-border"}`,
691
+ children: /* @__PURE__ */ jsxRuntime.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"}` })
609
692
  }
610
693
  );
611
694
  }
695
+ function OptionalColor({ value, onChange, fallback = "#a78bfa" }) {
696
+ const on = !!value;
697
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
698
+ on && /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value, onChange }),
699
+ /* @__PURE__ */ jsxRuntime.jsx(Switch, { value: on, onChange: (v) => onChange(v ? fallback : "") })
700
+ ] });
701
+ }
612
702
  function Choice({ value, options, onChange }) {
613
703
  const { theme } = useDashboard();
614
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-28", children: /* @__PURE__ */ jsxRuntime.jsx(
704
+ return /* @__PURE__ */ jsxRuntime.jsx(
615
705
  "select",
616
706
  {
617
707
  value,
618
708
  onChange: (e) => onChange(e.target.value),
619
- 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`,
709
+ 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`,
620
710
  children: options.map((o) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: o, children: o }, o))
621
711
  }
622
- ) });
623
- }
624
- function Group({ children, last }) {
625
- const { theme } = useDashboard();
626
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `space-y-2 pb-3 ${last ? "" : `mb-3 border-b ${theme.border}`}`, children });
712
+ );
627
713
  }
628
714
  function ChartStyleControls({ style, onChange }) {
629
715
  const s = withStyleDefaults(style);
630
716
  const set = (key, value) => onChange({ ...s, [key]: value });
631
717
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
632
- /* @__PURE__ */ jsxRuntime.jsxs(Group, { children: [
718
+ /* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "Canvas", children: [
633
719
  /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Background", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.background, onChange: (v) => set("background", v) }) }),
634
- /* @__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
- /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Grid color", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.gridColor, onChange: (v) => set("gridColor", v) }) }),
637
- /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Font size", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.fontSize, min: 6, max: 24, onChange: (v) => set("fontSize", v) }) })
720
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Text", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.textColor, onChange: (v) => set("textColor", v) }) }),
721
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Grid", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.gridColor, onChange: (v) => set("gridColor", v) }) }),
722
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Font size", children: /* @__PURE__ */ jsxRuntime.jsx(SliderControl, { value: s.fontSize, min: 6, max: 24, unit: "px", onChange: (v) => set("fontSize", v) }) })
638
723
  ] }),
639
- /* @__PURE__ */ jsxRuntime.jsxs(Group, { children: [
640
- /* @__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
- /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X title offset", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.xLegendOffset, onChange: (v) => set("xLegendOffset", v) }) }),
642
- /* @__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
- /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Y title offset", children: /* @__PURE__ */ jsxRuntime.jsx(NumberControl, { value: s.yLegendOffset, onChange: (v) => set("yLegendOffset", v) }) })
724
+ /* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "Axis titles", children: [
725
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X title", children: /* @__PURE__ */ jsxRuntime.jsx(TextControl, { value: s.xAxisLabel, placeholder: "(column)", onChange: (v) => set("xAxisLabel", v) }) }),
726
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Show X", children: /* @__PURE__ */ jsxRuntime.jsx(Switch, { value: s.showXLegend, onChange: (v) => set("showXLegend", v) }) }),
727
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Y title", children: /* @__PURE__ */ jsxRuntime.jsx(TextControl, { value: s.yAxisLabel, placeholder: "(column)", onChange: (v) => set("yAxisLabel", v) }) }),
728
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Show Y", children: /* @__PURE__ */ jsxRuntime.jsx(Switch, { value: s.showYLegend, onChange: (v) => set("showYLegend", v) }) }),
729
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Color", children: /* @__PURE__ */ jsxRuntime.jsx(ColorControl, { value: s.legendColor, onChange: (v) => set("legendColor", v) }) }),
730
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Bold", children: /* @__PURE__ */ jsxRuntime.jsx(Switch, { value: s.legendBold, onChange: (v) => set("legendBold", v) }) }),
731
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Highlight", children: /* @__PURE__ */ jsxRuntime.jsx(OptionalColor, { value: s.legendHighlight, onChange: (v) => set("legendHighlight", v) }) })
644
732
  ] }),
645
- /* @__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) }) })
648
- ] })
733
+ /* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "Axis placement", children: [
734
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X pos", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.xLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("xLegendPosition", v) }) }),
735
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X offset", children: /* @__PURE__ */ jsxRuntime.jsx(SliderControl, { value: s.xLegendOffset, min: -80, max: 80, onChange: (v) => set("xLegendOffset", v) }) }),
736
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Y pos", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.yLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("yLegendPosition", v) }) }),
737
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Y offset", children: /* @__PURE__ */ jsxRuntime.jsx(SliderControl, { value: s.yLegendOffset, min: -80, max: 80, onChange: (v) => set("yLegendOffset", v) }) })
738
+ ] }),
739
+ /* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "Ticks", children: [
740
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "X angle", children: /* @__PURE__ */ jsxRuntime.jsx(SliderControl, { value: s.xTickRotation, min: -90, max: 90, unit: "\xB0", onChange: (v) => set("xTickRotation", v) }) }),
741
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Y angle", children: /* @__PURE__ */ jsxRuntime.jsx(SliderControl, { value: s.yTickRotation, min: -90, max: 90, unit: "\xB0", onChange: (v) => set("yTickRotation", v) }) }),
742
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Truncate", children: /* @__PURE__ */ jsxRuntime.jsx(SliderControl, { value: s.tickTruncate, min: 0, max: 40, onChange: (v) => set("tickTruncate", v) }) })
743
+ ] }),
744
+ /* @__PURE__ */ jsxRuntime.jsxs(Section, { title: "Panel title", children: [
745
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Align", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.titleAlign, options: ["left", "center", "right"], onChange: (v) => set("titleAlign", v) }) }),
746
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Bold", children: /* @__PURE__ */ jsxRuntime.jsx(Switch, { value: s.titleBold, onChange: (v) => set("titleBold", v) }) }),
747
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Color", children: /* @__PURE__ */ jsxRuntime.jsx(OptionalColor, { value: s.titleColor, onChange: (v) => set("titleColor", v), fallback: "#e2e8f0" }) }),
748
+ /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Background", children: /* @__PURE__ */ jsxRuntime.jsx(OptionalColor, { value: s.titleBackground, onChange: (v) => set("titleBackground", v), fallback: "#1e293b" }) })
749
+ ] }),
750
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { title: "Series legend", children: /* @__PURE__ */ jsxRuntime.jsx(Row, { label: "Position", children: /* @__PURE__ */ jsxRuntime.jsx(Choice, { value: s.legendAnchor, options: ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"], onChange: (v) => set("legendAnchor", v) }) }) })
649
751
  ] });
650
752
  }
651
753
  var AGGREGATES = [
@@ -1369,6 +1471,7 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1369
1471
  const [showTotals, setShowTotals] = react.useState(false);
1370
1472
  const [style, setStyle] = react.useState(DEFAULT_CHART_STYLE);
1371
1473
  const [showStyle, setShowStyle] = react.useState(false);
1474
+ const [showFields, setShowFields] = react.useState(false);
1372
1475
  const [sqlRows, setSqlRows] = react.useState(null);
1373
1476
  const [sqlLoading, setSqlLoading] = react.useState(false);
1374
1477
  const [sqlError, setSqlError] = react.useState(null);
@@ -1654,8 +1757,23 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1654
1757
  )
1655
1758
  ] }),
1656
1759
  /* @__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: [
1760
+ /* @__PURE__ */ jsxRuntime.jsxs(
1761
+ "button",
1762
+ {
1763
+ onClick: () => setShowFields((s) => !s),
1764
+ className: "flex items-center gap-1 text-xs uppercase text-midnight-text-muted font-mono hover:text-midnight-text-body transition-colors",
1765
+ children: [
1766
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: `w-3 h-3 transition-transform ${showFields ? "" : "-rotate-90"}` }),
1767
+ "Available Fields",
1768
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-midnight-text-muted/60 normal-case", children: [
1769
+ "(",
1770
+ columns.length,
1771
+ ")"
1772
+ ] })
1773
+ ]
1774
+ }
1775
+ ),
1776
+ 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
1777
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-midnight-text-body", children: col.name }),
1660
1778
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-midnight-text-muted", children: col.type })
1661
1779
  ] }, col.name)) })
@@ -1685,6 +1803,15 @@ function ViewLoading() {
1685
1803
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-xs text-midnight-text-muted", children: "Loading..." });
1686
1804
  }
1687
1805
  var SQL_CHART_TYPES = /* @__PURE__ */ new Set(["bar", "grouped-bar", "pie", "line", "scatter", "heatmap"]);
1806
+ var SQL_DATA_TYPES = /* @__PURE__ */ new Set(["table", "metric"]);
1807
+ var TABLE_PREVIEW_ROWS = 200;
1808
+ function tableSql(config) {
1809
+ const cols = config.columns?.length ? config.columns.map(qIdent).join(", ") : "*";
1810
+ return `SELECT ${cols} FROM data LIMIT ${TABLE_PREVIEW_ROWS}`;
1811
+ }
1812
+ function metricSql(config) {
1813
+ return `SELECT ${aggExpr(config.agg, config.column)} AS v FROM data`;
1814
+ }
1688
1815
  function panelToChartConfig(chartType, config) {
1689
1816
  const cfg = { chartType, xFields: [], yFields: [], filters: config.filters || [] };
1690
1817
  switch (chartType) {
@@ -1709,11 +1836,23 @@ function panelToChartConfig(chartType, config) {
1709
1836
  }
1710
1837
  return cfg;
1711
1838
  }
1712
- function panelSql(chartType, config) {
1839
+ function panelSql(type, config) {
1713
1840
  if (config.sql) return config.sql;
1714
- if (SQL_CHART_TYPES.has(chartType)) return buildChartSQL(panelToChartConfig(chartType, config)) || "";
1841
+ if (type === "table") return tableSql(config);
1842
+ if (type === "metric") return metricSql(config);
1843
+ if (SQL_CHART_TYPES.has(type)) return buildChartSQL(panelToChartConfig(type, config)) || "";
1715
1844
  return "";
1716
1845
  }
1846
+ function DataTable({ rows, columns }) {
1847
+ const colNames = columns?.length ? columns : rows[0] ? Object.keys(rows[0]) : [];
1848
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto h-full text-xs", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full", children: [
1849
+ /* @__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)) }) }),
1850
+ /* @__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) => {
1851
+ const cellValue = r[n];
1852
+ 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);
1853
+ }) }, i)) })
1854
+ ] }) });
1855
+ }
1717
1856
  function SqlPanel({ panel }) {
1718
1857
  const { runQuery, persistPanelData } = useDashboard();
1719
1858
  const { canRefresh } = useCapabilities();
@@ -1805,6 +1944,8 @@ function SqlPanel({ panel }) {
1805
1944
  const first = rows[0];
1806
1945
  const v = first ? Object.values(first)[0] : 0;
1807
1946
  chart = /* @__PURE__ */ jsxRuntime.jsx(MetricView, { value: Number(v) || 0, config: { column: config.column || "", agg: config.agg, label: config.label } });
1947
+ } else if (chartType === "table") {
1948
+ chart = /* @__PURE__ */ jsxRuntime.jsx(DataTable, { rows, columns: config.columns });
1808
1949
  } else {
1809
1950
  const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
1810
1951
  switch (chartType) {
@@ -1844,7 +1985,7 @@ function PanelContent({ panel, records, columns }) {
1844
1985
  const { type } = panel;
1845
1986
  const config = panel.config || {};
1846
1987
  const effType = config.chartType || type;
1847
- if (config.sql || SQL_CHART_TYPES.has(effType)) {
1988
+ if (config.sql || SQL_CHART_TYPES.has(effType) || SQL_DATA_TYPES.has(effType)) {
1848
1989
  return /* @__PURE__ */ jsxRuntime.jsx(SqlPanel, { panel });
1849
1990
  }
1850
1991
  if (!records?.length && type !== "insight") {
@@ -1867,17 +2008,8 @@ function PanelContent({ panel, records, columns }) {
1867
2008
  return /* @__PURE__ */ jsxRuntime.jsx(MetricView, { records, config: { column: config.column || "", agg: config.agg, label: config.label } });
1868
2009
  case "insight":
1869
2010
  return /* @__PURE__ */ jsxRuntime.jsx(InsightView, { config: { text: config.text } });
1870
- case "table": {
1871
- const cols = config.columns ? config.columns.map((c) => ({ name: c })) : columns;
1872
- const colNames = cols?.map((c) => c.name) || (records && records[0] ? Object.keys(records[0]) : []);
1873
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto h-full text-xs", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full", children: [
1874
- /* @__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)) }) }),
1875
- /* @__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) => {
1876
- const cellValue = r[n];
1877
- 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);
1878
- }) }, i)) })
1879
- ] }) });
1880
- }
2011
+ case "table":
2012
+ return /* @__PURE__ */ jsxRuntime.jsx(DataTable, { rows: (records || []).slice(0, 50), columns: config.columns || columns?.map((c) => c.name) });
1881
2013
  default:
1882
2014
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
1883
2015
  "Unknown panel type: ",
@@ -1885,45 +2017,112 @@ function PanelContent({ panel, records, columns }) {
1885
2017
  ] });
1886
2018
  }
1887
2019
  }
1888
- var ROW_HEIGHT = 180;
2020
+ var GRID_COLS = 12;
2021
+ var ROW_HEIGHT = 80;
2022
+ var GRID_MARGIN = 10;
2023
+ var DRAG_HANDLE = "panel-drag-handle";
2024
+ var RGL_CSS = `
2025
+ .react-grid-layout { position: relative; transition: height 200ms ease; }
2026
+ .react-grid-item { transition: all 200ms ease; transition-property: left, top, width, height; box-sizing: border-box; }
2027
+ .react-grid-item.cssTransforms { transition-property: transform, width, height; }
2028
+ .react-grid-item.resizing { transition: none; z-index: 3; will-change: width, height; }
2029
+ .react-grid-item.react-draggable-dragging { transition: none; z-index: 3; will-change: transform; }
2030
+ .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; }
2031
+ .react-grid-item > .react-resizable-handle { position: absolute; width: 18px; height: 18px; bottom: 0; right: 0; cursor: se-resize; }
2032
+ .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); }
2033
+ .${DRAG_HANDLE} { cursor: grab; }
2034
+ .react-grid-item.react-draggable-dragging .${DRAG_HANDLE} { cursor: grabbing; }
2035
+ `;
2036
+ var rglCssInjected = false;
2037
+ function useInjectRglCss() {
2038
+ react.useEffect(() => {
2039
+ if (rglCssInjected || typeof document === "undefined") return;
2040
+ const el = document.createElement("style");
2041
+ el.setAttribute("data-rgl", "dashboard-renderer");
2042
+ el.textContent = RGL_CSS;
2043
+ document.head.appendChild(el);
2044
+ rglCssInjected = true;
2045
+ }, []);
2046
+ }
2047
+ var GridLayoutWithWidth = GridLayout.WidthProvider(GridLayout__default.default);
2048
+ function buildLayout(panels) {
2049
+ let cx = 0, cy = 0, rowH = 0;
2050
+ return panels.map((p) => {
2051
+ const w = Math.min(Math.max(p.width || 6, 1), GRID_COLS);
2052
+ const h = Math.max(p.height || 4, 1);
2053
+ if (typeof p.x === "number" && typeof p.y === "number") {
2054
+ return { i: p.id, x: p.x, y: p.y, w, h, minW: 2, minH: 2 };
2055
+ }
2056
+ if (cx + w > GRID_COLS) {
2057
+ cx = 0;
2058
+ cy += rowH;
2059
+ rowH = 0;
2060
+ }
2061
+ const item = { i: p.id, x: cx, y: cy, w, h, minW: 2, minH: 2 };
2062
+ cx += w;
2063
+ rowH = Math.max(rowH, h);
2064
+ return item;
2065
+ });
2066
+ }
1889
2067
  function DashboardRenderer({ dashboard, records, columns }) {
1890
- const { theme, removePanel } = useDashboard();
1891
- const { canEditPanels } = useCapabilities();
2068
+ const { theme, removePanel, persistLayout } = useDashboard();
2069
+ const { canEditPanels, canEditLayout } = useCapabilities();
2070
+ useInjectRglCss();
2071
+ const panels = dashboard?.panels ?? [];
2072
+ const layout = react.useMemo(() => buildLayout(panels), [panels]);
2073
+ const onLayoutChange = react.useCallback((next) => {
2074
+ if (!persistLayout) return;
2075
+ persistLayout(next.map((l) => ({ id: l.i, x: l.x, y: l.y, w: l.w, h: l.h })));
2076
+ }, [persistLayout]);
1892
2077
  if (!dashboard) return null;
1893
2078
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4 p-2", children: [
1894
2079
  dashboard.insights && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `border ${theme.border} bg-midnight-elevated px-4 py-3 flex items-start gap-3`, children: [
1895
2080
  /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "w-5 h-5 text-midnight-accent shrink-0 mt-0.5" }),
1896
2081
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-sm ${theme.text} leading-relaxed`, children: dashboard.insights })
1897
2082
  ] }),
1898
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-12 gap-3", children: dashboard.panels?.map((panel) => {
1899
- const colSpan = Math.min(Math.max(panel.width || 6, 1), 12);
1900
- const rowSpan = Math.min(Math.max(panel.height || 2, 1), 4);
1901
- return /* @__PURE__ */ jsxRuntime.jsxs(
1902
- "div",
1903
- {
1904
- className: `border ${theme.border} bg-midnight-surface flex flex-col`,
1905
- style: {
1906
- gridColumn: `span ${colSpan}`,
1907
- minHeight: `${rowSpan * ROW_HEIGHT}px`
1908
- },
1909
- children: [
1910
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 border-b ${theme.border} bg-midnight-elevated`, children: [
1911
- /* @__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 }),
1912
- canEditPanels && removePanel && /* @__PURE__ */ jsxRuntime.jsx(
1913
- "button",
1914
- {
1915
- onClick: () => removePanel(panel.id),
1916
- className: "p-0.5 hover:bg-midnight-raised text-midnight-text-muted hover:text-midnight-text-body transition-colors",
1917
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "w-3 h-3" })
1918
- }
1919
- )
1920
- ] }),
1921
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx(ViewLoading, {}), children: /* @__PURE__ */ jsxRuntime.jsx(PanelContent, { panel, records, columns }) }) })
1922
- ]
1923
- },
1924
- panel.id
1925
- );
1926
- }) })
2083
+ /* @__PURE__ */ jsxRuntime.jsx(
2084
+ GridLayoutWithWidth,
2085
+ {
2086
+ className: "layout",
2087
+ layout,
2088
+ cols: GRID_COLS,
2089
+ rowHeight: ROW_HEIGHT,
2090
+ margin: [GRID_MARGIN, GRID_MARGIN],
2091
+ isDraggable: canEditLayout,
2092
+ isResizable: canEditLayout,
2093
+ draggableHandle: `.${DRAG_HANDLE}`,
2094
+ onDragStop: onLayoutChange,
2095
+ onResizeStop: onLayoutChange,
2096
+ compactType: canEditLayout ? "vertical" : null,
2097
+ children: panels.map((panel) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `border ${theme.border} bg-midnight-surface flex flex-col overflow-hidden`, children: [
2098
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 border-b ${theme.border} bg-midnight-elevated ${canEditLayout ? DRAG_HANDLE : ""}`, children: [
2099
+ /* @__PURE__ */ jsxRuntime.jsx(
2100
+ "span",
2101
+ {
2102
+ className: "flex-1 text-xs font-mono text-midnight-text-body truncate px-1",
2103
+ style: {
2104
+ textAlign: panel.config?.style?.titleAlign || "left",
2105
+ fontWeight: panel.config?.style?.titleBold ? 700 : void 0,
2106
+ background: panel.config?.style?.titleBackground || void 0,
2107
+ color: panel.config?.style?.titleColor || void 0
2108
+ },
2109
+ children: panel.title || panel.type
2110
+ }
2111
+ ),
2112
+ canEditPanels && removePanel && /* @__PURE__ */ jsxRuntime.jsx(
2113
+ "button",
2114
+ {
2115
+ onMouseDown: (e) => e.stopPropagation(),
2116
+ onClick: () => removePanel(panel.id),
2117
+ className: "p-0.5 hover:bg-midnight-raised text-midnight-text-muted hover:text-midnight-text-body transition-colors",
2118
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "w-3 h-3" })
2119
+ }
2120
+ )
2121
+ ] }),
2122
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx(ViewLoading, {}), children: /* @__PURE__ */ jsxRuntime.jsx(PanelContent, { panel, records, columns }) }) })
2123
+ ] }, panel.id))
2124
+ }
2125
+ )
1927
2126
  ] });
1928
2127
  }
1929
2128
  function ProfileSummary({ profile, theme }) {