@bluemarble/bi 0.3.1 → 0.5.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,7 +1,7 @@
1
- import { A as modelEdges, C as VALUE_FORMATS, D as validateModel, N as referencedTables, S as AGGREGATION_LABEL, T as isAggregate, _ as canonicalJson, a as FILTER_FIELDS, b as toModelCode, c as chartOfType, d as filterReferences, g as validateLayout, i as CARD_FIELDS, j as parseColumnRef, k as measureHome, l as chartReferences, m as layoutModelWarnings, n as CANVAS_PRESETS, p as layoutFileContent, r as CARDS_FIELDS, s as chartColumns, t as BUILTIN_CHART_TYPES, x as AGGREGATIONS } from "./layout-DTR4vI-I.js";
1
+ import { A as measureHome, C as AGGREGATION_LABEL, E as isAggregate, M as parseColumnRef, O as validateModel, P as referencedTables, S as AGGREGATIONS, _ as validateLayout, a as FILTER_FIELDS, c as chartOfType, d as filterReferences, h as orderedColumns, i as CARD_FIELDS, j as modelEdges, l as chartReferences, m as layoutModelWarnings, n as CANVAS_PRESETS, p as layoutFileContent, r as CARDS_FIELDS, s as chartColumns, t as BUILTIN_CHART_TYPES, v as canonicalJson, w as VALUE_FORMATS, x as toModelCode } from "./layout-QuOYFmp1.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-kUKcdbc8.js";
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, TableSortLabel, Tabs, TextField, ThemeProvider, ToggleButton, ToggleButtonGroup, Tooltip, Typography, createTheme } from "@mui/material";
3
+ import { d as useDashboard, f as errorMessage, h as parseLayoutItemKey, m as layoutItemKey, n as INSPECT_ATTRIBUTE, o as formatMeasure, p as LAYOUT_ITEM_ATTRIBUTE, t as ModelDictionary } from "./ModelDictionary-D3JzCd6x.js";
4
+ import { Alert, Autocomplete, Box, Button, Checkbox, Chip, CircularProgress, Collapse, Dialog, DialogActions, DialogContent, DialogTitle, Divider, FormControlLabel, IconButton, List, ListItemButton, ListSubheader, Menu, MenuItem, MenuList, Paper, Popover, Popper, Portal, Tab, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TableSortLabel, 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";
7
7
  import { createPortal } from "react-dom";
@@ -960,8 +960,8 @@ const chartSummary = (chart, types) => {
960
960
  };
961
961
  /**
962
962
  * Editor dos layouts da página (filtros do `<DashboardFilters>` e gráficos do `<DashboardCharts>`).
963
- * Cada edição válida vira rascunho na hora — a página re-renderiza com ele — e "Salvar no arquivo"
964
- * grava o JSON do layout.
963
+ * Como no modelo: as edições ficam no editor até "Aplicar na página" (vira rascunho e a página
964
+ * re-renderiza com ele); "Salvar no arquivo" grava o JSON do layout com o que está no editor.
965
965
  */
966
966
  function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
967
967
  const { layouts } = devtools;
@@ -970,7 +970,8 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
970
970
  const [layoutId, setLayoutId] = useState(null);
971
971
  const entry = entries.find((item) => item.registered.id === layoutId) ?? entries[0] ?? null;
972
972
  const reference = entry ? entry.draft ?? entry.registered : null;
973
- const [editing, setEditing] = useState(reference);
973
+ const pendingOrReference = () => (entry && layouts.pendingOf(entry.registered.id)) ?? reference;
974
+ const [editing, setEditingState] = useState(pendingOrReference);
974
975
  const [selection, setSelection] = useState(null);
975
976
  const [sidebar, setSidebar] = useState(loadSidebar);
976
977
  const [drag, setDrag] = useState(null);
@@ -981,7 +982,19 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
981
982
  const [save, setSave] = useState(null);
982
983
  const [addMenu, setAddMenu] = useState(null);
983
984
  const referenceKey = canonicalJson(reference);
984
- useEffect(() => setEditing(reference), [referenceKey]);
985
+ /** Muda o editor e guarda como pendente o que difere do que a página mostra. */
986
+ const setEditing = (next) => {
987
+ setEditingState(next);
988
+ if (entry) layouts.setPending(entry.registered.id, next && canonicalJson(next) !== referenceKey ? next : null);
989
+ };
990
+ const firstRun = useRef(true);
991
+ useEffect(() => {
992
+ if (firstRun.current) {
993
+ firstRun.current = false;
994
+ return;
995
+ }
996
+ setEditingState(pendingOrReference());
997
+ }, [referenceKey]);
985
998
  useEffect(() => {
986
999
  const parsed = picked && parseLayoutItemKey(picked.item);
987
1000
  if (!parsed) return;
@@ -1025,13 +1038,16 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1025
1038
  const selected = selection?.kind === "filter" || selection?.kind === "canvas" || selection?.kind === "cards" ? null : charts.find((chart) => chart.id === selection?.key) ?? charts[0] ?? null;
1026
1039
  const layoutKey = entry.registered.id;
1027
1040
  const differsFromFile = canonicalJson(editing) !== canonicalJson(entry.registered);
1041
+ const dirty = canonicalJson(editing) !== referenceKey;
1028
1042
  const update = (next) => {
1029
1043
  setEditing(next);
1030
1044
  setSave(null);
1031
- if (!validateLayout(next, chartTypes).length) {
1032
- devtools.store.expectRefetch();
1033
- layouts.setDraft(entry.registered.id, next);
1034
- }
1045
+ };
1046
+ const apply = () => {
1047
+ if (validateLayout(editing, chartTypes).length) return;
1048
+ devtools.store.expectRefetch();
1049
+ layouts.setPending(entry.registered.id, null);
1050
+ layouts.setDraft(entry.registered.id, editing);
1035
1051
  };
1036
1052
  const setCharts = (next) => update({
1037
1053
  ...editing,
@@ -1134,6 +1150,11 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1134
1150
  } }
1135
1151
  });
1136
1152
  setSave(mode === "write" ? null : result.devtools?.save ?? null);
1153
+ if (mode === "write" && editing && entry) {
1154
+ devtools.store.expectRefetch();
1155
+ layouts.setPending(entry.registered.id, null);
1156
+ layouts.setDraft(entry.registered.id, editing);
1157
+ }
1137
1158
  } catch (error) {
1138
1159
  setActionError(errorMessage(error));
1139
1160
  } finally {
@@ -1398,29 +1419,53 @@ function LayoutPanel({ devtools, model, capabilities, picked, onHover }) {
1398
1419
  borderColor: "divider"
1399
1420
  },
1400
1421
  children: [
1422
+ dirty && /* @__PURE__ */ jsx(Typography, {
1423
+ sx: {
1424
+ fontSize: 12,
1425
+ color: "warning.main",
1426
+ mr: "auto"
1427
+ },
1428
+ children: "Edições não aplicadas"
1429
+ }),
1401
1430
  /* @__PURE__ */ jsx(CopyButton, {
1402
1431
  text: layoutFileContent(editing),
1403
1432
  label: "Copiar JSON"
1404
1433
  }),
1434
+ /* @__PURE__ */ jsx(Button, {
1435
+ size: "small",
1436
+ disabled: !dirty,
1437
+ onClick: () => setEditing(reference),
1438
+ title: "Desfaz as edições ainda não aplicadas",
1439
+ children: "Desfazer"
1440
+ }),
1405
1441
  /* @__PURE__ */ jsx(Button, {
1406
1442
  size: "small",
1407
1443
  color: "warning",
1408
- disabled: !entry.draft && !differsFromFile,
1444
+ disabled: !entry.draft,
1409
1445
  onClick: () => {
1410
1446
  devtools.store.expectRefetch();
1447
+ layouts.setPending(entry.registered.id, null);
1411
1448
  layouts.setDraft(entry.registered.id, null);
1412
- setEditing(entry.registered);
1449
+ setEditingState(entry.registered);
1413
1450
  },
1414
- title: "Volta ao layout do arquivo",
1451
+ title: "Volta a página e o editor ao layout do arquivo",
1415
1452
  children: "Descartar rascunho"
1416
1453
  }),
1417
1454
  capabilities?.saveLayout && /* @__PURE__ */ jsx(Button, {
1418
1455
  size: "small",
1419
- variant: "contained",
1456
+ variant: "outlined",
1420
1457
  disabled: !differsFromFile || !!errors.length || busy,
1421
1458
  onClick: () => saveFile("preview"),
1422
- title: `Reescreve ${entry.registered.id}.json`,
1459
+ title: `Reescreve ${entry.registered.id}.json com o que está no editor`,
1423
1460
  children: busy ? "Gerando…" : "Salvar no arquivo"
1461
+ }),
1462
+ /* @__PURE__ */ jsx(Button, {
1463
+ size: "small",
1464
+ variant: "contained",
1465
+ disabled: !dirty || !!errors.length,
1466
+ onClick: apply,
1467
+ title: "Mostra as edições na página (sem gravar o arquivo)",
1468
+ children: "Aplicar na página"
1424
1469
  })
1425
1470
  ]
1426
1471
  }),
@@ -1646,16 +1691,19 @@ function SmallButton({ title, disabled, onClick, children }) {
1646
1691
  }
1647
1692
  function ChartForm({ chart, cards, types, model, formatters, columns, onChange }) {
1648
1693
  const set = (key, value) => onChange((current) => withField(current, key, value));
1694
+ const setMany = (changes) => onChange((current) => Object.entries(changes).reduce((chart, [key, value]) => withField(chart, key, value), current));
1649
1695
  const setCard = (key, value) => onChange((current) => withField(current, "card", nonEmpty(withField(current.card ?? {}, key, value))));
1650
1696
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FieldsForm, {
1651
1697
  fields: types[chart.type]?.fields ?? {},
1652
1698
  values: chart,
1653
1699
  onSet: set,
1700
+ onSetMany: setMany,
1654
1701
  model,
1655
1702
  formatters,
1656
1703
  columns: chartColumns(chart, types),
1657
1704
  formColumns: columns,
1658
1705
  chosenDimensions: chartReferences(chart, types).dimensions,
1706
+ chosenMeasures: chartReferences(chart, types).measures,
1659
1707
  header: /* @__PURE__ */ jsxs(Fragment, { children: [field("Tipo", /* @__PURE__ */ jsxs(TextField, {
1660
1708
  select: true,
1661
1709
  size: "small",
@@ -1703,7 +1751,7 @@ function FilterForm({ filter, model, formatters, columns, onChange }) {
1703
1751
  });
1704
1752
  }
1705
1753
  /** Formulário gerado dos `FieldSpec` (campos do tipo de gráfico ou do filtro). */
1706
- function FieldsForm({ fields, values, onSet: set, model, formatters, columns, formColumns, chosenDimensions, header }) {
1754
+ function FieldsForm({ fields, values, onSet: set, onSetMany, model, formatters, columns, formColumns, chosenDimensions, chosenMeasures = [], header }) {
1707
1755
  const dimensions = Object.entries(model.dimensions).map(([name, dim]) => ({
1708
1756
  name,
1709
1757
  label: dim.label
@@ -1713,6 +1761,106 @@ function FieldsForm({ fields, values, onSet: set, model, formatters, columns, fo
1713
1761
  label: measure.label
1714
1762
  }));
1715
1763
  const entries = Object.entries(fields);
1764
+ const keyOf = (kind) => entries.find(([, spec]) => spec.kind === kind)?.[0];
1765
+ const labelsKey = keyOf("labels");
1766
+ const formatsKey = keyOf("formats");
1767
+ const columnOptions = !!(labelsKey || formatsKey);
1768
+ const formattersKey = columnOptions ? keyOf("formatters") : void 0;
1769
+ const single = (kind) => entries.filter(([, spec]) => spec.kind === kind);
1770
+ const formatterKey = columnOptions && single("dimension").length === 1 && single("formatter").length === 1 ? single("formatter")[0]?.[0] : void 0;
1771
+ const labels = labelsKey && values[labelsKey] || {};
1772
+ const formats = formatsKey && values[formatsKey] || {};
1773
+ const dimensionFormatters = formattersKey && values[formattersKey] || {};
1774
+ const optionsButton = (kind, name) => {
1775
+ if (!columnOptions || !name) return null;
1776
+ const canFormatter = kind === "dimension" && !!(formattersKey || formatterKey);
1777
+ return /* @__PURE__ */ jsx(ColumnOptions, {
1778
+ kind,
1779
+ name,
1780
+ model,
1781
+ label: labelsKey ? labels[name] ?? "" : void 0,
1782
+ onLabel: (text) => labelsKey && set(labelsKey, pruned(withField(labels, name, text), columns)),
1783
+ format: kind === "measure" && formatsKey ? formats[name] ?? {} : void 0,
1784
+ onFormat: (format) => {
1785
+ if (!formatsKey) return;
1786
+ const map = { ...formats };
1787
+ if (Object.keys(format).length) map[name] = format;
1788
+ else delete map[name];
1789
+ set(formatsKey, pruned(map, chosenMeasures));
1790
+ },
1791
+ formatter: !canFormatter ? void 0 : formattersKey ? dimensionFormatters[name] ?? "" : (formatterKey && values[formatterKey]) ?? "",
1792
+ formatters,
1793
+ onFormatter: (formatter) => {
1794
+ if (formattersKey) set(formattersKey, pruned(withField(dimensionFormatters, name, formatter), chosenDimensions));
1795
+ else if (formatterKey) set(formatterKey, formatter || void 0);
1796
+ },
1797
+ onReset: () => {
1798
+ const changes = {};
1799
+ if (labelsKey) changes[labelsKey] = pruned(withField(labels, name, void 0), columns);
1800
+ if (formatsKey && kind === "measure") changes[formatsKey] = pruned(withField(formats, name, void 0), chosenMeasures);
1801
+ if (canFormatter && formattersKey) changes[formattersKey] = pruned(withField(dimensionFormatters, name, void 0), chosenDimensions);
1802
+ else if (canFormatter && formatterKey) changes[formatterKey] = void 0;
1803
+ if (onSetMany) onSetMany(changes);
1804
+ else for (const [key, value] of Object.entries(changes)) set(key, value);
1805
+ }
1806
+ });
1807
+ };
1808
+ const merged = (() => {
1809
+ for (const [dimensionsKey, spec] of entries) {
1810
+ if (spec.kind !== "dimensions") continue;
1811
+ const pair = entries.find(([, other]) => other.kind === "measures" && other.section === spec.section);
1812
+ if (!pair) continue;
1813
+ const order = entries.find(([, other]) => other.kind === "columnOrder")?.[0];
1814
+ return {
1815
+ dimensions: dimensionsKey,
1816
+ dimensionsSpec: spec,
1817
+ measures: pair[0],
1818
+ measuresSpec: pair[1],
1819
+ order
1820
+ };
1821
+ }
1822
+ })();
1823
+ const mergedInput = (pair) => {
1824
+ const dimensionNames = values[pair.dimensions] ?? [];
1825
+ const measureNames = values[pair.measures] ?? [];
1826
+ const all = [...dimensionNames.map((name) => ({
1827
+ kind: "dimension",
1828
+ name
1829
+ })), ...measureNames.map((name) => ({
1830
+ kind: "measure",
1831
+ name
1832
+ }))];
1833
+ const order = pair.order ? values[pair.order] : void 0;
1834
+ const sortedNames = orderedColumns(all.map((item) => item.name), order);
1835
+ const pool = [...all];
1836
+ const items = sortedNames.map((name) => {
1837
+ const index = pool.findIndex((item) => item.name === name);
1838
+ return pool.splice(index, 1)[0];
1839
+ });
1840
+ const required = !!pair.dimensionsSpec.required || !!pair.measuresSpec.required;
1841
+ return /* @__PURE__ */ jsx(ColumnList, {
1842
+ items,
1843
+ options: {
1844
+ dimension: options(pair.dimensionsSpec),
1845
+ measure: options(pair.measuresSpec)
1846
+ },
1847
+ extra: (item) => optionsButton(item.kind, item.name),
1848
+ error: !!pair.measuresSpec.required && !measureNames.length || !!pair.dimensionsSpec.required && !dimensionNames.length || required && !items.length,
1849
+ onChange: (next) => {
1850
+ const dimensionList = next.filter((item) => item.kind === "dimension");
1851
+ const measureList = next.filter((item) => item.kind === "measure");
1852
+ const names = next.map((item) => item.name);
1853
+ const standard = [...dimensionList, ...measureList].map((item) => item.name);
1854
+ const changes = {
1855
+ [pair.dimensions]: dimensionList.length || pair.dimensionsSpec.required ? dimensionList.map((item) => item.name) : void 0,
1856
+ [pair.measures]: measureList.length || pair.measuresSpec.required ? measureList.map((item) => item.name) : void 0
1857
+ };
1858
+ if (pair.order) changes[pair.order] = names.join("\0") === standard.join("\0") ? void 0 : names;
1859
+ if (onSetMany) onSetMany(changes);
1860
+ else for (const [key, value] of Object.entries(changes)) set(key, value);
1861
+ }
1862
+ });
1863
+ };
1716
1864
  const options = (spec) => {
1717
1865
  switch (spec.kind) {
1718
1866
  case "dimension":
@@ -1742,37 +1890,11 @@ function FieldsForm({ fields, values, onSet: set, model, formatters, columns, fo
1742
1890
  onChange: (e) => set(key, e.target.value),
1743
1891
  sx: inputSx$1
1744
1892
  });
1745
- case "color": {
1746
- const fallback = spec.default;
1747
- return /* @__PURE__ */ jsxs(Box, {
1748
- sx: {
1749
- display: "flex",
1750
- gap: .5,
1751
- alignItems: "center"
1752
- },
1753
- children: [/* @__PURE__ */ jsx(Box, {
1754
- component: "input",
1755
- type: "color",
1756
- value: value ?? fallback,
1757
- onChange: (e) => set(key, e.target.value),
1758
- sx: {
1759
- width: 32,
1760
- height: 30,
1761
- p: 0,
1762
- border: 0,
1763
- bgcolor: "transparent",
1764
- cursor: "pointer"
1765
- }
1766
- }), /* @__PURE__ */ jsx(TextField, {
1767
- size: "small",
1768
- fullWidth: true,
1769
- value: value ?? "",
1770
- placeholder: fallback,
1771
- onChange: (e) => set(key, e.target.value),
1772
- sx: inputSx$1
1773
- })]
1774
- });
1775
- }
1893
+ case "color": return /* @__PURE__ */ jsx(ColorInput, {
1894
+ value,
1895
+ fallback: spec.default,
1896
+ onCommit: (color) => set(key, color)
1897
+ });
1776
1898
  case "boolean": {
1777
1899
  const fallback = spec.default;
1778
1900
  return /* @__PURE__ */ jsx(FormControlLabel, {
@@ -1818,12 +1940,24 @@ function FieldsForm({ fields, values, onSet: set, model, formatters, columns, fo
1818
1940
  sx: inputSx$1
1819
1941
  });
1820
1942
  case "dimensions":
1821
- case "measures": return /* @__PURE__ */ jsx(MultiNameSelect, {
1822
- value: value ?? [],
1823
- options: options(spec),
1824
- required: !!spec.required,
1825
- onChange: (next) => set(key, next.length || spec.required ? next : void 0)
1826
- });
1943
+ case "measures": {
1944
+ const kind = spec.kind === "dimensions" ? "dimension" : "measure";
1945
+ if (merged?.dimensions === key) return mergedInput(merged);
1946
+ const names = value ?? [];
1947
+ return /* @__PURE__ */ jsx(ColumnList, {
1948
+ items: names.map((name) => ({
1949
+ kind,
1950
+ name
1951
+ })),
1952
+ options: { [kind]: options(spec) },
1953
+ extra: (item) => optionsButton(item.kind, item.name),
1954
+ error: !!spec.required && !names.length,
1955
+ onChange: (next) => {
1956
+ const list = next.map((item) => item.name);
1957
+ set(key, list.length || spec.required ? list : void 0);
1958
+ }
1959
+ });
1960
+ }
1827
1961
  case "formatters": {
1828
1962
  const map = value ?? {};
1829
1963
  if (!chosenDimensions.length) return /* @__PURE__ */ jsx(Typography, {
@@ -1863,17 +1997,37 @@ function FieldsForm({ fields, values, onSet: set, model, formatters, columns, fo
1863
1997
  }, dimension))
1864
1998
  });
1865
1999
  }
1866
- default: return /* @__PURE__ */ jsx(NameSelect, {
1867
- value: value ?? "",
1868
- options: options(spec),
1869
- required: spec.required,
1870
- emptyLabel: spec.kind === "enum" && spec.default ? `padrão (${spec.labels?.[spec.default] ?? spec.default})` : void 0,
1871
- onChange: (name) => set(key, name === spec.default ? void 0 : name)
1872
- });
2000
+ default: {
2001
+ const select = /* @__PURE__ */ jsx(NameSelect, {
2002
+ value: value ?? "",
2003
+ options: options(spec),
2004
+ required: spec.required,
2005
+ emptyLabel: spec.kind === "enum" && spec.default ? `padrão (${spec.labels?.[spec.default] ?? spec.default})` : void 0,
2006
+ onChange: (name) => set(key, name === spec.default ? void 0 : name)
2007
+ });
2008
+ const button = spec.kind === "dimension" || spec.kind === "measure" ? optionsButton(spec.kind, value) : null;
2009
+ return button ? /* @__PURE__ */ jsxs(Box, {
2010
+ sx: {
2011
+ display: "flex",
2012
+ gap: .25,
2013
+ alignItems: "center"
2014
+ },
2015
+ children: [select, button]
2016
+ }) : select;
2017
+ }
1873
2018
  }
1874
2019
  };
1875
- const sections = [...new Set(entries.map(([, spec]) => spec.section))];
1876
- const wide = (spec, key) => key === "title" || spec.kind === "dimensions" || spec.kind === "measures" || spec.kind === "formatters";
2020
+ const hidden = new Set([
2021
+ merged?.measures,
2022
+ ...entries.filter(([, spec]) => spec.kind === "columnOrder").map(([key]) => key),
2023
+ labelsKey,
2024
+ formatsKey,
2025
+ formattersKey,
2026
+ formatterKey
2027
+ ].filter(Boolean));
2028
+ const sections = [...new Set(entries.filter(([key]) => !hidden.has(key)).map(([, spec]) => spec.section))];
2029
+ const fullRow = (spec) => spec.kind === "dimensions" || spec.kind === "measures" || spec.kind === "formatters";
2030
+ const gridColumn = (spec, key) => fullRow(spec) ? "1 / -1" : key === "title" && formColumns > 1 ? "span 2" : void 0;
1877
2031
  return /* @__PURE__ */ jsx(Fragment, { children: sections.map((section) => {
1878
2032
  const inSection = entries.filter(([, spec]) => spec.section === section);
1879
2033
  return /* @__PURE__ */ jsx(Section, {
@@ -1881,28 +2035,314 @@ function FieldsForm({ fields, values, onSet: set, model, formatters, columns, fo
1881
2035
  collapseKey: `layout:form:${section}`,
1882
2036
  children: /* @__PURE__ */ jsxs(Box, {
1883
2037
  sx: formGrid(formColumns),
1884
- children: [section === sections[0] && header, inSection.map(([key, spec]) => /* @__PURE__ */ jsx(Box, {
2038
+ children: [section === sections[0] && header, inSection.filter(([key]) => !hidden.has(key)).map(([key, spec]) => /* @__PURE__ */ jsx(Box, {
1885
2039
  sx: {
1886
- gridColumn: wide(spec, key) && formColumns > 1 ? "span 2" : void 0,
2040
+ gridColumn: gridColumn(spec, key),
1887
2041
  alignSelf: spec.kind === "boolean" ? "end" : void 0
1888
2042
  },
1889
- children: spec.kind === "boolean" ? withHint(input(key, spec), spec.hint) : field(spec.label, input(key, spec), spec.hint)
2043
+ children: spec.kind === "boolean" ? withHint(input(key, spec), spec.hint) : merged?.dimensions === key ? field("Dimensões e medidas", input(key, spec), `${merged.order ? "Em qualquer ordem: é a ordem das colunas. " : ""}${[spec.hint, merged.measuresSpec.hint].filter(Boolean).join(" · ")}`) : field(spec.label, input(key, spec), spec.hint)
1890
2044
  }, key))]
1891
2045
  })
1892
2046
  }, section);
1893
2047
  }) });
1894
2048
  }
2049
+ /**
2050
+ * Cor com o seletor nativo. Mexer no seletor só muda a amostra: o valor vai para o editor quando
2051
+ * ele fecha — o `change` nativo; o `onChange` do React dispara a cada movimento. O texto entra no
2052
+ * Enter ou ao sair do campo.
2053
+ */
2054
+ function ColorInput({ value, fallback, onCommit }) {
2055
+ const [picking, setPicking] = useState(null);
2056
+ const [typing, setTyping] = useState(null);
2057
+ const picker = useRef(null);
2058
+ const commit = useRef(onCommit);
2059
+ useEffect(() => {
2060
+ commit.current = onCommit;
2061
+ });
2062
+ useEffect(() => {
2063
+ const input = picker.current;
2064
+ if (!input) return;
2065
+ const listener = () => {
2066
+ commit.current(input.value);
2067
+ setPicking(null);
2068
+ };
2069
+ input.addEventListener("change", listener);
2070
+ return () => input.removeEventListener("change", listener);
2071
+ }, []);
2072
+ const commitText = () => {
2073
+ if (typing === null) return;
2074
+ const text = typing.trim();
2075
+ setTyping(null);
2076
+ if (text !== (value ?? "")) onCommit(text || void 0);
2077
+ };
2078
+ return /* @__PURE__ */ jsxs(Box, {
2079
+ sx: {
2080
+ display: "flex",
2081
+ gap: .5,
2082
+ alignItems: "center"
2083
+ },
2084
+ children: [/* @__PURE__ */ jsx(Box, {
2085
+ component: "input",
2086
+ type: "color",
2087
+ ref: picker,
2088
+ value: picking ?? value ?? fallback,
2089
+ onChange: (e) => setPicking(e.target.value),
2090
+ sx: {
2091
+ width: 32,
2092
+ height: 30,
2093
+ p: 0,
2094
+ border: 0,
2095
+ bgcolor: "transparent",
2096
+ cursor: "pointer"
2097
+ }
2098
+ }), /* @__PURE__ */ jsx(TextField, {
2099
+ size: "small",
2100
+ fullWidth: true,
2101
+ value: typing ?? picking ?? value ?? "",
2102
+ placeholder: fallback,
2103
+ onChange: (e) => setTyping(e.target.value),
2104
+ onBlur: commitText,
2105
+ onKeyDown: (e) => {
2106
+ if (e.key === "Enter") commitText();
2107
+ if (e.key === "Escape") setTyping(null);
2108
+ },
2109
+ sx: inputSx$1
2110
+ })]
2111
+ });
2112
+ }
2113
+ /** Sem as chaves de colunas que saíram do gráfico; vazio = `undefined` (sai do JSON). */
2114
+ function pruned(map, keep) {
2115
+ const next = Object.fromEntries(Object.entries(map).filter(([key]) => keep.includes(key)));
2116
+ return Object.keys(next).length ? next : void 0;
2117
+ }
2118
+ const FORMAT_LABEL = {
2119
+ number: "número",
2120
+ integer: "inteiro",
2121
+ currency: "R$",
2122
+ percent: "%"
2123
+ };
2124
+ /** Número de exemplo da prévia do formato. */
2125
+ const SAMPLE = 1234567.891;
2126
+ /**
2127
+ * Ícone à direita de uma dimensão/medida que abre as opções dela no gráfico: rótulo, formatter
2128
+ * (dimensão) e formato (medida). Vazio = o do dicionário do modelo (que aparece como placeholder).
2129
+ * Opção ausente (`undefined`) = o tipo do gráfico não tem esse campo.
2130
+ */
2131
+ function ColumnOptions({ kind, name, model, label, onLabel, format, onFormat, formatter, formatters, onFormatter, onReset }) {
2132
+ const [anchor, setAnchor] = useState(null);
2133
+ const fromModel = kind === "measure" ? model.measures[name] : model.dimensions[name];
2134
+ const measureMeta = kind === "measure" ? model.measures[name] : void 0;
2135
+ const customized = !!label || !!formatter || format !== void 0 && Object.keys(format).length > 0;
2136
+ const setFormat = (key, next) => onFormat(withField(format ?? {}, key, next));
2137
+ const decimals = format?.decimals;
2138
+ const resolved = {
2139
+ ...measureMeta?.format !== void 0 && { format: measureMeta.format },
2140
+ ...measureMeta?.decimals !== void 0 && { decimals: measureMeta.decimals },
2141
+ ...format
2142
+ };
2143
+ const small = {
2144
+ ...inputSx$1,
2145
+ "& .MuiInputBase-input": {
2146
+ fontFamily: MONO,
2147
+ fontSize: 12,
2148
+ px: 1
2149
+ }
2150
+ };
2151
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Tooltip, {
2152
+ title: customized ? "Opções da coluna (personalizada)" : "Opções da coluna",
2153
+ children: /* @__PURE__ */ jsx(IconButton, {
2154
+ size: "small",
2155
+ "aria-label": `Opções de ${name}`,
2156
+ onClick: (e) => setAnchor(e.currentTarget),
2157
+ sx: {
2158
+ p: .25,
2159
+ fontSize: 14,
2160
+ color: customized ? "primary.main" : "text.secondary"
2161
+ },
2162
+ children: "⚙"
2163
+ })
2164
+ }), /* @__PURE__ */ jsxs(Popover, {
2165
+ open: !!anchor,
2166
+ anchorEl: anchor,
2167
+ onClose: () => setAnchor(null),
2168
+ anchorOrigin: {
2169
+ vertical: "bottom",
2170
+ horizontal: "right"
2171
+ },
2172
+ transformOrigin: {
2173
+ vertical: "top",
2174
+ horizontal: "right"
2175
+ },
2176
+ slotProps: { paper: { sx: {
2177
+ p: 1.5,
2178
+ width: 320
2179
+ } } },
2180
+ children: [/* @__PURE__ */ jsxs(Typography, {
2181
+ sx: {
2182
+ fontSize: 12,
2183
+ fontWeight: 700,
2184
+ mb: 1
2185
+ },
2186
+ children: [
2187
+ /* @__PURE__ */ jsx(Box, {
2188
+ component: "span",
2189
+ sx: {
2190
+ color: KIND_STYLE[kind].color,
2191
+ mr: .75
2192
+ },
2193
+ children: KIND_STYLE[kind].icon
2194
+ }),
2195
+ /* @__PURE__ */ jsx(Box, {
2196
+ component: "span",
2197
+ sx: { fontFamily: MONO },
2198
+ children: name
2199
+ }),
2200
+ /* @__PURE__ */ jsx(Box, {
2201
+ component: "span",
2202
+ sx: {
2203
+ color: "text.secondary",
2204
+ fontWeight: 400,
2205
+ ml: 1
2206
+ },
2207
+ children: KIND_STYLE[kind].noun
2208
+ })
2209
+ ]
2210
+ }), /* @__PURE__ */ jsxs(Box, {
2211
+ sx: {
2212
+ display: "flex",
2213
+ flexDirection: "column",
2214
+ gap: 1.25
2215
+ },
2216
+ children: [
2217
+ label !== void 0 && field("Rótulo", /* @__PURE__ */ jsx(TextField, {
2218
+ size: "small",
2219
+ fullWidth: true,
2220
+ autoFocus: true,
2221
+ value: label,
2222
+ placeholder: fromModel?.label ?? name,
2223
+ onChange: (e) => onLabel(e.target.value),
2224
+ sx: inputSx$1
2225
+ }), "Legenda, tooltip e cabeçalho, só neste gráfico; vazio = rótulo do dicionário"),
2226
+ formatter !== void 0 && field("Formatter", /* @__PURE__ */ jsx(NameSelect, {
2227
+ value: formatter,
2228
+ options: formatters.map((option) => ({ name: option })),
2229
+ onChange: onFormatter
2230
+ }), "Formatters passados ao <DashboardCharts formatters={{ ... }}> da página"),
2231
+ format !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
2232
+ /* @__PURE__ */ jsxs(Box, {
2233
+ sx: {
2234
+ display: "grid",
2235
+ gridTemplateColumns: "1fr 1fr",
2236
+ gap: 1
2237
+ },
2238
+ children: [
2239
+ field("Formato", /* @__PURE__ */ jsxs(TextField, {
2240
+ select: true,
2241
+ size: "small",
2242
+ fullWidth: true,
2243
+ value: format.format ?? "",
2244
+ onChange: (e) => setFormat("format", e.target.value || void 0),
2245
+ slotProps: { select: { displayEmpty: true } },
2246
+ sx: small,
2247
+ children: [/* @__PURE__ */ jsx(MenuItem, {
2248
+ value: "",
2249
+ children: /* @__PURE__ */ jsxs("em", { children: ["padrão", measureMeta?.format ? ` (${FORMAT_LABEL[measureMeta.format]})` : ""] })
2250
+ }), VALUE_FORMATS.map((option) => /* @__PURE__ */ jsx(MenuItem, {
2251
+ value: option,
2252
+ children: FORMAT_LABEL[option]
2253
+ }, option))]
2254
+ })),
2255
+ field("Casas decimais", /* @__PURE__ */ jsx(TextField, {
2256
+ size: "small",
2257
+ fullWidth: true,
2258
+ type: "number",
2259
+ value: decimals ?? "",
2260
+ placeholder: measureMeta?.decimals === void 0 ? "padrão" : String(measureMeta.decimals),
2261
+ error: decimals !== void 0 && (!Number.isInteger(decimals) || decimals < 0 || decimals > 10),
2262
+ onChange: (e) => setFormat("decimals", e.target.value.trim() === "" ? void 0 : Number(e.target.value)),
2263
+ slotProps: { htmlInput: {
2264
+ min: 0,
2265
+ max: 10
2266
+ } },
2267
+ sx: small
2268
+ })),
2269
+ field("Prefixo", /* @__PURE__ */ jsx(TextField, {
2270
+ size: "small",
2271
+ fullWidth: true,
2272
+ value: format.prefix ?? "",
2273
+ placeholder: "ex.: ~",
2274
+ onChange: (e) => setFormat("prefix", e.target.value),
2275
+ sx: small
2276
+ })),
2277
+ field("Sufixo", /* @__PURE__ */ jsx(TextField, {
2278
+ size: "small",
2279
+ fullWidth: true,
2280
+ value: format.suffix ?? "",
2281
+ placeholder: "ex.: t",
2282
+ onChange: (e) => setFormat("suffix", e.target.value),
2283
+ sx: small
2284
+ }))
2285
+ ]
2286
+ }),
2287
+ /* @__PURE__ */ jsx(FormControlLabel, {
2288
+ sx: { m: 0 },
2289
+ control: /* @__PURE__ */ jsx(Checkbox, {
2290
+ size: "small",
2291
+ sx: {
2292
+ p: .25,
2293
+ mr: .5
2294
+ },
2295
+ checked: format.compact ?? false,
2296
+ onChange: (e) => setFormat("compact", e.target.checked || void 0)
2297
+ }),
2298
+ label: /* @__PURE__ */ jsx(Typography, {
2299
+ sx: { fontSize: 12 },
2300
+ children: "Notação curta (1,2 mil / 3,4 mi)"
2301
+ })
2302
+ }),
2303
+ /* @__PURE__ */ jsxs(Typography, {
2304
+ sx: {
2305
+ fontSize: 11,
2306
+ color: "text.secondary"
2307
+ },
2308
+ children: [
2309
+ "Exemplo:",
2310
+ " ",
2311
+ /* @__PURE__ */ jsx(Box, {
2312
+ component: "span",
2313
+ sx: {
2314
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
2315
+ color: "text.primary"
2316
+ },
2317
+ children: formatMeasure(SAMPLE, resolved, 2)
2318
+ })
2319
+ ]
2320
+ })
2321
+ ] }),
2322
+ /* @__PURE__ */ jsx(Box, {
2323
+ sx: {
2324
+ display: "flex",
2325
+ justifyContent: "flex-end"
2326
+ },
2327
+ children: /* @__PURE__ */ jsx(Button, {
2328
+ size: "small",
2329
+ disabled: !customized,
2330
+ onClick: onReset,
2331
+ children: "Restaurar padrão"
2332
+ })
2333
+ })
2334
+ ]
2335
+ })]
2336
+ })] });
2337
+ }
1895
2338
  const formGrid = (columns) => ({
1896
2339
  display: "grid",
1897
2340
  gap: 1.5,
1898
2341
  gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`
1899
2342
  });
1900
- /** Rótulo padrão (do dicionário) como placeholder dos campos `*Label` do combo. */
2343
+ /** Rótulo padrão (do dicionário) como placeholder do `label` do filtro. */
1901
2344
  function labelPlaceholder(key, values, model) {
1902
2345
  if (key === "label" && typeof values.dimension === "string") return model.dimensions[values.dimension]?.label ?? values.dimension;
1903
- const measureKey = key.endsWith("Label") ? key.slice(0, -5) : null;
1904
- const measure = measureKey && values[measureKey];
1905
- return typeof measure === "string" ? model.measures[measure]?.label ?? measure : void 0;
1906
2346
  }
1907
2347
  function field(label, input, hint) {
1908
2348
  return /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Typography, {
@@ -1967,52 +2407,205 @@ function NameSelect({ value, options, required, emptyLabel, onChange }) {
1967
2407
  ]
1968
2408
  });
1969
2409
  }
1970
- /** Lista ordenada de nomes: a ordem de escolha é a ordem das séries/colunas. */
1971
- function MultiNameSelect({ value, options, required, onChange }) {
1972
- const unknown = value.filter((name) => !options.some((option) => option.name === name));
1973
- return /* @__PURE__ */ jsxs(TextField, {
1974
- select: true,
1975
- size: "small",
1976
- fullWidth: true,
1977
- value,
1978
- error: !!unknown.length || required && !value.length,
1979
- onChange: (e) => {
1980
- const next = e.target.value;
1981
- onChange(next);
2410
+ const KIND_STYLE = {
2411
+ dimension: {
2412
+ icon: "◇",
2413
+ color: "#4dabf7",
2414
+ noun: "dimensão",
2415
+ group: "Dimensões"
2416
+ },
2417
+ measure: {
2418
+ icon: "Σ",
2419
+ color: "#F58220",
2420
+ noun: "medida",
2421
+ group: "Medidas"
2422
+ }
2423
+ };
2424
+ /**
2425
+ * Lista ordenada, uma linha por item: a ordem é a das séries/colunas. Reordena arrastando pela alça
2426
+ * (⋮⋮) ou com ↑/↓; um único "+ adicionar" no fim (já com o nome escolhido, sem linha vazia). Com
2427
+ * dois tipos (dimensões e medidas juntas), o adicionar agrupa as opções por tipo e a ordem mistura
2428
+ * os dois.
2429
+ */
2430
+ function ColumnList({ items, options, extra, error, onChange }) {
2431
+ const [drag, setDrag] = useState(null);
2432
+ const [grabbed, setGrabbed] = useState(null);
2433
+ const kinds = Object.keys(options);
2434
+ const mixed = kinds.length > 1;
2435
+ const used = (kind, name) => items.some((item) => item.kind === kind && item.name === name);
2436
+ const free = (kind, keep) => (options[kind] ?? []).filter((option) => option.name === keep || !used(kind, option.name));
2437
+ const replace = (index, name) => onChange(items.map((item, i) => i === index ? {
2438
+ ...item,
2439
+ name
2440
+ } : item));
2441
+ const marker = {
2442
+ content: "\"\"",
2443
+ position: "absolute",
2444
+ left: 0,
2445
+ right: 0,
2446
+ height: 2,
2447
+ bgcolor: "primary.main"
2448
+ };
2449
+ const addable = kinds.filter((kind) => free(kind).length);
2450
+ const addOptions = addable.flatMap((kind) => [...mixed ? [/* @__PURE__ */ jsx(ListSubheader, { children: KIND_STYLE[kind].group }, `h:${kind}`)] : [], ...free(kind).map((option) => /* @__PURE__ */ jsxs(MenuItem, {
2451
+ value: `${kind}:${option.name}`,
2452
+ children: [mixed && /* @__PURE__ */ jsx(Box, {
2453
+ component: "span",
2454
+ sx: {
2455
+ color: KIND_STYLE[kind].color,
2456
+ mr: .75
2457
+ },
2458
+ children: KIND_STYLE[kind].icon
2459
+ }), /* @__PURE__ */ jsx(OptionText, { option })]
2460
+ }, `${kind}:${option.name}`))]);
2461
+ const addNoun = mixed ? kinds.map((kind) => KIND_STYLE[kind].noun).join(" ou ") : KIND_STYLE[kinds[0] ?? "measure"].noun;
2462
+ return /* @__PURE__ */ jsxs(Box, {
2463
+ sx: {
2464
+ display: "flex",
2465
+ flexDirection: "column",
2466
+ gap: .5,
2467
+ maxWidth: 760
1982
2468
  },
1983
- slotProps: { select: {
1984
- multiple: true,
1985
- renderValue: (selected) => selected.join(", ") || "(nenhum)",
1986
- displayEmpty: true
1987
- } },
1988
- sx: inputSx$1,
1989
- children: [unknown.map((name) => /* @__PURE__ */ jsxs(MenuItem, {
1990
- value: name,
1991
- children: [name, " (não existe)"]
1992
- }, name)), options.map((option) => /* @__PURE__ */ jsxs(MenuItem, {
1993
- value: option.name,
1994
- dense: true,
2469
+ children: [items.map((item, index) => /* @__PURE__ */ jsxs(Box, {
2470
+ draggable: grabbed === index,
2471
+ onDragStart: (e) => {
2472
+ e.dataTransfer.effectAllowed = "move";
2473
+ e.dataTransfer.setData("text/plain", item.name);
2474
+ setDrag({
2475
+ from: index,
2476
+ to: index
2477
+ });
2478
+ },
2479
+ onDragOver: (e) => {
2480
+ if (!drag) return;
2481
+ e.preventDefault();
2482
+ const rect = e.currentTarget.getBoundingClientRect();
2483
+ const to = e.clientY < rect.top + rect.height / 2 ? index : index + 1;
2484
+ if (to !== drag.to) setDrag({
2485
+ ...drag,
2486
+ to
2487
+ });
2488
+ },
2489
+ onDrop: (e) => {
2490
+ e.preventDefault();
2491
+ if (drag && drag.to !== drag.from && drag.to !== drag.from + 1) onChange(moveTo(items, drag.from, drag.to));
2492
+ setDrag(null);
2493
+ },
2494
+ onDragEnd: () => {
2495
+ setDrag(null);
2496
+ setGrabbed(null);
2497
+ },
2498
+ sx: {
2499
+ position: "relative",
2500
+ display: "flex",
2501
+ gap: .25,
2502
+ alignItems: "center",
2503
+ opacity: drag?.from === index ? .4 : 1,
2504
+ "&:hover .row-actions": { visibility: "visible" },
2505
+ "&::before": drag && drag.from !== index && drag.to === index ? {
2506
+ ...marker,
2507
+ top: -3
2508
+ } : void 0,
2509
+ "&::after": drag && index === items.length - 1 && drag.to === items.length ? {
2510
+ ...marker,
2511
+ bottom: -3
2512
+ } : void 0
2513
+ },
1995
2514
  children: [
1996
- /* @__PURE__ */ jsx(Checkbox, {
1997
- size: "small",
1998
- checked: value.includes(option.name),
2515
+ /* @__PURE__ */ jsx(Box, {
2516
+ component: "span",
2517
+ title: "Arraste para reordenar",
2518
+ onMouseDown: () => setGrabbed(index),
2519
+ onMouseUp: () => setGrabbed(null),
1999
2520
  sx: {
2000
- p: .25,
2001
- mr: 1
2002
- }
2521
+ color: "text.disabled",
2522
+ fontSize: 12,
2523
+ cursor: "grab",
2524
+ userSelect: "none",
2525
+ px: .25
2526
+ },
2527
+ children: "⋮⋮"
2003
2528
  }),
2004
- value.includes(option.name) && /* @__PURE__ */ jsx(Box, {
2005
- component: "span",
2529
+ /* @__PURE__ */ jsx(Typography, {
2006
2530
  sx: {
2531
+ fontFamily: MONO,
2532
+ fontSize: 11,
2007
2533
  color: "primary.main",
2008
- mr: .75,
2009
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
2534
+ minWidth: 14
2535
+ },
2536
+ children: index + 1
2537
+ }),
2538
+ mixed && /* @__PURE__ */ jsx(Tooltip, {
2539
+ title: KIND_STYLE[item.kind].noun,
2540
+ children: /* @__PURE__ */ jsx(Box, {
2541
+ component: "span",
2542
+ sx: {
2543
+ color: KIND_STYLE[item.kind].color,
2544
+ fontSize: 12,
2545
+ minWidth: 12
2546
+ },
2547
+ children: KIND_STYLE[item.kind].icon
2548
+ })
2549
+ }),
2550
+ /* @__PURE__ */ jsx(NameSelect, {
2551
+ value: item.name,
2552
+ options: free(item.kind, item.name),
2553
+ required: true,
2554
+ onChange: (next) => replace(index, next)
2555
+ }),
2556
+ /* @__PURE__ */ jsxs(Box, {
2557
+ className: "row-actions",
2558
+ sx: {
2559
+ display: "flex",
2560
+ visibility: "hidden"
2010
2561
  },
2011
- children: value.indexOf(option.name) + 1
2562
+ children: [
2563
+ /* @__PURE__ */ jsx(SmallButton, {
2564
+ title: "Subir",
2565
+ disabled: index === 0,
2566
+ onClick: () => onChange(moveTo(items, index, index - 1)),
2567
+ children: "↑"
2568
+ }),
2569
+ /* @__PURE__ */ jsx(SmallButton, {
2570
+ title: "Descer",
2571
+ disabled: index === items.length - 1,
2572
+ onClick: () => onChange(moveTo(items, index, index + 2)),
2573
+ children: "↓"
2574
+ }),
2575
+ /* @__PURE__ */ jsx(SmallButton, {
2576
+ title: "Remover",
2577
+ onClick: () => onChange(items.filter((_, i) => i !== index)),
2578
+ children: "×"
2579
+ })
2580
+ ]
2012
2581
  }),
2013
- /* @__PURE__ */ jsx(OptionText, { option })
2582
+ extra?.(item)
2014
2583
  ]
2015
- }, option.name))]
2584
+ }, `${item.kind}:${item.name}`)), addable.length > 0 && /* @__PURE__ */ jsx(TextField, {
2585
+ select: true,
2586
+ size: "small",
2587
+ fullWidth: true,
2588
+ value: "",
2589
+ error,
2590
+ onChange: (e) => {
2591
+ const [kind, ...rest] = e.target.value.split(":");
2592
+ const name = rest.join(":");
2593
+ if (name) onChange([...items, {
2594
+ kind,
2595
+ name
2596
+ }]);
2597
+ },
2598
+ slotProps: { select: {
2599
+ displayEmpty: true,
2600
+ renderValue: () => /* @__PURE__ */ jsxs(Box, {
2601
+ component: "span",
2602
+ sx: { color: "text.secondary" },
2603
+ children: ["+ adicionar ", addNoun]
2604
+ })
2605
+ } },
2606
+ sx: inputSx$1,
2607
+ children: addOptions
2608
+ })]
2016
2609
  });
2017
2610
  }
2018
2611
  //#endregion
@@ -2034,10 +2627,7 @@ function toEditor(model) {
2034
2627
  const uidOf = (name) => tables.find((table) => table.name === name)?.uid ?? "";
2035
2628
  const positions = Object.fromEntries(Object.entries(model.diagram ?? {}).flatMap(([name, point]) => {
2036
2629
  const uid = uidOf(name);
2037
- return uid ? [[uid, {
2038
- x: point.x,
2039
- y: point.y
2040
- }]] : [];
2630
+ return uid ? [[uid, { ...point }]] : [];
2041
2631
  }));
2042
2632
  return {
2043
2633
  name: model.name,
@@ -2132,14 +2722,18 @@ function fromEditor(editor) {
2132
2722
  ...Object.keys(diagram).length && { diagram }
2133
2723
  };
2134
2724
  }
2135
- /** Posições pelo nome, na ordem dos cards; arredonda (o arquivo não precisa de frações). */
2725
+ /** Posições (e tamanhos) pelo nome, na ordem dos cards; arredonda (o arquivo não precisa de frações). */
2136
2726
  function diagramOf(editor) {
2137
2727
  return Object.fromEntries(editor.tables.flatMap(({ uid, name }) => {
2138
2728
  const point = editor.positions[uid];
2139
- return point && name ? [[name, {
2140
- x: Math.round(point.x),
2141
- y: Math.round(point.y)
2142
- }]] : [];
2729
+ if (!point || !name) return [];
2730
+ const { x, y, width, height } = point;
2731
+ return [[name, {
2732
+ x: Math.round(x),
2733
+ y: Math.round(y),
2734
+ ...width !== void 0 && { width: Math.round(width) },
2735
+ ...height !== void 0 && { height: Math.round(height) }
2736
+ }]];
2143
2737
  }));
2144
2738
  }
2145
2739
  /** O modelo sem as posições do diagrama: o que muda as consultas. */
@@ -2918,6 +3512,11 @@ const HEADER = 34;
2918
3512
  const ROW = 22;
2919
3513
  const MAX_BODY = 264;
2920
3514
  const PADDING = 4;
3515
+ /** Limites do redimensionamento do card. */
3516
+ const MIN_WIDTH = 160;
3517
+ const MAX_WIDTH = 800;
3518
+ const MIN_HEIGHT$1 = 64;
3519
+ const MAX_HEIGHT = 1600;
2921
3520
  const ROLE_STYLE = {
2922
3521
  one: {
2923
3522
  color: "#4dabf7",
@@ -3034,25 +3633,27 @@ function buildNodes(editor, columns, errors) {
3034
3633
  };
3035
3634
  });
3036
3635
  }
3037
- const cardHeight = (node, collapsed) => collapsed ? HEADER : HEADER + Math.min(node.items.length * ROW, MAX_BODY) + 8;
3636
+ /** Altura do conteúdo (sem tamanho salvo): até `MAX_BODY` de linhas, o resto rola. */
3637
+ const contentHeight = (node) => HEADER + Math.min(node.items.length * ROW, MAX_BODY) + 8;
3638
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
3038
3639
  /** Quem só filtra (lado 1) em cima, o resto embaixo (como costuma ficar no Power BI). */
3039
- function autoLayout(nodes, heights) {
3640
+ function autoLayout(nodes, sizes) {
3040
3641
  const top = nodes.filter((node) => node.role === "one");
3041
3642
  const bottom = nodes.filter((node) => node.role !== "one");
3042
3643
  const positions = {};
3043
- top.forEach((node, index) => {
3044
- positions[node.id] = {
3045
- x: 40 + index * 290,
3046
- y: 40
3047
- };
3048
- });
3049
- const topHeight = Math.max(0, ...top.map((node) => heights.get(node.id) ?? HEADER));
3050
- bottom.forEach((node, index) => {
3051
- positions[node.id] = {
3052
- x: 40 + index * 290,
3053
- y: 40 + topHeight + 140
3054
- };
3055
- });
3644
+ const gap = 60;
3645
+ const row = (list, y) => {
3646
+ let x = 40;
3647
+ for (const node of list) {
3648
+ positions[node.id] = {
3649
+ x,
3650
+ y
3651
+ };
3652
+ x += (sizes.get(node.id)?.width ?? CARD_WIDTH) + gap;
3653
+ }
3654
+ };
3655
+ row(top, 40);
3656
+ row(bottom, 40 + Math.max(0, ...top.map((node) => sizes.get(node.id)?.height ?? HEADER)) + 140);
3056
3657
  return positions;
3057
3658
  }
3058
3659
  function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingColumns, onLoadColumns, onTry, usage }) {
@@ -3062,6 +3663,7 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3062
3663
  errors
3063
3664
  ]);
3064
3665
  const [moving, setMoving] = useState(null);
3666
+ const [resizing, setResizing] = useState(null);
3065
3667
  const [collapsed, setCollapsed] = useState({});
3066
3668
  const [scrolls, setScrolls] = useState({});
3067
3669
  const [view, setView] = useState({
@@ -3099,9 +3701,39 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3099
3701
  requested.current = true;
3100
3702
  onLoadColumns();
3101
3703
  }, [columns, onLoadColumns]);
3102
- const heights = useMemo(() => new Map(nodes.map((node) => [node.id, cardHeight(node, !!collapsed[node.id])])), [nodes, collapsed]);
3103
- const fallback = useMemo(() => autoLayout(nodes, heights), [nodes, heights]);
3104
- const placed = (node) => (moving?.id === node.id ? moving.point : editor.positions[node.id]) ?? fallback[node.id];
3704
+ const sizes = useMemo(() => new Map(nodes.map((node) => {
3705
+ const saved = resizing?.id === node.id ? resizing : editor.positions[node.id];
3706
+ const width = saved?.width ?? CARD_WIDTH;
3707
+ if (collapsed[node.id]) return [node.id, {
3708
+ width,
3709
+ height: HEADER
3710
+ }];
3711
+ return [node.id, {
3712
+ width,
3713
+ height: saved?.height ?? contentHeight(node)
3714
+ }];
3715
+ })), [
3716
+ nodes,
3717
+ collapsed,
3718
+ editor.positions,
3719
+ resizing
3720
+ ]);
3721
+ const sizeOf = (node) => sizes.get(node.id) ?? {
3722
+ width: CARD_WIDTH,
3723
+ height: HEADER
3724
+ };
3725
+ const fallback = useMemo(() => autoLayout(nodes, sizes), [nodes, sizes]);
3726
+ const placed = (node) => {
3727
+ if (moving?.id === node.id) return moving.point;
3728
+ const saved = editor.positions[node.id];
3729
+ return saved ? {
3730
+ x: saved.x,
3731
+ y: saved.y
3732
+ } : fallback[node.id] ?? {
3733
+ x: 0,
3734
+ y: 0
3735
+ };
3736
+ };
3105
3737
  const setPositions = (update) => setEditor((current) => ({
3106
3738
  ...current,
3107
3739
  positions: update(current.positions)
@@ -3110,7 +3742,7 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3110
3742
  /** Ponto da linha na coluna do card (lado esquerdo ou direito). */
3111
3743
  const anchor = (node, column, side) => {
3112
3744
  const { x, y } = placed(node);
3113
- const height = heights.get(node.id) ?? HEADER;
3745
+ const { width, height } = sizeOf(node);
3114
3746
  const index = node.items.findIndex((item) => item.kind === "column" && item.name === column);
3115
3747
  let rowY = y + HEADER / 2;
3116
3748
  if (!collapsed[node.id] && index >= 0) {
@@ -3118,7 +3750,7 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3118
3750
  rowY = Math.min(Math.max(raw, y + HEADER + 2), y + height - 2);
3119
3751
  }
3120
3752
  return {
3121
- x: side === "left" ? x : x + CARD_WIDTH,
3753
+ x: side === "left" ? x : x + width,
3122
3754
  y: rowY
3123
3755
  };
3124
3756
  };
@@ -3298,11 +3930,56 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3298
3930
  setMoving(null);
3299
3931
  if (moved) setPositions((current) => ({
3300
3932
  ...current,
3301
- [node.id]: point
3933
+ [node.id]: {
3934
+ ...current[node.id],
3935
+ ...point
3936
+ }
3302
3937
  }));
3303
3938
  else selectNode(node);
3304
3939
  });
3305
3940
  };
3941
+ /** Arrasta a borda direita (`width`), a de baixo (`height`) ou o canto (as duas). */
3942
+ const startResize = (event, node, edges) => {
3943
+ if (event.button !== 0) return;
3944
+ const start = sizeOf(node);
3945
+ const { zoom } = view;
3946
+ let size = start;
3947
+ track(event, (dx, dy) => {
3948
+ size = {
3949
+ width: edges.width ? clamp(start.width + dx / zoom, MIN_WIDTH, MAX_WIDTH) : start.width,
3950
+ height: edges.height ? clamp(start.height + dy / zoom, MIN_HEIGHT$1, MAX_HEIGHT) : start.height
3951
+ };
3952
+ setResizing({
3953
+ id: node.id,
3954
+ ...size
3955
+ });
3956
+ }, (_, moved) => {
3957
+ setResizing(null);
3958
+ if (!moved) return;
3959
+ const point = placed(node);
3960
+ setPositions((current) => ({
3961
+ ...current,
3962
+ [node.id]: {
3963
+ ...point,
3964
+ ...current[node.id],
3965
+ ...edges.width && { width: size.width },
3966
+ ...edges.height && { height: size.height }
3967
+ }
3968
+ }));
3969
+ });
3970
+ };
3971
+ /** Duplo clique na alça: volta ao tamanho automático (largura padrão, altura do conteúdo). */
3972
+ const resetSize = (node) => setPositions((current) => {
3973
+ const saved = current[node.id];
3974
+ if (!saved) return current;
3975
+ return {
3976
+ ...current,
3977
+ [node.id]: {
3978
+ x: saved.x,
3979
+ y: saved.y
3980
+ }
3981
+ };
3982
+ });
3306
3983
  const startLink = (event, node, column) => {
3307
3984
  if (event.button !== 0) return;
3308
3985
  track(event, (_, __, moveEvent) => setLinking({
@@ -3355,11 +4032,12 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3355
4032
  if (!rect || !nodes.length) return;
3356
4033
  const boxes = nodes.map((node) => ({
3357
4034
  ...layout?.[node.id] ?? placed(node),
3358
- h: heights.get(node.id) ?? HEADER
4035
+ w: sizeOf(node).width,
4036
+ h: sizeOf(node).height
3359
4037
  }));
3360
4038
  const minX = Math.min(...boxes.map((box) => box.x));
3361
4039
  const minY = Math.min(...boxes.map((box) => box.y));
3362
- const maxX = Math.max(...boxes.map((box) => box.x + CARD_WIDTH));
4040
+ const maxX = Math.max(...boxes.map((box) => box.x + box.w));
3363
4041
  const maxY = Math.max(...boxes.map((box) => box.y + box.h));
3364
4042
  const zoom = Math.min(1.2, Math.max(.3, Math.min((rect.width - 60) / (maxX - minX), (rect.height - 60) / (maxY - minY))));
3365
4043
  setView({
@@ -3375,7 +4053,7 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3375
4053
  const isSelected = (node) => selection?.kind === "table" && selection.uid === node.uid;
3376
4054
  const lines = [];
3377
4055
  const pushLine = (id, many, manyColumn, one, oneColumn, extra) => {
3378
- const rightward = placed(many).x + CARD_WIDTH / 2 <= placed(one).x + CARD_WIDTH / 2;
4056
+ const rightward = placed(many).x + sizeOf(many).width / 2 <= placed(one).x + sizeOf(one).width / 2;
3379
4057
  lines.push({
3380
4058
  id,
3381
4059
  from: anchor(many, manyColumn, rightward ? "right" : "left"),
@@ -3462,7 +4140,7 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3462
4140
  }), nodes.map((node) => /* @__PURE__ */ jsx(TableCard, {
3463
4141
  node,
3464
4142
  position: placed(node),
3465
- height: heights.get(node.id) ?? HEADER,
4143
+ size: sizeOf(node),
3466
4144
  collapsed: !!collapsed[node.id],
3467
4145
  selected: !!isSelected(node),
3468
4146
  linking: !!linking && linking.node.id !== node.id,
@@ -3476,6 +4154,8 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3476
4154
  [node.id]: top
3477
4155
  })),
3478
4156
  onHeaderDown: (event) => startMove(event, node),
4157
+ onResizeDown: (event, edges) => startResize(event, node, edges),
4158
+ onResizeReset: () => resetSize(node),
3479
4159
  onColumnDown: (event, column) => startLink(event, node, column),
3480
4160
  onFieldClick: (item) => setSelection({
3481
4161
  kind: item.kind,
@@ -3532,8 +4212,11 @@ function ModelDiagram({ editor, setEditor, columns, errors, warnings, loadingCol
3532
4212
  /* @__PURE__ */ jsx(Button, {
3533
4213
  size: "small",
3534
4214
  onClick: () => {
3535
- const layout = autoLayout(nodes, heights);
3536
- setPositions(() => layout);
4215
+ const layout = autoLayout(nodes, sizes);
4216
+ setPositions((current) => Object.fromEntries(Object.entries(layout).map(([id, point]) => [id, {
4217
+ ...current[id],
4218
+ ...point
4219
+ }])));
3537
4220
  fitToScreen(layout);
3538
4221
  },
3539
4222
  title: "Dimensões em cima, fatos embaixo (troca as posições salvas)",
@@ -3842,7 +4525,7 @@ function RelationshipLine({ line, hovered, onHover }) {
3842
4525
  ]
3843
4526
  });
3844
4527
  }
3845
- function TableCard({ node, position, height, collapsed, selected, linking, selectedField, onToggle, onScroll, onHeaderDown, onColumnDown, onFieldClick, onAddField }) {
4528
+ function TableCard({ node, position, size, collapsed, selected, linking, selectedField, onToggle, onScroll, onHeaderDown, onResizeDown, onResizeReset, onColumnDown, onFieldClick, onAddField }) {
3846
4529
  const style = ROLE_STYLE[node.role];
3847
4530
  const columnActions = ["dimension", "measure"];
3848
4531
  return /* @__PURE__ */ jsxs(Box, {
@@ -3851,8 +4534,8 @@ function TableCard({ node, position, height, collapsed, selected, linking, selec
3851
4534
  position: "absolute",
3852
4535
  left: position.x,
3853
4536
  top: position.y,
3854
- width: CARD_WIDTH,
3855
- height,
4537
+ width: size.width,
4538
+ height: size.height,
3856
4539
  bgcolor: "#23272e",
3857
4540
  border: "1px solid",
3858
4541
  borderColor: node.error ? "error.main" : selected ? style.color : "#3a3f47",
@@ -3864,187 +4547,252 @@ function TableCard({ node, position, height, collapsed, selected, linking, selec
3864
4547
  userSelect: "none",
3865
4548
  cursor: "default"
3866
4549
  },
3867
- children: [/* @__PURE__ */ jsxs(Box, {
3868
- onMouseDown: onHeaderDown,
3869
- title: node.error,
3870
- sx: {
3871
- height: 31,
3872
- display: "flex",
3873
- alignItems: "center",
3874
- gap: .75,
3875
- px: 1,
3876
- cursor: "move",
3877
- borderBottom: collapsed ? 0 : "1px solid #3a3f47",
3878
- flexShrink: 0
3879
- },
3880
- children: [
3881
- /* @__PURE__ */ jsx(Box, {
3882
- component: "span",
3883
- sx: {
3884
- color: style.color,
3885
- fontSize: 13
3886
- },
3887
- children: "▦"
3888
- }),
3889
- /* @__PURE__ */ jsx(Typography, {
3890
- sx: {
3891
- fontWeight: 700,
3892
- fontSize: 13,
3893
- overflow: "hidden",
3894
- textOverflow: "ellipsis",
3895
- whiteSpace: "nowrap",
3896
- flex: 1
3897
- },
3898
- children: node.name || /* @__PURE__ */ jsx("em", { children: "sem nome" })
3899
- }),
3900
- /* @__PURE__ */ jsx(Box, {
3901
- component: "span",
3902
- title: style.title,
3903
- sx: {
3904
- fontSize: 10,
3905
- color: style.color
3906
- },
3907
- children: style.label
3908
- }),
3909
- node.error && /* @__PURE__ */ jsx(Box, {
3910
- component: "span",
3911
- sx: {
3912
- color: "error.main",
3913
- fontSize: 12
3914
- },
3915
- children: "⚠"
3916
- }),
3917
- /* @__PURE__ */ jsx(Box, {
3918
- component: "button",
3919
- type: "button",
3920
- "aria-label": collapsed ? "Expandir" : "Recolher",
3921
- onMouseDown: (event) => event.stopPropagation(),
3922
- onClick: onToggle,
3923
- sx: {
3924
- border: 0,
3925
- bgcolor: "transparent",
3926
- color: "text.secondary",
3927
- cursor: "pointer",
3928
- p: 0,
3929
- fontSize: 11
3930
- },
3931
- children: collapsed ? "▾" : "▴"
3932
- })
3933
- ]
3934
- }), !collapsed && /* @__PURE__ */ jsxs(Box, {
3935
- onScroll: (event) => onScroll(event.currentTarget.scrollTop),
3936
- sx: {
3937
- overflow: "auto",
3938
- flex: 1,
3939
- py: `${PADDING}px`
3940
- },
3941
- children: [!node.items.length && /* @__PURE__ */ jsx(Typography, {
3942
- sx: {
3943
- fontSize: 11,
3944
- color: "text.disabled",
3945
- px: 1,
3946
- lineHeight: `${ROW}px`
3947
- },
3948
- children: "sem colunas lidas"
3949
- }), node.items.map((item) => item.kind === "column" ? /* @__PURE__ */ jsxs(Box, {
3950
- "data-diagram-node": node.id,
3951
- "data-diagram-column": item.name,
3952
- onMouseDown: (event) => onColumnDown(event, item.name),
3953
- title: item.missing ? "Citada no modelo, mas a tabela não devolve essa coluna" : "Arraste até a coluna de outra tabela para relacionar",
4550
+ children: [
4551
+ /* @__PURE__ */ jsxs(Box, {
4552
+ onMouseDown: onHeaderDown,
4553
+ title: node.error,
3954
4554
  sx: {
3955
- height: ROW,
4555
+ height: 31,
3956
4556
  display: "flex",
3957
4557
  alignItems: "center",
3958
4558
  gap: .75,
3959
4559
  px: 1,
3960
- fontSize: 12,
3961
- cursor: "crosshair",
3962
- "& .column-actions": { display: "none" },
3963
- "&:hover .column-actions": { display: linking ? "none" : "flex" },
3964
- color: item.missing ? "error.main" : item.linked ? "text.primary" : "#adb5bd",
3965
- fontWeight: item.linked ? 700 : 400,
3966
- "&:hover": { bgcolor: linking ? "rgba(245,130,32,.25)" : "rgba(255,255,255,.06)" }
4560
+ cursor: "move",
4561
+ borderBottom: collapsed ? 0 : "1px solid #3a3f47",
4562
+ flexShrink: 0
3967
4563
  },
3968
4564
  children: [
3969
4565
  /* @__PURE__ */ jsx(Box, {
3970
4566
  component: "span",
3971
4567
  sx: {
3972
- width: 14,
3973
- textAlign: "center",
3974
- color: "text.secondary"
4568
+ color: style.color,
4569
+ fontSize: 13
3975
4570
  },
3976
- children: item.isKey ? "🔑" : item.numeric ? "Σ" : ""
4571
+ children: "▦"
3977
4572
  }),
3978
- /* @__PURE__ */ jsx(Box, {
3979
- component: "span",
4573
+ /* @__PURE__ */ jsx(Typography, {
3980
4574
  sx: {
4575
+ fontWeight: 700,
4576
+ fontSize: 13,
3981
4577
  overflow: "hidden",
3982
4578
  textOverflow: "ellipsis",
3983
- whiteSpace: "nowrap"
4579
+ whiteSpace: "nowrap",
4580
+ flex: 1
3984
4581
  },
3985
- children: item.name
4582
+ children: node.name || /* @__PURE__ */ jsx("em", { children: "sem nome" })
3986
4583
  }),
3987
4584
  /* @__PURE__ */ jsx(Box, {
3988
- className: "column-actions",
4585
+ component: "span",
4586
+ title: style.title,
3989
4587
  sx: {
3990
- ml: "auto",
3991
- gap: .25
4588
+ fontSize: 10,
4589
+ color: style.color
3992
4590
  },
3993
- children: columnActions.map((kind) => /* @__PURE__ */ jsx(Box, {
3994
- component: "button",
3995
- type: "button",
3996
- title: kind === "dimension" ? `Nova dimensão com ${node.name}.${item.name}` : `Nova medida: ${item.numeric ? "soma" : "contagem"} de ${node.name}.${item.name}`,
3997
- onMouseDown: (event) => event.stopPropagation(),
3998
- onClick: () => onAddField(kind, item),
3999
- sx: {
4000
- border: "1px solid #3a3f47",
4001
- borderRadius: .5,
4002
- bgcolor: "#1b1e23",
4003
- color: kind === "measure" ? "#ffd8a8" : "#a5d8ff",
4004
- cursor: "pointer",
4005
- fontSize: 10,
4006
- lineHeight: "14px",
4007
- px: .5
4008
- },
4009
- children: kind === "dimension" ? "+◇" : "+Σ"
4010
- }, kind))
4591
+ children: style.label
4592
+ }),
4593
+ node.error && /* @__PURE__ */ jsx(Box, {
4594
+ component: "span",
4595
+ sx: {
4596
+ color: "error.main",
4597
+ fontSize: 12
4598
+ },
4599
+ children: "⚠"
4600
+ }),
4601
+ /* @__PURE__ */ jsx(Box, {
4602
+ component: "button",
4603
+ type: "button",
4604
+ "aria-label": collapsed ? "Expandir" : "Recolher",
4605
+ onMouseDown: (event) => event.stopPropagation(),
4606
+ onClick: onToggle,
4607
+ sx: {
4608
+ border: 0,
4609
+ bgcolor: "transparent",
4610
+ color: "text.secondary",
4611
+ cursor: "pointer",
4612
+ p: 0,
4613
+ fontSize: 11
4614
+ },
4615
+ children: collapsed ? "▾" : "▴"
4011
4616
  })
4012
4617
  ]
4013
- }, `c:${item.name}`) : /* @__PURE__ */ jsxs(Box, {
4014
- onMouseDown: (event) => event.stopPropagation(),
4015
- onClick: () => onFieldClick(item),
4618
+ }),
4619
+ !collapsed && /* @__PURE__ */ jsxs(Box, {
4620
+ onScroll: (event) => onScroll(event.currentTarget.scrollTop),
4016
4621
  sx: {
4017
- height: ROW,
4018
- display: "flex",
4019
- alignItems: "center",
4020
- gap: .75,
4021
- px: 1,
4022
- fontSize: 12,
4023
- cursor: "pointer",
4024
- color: item.kind === "measure" ? "#ffd8a8" : "#a5d8ff",
4025
- bgcolor: selectedField === item.uid ? "rgba(245,130,32,.2)" : void 0,
4026
- "&:hover": { bgcolor: "rgba(255,255,255,.06)" }
4622
+ overflow: "auto",
4623
+ flex: 1,
4624
+ py: `${PADDING}px`
4027
4625
  },
4028
- children: [/* @__PURE__ */ jsx(Box, {
4029
- component: "span",
4626
+ children: [!node.items.length && /* @__PURE__ */ jsx(Typography, {
4030
4627
  sx: {
4031
- width: 14,
4032
- textAlign: "center"
4628
+ fontSize: 11,
4629
+ color: "text.disabled",
4630
+ px: 1,
4631
+ lineHeight: `${ROW}px`
4033
4632
  },
4034
- children: item.icon
4035
- }), /* @__PURE__ */ jsx(Box, {
4036
- component: "span",
4633
+ children: "sem colunas lidas"
4634
+ }), node.items.map((item) => item.kind === "column" ? /* @__PURE__ */ jsxs(Box, {
4635
+ "data-diagram-node": node.id,
4636
+ "data-diagram-column": item.name,
4637
+ onMouseDown: (event) => onColumnDown(event, item.name),
4638
+ title: item.missing ? "Citada no modelo, mas a tabela não devolve essa coluna" : "Arraste até a coluna de outra tabela para relacionar",
4037
4639
  sx: {
4038
- overflow: "hidden",
4039
- textOverflow: "ellipsis",
4040
- whiteSpace: "nowrap"
4640
+ height: ROW,
4641
+ display: "flex",
4642
+ alignItems: "center",
4643
+ gap: .75,
4644
+ px: 1,
4645
+ fontSize: 12,
4646
+ cursor: "crosshair",
4647
+ "& .column-actions": { display: "none" },
4648
+ "&:hover .column-actions": { display: linking ? "none" : "flex" },
4649
+ color: item.missing ? "error.main" : item.linked ? "text.primary" : "#adb5bd",
4650
+ fontWeight: item.linked ? 700 : 400,
4651
+ "&:hover": { bgcolor: linking ? "rgba(245,130,32,.25)" : "rgba(255,255,255,.06)" }
4041
4652
  },
4042
- children: item.name || /* @__PURE__ */ jsx("em", { children: "sem nome" })
4043
- })]
4044
- }, `f:${item.uid}`))]
4045
- })]
4653
+ children: [
4654
+ /* @__PURE__ */ jsx(Box, {
4655
+ component: "span",
4656
+ sx: {
4657
+ width: 14,
4658
+ textAlign: "center",
4659
+ color: "text.secondary"
4660
+ },
4661
+ children: item.isKey ? "🔑" : item.numeric ? "Σ" : ""
4662
+ }),
4663
+ /* @__PURE__ */ jsx(Box, {
4664
+ component: "span",
4665
+ sx: {
4666
+ overflow: "hidden",
4667
+ textOverflow: "ellipsis",
4668
+ whiteSpace: "nowrap"
4669
+ },
4670
+ children: item.name
4671
+ }),
4672
+ /* @__PURE__ */ jsx(Box, {
4673
+ className: "column-actions",
4674
+ sx: {
4675
+ ml: "auto",
4676
+ gap: .25
4677
+ },
4678
+ children: columnActions.map((kind) => /* @__PURE__ */ jsx(Box, {
4679
+ component: "button",
4680
+ type: "button",
4681
+ title: kind === "dimension" ? `Nova dimensão com ${node.name}.${item.name}` : `Nova medida: ${item.numeric ? "soma" : "contagem"} de ${node.name}.${item.name}`,
4682
+ onMouseDown: (event) => event.stopPropagation(),
4683
+ onClick: () => onAddField(kind, item),
4684
+ sx: {
4685
+ border: "1px solid #3a3f47",
4686
+ borderRadius: .5,
4687
+ bgcolor: "#1b1e23",
4688
+ color: kind === "measure" ? "#ffd8a8" : "#a5d8ff",
4689
+ cursor: "pointer",
4690
+ fontSize: 10,
4691
+ lineHeight: "14px",
4692
+ px: .5
4693
+ },
4694
+ children: kind === "dimension" ? "+◇" : "+Σ"
4695
+ }, kind))
4696
+ })
4697
+ ]
4698
+ }, `c:${item.name}`) : /* @__PURE__ */ jsxs(Box, {
4699
+ onMouseDown: (event) => event.stopPropagation(),
4700
+ onClick: () => onFieldClick(item),
4701
+ sx: {
4702
+ height: ROW,
4703
+ display: "flex",
4704
+ alignItems: "center",
4705
+ gap: .75,
4706
+ px: 1,
4707
+ fontSize: 12,
4708
+ cursor: "pointer",
4709
+ color: item.kind === "measure" ? "#ffd8a8" : "#a5d8ff",
4710
+ bgcolor: selectedField === item.uid ? "rgba(245,130,32,.2)" : void 0,
4711
+ "&:hover": { bgcolor: "rgba(255,255,255,.06)" }
4712
+ },
4713
+ children: [/* @__PURE__ */ jsx(Box, {
4714
+ component: "span",
4715
+ sx: {
4716
+ width: 14,
4717
+ textAlign: "center"
4718
+ },
4719
+ children: item.icon
4720
+ }), /* @__PURE__ */ jsx(Box, {
4721
+ component: "span",
4722
+ sx: {
4723
+ overflow: "hidden",
4724
+ textOverflow: "ellipsis",
4725
+ whiteSpace: "nowrap"
4726
+ },
4727
+ children: item.name || /* @__PURE__ */ jsx("em", { children: "sem nome" })
4728
+ })]
4729
+ }, `f:${item.uid}`))]
4730
+ }),
4731
+ /* @__PURE__ */ jsx(Box, {
4732
+ onMouseDown: (event) => onResizeDown(event, {
4733
+ width: true,
4734
+ height: false
4735
+ }),
4736
+ onDoubleClick: onResizeReset,
4737
+ title: "Arraste para mudar a largura (duplo clique: tamanho automático)",
4738
+ sx: {
4739
+ ...resizeHandleSx,
4740
+ top: 0,
4741
+ right: -4,
4742
+ bottom: 8,
4743
+ width: 8,
4744
+ cursor: "ew-resize"
4745
+ }
4746
+ }),
4747
+ !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Box, {
4748
+ onMouseDown: (event) => onResizeDown(event, {
4749
+ width: false,
4750
+ height: true
4751
+ }),
4752
+ onDoubleClick: onResizeReset,
4753
+ title: "Arraste para mudar a altura (duplo clique: tamanho automático)",
4754
+ sx: {
4755
+ ...resizeHandleSx,
4756
+ left: 0,
4757
+ right: 8,
4758
+ bottom: -4,
4759
+ height: 8,
4760
+ cursor: "ns-resize"
4761
+ }
4762
+ }), /* @__PURE__ */ jsx(Box, {
4763
+ onMouseDown: (event) => onResizeDown(event, {
4764
+ width: true,
4765
+ height: true
4766
+ }),
4767
+ onDoubleClick: onResizeReset,
4768
+ title: "Arraste para redimensionar (duplo clique: tamanho automático)",
4769
+ sx: {
4770
+ ...resizeHandleSx,
4771
+ right: -4,
4772
+ bottom: -4,
4773
+ width: 14,
4774
+ height: 14,
4775
+ cursor: "nwse-resize",
4776
+ "&::after": {
4777
+ content: "\"\"",
4778
+ position: "absolute",
4779
+ right: 6,
4780
+ bottom: 6,
4781
+ width: 6,
4782
+ height: 6,
4783
+ borderRight: "2px solid #5c636e",
4784
+ borderBottom: "2px solid #5c636e"
4785
+ },
4786
+ "&:hover::after": { borderColor: ROLE_STYLE[node.role].color }
4787
+ }
4788
+ })] })
4789
+ ]
4046
4790
  });
4047
4791
  }
4792
+ const resizeHandleSx = {
4793
+ position: "absolute",
4794
+ zIndex: 1
4795
+ };
4048
4796
  function PropertyBlock({ title, children }) {
4049
4797
  return /* @__PURE__ */ jsxs(Box, {
4050
4798
  sx: {
@@ -6883,4 +7631,4 @@ function ToggleButton$1({ draft, loading, problems, inspecting, onOpen }) {
6883
7631
  //#endregion
6884
7632
  export { DashboardDevtools as default };
6885
7633
 
6886
- //# sourceMappingURL=DashboardDevtools-C4E9afPT.js.map
7634
+ //# sourceMappingURL=DashboardDevtools-6v5HDe-A.js.map