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

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';
@@ -7,10 +7,12 @@ import { ResponsiveScatterPlot } from '@nivo/scatterplot';
7
7
  import { ResponsiveHeatMap } from '@nivo/heatmap';
8
8
  import PivotTableUI from 'react-pivottable/PivotTableUI';
9
9
  import 'react-pivottable/pivottable.css';
10
- import { Loader2, Play, ChevronDown, AlertCircle, Rows3, BarChart3, PieChart, TrendingUp, ScatterChart, LayoutGrid, Code, Sparkles, X, Terminal, RefreshCw, Minimize2, Maximize2, Plus, Save, FolderOpen, Trash2, Filter, Database, Send, GripVertical } from 'lucide-react';
10
+ import { TerminalSlider, TerminalInput, TerminalToggle, TerminalSelect } from '@quantumwake/terminal-ux-components';
11
+ import { Loader2, Play, ChevronDown, AlertCircle, Rows3, BarChart3, PieChart, TrendingUp, ScatterChart, LayoutGrid, Code, Sparkles, Terminal, RefreshCw, Minimize2, Maximize2, Plus, Save, FolderOpen, Trash2, X, Filter, Database, Send, GripVertical } from 'lucide-react';
11
12
  import CodeMirror, { EditorView, Prec, keymap } from '@uiw/react-codemirror';
12
13
  import { sql, PostgreSQL, schemaCompletionSource, keywordCompletionSource } from '@codemirror/lang-sql';
13
14
  import { snippetCompletion, completeFromList, autocompletion, acceptCompletion } from '@codemirror/autocomplete';
15
+ import GridLayout, { WidthProvider } from 'react-grid-layout';
14
16
 
15
17
  // src/context/DashboardContext.tsx
16
18
  var DashboardContext = createContext(null);
@@ -39,7 +41,9 @@ function useCapabilities() {
39
41
  canAddPanel: !!c.addPanel,
40
42
  canEditPanels: !!c.removePanel,
41
43
  // Studio can recompute + persist a panel's precomputed result.
42
- canRefresh: !!c.persistPanelData
44
+ canRefresh: !!c.persistPanelData,
45
+ // Studio can drag/resize panels and persist the grid layout.
46
+ canEditLayout: !!c.persistLayout
43
47
  };
44
48
  }
45
49
 
@@ -224,8 +228,15 @@ var DEFAULT_CHART_STYLE = {
224
228
  legendBold: false,
225
229
  legendHighlight: "",
226
230
  // Tick overflow handling.
227
- xTickRotation: -35,
231
+ xTickRotation: 0,
232
+ yTickRotation: 0,
228
233
  tickTruncate: 0,
234
+ tickWrap: true,
235
+ tickWrapWidth: 14,
236
+ // Table cells.
237
+ cellWrap: true,
238
+ cellPrecision: 2,
239
+ cellTruncate: 0,
229
240
  // Series legend placement (charts that have one: pie, grouped bar).
230
241
  legendAnchor: "right",
231
242
  // Panel title (rendered by DashboardRenderer's panel header).
@@ -300,7 +311,7 @@ var axisLegend = (style, axis, columnName, opts2 = {}) => {
300
311
  out.legendPosition = isX ? s.xLegendPosition : s.yLegendPosition;
301
312
  out.legendOffset = isX ? s.xLegendOffset : s.yLegendOffset;
302
313
  }
303
- if (isX) out.tickRotation = s.xTickRotation;
314
+ out.tickRotation = isX ? s.xTickRotation : s.yTickRotation;
304
315
  if (opts2.numeric) out.format = (v) => Number(v).toLocaleString();
305
316
  else if (s.tickTruncate > 0) out.format = (v) => truncate(v, s.tickTruncate);
306
317
  return out;
@@ -350,6 +361,78 @@ function MetricView({ records, config, value: presetValue }) {
350
361
  function InsightView({ config }) {
351
362
  return /* @__PURE__ */ jsx("div", { className: "h-full p-4 overflow-auto", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-midnight-text-body leading-relaxed whitespace-pre-wrap", children: config.text }) });
352
363
  }
364
+ function formatVal(value, s, numeric) {
365
+ let str = numeric ? Number(value).toLocaleString() : String(value);
366
+ if (s.tickTruncate > 0 && str.length > s.tickTruncate) str = `${str.slice(0, s.tickTruncate)}\u2026`;
367
+ return str;
368
+ }
369
+ function wrapText(str, width) {
370
+ if (width <= 0 || str.length <= width) return [str];
371
+ const words = str.split(/\s+/);
372
+ const lines = [];
373
+ let cur = "";
374
+ const push = (t) => {
375
+ if (t) lines.push(t);
376
+ };
377
+ for (const w of words) {
378
+ if (w.length > width) {
379
+ push(cur);
380
+ cur = "";
381
+ for (let i = 0; i < w.length; i += width) lines.push(w.slice(i, i + width));
382
+ continue;
383
+ }
384
+ if (!cur) cur = w;
385
+ else if (cur.length + 1 + w.length <= width) cur += ` ${w}`;
386
+ else {
387
+ push(cur);
388
+ cur = w;
389
+ }
390
+ }
391
+ push(cur);
392
+ if (lines.length > 4) {
393
+ const kept = lines.slice(0, 4);
394
+ kept[3] = `${kept[3].slice(0, Math.max(0, width - 1))}\u2026`;
395
+ return kept;
396
+ }
397
+ return lines;
398
+ }
399
+ function makeAxis(style, axis, columnName, opts2 = {}) {
400
+ const s = withStyleDefaults(style);
401
+ const isX = axis === "x";
402
+ const numeric = !!opts2.numeric;
403
+ const show = isX ? s.showXLegend : s.showYLegend;
404
+ const label = (isX ? s.xAxisLabel : s.yAxisLabel) || columnName;
405
+ const rotate = isX ? s.xTickRotation : s.yTickRotation;
406
+ const out = { tickSize: 5, tickPadding: 5 };
407
+ if (show && label) {
408
+ out.legend = label;
409
+ out.legendPosition = isX ? s.xLegendPosition : s.yLegendPosition;
410
+ out.legendOffset = isX ? s.xLegendOffset : s.yLegendOffset;
411
+ }
412
+ if (s.tickWrap) {
413
+ out.renderTick = (tick) => {
414
+ const lines = wrapText(formatVal(tick.value, s, numeric), s.tickWrapWidth);
415
+ const firstDy = isX ? "0" : `${-((lines.length - 1) * 0.55)}em`;
416
+ return /* @__PURE__ */ jsxs("g", { transform: `translate(${tick.x},${tick.y})`, children: [
417
+ /* @__PURE__ */ jsx("line", { x2: isX ? 0 : -5, y2: isX ? 5 : 0, style: { stroke: s.textColor, strokeWidth: 1, opacity: 0.3 } }),
418
+ /* @__PURE__ */ jsx(
419
+ "text",
420
+ {
421
+ transform: `translate(${tick.textX},${tick.textY}) rotate(${rotate})`,
422
+ textAnchor: tick.textAnchor,
423
+ dominantBaseline: tick.textBaseline,
424
+ style: { fill: s.textColor, fontSize: s.fontSize },
425
+ children: lines.map((ln, i) => /* @__PURE__ */ jsx("tspan", { x: 0, dy: i === 0 ? firstDy : "1.1em", children: ln }, i))
426
+ }
427
+ )
428
+ ] });
429
+ };
430
+ } else {
431
+ out.tickRotation = rotate;
432
+ out.format = (v) => formatVal(v, s, numeric);
433
+ }
434
+ return out;
435
+ }
353
436
  function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: presetData, style }) {
354
437
  const computed = useMemo(() => {
355
438
  if (presetData || !records) return [];
@@ -357,7 +440,7 @@ function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: pre
357
440
  return Object.entries(groups).map(([key, recs]) => ({ group: key, value: aggregate(recs, valueColumn, aggFn) })).sort((a, b) => b.value - a.value).slice(0, 50);
358
441
  }, [records, groupColumn, valueColumn, aggFn, presetData]);
359
442
  const data = presetData || computed;
360
- return /* @__PURE__ */ jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsx(
443
+ return /* @__PURE__ */ jsx("div", { className: "h-full w-full min-h-[160px]", children: /* @__PURE__ */ jsx(
361
444
  ResponsiveBar,
362
445
  {
363
446
  data,
@@ -367,8 +450,8 @@ function BarView({ records, groupColumn, valueColumn, aggFn = "count", data: pre
367
450
  padding: 0.3,
368
451
  colors: ["rgba(74, 222, 128, 0.8)"],
369
452
  borderColor: { from: "color", modifiers: [["darker", 1.6]] },
370
- axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", groupColumn) },
371
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", valueColumn, { numeric: true }) },
453
+ axisBottom: makeAxis(style, "x", groupColumn),
454
+ axisLeft: makeAxis(style, "y", valueColumn, { numeric: true }),
372
455
  labelSkipWidth: 12,
373
456
  labelSkipHeight: 12,
374
457
  labelTextColor: { from: "color", modifiers: [["darker", 3]] },
@@ -389,7 +472,7 @@ function PieView({ records, groupColumn, data: presetData, style }) {
389
472
  }, [records, groupColumn, presetData]);
390
473
  const data = presetData || computed;
391
474
  const legend = legendConfig(style);
392
- return /* @__PURE__ */ jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsx(
475
+ return /* @__PURE__ */ jsx("div", { className: "h-full w-full min-h-[160px]", children: /* @__PURE__ */ jsx(
393
476
  ResponsivePie,
394
477
  {
395
478
  data,
@@ -425,7 +508,7 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
425
508
  }];
426
509
  }, [records, xColumn, yColumn, presetData]);
427
510
  const data = presetData || computed;
428
- return /* @__PURE__ */ jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsx(
511
+ return /* @__PURE__ */ jsx("div", { className: "h-full w-full min-h-[160px]", children: /* @__PURE__ */ jsx(
429
512
  ResponsiveLine,
430
513
  {
431
514
  data,
@@ -441,8 +524,8 @@ function LineView({ records, xColumn, yColumn, data: presetData, style }) {
441
524
  pointBorderWidth: 2,
442
525
  pointBorderColor: { from: "serieColor" },
443
526
  enableGridX: false,
444
- axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn) },
445
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn, { numeric: true }) },
527
+ axisBottom: makeAxis(style, "x", xColumn),
528
+ axisLeft: makeAxis(style, "y", yColumn, { numeric: true }),
446
529
  useMesh: true,
447
530
  theme: buildNivoTheme(style)
448
531
  }
@@ -455,7 +538,7 @@ function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
455
538
  return [{ id: `${xColumn} vs ${yColumn}`, data: points }];
456
539
  }, [records, xColumn, yColumn, presetData]);
457
540
  const data = presetData || computed;
458
- return /* @__PURE__ */ jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsx(
541
+ return /* @__PURE__ */ jsx("div", { className: "h-full w-full min-h-[160px]", children: /* @__PURE__ */ jsx(
459
542
  ResponsiveScatterPlot,
460
543
  {
461
544
  data,
@@ -464,8 +547,8 @@ function ScatterView({ records, xColumn, yColumn, data: presetData, style }) {
464
547
  yScale: { type: "linear", min: "auto", max: "auto" },
465
548
  colors: ["rgba(167, 139, 250, 0.7)"],
466
549
  nodeSize: 6,
467
- axisBottom: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "x", xColumn, { numeric: true }) },
468
- axisLeft: { tickSize: 5, tickPadding: 5, ...axisLegend(style, "y", yColumn, { numeric: true }) },
550
+ axisBottom: makeAxis(style, "x", xColumn, { numeric: true }),
551
+ axisLeft: makeAxis(style, "y", yColumn, { numeric: true }),
469
552
  useMesh: true,
470
553
  theme: buildNivoTheme(style)
471
554
  }
@@ -559,7 +642,7 @@ function HeatmapView({
559
642
  return /* @__PURE__ */ jsx("div", { className: "p-8 text-center text-midnight-text-muted", children: "Not enough distinct values for a heatmap" });
560
643
  }
561
644
  const margin = showTotals ? { top: 60, right: 70, bottom: 60, left: 100 } : { top: 60, right: 20, bottom: 20, left: 100 };
562
- return /* @__PURE__ */ jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsx(
645
+ return /* @__PURE__ */ jsx("div", { className: "h-full w-full min-h-[160px]", children: /* @__PURE__ */ jsx(
563
646
  ResponsiveHeatMap,
564
647
  {
565
648
  data,
@@ -609,120 +692,80 @@ function PivotView({ records }) {
609
692
  )
610
693
  ] });
611
694
  }
695
+ function Section({ title, children }) {
696
+ return /* @__PURE__ */ jsxs("div", { className: "border-t border-midnight-border font-mono", style: { paddingTop: 12, paddingBottom: 12 }, children: [
697
+ /* @__PURE__ */ jsx("div", { className: "uppercase tracking-wider text-midnight-text-subdued", style: { fontSize: 10, marginBottom: 8 }, children: title }),
698
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexDirection: "column", rowGap: 6 }, children })
699
+ ] });
700
+ }
612
701
  function Row({ label, children }) {
613
- const { theme } = useDashboard();
614
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 min-h-[28px]", children: [
615
- /* @__PURE__ */ jsx("span", { className: `text-xs ${theme.font} text-midnight-text-muted`, children: label }),
616
- /* @__PURE__ */ jsx("div", { className: "flex items-center", children })
702
+ return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gridTemplateColumns: "84px minmax(0, 1fr)", alignItems: "center", columnGap: 8, height: 32 }, children: [
703
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted truncate", title: label, children: label }),
704
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", justifyContent: "flex-end", width: "100%", minWidth: 0 }, children })
617
705
  ] });
618
706
  }
619
707
  function ColorControl({ value, onChange }) {
620
- const { theme } = useDashboard();
621
708
  return /* @__PURE__ */ jsx(
622
709
  "input",
623
710
  {
624
711
  type: "color",
625
712
  value: /^#/.test(value) ? value : "#94a3b8",
626
713
  onChange: (e) => onChange(e.target.value),
627
- className: `w-9 h-6 bg-transparent border ${theme.border} cursor-pointer`
628
- }
629
- );
630
- }
631
- function NumberControl({ value, onChange, min, max }) {
632
- const { theme } = useDashboard();
633
- return /* @__PURE__ */ jsx(
634
- "input",
635
- {
636
- type: "number",
637
- value,
638
- min,
639
- max,
640
- onChange: (e) => onChange(Number(e.target.value)),
641
- 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`
642
- }
643
- );
644
- }
645
- function TextControl({ value, onChange, placeholder }) {
646
- const { theme } = useDashboard();
647
- return /* @__PURE__ */ jsx(
648
- "input",
649
- {
650
- type: "text",
651
- value,
652
- placeholder,
653
- onChange: (e) => onChange(e.target.value),
654
- className: `w-28 px-2 py-1 text-xs ${theme.font} bg-midnight-surface border ${theme.border} text-midnight-text-body outline-none focus:border-midnight-accent`
655
- }
656
- );
657
- }
658
- function Toggle({ value, onChange }) {
659
- return /* @__PURE__ */ jsx(
660
- "input",
661
- {
662
- type: "checkbox",
663
- checked: value,
664
- onChange: (e) => onChange(e.target.checked),
665
- className: "w-4 h-4 accent-midnight-accent cursor-pointer"
714
+ className: "w-9 h-6 bg-transparent border border-midnight-border cursor-pointer p-0"
666
715
  }
667
716
  );
668
717
  }
669
718
  function OptionalColor({ value, onChange, fallback = "#a78bfa" }) {
670
719
  const on = !!value;
671
720
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
672
- /* @__PURE__ */ jsx(Toggle, { value: on, onChange: (v) => onChange(v ? fallback : "") }),
673
- on && /* @__PURE__ */ jsx(ColorControl, { value, onChange })
721
+ on && /* @__PURE__ */ jsx(ColorControl, { value, onChange }),
722
+ /* @__PURE__ */ jsx(TerminalToggle, { size: "small", checked: on, onChange: (v) => onChange(v ? fallback : "") })
674
723
  ] });
675
724
  }
676
- function Choice({ value, options, onChange }) {
677
- const { theme } = useDashboard();
678
- return /* @__PURE__ */ jsx("div", { className: "w-28", children: /* @__PURE__ */ jsx(
679
- "select",
680
- {
681
- value,
682
- onChange: (e) => onChange(e.target.value),
683
- 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`,
684
- children: options.map((o) => /* @__PURE__ */ jsx("option", { value: o, children: o }, o))
685
- }
686
- ) });
687
- }
688
- function Group({ children, last }) {
689
- const { theme } = useDashboard();
690
- return /* @__PURE__ */ jsx("div", { className: `space-y-2 pb-3 ${last ? "" : `mb-3 border-b ${theme.border}`}`, children });
691
- }
692
725
  function ChartStyleControls({ style, onChange }) {
693
726
  const s = withStyleDefaults(style);
694
727
  const set = (key, value) => onChange({ ...s, [key]: value });
695
728
  return /* @__PURE__ */ jsxs("div", { children: [
696
- /* @__PURE__ */ jsxs(Group, { children: [
729
+ /* @__PURE__ */ jsxs(Section, { title: "Canvas", children: [
697
730
  /* @__PURE__ */ jsx(Row, { label: "Background", children: /* @__PURE__ */ jsx(ColorControl, { value: s.background, onChange: (v) => set("background", v) }) }),
698
- /* @__PURE__ */ jsx(Row, { label: "Text color", children: /* @__PURE__ */ jsx(ColorControl, { value: s.textColor, onChange: (v) => set("textColor", v) }) }),
699
- /* @__PURE__ */ jsx(Row, { label: "Grid color", children: /* @__PURE__ */ jsx(ColorControl, { value: s.gridColor, onChange: (v) => set("gridColor", v) }) }),
700
- /* @__PURE__ */ jsx(Row, { label: "Font size", children: /* @__PURE__ */ jsx(NumberControl, { value: s.fontSize, min: 6, max: 24, onChange: (v) => set("fontSize", v) }) })
731
+ /* @__PURE__ */ jsx(Row, { label: "Text", children: /* @__PURE__ */ jsx(ColorControl, { value: s.textColor, onChange: (v) => set("textColor", v) }) }),
732
+ /* @__PURE__ */ jsx(Row, { label: "Grid", children: /* @__PURE__ */ jsx(ColorControl, { value: s.gridColor, onChange: (v) => set("gridColor", v) }) }),
733
+ /* @__PURE__ */ jsx(Row, { label: "Font size", children: /* @__PURE__ */ jsx(TerminalSlider, { value: s.fontSize, min: 6, max: 24, unit: "px", onChange: (v) => set("fontSize", v) }) })
701
734
  ] }),
702
- /* @__PURE__ */ jsxs(Group, { children: [
703
- /* @__PURE__ */ jsx(Row, { label: "X axis title", children: /* @__PURE__ */ jsx(TextControl, { value: s.xAxisLabel, placeholder: "(column)", onChange: (v) => set("xAxisLabel", v) }) }),
704
- /* @__PURE__ */ jsx(Row, { label: "Show X title", children: /* @__PURE__ */ jsx(Toggle, { value: s.showXLegend, onChange: (v) => set("showXLegend", v) }) }),
705
- /* @__PURE__ */ jsx(Row, { label: "Y axis title", children: /* @__PURE__ */ jsx(TextControl, { value: s.yAxisLabel, placeholder: "(column)", onChange: (v) => set("yAxisLabel", v) }) }),
706
- /* @__PURE__ */ jsx(Row, { label: "Show Y title", children: /* @__PURE__ */ jsx(Toggle, { value: s.showYLegend, onChange: (v) => set("showYLegend", v) }) }),
707
- /* @__PURE__ */ jsx(Row, { label: "Title color", children: /* @__PURE__ */ jsx(ColorControl, { value: s.legendColor, onChange: (v) => set("legendColor", v) }) }),
708
- /* @__PURE__ */ jsx(Row, { label: "Title bold", children: /* @__PURE__ */ jsx(Toggle, { value: s.legendBold, onChange: (v) => set("legendBold", v) }) }),
709
- /* @__PURE__ */ jsx(Row, { label: "Title highlight", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.legendHighlight, onChange: (v) => set("legendHighlight", v) }) }),
710
- /* @__PURE__ */ jsx(Row, { label: "X title pos", children: /* @__PURE__ */ jsx(Choice, { value: s.xLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("xLegendPosition", v) }) }),
711
- /* @__PURE__ */ jsx(Row, { label: "X title offset", children: /* @__PURE__ */ jsx(NumberControl, { value: s.xLegendOffset, onChange: (v) => set("xLegendOffset", v) }) }),
712
- /* @__PURE__ */ jsx(Row, { label: "Y title pos", children: /* @__PURE__ */ jsx(Choice, { value: s.yLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("yLegendPosition", v) }) }),
713
- /* @__PURE__ */ jsx(Row, { label: "Y title offset", children: /* @__PURE__ */ jsx(NumberControl, { value: s.yLegendOffset, onChange: (v) => set("yLegendOffset", v) }) })
735
+ /* @__PURE__ */ jsxs(Section, { title: "Axis titles", children: [
736
+ /* @__PURE__ */ jsx(Row, { label: "X title", children: /* @__PURE__ */ jsx(TerminalInput, { size: "small", value: s.xAxisLabel, placeholder: "(column)", onChange: (e) => set("xAxisLabel", e.target.value) }) }),
737
+ /* @__PURE__ */ jsx(Row, { label: "Show X", children: /* @__PURE__ */ jsx(TerminalToggle, { size: "small", checked: s.showXLegend, onChange: (v) => set("showXLegend", v) }) }),
738
+ /* @__PURE__ */ jsx(Row, { label: "Y title", children: /* @__PURE__ */ jsx(TerminalInput, { size: "small", value: s.yAxisLabel, placeholder: "(column)", onChange: (e) => set("yAxisLabel", e.target.value) }) }),
739
+ /* @__PURE__ */ jsx(Row, { label: "Show Y", children: /* @__PURE__ */ jsx(TerminalToggle, { size: "small", checked: s.showYLegend, onChange: (v) => set("showYLegend", v) }) }),
740
+ /* @__PURE__ */ jsx(Row, { label: "Color", children: /* @__PURE__ */ jsx(ColorControl, { value: s.legendColor, onChange: (v) => set("legendColor", v) }) }),
741
+ /* @__PURE__ */ jsx(Row, { label: "Bold", children: /* @__PURE__ */ jsx(TerminalToggle, { size: "small", checked: s.legendBold, onChange: (v) => set("legendBold", v) }) }),
742
+ /* @__PURE__ */ jsx(Row, { label: "Highlight", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.legendHighlight, onChange: (v) => set("legendHighlight", v) }) })
714
743
  ] }),
715
- /* @__PURE__ */ jsxs(Group, { children: [
716
- /* @__PURE__ */ jsx(Row, { label: "X tick angle", children: /* @__PURE__ */ jsx(NumberControl, { value: s.xTickRotation, min: -90, max: 90, onChange: (v) => set("xTickRotation", v) }) }),
717
- /* @__PURE__ */ jsx(Row, { label: "Truncate ticks", children: /* @__PURE__ */ jsx(NumberControl, { value: s.tickTruncate, min: 0, max: 40, onChange: (v) => set("tickTruncate", v) }) })
744
+ /* @__PURE__ */ jsxs(Section, { title: "Axis placement", children: [
745
+ /* @__PURE__ */ jsx(Row, { label: "X pos", children: /* @__PURE__ */ jsx(TerminalSelect, { size: "small", value: s.xLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("xLegendPosition", v) }) }),
746
+ /* @__PURE__ */ jsx(Row, { label: "X offset", children: /* @__PURE__ */ jsx(TerminalSlider, { value: s.xLegendOffset, min: -80, max: 80, onChange: (v) => set("xLegendOffset", v) }) }),
747
+ /* @__PURE__ */ jsx(Row, { label: "Y pos", children: /* @__PURE__ */ jsx(TerminalSelect, { size: "small", value: s.yLegendPosition, options: ["start", "middle", "end"], onChange: (v) => set("yLegendPosition", v) }) }),
748
+ /* @__PURE__ */ jsx(Row, { label: "Y offset", children: /* @__PURE__ */ jsx(TerminalSlider, { value: s.yLegendOffset, min: -80, max: 80, onChange: (v) => set("yLegendOffset", v) }) })
718
749
  ] }),
719
- /* @__PURE__ */ jsxs(Group, { last: true, children: [
720
- /* @__PURE__ */ jsx(Row, { label: "Series legend", children: /* @__PURE__ */ jsx(Choice, { value: s.legendAnchor, options: ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"], onChange: (v) => set("legendAnchor", v) }) }),
721
- /* @__PURE__ */ jsx(Row, { label: "Panel title align", children: /* @__PURE__ */ jsx(Choice, { value: s.titleAlign, options: ["left", "center", "right"], onChange: (v) => set("titleAlign", v) }) }),
722
- /* @__PURE__ */ jsx(Row, { label: "Panel title bold", children: /* @__PURE__ */ jsx(Toggle, { value: s.titleBold, onChange: (v) => set("titleBold", v) }) }),
723
- /* @__PURE__ */ jsx(Row, { label: "Panel title color", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.titleColor, onChange: (v) => set("titleColor", v), fallback: "#e2e8f0" }) }),
724
- /* @__PURE__ */ jsx(Row, { label: "Panel title bg", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.titleBackground, onChange: (v) => set("titleBackground", v), fallback: "#1e293b" }) })
725
- ] })
750
+ /* @__PURE__ */ jsxs(Section, { title: "Ticks", children: [
751
+ /* @__PURE__ */ jsx(Row, { label: "Wrap", children: /* @__PURE__ */ jsx(TerminalToggle, { size: "small", checked: s.tickWrap, onChange: (v) => set("tickWrap", v) }) }),
752
+ /* @__PURE__ */ jsx(Row, { label: "Wrap width", children: /* @__PURE__ */ jsx(TerminalSlider, { value: s.tickWrapWidth, min: 4, max: 40, onChange: (v) => set("tickWrapWidth", v) }) }),
753
+ /* @__PURE__ */ jsx(Row, { label: "X angle", children: /* @__PURE__ */ jsx(TerminalSlider, { value: s.xTickRotation, min: -90, max: 90, unit: "\xB0", onChange: (v) => set("xTickRotation", v) }) }),
754
+ /* @__PURE__ */ jsx(Row, { label: "Y angle", children: /* @__PURE__ */ jsx(TerminalSlider, { value: s.yTickRotation, min: -90, max: 90, unit: "\xB0", onChange: (v) => set("yTickRotation", v) }) }),
755
+ /* @__PURE__ */ jsx(Row, { label: "Truncate", children: /* @__PURE__ */ jsx(TerminalSlider, { value: s.tickTruncate, min: 0, max: 40, onChange: (v) => set("tickTruncate", v) }) })
756
+ ] }),
757
+ /* @__PURE__ */ jsxs(Section, { title: "Table cells", children: [
758
+ /* @__PURE__ */ jsx(Row, { label: "Wrap", children: /* @__PURE__ */ jsx(TerminalToggle, { size: "small", checked: s.cellWrap, onChange: (v) => set("cellWrap", v) }) }),
759
+ /* @__PURE__ */ jsx(Row, { label: "Decimals", children: /* @__PURE__ */ jsx(TerminalSlider, { value: s.cellPrecision, min: 0, max: 8, onChange: (v) => set("cellPrecision", v) }) }),
760
+ /* @__PURE__ */ jsx(Row, { label: "Truncate", children: /* @__PURE__ */ jsx(TerminalSlider, { value: s.cellTruncate, min: 0, max: 80, onChange: (v) => set("cellTruncate", v) }) })
761
+ ] }),
762
+ /* @__PURE__ */ jsxs(Section, { title: "Panel title", children: [
763
+ /* @__PURE__ */ jsx(Row, { label: "Align", children: /* @__PURE__ */ jsx(TerminalSelect, { size: "small", value: s.titleAlign, options: ["left", "center", "right"], onChange: (v) => set("titleAlign", v) }) }),
764
+ /* @__PURE__ */ jsx(Row, { label: "Bold", children: /* @__PURE__ */ jsx(TerminalToggle, { size: "small", checked: s.titleBold, onChange: (v) => set("titleBold", v) }) }),
765
+ /* @__PURE__ */ jsx(Row, { label: "Color", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.titleColor, onChange: (v) => set("titleColor", v), fallback: "#e2e8f0" }) }),
766
+ /* @__PURE__ */ jsx(Row, { label: "Background", children: /* @__PURE__ */ jsx(OptionalColor, { value: s.titleBackground, onChange: (v) => set("titleBackground", v), fallback: "#1e293b" }) })
767
+ ] }),
768
+ /* @__PURE__ */ jsx(Section, { title: "Series legend", children: /* @__PURE__ */ jsx(Row, { label: "Position", children: /* @__PURE__ */ jsx(TerminalSelect, { size: "small", value: s.legendAnchor, options: ["right", "top-left", "top-right", "bottom-left", "bottom-right", "none"], onChange: (v) => set("legendAnchor", v) }) }) })
726
769
  ] });
727
770
  }
728
771
  var AGGREGATES = [
@@ -1330,7 +1373,7 @@ var GroupedBarChart = ({ records, xFields = [], yFields = [], groupField, data:
1330
1373
  const keys = presetKeys || yFields.map((yf) => `${yf.agg}(${yf.name})`);
1331
1374
  const colors = ["rgba(74,222,128,0.8)", "rgba(96,165,250,0.8)", "rgba(251,146,60,0.8)", "rgba(167,139,250,0.8)", "rgba(248,113,113,0.8)"];
1332
1375
  const legend = legendConfig(style);
1333
- return /* @__PURE__ */ jsx("div", { className: "h-full min-h-[400px]", children: /* @__PURE__ */ jsx(
1376
+ return /* @__PURE__ */ jsx("div", { className: "h-full w-full min-h-[160px]", children: /* @__PURE__ */ jsx(
1334
1377
  ResponsiveBar,
1335
1378
  {
1336
1379
  data,
@@ -1340,8 +1383,8 @@ var GroupedBarChart = ({ records, xFields = [], yFields = [], groupField, data:
1340
1383
  margin: { top: 20, right: 120, bottom: 60, left: 60 },
1341
1384
  padding: 0.3,
1342
1385
  colors: colors.slice(0, keys.length),
1343
- axisBottom: { tickSize: 5, tickPadding: 5, tickRotation: -35 },
1344
- axisLeft: { tickSize: 5, tickPadding: 5, format: (v) => Number(v).toLocaleString() },
1386
+ axisBottom: makeAxis(style, "x", xFields[0]?.name || ""),
1387
+ axisLeft: makeAxis(style, "y", "", { numeric: true }),
1345
1388
  labelSkipWidth: 12,
1346
1389
  labelSkipHeight: 12,
1347
1390
  labelTextColor: { from: "color", modifiers: [["darker", 3]] },
@@ -1447,6 +1490,7 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1447
1490
  const [style, setStyle] = useState(DEFAULT_CHART_STYLE);
1448
1491
  const [showStyle, setShowStyle] = useState(false);
1449
1492
  const [showFields, setShowFields] = useState(false);
1493
+ const [fillContainer, setFillContainer] = useState(false);
1450
1494
  const [sqlRows, setSqlRows] = useState(null);
1451
1495
  const [sqlLoading, setSqlLoading] = useState(false);
1452
1496
  const [sqlError, setSqlError] = useState(null);
@@ -1561,12 +1605,13 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1561
1605
  }
1562
1606
  }
1563
1607
  config.style = style;
1608
+ config.fill = fillContainer;
1564
1609
  onSave({
1565
1610
  title: title || `${chartType} chart`,
1566
1611
  type: chartType === "grouped-bar" ? "bar" : chartType,
1567
1612
  config,
1568
- width: 6,
1569
- height: 2
1613
+ width: fillContainer ? 12 : 6,
1614
+ height: fillContainer ? 8 : 2
1570
1615
  });
1571
1616
  };
1572
1617
  return /* @__PURE__ */ jsxs("div", { className: "flex h-full", children: [
@@ -1722,6 +1767,10 @@ function ChartBuilder({ records, columns, stateId, onSave }) {
1722
1767
  className: "w-full mb-2 px-2 py-1 text-xs font-mono bg-midnight-surface border border-midnight-border text-midnight-text-body outline-none focus:border-midnight-accent transition-colors"
1723
1768
  }
1724
1769
  ),
1770
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
1771
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-mono text-midnight-text-muted", title: "Span the full dashboard width and height", children: "Maximize size" }),
1772
+ /* @__PURE__ */ jsx(TerminalToggle, { size: "small", checked: fillContainer, onChange: setFillContainer })
1773
+ ] }),
1725
1774
  /* @__PURE__ */ jsx(
1726
1775
  "button",
1727
1776
  {
@@ -1818,14 +1867,26 @@ function panelSql(type, config) {
1818
1867
  if (SQL_CHART_TYPES.has(type)) return buildChartSQL(panelToChartConfig(type, config)) || "";
1819
1868
  return "";
1820
1869
  }
1821
- function DataTable({ rows, columns }) {
1870
+ function formatCell(value, s) {
1871
+ if (value == null) return "";
1872
+ if (typeof value === "object") return JSON.stringify(value);
1873
+ let str;
1874
+ const n = typeof value === "number" ? value : Number(value);
1875
+ if (value !== "" && typeof value !== "boolean" && Number.isFinite(n)) {
1876
+ str = Number.isInteger(n) ? n.toLocaleString() : n.toLocaleString(void 0, { minimumFractionDigits: s.cellPrecision, maximumFractionDigits: s.cellPrecision });
1877
+ } else {
1878
+ str = String(value);
1879
+ }
1880
+ if (s.cellTruncate > 0 && str.length > s.cellTruncate) str = `${str.slice(0, s.cellTruncate)}\u2026`;
1881
+ return str;
1882
+ }
1883
+ function DataTable({ rows, columns, style }) {
1884
+ const s = withStyleDefaults(style);
1822
1885
  const colNames = columns?.length ? columns : rows[0] ? Object.keys(rows[0]) : [];
1886
+ const cellCls = s.cellWrap ? "px-2 py-1 text-midnight-text-body align-top whitespace-normal break-words max-w-[280px]" : "px-2 py-1 text-midnight-text-body truncate max-w-[200px]";
1823
1887
  return /* @__PURE__ */ jsx("div", { className: "overflow-auto h-full text-xs", children: /* @__PURE__ */ jsxs("table", { className: "w-full", children: [
1824
1888
  /* @__PURE__ */ jsx("thead", { className: "sticky top-0 bg-midnight-elevated", children: /* @__PURE__ */ jsx("tr", { children: colNames.map((n) => /* @__PURE__ */ jsx("th", { className: "px-2 py-1 text-left text-midnight-text-muted font-mono border-b border-midnight-border", children: n }, n)) }) }),
1825
- /* @__PURE__ */ jsx("tbody", { children: rows.map((r, i) => /* @__PURE__ */ jsx("tr", { className: "border-b border-dashed border-midnight-border hover:bg-midnight-raised", children: colNames.map((n) => {
1826
- const cellValue = r[n];
1827
- return /* @__PURE__ */ jsx("td", { className: "px-2 py-1 text-midnight-text-body truncate max-w-[200px]", children: cellValue == null ? "" : typeof cellValue === "object" ? JSON.stringify(cellValue) : String(cellValue) }, n);
1828
- }) }, i)) })
1889
+ /* @__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) => /* @__PURE__ */ jsx("td", { className: cellCls, children: formatCell(r[n], s) }, n)) }, i)) })
1829
1890
  ] }) });
1830
1891
  }
1831
1892
  function SqlPanel({ panel }) {
@@ -1920,7 +1981,7 @@ function SqlPanel({ panel }) {
1920
1981
  const v = first ? Object.values(first)[0] : 0;
1921
1982
  chart = /* @__PURE__ */ jsx(MetricView, { value: Number(v) || 0, config: { column: config.column || "", agg: config.agg, label: config.label } });
1922
1983
  } else if (chartType === "table") {
1923
- chart = /* @__PURE__ */ jsx(DataTable, { rows, columns: config.columns });
1984
+ chart = /* @__PURE__ */ jsx(DataTable, { rows, columns: config.columns, style: config.style });
1924
1985
  } else {
1925
1986
  const shaped = shapeChartData(chartType, rows, { yFields: config.yFields || [] });
1926
1987
  switch (chartType) {
@@ -1984,7 +2045,7 @@ function PanelContent({ panel, records, columns }) {
1984
2045
  case "insight":
1985
2046
  return /* @__PURE__ */ jsx(InsightView, { config: { text: config.text } });
1986
2047
  case "table":
1987
- return /* @__PURE__ */ jsx(DataTable, { rows: (records || []).slice(0, 50), columns: config.columns || columns?.map((c) => c.name) });
2048
+ return /* @__PURE__ */ jsx(DataTable, { rows: (records || []).slice(0, 50), columns: config.columns || columns?.map((c) => c.name), style: config.style });
1988
2049
  default:
1989
2050
  return /* @__PURE__ */ jsxs("div", { className: "p-4 text-xs text-midnight-text-muted", children: [
1990
2051
  "Unknown panel type: ",
@@ -1992,57 +2053,121 @@ function PanelContent({ panel, records, columns }) {
1992
2053
  ] });
1993
2054
  }
1994
2055
  }
1995
- var ROW_HEIGHT = 180;
2056
+ var GRID_COLS = 12;
2057
+ var ROW_HEIGHT = 80;
2058
+ var GRID_MARGIN = 10;
2059
+ var DRAG_HANDLE = "panel-drag-handle";
2060
+ var RGL_CSS = `
2061
+ .react-grid-layout { position: relative; transition: height 200ms ease; }
2062
+ .react-grid-item { transition: all 200ms ease; transition-property: left, top, width, height; box-sizing: border-box; }
2063
+ .react-grid-item.cssTransforms { transition-property: transform, width, height; }
2064
+ .react-grid-item.resizing { transition: none; z-index: 3; will-change: width, height; }
2065
+ .react-grid-item.react-draggable-dragging { transition: none; z-index: 3; will-change: transform; }
2066
+ .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; }
2067
+ .react-grid-item > .react-resizable-handle { position: absolute; width: 18px; height: 18px; bottom: 0; right: 0; cursor: se-resize; }
2068
+ .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); }
2069
+ .${DRAG_HANDLE} { cursor: grab; }
2070
+ .react-grid-item.react-draggable-dragging .${DRAG_HANDLE} { cursor: grabbing; }
2071
+ `;
2072
+ var rglCssInjected = false;
2073
+ function useInjectRglCss() {
2074
+ useEffect(() => {
2075
+ if (rglCssInjected || typeof document === "undefined") return;
2076
+ const el = document.createElement("style");
2077
+ el.setAttribute("data-rgl", "dashboard-renderer");
2078
+ el.textContent = RGL_CSS;
2079
+ document.head.appendChild(el);
2080
+ rglCssInjected = true;
2081
+ }, []);
2082
+ }
2083
+ var GridLayoutWithWidth = WidthProvider(GridLayout);
2084
+ function buildLayout(panels) {
2085
+ let cx = 0, cy = 0, rowH = 0;
2086
+ return panels.map((p) => {
2087
+ const fill = !!p.config?.fill;
2088
+ const w = fill ? GRID_COLS : Math.min(Math.max(p.width || 6, 1), GRID_COLS);
2089
+ const h = fill ? Math.max(p.height || 8, 6) : Math.max(p.height || 4, 1);
2090
+ if (typeof p.x === "number" && typeof p.y === "number") {
2091
+ return { i: p.id, x: fill ? 0 : p.x, y: p.y, w, h, minW: 2, minH: 2 };
2092
+ }
2093
+ if (cx + w > GRID_COLS) {
2094
+ cx = 0;
2095
+ cy += rowH;
2096
+ rowH = 0;
2097
+ }
2098
+ const item = { i: p.id, x: cx, y: cy, w, h, minW: 2, minH: 2 };
2099
+ cx += w;
2100
+ rowH = Math.max(rowH, h);
2101
+ return item;
2102
+ });
2103
+ }
1996
2104
  function DashboardRenderer({ dashboard, records, columns }) {
1997
- const { theme, removePanel } = useDashboard();
1998
- const { canEditPanels } = useCapabilities();
2105
+ const { theme, removePanel, persistLayout } = useDashboard();
2106
+ const { canEditPanels, canEditLayout } = useCapabilities();
2107
+ useInjectRglCss();
2108
+ const panels = dashboard?.panels ?? [];
2109
+ const layout = useMemo(() => buildLayout(panels), [panels]);
2110
+ const onLayoutChange = useCallback((next) => {
2111
+ if (!persistLayout) return;
2112
+ persistLayout(next.map((l) => ({ id: l.i, x: l.x, y: l.y, w: l.w, h: l.h })));
2113
+ }, [persistLayout]);
1999
2114
  if (!dashboard) return null;
2000
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 p-2", children: [
2001
- dashboard.insights && /* @__PURE__ */ jsxs("div", { className: `border ${theme.border} bg-midnight-elevated px-4 py-3 flex items-start gap-3`, children: [
2002
- /* @__PURE__ */ jsx(Sparkles, { className: "w-5 h-5 text-midnight-accent shrink-0 mt-0.5" }),
2003
- /* @__PURE__ */ jsx("p", { className: `text-sm ${theme.text} leading-relaxed`, children: dashboard.insights })
2004
- ] }),
2005
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-12 gap-3", children: dashboard.panels?.map((panel) => {
2006
- const colSpan = Math.min(Math.max(panel.width || 6, 1), 12);
2007
- const rowSpan = Math.min(Math.max(panel.height || 2, 1), 4);
2008
- return /* @__PURE__ */ jsxs(
2009
- "div",
2115
+ const panelFrame = (panel, draggable) => /* @__PURE__ */ jsxs("div", { className: `border ${theme.border} bg-midnight-surface flex flex-col overflow-hidden h-full`, children: [
2116
+ /* @__PURE__ */ jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 border-b ${theme.border} bg-midnight-elevated ${draggable ? DRAG_HANDLE : ""}`, children: [
2117
+ /* @__PURE__ */ jsx(
2118
+ "span",
2010
2119
  {
2011
- className: `border ${theme.border} bg-midnight-surface flex flex-col`,
2120
+ className: "flex-1 text-xs font-mono text-midnight-text-body truncate px-1",
2012
2121
  style: {
2013
- gridColumn: `span ${colSpan}`,
2014
- minHeight: `${rowSpan * ROW_HEIGHT}px`
2122
+ textAlign: panel.config?.style?.titleAlign || "left",
2123
+ fontWeight: panel.config?.style?.titleBold ? 700 : void 0,
2124
+ background: panel.config?.style?.titleBackground || void 0,
2125
+ color: panel.config?.style?.titleColor || void 0
2015
2126
  },
2016
- children: [
2017
- /* @__PURE__ */ jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 border-b ${theme.border} bg-midnight-elevated`, children: [
2018
- /* @__PURE__ */ jsx(
2019
- "span",
2020
- {
2021
- className: "flex-1 text-xs font-mono text-midnight-text-body truncate px-1",
2022
- style: {
2023
- textAlign: panel.config?.style?.titleAlign || "left",
2024
- fontWeight: panel.config?.style?.titleBold ? 700 : void 0,
2025
- background: panel.config?.style?.titleBackground || void 0,
2026
- color: panel.config?.style?.titleColor || void 0
2027
- },
2028
- children: panel.title || panel.type
2029
- }
2030
- ),
2031
- canEditPanels && removePanel && /* @__PURE__ */ jsx(
2032
- "button",
2033
- {
2034
- onClick: () => removePanel(panel.id),
2035
- className: "p-0.5 hover:bg-midnight-raised text-midnight-text-muted hover:text-midnight-text-body transition-colors",
2036
- children: /* @__PURE__ */ jsx(X, { className: "w-3 h-3" })
2037
- }
2038
- )
2039
- ] }),
2040
- /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewLoading, {}), children: /* @__PURE__ */ jsx(PanelContent, { panel, records, columns }) }) })
2041
- ]
2042
- },
2043
- panel.id
2044
- );
2045
- }) })
2127
+ children: panel.title || panel.type
2128
+ }
2129
+ ),
2130
+ canEditPanels && removePanel && /* @__PURE__ */ jsx(
2131
+ "button",
2132
+ {
2133
+ onMouseDown: (e) => e.stopPropagation(),
2134
+ onClick: () => removePanel(panel.id),
2135
+ className: "p-0.5 hover:bg-midnight-raised text-midnight-text-muted hover:text-midnight-text-body transition-colors",
2136
+ children: /* @__PURE__ */ jsx(X, { className: "w-3 h-3" })
2137
+ }
2138
+ )
2139
+ ] }),
2140
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ViewLoading, {}), children: /* @__PURE__ */ jsx(PanelContent, { panel, records, columns }) }) })
2141
+ ] }, panel.id);
2142
+ const insightsBanner = dashboard.insights ? /* @__PURE__ */ jsxs("div", { className: `border ${theme.border} bg-midnight-elevated px-4 py-3 flex items-start gap-3`, children: [
2143
+ /* @__PURE__ */ jsx(Sparkles, { className: "w-5 h-5 text-midnight-accent shrink-0 mt-0.5" }),
2144
+ /* @__PURE__ */ jsx("p", { className: `text-sm ${theme.text} leading-relaxed`, children: dashboard.insights })
2145
+ ] }) : null;
2146
+ if (panels.length === 1) {
2147
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 p-2 h-full min-h-[60vh]", children: [
2148
+ insightsBanner,
2149
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: panelFrame(panels[0], false) })
2150
+ ] });
2151
+ }
2152
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 p-2", children: [
2153
+ insightsBanner,
2154
+ /* @__PURE__ */ jsx(
2155
+ GridLayoutWithWidth,
2156
+ {
2157
+ className: "layout",
2158
+ layout,
2159
+ cols: GRID_COLS,
2160
+ rowHeight: ROW_HEIGHT,
2161
+ margin: [GRID_MARGIN, GRID_MARGIN],
2162
+ isDraggable: canEditLayout,
2163
+ isResizable: canEditLayout,
2164
+ draggableHandle: `.${DRAG_HANDLE}`,
2165
+ onDragStop: onLayoutChange,
2166
+ onResizeStop: onLayoutChange,
2167
+ compactType: canEditLayout ? "vertical" : null,
2168
+ children: panels.map((panel) => panelFrame(panel, canEditLayout))
2169
+ }
2170
+ )
2046
2171
  ] });
2047
2172
  }
2048
2173
  function ProfileSummary({ profile, theme }) {