@bluemarble/bi 0.1.0 → 0.2.0

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.
@@ -1,6 +1,6 @@
1
- import { C as validateModel, D as parseColumnRef, E as modelEdges, T as measureHome, _ as AGGREGATIONS, a as FILTER_FIELDS, c as chartReferences, d as layoutModelWarnings, f as validateLayout, g as toModelCode, i as CHART_TYPE_LABEL, k as referencedTables, l as filterReferences, n as CHART_FIELDS, o as chartColumns, p as canonicalJson, r as CHART_TYPES, s as chartOfType, t as CANVAS_PRESETS, u as layoutFileContent, v as AGGREGATION_LABEL, x as isAggregate, y as VALUE_FORMATS } from "./layout-iPByu6V0.js";
1
+ import { A as referencedTables, D as modelEdges, E as measureHome, O as parseColumnRef, S as isAggregate, _ as toModelCode, a as chartOfType, b as VALUE_FORMATS, c as filterReferences, d as layoutModelWarnings, i as chartColumns, m as canonicalJson, n as CANVAS_PRESETS, o as chartReferences, p as validateLayout, r as FILTER_FIELDS, t as BUILTIN_CHART_TYPES, u as layoutFileContent, v as AGGREGATIONS, w as validateModel, y as AGGREGATION_LABEL } from "./layout-DvHS8XFz.js";
2
2
  import { n as diffRows } from "./compare-DloZhg1T.js";
3
- import { a as useDashboard, c as layoutItemKey, l as parseLayoutItemKey, n as INSPECT_ATTRIBUTE, o as errorMessage, s as LAYOUT_ITEM_ATTRIBUTE, t as ModelDictionary } from "./ModelDictionary-AjNbffwG.js";
3
+ import { a as useDashboard, c as layoutItemKey, l as parseLayoutItemKey, n as INSPECT_ATTRIBUTE, o as errorMessage, s as LAYOUT_ITEM_ATTRIBUTE, t as ModelDictionary } from "./ModelDictionary-DN77UDVM.js";
4
4
  import { Alert, Autocomplete, Box, Button, Checkbox, Chip, CircularProgress, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControlLabel, IconButton, List, ListItemButton, Menu, MenuItem, MenuList, Paper, Popper, Portal, Tab, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tabs, TextField, ThemeProvider, ToggleButton, ToggleButtonGroup, Tooltip, Typography, createTheme } from "@mui/material";
5
5
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -660,6 +660,35 @@ const inputSx$1 = { "& .MuiInputBase-input": {
660
660
  } };
661
661
  const itemElement = (layoutId, item) => document.querySelector(`[${LAYOUT_ITEM_ATTRIBUTE}="${CSS.escape(layoutItemKey(layoutId, item))}"]`);
662
662
  const itemQueryId = (layoutId, item) => itemElement(layoutId, item)?.querySelector(`[data-bm-dashboard-query]`)?.getAttribute("data-bm-dashboard-query") ?? null;
663
+ const SIDEBAR_KEY = "bm-dashboard-devtools:layout-sidebar";
664
+ /** Abaixo disso a lista vai para cima do formulário, em vez de ficar ao lado. */
665
+ const STACK_WIDTH = 640;
666
+ function loadSidebar() {
667
+ try {
668
+ return localStorage.getItem(SIDEBAR_KEY) !== "closed";
669
+ } catch {
670
+ return true;
671
+ }
672
+ }
673
+ /** Largura do elemento, acompanhando o redimensionamento do devtools. */
674
+ function useWidth() {
675
+ const [element, setElement] = useState(null);
676
+ const [width, setWidth] = useState(Number.POSITIVE_INFINITY);
677
+ useEffect(() => {
678
+ if (!element) return;
679
+ const observer = new ResizeObserver(([entry]) => entry && setWidth(entry.contentRect.width));
680
+ observer.observe(element);
681
+ return () => observer.disconnect();
682
+ }, [element]);
683
+ return [setElement, width];
684
+ }
685
+ /** Move o item `from` para antes da posição `to` (`to === list.length` = fim). */
686
+ function moveTo(list, from, to) {
687
+ const next = [...list];
688
+ const [item] = next.splice(from, 1);
689
+ next.splice(to > from ? to - 1 : to, 0, item);
690
+ return next;
691
+ }
663
692
  const uniqueId = (charts, base) => {
664
693
  const ids = new Set(charts.map((chart) => chart.id));
665
694
  if (!ids.has(base)) return base;
@@ -701,15 +730,19 @@ const NEW_CHART = {
701
730
  span: 12
702
731
  }
703
732
  };
704
- const newChart = (type, model, charts) => {
705
- const { id, ...rest } = NEW_CHART[type];
733
+ const newChart = (type, model, charts, types) => {
734
+ const { id, ...rest } = NEW_CHART[type] ?? {
735
+ id: type,
736
+ title: `Novo ${types[type]?.label ?? type}`,
737
+ span: 6
738
+ };
706
739
  return chartOfType(type, model, {
707
740
  id: uniqueId(charts, id),
708
741
  ...rest
709
- });
742
+ }, types);
710
743
  };
711
- const chartSummary = (chart) => {
712
- const { dimensions, measures } = chartReferences(chart);
744
+ const chartSummary = (chart, types) => {
745
+ const { dimensions, measures } = chartReferences(chart, types);
713
746
  return `${chart.type} · ${dimensions.join(", ") || "—"} · ${measures.join(", ")}`;
714
747
  };
715
748
  /**
@@ -719,13 +752,17 @@ const chartSummary = (chart) => {
719
752
  */
720
753
  function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
721
754
  const { layouts } = devtools;
722
- const { fetcher, model: modelName } = useDashboard();
755
+ const { fetcher, model: modelName, chartTypes } = useDashboard();
723
756
  const entries = useSyncExternalStore(layouts.subscribe, layouts.getSnapshot, layouts.getSnapshot);
724
757
  const [layoutId, setLayoutId] = useState(null);
725
758
  const entry = entries.find((item) => item.registered.id === layoutId) ?? entries[0] ?? null;
726
759
  const reference = entry ? entry.draft ?? entry.registered : null;
727
760
  const [editing, setEditing] = useState(reference);
728
761
  const [selection, setSelection] = useState(null);
762
+ const [sidebar, setSidebar] = useState(loadSidebar);
763
+ const [drag, setDrag] = useState(null);
764
+ const [rootRef, rootWidth] = useWidth();
765
+ const [editorRef, editorWidth] = useWidth();
729
766
  const [busy, setBusy] = useState(false);
730
767
  const [actionError, setActionError] = useState(null);
731
768
  const [save, setSave] = useState(null);
@@ -738,12 +775,14 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
738
775
  setLayoutId(parsed.layoutId);
739
776
  setSelection(parsed.item);
740
777
  }, [picked]);
741
- const errors = useMemo(() => editing ? validateLayout(editing) : [], [editing]);
778
+ const errors = useMemo(() => editing ? validateLayout(editing, chartTypes) : [], [editing, chartTypes]);
742
779
  const warnings = useMemo(() => {
743
780
  if (!editing || errors.length) return [];
744
- const list = layoutModelWarnings(editing, model);
781
+ const list = layoutModelWarnings(editing, model, chartTypes);
745
782
  const { charts: chartFormatters = [], filters: filterFormatters } = entry?.parts ?? {};
746
- for (const chart of editing.charts) for (const name of chartReferences(chart).formatters) if (!chartFormatters.includes(name)) list.push(`"${chart.title || chart.id}": formatter "${name}" não foi passado ao <DashboardCharts formatters>`);
783
+ const serverTypes = new Set(capabilities?.chartTypes ?? []);
784
+ for (const type of new Set(editing.charts.map((chart) => chart.type))) if (capabilities?.saveLayout && !BUILTIN_CHART_TYPES[type] && !serverTypes.has(type)) list.push(`Tipo "${type}" não foi passado ao \`devtools.chartTypes\` do endpoint: "Salvar no arquivo" vai recusar.`);
785
+ for (const chart of editing.charts) for (const name of chartReferences(chart, chartTypes).formatters) if (!chartFormatters.includes(name)) list.push(`"${chart.title || chart.id}": formatter "${name}" não foi passado ao <DashboardCharts formatters>`);
747
786
  if (editing.canvas && !entry?.parts.canvas) list.push("A página não usa <DashboardCanvas layout={...}>: o tamanho fixo da tela não é aplicado.");
748
787
  if (editing.filters?.length && !filterFormatters) list.push("A página não usa <DashboardFilters layout={...}>: os filtros do layout não aparecem.");
749
788
  for (const filter of editing.filters ?? []) for (const name of filterReferences(filter).formatters) if (filterFormatters && !filterFormatters.includes(name)) list.push(`filtro "${filter.dimension}": formatter "${name}" não foi passado ao <DashboardFilters formatters>`);
@@ -752,7 +791,9 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
752
791
  editing,
753
792
  errors,
754
793
  model,
755
- entry
794
+ entry,
795
+ chartTypes,
796
+ capabilities
756
797
  ]);
757
798
  if (!entry || !editing) return /* @__PURE__ */ jsxs(Empty, { children: [
758
799
  "Nenhum ",
@@ -768,13 +809,13 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
768
809
  const charts = editing.charts;
769
810
  const filters = editing.filters ?? [];
770
811
  const selectedFilter = selection?.kind === "filter" ? filters.find((filter) => filter.dimension === selection.key) ?? null : null;
771
- const selected = selectedFilter || selection?.kind === "filter" ? null : charts.find((chart) => chart.id === selection?.key) ?? charts[0] ?? null;
812
+ const selected = selection?.kind === "filter" || selection?.kind === "canvas" ? null : charts.find((chart) => chart.id === selection?.key) ?? charts[0] ?? null;
772
813
  const layoutKey = entry.registered.id;
773
814
  const differsFromFile = canonicalJson(editing) !== canonicalJson(entry.registered);
774
815
  const update = (next) => {
775
816
  setEditing(next);
776
817
  setSave(null);
777
- if (!validateLayout(next).length) {
818
+ if (!validateLayout(next, chartTypes).length) {
778
819
  devtools.store.expectRefetch();
779
820
  layouts.setDraft(entry.registered.id, next);
780
821
  }
@@ -790,21 +831,39 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
790
831
  filters: next
791
832
  });
792
833
  const patchFilter = (dimension, change) => setFilters(filters.map((filter) => filter.dimension === dimension ? change(filter) : filter));
793
- const swap = (list, index, delta) => {
794
- const target = index + delta;
795
- if (target < 0 || target >= list.length) return null;
796
- const next = [...list];
797
- [next[index], next[target]] = [next[target], next[index]];
798
- return next;
799
- };
800
- const move = (index, delta) => {
801
- const next = swap(charts, index, delta);
802
- if (next) setCharts(next);
803
- };
804
- const moveFilter = (index, delta) => {
805
- const next = swap(filters, index, delta);
806
- if (next) setFilters(next);
834
+ const toggleSidebar = () => {
835
+ const next = !sidebar;
836
+ setSidebar(next);
837
+ try {
838
+ localStorage.setItem(SIDEBAR_KEY, next ? "open" : "closed");
839
+ } catch {}
807
840
  };
841
+ const dragProps = (kind, index, length) => ({
842
+ dropBefore: drag?.kind === kind && drag.from !== index && drag.to === index,
843
+ dropAfter: drag?.kind === kind && index === length - 1 && drag.to === length,
844
+ onDragStart: () => setDrag({
845
+ kind,
846
+ from: index,
847
+ to: index
848
+ }),
849
+ onDragOver: (e) => {
850
+ if (drag?.kind !== kind) return;
851
+ e.preventDefault();
852
+ const rect = e.currentTarget.getBoundingClientRect();
853
+ const to = e.clientY < rect.top + rect.height / 2 ? index : index + 1;
854
+ if (to !== drag.to) setDrag({
855
+ ...drag,
856
+ to
857
+ });
858
+ },
859
+ onDrop: () => {
860
+ if (drag?.kind !== kind) return;
861
+ if (drag.to !== drag.from && drag.to !== drag.from + 1) if (kind === "chart") setCharts(moveTo(charts, drag.from, drag.to));
862
+ else setFilters(moveTo(filters, drag.from, drag.to));
863
+ setDrag(null);
864
+ },
865
+ onDragEnd: () => setDrag(null)
866
+ });
808
867
  const addFilter = () => {
809
868
  const used = new Set(filters.map((filter) => filter.dimension));
810
869
  const dimension = Object.keys(model.dimensions).find((name) => !used.has(name));
@@ -817,11 +876,15 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
817
876
  };
818
877
  const focus = (item) => {
819
878
  setSelection(item);
879
+ if (stacked) setSidebar(false);
820
880
  itemElement(layoutKey, item)?.scrollIntoView({
821
881
  behavior: "smooth",
822
882
  block: "center"
823
883
  });
824
884
  };
885
+ const stacked = rootWidth < STACK_WIDTH;
886
+ const formColumns = editorWidth < 420 ? 1 : editorWidth < 720 ? 2 : 4;
887
+ const selectedTitle = selection?.kind === "canvas" ? "Tela" : selectedFilter ? selectedFilter.label || model.dimensions[selectedFilter.dimension]?.label || selectedFilter.dimension : selected ? selected.title || selected.id : "";
825
888
  const hover = (item) => onHover(item && itemQueryId(layoutKey, item));
826
889
  const add = (type, base) => {
827
890
  setAddMenu(null);
@@ -829,7 +892,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
829
892
  ...base,
830
893
  id: uniqueId(charts, base.id),
831
894
  title: `${base.title} (cópia)`
832
- } : newChart(type, model, charts);
895
+ } : newChart(type, model, charts, chartTypes);
833
896
  const index = base ? charts.findIndex((item) => item.id === base.id) + 1 : charts.length;
834
897
  setCharts([
835
898
  ...charts.slice(0, index),
@@ -864,6 +927,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
864
927
  }
865
928
  };
866
929
  return /* @__PURE__ */ jsxs(Box, {
930
+ ref: rootRef,
867
931
  sx: {
868
932
  display: "flex",
869
933
  flexDirection: "column",
@@ -871,19 +935,45 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
871
935
  minHeight: 0
872
936
  },
873
937
  children: [
938
+ /* @__PURE__ */ jsxs(Box, {
939
+ sx: {
940
+ display: "flex",
941
+ alignItems: "center",
942
+ gap: 1,
943
+ px: 1,
944
+ py: .5,
945
+ borderBottom: 1,
946
+ borderColor: "divider"
947
+ },
948
+ children: [/* @__PURE__ */ jsx(SmallButton, {
949
+ title: sidebar ? "Recolher a lista" : "Mostrar a lista",
950
+ onClick: toggleSidebar,
951
+ children: sidebar ? "«" : "☰"
952
+ }), /* @__PURE__ */ jsx(Typography, {
953
+ noWrap: true,
954
+ sx: {
955
+ fontSize: 12,
956
+ fontWeight: 700
957
+ },
958
+ children: stacked && sidebar ? `Layout ${layoutKey}` : selectedTitle
959
+ })]
960
+ }),
874
961
  /* @__PURE__ */ jsxs(Box, {
875
962
  sx: {
876
963
  flex: 1,
877
964
  minHeight: 0,
878
- display: "grid",
879
- gridTemplateColumns: "280px 1fr"
965
+ display: "flex"
880
966
  },
881
- children: [/* @__PURE__ */ jsxs(Box, {
967
+ children: [sidebar && /* @__PURE__ */ jsxs(Box, {
882
968
  sx: {
883
- borderRight: 1,
884
- borderColor: "divider",
885
969
  overflow: "auto",
886
- p: 1.5
970
+ p: 1.5,
971
+ ...stacked ? { flex: 1 } : {
972
+ width: 280,
973
+ flexShrink: 0,
974
+ borderRight: 1,
975
+ borderColor: "divider"
976
+ }
887
977
  },
888
978
  children: [
889
979
  entries.length > 1 && /* @__PURE__ */ jsx(TextField, {
@@ -902,9 +992,17 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
902
992
  children: [item.registered.id, item.draft ? " · rascunho" : ""]
903
993
  }, item.registered.id))
904
994
  }),
905
- /* @__PURE__ */ jsx(CanvasSection, {
906
- canvas: editing.canvas,
907
- onChange: setCanvas
995
+ /* @__PURE__ */ jsx(Section, {
996
+ title: "Tela",
997
+ children: /* @__PURE__ */ jsx(ItemRow, {
998
+ title: editing.canvas ? "Tamanho fixo" : "Responsiva",
999
+ subtitle: editing.canvas ? presetLabel(editing.canvas) : "reorganiza conforme a largura",
1000
+ selected: selection?.kind === "canvas",
1001
+ onSelect: () => {
1002
+ setSelection({ kind: "canvas" });
1003
+ if (stacked) setSidebar(false);
1004
+ }
1005
+ })
908
1006
  }),
909
1007
  /* @__PURE__ */ jsxs(Section, {
910
1008
  title: `Filtros (${filters.length})`,
@@ -932,8 +1030,7 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
932
1030
  selected: selectedFilter?.dimension === filter.dimension,
933
1031
  onSelect: () => focus(item),
934
1032
  onHover: (on) => hover(on ? item : null),
935
- onUp: index ? () => moveFilter(index, -1) : void 0,
936
- onDown: index < filters.length - 1 ? () => moveFilter(index, 1) : void 0,
1033
+ ...dragProps("filter", index, filters.length),
937
1034
  onRemove: () => setFilters(filters.filter((other) => other.dimension !== filter.dimension))
938
1035
  }, filter.dimension);
939
1036
  })]
@@ -948,10 +1045,10 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
948
1045
  anchorEl: addMenu,
949
1046
  open: !!addMenu,
950
1047
  onClose: () => setAddMenu(null),
951
- children: CHART_TYPES.map((type) => /* @__PURE__ */ jsx(MenuItem, {
1048
+ children: Object.entries(chartTypes).map(([type, { label }]) => /* @__PURE__ */ jsx(MenuItem, {
952
1049
  dense: true,
953
1050
  onClick: () => add(type),
954
- children: CHART_TYPE_LABEL[type]
1051
+ children: label
955
1052
  }, type))
956
1053
  })] }),
957
1054
  children: charts.map((chart, index) => {
@@ -961,83 +1058,95 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
961
1058
  };
962
1059
  return /* @__PURE__ */ jsx(ItemRow, {
963
1060
  title: chart.title || "(sem título)",
964
- subtitle: `${chartSummary(chart)} · ${chart.span ?? 12}/12`,
1061
+ subtitle: `${chartSummary(chart, chartTypes)} · ${chart.span ?? 12}/12`,
965
1062
  selected: chart.id === selected?.id,
966
1063
  onSelect: () => focus(item),
967
1064
  onHover: (on) => hover(on ? item : null),
968
- onUp: index ? () => move(index, -1) : void 0,
969
- onDown: index < charts.length - 1 ? () => move(index, 1) : void 0,
1065
+ ...dragProps("chart", index, charts.length),
970
1066
  onDuplicate: () => add(chart.type, chart),
971
1067
  onRemove: () => setCharts(charts.filter((other) => other.id !== chart.id))
972
1068
  }, chart.id);
973
1069
  })
974
1070
  })
975
1071
  ]
976
- }), /* @__PURE__ */ jsxs(Box, {
1072
+ }), !(stacked && sidebar) && /* @__PURE__ */ jsx(Box, {
1073
+ ref: editorRef,
977
1074
  sx: {
978
- overflow: "auto",
979
- p: 1.5
1075
+ flex: 1,
1076
+ minWidth: 0,
1077
+ overflow: "auto"
980
1078
  },
981
- children: [
982
- entry.draft && /* @__PURE__ */ jsxs(Alert, {
983
- severity: "warning",
984
- sx: { mb: 1.5 },
985
- children: [
986
- "Rascunho aplicado: a página mostra o layout editado, não o arquivo",
987
- " ",
988
- /* @__PURE__ */ jsxs("code", { children: [entry.registered.id, ".json"] }),
989
- ".",
990
- capabilities?.saveLayout ? " Use \"Salvar no arquivo\" quando terminar." : " Copie o JSON para o arquivo quando terminar."
991
- ]
992
- }),
993
- !!errors.length && /* @__PURE__ */ jsxs(Alert, {
994
- severity: "error",
995
- sx: { mb: 1.5 },
996
- children: ["Layout inválido — as edições não foram aplicadas na página:", errors.map((error) => /* @__PURE__ */ jsx("div", { children: error }, error))]
997
- }),
998
- !!warnings.length && /* @__PURE__ */ jsx(Alert, {
999
- severity: "warning",
1000
- sx: { mb: 1.5 },
1001
- children: warnings.map((warning) => /* @__PURE__ */ jsx("div", { children: warning }, warning))
1002
- }),
1003
- actionError && /* @__PURE__ */ jsx(Alert, {
1004
- severity: "error",
1005
- sx: {
1006
- mb: 1.5,
1007
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
1008
- fontSize: 12
1009
- },
1010
- children: actionError
1011
- }),
1012
- selectedFilter ? /* @__PURE__ */ jsx(FilterForm, {
1013
- filter: selectedFilter,
1014
- model,
1015
- formatters: entry.parts.filters ?? [],
1016
- onChange: (change) => {
1017
- patchFilter(selectedFilter.dimension, change);
1018
- setSelection({
1019
- kind: "filter",
1020
- key: change(selectedFilter).dimension
1021
- });
1022
- }
1023
- }, `filter:${selectedFilter.dimension}`) : selected ? /* @__PURE__ */ jsx(ChartForm, {
1024
- chart: selected,
1025
- model,
1026
- formatters: entry.parts.charts ?? [],
1027
- onChange: (change) => {
1028
- patchChart(selected.id, change);
1029
- setSelection({
1030
- kind: "chart",
1031
- key: change(selected).id
1032
- });
1033
- }
1034
- }, `chart:${selected.id}`) : /* @__PURE__ */ jsx(Empty, { children: "Escolha um filtro ou gráfico, ou use \"+ Novo\"." })
1035
- ]
1079
+ children: /* @__PURE__ */ jsxs(Box, {
1080
+ sx: { p: 1.5 },
1081
+ children: [
1082
+ entry.draft && /* @__PURE__ */ jsxs(Alert, {
1083
+ severity: "warning",
1084
+ sx: { mb: 1.5 },
1085
+ children: [
1086
+ "Rascunho aplicado: a página mostra o layout editado, não o arquivo",
1087
+ " ",
1088
+ /* @__PURE__ */ jsxs("code", { children: [entry.registered.id, ".json"] }),
1089
+ ".",
1090
+ capabilities?.saveLayout ? " Use \"Salvar no arquivo\" quando terminar." : " Copie o JSON para o arquivo quando terminar."
1091
+ ]
1092
+ }),
1093
+ !!errors.length && /* @__PURE__ */ jsxs(Alert, {
1094
+ severity: "error",
1095
+ sx: { mb: 1.5 },
1096
+ children: ["Layout inválido — as edições não foram aplicadas na página:", errors.map((error) => /* @__PURE__ */ jsx("div", { children: error }, error))]
1097
+ }),
1098
+ !!warnings.length && /* @__PURE__ */ jsx(Alert, {
1099
+ severity: "warning",
1100
+ sx: { mb: 1.5 },
1101
+ children: warnings.map((warning) => /* @__PURE__ */ jsx("div", { children: warning }, warning))
1102
+ }),
1103
+ actionError && /* @__PURE__ */ jsx(Alert, {
1104
+ severity: "error",
1105
+ sx: {
1106
+ mb: 1.5,
1107
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
1108
+ fontSize: 12
1109
+ },
1110
+ children: actionError
1111
+ }),
1112
+ selection?.kind === "canvas" ? /* @__PURE__ */ jsx(CanvasForm, {
1113
+ canvas: editing.canvas,
1114
+ columns: formColumns,
1115
+ onChange: setCanvas
1116
+ }) : selectedFilter ? /* @__PURE__ */ jsx(FilterForm, {
1117
+ filter: selectedFilter,
1118
+ model,
1119
+ formatters: entry.parts.filters ?? [],
1120
+ columns: formColumns,
1121
+ onChange: (change) => {
1122
+ patchFilter(selectedFilter.dimension, change);
1123
+ setSelection({
1124
+ kind: "filter",
1125
+ key: change(selectedFilter).dimension
1126
+ });
1127
+ }
1128
+ }, `filter:${selectedFilter.dimension}`) : selected ? /* @__PURE__ */ jsx(ChartForm, {
1129
+ chart: selected,
1130
+ types: chartTypes,
1131
+ model,
1132
+ formatters: entry.parts.charts ?? [],
1133
+ columns: formColumns,
1134
+ onChange: (change) => {
1135
+ patchChart(selected.id, change);
1136
+ setSelection({
1137
+ kind: "chart",
1138
+ key: change(selected).id
1139
+ });
1140
+ }
1141
+ }, `chart:${selected.id}`) : /* @__PURE__ */ jsx(Empty, { children: "Escolha a tela, um filtro ou um gráfico, ou use \"+ Novo\"." })
1142
+ ]
1143
+ })
1036
1144
  })]
1037
1145
  }),
1038
1146
  /* @__PURE__ */ jsxs(Box, {
1039
1147
  sx: {
1040
1148
  display: "flex",
1149
+ flexWrap: "wrap",
1041
1150
  gap: 1,
1042
1151
  justifyContent: "flex-end",
1043
1152
  alignItems: "center",
@@ -1120,74 +1229,88 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1120
1229
  }
1121
1230
  const presetLabel = ({ width, height }) => `${width}×${height ?? "auto"}`;
1122
1231
  /** Tamanho fixo da tela (`layout.canvas`): desligado = responsivo. */
1123
- function CanvasSection({ canvas, onChange }) {
1232
+ function CanvasForm({ canvas, columns, onChange }) {
1124
1233
  const preset = canvas && CANVAS_PRESETS.find((item) => presetLabel(item) === presetLabel(canvas));
1125
1234
  const number = (value) => value === "" ? void 0 : Number(value);
1126
- return /* @__PURE__ */ jsxs(Section, {
1235
+ return /* @__PURE__ */ jsx(Section, {
1127
1236
  title: "Tela",
1128
- children: [/* @__PURE__ */ jsx(Tooltip, {
1129
- title: "Desenha o dashboard nesse tamanho e escala para caber na tela, mantendo a proporção, em vez de reorganizar os gráficos. A página precisa do <DashboardCanvas>.",
1130
- placement: "right",
1131
- children: /* @__PURE__ */ jsx(FormControlLabel, {
1132
- control: /* @__PURE__ */ jsx(Checkbox, {
1133
- size: "small",
1134
- checked: !!canvas,
1135
- onChange: (e) => onChange(e.target.checked ? CANVAS_PRESETS[0] : void 0)
1136
- }),
1137
- label: /* @__PURE__ */ jsx(Typography, {
1138
- sx: { fontSize: 12 },
1139
- children: "Tamanho fixo"
1140
- })
1141
- })
1142
- }), canvas && /* @__PURE__ */ jsxs(Box, {
1143
- sx: {
1144
- display: "grid",
1145
- gridTemplateColumns: "1fr 1fr",
1146
- gap: 1,
1147
- mt: .5
1148
- },
1149
- children: [
1150
- /* @__PURE__ */ jsx(TextField, {
1237
+ children: /* @__PURE__ */ jsxs(Box, {
1238
+ sx: formGrid(columns),
1239
+ children: [/* @__PURE__ */ jsx(Box, {
1240
+ sx: { gridColumn: "1 / -1" },
1241
+ children: withHint(/* @__PURE__ */ jsx(FormControlLabel, {
1242
+ control: /* @__PURE__ */ jsx(Checkbox, {
1243
+ size: "small",
1244
+ checked: !!canvas,
1245
+ onChange: (e) => onChange(e.target.checked ? CANVAS_PRESETS[0] : void 0)
1246
+ }),
1247
+ label: /* @__PURE__ */ jsx(Typography, {
1248
+ sx: { fontSize: 12 },
1249
+ children: "Tamanho fixo"
1250
+ })
1251
+ }), "Desenha o dashboard nesse tamanho e escala para caber na tela, mantendo a proporção, em vez de reorganizar os gráficos. A página precisa do <DashboardCanvas>.")
1252
+ }), canvas && /* @__PURE__ */ jsxs(Fragment, { children: [
1253
+ field("Predefinido", /* @__PURE__ */ jsx(TextField, {
1151
1254
  select: true,
1152
1255
  size: "small",
1153
- label: "Predefinido",
1256
+ fullWidth: true,
1154
1257
  value: preset ? presetLabel(preset) : "",
1155
1258
  onChange: (e) => onChange(CANVAS_PRESETS.find((item) => presetLabel(item) === e.target.value)),
1156
- sx: {
1157
- ...inputSx$1,
1158
- gridColumn: "1 / -1"
1159
- },
1259
+ sx: inputSx$1,
1160
1260
  children: CANVAS_PRESETS.map((item) => /* @__PURE__ */ jsx(MenuItem, {
1161
1261
  value: presetLabel(item),
1162
1262
  children: presetLabel(item)
1163
1263
  }, presetLabel(item)))
1164
- }),
1165
- /* @__PURE__ */ jsx(TextField, {
1264
+ })),
1265
+ field("Largura", /* @__PURE__ */ jsx(TextField, {
1166
1266
  size: "small",
1267
+ fullWidth: true,
1167
1268
  type: "number",
1168
- label: "Largura",
1169
1269
  value: canvas.width ?? "",
1170
1270
  onChange: (e) => onChange(withField(canvas, "width", number(e.target.value))),
1171
1271
  sx: inputSx$1
1172
- }),
1173
- /* @__PURE__ */ jsx(TextField, {
1272
+ })),
1273
+ field("Altura mín.", /* @__PURE__ */ jsx(TextField, {
1174
1274
  size: "small",
1275
+ fullWidth: true,
1175
1276
  type: "number",
1176
- label: "Altura mín.",
1177
1277
  value: canvas.height ?? "",
1278
+ placeholder: "auto",
1178
1279
  onChange: (e) => onChange(withField(canvas, "height", number(e.target.value))),
1179
1280
  sx: inputSx$1
1180
- })
1181
- ]
1182
- })]
1281
+ }))
1282
+ ] })]
1283
+ })
1183
1284
  });
1184
1285
  }
1185
- function ItemRow({ title, subtitle, selected, onSelect, onHover, onUp, onDown, onDuplicate, onRemove }) {
1286
+ function ItemRow({ title, subtitle, selected, onSelect, onHover, onDuplicate, onRemove, dropBefore, dropAfter, onDragStart, onDragOver, onDrop, onDragEnd }) {
1287
+ const marker = {
1288
+ content: "\"\"",
1289
+ position: "absolute",
1290
+ left: 0,
1291
+ right: 0,
1292
+ height: 2,
1293
+ bgcolor: "primary.main"
1294
+ };
1186
1295
  return /* @__PURE__ */ jsxs(Box, {
1187
1296
  onClick: onSelect,
1188
- onMouseEnter: () => onHover(true),
1189
- onMouseLeave: () => onHover(false),
1297
+ onMouseEnter: () => onHover?.(true),
1298
+ onMouseLeave: () => onHover?.(false),
1299
+ draggable: !!onDragStart,
1300
+ onDragStart: (e) => {
1301
+ if (!onDragStart) return;
1302
+ e.dataTransfer.effectAllowed = "move";
1303
+ e.dataTransfer.setData("text/plain", title);
1304
+ onDragStart();
1305
+ },
1306
+ onDragOver,
1307
+ onDrop: (e) => {
1308
+ e.preventDefault();
1309
+ onDrop?.();
1310
+ },
1311
+ onDragEnd,
1190
1312
  sx: {
1313
+ position: "relative",
1191
1314
  display: "flex",
1192
1315
  alignItems: "center",
1193
1316
  gap: .5,
@@ -1195,64 +1318,71 @@ function ItemRow({ title, subtitle, selected, onSelect, onHover, onUp, onDown, o
1195
1318
  py: .5,
1196
1319
  mb: .5,
1197
1320
  borderRadius: 1,
1198
- cursor: "pointer",
1321
+ cursor: onDragStart ? "grab" : "pointer",
1199
1322
  border: 1,
1200
1323
  borderColor: selected ? "primary.main" : "divider",
1201
- "&:hover .item-actions": { visibility: "visible" }
1324
+ "&:hover .item-actions": { visibility: "visible" },
1325
+ "&::before": dropBefore ? {
1326
+ ...marker,
1327
+ top: -4
1328
+ } : void 0,
1329
+ "&::after": dropAfter ? {
1330
+ ...marker,
1331
+ bottom: -4
1332
+ } : void 0
1202
1333
  },
1203
- children: [/* @__PURE__ */ jsxs(Box, {
1204
- sx: {
1205
- minWidth: 0,
1206
- flex: 1
1207
- },
1208
- children: [/* @__PURE__ */ jsx(Typography, {
1209
- noWrap: true,
1334
+ children: [
1335
+ onDragStart && /* @__PURE__ */ jsx(Box, {
1336
+ component: "span",
1337
+ "aria-hidden": true,
1210
1338
  sx: {
1339
+ color: "text.disabled",
1211
1340
  fontSize: 12,
1212
- fontWeight: 700
1341
+ lineHeight: 1,
1342
+ userSelect: "none"
1213
1343
  },
1214
- children: title
1215
- }), /* @__PURE__ */ jsx(Typography, {
1216
- noWrap: true,
1344
+ children: "⋮⋮"
1345
+ }),
1346
+ /* @__PURE__ */ jsxs(Box, {
1217
1347
  sx: {
1218
- fontSize: 11,
1219
- color: "text.secondary",
1220
- fontFamily: MONO
1348
+ minWidth: 0,
1349
+ flex: 1
1221
1350
  },
1222
- children: subtitle
1223
- })]
1224
- }), /* @__PURE__ */ jsxs(Box, {
1225
- className: "item-actions",
1226
- onClick: (e) => e.stopPropagation(),
1227
- sx: {
1228
- display: "flex",
1229
- visibility: "hidden"
1230
- },
1231
- children: [
1232
- /* @__PURE__ */ jsx(SmallButton, {
1233
- title: "Subir",
1234
- disabled: !onUp,
1235
- onClick: () => onUp?.(),
1236
- children: "↑"
1237
- }),
1238
- /* @__PURE__ */ jsx(SmallButton, {
1239
- title: "Descer",
1240
- disabled: !onDown,
1241
- onClick: () => onDown?.(),
1242
- children: "↓"
1243
- }),
1244
- onDuplicate && /* @__PURE__ */ jsx(SmallButton, {
1351
+ children: [/* @__PURE__ */ jsx(Typography, {
1352
+ noWrap: true,
1353
+ sx: {
1354
+ fontSize: 12,
1355
+ fontWeight: 700
1356
+ },
1357
+ children: title
1358
+ }), /* @__PURE__ */ jsx(Typography, {
1359
+ noWrap: true,
1360
+ sx: {
1361
+ fontSize: 11,
1362
+ color: "text.secondary",
1363
+ fontFamily: MONO
1364
+ },
1365
+ children: subtitle
1366
+ })]
1367
+ }),
1368
+ (onDuplicate || onRemove) && /* @__PURE__ */ jsxs(Box, {
1369
+ className: "item-actions",
1370
+ onClick: (e) => e.stopPropagation(),
1371
+ sx: {
1372
+ display: "flex",
1373
+ visibility: selected ? "visible" : "hidden"
1374
+ },
1375
+ children: [onDuplicate && /* @__PURE__ */ jsx(SmallButton, {
1245
1376
  title: "Duplicar",
1246
1377
  onClick: onDuplicate,
1247
1378
  children: "⧉"
1248
- }),
1249
- /* @__PURE__ */ jsx(SmallButton, {
1379
+ }), onRemove && /* @__PURE__ */ jsx(SmallButton, {
1250
1380
  title: "Remover",
1251
1381
  onClick: onRemove,
1252
1382
  children: "×"
1253
- })
1254
- ]
1255
- })]
1383
+ })]
1384
+ })
1385
+ ]
1256
1386
  });
1257
1387
  }
1258
1388
  function SmallButton({ title, disabled, onClick, children }) {
@@ -1271,27 +1401,32 @@ function SmallButton({ title, disabled, onClick, children }) {
1271
1401
  }) })
1272
1402
  });
1273
1403
  }
1274
- function ChartForm({ chart, model, formatters, onChange }) {
1404
+ function ChartForm({ chart, types, model, formatters, columns, onChange }) {
1275
1405
  const set = (key, value) => onChange((current) => withField(current, key, value));
1276
1406
  return /* @__PURE__ */ jsx(FieldsForm, {
1277
- fields: CHART_FIELDS[chart.type],
1407
+ fields: types[chart.type]?.fields ?? {},
1278
1408
  values: chart,
1279
1409
  onSet: set,
1280
1410
  model,
1281
1411
  formatters,
1282
- columns: chartColumns(chart),
1283
- chosenDimensions: chartReferences(chart).dimensions,
1284
- header: /* @__PURE__ */ jsxs(Fragment, { children: [field("Tipo", /* @__PURE__ */ jsx(TextField, {
1412
+ columns: chartColumns(chart, types),
1413
+ formColumns: columns,
1414
+ chosenDimensions: chartReferences(chart, types).dimensions,
1415
+ header: /* @__PURE__ */ jsxs(Fragment, { children: [field("Tipo", /* @__PURE__ */ jsxs(TextField, {
1285
1416
  select: true,
1286
1417
  size: "small",
1287
1418
  fullWidth: true,
1288
1419
  value: chart.type,
1289
- onChange: (e) => onChange((current) => chartOfType(e.target.value, model, current)),
1420
+ onChange: (e) => onChange((current) => chartOfType(e.target.value, model, current, types)),
1290
1421
  sx: inputSx$1,
1291
- children: CHART_TYPES.map((type) => /* @__PURE__ */ jsx(MenuItem, {
1422
+ children: [Object.entries(types).map(([type, { label }]) => /* @__PURE__ */ jsx(MenuItem, {
1292
1423
  value: type,
1293
- children: CHART_TYPE_LABEL[type]
1294
- }, type))
1424
+ children: label
1425
+ }, type)), !types[chart.type] && /* @__PURE__ */ jsxs(MenuItem, {
1426
+ value: chart.type,
1427
+ disabled: true,
1428
+ children: [chart.type, " (não registrado)"]
1429
+ })]
1295
1430
  })), field("id", /* @__PURE__ */ jsx(TextField, {
1296
1431
  size: "small",
1297
1432
  fullWidth: true,
@@ -1302,7 +1437,7 @@ function ChartForm({ chart, model, formatters, onChange }) {
1302
1437
  }), "Identificador no JSON (letras, números, \"-\" e \"_\")")] })
1303
1438
  });
1304
1439
  }
1305
- function FilterForm({ filter, model, formatters, onChange }) {
1440
+ function FilterForm({ filter, model, formatters, columns, onChange }) {
1306
1441
  return /* @__PURE__ */ jsx(FieldsForm, {
1307
1442
  fields: FILTER_FIELDS,
1308
1443
  values: filter,
@@ -1310,11 +1445,12 @@ function FilterForm({ filter, model, formatters, onChange }) {
1310
1445
  model,
1311
1446
  formatters,
1312
1447
  columns: [],
1448
+ formColumns: columns,
1313
1449
  chosenDimensions: [filter.dimension]
1314
1450
  });
1315
1451
  }
1316
1452
  /** Formulário gerado dos `FieldSpec` (campos do tipo de gráfico ou do filtro). */
1317
- function FieldsForm({ fields, values, onSet: set, model, formatters, columns, chosenDimensions, header }) {
1453
+ function FieldsForm({ fields, values, onSet: set, model, formatters, columns, formColumns, chosenDimensions, header }) {
1318
1454
  const dimensions = Object.entries(model.dimensions).map(([name, dim]) => ({
1319
1455
  name,
1320
1456
  label: dim.label
@@ -1484,21 +1620,16 @@ function FieldsForm({ fields, values, onSet: set, model, formatters, columns, ch
1484
1620
  }
1485
1621
  };
1486
1622
  const sections = [...new Set(entries.map(([, spec]) => spec.section))];
1487
- const grid = {
1488
- display: "grid",
1489
- gap: 1.5,
1490
- gridTemplateColumns: "repeat(4, minmax(0, 1fr))"
1491
- };
1492
1623
  const wide = (spec, key) => key === "title" || spec.kind === "dimensions" || spec.kind === "measures" || spec.kind === "formatters";
1493
1624
  return /* @__PURE__ */ jsx(Fragment, { children: sections.map((section) => {
1494
1625
  const inSection = entries.filter(([, spec]) => spec.section === section);
1495
1626
  return /* @__PURE__ */ jsx(Section, {
1496
1627
  title: section,
1497
1628
  children: /* @__PURE__ */ jsxs(Box, {
1498
- sx: grid,
1629
+ sx: formGrid(formColumns),
1499
1630
  children: [section === sections[0] && header, inSection.map(([key, spec]) => /* @__PURE__ */ jsx(Box, {
1500
1631
  sx: {
1501
- gridColumn: wide(spec, key) ? "span 2" : void 0,
1632
+ gridColumn: wide(spec, key) && formColumns > 1 ? "span 2" : void 0,
1502
1633
  alignSelf: spec.kind === "boolean" ? "end" : void 0
1503
1634
  },
1504
1635
  children: spec.kind === "boolean" ? withHint(input(key, spec), spec.hint) : field(spec.label, input(key, spec), spec.hint)
@@ -1507,6 +1638,11 @@ function FieldsForm({ fields, values, onSet: set, model, formatters, columns, ch
1507
1638
  }, section);
1508
1639
  }) });
1509
1640
  }
1641
+ const formGrid = (columns) => ({
1642
+ display: "grid",
1643
+ gap: 1.5,
1644
+ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`
1645
+ });
1510
1646
  /** Rótulo padrão (do dicionário) como placeholder dos campos `*Label` do combo. */
1511
1647
  function labelPlaceholder(key, values, model) {
1512
1648
  if (key === "label" && typeof values.dimension === "string") return model.dimensions[values.dimension]?.label ?? values.dimension;
@@ -6258,4 +6394,4 @@ function ToggleButton$1({ draft, loading, problems, inspecting, onOpen }) {
6258
6394
  //#endregion
6259
6395
  export { DashboardDevtools as default };
6260
6396
 
6261
- //# sourceMappingURL=DashboardDevtools-ByrFb6HT.js.map
6397
+ //# sourceMappingURL=DashboardDevtools-5wh3brAT.js.map