@quantumwake/terminal-ux-dashboard-components 0.1.13 → 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.d.cts CHANGED
@@ -31,6 +31,14 @@ interface DashboardCapabilities {
31
31
  addPanel?: (panel: PanelInput) => void;
32
32
  removePanel?: (panelId: string) => void;
33
33
  persistPanelData?: (panelId: string, rows: QueryResult['rows'], refreshedAt: string) => void;
34
+ persistLayout?: (items: PanelLayout[]) => void;
35
+ }
36
+ interface PanelLayout {
37
+ id: string;
38
+ x: number;
39
+ y: number;
40
+ w: number;
41
+ h: number;
34
42
  }
35
43
  interface DashboardContextValue extends DashboardCapabilities {
36
44
  theme: DashboardTheme;
@@ -54,6 +62,7 @@ declare function useCapabilities(): {
54
62
  canAddPanel: boolean;
55
63
  canEditPanels: boolean;
56
64
  canRefresh: boolean;
65
+ canEditLayout: boolean;
57
66
  };
58
67
 
59
68
  type ChartType = 'bar' | 'grouped-bar' | 'pie' | 'line' | 'scatter' | 'heatmap';
@@ -105,6 +114,7 @@ interface ChartStyle {
105
114
  legendBold: boolean;
106
115
  legendHighlight: string;
107
116
  xTickRotation: number;
117
+ yTickRotation: number;
108
118
  tickTruncate: number;
109
119
  legendAnchor: LegendAnchor;
110
120
  titleAlign: TitleAlign;
@@ -325,12 +335,6 @@ interface ChartStyleControlsProps {
325
335
  style?: Partial<ChartStyle> | null;
326
336
  onChange: (next: ChartStyle) => void;
327
337
  }
328
- /**
329
- * ChartStyleControls — the standardized appearance editor used across all
330
- * charts. Edits a plain style object (see chartStyle.ts); theme-aware via the
331
- * host-injected DashboardContext theme. Parent persists the style and passes it
332
- * to the views.
333
- */
334
338
  declare function ChartStyleControls({ style, onChange }: ChartStyleControlsProps): react.JSX.Element;
335
339
 
336
340
  interface SqlConsoleColumn {
@@ -382,6 +386,8 @@ interface DashboardPanel$1 {
382
386
  title?: string;
383
387
  width?: number;
384
388
  height?: number;
389
+ x?: number;
390
+ y?: number;
385
391
  config?: PanelConfig;
386
392
  }
387
393
  interface Dashboard$1 {
@@ -398,8 +404,12 @@ interface DashboardRendererProps {
398
404
  }
399
405
  /**
400
406
  * DashboardRenderer — renders a grid of AI-generated or manually configured
401
- * panels. The per-panel remove button is gated behind canEditPanels: the
402
- * published viewer (no removePanel) renders the dashboard READ-ONLY.
407
+ * panels via react-grid-layout. Editing is capability-gated:
408
+ * - studio (persistLayout wired) ⇒ panels drag (by their header) + resize, and
409
+ * the new geometry is persisted back to the host.
410
+ * - published viewer (no persistLayout) ⇒ a static grid, rendered exactly where
411
+ * the studio left the panels.
412
+ * The per-panel remove button is similarly gated behind canEditPanels.
403
413
  */
404
414
  declare function DashboardRenderer({ dashboard, records, columns }: DashboardRendererProps): react.JSX.Element | null;
405
415
 
@@ -504,4 +514,4 @@ interface DataExplorerProps {
504
514
  }
505
515
  declare function DataExplorer({ records, columns, states, activeStateId, profile, dashboard, dashboardId, savedDashboards, loading, analyzing, defaultMode, tabs, onSelectState, onRefreshStates, }: DataExplorerProps): react.JSX.Element;
506
516
 
507
- export { type AggFn, type BarDatum, BarView, type BarViewProps, ChartBuilder, type ChartBuilderColumn, type ChartBuilderProps, type ChartConfig, type ChartField, type ChartFilter, type ChartPanel, type ChartStyle, ChartStyleControls, type ChartStyleControlsProps, type ChartType, DEFAULT_CHART_STYLE, type Dashboard$1 as Dashboard, type DashboardCapabilities, type DashboardContextValue, type DashboardPanel$1 as DashboardPanel, DashboardProvider, type DashboardProviderProps, DashboardRenderer, type DashboardRendererProps, type DashboardTheme, DataExplorer, type DataExplorerProps, type HeatmapSerie, HeatmapView, type HeatmapViewProps, type InsightConfig, InsightView, type InsightViewProps, LEGEND_ANCHORS, type LegendAnchor, type LegendPosition, type LineSerie, LineView, type LineViewProps, type MetricConfig, MetricView, type MetricViewProps, type PanelInput, type PieDatum, PieView, type PieViewProps, PivotView, type PivotViewProps, type QueryResult, type Row, type SavedDashboard, type ScatterSerie, ScatterView, type ScatterViewProps, SqlConsole, type SqlConsoleColumn, type SqlConsoleProps, type TitleAlign, aggExpr, aggregate, axisLegend, buildChartSQL, buildNivoTheme, compileWhere, groupBy, legendConfig, qIdent, qLit, shapeChartData, useCapabilities, useDashboard, withStyleDefaults };
517
+ export { type AggFn, type BarDatum, BarView, type BarViewProps, ChartBuilder, type ChartBuilderColumn, type ChartBuilderProps, type ChartConfig, type ChartField, type ChartFilter, type ChartPanel, type ChartStyle, ChartStyleControls, type ChartStyleControlsProps, type ChartType, DEFAULT_CHART_STYLE, type Dashboard$1 as Dashboard, type DashboardCapabilities, type DashboardContextValue, type DashboardPanel$1 as DashboardPanel, DashboardProvider, type DashboardProviderProps, DashboardRenderer, type DashboardRendererProps, type DashboardTheme, DataExplorer, type DataExplorerProps, type HeatmapSerie, HeatmapView, type HeatmapViewProps, type InsightConfig, InsightView, type InsightViewProps, LEGEND_ANCHORS, type LegendAnchor, type LegendPosition, type LineSerie, LineView, type LineViewProps, type MetricConfig, MetricView, type MetricViewProps, type PanelInput, type PanelLayout, type PieDatum, PieView, type PieViewProps, PivotView, type PivotViewProps, type QueryResult, type Row, type SavedDashboard, type ScatterSerie, ScatterView, type ScatterViewProps, SqlConsole, type SqlConsoleColumn, type SqlConsoleProps, type TitleAlign, aggExpr, aggregate, axisLegend, buildChartSQL, buildNivoTheme, compileWhere, groupBy, legendConfig, qIdent, qLit, shapeChartData, useCapabilities, useDashboard, withStyleDefaults };
package/dist/index.d.ts CHANGED
@@ -31,6 +31,14 @@ interface DashboardCapabilities {
31
31
  addPanel?: (panel: PanelInput) => void;
32
32
  removePanel?: (panelId: string) => void;
33
33
  persistPanelData?: (panelId: string, rows: QueryResult['rows'], refreshedAt: string) => void;
34
+ persistLayout?: (items: PanelLayout[]) => void;
35
+ }
36
+ interface PanelLayout {
37
+ id: string;
38
+ x: number;
39
+ y: number;
40
+ w: number;
41
+ h: number;
34
42
  }
35
43
  interface DashboardContextValue extends DashboardCapabilities {
36
44
  theme: DashboardTheme;
@@ -54,6 +62,7 @@ declare function useCapabilities(): {
54
62
  canAddPanel: boolean;
55
63
  canEditPanels: boolean;
56
64
  canRefresh: boolean;
65
+ canEditLayout: boolean;
57
66
  };
58
67
 
59
68
  type ChartType = 'bar' | 'grouped-bar' | 'pie' | 'line' | 'scatter' | 'heatmap';
@@ -105,6 +114,7 @@ interface ChartStyle {
105
114
  legendBold: boolean;
106
115
  legendHighlight: string;
107
116
  xTickRotation: number;
117
+ yTickRotation: number;
108
118
  tickTruncate: number;
109
119
  legendAnchor: LegendAnchor;
110
120
  titleAlign: TitleAlign;
@@ -325,12 +335,6 @@ interface ChartStyleControlsProps {
325
335
  style?: Partial<ChartStyle> | null;
326
336
  onChange: (next: ChartStyle) => void;
327
337
  }
328
- /**
329
- * ChartStyleControls — the standardized appearance editor used across all
330
- * charts. Edits a plain style object (see chartStyle.ts); theme-aware via the
331
- * host-injected DashboardContext theme. Parent persists the style and passes it
332
- * to the views.
333
- */
334
338
  declare function ChartStyleControls({ style, onChange }: ChartStyleControlsProps): react.JSX.Element;
335
339
 
336
340
  interface SqlConsoleColumn {
@@ -382,6 +386,8 @@ interface DashboardPanel$1 {
382
386
  title?: string;
383
387
  width?: number;
384
388
  height?: number;
389
+ x?: number;
390
+ y?: number;
385
391
  config?: PanelConfig;
386
392
  }
387
393
  interface Dashboard$1 {
@@ -398,8 +404,12 @@ interface DashboardRendererProps {
398
404
  }
399
405
  /**
400
406
  * DashboardRenderer — renders a grid of AI-generated or manually configured
401
- * panels. The per-panel remove button is gated behind canEditPanels: the
402
- * published viewer (no removePanel) renders the dashboard READ-ONLY.
407
+ * panels via react-grid-layout. Editing is capability-gated:
408
+ * - studio (persistLayout wired) ⇒ panels drag (by their header) + resize, and
409
+ * the new geometry is persisted back to the host.
410
+ * - published viewer (no persistLayout) ⇒ a static grid, rendered exactly where
411
+ * the studio left the panels.
412
+ * The per-panel remove button is similarly gated behind canEditPanels.
403
413
  */
404
414
  declare function DashboardRenderer({ dashboard, records, columns }: DashboardRendererProps): react.JSX.Element | null;
405
415
 
@@ -504,4 +514,4 @@ interface DataExplorerProps {
504
514
  }
505
515
  declare function DataExplorer({ records, columns, states, activeStateId, profile, dashboard, dashboardId, savedDashboards, loading, analyzing, defaultMode, tabs, onSelectState, onRefreshStates, }: DataExplorerProps): react.JSX.Element;
506
516
 
507
- export { type AggFn, type BarDatum, BarView, type BarViewProps, ChartBuilder, type ChartBuilderColumn, type ChartBuilderProps, type ChartConfig, type ChartField, type ChartFilter, type ChartPanel, type ChartStyle, ChartStyleControls, type ChartStyleControlsProps, type ChartType, DEFAULT_CHART_STYLE, type Dashboard$1 as Dashboard, type DashboardCapabilities, type DashboardContextValue, type DashboardPanel$1 as DashboardPanel, DashboardProvider, type DashboardProviderProps, DashboardRenderer, type DashboardRendererProps, type DashboardTheme, DataExplorer, type DataExplorerProps, type HeatmapSerie, HeatmapView, type HeatmapViewProps, type InsightConfig, InsightView, type InsightViewProps, LEGEND_ANCHORS, type LegendAnchor, type LegendPosition, type LineSerie, LineView, type LineViewProps, type MetricConfig, MetricView, type MetricViewProps, type PanelInput, type PieDatum, PieView, type PieViewProps, PivotView, type PivotViewProps, type QueryResult, type Row, type SavedDashboard, type ScatterSerie, ScatterView, type ScatterViewProps, SqlConsole, type SqlConsoleColumn, type SqlConsoleProps, type TitleAlign, aggExpr, aggregate, axisLegend, buildChartSQL, buildNivoTheme, compileWhere, groupBy, legendConfig, qIdent, qLit, shapeChartData, useCapabilities, useDashboard, withStyleDefaults };
517
+ export { type AggFn, type BarDatum, BarView, type BarViewProps, ChartBuilder, type ChartBuilderColumn, type ChartBuilderProps, type ChartConfig, type ChartField, type ChartFilter, type ChartPanel, type ChartStyle, ChartStyleControls, type ChartStyleControlsProps, type ChartType, DEFAULT_CHART_STYLE, type Dashboard$1 as Dashboard, type DashboardCapabilities, type DashboardContextValue, type DashboardPanel$1 as DashboardPanel, DashboardProvider, type DashboardProviderProps, DashboardRenderer, type DashboardRendererProps, type DashboardTheme, DataExplorer, type DataExplorerProps, type HeatmapSerie, HeatmapView, type HeatmapViewProps, type InsightConfig, InsightView, type InsightViewProps, LEGEND_ANCHORS, type LegendAnchor, type LegendPosition, type LineSerie, LineView, type LineViewProps, type MetricConfig, MetricView, type MetricViewProps, type PanelInput, type PanelLayout, type PieDatum, PieView, type PieViewProps, PivotView, type PivotViewProps, type QueryResult, type Row, type SavedDashboard, type ScatterSerie, ScatterView, type ScatterViewProps, SqlConsole, type SqlConsoleColumn, type SqlConsoleProps, type TitleAlign, aggExpr, aggregate, axisLegend, buildChartSQL, buildNivoTheme, compileWhere, groupBy, legendConfig, qIdent, qLit, shapeChartData, useCapabilities, useDashboard, withStyleDefaults };
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
 
@@ -225,6 +228,7 @@ var DEFAULT_CHART_STYLE = {
225
228
  legendHighlight: "",
226
229
  // Tick overflow handling.
227
230
  xTickRotation: -35,
231
+ yTickRotation: 0,
228
232
  tickTruncate: 0,
229
233
  // Series legend placement (charts that have one: pie, grouped bar).
230
234
  legendAnchor: "right",
@@ -300,7 +304,7 @@ var axisLegend = (style, axis, columnName, opts2 = {}) => {
300
304
  out.legendPosition = isX ? s.xLegendPosition : s.yLegendPosition;
301
305
  out.legendOffset = isX ? s.xLegendOffset : s.yLegendOffset;
302
306
  }
303
- if (isX) out.tickRotation = s.xTickRotation;
307
+ out.tickRotation = isX ? s.xTickRotation : s.yTickRotation;
304
308
  if (opts2.numeric) out.format = (v) => Number(v).toLocaleString();
305
309
  else if (s.tickTruncate > 0) out.format = (v) => truncate(v, s.tickTruncate);
306
310
  return out;
@@ -609,11 +613,17 @@ function PivotView({ records }) {
609
613
  )
610
614
  ] });
611
615
  }
612
- function Row({ label, children }) {
616
+ function Section({ title, children }) {
613
617
  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 })
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 })
617
627
  ] });
618
628
  }
619
629
  function ColorControl({ value, onChange }) {
@@ -624,23 +634,29 @@ function ColorControl({ value, onChange }) {
624
634
  type: "color",
625
635
  value: /^#/.test(value) ? value : "#94a3b8",
626
636
  onChange: (e) => onChange(e.target.value),
627
- 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`
628
638
  }
629
639
  );
630
640
  }
631
- function NumberControl({ value, onChange, min, max }) {
632
- const { theme } = useDashboard();
633
- return /* @__PURE__ */ jsx(
634
- "input",
635
- {
636
- type: "number",
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: [
637
656
  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
- );
657
+ unit
658
+ ] })
659
+ ] });
644
660
  }
645
661
  function TextControl({ value, onChange, placeholder }) {
646
662
  const { theme } = useDashboard();
@@ -651,78 +667,79 @@ function TextControl({ value, onChange, placeholder }) {
651
667
  value,
652
668
  placeholder,
653
669
  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`
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`
655
671
  }
656
672
  );
657
673
  }
658
- function Toggle({ value, onChange }) {
674
+ function Switch({ value, onChange }) {
659
675
  return /* @__PURE__ */ jsx(
660
- "input",
676
+ "button",
661
677
  {
662
- type: "checkbox",
663
- checked: value,
664
- onChange: (e) => onChange(e.target.checked),
665
- className: "w-4 h-4 accent-midnight-accent cursor-pointer"
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"}` })
666
684
  }
667
685
  );
668
686
  }
669
687
  function OptionalColor({ value, onChange, fallback = "#a78bfa" }) {
670
688
  const on = !!value;
671
689
  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 })
690
+ on && /* @__PURE__ */ jsx(ColorControl, { value, onChange }),
691
+ /* @__PURE__ */ jsx(Switch, { value: on, onChange: (v) => onChange(v ? fallback : "") })
674
692
  ] });
675
693
  }
676
694
  function Choice({ value, options, onChange }) {
677
695
  const { theme } = useDashboard();
678
- return /* @__PURE__ */ jsx("div", { className: "w-28", children: /* @__PURE__ */ jsx(
696
+ return /* @__PURE__ */ jsx(
679
697
  "select",
680
698
  {
681
699
  value,
682
700
  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`,
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`,
684
702
  children: options.map((o) => /* @__PURE__ */ jsx("option", { value: o, children: o }, o))
685
703
  }
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 });
704
+ );
691
705
  }
692
706
  function ChartStyleControls({ style, onChange }) {
693
707
  const s = withStyleDefaults(style);
694
708
  const set = (key, value) => onChange({ ...s, [key]: value });
695
709
  return /* @__PURE__ */ jsxs("div", { children: [
696
- /* @__PURE__ */ jsxs(Group, { children: [
710
+ /* @__PURE__ */ jsxs(Section, { title: "Canvas", children: [
697
711
  /* @__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) }) })
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) }) })
701
715
  ] }),
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) }) })
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) }) })
714
724
  ] }),
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) }) })
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) }) })
718
730
  ] }),
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
- ] })
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) }) }) })
726
743
  ] });
727
744
  }
728
745
  var AGGREGATES = [
@@ -1992,57 +2009,112 @@ function PanelContent({ panel, records, columns }) {
1992
2009
  ] });
1993
2010
  }
1994
2011
  }
1995
- 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
+ }
1996
2059
  function DashboardRenderer({ dashboard, records, columns }) {
1997
- const { theme, removePanel } = useDashboard();
1998
- 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]);
1999
2069
  if (!dashboard) return null;
2000
2070
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 p-2", children: [
2001
2071
  dashboard.insights && /* @__PURE__ */ jsxs("div", { className: `border ${theme.border} bg-midnight-elevated px-4 py-3 flex items-start gap-3`, children: [
2002
2072
  /* @__PURE__ */ jsx(Sparkles, { className: "w-5 h-5 text-midnight-accent shrink-0 mt-0.5" }),
2003
2073
  /* @__PURE__ */ jsx("p", { className: `text-sm ${theme.text} leading-relaxed`, children: dashboard.insights })
2004
2074
  ] }),
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",
2010
- {
2011
- className: `border ${theme.border} bg-midnight-surface flex flex-col`,
2012
- style: {
2013
- gridColumn: `span ${colSpan}`,
2014
- minHeight: `${rowSpan * ROW_HEIGHT}px`
2015
- },
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
- }) })
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
+ )
2046
2118
  ] });
2047
2119
  }
2048
2120
  function ProfileSummary({ profile, theme }) {